· 6 years ago · Dec 22, 2019, 08:02 AM
1--[[------------------------------------------------------------------------------
2
3-- Communitas.lua map script --
4
5 Created by:
6- Cephalo (Rich Marinaccio) - Perlin landform, elevation and rainfall creation
7- Sirian (Bob Thomas) - Island creation, some code from Continents and Terra scripts
8- WHoward69 - Mountain-pass finding algorithm
9- Bobert13 - Bug fixes and optimizations
10- Thalassicus (Victor Isbell) - Ocean rifts, rivers through lakes, natural wonder placement,
11 resource placement, map options, inland seas, aesthetic polishing
12
13
14This map script generates climate based on a simplified model of geostrophic
15and monsoon wind patterns. Rivers are generated along accurate drainage paths
16governed by the elevation map used to create the landforms.
17
18- Natural wonders appear in useful locations.
19- Islands reward exploration and settlement.
20- Ocean rifts prevent ancient ships from circling the world.
21- Inland seas, lakes, and rivers flowing out of lakes.
22
23--]]------------------------------------------------------------------------------
24
25include("MapGenerator")
26include("FeatureGenerator")
27include("TerrainGenerator")
28include("IslandMaker")
29include("FLuaVector")
30
31MapGlobals = {}
32
33
34local debugTime = false
35local debugPrint = false
36local debugWithLogger = false
37
38--[[
39Setting "overrideAssignStartingPlots = false" may help make the map compatible
40with core game patches in the distant future when I'm no longer modding Civ 5.
41
42This disables some advanced features of the map, so it's better to
43modify the map's changes to AssignStartingPlots if possible.
44
45~ Thalassicus @ Nov 5 2013
46--]]
47local overrideAssignStartingPlots = true
48
49
50
51
52
53--
54-- Map Information
55--
56
57function MapGlobals:New()
58 print("MapGlobals:New")
59 local mglobal = {}
60 setmetatable(mglobal, self)
61 self.__index = self
62
63 local mapW, mapH = Map.GetGridSize()
64
65
66 --Percent of land tiles on the map.
67 mglobal.landPercent = 0.35
68
69 --Top and bottom map latitudes.
70 mglobal.topLatitude = 90
71 mglobal.bottomLatitude = -90
72
73
74 --Important latitude markers used for generating climate.
75 mglobal.tropicLatitudes = 22 -- tropicLatitudes to 0 : grass, jungle
76 mglobal.horseLatitudes = 23 -- polarFrontLatitude to horseLatitudes : plains, desert
77 mglobal.iceLatitude = 67 -- bottomLatitude to iceLatitude : ice
78 mglobal.polarFrontLatitude = 35 -- bottomLatitude to polarFrontLatitude : snow, tundra
79
80
81 --Adjusting these will generate larger or smaller landmasses and features.
82 mglobal.landMinScatter = 0.02 --Recommended range:[0.02 to 0.1]
83 mglobal.landMaxScatter = 0.06 --Recommended range:[0.03 to 0.3]
84 --Higher values makes continental divisions and stringy features more likely,
85 --and very high values result in a lot of stringy continents and islands.
86
87 mglobal.coastScatter = 0.09 --Recommended range:[0.01 to 0.3]
88 --Higher values result in more islands and variance on landmasses and coastlines.
89
90 mglobal.mountainScatter = 250 * mapW --Recommended range:[130 to 1000]
91 --Lower values make large, long, mountain ranges. Higher values make sporadic mountainous features.
92
93
94 -- Terrain
95 mglobal.mountainWeight = 0.7 --Weight of the mountain elevation map versus the coastline elevation map.
96 mglobal.belowMountainPercent = 0.94 -- Percent of non-mountain land
97 -- flatPercent to belowMountainPercent : hills
98 mglobal.flatPercent = 0.78 -- Percent of flat land
99 mglobal.hillsBlendPercent = 0.45 -- Chance for flat land to become hills per near mountain. Requires at least 2 near mountains.
100 mglobal.terrainBlendRange = 3 -- range to smooth terrain (desert surrounded by plains turns to plains, etc)
101 mglobal.terrainBlendRandom = 0.6 -- random modifier for terrain smoothing
102
103
104 -- Features
105 mglobal.featurePercent = 0.60 -- Percent of potential feature tiles that actually create a feature (marsh/jungle/forest)
106 mglobal.featureWetVariance = 0.15 -- Percent chance increase if freshwater, decrease if dry (groups features near rivers)
107 mglobal.islePercent = 0.06 -- Percent of coast tiles with an isle
108 mglobal.numNaturalWonders = 2 + GameInfo.Worlds[Map.GetWorldSize()].NumNaturalWonders
109
110
111 -- Rain
112 mglobal.marshPercent = 0.10 -- Percent chance increase for marsh from each nearby watery tile
113 -- junglePercent to 1 : marsh
114 mglobal.junglePercent = 0.99 -- junglePercent to 1 : jungle
115 mglobal.zeroTreesPercent = 0.35 -- zeroTreesPercent to 1 : forest
116 -- plainsPercent to 1 : grass
117 mglobal.plainsPercent = 0.50 -- desertPercent to plainsPercent : plains
118 mglobal.desertPercent = 0.67 -- 0 to desertPercent : desert
119
120
121 -- Temperature
122 mglobal.jungleMinTemperature = 0.74 -- jungle: jungleMinTemperature to 1
123 mglobal.desertMinTemperature = 0.61 -- desert: desertMinTemperature to 1
124 -- grass: tundraTemperature to 1
125 -- plains: tundraTemperature to 1
126 mglobal.tundraTemperature = 0.29 -- tundra: snowTemperature to tundraTemperature
127 mglobal.snowTemperature = 0.17 -- snow: 0 to snowTemperature
128 mglobal.treesMinTemperature = 0.33 -- trees: treesMinTemperature to 1
129 mglobal.forestRandomPercent = 0.07 -- Percent of barren flatland which randomly gets a forest
130 mglobal.forestTundraPercent = 0.30 -- Percent of barren tundra which randomly gets a forest
131
132
133
134 -- Water
135 mglobal.riverPercent = 0.25 -- Percent of river junctions that are large enough to become rivers.
136 mglobal.riverRainCheatFactor = 1.25 -- Values greater than one favor watershed size. Values less than one favor actual rain amount.
137 mglobal.minWaterTemp = 0.26 -- Sets water temperature compression that creates the land/sea seasonal temperature differences that cause monsoon winds.
138 mglobal.maxWaterTemp = 0.39
139 mglobal.geostrophicFactor = 3.0 -- Strength of latitude climate versus monsoon climate.
140 mglobal.geostrophicLateralWindStrength = 0.6
141 mglobal.lakeSize = 10 -- read-only; cannot change lake sizes with a map script
142 mglobal.oceanMaxWander = 1 -- number of tiles a rift can randomly wander from its intended path
143 mglobal.oceanElevationWeight = 0.31 -- higher numbers make oceans avoid continents
144 mglobal.oceanRiftWidth = math.max(1, Round(mapW/40)) -- minimum number of ocean tiles in a rift
145
146 -- percent of map width:
147 mglobal.atlanticSize = 0.05 -- size near poles
148 mglobal.atlanticBulge = 0 -- size increase at equator
149 mglobal.atlanticCurve = 0.04 -- S-curve distance
150 mglobal.pacificSize = 0.05 -- size near poles
151 mglobal.pacificBulge = 0.20 -- size increase at equator
152 mglobal.pacificCurve = 0 -- S-curve distance
153
154
155 mglobal.atlanticSize = math.min(4, Round(mglobal.atlanticSize * mapW))
156 mglobal.pacificBulge = Round(mglobal.pacificBulge * mapW)
157 mglobal.atlanticCurve = math.min(5, Round(mglobal.atlanticCurve * mapW))
158 mglobal.pacificCurve = math.min(3, Round(mglobal.pacificCurve * mapW))
159
160
161 -- Resources
162 mglobal.fishTargetFertility = 60 -- fish appear to create this average city fertility
163
164
165 -- Quality vs Performance
166 -- Lowering these reduces map quality and creation time.
167 -- Try reducing these slightly if you experience crashes on huge maps
168 mglobal.tempBlendMaxRange = 10 -- range to smooth temperature map
169 mglobal.elevationBlendRange = 9 -- range to smooth elevation map
170
171
172
173
174
175
176
177
178 --[[
179
180 MAP OPTIONS
181
182 1 - world_age
183 2 - temperature
184 3 - rainfall
185 4 - sea_level
186 5 - resources
187 6 - Players Start
188 7 - Ocean Rifts
189 8 - Ocean Rift width
190
191 --]]
192
193 do
194
195 local oWorldAge = Map.GetCustomOption(1)
196 if oWorldAge == 4 then oWorldAge = 1 + Map.Rand(3, "Communitas random world age - Lua") end
197 if oWorldAge == 1 then
198 print("Map Age: New")
199 mglobal.belowMountainPercent = 1 - (1 - mglobal.belowMountainPercent) * 1.5
200 mglobal.flatPercent = 1 - (1 - mglobal.flatPercent) * 1.5
201 mglobal.landMinScatter = mglobal.landMinScatter / 1.5
202 mglobal.landMaxScatter = mglobal.landMaxScatter / 1.5
203 mglobal.coastScatter = mglobal.coastScatter / 1.5
204 mglobal.mountainScatter = mglobal.mountainScatter
205 elseif oWorldAge == 3 then
206 print("Map Age: Old")
207 mglobal.belowMountainPercent = 1 - (1 - mglobal.belowMountainPercent) / 1.5
208 mglobal.flatPercent = 1 - (1 - mglobal.flatPercent) / 1.5
209 mglobal.landMinScatter = mglobal.landMinScatter * 1.5
210 mglobal.landMaxScatter = mglobal.landMaxScatter * 1.5
211 mglobal.coastScatter = mglobal.coastScatter * 1.5
212 mglobal.mountainScatter = mglobal.mountainScatter
213 else
214 print("Map Age: Normal")
215 end
216 mglobal.mountainScatter = mglobal.mountainScatter * 0.00001
217
218
219 local oTemp = Map.GetCustomOption(2)
220 if oTemp == 4 then oTemp = 1 + Map.Rand(3, "Communitas random temperature - Lua") end
221 if oTemp == 1 then
222 print("Map Temp: Cool")
223 mglobal.tropicLatitudes = mglobal.tropicLatitudes / 1.5
224 mglobal.horseLatitudes = mglobal.horseLatitudes / 1.5
225 mglobal.iceLatitude = mglobal.iceLatitude / 3
226 mglobal.polarFrontLatitude = mglobal.polarFrontLatitude / 1.5
227 mglobal.tundraTemperature = mglobal.tundraTemperature * 1.25
228 --mglobal.snowTemperature = mglobal.snowTemperature * 1.25 -- snow is just horrible
229 elseif oTemp == 3 then
230 print("Map Temp: Hot")
231 mglobal.tropicLatitudes = mglobal.tropicLatitudes * 1.5
232 mglobal.horseLatitudes = mglobal.horseLatitudes * 1.5
233 mglobal.iceLatitude = 60
234 mglobal.polarFrontLatitude = 65
235 mglobal.tundraTemperature = mglobal.tundraTemperature / 1.25
236 mglobal.snowTemperature = mglobal.snowTemperature / 1.25
237 else
238 print("Map Temp: Normal")
239 end
240
241
242 local oRainfall = Map.GetCustomOption(3)
243 if oRainfall == 4 then oRainfall = 1 + Map.Rand(3, "Communitas random rain - Lua") end
244 if oRainfall == 1 then
245 print("Map Rain: Arid")
246 mglobal.riverPercent = mglobal.riverPercent / 1.5
247 mglobal.featurePercent = mglobal.featurePercent / 1.5
248 mglobal.marshPercent = mglobal.marshPercent / 1.5
249 mglobal.junglePercent = 1 - (1 - mglobal.junglePercent) / 1.5
250 mglobal.zeroTreesPercent = mglobal.zeroTreesPercent * 1.5
251 mglobal.plainsPercent = mglobal.plainsPercent * 1.25
252 mglobal.desertPercent = mglobal.desertPercent * 1.25
253 elseif oRainfall == 3 then
254 print("Map Rain: Wet")
255 mglobal.featurePercent = 0.9 -- should not go above 90%
256 mglobal.riverPercent = mglobal.riverPercent * 1.5
257 mglobal.marshPercent = mglobal.marshPercent * 1.5
258 mglobal.junglePercent = 1 - (1 - mglobal.junglePercent) * 1.5
259 mglobal.zeroTreesPercent = mglobal.zeroTreesPercent / 1.5
260 mglobal.plainsPercent = mglobal.plainsPercent / 1.5
261 mglobal.desertPercent = mglobal.desertPercent / 1.5
262 else
263 print("Map Rain: Normal")
264 end
265
266
267 local oSeaLevel = Map.GetCustomOption(4)
268 if oSeaLevel == 4 then oSeaLevel = 1 + Map.Rand(3, "Communitas random sea level - Lua") end
269 if oSeaLevel == 1 then
270 print("Map Seas: Low")
271 mglobal.landPercent = mglobal.landPercent * 1.25
272 elseif oSeaLevel == 3 then
273 print("Map Seas: High")
274 mglobal.landPercent = mglobal.landPercent / 1.25
275 else
276 print("Map Seas: Normal")
277 end
278
279
280 local oStarts = Map.GetCustomOption(6)
281 if oStarts == 1 then
282 print("Map Starts: Everywhere")
283 mglobal.offsetAtlanticPercent = 0.55 -- Percent of land to divide at the Atlantic Ocean (50% is usually halfway on the map)
284 --mglobal.offshoreCS = 0.50 -- no longer needed
285 else
286 print("Map Starts: Largest Continent")
287 mglobal.offsetAtlanticPercent = 0.35 -- Percent of land to divide at the Atlantic Ocean
288 mglobal.percentLargestContinent = 0.37 -- Eurasia must be this percent of total land (ensures citystates can appear there)
289 mglobal.terraConnectWeight = 10 -- if Eurasia is too small, connect sub-continents with this (size/distance) from Eurasia
290 --mglobal.offshoreCS = 0.75 -- no longer needed
291 mglobal.numNaturalWonders = Round (1.25 * mglobal.numNaturalWonders) -- extra wonders for larger map sizes
292 end
293
294
295 local oRiftWidth = Map.GetCustomOption(8)
296 --mglobal.oceanRiftWidth = mglobal.oceanRiftWidth * mapW
297 if oRiftWidth == 1 then
298 print("Map Ocean Width: Narrow")
299 mglobal.oceanRiftWidth = 1
300 mglobal.landPercent = mglobal.landPercent - 0.02
301 elseif oRiftWidth == 3 then
302 print("Map Ocean Width: Wide")
303 mglobal.oceanRiftWidth = math.max(4, Round(mapW/20))
304 mglobal.landPercent = mglobal.landPercent + 0.05
305 end
306
307 -- Ocean rift sizes
308
309 mglobal.oceanRiftWidth = Round(mglobal.oceanRiftWidth)
310
311 end
312
313
314
315
316
317
318
319
320
321
322 --
323 -- Other settings
324 --
325
326 do
327 --These attenuation factors lower the altitude of the map edges. This is
328 --currently used to prevent large continents in the uninhabitable polar
329 --regions. East/west attenuation is set to zero, but modded maps may
330 --have need for them.
331 mglobal.northAttenuationFactor = 0.0
332 mglobal.northAttenuationRange = 0.0 --percent of the map height.
333 mglobal.southAttenuationFactor = 0.0
334 mglobal.southAttenuationRange = 0.0
335
336 --east west attenuation may be desired for flat maps.
337 mglobal.eastAttenuationFactor = 0.0
338 mglobal.eastAttenuationRange = 0.0 --percent of the map width.
339 mglobal.westAttenuationFactor = 0.0
340 mglobal.westAttenuationRange = 0.0
341
342 -- Rain tweaking variables. I wouldn't touch these.
343 mglobal.pressureNorm = 1.0 --[1.0 = no normalization] Helps to prevent exaggerated Jungle/Marsh banding on the equator. -Bobert13
344 mglobal.minimumRainCost = 0.0001
345 mglobal.upLiftExponent = 4
346 mglobal.polarRainBoost = 0.00
347
348 --North and south isle latitude limits.
349 mglobal.islesNearIce = false
350 mglobal.isleNorthLatitudeLimit = 65
351 mglobal.isleSouthLatitudeLimit = -65
352 mglobal.isleMinDeepWaterNeighbors = 0
353
354 end
355
356
357
358
359
360
361
362
363
364
365
366
367
368 -----------------------------------------------------------------------
369 --Below is map data that should not be altered.
370
371 do
372 mglobal.MountainPasses = {}
373 mglobal.tropicalPlots = {}
374 mglobal.oceanRiftPlots = {}
375 mglobal.islandAreaBuffed = {}
376 mglobal.lakePlots = {}
377 mglobal.seaPlots = {}
378 mglobal.elevationRect = {}
379 mglobal.oceanRiftMidlines = {}
380
381 -- Directions
382 mglobal.C = DirectionTypes.NO_DIRECTION
383 mglobal.NE = DirectionTypes.DIRECTION_NORTHEAST
384 mglobal.E = DirectionTypes.DIRECTION_EAST
385 mglobal.SE = DirectionTypes.DIRECTION_SOUTHEAST
386 mglobal.SW = DirectionTypes.DIRECTION_SOUTHWEST
387 mglobal.W = DirectionTypes.DIRECTION_WEST
388 mglobal.NW = DirectionTypes.DIRECTION_NORTHWEST
389 mglobal.N = DirectionTypes.DIRECTION_NORTHWEST + 1
390 mglobal.S = DirectionTypes.DIRECTION_NORTHWEST + 2
391
392 mglobal.edgeDirections = {
393 mglobal.NE,
394 mglobal.E,
395 mglobal.SE,
396 mglobal.SW,
397 mglobal.W,
398 mglobal.NW
399 }
400
401 mglobal.directionNames = {
402 [mglobal.C] = "C" ,
403 [mglobal.NE] = "NE" ,
404 [mglobal.E] = "E" ,
405 [mglobal.SE] = "SE" ,
406 [mglobal.SW] = "SW" ,
407 [mglobal.W] = "W" ,
408 [mglobal.NW] = "NW" ,
409 [mglobal.N] = "N" ,
410 [mglobal.S] = "S"
411 }
412
413 -- Flow Directions
414 mglobal.NOFLOW = 0
415 mglobal.WESTFLOW = 1
416 mglobal.EASTFLOW = 2
417 mglobal.VERTFLOW = 3
418
419 mglobal.flowNONE = FlowDirectionTypes.NO_FLOWDIRECTION
420 mglobal.flowN = FlowDirectionTypes.FLOWDIRECTION_NORTH
421 mglobal.flowNE = FlowDirectionTypes.FLOWDIRECTION_NORTHEAST
422 mglobal.flowSE = FlowDirectionTypes.FLOWDIRECTION_SOUTHEAST
423 mglobal.flowS = FlowDirectionTypes.FLOWDIRECTION_SOUTH
424 mglobal.flowSW = FlowDirectionTypes.FLOWDIRECTION_SOUTHWEST
425 mglobal.flowNW = FlowDirectionTypes.FLOWDIRECTION_NORTHWEST
426
427 mglobal.flowNames = {
428 [mglobal.flowNONE] = "NONE",
429 [mglobal.flowN] = "N" ,
430 [mglobal.flowNE] = "NE" ,
431 [mglobal.flowSE] = "SE" ,
432 [mglobal.flowS] = "S" ,
433 [mglobal.flowSW] = "SW" ,
434 [mglobal.flowNW] = "NW"
435 }
436
437 -- basic tile yields
438 mglobal.basicYields = {
439 YieldTypes.YIELD_FOOD,
440 YieldTypes.YIELD_PRODUCTION,
441 YieldTypes.YIELD_GOLD,
442 YieldTypes.YIELD_SCIENCE,
443 YieldTypes.YIELD_CULTURE,
444 YieldTypes.YIELD_FAITH
445 }
446
447 --wind zones
448 mglobal.NOZONE = -1
449 mglobal.NPOLAR = 0
450 mglobal.NTEMPERATE = 1
451 mglobal.NEQUATOR = 2
452 mglobal.SEQUATOR = 3
453 mglobal.STEMPERATE = 4
454 mglobal.SPOLAR = 5
455
456 --Hex maps are shorter in the y direction than they are
457 --wide per unit by this much. We need to know this to sample the perlin
458 --maps properly so they don't look squished.
459 mglobal.YtoXRatio = 1.5/(math.sqrt(0.75) * 2)
460
461
462 -- Array of route types - you can change the text, but NOT the order
463 mglobal.routes = {"Land", "Road", "Railroad", "Coastal", "Ocean", "Submarine"}
464
465 -- Array of highlight colours
466 mglobal.highlights = { Red = Vector4(1.0, 0.0, 0.0, 1.0),
467 Green = Vector4(0.0, 1.0, 0.0, 1.0),
468 Blue = Vector4(0.0, 0.0, 1.0, 1.0),
469 Cyan = Vector4(0.0, 1.0, 1.0, 1.0),
470 Yellow = Vector4(1.0, 1.0, 0.0 ,1.0),
471 Magenta = Vector4(1.0, 0.0, 1.0, 1.0),
472 Black = Vector4(0.5, 0.5, 0.5, 1.0)}
473
474 mglobal.lastRouteLength = 0
475 mglobal.pathDirections = {DirectionTypes.DIRECTION_NORTHEAST, DirectionTypes.DIRECTION_EAST, DirectionTypes.DIRECTION_SOUTHEAST,
476 DirectionTypes.DIRECTION_SOUTHWEST, DirectionTypes.DIRECTION_WEST, DirectionTypes.DIRECTION_NORTHWEST}
477
478 end
479
480 return mglobal
481end
482
483function GetMapScriptInfo()
484 local world_age, temperature, rainfall, sea_level, resources = GetCoreMapOptions()
485 return {
486 Name = "Communitas",
487 Description = "Creates continents and islands, with climate based on elevation and wind. Includes custom game setup options.",
488 IsAdvancedMap = 0,
489 SupportsMultiplayer = true,
490 IconIndex = 5,
491 SortIndex = -999,
492 CustomOptions = {
493 world_age,
494 temperature,
495 rainfall,
496 sea_level,
497 resources,
498 {
499 Name = "Players Start",
500 Values = {
501 "Continents - Everywhere",
502 "Terra - Largest Continent"
503 },
504 DefaultValue = 1,
505 SortPriority = 1,
506 },
507 {
508 Name = "Ocean Rifts",
509 Values = {
510 "Pacific and Atlantic",
511 "2 Atlantic",
512 "2 Pacific",
513 "2 Random",
514 "1 Random",
515 "None"
516 },
517 DefaultValue = 1,
518 SortPriority = 2,
519 },
520 {
521 Name = "Rift Width",
522 Values = {
523 "Narrow",
524 "Normal",
525 "Wide"
526 },
527 DefaultValue = 2,
528 SortPriority = 3,
529 },
530 },
531 }
532end
533
534function GetMapInitData(worldSize)
535 print("GetMapInitData")
536 local worldsizes = {
537 [GameInfo.Worlds.WORLDSIZE_DUEL.ID] = {39, 28},
538 [GameInfo.Worlds.WORLDSIZE_TINY.ID] = {56, 38},
539 [GameInfo.Worlds.WORLDSIZE_SMALL.ID] = {69, 46},
540 [GameInfo.Worlds.WORLDSIZE_STANDARD.ID] = {79, 53},
541 [GameInfo.Worlds.WORLDSIZE_LARGE.ID] = {87, 59},
542 [GameInfo.Worlds.WORLDSIZE_HUGE.ID] = {97, 66}
543 }
544
545 if Map.GetCustomOption(6) == 2 then
546 -- Enlarge terra-style maps 30% to create expansion room on the new world
547 worldsizes = {
548 [GameInfo.Worlds.WORLDSIZE_DUEL.ID] = {44, 31},
549 [GameInfo.Worlds.WORLDSIZE_TINY.ID] = {64, 43},
550 [GameInfo.Worlds.WORLDSIZE_SMALL.ID] = {78, 52},
551 [GameInfo.Worlds.WORLDSIZE_STANDARD.ID] = {90, 60},
552 [GameInfo.Worlds.WORLDSIZE_LARGE.ID] = {99, 66},
553 [GameInfo.Worlds.WORLDSIZE_HUGE.ID] = {109, 74}
554 }
555 end
556 --
557 local grid_size = worldsizes[worldSize]
558
559
560 --
561 local world = GameInfo.Worlds[worldSize]
562 if(world ~= nil) then
563 return {
564 Width = grid_size[1],
565 Height = grid_size[2],
566 WrapX = true,
567 }
568 end
569end
570
571function DetermineContinents()
572 print("Determining continents for art purposes (CommunitasMap)")
573 -- Each plot has a continent art type.
574 -- Command for setting the art type for a plot is: <plot object>:SetContinentArtType(<art_set_number>)
575
576 -- CONTINENTAL ART SETS - in order from hot to cold
577 -- 0) Ocean
578 -- 3) Africa
579 -- 2) Asia
580 -- 1) America
581 -- 4) Europe
582
583 contArt = {}
584 contArt.OCEAN = 0
585 contArt.AFRICA = 3
586 contArt.ASIA = 2
587 contArt.AMERICA = 1
588 contArt.EUROPE = 4
589
590 local mapW, mapH = Map.GetGridSize()
591
592 --[[
593 for i, plot in Plots() do
594 if plot:IsWater() then
595 plot:SetContinentArtType(contArt.OCEAN)
596 else
597 plot:SetContinentArtType(contArt.AFRICA)
598 end
599 end
600 --]]
601
602 local continentMap = PWAreaMap:New(elevationMap.width,elevationMap.height,elevationMap.wrapX,elevationMap.wrapY)
603 continentMap:DefineAreas(oceanMatch)
604 table.sort(continentMap.areaList,function (a,b) return a.size > b.size end)
605
606 --check for jungle
607 for y=0, elevationMap.height - 1 do
608 for x=0,elevationMap.width - 1 do
609 local i = elevationMap:GetIndex(x,y)
610 local area = continentMap:GetAreaByID(continentMap.data[i])
611 area.hasJungle = false
612 end
613 end
614 for y=0, elevationMap.height - 1 do
615 for x=0, elevationMap.width - 1 do
616 local plot = Map.GetPlot(x,y)
617 if plot:GetFeatureType() == FeatureTypes.FEATURE_JUNGLE then
618 local i = elevationMap:GetIndex(x,y)
619 local area = continentMap:GetAreaByID(continentMap.data[i])
620 area.hasJungle = true
621 end
622 end
623 end
624 for n=1, #continentMap.areaList do
625-- if not continentMap.areaList[n].trueMatch and not continentMap.areaList[n].hasJungle then
626 if not continentMap.areaList[n].trueMatch then
627 continentMap.areaList[n].artStyle = 1 + Map.Rand(2, "Continent Art Styles - Lua") -- left out America's orange trees
628 end
629 end
630 for y=0, elevationMap.height - 1 do
631 for x=0, elevationMap.width - 1 do
632 local plot = Map.GetPlot(x,y)
633 local i = elevationMap:GetIndex(x,y)
634 local artStyle = continentMap:GetAreaByID(continentMap.data[i]).artStyle
635 if plot:IsWater() then
636 plot:SetContinentArtType(contArt.OCEAN)
637 elseif jungleMatch(x,y) then
638 plot:SetContinentArtType(contArt.ASIA)
639 else
640 plot:SetContinentArtType(contArt.AFRICA)
641 end
642 end
643 end
644
645 --Africa has the best looking deserts, so for the biggest
646 --desert use Africa. America has a nice dirty looking desert also, so
647 --that should be the second biggest desert.
648 local desertMap = PWAreaMap:New(elevationMap.width,elevationMap.height,elevationMap.wrapX,elevationMap.wrapY)
649 desertMap:DefineAreas(desertMatch)
650 table.sort(desertMap.areaList,function (a,b) return a.size > b.size end)
651 local largestDesertID = nil
652 local secondLargestDesertID = nil
653 for n=1,#desertMap.areaList do
654 --if debugTime then print(string.format("area[%d] size = %d",n,desertMap.areaList[n].size)) end
655 if desertMap.areaList[n].trueMatch then
656 if largestDesertID == nil then
657 largestDesertID = desertMap.areaList[n].id
658 else
659 secondLargestDesertID = desertMap.areaList[n].id
660 break
661 end
662 end
663 end
664 for y=0,elevationMap.height - 1 do
665 for x=0,elevationMap.width - 1 do
666 local plot = Map.GetPlot(x,y)
667 local i = elevationMap:GetIndex(x,y)
668 if desertMap.data[i] == largestDesertID then
669 plot:SetContinentArtType(contArt.AFRICA)
670 elseif desertMap.data[i] == secondLargestDesertID then
671 plot:SetContinentArtType(contArt.AMERICA)
672 elseif plot:GetTerrainType() == TerrainTypes.TERRAIN_DESERT then
673 plot:SetContinentArtType(contArt.ASIA)
674 end
675 end
676 end
677
678 -- Set tundra/mountains -> snowy when near to snow tiles
679 for y = 0, mapH-1 do
680 for x = 0, mapW-1 do
681 local plot = Map.GetPlot(x,y)
682 local plotTerrainID = plot:GetTerrainType()
683 if IsMountain(plot) then
684 local coldness = 0
685 local zone = elevationMap:GetZone(y)
686
687 if (zone == mg.NPOLAR or zone == mg.SPOLAR) then
688 coldness = coldness + 2
689 elseif (zone == mg.NTEMPERATE or zone == mg.STEMPERATE) then
690 coldness = coldness + 1
691 else
692 coldness = coldness - 1
693 end
694
695 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 1) do
696 local nearTerrainID = nearPlot:GetTerrainType()
697 local nearFeatureID = nearPlot:GetFeatureType()
698 if IsMountain(nearPlot) then
699 coldness = coldness + 0.5
700 elseif nearTerrainID == TerrainTypes.TERRAIN_SNOW then
701 coldness = coldness + 2
702 elseif nearTerrainID == TerrainTypes.TERRAIN_TUNDRA then
703 coldness = coldness + 1
704 elseif nearTerrainID == TerrainTypes.TERRAIN_DESERT then
705 coldness = coldness - 1
706 elseif nearFeatureID == FeatureTypes.FEATURE_JUNGLE or nearFeatureID == FeatureTypes.FEATURE_MARSH then
707 coldness = coldness - 8
708 end
709 end
710
711 for nearPlot in Plot_GetPlotsInCircle(plot, 2, 2) do
712 if IsMountain(nearPlot) then
713 coldness = coldness + 0.25
714 end
715 end
716
717 -- Avoid snow near tropical jungle
718 if coldness >= 1 then
719 for nearPlot in Plot_GetPlotsInCircle(plot, 2, 3) do
720 local nearFeatureID = nearPlot:GetFeatureType()
721 if nearFeatureID == FeatureTypes.FEATURE_JUNGLE or nearFeatureID == FeatureTypes.FEATURE_MARSH then
722 coldness = coldness - 8 / math.max(1, Map.PlotDistance(x, y, nearPlot:GetX(), nearPlot:GetY()))
723 end
724 end
725 end
726
727 if coldness >= 6 then
728 --plot:SetTerrainType(TerrainTypes.TERRAIN_SNOW, false, true)
729 plot:SetContinentArtType(contArt.EUROPE)
730 elseif coldness >= 4 then
731 --plot:SetTerrainType(TerrainTypes.TERRAIN_TUNDRA, false, true)
732 plot:SetContinentArtType(contArt.AMERICA)
733 elseif coldness >= 2 then
734 --plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, false, true)
735 plot:SetContinentArtType(contArt.ASIA)
736 else
737 --plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, false, true)
738 plot:SetContinentArtType(contArt.AFRICA)
739 end
740
741
742 elseif plotTerrainID == TerrainTypes.TERRAIN_TUNDRA then
743 local coldness = 0
744 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 1) do
745 local nearTerrainID = nearPlot:GetTerrainType()
746 local nearFeatureID = nearPlot:GetFeatureType()
747 if nearTerrainID == TerrainTypes.TERRAIN_SNOW then
748 coldness = coldness + 5
749 elseif nearTerrainID == TerrainTypes.TERRAIN_TUNDRA then
750 coldness = coldness + 1
751 elseif nearTerrainID == TerrainTypes.TERRAIN_DESERT or nearFeatureID == FeatureTypes.FEATURE_JUNGLE or nearFeatureID == FeatureTypes.FEATURE_MARSH then
752 coldness = coldness - 2
753 end
754 end
755 for nearPlot in Plot_GetPlotsInCircle(plot, 2, 2) do
756 if nearTerrainID == TerrainTypes.TERRAIN_DESERT or nearFeatureID == FeatureTypes.FEATURE_JUNGLE or nearFeatureID == FeatureTypes.FEATURE_MARSH then
757 coldness = coldness - 1
758 end
759 end
760 if coldness >= 6 then
761 if plot:GetFeatureType() == FeatureTypes.FEATURE_FOREST then
762 plot:SetContinentArtType(contArt.ASIA)
763 else
764 plot:SetContinentArtType(contArt.EUROPE)
765 end
766 else
767 plot:SetContinentArtType(contArt.AFRICA)
768 end
769 elseif plotTerrainID == TerrainTypes.TERRAIN_SNOW then
770 plot:SetContinentArtType(contArt.EUROPE)
771 end
772 end
773 end
774end
775
776function inheritsFrom( baseClass )
777 --inheritance mechanism from http://www.gamedev.net/community/forums/topic.asp?topic_id=561909
778
779 local new_class = {}
780 local class_mt = { __index = new_class }
781
782 function new_class:create()
783 local newinst = {}
784 setmetatable( newinst, class_mt )
785 return newinst
786 end
787
788 if nil ~= baseClass then
789 setmetatable( new_class, { __index = baseClass } )
790 end
791
792 -- Implementation of additional OO properties starts here --
793
794 -- Return the class object of the instance
795 function new_class:class()
796 return new_class
797 end
798
799 -- Return the super class object of the instance, optional base class of the given class (must be part of hiearchy)
800 function new_class:baseClass(class)
801 return new_class:_B(class)
802 end
803
804 -- Return the super class object of the instance, optional base class of the given class (must be part of hiearchy)
805 function new_class:_B(class)
806 if (class==nil) or (new_class==class) then
807 return baseClass
808 elseif(baseClass~=nil) then
809 return baseClass:_B(class)
810 end
811 return nil
812 end
813
814 -- Return true if the caller is an instance of theClass
815 function new_class:_ISA( theClass )
816 local b_isa = false
817
818 local cur_class = new_class
819
820 while ( nil ~= cur_class ) and ( false == b_isa ) do
821 if cur_class == theClass then
822 b_isa = true
823 else
824 cur_class = cur_class:baseClass()
825 end
826 end
827
828 return b_isa
829 end
830
831 return new_class
832end
833
834function Logger(self)
835 local logger = {}
836 setmetatable(logger, self)
837 self.__index = self
838
839 logger.level = LEVEL.INFO
840
841 logger.SetLevel = function (self, level)
842 self.level = level
843 end
844
845 logger.Message = function (self, level, ...)
846 local arg = {...}
847 if LEVEL[level] < LEVEL[self.level] then
848 return false
849 end
850 if type(arg[1]) == "string" then
851 local _, numCommands = string.gsub(arg[1], "[%%]", "")
852 for i = 2, numCommands+1 do
853 if type(arg[i]) ~= "number" and type(arg[i]) ~= "string" then
854 arg[i] = tostring(arg[i])
855 end
856 end
857 else
858 arg[1] = tostring(arg[1])
859 end
860 local message = string.format(unpack(arg))
861 if level == LOG_FATAL then
862 message = string.format("Turn %-3s %s", Game.GetGameTurn(), message)
863 print(level .. string.rep(" ", 7-level:len()) .. message)
864 if debug then print(debug.traceback()) end
865 else
866 if level >= LOG_INFO then
867 message = string.format("Turn %-3s %s", Game.GetGameTurn(), message)
868 end
869 print(level .. string.rep(" ", 7-level:len()) .. message)
870 end
871 return true
872 end
873
874 if debugWithLogger then
875 logger.Trace = function (logger, ...) return logger:Message(LOG_TRACE, unpack{...}) end
876 logger.Debug = function (logger, ...) return logger:Message(LOG_DEBUG, unpack{...}) end
877 logger.Info = function (logger, ...) return logger:Message(LOG_INFO, unpack{...}) end
878 logger.Warn = function (logger, ...) return logger:Message(LOG_WARN, unpack{...}) end
879 logger.Error = function (logger, ...) return logger:Message(LOG_ERROR, unpack{...}) end
880 logger.Fatal = function (logger, ...) return logger:Message(LOG_FATAL, unpack{...}) end
881 else
882 logger.Trace = function () end
883 logger.Debug = function () end
884 logger.Info = function () end
885 logger.Warn = function () end
886 logger.Error = function () end
887 logger.Fatal = function () end
888 end
889 return logger
890end
891
892LOG_TRACE = "TRACE"
893LOG_DEBUG = "DEBUG"
894LOG_INFO = "INFO"
895LOG_WARN = "WARN"
896LOG_ERROR = "ERROR"
897LOG_FATAL = "FATAL"
898
899LEVEL = {
900 [LOG_TRACE] = 1,
901 [LOG_DEBUG] = 2,
902 [LOG_INFO] = 3,
903 [LOG_WARN] = 4,
904 [LOG_ERROR] = 5,
905 [LOG_FATAL] = 6,
906}
907
908LuaLogger = {}
909LuaLogger.New = Logger
910
911log = LuaLogger:New()
912log:SetLevel("INFO")
913
914
915
916
917
918
919
920
921
922
923
924--
925-- Generate Plots
926--
927
928function StartPlotSystem()
929 -- Get Resources setting input by user.
930 local res = Map.GetCustomOption(5) or 2
931 if res == 6 then
932 res = 1 + Map.Rand(3, "Random Resources Option - Lua")
933 end
934
935 local oStarts = Map.GetCustomOption(6)
936 local divMethod = nil
937 if oStarts == 1 then
938 -- Continents
939 divMethod = 2
940 else
941 --Terra
942 divMethod = 1
943 end
944
945 print("Creating start plot database.")
946 local start_plot_database = AssignStartingPlots.Create()
947
948 print("Dividing the map in to Regions.")
949 -- Regional Division Method 2: Continental or 1:Terra
950 local args = {
951 method = divMethod,
952 resources = res,
953 }
954 start_plot_database:GenerateRegions(args)
955
956 print("Choosing start locations for civilizations.")
957 start_plot_database:ChooseLocations()
958
959 print("Normalizing start locations and assigning them to Players.")
960 start_plot_database:BalanceAndAssign()
961
962 --error(":P")
963 print("Placing Natural Wonders.")
964 start_plot_database:PlaceNaturalWonders()
965
966 print("Placing Resources and City States.")
967 start_plot_database:PlaceResourcesAndCityStates()
968end
969
970function GeneratePlotTypes()
971 print("Creating initial map data - CommunitasMap")
972 --[[
973 local plot = Map.GetPlot(5, 5)
974 for nearPlot, distance in Plot_GetPlotsInCircle(plot, 0, 1) do
975 print(string.format(
976 "plot %s distance=%s",
977 Plot_GetID(nearPlot),
978 distance
979 ))
980 end
981 --]]
982
983 local timeStart = debugTime and os.clock() or 0
984 local mapW, mapH = Map.GetGridSize()
985
986 --first do all the preliminary calculations in this function
987 if debugTime then print(string.format("map size: width=%d, height=%d",mapW,mapH)) end
988 mg = MapGlobals:New()
989 PWRandSeed()
990
991 -- Elevations
992
993 elevationMap = GenerateElevationMap(mapW,mapH,true,false)
994
995 --elevationMap:Save("elevationMap.csv")
996
997 -- Plots
998 print("Generating plot types - CommunitasMap")
999 ShiftMaps()
1000 DiffMap = GenerateDiffMap(mapW,mapH,true,false)
1001 CreateArcticOceans()
1002 CreateVerticalOceans()
1003 ConnectSeasToOceans()
1004 FillInLakes()
1005 elevationMap = SetOceanRiftElevations(elevationMap)
1006 ConnectTerraContinents()
1007
1008 -- Rainfall
1009 rainfallMap, temperatureMap = GenerateRainfallMap(elevationMap)
1010 --rainfallMap:Save("rainfallMap.csv")
1011
1012 -- Rivers
1013 riverMap = RiverMap:New(elevationMap)
1014 riverMap:SetJunctionAltitudes()
1015 riverMap:SiltifyLakes()
1016 riverMap:SetFlowDestinations()
1017 riverMap:SetRiverSizes(rainfallMap)
1018
1019 --find exact thresholds
1020 local hillsThreshold = DiffMap:FindThresholdFromPercent(mg.flatPercent,false,true)
1021 local mountainsThreshold = DiffMap:FindThresholdFromPercent(mg.belowMountainPercent,false,true)
1022 local i = 0
1023 for y = 0, mapH - 1,1 do
1024 for x = 0,mapW - 1,1 do
1025 local plot = Map.GetPlot(x,y)
1026 if elevationMap:IsBelowSeaLevel(x,y) then
1027 plot:SetPlotType(PlotTypes.PLOT_OCEAN, false, false)
1028 elseif DiffMap.data[i] < hillsThreshold then
1029 plot:SetPlotType(PlotTypes.PLOT_LAND,false,false)
1030 --This code makes the game only ever plot flat land if it's within two tiles of
1031 --the seam. This prevents issues with tiles that don't look like what they are.
1032 elseif x == 0 or x == 1 or x == mapW - 1 or x == mapW -2 then
1033 plot:SetPlotType(PlotTypes.PLOT_LAND,false,false)
1034 -- Bobert13
1035 elseif DiffMap.data[i] < mountainsThreshold then
1036 plot:SetPlotType(PlotTypes.PLOT_HILLS,false,false)
1037 else
1038 plot:SetPlotType(PlotTypes.PLOT_MOUNTAIN,false,false)
1039 end
1040 i=i+1
1041 end
1042 end
1043 Map.RecalculateAreas()
1044 GenerateIslands()
1045 GenerateCoasts()
1046 SetOceanRiftPlots()
1047end
1048
1049function ConnectSeasToOceans()
1050 local areaMap = PWAreaMap:New(elevationMap.width,elevationMap.height,elevationMap.wrapX,elevationMap.wrapY)
1051 areaMap:DefineAreas(waterMatch)
1052 local oceanArea, oceanSize = GetLargestArea(areaMap)
1053
1054 if not oceanArea then
1055 print("ConnectSeasToOceans: No ocean!")
1056 return
1057 end
1058
1059 local plotFunc = function(plot)
1060 return not Plot_IsWater(plot, true)
1061 end
1062
1063 log:Info("ConnectSeasToOceans: oceanSize = %s", oceanSize)
1064 local newWater = {}
1065 for areaID=1, #areaMap.areaList do
1066 local seaArea = areaMap.areaList[areaID]
1067 if seaArea.trueMatch and seaArea.size < oceanSize then
1068 local pathPlots, distance, airDistance = GetPathBetweenAreas(areaMap, seaArea, oceanArea, true, plotFunc)
1069 if seaArea.size >= mg.lakeSize then--or seaArea.size >= 2 * distance then
1070 log:Info("ConnectSeasToOceans: Connect seaArea.size = %-3s distance = %-3s airDistance = %-3s", seaArea.size, distance, airDistance)
1071 --log:Info(" Connect")
1072 for _, plot in pairs(pathPlots) do
1073 local plotID = Plot_GetID(plot)
1074 newWater[Plot_GetID(plot)] = elevationMap.seaLevelThreshold - 0.01
1075 if seaArea.size <= 2 * mg.lakeSize then
1076 table.insert(mg.seaPlots, plot)
1077 end
1078 --plot:SetFeatureType(FeatureTypes.FEATURE_ICE, -1)
1079 end
1080 end
1081 end
1082 end
1083 for plotID, elevation in pairs(newWater) do
1084 elevationMap.data[plotID] = elevation
1085 end
1086end
1087
1088function ConnectTerraContinents()
1089 if Map.GetCustomOption(6) == 1 then
1090 -- Continents-style formation
1091 return
1092 end
1093
1094 log:Info("ConnectTerraContinents")
1095
1096 local oceanX1 = mg.oceanRiftMidlines[1]
1097 local oceanX2 = mg.oceanRiftMidlines[2]
1098 local continents = {}
1099 local totalLand = 0
1100 local areaMap = PWAreaMap:New(elevationMap.width,elevationMap.height,elevationMap.wrapX,elevationMap.wrapY)
1101
1102 areaMap:DefineAreas(landMatch)
1103
1104 for areaID=1, #areaMap.areaList do
1105 local area = areaMap.areaList[areaID]
1106 if area.trueMatch and area.size > 10 then
1107 if not IsAreaBetweenOceans(area, oceanX1, oceanX2) then
1108 table.insert(continents, area)
1109 end
1110 end
1111 end
1112
1113 if continents == {} then
1114 print("ConnectTerraContinents: No biggest continent!")
1115 return
1116 end
1117
1118 for plotID, elevation in pairs(elevationMap.data) do
1119 if elevation > elevationMap.seaLevelThreshold then
1120 totalLand = totalLand + 1
1121 end
1122 end
1123
1124 table.sort(continents, function(a, b)
1125 return a.size > b.size
1126 end)
1127
1128 log:Info("ConnectTerraContinents: largestLand = %s%% of %s totalLand", Round(100 * continents[1].size / totalLand), totalLand)
1129 local largestSize = continents[1].size
1130 if largestSize > mg.percentLargestContinent * totalLand then
1131 return
1132 end
1133
1134 --
1135
1136 local plotFunc = function(plot)
1137 return Plot_IsWater(plot, true)
1138 end
1139
1140 for i = 1, #continents do
1141 if i == 1 then
1142 continents[i].pathPlots = {}
1143 continents[i].airDistance = 0
1144 continents[i].distance = 0
1145 else
1146 local pathPlots, distance, airDistance = GetPathBetweenAreas(areaMap, continents[i], continents[1], false, plotFunc)
1147 continents[i].pathPlots = pathPlots
1148 continents[i].airDistance = airDistance
1149 continents[i].distance = distance
1150 end
1151 end
1152
1153 for index, area in ipairs(continents) do
1154 log:Info("ConnectTerraContinents: continent #%-2s size = %-4s distance = %-3s size/distance = %s", index, area.size, area.distance, Round(area.size / math.max(1, area.distance)))
1155 end
1156
1157 local newLand = {}
1158
1159 for index, area in ipairs(continents) do
1160 if index ~= 1 and area.distance < mg.oceanRiftWidth + 2 and area.size / math.max(1, area.distance) > mg.terraConnectWeight then
1161 log:Info("ConnectTerraContinents: Connect continents[%s].size = %-3s distance = %-3s airDistance = %-3s size/distance = %s",
1162 index,
1163 area.size,
1164 area.distance,
1165 area.airDistance,
1166 Round(area.size / math.max(1, area.distance))
1167 )
1168 for _, plot in pairs(area.pathPlots) do
1169 newLand[Plot_GetID(plot)] = elevationMap.seaLevelThreshold
1170 end
1171 largestSize = largestSize + area.size
1172 if largestSize > mg.percentLargestContinent * totalLand then
1173 break
1174 end
1175 end
1176 end
1177 for plotID, elevation in pairs(newLand) do
1178 --Map.GetPlotByIndex(plotID):SetFeatureType(FeatureTypes.FEATURE_ICE, -1)
1179 elevationMap.data[plotID] = elevation
1180 end
1181end
1182
1183function IsAreaBetweenOceans(area, oceanX1, oceanX2)
1184 return false
1185end
1186
1187function GetPathBetweenAreas(areaMap, areaA, areaB, findLowest, plotMatchFunc)
1188 -- using Dijkstra's algorithm
1189 local mapW, mapH = Map.GetGridSize()
1190
1191 -- initialize
1192 local plots = {}
1193 for plotID = 0, areaMap.length - 1 do
1194 plots[plotID] = {}
1195 plots[plotID].plot = Map.GetPlot(elevationMap:GetXYFromIndex(plotID))
1196 plots[plotID].areaID = areaMap.data[plotID]
1197
1198 if plots[plotID].areaID == areaA.id or plots[plotID].areaID == areaB.id then
1199 -- consider all plots equal in start and end areas
1200 plots[plotID].elevation = 0
1201 else
1202 if findLowest then
1203 -- connect oceans
1204 plots[plotID].elevation = GetElevationByPlotID(plotID) ^ 2
1205 else
1206 -- connect continents
1207 plots[plotID].elevation = 1000 - GetElevationByPlotID(plotID) ^ 2
1208 end
1209 end
1210
1211 if plots[plotID].areaID == areaA.id then
1212 plots[plotID].sumElevation = 0
1213 else
1214 plots[plotID].sumElevation = 30000
1215 end
1216 end
1217
1218 -- main loop
1219 local lowestID = -1
1220 local lowest = nil
1221 local attempts = 0
1222 while attempts < mapW * mapH do
1223 lowestID, lowest = GetBestFromTable(plots,
1224 function(a, b)
1225 if not a or not b then
1226 return a or b
1227 end
1228 if a.sumElevation ~= b.sumElevation then
1229 return a.sumElevation < b.sumElevation
1230 end
1231 return a.elevation < b.elevation
1232 end
1233 )
1234
1235 if not lowest or lowest.sumElevation == math.huge or lowest.areaID == areaB.id then
1236 break
1237 end
1238 plots[lowestID] = nil
1239 for nearPlot in Plot_GetPlotsInCircle(lowest.plot, 1) do
1240 local nearID = Plot_GetID(nearPlot)
1241 local nearPlotInfo = plots[nearID]
1242 if nearPlotInfo then
1243 local altSumElevation = lowest.sumElevation + nearPlotInfo.elevation
1244 if altSumElevation < nearPlotInfo.sumElevation then
1245 plots[nearID].sumElevation = altSumElevation
1246 plots[nearID].previous = lowest
1247 end
1248 end
1249 end
1250 end
1251
1252 local path = {}
1253 local start = lowest
1254 while lowest.previous do
1255 if plotMatchFunc(lowest.plot) then
1256 table.insert(path, lowest.plot)
1257 end
1258 lowest = lowest.previous
1259 end
1260 return path, #path, Map.PlotDistance(start.plot:GetX(), start.plot:GetY(), lowest.plot:GetX(), lowest.plot:GetY())
1261end
1262
1263function GetBestFromTable(list, compareFunc)
1264 local least = nil
1265 local leastID = -1
1266 for k, v in pairs(list) do
1267 if compareFunc(v, least) then
1268 leastID = k
1269 least = v
1270 end
1271 end
1272 return leastID, least
1273end
1274
1275
1276
1277function GetLargestArea(areaMap)
1278 local largestArea = nil
1279 local largestSize = 0
1280 for areaID=1, #areaMap.areaList do
1281 local area = areaMap.areaList[areaID]
1282 if area.trueMatch and area.size > largestSize then
1283 largestSize = area.size
1284 largestArea = area
1285 end
1286 end
1287 return largestArea, largestSize
1288end
1289
1290function FillInLakes()
1291 local areaMap = PWAreaMap:New(elevationMap.width,elevationMap.height,elevationMap.wrapX,elevationMap.wrapY)
1292 areaMap:DefineAreas(waterMatch)
1293 for i=1, #areaMap.areaList do
1294 local area = areaMap.areaList[i]
1295 if area.trueMatch and area.size <= mg.lakeSize then
1296 for n = 0, areaMap.length do
1297 if areaMap.data[n] == area.id then
1298 elevationMap.data[n] = elevationMap.seaLevelThreshold
1299 --print("Saving lake of size ".. area.size)
1300 table.insert(mg.lakePlots, Map.GetPlot(elevationMap:GetXYFromIndex(n)))
1301 end
1302 end
1303 end
1304 end
1305end
1306
1307function RestoreLakes()
1308 log:Info("RestoreLakes")
1309
1310 --[[
1311 -- Seperate lakes from new ocean rifts
1312 for index, plot in pairs(mg.lakePlots) do
1313 local isLake = true
1314 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
1315 if nearPlot:IsWater() and not nearPlot:IsLake() then
1316 table.remove(mg.lakePlots, index)
1317 break
1318 end
1319 end
1320 end
1321 --]]
1322
1323 -- Remove all rivers bordering lakes or oceans
1324 for _, plot in Plots() do
1325 if Plot_IsWater(plot) then
1326 for edgeDirection = 0, DirectionTypes.NUM_DIRECTION_TYPES - 1 do
1327 Plot_SetRiver(plot, edgeDirection, mg.flowNONE)
1328 end
1329 end
1330 end
1331
1332 -- Add lakes
1333 for _, plot in pairs(mg.lakePlots) do
1334 local isIce = (plot:GetFeatureType() == FeatureTypes.FEATURE_ICE)
1335 plot:SetTerrainType(TerrainTypes.TERRAIN_COAST, false, true)
1336 if isIce then
1337 --plot:SetFeatureType(FeatureTypes.FEATURE_ICE, -1)
1338 end
1339 end
1340
1341 -- Calculate outflow from lakes
1342 local riversToAdd = {}
1343 local lakesDone = {}
1344 for plotID, plot in Plots(Shuffle) do
1345 for edgeDirection = 0, DirectionTypes.NUM_DIRECTION_TYPES - 1 do
1346 if Plot_IsRiver(plot, edgeDirection) then
1347 prevPlot, edgeA, edgeB, flowA, flowB = Plot_GetPreviousRiverPlot(plot, edgeDirection)
1348 if prevPlot and Plot_IsLake(prevPlot) and not Contains(lakesDone, prevPlot) then
1349 print(string.format(
1350 "%2s flowing river: add edge %2s flowing %2s, edge %2s flowing %2s",
1351 mg.flowNames[Plot_GetRiverFlowDirection(plot, edgeDirection)],
1352 mg.directionNames[edgeA],
1353 mg.flowNames[flowA],
1354 mg.directionNames[edgeB],
1355 mg.flowNames[flowB]
1356 ))
1357 table.insert(riversToAdd, {plot=prevPlot, edge=edgeA, flow=flowA})
1358 table.insert(riversToAdd, {plot=prevPlot, edge=edgeB, flow=flowB})
1359 table.insert(lakesDone, prevPlot)
1360 end
1361 end
1362 end
1363 end
1364
1365 for _, v in pairs(riversToAdd) do
1366 Plot_SetRiver(v.plot, v.edge, v.flow)
1367 end
1368end
1369
1370function AddLakes()
1371 -- disable vanilla lake creation
1372end
1373
1374
1375
1376
1377
1378
1379
1380
1381--
1382-- Generate Terrain
1383--
1384
1385function GenerateTerrain()
1386 print("Generating terrain - CommunitasMap")
1387 local timeStart = debugTime and os.clock() or 0
1388 local terrainTundra = GameInfoTypes["TERRAIN_TUNDRA"]
1389 local terrainGrass = GameInfoTypes["TERRAIN_GRASS"]
1390
1391 local mapW, mapH = Map.GetGridSize()
1392
1393 --first find minimum rain above sea level for a soft desert transition
1394 local minRain = 100.0
1395 for y = 0, mapH-1 do
1396 for x = 0,mapW-1 do
1397 local i = elevationMap:GetIndex(x,y)
1398 if not elevationMap:IsBelowSeaLevel(x,y) then
1399 if rainfallMap.data[i] < minRain then
1400 minRain = rainfallMap.data[i]
1401 end
1402 end
1403 end
1404 end
1405
1406 --find exact thresholds
1407 local desertThreshold = rainfallMap:FindThresholdFromPercent(mg.desertPercent,false,true)
1408 local plainsThreshold = rainfallMap:FindThresholdFromPercent(mg.plainsPercent,false,true)
1409 for y = 0, mapH-1 do
1410 for x = 0,mapW-1 do
1411 local i = elevationMap:GetIndex(x,y)
1412 local plot = Map.GetPlot(x, y)
1413 if not elevationMap:IsBelowSeaLevel(x,y) then
1414 if rainfallMap.data[i] < desertThreshold then
1415 if temperatureMap.data[i] < mg.snowTemperature then--and elevationMap:GetZone(y) ~= mg.NEQUATOR and elevationMap:GetZone(y) ~= mg.SEQUATOR then
1416 plot:SetTerrainType(TerrainTypes.TERRAIN_SNOW,false,false)
1417 elseif temperatureMap.data[i] < mg.tundraTemperature then
1418 plot:SetTerrainType(terrainTundra,false,false)
1419 elseif temperatureMap.data[i] < mg.desertMinTemperature then
1420 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS,false,false)
1421 else
1422 --if rainfallMap.data[i] < (PWRand() * (desertThreshold - minRain) + desertThreshold - minRain)/2.0 + minRain then
1423 plot:SetTerrainType(TerrainTypes.TERRAIN_DESERT,false,false)
1424 --else
1425 --plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS,false,false)
1426 --end
1427 end
1428 elseif rainfallMap.data[i] < plainsThreshold then
1429 if temperatureMap.data[i] < mg.snowTemperature then--and elevationMap:GetZone(y) ~= mg.NEQUATOR and elevationMap:GetZone(y) ~= mg.SEQUATOR then
1430 plot:SetTerrainType(TerrainTypes.TERRAIN_SNOW,false,false)
1431 elseif temperatureMap.data[i] < mg.tundraTemperature then
1432 plot:SetTerrainType(terrainTundra,false,false)
1433 else
1434 if rainfallMap.data[i] < (PWRand() * (plainsThreshold - desertThreshold) + plainsThreshold - desertThreshold)/2.0 + desertThreshold then
1435 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS,false,false)
1436 else
1437 plot:SetTerrainType(terrainGrass,false,false)
1438 end
1439 end
1440 else
1441 if temperatureMap.data[i] < mg.snowTemperature then--and elevationMap:GetZone(y) ~= mg.NEQUATOR and elevationMap:GetZone(y) ~= mg.SEQUATOR then
1442 plot:SetTerrainType(TerrainTypes.TERRAIN_SNOW,false,false)
1443 elseif temperatureMap.data[i] < mg.tundraTemperature then
1444 plot:SetTerrainType(terrainTundra,false,false)
1445 else
1446 plot:SetTerrainType(terrainGrass,false,false)
1447 end
1448 end
1449 end
1450 end
1451 end
1452
1453
1454 if debugTime then print(string.format("%5s ms, GenerateTerrain %s", math.floor((os.clock() - timeStart) * 1000), "Main")) end
1455 if debugTime then timeStart = os.clock() end
1456
1457 if debugTime then print(string.format("%5s ms, GenerateTerrain %s", math.floor((os.clock() - timeStart) * 1000), "SetOceanRiftPlots")) end
1458 if debugTime then timeStart = os.clock() end
1459 BlendTerrain()
1460 if debugTime then print(string.format("%5s ms, GenerateTerrain %s", math.floor((os.clock() - timeStart) * 1000), "BlendTerrain")) end
1461end
1462
1463
1464function GenerateIslands()
1465 -- A cell system will be used to combine predefined land chunks with randomly generated island groups.
1466 -- Define the cell traits. (These need to fit correctly with the map grid width and height.)
1467 local iW, iH = Map.GetGridSize()
1468 local iCellWidth = 10
1469 local iCellHeight = 8
1470 local iNumCellColumns = math.floor(iW / iCellWidth)
1471 local iNumCellRows = math.floor(iH / iCellHeight)
1472 local iNumTotalCells = iNumCellColumns * iNumCellRows
1473 local cell_data = table.fill(false, iNumTotalCells) -- Stores data on map cells in use. All cells begin as empty.
1474 local iNumCellsInUse = 0
1475 local iNumCellTarget = math.floor(iNumTotalCells * 1)
1476 local island_chain_PlotTypes = table.fill(PlotTypes.PLOT_OCEAN, iW * iH)
1477
1478 -- Add randomly generated island groups
1479 local iNumGroups = iNumCellTarget -- Should virtually never use all the groups.
1480 for group = 1, iNumGroups do
1481 if iNumCellsInUse >= iNumCellTarget then -- Map has reeached desired island population.
1482 print("-") print("** Number of Island Groups produced:", group - 1) print("-")
1483 break
1484 end
1485 --[[ Formation Chart
1486 1. Single Cell, Axis Only
1487 2. Double Cell, Horizontal, Axis Only
1488 3. Single Cell With Dots
1489 4. Double Cell, Horizontal, With Shifted Dots
1490 5. Double Cell, Vertical, Axis Only
1491 6. Double Cell, Vertical, With Shifted Dots
1492 7. Triple Cell, Vertical, With Double Dots
1493 8. Square of Cells 2x2 With Double Dots
1494 9. Rectangle 3x2 With Double Dots
1495 10. Rectangle 2x3 With Double Dots ]]--
1496 --
1497 -- Choose a formation
1498 local rate_threshold = {}
1499 local total_appearance_rate, iNumFormations = 0, 0
1500 local appearance_rates = { -- These numbers are relative to one another. No specific target total is necessary.
1501 7, -- #1
1502 3, -- #2
1503 15, --#3
1504 8, -- #4
1505 3, -- #5
1506 6, -- #6
1507 4, -- #7
1508 6, -- #8
1509 4, -- #9
1510 3, -- #10
1511 }
1512 for i, rate in ipairs(appearance_rates) do
1513 total_appearance_rate = total_appearance_rate + rate
1514 iNumFormations = iNumFormations + 1
1515 end
1516 local accumulated_rate = 0
1517 for index = 1, iNumFormations do
1518 local threshold = (appearance_rates[index] + accumulated_rate) * 10000 / total_appearance_rate
1519 table.insert(rate_threshold, threshold)
1520 accumulated_rate = accumulated_rate + appearance_rates[index]
1521 end
1522 local formation_type
1523 local diceroll = Map.Rand(10000, "Choose formation type - Island Making - Lua")
1524 for index, threshold in ipairs(rate_threshold) do
1525 if diceroll <= threshold then -- Choose this formation type.
1526 formation_type = index
1527 break
1528 end
1529 end
1530 -- Choose cell(s) not in use
1531 local iNumAttemptsToFindOpenCells = 0
1532 local found_unoccupied_cell = false
1533 local anchor_cell, cell_x, cell_y, foo
1534 while found_unoccupied_cell == false do
1535 if iNumAttemptsToFindOpenCells > 100 then -- Too many attempts on this pass. Might not be any valid locations for this formation.
1536 print("-") print("*-* ERROR: Formation type of", formation_type, "for island group#", group, "unable to find an open space. Switching to single-cell.")
1537 formation_type = 3 -- Reset formation type.
1538 iNumAttemptsToFindOpenCells = 0
1539 end
1540 local diceroll = 1 + Map.Rand(iNumTotalCells, "Choosing a cell for an island group - Polynesia LUA")
1541 if cell_data[diceroll] == false then -- Anchor cell is unoccupied.
1542 -- If formation type is multiple-cell, all secondary cells must also be unoccupied.
1543 if formation_type == 1 or formation_type == 3 then -- single cell, proceed.
1544 anchor_cell = diceroll
1545 found_unoccupied_cell = true
1546 elseif formation_type == 2 or formation_type == 4 then -- double cell, horizontal.
1547 -- Check to see if anchor cell is in the final column. If so, reject.
1548 cell_x = math.fmod(diceroll, iNumCellColumns)
1549 if cell_x ~= 0 then -- Anchor cell is valid, but still have to check near cell.
1550 if cell_data[diceroll + 1] == false then -- Adjacent cell is unoccupied.
1551 anchor_cell = diceroll
1552 found_unoccupied_cell = true
1553 end
1554 end
1555 elseif formation_type == 5 or formation_type == 6 then -- double cell, vertical.
1556 -- Check to see if anchor cell is in the final row. If so, reject.
1557 cell_y, foo = math.modf(diceroll / iNumCellColumns)
1558 cell_y = cell_y + 1
1559 if cell_y < iNumCellRows then -- Anchor cell is valid, but still have to check cell above it.
1560 if cell_data[diceroll + iNumCellColumns] == false then -- Adjacent cell is unoccupied.
1561 anchor_cell = diceroll
1562 found_unoccupied_cell = true
1563 end
1564 end
1565 elseif formation_type == 7 then -- triple cell, vertical.
1566 -- Check to see if anchor cell is in the northern two rows. If so, reject.
1567 cell_y, foo = math.modf(diceroll / iNumCellColumns)
1568 cell_y = cell_y + 1
1569 if cell_y < iNumCellRows - 1 then -- Anchor cell is valid, but still have to check cells above it.
1570 if cell_data[diceroll + iNumCellColumns] == false then -- Cell directly above is unoccupied.
1571 if cell_data[diceroll + (iNumCellColumns * 2)] == false then -- Cell two rows above is unoccupied.
1572 anchor_cell = diceroll
1573 found_unoccupied_cell = true
1574 end
1575 end
1576 end
1577 elseif formation_type == 8 then -- square, 2x2.
1578 -- Check to see if anchor cell is in the final row or column. If so, reject.
1579 cell_x = math.fmod(diceroll, iNumCellColumns)
1580 if cell_x ~= 0 then
1581 cell_y, foo = math.modf(diceroll / iNumCellColumns)
1582 cell_y = cell_y + 1
1583 if cell_y < iNumCellRows then -- Anchor cell is valid. Still have to check the other three cells.
1584 if cell_data[diceroll + iNumCellColumns] == false then
1585 if cell_data[diceroll + 1] == false then
1586 if cell_data[diceroll + iNumCellColumns + 1] == false then -- All cells are open.
1587 anchor_cell = diceroll
1588 found_unoccupied_cell = true
1589 end
1590 end
1591 end
1592 end
1593 end
1594 elseif formation_type == 9 then -- horizontal, 3x2.
1595 -- Check to see if anchor cell is too near to an edge. If so, reject.
1596 cell_x = math.fmod(diceroll, iNumCellColumns)
1597 if cell_x ~= 0 and cell_x ~= iNumCellColumns - 1 then
1598 cell_y, foo = math.modf(diceroll / iNumCellColumns)
1599 cell_y = cell_y + 1
1600 if cell_y < iNumCellRows then -- Anchor cell is valid. Still have to check the other cells.
1601 if cell_data[diceroll + iNumCellColumns] == false then
1602 if cell_data[diceroll + 1] == false and cell_data[diceroll + 2] == false then
1603 if cell_data[diceroll + iNumCellColumns + 1] == false then
1604 if cell_data[diceroll + iNumCellColumns + 2] == false then -- All cells are open.
1605 anchor_cell = diceroll
1606 found_unoccupied_cell = true
1607 end
1608 end
1609 end
1610 end
1611 end
1612 end
1613 elseif formation_type == 10 then -- vertical, 2x3.
1614 -- Check to see if anchor cell is too near to an edge. If so, reject.
1615 cell_x = math.fmod(diceroll, iNumCellColumns)
1616 if cell_x ~= 0 then
1617 cell_y, foo = math.modf(diceroll / iNumCellColumns)
1618 cell_y = cell_y + 1
1619 if cell_y < iNumCellRows - 1 then -- Anchor cell is valid. Still have to check the other cells.
1620 if cell_data[diceroll + iNumCellColumns] == false then
1621 if cell_data[diceroll + 1] == false then
1622 if cell_data[diceroll + iNumCellColumns + 1] == false then
1623 if cell_data[diceroll + (iNumCellColumns * 2)] == false then
1624 if cell_data[diceroll + (iNumCellColumns * 2) + 1] == false then -- All cells are open.
1625 anchor_cell = diceroll
1626 found_unoccupied_cell = true
1627 end
1628 end
1629 end
1630 end
1631 end
1632 end
1633 end
1634 end
1635 end
1636 iNumAttemptsToFindOpenCells = iNumAttemptsToFindOpenCells + 1
1637 end
1638 -- Find Cell X and Y
1639 cell_x = math.fmod(anchor_cell, iNumCellColumns)
1640 if cell_x == 0 then
1641 cell_x = iNumCellColumns
1642 end
1643 cell_y, foo = math.modf(anchor_cell / iNumCellColumns)
1644 cell_y = cell_y + 1
1645
1646 -- Debug
1647 --print("-") print("-") print("* Group# " .. group, "Formation Type: " .. formation_type, "Cell X, Y: " .. cell_x .. ", " .. cell_y)
1648
1649 -- Create this island group.
1650 local iWidth, iHeight, fTilt -- Scope the variables needed for island group creation.
1651 local plot_data = {}
1652 local x_shift, y_shift
1653 if formation_type == 1 then -- single cell
1654 local x_shrinkage = Map.Rand(4, "Cell Width adjustment - Lua")
1655 if x_shrinkage > 2 then
1656 x_shrinkage = 0
1657 end
1658 local y_shrinkage = Map.Rand(5, "Cell Height adjustment - Lua")
1659 if y_shrinkage > 2 then
1660 y_shrinkage = 0
1661 end
1662 x_shift, y_shift = 0, 0
1663 if x_shrinkage > 0 then
1664 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1665 end
1666 if y_shrinkage > 0 then
1667 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1668 end
1669 iWidth = iCellWidth - x_shrinkage
1670 iHeight = iCellHeight - y_shrinkage
1671 fTilt = Map.Rand(181, "Angle for island chain axis - LUA")
1672 plot_data = CreateSingleAxisIslandChain(iWidth, iHeight, fTilt)
1673
1674 elseif formation_type == 2 then -- two cells, horizontal
1675 local x_shrinkage = Map.Rand(8, "Cell Width adjustment - Lua")
1676 if x_shrinkage > 5 then
1677 x_shrinkage = 0
1678 end
1679 local y_shrinkage = Map.Rand(5, "Cell Height adjustment - Lua")
1680 if y_shrinkage > 2 then
1681 y_shrinkage = 0
1682 end
1683 x_shift, y_shift = 0, 0
1684 if x_shrinkage > 0 then
1685 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1686 end
1687 if y_shrinkage > 0 then
1688 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1689 end
1690 iWidth = iCellWidth * 2 - x_shrinkage
1691 iHeight = iCellHeight - y_shrinkage
1692 -- Limit angles to mostly horizontal ones.
1693 fTilt = 145 + Map.Rand(90, "Angle for island chain axis - LUA")
1694 if fTilt > 180 then
1695 fTilt = fTilt - 180
1696 end
1697 plot_data = CreateSingleAxisIslandChain(iWidth, iHeight, fTilt)
1698
1699 elseif formation_type == 3 then -- single cell, with dots
1700 local x_shrinkage = Map.Rand(4, "Cell Width adjustment - Lua")
1701 if x_shrinkage > 2 then
1702 x_shrinkage = 0
1703 end
1704 local y_shrinkage = Map.Rand(5, "Cell Height adjustment - Lua")
1705 if y_shrinkage > 2 then
1706 y_shrinkage = 0
1707 end
1708 x_shift, y_shift = 0, 0
1709 if x_shrinkage > 0 then
1710 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1711 end
1712 if y_shrinkage > 0 then
1713 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1714 end
1715 iWidth = iCellWidth - x_shrinkage
1716 iHeight = iCellHeight - y_shrinkage
1717 fTilt = Map.Rand(181, "Angle for island chain axis - LUA")
1718 -- Determine "dot box"
1719 local iInnerWidth, iInnerHeight = iWidth - 2, iHeight - 2
1720 -- Determine number of dots
1721 local die_1 = Map.Rand(4, "Diceroll - Lua")
1722 local die_2 = Map.Rand(8, "Diceroll - Lua")
1723 local iNumDots = 4
1724 if die_1 + die_2 > 1 then
1725 iNumDots = iNumDots + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1726 else
1727 iNumDots = iNumDots + die_1 + die_2
1728 end
1729 plot_data = CreateAxisChainWithDots(iWidth, iHeight, fTilt, iInnerWidth, iInnerHeight, iNumDots)
1730
1731 elseif formation_type == 4 then -- two cells, horizontal, with dots
1732 local x_shrinkage = Map.Rand(8, "Cell Width adjustment - Lua")
1733 if x_shrinkage > 5 then
1734 x_shrinkage = 0
1735 end
1736 local y_shrinkage = Map.Rand(5, "Cell Height adjustment - Lua")
1737 if y_shrinkage > 2 then
1738 y_shrinkage = 0
1739 end
1740 x_shift, y_shift = 0, 0
1741 if x_shrinkage > 0 then
1742 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1743 end
1744 if y_shrinkage > 0 then
1745 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1746 end
1747 iWidth = iCellWidth * 2 - x_shrinkage
1748 iHeight = iCellHeight - y_shrinkage
1749 -- Limit angles to mostly horizontal ones.
1750 fTilt = 145 + Map.Rand(90, "Angle for island chain axis - LUA")
1751 if fTilt > 180 then
1752 fTilt = fTilt - 180
1753 end
1754 -- Determine "dot box"
1755 local iInnerWidth = math.floor(iWidth / 2)
1756 local iInnerHeight = iHeight - 2
1757 local iInnerWest = 2 + Map.Rand((iWidth - 1) - iInnerWidth, "Shift for sub island group - Lua")
1758 local iInnerSouth = 2
1759 -- Determine number of dots
1760 local die_1 = Map.Rand(4, "Diceroll - Lua")
1761 local die_2 = Map.Rand(10, "Diceroll - Lua")
1762 local iNumDots = 5
1763 if die_1 + die_2 > 1 then
1764 iNumDots = iNumDots + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1765 else
1766 iNumDots = iNumDots + die_1 + die_2
1767 end
1768 plot_data = CreateAxisChainWithShiftedDots(iWidth, iHeight, fTilt, iInnerWidth, iInnerHeight, iInnerWest, iInnerSouth, iNumDots)
1769
1770 elseif formation_type == 5 then -- Double Cell, Vertical, Axis Only
1771 local x_shrinkage = Map.Rand(5, "Cell Width adjustment - Lua")
1772 if x_shrinkage > 2 then
1773 x_shrinkage = 0
1774 end
1775 local y_shrinkage = Map.Rand(7, "Cell Height adjustment - Lua")
1776 if y_shrinkage > 4 then
1777 y_shrinkage = 0
1778 end
1779 x_shift, y_shift = 0, 0
1780 if x_shrinkage > 0 then
1781 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1782 end
1783 if y_shrinkage > 0 then
1784 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1785 end
1786 iWidth = iCellWidth - x_shrinkage
1787 iHeight = iCellHeight * 2 - y_shrinkage
1788 -- Limit angles to mostly vertical ones.
1789 fTilt = 55 + Map.Rand(71, "Angle for island chain axis - LUA")
1790 plot_data = CreateSingleAxisIslandChain(iWidth, iHeight, fTilt)
1791
1792 elseif formation_type == 6 then -- Double Cell, Vertical With Dots
1793 local x_shrinkage = Map.Rand(5, "Cell Width adjustment - Lua")
1794 if x_shrinkage > 2 then
1795 x_shrinkage = 0
1796 end
1797 local y_shrinkage = Map.Rand(7, "Cell Height adjustment - Lua")
1798 if y_shrinkage > 4 then
1799 y_shrinkage = 0
1800 end
1801 x_shift, y_shift = 0, 0
1802 if x_shrinkage > 0 then
1803 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1804 end
1805 if y_shrinkage > 0 then
1806 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1807 end
1808 iWidth = iCellWidth - x_shrinkage
1809 iHeight = iCellHeight * 2 - y_shrinkage
1810 -- Limit angles to mostly vertical ones.
1811 fTilt = 55 + Map.Rand(71, "Angle for island chain axis - LUA")
1812 -- Determine "dot box"
1813 local iInnerWidth = iWidth - 2
1814 local iInnerHeight = math.floor(iHeight / 2)
1815 local iInnerWest = 2
1816 local iInnerSouth = 2 + Map.Rand((iHeight - 1) - iInnerHeight, "Shift for sub island group - Lua")
1817 -- Determine number of dots
1818 local die_1 = Map.Rand(4, "Diceroll - Lua")
1819 local die_2 = Map.Rand(10, "Diceroll - Lua")
1820 local iNumDots = 5
1821 if die_1 + die_2 > 1 then
1822 iNumDots = iNumDots + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1823 else
1824 iNumDots = iNumDots + die_1 + die_2
1825 end
1826 plot_data = CreateAxisChainWithShiftedDots(iWidth, iHeight, fTilt, iInnerWidth, iInnerHeight, iInnerWest, iInnerSouth, iNumDots)
1827
1828 elseif formation_type == 7 then -- Triple Cell, Vertical With Double Dots
1829 local x_shrinkage = Map.Rand(4, "Cell Width adjustment - Lua")
1830 if x_shrinkage > 1 then
1831 x_shrinkage = 0
1832 end
1833 local y_shrinkage = Map.Rand(9, "Cell Height adjustment - Lua")
1834 if y_shrinkage > 5 then
1835 y_shrinkage = 0
1836 end
1837 x_shift, y_shift = 0, 0
1838 if x_shrinkage > 0 then
1839 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1840 end
1841 if y_shrinkage > 0 then
1842 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1843 end
1844 iWidth = iCellWidth - x_shrinkage
1845 iHeight = iCellHeight * 3 - y_shrinkage
1846 -- Limit angles to steep ones.
1847 fTilt = 70 + Map.Rand(41, "Angle for island chain axis - LUA")
1848 -- Handle Dots Group 1.
1849 local iInnerWidth1 = iWidth - 3
1850 local iInnerHeight1 = iCellHeight - 1
1851 local iInnerWest1 = 2 + Map.Rand(2, "Shift for sub island group - Lua")
1852 local iInnerSouth1 = 2 + Map.Rand(iCellHeight - 3, "Shift for sub island group - Lua")
1853 -- Determine number of dots
1854 local die_1 = Map.Rand(4, "Diceroll - Lua")
1855 local die_2 = Map.Rand(8, "Diceroll - Lua")
1856 local iNumDots1 = 4
1857 if die_1 + die_2 > 1 then
1858 iNumDots1 = iNumDots1 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1859 else
1860 iNumDots1 = iNumDots1 + die_1 + die_2
1861 end
1862 -- Handle Dots Group 2.
1863 local iInnerWidth2 = iWidth - 3
1864 local iInnerHeight2 = iCellHeight - 1
1865 local iInnerWest2 = 2 + Map.Rand(2, "Shift for sub island group - Lua")
1866 local iInnerSouth2 = iCellHeight + 2 + Map.Rand(iCellHeight - 3, "Shift for sub island group - Lua")
1867 -- Determine number of dots
1868 local die_1 = Map.Rand(4, "Diceroll - Lua")
1869 local die_2 = Map.Rand(8, "Diceroll - Lua")
1870 local iNumDots2 = 4
1871 if die_1 + die_2 > 1 then
1872 iNumDots2 = iNumDots2 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1873 else
1874 iNumDots2 = iNumDots2 + die_1 + die_2
1875 end
1876 plot_data = CreateAxisChainWithDoubleDots(iWidth, iHeight, fTilt, iInnerWidth1, iInnerHeight1, iInnerWest1, iInnerSouth1,
1877 iNumDots1, iInnerWidth2, iInnerHeight2, iInnerWest2, iInnerSouth2, iNumDots2)
1878 elseif formation_type == 8 then -- Square Block 2x2 With Double Dots
1879 local x_shrinkage = Map.Rand(6, "Cell Width adjustment - Lua")
1880 if x_shrinkage > 4 then
1881 x_shrinkage = 0
1882 end
1883 local y_shrinkage = Map.Rand(5, "Cell Height adjustment - Lua")
1884 if y_shrinkage > 3 then
1885 y_shrinkage = 0
1886 end
1887 x_shift, y_shift = 0, 0
1888 if x_shrinkage > 0 then
1889 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1890 end
1891 if y_shrinkage > 0 then
1892 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1893 end
1894 iWidth = iCellWidth * 2 - x_shrinkage
1895 iHeight = iCellHeight * 2 - y_shrinkage
1896 -- Full range of angles
1897 fTilt = Map.Rand(181, "Angle for island chain axis - LUA")
1898 -- Handle Dots Group 1.
1899 local iInnerWidth1 = iCellWidth - 2
1900 local iInnerHeight1 = iCellHeight - 2
1901 local iInnerWest1 = 3 + Map.Rand(iCellWidth - 2, "Shift for sub island group - Lua")
1902 local iInnerSouth1 = 3 + Map.Rand(iCellHeight - 2, "Shift for sub island group - Lua")
1903 -- Determine number of dots
1904 local die_1 = Map.Rand(6, "Diceroll - Lua")
1905 local die_2 = Map.Rand(10, "Diceroll - Lua")
1906 local iNumDots1 = 5
1907 if die_1 + die_2 > 1 then
1908 iNumDots1 = iNumDots1 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1909 else
1910 iNumDots1 = iNumDots1 + die_1 + die_2
1911 end
1912 -- Handle Dots Group 2.
1913 local iInnerWidth2 = iCellWidth - 2
1914 local iInnerHeight2 = iCellHeight - 2
1915 local iInnerWest2 = 3 + Map.Rand(iCellWidth - 2, "Shift for sub island group - Lua")
1916 local iInnerSouth2 = 3 + Map.Rand(iCellHeight - 2, "Shift for sub island group - Lua")
1917 -- Determine number of dots
1918 local die_1 = Map.Rand(4, "Diceroll - Lua")
1919 local die_2 = Map.Rand(8, "Diceroll - Lua")
1920 local iNumDots2 = 5
1921 if die_1 + die_2 > 1 then
1922 iNumDots2 = iNumDots2 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1923 else
1924 iNumDots2 = iNumDots2 + die_1 + die_2
1925 end
1926 plot_data = CreateAxisChainWithDoubleDots(iWidth, iHeight, fTilt, iInnerWidth1, iInnerHeight1, iInnerWest1, iInnerSouth1,
1927 iNumDots1, iInnerWidth2, iInnerHeight2, iInnerWest2, iInnerSouth2, iNumDots2)
1928
1929 elseif formation_type == 9 then -- Horizontal Block 3x2 With Double Dots
1930 local x_shrinkage = Map.Rand(8, "Cell Width adjustment - Lua")
1931 if x_shrinkage > 5 then
1932 x_shrinkage = 0
1933 end
1934 local y_shrinkage = Map.Rand(5, "Cell Height adjustment - Lua")
1935 if y_shrinkage > 3 then
1936 y_shrinkage = 0
1937 end
1938 x_shift, y_shift = 0, 0
1939 if x_shrinkage > 0 then
1940 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1941 end
1942 if y_shrinkage > 0 then
1943 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1944 end
1945 iWidth = iCellWidth * 3 - x_shrinkage
1946 iHeight = iCellHeight * 2 - y_shrinkage
1947 -- Limit angles to mostly horizontal ones.
1948 fTilt = 145 + Map.Rand(90, "Angle for island chain axis - LUA")
1949 if fTilt > 180 then
1950 fTilt = fTilt - 180
1951 end
1952 -- Handle Dots Group 1.
1953 local iInnerWidth1 = iCellWidth
1954 local iInnerHeight1 = iCellHeight - 2
1955 local iInnerWest1 = 4 + Map.Rand(iCellWidth + 2, "Shift for sub island group - Lua")
1956 local iInnerSouth1 = 3 + Map.Rand(iCellHeight - 2, "Shift for sub island group - Lua")
1957 -- Determine number of dots
1958 local die_1 = Map.Rand(4, "Diceroll - Lua")
1959 local die_2 = Map.Rand(8, "Diceroll - Lua")
1960 local iNumDots1 = 9
1961 if die_1 + die_2 > 1 then
1962 iNumDots1 = iNumDots1 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1963 else
1964 iNumDots1 = iNumDots1 + die_1 + die_2
1965 end
1966 -- Handle Dots Group 2.
1967 local iInnerWidth2 = iCellWidth
1968 local iInnerHeight2 = iCellHeight - 2
1969 local iInnerWest2 = 4 + Map.Rand(iCellWidth + 2, "Shift for sub island group - Lua")
1970 local iInnerSouth2 = 3 + Map.Rand(iCellHeight - 2, "Shift for sub island group - Lua")
1971 -- Determine number of dots
1972 local die_1 = Map.Rand(5, "Diceroll - Lua")
1973 local die_2 = Map.Rand(7, "Diceroll - Lua")
1974 local iNumDots2 = 8
1975 if die_1 + die_2 > 1 then
1976 iNumDots2 = iNumDots2 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
1977 else
1978 iNumDots2 = iNumDots2 + die_1 + die_2
1979 end
1980 plot_data = CreateAxisChainWithDoubleDots(iWidth, iHeight, fTilt, iInnerWidth1, iInnerHeight1, iInnerWest1, iInnerSouth1,
1981 iNumDots1, iInnerWidth2, iInnerHeight2, iInnerWest2, iInnerSouth2, iNumDots2)
1982
1983 elseif formation_type == 10 then -- Vertical Block 2x3 With Double Dots
1984 local x_shrinkage = Map.Rand(6, "Cell Width adjustment - Lua")
1985 if x_shrinkage > 4 then
1986 x_shrinkage = 0
1987 end
1988 local y_shrinkage = Map.Rand(8, "Cell Height adjustment - Lua")
1989 if y_shrinkage > 5 then
1990 y_shrinkage = 0
1991 end
1992 x_shift, y_shift = 0, 0
1993 if x_shrinkage > 0 then
1994 x_shift = Map.Rand(x_shrinkage, "Cell Width offset - Lua")
1995 end
1996 if y_shrinkage > 0 then
1997 y_shift = Map.Rand(y_shrinkage, "Cell Height offset - Lua")
1998 end
1999 iWidth = iCellWidth * 2 - x_shrinkage
2000 iHeight = iCellHeight * 3 - y_shrinkage
2001 -- Mostly vertical
2002 fTilt = 55 + Map.Rand(71, "Angle for island chain axis - LUA")
2003 -- Handle Dots Group 1.
2004 local iInnerWidth1 = iCellWidth - 2
2005 local iInnerHeight1 = iCellHeight
2006 local iInnerWest1 = 3 + Map.Rand(iCellWidth - 2, "Shift for sub island group - Lua")
2007 local iInnerSouth1 = 4 + Map.Rand(iCellHeight + 2, "Shift for sub island group - Lua")
2008 -- Determine number of dots
2009 local die_1 = Map.Rand(4, "Diceroll - Lua")
2010 local die_2 = Map.Rand(10, "Diceroll - Lua")
2011 local iNumDots1 = 8
2012 if die_1 + die_2 > 1 then
2013 iNumDots1 = iNumDots1 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
2014 else
2015 iNumDots1 = iNumDots1 + die_1 + die_2
2016 end
2017 -- Handle Dots Group 2.
2018 local iInnerWidth2 = iCellWidth - 2
2019 local iInnerHeight2 = iCellHeight
2020 local iInnerWest2 = 3 + Map.Rand(iCellWidth - 2, "Shift for sub island group - Lua")
2021 local iInnerSouth2 = 4 + Map.Rand(iCellHeight + 2, "Shift for sub island group - Lua")
2022 -- Determine number of dots
2023 local die_1 = Map.Rand(4, "Diceroll - Lua")
2024 local die_2 = Map.Rand(8, "Diceroll - Lua")
2025 local iNumDots2 = 7
2026 if die_1 + die_2 > 1 then
2027 iNumDots2 = iNumDots2 + Map.Rand(die_1 + die_2, "Number of dots to add to island chain - Lua")
2028 else
2029 iNumDots2 = iNumDots2 + die_1 + die_2
2030 end
2031 plot_data = CreateAxisChainWithDoubleDots(iWidth, iHeight, fTilt, iInnerWidth1, iInnerHeight1, iInnerWest1, iInnerSouth1,
2032 iNumDots1, iInnerWidth2, iInnerHeight2, iInnerWest2, iInnerSouth2, iNumDots2)
2033 end
2034
2035 -- Obtain land plots from the plot data
2036 local x_adjust = (cell_x - 1) * iCellWidth + x_shift
2037 local y_adjust = (cell_y - 1) * iCellHeight + y_shift
2038 for y = 1, iHeight do
2039 for x = 1, iWidth do
2040 local data_index = (y - 1) * iWidth + x
2041 if plot_data[data_index] == true then -- This plot is land.
2042 local real_x, real_y = x + x_adjust - 1, y + y_adjust - 1
2043 local plot_index = real_y * iW + real_x + 1
2044 island_chain_PlotTypes[plot_index] = PlotTypes.PLOT_LAND
2045 end
2046 end
2047 end
2048
2049 -- Record cells in use
2050 if formation_type == 1 then -- single cell
2051 cell_data[anchor_cell] = true
2052 iNumCellsInUse = iNumCellsInUse + 1
2053 elseif formation_type == 2 then
2054 cell_data[anchor_cell], cell_data[anchor_cell + 1] = true, true
2055 iNumCellsInUse = iNumCellsInUse + 2
2056 elseif formation_type == 3 then
2057 cell_data[anchor_cell] = true
2058 iNumCellsInUse = iNumCellsInUse + 1
2059 elseif formation_type == 4 then
2060 cell_data[anchor_cell], cell_data[anchor_cell + 1] = true, true
2061 iNumCellsInUse = iNumCellsInUse + 2
2062 elseif formation_type == 5 then
2063 cell_data[anchor_cell], cell_data[anchor_cell + iNumCellColumns] = true, true
2064 iNumCellsInUse = iNumCellsInUse + 2
2065 elseif formation_type == 6 then
2066 cell_data[anchor_cell], cell_data[anchor_cell + iNumCellColumns] = true, true
2067 iNumCellsInUse = iNumCellsInUse + 2
2068 elseif formation_type == 7 then
2069 cell_data[anchor_cell], cell_data[anchor_cell + iNumCellColumns] = true, true
2070 cell_data[anchor_cell + (iNumCellColumns * 2)] = true
2071 iNumCellsInUse = iNumCellsInUse + 3
2072 elseif formation_type == 8 then
2073 cell_data[anchor_cell], cell_data[anchor_cell + 1] = true, true
2074 cell_data[anchor_cell + iNumCellColumns], cell_data[anchor_cell + iNumCellColumns + 1] = true, true
2075 iNumCellsInUse = iNumCellsInUse + 4
2076 elseif formation_type == 9 then
2077 cell_data[anchor_cell], cell_data[anchor_cell + 1] = true, true
2078 cell_data[anchor_cell + iNumCellColumns], cell_data[anchor_cell + iNumCellColumns + 1] = true, true
2079 cell_data[anchor_cell + 2], cell_data[anchor_cell + iNumCellColumns + 2] = true, true
2080 iNumCellsInUse = iNumCellsInUse + 6
2081 elseif formation_type == 10 then
2082 cell_data[anchor_cell], cell_data[anchor_cell + 1] = true, true
2083 cell_data[anchor_cell + iNumCellColumns], cell_data[anchor_cell + iNumCellColumns + 1] = true, true
2084 cell_data[anchor_cell + (iNumCellColumns * 2)], cell_data[anchor_cell + (iNumCellColumns * 2) + 1] = true, true
2085 iNumCellsInUse = iNumCellsInUse + 6
2086 end
2087 end
2088
2089 -- Debug check of cell occupation.
2090 --print("- - -")
2091 for loop = iNumCellRows, 1, -1 do
2092 local c = (loop - 1) * iNumCellColumns
2093 local stringdata = {}
2094 for innerloop = 1, iNumCellColumns do
2095 if cell_data[c + innerloop] == false then
2096 stringdata[innerloop] = "false"
2097 else
2098 stringdata[innerloop] = "true "
2099 end
2100 end
2101 --print("Row: ", table.concat(stringdata))
2102 end
2103 --
2104
2105 -- Add Hills and Peaks to randomly generated islands.
2106 local regionHillsFrac = Fractal.Create(iW, iH, 5, {}, 7, 7)
2107 local regionPeaksFrac = Fractal.Create(iW, iH, 6, {}, 7, 7)
2108 local iHillsBottom1 = regionHillsFrac:GetHeight(20)
2109 local iHillsTop1 = regionHillsFrac:GetHeight(35)
2110 local iHillsBottom2 = regionHillsFrac:GetHeight(65)
2111 local iHillsTop2 = regionHillsFrac:GetHeight(80)
2112 local iPeakThreshold = regionPeaksFrac:GetHeight(80)
2113 for x = 0, iW - 1 do
2114 for y = 0, iH - 1 do
2115 local i = y * iW + x + 1
2116 if island_chain_PlotTypes[i] ~= PlotTypes.PLOT_OCEAN then
2117 local hillVal = regionHillsFrac:GetHeight(x,y)
2118 if ((hillVal >= iHillsBottom1 and hillVal <= iHillsTop1) or (hillVal >= iHillsBottom2 and hillVal <= iHillsTop2)) then
2119 local peakVal = regionPeaksFrac:GetHeight(x,y)
2120 if (peakVal >= iPeakThreshold) then
2121 island_chain_PlotTypes[i] = PlotTypes.PLOT_MOUNTAIN
2122 else
2123 island_chain_PlotTypes[i] = PlotTypes.PLOT_HILLS
2124 end
2125 end
2126 end
2127 end
2128 end
2129
2130 -- Apply island data to the map.
2131 for y = 2, iH - 3 do -- avoid polar caps
2132 for x = 0, iW - 1 do
2133 local i = y * iW + x + 1
2134 local plot = Map.GetPlot(x, y)
2135 if island_chain_PlotTypes[i] ~= PlotTypes.PLOT_OCEAN and Plot_IsWater(plot, true) then
2136 local isValid = true
2137 local numAdjacentLand = 0
2138 -- Don't fill river deltas with land
2139 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
2140 if nearPlot:GetPlotType() ~= PlotTypes.PLOT_OCEAN and nearPlot:Area():GetNumTiles() >= 10 then
2141 numAdjacentLand = numAdjacentLand + 1
2142 if numAdjacentLand > 1 then
2143 isValid = false
2144 break
2145 end
2146 end
2147 end
2148 if isValid then
2149 plot:SetPlotType(island_chain_PlotTypes[i], false, false)
2150 else
2151 --plot:SetPlotType(PlotTypes.PLOT_MOUNTAIN, false, false)
2152 end
2153 end
2154 end
2155 end
2156end
2157
2158function GetTemperature(myPlot)
2159 if myPlot:GetTerrainType() == TerrainTypes.TERRAIN_SNOW then
2160 return 0
2161 elseif myPlot:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
2162 return 1
2163 end
2164 return 2
2165end
2166
2167function BlendTerrain()
2168 local mapW, mapH = Map.GetGridSize()
2169 local landPlots = {}
2170
2171 local mountainCheckTime = 0
2172 for plotID, plot in Plots(Shuffle) do
2173 if Plot_IsWater(plot) then
2174 --
2175 else
2176 local plotTerrainID = plot:GetTerrainType()
2177 local plotFeatureID = plot:GetFeatureType()
2178 local plotPercent = Plot_GetCirclePercents(plot, 1, mg.terrainBlendRange)
2179 local randPercent = 1 + PWRand() * 2 * mg.terrainBlendRandom - mg.terrainBlendRandom
2180
2181 if plot:IsMountain() then
2182 -- minimize necessary pathfinding
2183 local numNearMountains = 0
2184 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 1) do
2185 if nearPlot:IsMountain() then
2186 numNearMountains = numNearMountains + 1
2187 end
2188 end
2189 if debugTime then timeStart = os.clock() end
2190 if 2 <= numNearMountains and numNearMountains <= 4 then
2191 CreatePossibleMountainPass(plot)
2192 end
2193 if debugTime then mountainCheckTime = mountainCheckTime + (os.clock() - timeStart) end
2194 else
2195 if plotTerrainID == TerrainTypes.TERRAIN_GRASS then
2196 if plotPercent.TERRAIN_DESERT + plotPercent.TERRAIN_SNOW >= 0.33 * randPercent then
2197 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, true, true)
2198 if plot:GetFeatureType() == FeatureTypes.FEATURE_MARSH then
2199 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST, -1)
2200 end
2201 end
2202 elseif plotTerrainID == TerrainTypes.TERRAIN_PLAINS then
2203 if plotPercent.TERRAIN_DESERT >= 0.5 * randPercent then
2204 --plot:SetTerrainType(TerrainTypes.TERRAIN_DESERT, true, true)
2205 end
2206 elseif plotTerrainID == TerrainTypes.TERRAIN_DESERT then
2207 if plotPercent.TERRAIN_GRASS + plotPercent.TERRAIN_SNOW >= 0.25 * randPercent then
2208 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, true, true)
2209 elseif plotPercent.FEATURE_JUNGLE + plotPercent.FEATURE_MARSH >= 0.25 * randPercent then
2210 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, true, true)
2211 end
2212 elseif plotFeatureID == FeatureTypes.FEATURE_JUNGLE or plotFeatureID == FeatureTypes.FEATURE_MARSH then
2213 if plotPercent.TERRAIN_SNOW + plotPercent.TERRAIN_TUNDRA + plotPercent.TERRAIN_DESERT >= 0.25 * randPercent then
2214 plot:SetFeatureType(FeatureTypes.NO_FEATURE, -1)
2215 end
2216 elseif plotTerrainID == TerrainTypes.TERRAIN_TUNDRA then
2217 if 2 * plotPercent.TERRAIN_GRASS + plotPercent.TERRAIN_PLAINS + plotPercent.TERRAIN_DESERT >= 0.5 * randPercent then
2218 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, true, true)
2219 end
2220 end
2221 end
2222 if plotTerrainID == TerrainTypes.TERRAIN_SNOW then
2223 local isMountain = plot:IsMountain()
2224 local warmCount = 2 * plotPercent.FEATURE_JUNGLE + 2 * plotPercent.FEATURE_MARSH + plotPercent.TERRAIN_GRASS + plotPercent.TERRAIN_DESERT + 0.5 * plotPercent.TERRAIN_PLAINS
2225 if warmCount >= 0.25 * randPercent then
2226 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS, true, true)
2227 elseif warmCount >= 0.10 * randPercent or plot:IsFreshWater() then
2228 plot:SetTerrainType(TerrainTypes.TERRAIN_TUNDRA, true, true)
2229 end
2230 if isMountain then
2231 plot:SetPlotType(PlotTypes.PLOT_MOUNTAIN, false, false)
2232 end
2233 end
2234 if plot:IsHills() then
2235 -- warm hills -> flat when surrounded by cold
2236 local nearCold = 0
2237 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
2238 if not nearPlot:IsWater() and not IsMountain(nearPlot) and GetTemperature(nearPlot) < GetTemperature(plot) then
2239 nearCold = nearCold + 1
2240 end
2241 end
2242 if nearCold > 1 and (nearCold * mg.hillsBlendPercent * 100) >= Map.Rand(100, "Blend hills - Lua") then
2243 plot:SetPlotType(PlotTypes.PLOT_LAND, false, false)
2244 end
2245 end
2246 end
2247 end
2248 if debugTime then print(string.format("%5s ms, BlendTerrain %s", math.floor(mountainCheckTime * 1000), "MountainCheckTime")) end
2249
2250 -- flat -> hills near mountain, and flat cold -> hills when surrounded by warm
2251 for plotID, plot in Plots(Shuffle) do
2252 if plot:GetPlotType() == PlotTypes.PLOT_LAND then
2253 local nearMountains = 0
2254 local nearWarm = 0
2255 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
2256 if not nearPlot:IsWater() then
2257 local nearTerrainID = nearPlot:GetTerrainType()
2258
2259 if IsMountain(nearPlot) then
2260 nearMountains = nearMountains + 1
2261 end
2262
2263 if GetTemperature(nearPlot) > GetTemperature(plot) then
2264 nearWarm = nearWarm + 1
2265 end
2266 end
2267 end
2268 if (nearMountains > 1 and (nearMountains * mg.hillsBlendPercent * 100) >= Map.Rand(100, "Blend mountains - Lua")) then
2269 --print("Turning flatland near mountain into hills")
2270 plot:SetPlotType(PlotTypes.PLOT_HILLS, false, false)
2271 --plot:SetTerrainType(TerrainTypes.TERRAIN_SNOW, false, false)
2272 elseif nearWarm * 0.5 * mg.hillsBlendPercent * 100 >= Map.Rand(100, "Blend hills - Lua") then
2273 plot:SetPlotType(PlotTypes.PLOT_HILLS, false, false)
2274 end
2275 end
2276 end
2277end
2278
2279function CreatePossibleMountainPass(plot)
2280 local x, y = plot:GetX(), plot:GetY()
2281 if not plot:IsMountain() then
2282 return
2283 end
2284
2285 local longestRoute = 0
2286 for dirA = 0, 3 do
2287 plotA = Map.PlotDirection(x, y, dirA)
2288 if plotA and (plotA:GetPlotType() == PlotTypes.PLOT_LAND or plotA:GetPlotType() == PlotTypes.PLOT_HILLS) then
2289 for dirB = dirA+2, 5 do
2290 local plotB = Map.PlotDirection(x, y, dirB)
2291 if plotB and (plotB:GetPlotType() == PlotTypes.PLOT_LAND or plotB:GetPlotType() == PlotTypes.PLOT_HILLS) then
2292 local isConnected = isPlotConnected(nil, plotA, plotB, "Land", true)
2293 if longestRoute < getRouteLength() then
2294 longestRoute = getRouteLength()
2295 end
2296 if (getRouteLength() == 0 or getRouteLength() > 15) then
2297 print(string.format("CreatePossibleMountainPass path distance = %2s - Change to Hills", getRouteLength()))
2298 plot:SetPlotType(PlotTypes.PLOT_HILLS, false, true)
2299 --plot:SetFeatureType(FeatureTypes.FEATURE_FALLOUT, -1) -- debug
2300
2301 table.insert(mg.MountainPasses, plot) -- still consider this a mountain tile for art purposes
2302 return
2303 end
2304 end
2305 end
2306 end
2307 end
2308 --print(string.format("CreatePossibleMountainPass longestRoute = %2s", longestRoute))
2309end
2310
2311
2312function CreateArcticOceans()
2313 local mapW, mapH = Map.GetGridSize()
2314 CreateOceanRift{y = 0, direction = mg.E, totalSize = 1, oceanSize = 1, fill = true}
2315 CreateOceanRift{y = mapH-1, direction = mg.E, totalSize = 1, oceanSize = 1, fill = true}
2316end
2317
2318function CreateVerticalOceans()
2319 local oOceanRifts = Map.GetCustomOption(7)
2320 local mapW, mapH = Map.GetGridSize()
2321 if oOceanRifts == 6 then
2322 -- No vertical rifts
2323 return
2324 end
2325
2326 log:Info("CreateVerticalOceans mg.oceanRiftWidth = %s", mg.oceanRiftWidth)
2327
2328
2329 function CreatePacific(midline)
2330 CreateOceanRift{x = midline, totalSize = mg.pacificSize, bulge = mg.pacificBulge, curve = mg.pacificCurve, oceanSize = math.max(1, Round(mg.oceanRiftWidth - 1))}
2331 end
2332 function CreateAtlantic(midline)
2333 CreateOceanRift{x = midline, totalSize = mg.atlanticSize, bulge = mg.atlanticBulge, curve = mg.atlanticCurve, oceanSize = mg.oceanRiftWidth, cleanMid = true}
2334 end
2335
2336 local landInColumn = {}
2337 for x = 0, mapW - 1 do
2338 landInColumn[x] = 0
2339 mg.elevationRect[x] = {}
2340 mg.elevationRect[x][-1] = 0
2341 for y = 0, mapH - 1 do
2342 mg.elevationRect[x][y] = 0
2343 end
2344 end
2345
2346 -- scan plots
2347 local totalLand = 0
2348 for x = 0, mapW - 1 do
2349 for y = 0, mapH - 1 do
2350 plot = Map.GetPlot(x, y)
2351
2352 if not Plot_IsWater(plot, true) then
2353 totalLand = totalLand + 1
2354 landInColumn[x] = landInColumn[x] + 1
2355 end
2356
2357 -- elevation in a size-wide rectangle
2358 local elevation = Plot_GetElevation(plot, false)
2359 for x2 = 0 - math.ceil(2 + mg.oceanRiftWidth), 1 + math.floor(2 + mg.oceanRiftWidth) do
2360 xOffset = (x + x2) % mapW
2361 mg.elevationRect[xOffset][-1] = mg.elevationRect[xOffset][-1] + elevation
2362 mg.elevationRect[xOffset][y] = mg.elevationRect[xOffset][y] + elevation
2363 end
2364 end
2365 end
2366
2367 -- find biggest ocean (usually Pacific)
2368 local lowestElevation = 0
2369 local startX = 0
2370 local startY = math.floor(mapH/2)
2371 for x = 0, mapW - 1 do
2372 local elevation = mg.elevationRect[xOffset][-1]
2373 if elevation < lowestElevation then
2374 lowestElevation = elevation
2375 startX = x
2376 end
2377 end
2378
2379 table.insert(mg.oceanRiftMidlines, startX)
2380 if mapW < 60 then
2381 log:Debug("CreateVerticalOceans: Creating Atlantic at x=%s", startX)
2382 CreateAtlantic(startX)
2383 return
2384 elseif oOceanRifts == 1 or oOceanRifts == 3 then
2385 -- PA or PP
2386 log:Debug("CreateVerticalOceans: Creating Pacific at x=%s", startX)
2387 CreatePacific(startX)
2388 elseif oOceanRifts == 2 then
2389 -- AA
2390 log:Debug("CreateVerticalOceans: Creating Atlantic at x=%s", startX)
2391 CreateAtlantic(startX)
2392 elseif oOceanRifts == 4 or oOceanRifts == 5 then
2393 -- 1 or 2 random
2394 if 50 >= Map.Rand(100, "Random ocean rift - Lua") then
2395 log:Debug("CreateVerticalOceans: Creating Pacific at x=%s (Random)", startX)
2396 CreatePacific(startX)
2397 else
2398 log:Debug("CreateVerticalOceans: Creating Atlantic at x=%s (Random)", startX)
2399 CreateAtlantic(startX)
2400 end
2401 end
2402
2403
2404 if oOceanRifts == 5 then
2405 -- Only one rift
2406 return
2407 end
2408
2409 -- find median land (usually place for Atlantic)
2410 local startX = 0
2411 local sumLand = 0
2412 local offsetAtlanticPercent = (0 == Map.Rand(2, "Atlantic Offset - Lua")) and mg.offsetAtlanticPercent or 1 - mg.offsetAtlanticPercent
2413 for x = 0, mapW - 1 do
2414 local xOffset = (x + startX) % mapW
2415 sumLand = sumLand + landInColumn[xOffset]
2416 if sumLand > offsetAtlanticPercent * totalLand then
2417 startX = xOffset
2418 break
2419 end
2420 end
2421
2422 log:Debug("totalLand=%4f sumElevation=%4f", totalLand, sumLand)
2423
2424 table.insert(mg.oceanRiftMidlines, startX)
2425 if oOceanRifts == 1 or oOceanRifts == 2 then
2426 -- PA or AA
2427 log:Debug("CreateVerticalOceans: Creating Atlantic at x=%s", startX)
2428 CreateAtlantic(startX)
2429 elseif oOceanRifts == 3 then
2430 -- PP
2431 log:Debug("CreateVerticalOceans: Creating Pacific at x=%s", startX)
2432 CreatePacific(startX)
2433 elseif oOceanRifts == 4 then
2434 -- 2 random
2435 if 50 >= Map.Rand(100, "Random ocean rift - Lua") then
2436 log:Debug("CreateVerticalOceans: Creating Pacific at x=%s (Random)", startX)
2437 CreatePacific(startX)
2438 else
2439 log:Debug("CreateVerticalOceans: Creating Atlantic at x=%s (Random)", startX)
2440 CreateAtlantic(startX)
2441 end
2442 end
2443end
2444
2445function CreateOceanRift(args)
2446 log:Debug("CreateOceanRift")
2447 for k, v in pairs(args) do
2448 log:Debug("%s = %s", k, v)
2449 end
2450 local x = args.x or 0
2451 local y = args.y or 0
2452 local midline = args.midline or x
2453 local direction = args.direction or mg.N
2454 local totalSize = args.totalSize or 3
2455 local oceanSize = args.oceanSize or mg.oceanRiftWidth
2456 local bulge = args.bulge or 0
2457 local curve = args.curve or 0
2458 local fill = args.fill
2459 local cleanMid = args.cleanMid
2460
2461 local plots = {}
2462 if bulge ~= 0 then
2463 -- see which curve direction fits the land better
2464 plots = GetRiftPlots(x-2, midline-2, y, direction, totalSize, oceanSize, bulge, curve)
2465 local plotsB = GetRiftPlots(x+2, midline+2, y, direction, totalSize, oceanSize, bulge, curve)
2466
2467 if GetMatchingPlots(plotsB) > GetMatchingPlots(plots) then
2468 plots = DeepCopy(plotsB)
2469 end
2470 elseif curve ~= 0 then
2471 -- see which curve direction fits the land better
2472 plots = GetRiftPlots(x-1, midline-1, y, direction, totalSize, oceanSize, bulge, curve)
2473 local plotsB = GetRiftPlots(x+1, midline+1, y, direction, totalSize, oceanSize, bulge, -1 * curve)
2474
2475 if GetMatchingPlots(plotsB) > GetMatchingPlots(plots) then
2476 plots = DeepCopy(plotsB)
2477 end
2478 else
2479 plots = GetRiftPlots(x, midline, y, direction, totalSize, oceanSize, bulge, curve)
2480 end
2481
2482 for plotID, v in pairs(plots) do
2483 log:Trace("oceanRiftPlots %s, %s, %s", v.plot:GetX(), v.plot:GetY(), v.strip)
2484 if (fill and v.strip <= 0) or IsBetween(0, v.strip, oceanSize) then
2485 mg.oceanRiftPlots[plotID] = {
2486 isWater = true,
2487 terrainID = TerrainTypes.TERRAIN_OCEAN
2488 }
2489 elseif v.strip == -1 or v.strip == oceanSize+1 then
2490 if (cleanMid and v.strip <= 0) then
2491 mg.oceanRiftPlots[plotID] = {
2492 isWater = true,
2493 terrainID = TerrainTypes.TERRAIN_OCEAN
2494 }
2495 elseif 50 >= Map.Rand(100, "Ocean rift ocean/coast - Lua") then
2496 mg.oceanRiftPlots[plotID] = {
2497 isWater = true,
2498 terrainID = TerrainTypes.TERRAIN_OCEAN
2499 }
2500 else
2501 mg.oceanRiftPlots[plotID] = mg.oceanRiftPlots[plotID] or {
2502 isWater = true,
2503 terrainID = TerrainTypes.TERRAIN_COAST
2504 }
2505 end
2506 elseif v.strip == -2 or v.strip == oceanSize+2 then
2507 if 50 >= Map.Rand(100, "Ocean rift coast/land - Lua") then
2508 if (cleanMid and v.strip <= 0) then
2509 mg.oceanRiftPlots[plotID] = {
2510 isWater = true,
2511 terrainID = TerrainTypes.TERRAIN_OCEAN
2512 }
2513 else
2514 mg.oceanRiftPlots[plotID] = mg.oceanRiftPlots[plotID] or {
2515 isWater = true,
2516 terrainID = TerrainTypes.TERRAIN_COAST
2517 }
2518 end
2519 else
2520 mg.oceanRiftPlots[plotID] = mg.oceanRiftPlots[plotID] or {
2521 isWater = false,
2522 terrainID = TerrainTypes.TERRAIN_COAST
2523 }
2524 end
2525 else
2526
2527 end
2528 end
2529
2530
2531 function GetMatchingPlots(plots)
2532 local nicePlots = 0
2533 for plotID, v in pairs(plots) do
2534 if (fill and v.strip <= 0) or IsBetween(0, v.strip, oceanSize) or v.strip == -1 or v.strip == oceanSize+1 then
2535 -- turns plots to water
2536 if Plot_IsWater(v.plot, true) then
2537 nicePlots = nicePlots + 1
2538 else
2539 nicePlots = nicePlots - 1
2540 end
2541 elseif v.strip == -2 or v.strip == oceanSize+2 then
2542 -- 50% chance turns to water
2543 if Plot_IsWater(v.plot, true) then
2544 nicePlots = nicePlots + 1 -- okay if already water
2545 else
2546 nicePlots = nicePlots - 0.5 -- destroys land half the time
2547 end
2548 else
2549 -- encourage land in center area
2550 if Plot_IsWater(v.plot, true) then
2551 nicePlots = nicePlots - 0.1
2552 else
2553 nicePlots = nicePlots + 0.1
2554 end
2555 end
2556 end
2557 return nicePlots
2558 end
2559end
2560
2561function GetRiftPlots(x, midline, y, direction, totalSize, oceanSize, bulge, curve)
2562 local mapW, mapH = Map.GetGridSize()
2563 local riftPlots = {}
2564
2565 log:Debug("x=%-3s, y=%-3s Creating %s ocean rift with midline %s curve %s on map size (%s, %s)", x, y, mg.directionNames[direction], midline, curve, mapW, mapH)
2566
2567 local nextDirA = 0
2568 local nextDirB = 0
2569
2570 local curveNormal = (50 >= Map.Rand(100, "Ocean Rift Curve - Lua"))
2571
2572 function GetMidX(y)
2573 if curve == 0 then
2574 return midline
2575 end
2576 log:Trace("%s * GetSinCurve(%s, %s) = %s", curve, y, mapH, Round(curve * GetSinCurve(y, mapH, 3)))
2577 return (midline + Round(curve * GetSinCurve(y, mapH, 3))) % mapW
2578 end
2579
2580 if direction == mg.N then
2581 nextDirA = mg.NE
2582 nextDirB = mg.NW
2583 elseif direction == mg.S then
2584 nextDirA = mg.SE
2585 nextDirB = mg.SW
2586 elseif direction == mg.E then
2587 nextDirA = mg.E
2588 nextDirB = mg.E
2589 elseif direction == mg.W then
2590 nextDirA = mg.W
2591 nextDirB = mg.W
2592 else
2593 print("Invalid direction %s for CreateOceanRift")
2594 return
2595 end
2596
2597
2598 plot = Map.GetPlot(x, y)
2599 local attempts = 0
2600 while plot and attempts < mapW do
2601 local foundNewPlots = false
2602 local plotID = Plot_GetID(plot)
2603 local radius = math.max(0, Round((totalSize-1)/2 + bulge/2 * GetBellCurve(y, mapH)))
2604 local extraRadius = oceanSize + 2
2605
2606 if direction == mg.E or direction == mg.W then
2607 log:Trace("x=%-3s, y=%-3s radius=%-3s extraRadius=%-3s", x, y, radius, extraRadius)
2608 else
2609 log:Trace("x=%-3s, y=%-3s radius=%-3s extraRadius=%-3s midX=%-3s", x, y, radius, extraRadius, GetMidX(y))
2610 end
2611
2612 for nearPlot, nearDistance in Plot_GetPlotsInCircle(plot, 0, radius + extraRadius) do
2613 if not mg.oceanRiftPlots[nearPlotID] then
2614 local nearPlotID = Plot_GetID(nearPlot)
2615 if not riftPlots[nearPlotID] then
2616 riftPlots[nearPlotID] = {plot=nearPlot, minDistance=999, strip=0}
2617 end
2618 if nearDistance < riftPlots[nearPlotID].minDistance then
2619 foundNewPlots = true
2620 riftPlots[nearPlotID].minDistance = nearDistance
2621 riftPlots[nearPlotID].strip = nearDistance - radius
2622 end
2623 --[[
2624 log:Trace("%s, %s, %s, %s, %s",
2625 nearPlot:GetX(),
2626 nearPlot:GetY(),
2627 nearDistance,
2628 riftPlots[nearPlotID].minDistance,
2629 riftPlots[nearPlotID].strip
2630 )
2631 --]]
2632 end
2633 end
2634
2635 local nextPlotA = Map.PlotDirection(x, y, nextDirA)
2636 local nextPlotB = Map.PlotDirection(x, y, nextDirB)
2637 if not nextPlotA or not nextPlotB then
2638 -- reached edge of map
2639 --print("End ocean rift")
2640 return riftPlots
2641 end
2642
2643
2644 local oddsA = 0.50
2645 if nextPlotA == nextPlotB then
2646 plot = nextPlotA
2647 else
2648 local distanceA = Map.PlotDistance(nextPlotA:GetX(), y, GetMidX(y), y)
2649 local distanceB = Map.PlotDistance(nextPlotB:GetX(), y, GetMidX(y), y)
2650 if distanceA < distanceB then
2651 oddsA = oddsA + (0.50 * distanceA / mg.oceanMaxWander)
2652 elseif distanceA > distanceB then
2653 oddsA = oddsA - (0.50 * distanceA / mg.oceanMaxWander)
2654 end
2655
2656 --[[
2657 local nextElevationA = mg.elevationRect[nextPlotA:GetX()][nextPlotA:GetY()]
2658 local nextElevationB = mg.elevationRect[nextPlotB:GetX()][nextPlotB:GetY()]
2659
2660 if nextElevationA < nextElevationB then
2661 oddsA = oddsA + mg.oceanElevationWeight
2662 elseif nextElevationA > nextElevationB then
2663 oddsA = oddsA - mg.oceanElevationWeight
2664 end
2665 --]]
2666
2667 local randomPercent = 0.5--PWRand()
2668 log:Trace("distance A=%s B=%s oddsA=%.2f rand=%.2f", distanceA, distanceB, Round(oddsA, 2), Round(randomPercent, 2))
2669 if oddsA >= randomPercent then
2670 plot = nextPlotA
2671 log:Trace("choose A")
2672 else
2673 plot = nextPlotB
2674 log:Trace("choose B")
2675 end
2676 end
2677
2678 x = plot:GetX()
2679 y = plot:GetY()
2680 attempts = attempts + 1
2681 end
2682 return riftPlots
2683end
2684
2685function SetOceanRiftElevations(elevationMap)
2686 --
2687 for plotID, data in pairs(mg.oceanRiftPlots) do
2688 if data.isWater then
2689 local plot = Map.GetPlotByIndex(plotID)
2690 elevationMap.data[elevationMap:GetIndex(plot:GetX(), plot:GetY())] = 0
2691 end
2692 end
2693 --]]
2694 return elevationMap
2695end
2696
2697function SetOceanRiftPlots()
2698 print("SetOceanRiftPlots")
2699 for plotID, data in pairs(mg.oceanRiftPlots) do
2700 local plot = Map.GetPlotByIndex(plotID)
2701 --print(string.format("oceanRiftPlots plotID=%-4s isWater=%-6s terrainID=%-3s", plotID, tostring(data.isWater), data.terrainID))
2702 if data.isWater then
2703 if not plot:IsWater() or data.terrainID == TerrainTypes.TERRAIN_OCEAN then
2704 plot:SetTerrainType(data.terrainID, true, true)
2705 end
2706 end
2707 end
2708 --
2709 for plotID, data in pairs(mg.oceanRiftPlots) do
2710 local plot = Map.GetPlotByIndex(plotID)
2711 if plot:GetTerrainType() == TerrainTypes.TERRAIN_COAST then
2712 local foundLand = false
2713 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 2) do
2714 if not Plot_IsWater(nearPlot) then
2715 foundLand = true
2716 break
2717 end
2718 end
2719 if not foundLand then
2720 plot:SetTerrainType(TerrainTypes.TERRAIN_OCEAN, true, true)
2721 end
2722 elseif plot:GetTerrainType() == TerrainTypes.TERRAIN_OCEAN then
2723 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 1) do
2724 if not Plot_IsWater(nearPlot) then
2725 plot:SetTerrainType(TerrainTypes.TERRAIN_COAST, true, true)
2726 break
2727 end
2728 end
2729 end
2730 end
2731 --]]
2732end
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743--
2744-- Generate Features
2745--
2746
2747function AddFeatures()
2748 print("Adding Features CommunitasMap")
2749 local mapW, mapH = Map.GetGridSize()
2750 Map.RecalculateAreas()
2751 RestoreLakes()
2752
2753 local timeStart = debugTime and os.clock() or 0
2754 local zeroTreesThreshold = rainfallMap:FindThresholdFromPercent(mg.zeroTreesPercent,false,true)
2755 local jungleThreshold = rainfallMap:FindThresholdFromPercent(mg.junglePercent,false,true)
2756
2757 for plotID, plot in Plots(Shuffle) do
2758 Plot_AddMainFeatures(plot, zeroTreesThreshold, jungleThreshold)
2759 end
2760
2761 local potentialForestPlots = {}
2762 for plotID, plot in Plots(Shuffle) do
2763 if not plot:IsWater() then
2764 PlacePossibleOasis(plot)
2765 if IsGoodExtraForestTile(plot) then
2766 table.insert(potentialForestPlots, plot)
2767 end
2768 else
2769 PlacePossibleIce(plot)
2770 end
2771 end
2772
2773 for _, plot in pairs(potentialForestPlots) do
2774 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST, -1)
2775 end
2776
2777 AddIsles()
2778 --ConnectTerraContinents()
2779
2780 if debugTime then print(string.format("%5s ms, AddFeatures %s", math.floor((os.clock() - timeStart) * 1000), "End")) end
2781end
2782
2783function Plot_AddMainFeatures(plot, zeroTreesThreshold, jungleThreshold)
2784 local x, y = plot:GetX(), plot:GetY()
2785 local i = elevationMap:GetIndex(x,y)
2786 local mapW, mapH = Map.GetGridSize()
2787 local plotTerrainID = plot:GetTerrainType()
2788 --local zeroTreesThreshold = rainfallMap:FindThresholdFromPercent(mg.zeroTreesPercent,false,true)
2789 --local jungleThreshold = rainfallMap:FindThresholdFromPercent(mg.junglePercent,false,true)
2790
2791 if plot:IsWater() or plot:IsMountain() then
2792 return
2793 end
2794
2795 -- Set desert rivers to floodplains
2796 if plot:CanHaveFeature(FeatureTypes.FEATURE_FLOOD_PLAINS) then
2797 plot:SetFeatureType(FeatureTypes.FEATURE_FLOOD_PLAINS,-1)
2798 return
2799 end
2800
2801 -- Micro-climates for tiny volcanic islands
2802 if not plot:IsMountain() and (plotTerrainID == TerrainTypes.TERRAIN_PLAINS or plotTerrainID == TerrainTypes.TERRAIN_GRASS) then
2803 local areaSize = plot:Area():GetNumTiles()
2804 if areaSize <= 5 and (6 - areaSize) >= Map.Rand(5, "Add Island Features - Lua") then
2805 local zone = elevationMap:GetZone(y)
2806 if zone == mg.NEQUATOR or zone == mg.SEQUATOR then
2807 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS,false,false)
2808 plot:SetFeatureType(FeatureTypes.FEATURE_JUNGLE,-1)
2809 return
2810 elseif zone == mg.NTEMPERATE or zone == mg.STEMPERATE then
2811 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST,-1)
2812 return
2813 end
2814 end
2815 end
2816
2817 -- Too dry for jungle
2818
2819 if rainfallMap.data[i] < jungleThreshold then
2820 local treeRange = jungleThreshold - zeroTreesThreshold
2821 if (rainfallMap.data[i] > PWRand() * treeRange + zeroTreesThreshold) and (temperatureMap.data[i] > mg.treesMinTemperature) then
2822 if IsGoodFeaturePlot(plot) then
2823 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST,-1)
2824 end
2825 end
2826 return
2827 end
2828
2829 -- Too cold for jungle
2830 if temperatureMap.data[i] < mg.jungleMinTemperature then
2831 if temperatureMap.data[i] > mg.treesMinTemperature and IsGoodFeaturePlot(plot) then
2832 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST,-1)
2833 end
2834 return
2835 end
2836
2837 -- Too near desert for jungle
2838 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
2839 if nearPlot:GetTerrainType() == TerrainTypes.TERRAIN_DESERT then
2840 return
2841 end
2842 end
2843
2844 -- This tile is tropical forest or jungle
2845 table.insert(mg.tropicalPlots, plot)
2846
2847 -- Check marsh
2848 if temperatureMap.data[i] > mg.treesMinTemperature and IsGoodFeaturePlot(plot, FeatureTypes.FEATURE_MARSH) then
2849 plot:SetPlotType(PlotTypes.PLOT_LAND,false,false)
2850 plot:SetFeatureType(FeatureTypes.FEATURE_MARSH,-1)
2851 return
2852 end
2853
2854 --[[
2855 if plot:IsHills() then
2856 --jungle on hill looks terrible in Civ5. Can't use it.
2857 plot:SetTerrainType(terrainGrass,false,false)
2858 if IsGoodFeaturePlot(plot) then
2859 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST,-1)
2860 end
2861 return
2862 end
2863 --]]
2864
2865 if IsGoodFeaturePlot(plot) then
2866 plot:SetFeatureType(FeatureTypes.FEATURE_JUNGLE,-1)
2867 if plot:IsHills() then
2868 plot:SetTerrainType(TerrainTypes.TERRAIN_GRASS,false,false)
2869 else
2870 plot:SetTerrainType(TerrainTypes.TERRAIN_PLAINS,false,false)
2871 end
2872 else
2873 plot:SetTerrainType(TerrainTypes.TERRAIN_GRASS,false,false)
2874 end
2875end
2876
2877function IsGoodFeaturePlot(plot, featureID)
2878 local odds = 0
2879
2880 if featureID == FeatureTypes.FEATURE_MARSH then
2881 if plot:GetPlotType() ~= PlotTypes.PLOT_LAND then
2882 return false
2883 end
2884 for nearPlot in Plot_GetPlotsInCircle(plot, 0, 1) do
2885 local nearTerrainID = nearPlot:GetTerrainType()
2886 if nearPlot:GetPlotType() == PlotTypes.PLOT_OCEAN then
2887 if plot:IsRiverSide() then
2888 odds = odds + 2 * mg.marshPercent
2889 end
2890 if nearPlot:IsLake() then
2891 odds = odds + 4 * mg.marshPercent
2892 else
2893 odds = odds + mg.marshPercent
2894 end
2895 elseif nearTerrainID == TerrainTypes.TERRAIN_DESERT or nearTerrainID == TerrainTypes.TERRAIN_SNOW or nearTerrainID == TerrainTypes.TERRAIN_TUNDRA then
2896 return 0
2897 elseif nearPlot:GetFeatureType() == FeatureTypes.FEATURE_MARSH then
2898 odds = odds - 8 * mg.marshPercent -- avoid clumping
2899 elseif plot:IsRiverSide() and nearPlot:IsFreshWater() then
2900 odds = odds + mg.marshPercent
2901 end
2902 end
2903 return math.min(odds, mg.featurePercent) >= PWRand()
2904 end
2905
2906 odds = mg.featurePercent
2907 if plot:IsFreshWater() then
2908 odds = odds + mg.featureWetVariance
2909 else
2910 odds = odds - mg.featureWetVariance
2911 end
2912
2913 return odds >= PWRand()
2914end
2915
2916function IsBarrenDesert(plot)
2917 return plot:GetTerrainType() == TerrainTypes.TERRAIN_DESERT and not plot:IsMountain() and plot:GetFeatureType() == FeatureTypes.NO_FEATURE
2918end
2919
2920function PlacePossibleOasis(plot)
2921 local x, y = plot:GetX(), plot:GetY()
2922 if not plot:CanHaveFeature(FeatureTypes.FEATURE_OASIS) then
2923 return
2924 end
2925
2926 local odds = 0
2927 for nearPlot, distance in Plot_GetPlotsInCircle(plot, 3) do
2928 local distance = distance or 1
2929 local featureID = nearPlot:GetFeatureType()
2930
2931 if featureID == FeatureTypes.FEATURE_OASIS then
2932 if distance <= 2 then
2933 -- at least 2 tile spacing between oases
2934 return
2935
2936 end
2937 odds = odds - 200 / distance
2938 end
2939
2940 if featureID == FeatureTypes.NO_FEATURE and not nearPlot:IsFreshWater() then
2941 if nearPlot:GetTerrainType() == TerrainTypes.TERRAIN_DESERT then
2942 odds = odds + 10 / distance
2943 elseif IsMountain(nearPlot) or nearPlot:IsHills() then
2944 odds = odds + 5 / distance
2945 elseif nearPlot:GetTerrainType() == TerrainTypes.TERRAIN_PLAINS then
2946 odds = odds + 2 / distance
2947 end
2948 else
2949 odds = odds - 5 / distance
2950 end
2951 end
2952
2953 if odds >= Map.Rand(100, "PlacePossibleOasis - Lua") then
2954 plot:SetFeatureType(FeatureTypes.FEATURE_OASIS, -1)
2955 end
2956end
2957
2958function IsGoodExtraForestTile(plot)
2959 local x, y = plot:GetX(), plot:GetY()
2960 local odds = mg.forestRandomPercent
2961 local terrainID = plot:GetTerrainType()
2962 local resID = plot:GetResourceType()
2963 if not plot:CanHaveFeature(FeatureTypes.FEATURE_FOREST) then
2964 return false
2965 end
2966
2967 if terrainID == TerrainTypes.TERRAIN_TUNDRA then
2968 if resID ~= -1 then
2969 return true
2970 end
2971 if plot:IsFreshWater() then
2972 odds = odds + mg.featureWetVariance
2973 end
2974 end
2975
2976 -- Avoid filling flat holes of tropical areas, which are too dense already
2977 if not plot:IsHills() and Contains(mg.tropicalPlots, plot) then
2978 odds = odds - 0.30
2979 end
2980
2981 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 1) do
2982 local nearTerrainID = nearPlot:GetTerrainType()
2983 local nearFeatureID = nearPlot:GetFeatureType()
2984
2985 if nearPlot:IsMountain() then
2986 -- do nothing
2987 elseif nearPlot:IsHills() then
2988 -- Region already has enough production and rough terrain
2989 odds = odds - 0.10
2990 elseif nearTerrainID == TerrainTypes.TERRAIN_SNOW then
2991 -- Help extreme polar regions
2992 odds = odds + 0.2
2993 elseif nearTerrainID == TerrainTypes.TERRAIN_TUNDRA then
2994 odds = odds + 0.1
2995 elseif terrainID == TerrainTypes.TERRAIN_TUNDRA and Plot_IsWater(nearPlot) then
2996 odds = odds + 0.1
2997 end
2998
2999 -- Avoid tropics
3000 if Contains(mg.tropicalPlots, nearPlot) then
3001 odds = odds - 0.10
3002 end
3003
3004 -- Too dry
3005 if nearTerrainID == TerrainTypes.TERRAIN_DESERT then
3006 odds = odds - 0.20
3007 end
3008 end
3009
3010 if 100 * mg.featurePercent * math.min(1, odds) >= Map.Rand(100, "Add Extra Forest - Lua") then
3011 --plot:SetFeatureType(FeatureTypes.FEATURE_FOREST, -1)
3012 return true
3013 end
3014
3015 return false
3016end
3017
3018function PlacePossibleIce(plot)
3019 local mapW, mapH = Map.GetGridSize()
3020 local x, y = plot:GetX(), plot:GetY()
3021 if not plot:IsWater() then
3022 return
3023 end
3024
3025 local latitude = math.abs(temperatureMap:GetLatitudeForY(y))
3026 local lowestIce = mg.iceLatitude
3027 local odds = 100
3028
3029 if 0 < y and y < mapH - 1 then
3030 odds = 100 * (latitude - lowestIce) / (mg.topLatitude - lowestIce)
3031 end
3032
3033 --if not Cep then
3034 local nearLand = false
3035 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
3036 if not nearPlot:IsWater() then
3037 --odds * 0.5
3038 return
3039 end
3040 end
3041 --end
3042
3043 if odds >= Map.Rand(100, "PlacePossibleIce - Lua") then
3044 plot:SetFeatureType(FeatureTypes.FEATURE_ICE,-1)
3045 --[[
3046 if y < 5 then
3047 log:Debug("Ice at y=%s odds=%s latitude=%s lowestIce=%s topLatitude=%s",
3048 y,
3049 odds,
3050 latitude,
3051 lowestIce,
3052 mg.topLatitude
3053 )
3054 end
3055 --]]
3056 --[[
3057 for nearPlot in Plot_GetPlotsInCircle(plot, 1) do
3058 if nearPlot:GetTerrainType() == TerrainTypes.TERRAIN_GRASS then
3059 nearPlot:SetTerrainType(TerrainTypes.TERRAIN_TUNDRA, false, true)
3060 if nearPlot:GetFeatureType() == FeatureTypes.FEATURE_MARSH then
3061 nearPlot:SetFeatureType(FeatureTypes.NO_FEATURE, -1)
3062 end
3063 end
3064 end
3065 --]]
3066 end
3067end
3068
3069function AddIsles()
3070 -- This function added Feb 2011 by Bob Thomas.
3071 -- Adds the new feature Isles in to the game, for oceanic maps.
3072 local iW, iH = Map.GetGridSize()
3073 local biggest_ocean = Map.FindBiggestArea(true)
3074 local iNumBiggestOceanPlots = 0
3075
3076 if biggest_ocean ~= nil then
3077 iNumBiggestOceanPlots = biggest_ocean:GetNumTiles()
3078 end
3079 if iNumBiggestOceanPlots <= (iW * iH) / 4 then -- No major oceans on this world.
3080 return
3081 end
3082
3083 -- World has oceans, proceed with adding Isles.
3084 local iNumIslesPlaced = 0
3085 local direction_types = {
3086 mg.NE,
3087 mg.E,
3088 mg.SE,
3089 mg.SW,
3090 mg.W,
3091 mg.NW
3092 }
3093 local numCoast = 0
3094 local coastID = GameInfo.Terrains.TERRAIN_COAST.ID
3095 for i, plot in Plots() do
3096 if plot:GetTerrainType() == coastID then
3097 numCoast = numCoast + 1
3098 end
3099 end
3100 local isle_target = numCoast * mg.islePercent
3101 local variance = 25
3102 variance = isle_target * (Map.Rand(2 * variance, "Number of Isles to place - LUA") - variance) / 100
3103 local isle_number = math.floor(isle_target + variance)
3104 local isleInfo = GameInfo.Features.FEATURE_ATOLL
3105 local isleID = isleInfo.ID
3106
3107 -- Generate candidate plot lists.
3108 local temp_one_tile_island_list, temp_alpha_list, temp_beta_list = {}, {}, {}
3109 local temp_gamma_list, temp_delta_list, temp_epsilon_list = {}, {}, {}
3110 for y = 0, iH - 1 do
3111 for x = 0, iW - 1 do
3112 local i = y * iW + x + 1 -- Lua tables/lists/arrays start at 1, not 0 like C++ or Python
3113 local plot = Map.GetPlot(x, y)
3114 local plotType = plot:GetPlotType()
3115 local featureType = plot:GetFeatureType()
3116 local terrainType = plot:GetTerrainType()
3117 if terrainType == TerrainTypes.TERRAIN_COAST and featureType ~= FeatureTypes.FEATURE_ICE and not plot:IsLake() then
3118 -- Check all near plots and identify near landmasses.
3119 local iNumLandAdjacent, biggest_adj_area = 0, 0
3120 local bPlotValid = true
3121 for loop, direction in ipairs(direction_types) do
3122 local nearPlot = Map.PlotDirection(x, y, direction)
3123 if nearPlot ~= nil then
3124 local nearPlotType = nearPlot:GetPlotType()
3125 if not islesNearIce then
3126 if nearPlot:GetFeatureType() == FeatureTypes.FEATURE_ICE then
3127 bPlotValid = false
3128 elseif nearPlot:GetTerrainType() == TerrainTypes.TERRAIN_SNOW then
3129 bPlotValid = false
3130 elseif nearPlot:GetTerrainType() == TerrainTypes.TERRAIN_TUNDRA then
3131 bPlotValid = false
3132 end
3133 end
3134 if nearPlotType ~= PlotTypes.PLOT_OCEAN then -- Found land.
3135 iNumLandAdjacent = iNumLandAdjacent + 1
3136 if nearPlotType == PlotTypes.PLOT_LAND or nearPlotType == PlotTypes.PLOT_HILLS then
3137 local iArea = nearPlot:GetArea()
3138 local adjArea = Map.GetArea(iArea)
3139 local iNumAreaPlots = adjArea:GetNumTiles()
3140 if iNumAreaPlots > biggest_adj_area then
3141 biggest_adj_area = iNumAreaPlots
3142 end
3143 end
3144 end
3145 end
3146 end
3147 -- Only plots with a single land plot near can be eligible.
3148 if bPlotValid and iNumLandAdjacent <= 1 then
3149 --if biggest_adj_area >= 152 then
3150 -- discard this site
3151 if biggest_adj_area >= 82 then
3152 table.insert(temp_epsilon_list, i)
3153 elseif biggest_adj_area >= 34 then
3154 table.insert(temp_delta_list, i)
3155 elseif biggest_adj_area >= 16 or biggest_adj_area == 0 then
3156 table.insert(temp_gamma_list, i)
3157 elseif biggest_adj_area >= 6 then
3158 table.insert(temp_beta_list, i)
3159 elseif biggest_adj_area >= 1 then
3160 table.insert(temp_alpha_list, i)
3161 else -- Unexpected result
3162 --print("** Area Plot Count =", biggest_adj_area)
3163 end
3164 end
3165 end
3166 end
3167 end
3168 local alpha_list = GetShuffledCopyOfTable(temp_alpha_list)
3169 local beta_list = GetShuffledCopyOfTable(temp_beta_list)
3170 local gamma_list = GetShuffledCopyOfTable(temp_gamma_list)
3171 local delta_list = GetShuffledCopyOfTable(temp_delta_list)
3172 local epsilon_list = GetShuffledCopyOfTable(temp_epsilon_list)
3173
3174 -- Determine maximum number able to be placed, per candidate category.
3175 local max_alpha = math.ceil(table.maxn(alpha_list) )--* .25)
3176 local max_beta = math.ceil(table.maxn(beta_list) )--* .2)
3177 local max_gamma = math.ceil(table.maxn(gamma_list) )--* .25)
3178 local max_delta = math.ceil(table.maxn(delta_list) )--* .3)
3179 local max_epsilon = math.ceil(table.maxn(epsilon_list) )--* .4)
3180
3181 -- Place Isles.
3182 local plotIndex
3183 local i_alpha, i_beta, i_gamma, i_delta, i_epsilon = 1, 1, 1, 1, 1
3184 local passNum = 0
3185
3186 while (iNumIslesPlaced < isle_number) and (passNum < isle_number * 5) do
3187 local able_to_proceed = true
3188 local diceroll = 1 + Map.Rand(100, "Isle Placement Type - LUA")
3189 if diceroll <= 40 and max_alpha > 0 then
3190 plotIndex = alpha_list[i_alpha]
3191 i_alpha = i_alpha + 1
3192 max_alpha = max_alpha - 1
3193 --print("- Alpha site chosen")
3194 elseif diceroll <= 65 then
3195 if max_beta > 0 then
3196 plotIndex = beta_list[i_beta]
3197 i_beta = i_beta + 1
3198 max_beta = max_beta - 1
3199 --print("- Beta site chosen")
3200 elseif max_alpha > 0 then
3201 plotIndex = alpha_list[i_alpha]
3202 i_alpha = i_alpha + 1
3203 max_alpha = max_alpha - 1
3204 --print("- Alpha site chosen")
3205 else -- Unable to place this Isle
3206 --print("-") print("* Isle #", loop, "was unable to be placed.")
3207 able_to_proceed = false
3208 end
3209 elseif diceroll <= 80 then
3210 if max_gamma > 0 then
3211 plotIndex = gamma_list[i_gamma]
3212 i_gamma = i_gamma + 1
3213 max_gamma = max_gamma - 1
3214 --print("- Gamma site chosen")
3215 elseif max_beta > 0 then
3216 plotIndex = beta_list[i_beta]
3217 i_beta = i_beta + 1
3218 max_beta = max_beta - 1
3219 --print("- Beta site chosen")
3220 elseif max_alpha > 0 then
3221 plotIndex = alpha_list[i_alpha]
3222 i_alpha = i_alpha + 1
3223 max_alpha = max_alpha - 1
3224 --print("- Alpha site chosen")
3225 else -- Unable to place this Isle
3226 --print("-") print("* Isle #", loop, "was unable to be placed.")
3227 able_to_proceed = false
3228 end
3229 elseif diceroll <= 90 then
3230 if max_delta > 0 then
3231 plotIndex = delta_list[i_delta]
3232 i_delta = i_delta + 1
3233 max_delta = max_delta - 1
3234 --print("- Delta site chosen")
3235 elseif max_gamma > 0 then
3236 plotIndex = gamma_list[i_gamma]
3237 i_gamma = i_gamma + 1
3238 max_gamma = max_gamma - 1
3239 --print("- Gamma site chosen")
3240 elseif max_beta > 0 then
3241 plotIndex = beta_list[i_beta]
3242 i_beta = i_beta + 1
3243 max_beta = max_beta - 1
3244 --print("- Beta site chosen")
3245 elseif max_alpha > 0 then
3246 plotIndex = alpha_list[i_alpha]
3247 i_alpha = i_alpha + 1
3248 max_alpha = max_alpha - 1
3249 --print("- Alpha site chosen")
3250 else -- Unable to place this Isle
3251 --print("-") print("* Isle #", loop, "was unable to be placed.")
3252 able_to_proceed = false
3253 end
3254 else
3255 if max_epsilon > 0 then
3256 plotIndex = epsilon_list[i_epsilon]
3257 i_epsilon = i_epsilon + 1
3258 max_epsilon = max_epsilon - 1
3259 --print("- Epsilon site chosen")
3260 elseif max_delta > 0 then
3261 plotIndex = delta_list[i_delta]
3262 i_delta = i_delta + 1
3263 max_delta = max_delta - 1
3264 --print("- Delta site chosen")
3265 elseif max_gamma > 0 then
3266 plotIndex = gamma_list[i_gamma]
3267 i_gamma = i_gamma + 1
3268 max_gamma = max_gamma - 1
3269 --print("- Gamma site chosen")
3270 elseif max_beta > 0 then
3271 plotIndex = beta_list[i_beta]
3272 --print("- Beta site chosen")
3273 i_beta = i_beta + 1
3274 max_beta = max_beta - 1
3275 elseif max_alpha > 0 then
3276 plotIndex = alpha_list[i_alpha]
3277 i_alpha = i_alpha + 1
3278 max_alpha = max_alpha - 1
3279 --print("- Alpha site chosen")
3280 else -- Unable to place this Isle
3281 --print("-") print("* Isle #", loop, "was unable to be placed.")
3282 able_to_proceed = false
3283 end
3284 end
3285 if able_to_proceed and plotIndex then
3286 local x = (plotIndex - 1) % iW
3287 local y = (plotIndex - x - 1) / iW
3288 local plot = Map.GetPlot(x, y)
3289 for _, direction in ipairs(direction_types) do
3290 local nearPlot = Map.PlotDirection(x, y, direction)
3291 if nearPlot and nearPlot:GetFeatureType() == isleID then
3292 able_to_proceed = false
3293 --print("Adjacent isle")
3294 break
3295 end
3296 end
3297 if able_to_proceed then
3298 plot:SetFeatureType(isleID, -1)
3299 iNumIslesPlaced = iNumIslesPlaced + 1
3300 end
3301 end
3302 passNum = passNum + 1
3303 end
3304
3305 -- Debug report
3306 print("-")
3307 print("- Isle Target Number: ", isle_number)
3308 print("- Number of Isles placed: ", iNumIslesPlaced)
3309 print("- Attempts: ", passNum)
3310 print("-")
3311 print("- Isles placed in Alpha locations : ", i_alpha - 1)
3312 print("- Isles placed in Beta locations : ", i_beta - 1)
3313 print("- Isles placed in Gamma locations : ", i_gamma - 1)
3314 print("- Isles placed in Delta locations : ", i_delta - 1)
3315 print("- Isles placed in Epsilon locations : ", i_epsilon - 1)
3316 --]]
3317end
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331--
3332-- Other Generators
3333
3334DiffMap = inheritsFrom(FloatMap)
3335function GenerateDiffMap(width,height,xWrap,yWrap)
3336 DiffMap = FloatMap:New(width,height,xWrap,yWrap)
3337 local i = 0
3338 for y = 0, height - 1,1 do
3339 for x = 0,width - 1,1 do
3340 if elevationMap:IsBelowSeaLevel(x,y) then
3341 DiffMap.data[i] = 0.0
3342 else
3343 DiffMap.data[i] = GetDifferenceAroundHex(x,y)
3344 end
3345 i=i+1
3346 end
3347 end
3348
3349 DiffMap:Normalize()
3350 i = 0
3351 for y = 0, height - 1,1 do
3352 for x = 0,width - 1,1 do
3353 if elevationMap:IsBelowSeaLevel(x,y) then
3354 DiffMap.data[i] = 0.0
3355 else
3356 DiffMap.data[i] = DiffMap.data[i] + elevationMap.data[i] * 1.1
3357 end
3358 i=i+1
3359 end
3360 end
3361
3362 DiffMap:Normalize()
3363 return DiffMap
3364end
3365
3366function GenerateTwistedPerlinMap(width, height, xWrap, yWrap,minFreq,maxFreq,varFreq)
3367 local inputNoise = FloatMap:New(width,height,xWrap,yWrap)
3368 inputNoise:GenerateNoise()
3369 inputNoise:Normalize()
3370
3371 local freqMap = FloatMap:New(width,height,xWrap,yWrap)
3372 for y = 0, freqMap.height - 1 do
3373 for x = 0,freqMap.width - 1 do
3374 local i = freqMap:GetIndex(x,y)
3375 local odd = y % 2
3376 local xx = x + odd * 0.5
3377 freqMap.data[i] = GetPerlinNoise(xx,y * mg.YtoXRatio,freqMap.width,freqMap.height * mg.YtoXRatio,varFreq,1.0,0.1,8,inputNoise)
3378 end
3379 end
3380 freqMap:Normalize()
3381-- freqMap:Save("freqMap.csv")
3382
3383 local twistMap = FloatMap:New(width,height,xWrap,yWrap)
3384 for y = 0, twistMap.height - 1 do
3385 for x = 0,twistMap.width - 1 do
3386 local i = twistMap:GetIndex(x,y)
3387 local freq = freqMap.data[i] * (maxFreq - minFreq) + minFreq
3388 local mid = (maxFreq - minFreq)/2 + minFreq
3389 local coordScale = freq/mid
3390 local offset = (1.0 - coordScale)/mid
3391 --print("1-coordscale = " .. (1.0 - coordScale) .. ", offset = " .. offset)
3392 local ampChange = 0.85 - freqMap.data[i] * 0.5
3393 local odd = y % 2
3394 local xx = x + odd * 0.5
3395 twistMap.data[i] = GetPerlinNoise(xx + offset,(y + offset) * mg.YtoXRatio,twistMap.width,twistMap.height * mg.YtoXRatio,mid,1.0,ampChange,8,inputNoise)
3396 end
3397 end
3398
3399 twistMap:Normalize()
3400 --twistMap:Save("twistMap.csv")
3401 return twistMap
3402end
3403
3404function GenerateMountainMap(width,height,xWrap,yWrap,initFreq)
3405 local timeStart = debugTime and os.clock() or 0
3406
3407 local inputNoise = FloatMap:New(width,height,xWrap,yWrap)
3408 inputNoise:GenerateBinaryNoise()
3409 inputNoise:Normalize()
3410 local inputNoise2 = FloatMap:New(width,height,xWrap,yWrap)
3411 inputNoise2:GenerateNoise()
3412 inputNoise2:Normalize()
3413
3414 local mountainMap = FloatMap:New(width,height,xWrap,yWrap)
3415 local stdDevMap = FloatMap:New(width,height,xWrap,yWrap)
3416 local noiseMap = FloatMap:New(width,height,xWrap,yWrap)
3417
3418 if debugTime then print(string.format("%5s ms, GenerateMountainMap %s", math.floor((os.clock() - timeStart) * 1000), "Start")) end
3419 if debugTime then timeStart = os.clock() end
3420 for y = 0, mountainMap.height - 1 do
3421 for x = 0,mountainMap.width - 1 do
3422 local i = mountainMap:GetIndex(x,y)
3423 local odd = y % 2
3424 local xx = x + odd * 0.5
3425 mountainMap.data[i] = GetPerlinNoise(xx,y * mg.YtoXRatio,mountainMap.width,mountainMap.height * mg.YtoXRatio,initFreq,1.0,0.4,8,inputNoise)
3426 noiseMap.data[i] = GetPerlinNoise(xx,y * mg.YtoXRatio,mountainMap.width,mountainMap.height * mg.YtoXRatio,initFreq,1.0,0.4,8,inputNoise2)
3427 stdDevMap.data[i] = mountainMap.data[i]
3428 end
3429 end
3430 if debugTime then print(string.format("%5s ms, GenerateMountainMap %s", math.floor((os.clock() - timeStart) * 1000), "A")) end
3431 if debugTime then timeStart = os.clock() end
3432 mountainMap:Normalize()
3433 if debugTime then print(string.format("%5s ms, GenerateMountainMap %s", math.floor((os.clock() - timeStart) * 1000), "B")) end
3434 if debugTime then timeStart = os.clock() end
3435 stdDevMap:Deviate(mg.elevationBlendRange)
3436 if debugTime then print(string.format("%5s ms, GenerateMountainMap %s", math.floor((os.clock() - timeStart) * 1000), "C")) end
3437 if debugTime then timeStart = os.clock() end
3438 stdDevMap:Normalize()
3439 --stdDevMap:Save("stdDevMap.csv")
3440 --mountainMap:Save("mountainCloud.csv")
3441 noiseMap:Normalize()
3442 --noiseMap:Save("noiseMap.csv")
3443
3444 if debugTime then print(string.format("%5s ms, GenerateMountainMap %s", math.floor((os.clock() - timeStart) * 1000), "D")) end
3445 if debugTime then timeStart = os.clock() end
3446
3447 local moundMap = FloatMap:New(width,height,xWrap,yWrap)
3448 for y = 0, mountainMap.height - 1 do
3449 for x = 0,mountainMap.width - 1 do
3450 local i = mountainMap:GetIndex(x,y)
3451 local val = mountainMap.data[i]
3452 moundMap.data[i] = (math.sin(val*math.pi*2-math.pi*0.5)*0.5+0.5) * GetAttenuationFactor(mountainMap,x,y)
3453 if val < 0.5 then
3454 val = val^1 * 4
3455 else
3456 val = (1 - val)^1 * 4
3457 end
3458 --mountainMap.data[i] = val
3459 mountainMap.data[i] = moundMap.data[i]
3460 end
3461 end
3462 mountainMap:Normalize()
3463 --mountainMap:Save("premountMap.csv")
3464 --moundMap:Save("moundMap.csv")
3465
3466 for y = 0, mountainMap.height - 1 do
3467 for x = 0,mountainMap.width - 1 do
3468 local i = mountainMap:GetIndex(x,y)
3469 local val = mountainMap.data[i]
3470 --mountainMap.data[i] = (math.sin(val * 2 * math.pi + math.pi * 0.5)^8 * val) + moundMap.data[i] * 2 + noiseMap.data[i] * 0.6
3471 mountainMap.data[i] = (math.sin(val * 3 * math.pi + math.pi * 0.5)^16 * val)^0.5
3472 if mountainMap.data[i] > 0.2 then
3473 mountainMap.data[i] = 1.0
3474 else
3475 mountainMap.data[i] = 0.0
3476 end
3477 end
3478 end
3479 --mountainMap:Save("premountMap.csv")
3480
3481 local stdDevThreshold = stdDevMap:FindThresholdFromPercent(mg.landPercent + 0.05,true,false)
3482 log:Debug("stdDevThreshold = %s", stdDevThreshold)
3483
3484 for y = 0, mountainMap.height - 1 do
3485 for x = 0,mountainMap.width - 1 do
3486 local i = mountainMap:GetIndex(x,y)
3487 local val = mountainMap.data[i]
3488 local dev = 2.0 * stdDevMap.data[i] - 2.0 * stdDevThreshold
3489 --mountainMap.data[i] = (math.sin(val * 2 * math.pi + math.pi * 0.5)^8 * val) + moundMap.data[i] * 2 + noiseMap.data[i] * 0.6
3490 mountainMap.data[i] = (val + moundMap.data[i]) * dev
3491 end
3492 end
3493
3494 mountainMap:Normalize()
3495 --mountainMap:Save("mountainMap.csv")
3496
3497 return mountainMap
3498end
3499
3500function GetAttenuationFactor(map,x,y)
3501 local southY = map.height * mg.southAttenuationRange
3502 local southRange = map.height * mg.southAttenuationRange
3503 local yAttenuation = 1.0
3504 if y < southY then
3505 yAttenuation = mg.southAttenuationFactor + (y/southRange) * (1.0 - mg.southAttenuationFactor)
3506 end
3507
3508 local northY = map.height - (map.height * mg.northAttenuationRange)
3509 local northRange = map.height * mg.northAttenuationRange
3510 if y > northY then
3511 yAttenuation = mg.northAttenuationFactor + ((map.height - y)/northRange) * (1.0 - mg.northAttenuationFactor)
3512 end
3513
3514 local eastY = map.width - (map.width * mg.eastAttenuationRange)
3515 local eastRange = map.width * mg.eastAttenuationRange
3516 local xAttenuation = 1.0
3517 if x > eastY then
3518 xAttenuation = mg.eastAttenuationFactor + ((map.width - x)/eastRange) * (1.0 - mg.eastAttenuationFactor)
3519 end
3520
3521 local westY = map.width * mg.westAttenuationRange
3522 local westRange = map.width * mg.westAttenuationRange
3523 if x < westY then
3524 xAttenuation = mg.westAttenuationFactor + (x/westRange) * (1.0 - mg.westAttenuationFactor)
3525 end
3526
3527 return yAttenuation * xAttenuation
3528end
3529
3530function GenerateElevationMap(width,height,xWrap,yWrap)
3531 local timeStart = debugTime and os.clock() or 0
3532 local landMinScatter = 128/width * mg.landMinScatter --0.02/128
3533 local landMaxScatter = 128/width * mg.landMaxScatter --0.12/128
3534 local coastScatter = 128/width * mg.coastScatter --0.042/128
3535 local mountainScatter = 128/width * mg.mountainScatter --0.05/128
3536 local twistMap = GenerateTwistedPerlinMap(width,height,xWrap,yWrap,landMinScatter,landMaxScatter,coastScatter)
3537
3538 if debugTime then timeStart = os.clock() end
3539 local mountainMap = GenerateMountainMap(width,height,xWrap,yWrap,mountainScatter)
3540 if debugTime then print(string.format("%5s ms, GenerateElevationMap %s", math.floor((os.clock() - timeStart) * 1000), "GenerateMountainMap")) end
3541
3542 if debugTime then timeStart = os.clock() end
3543 local elevationMap = ElevationMap:New(width,height,xWrap,yWrap)
3544 for y = 0,height - 1 do
3545 for x = 0,width - 1 do
3546 local i = elevationMap:GetIndex(x,y)
3547 local tVal = twistMap.data[i]
3548 tVal = (math.sin(tVal*math.pi-math.pi*0.5)*0.5+0.5)^0.25 --this formula adds a curve flattening the extremes
3549 elevationMap.data[i] = (tVal + ((mountainMap.data[i] * 2) - 1) * mg.mountainWeight)
3550 end
3551 end
3552
3553 if debugTime then timeStart = os.clock() end
3554
3555 elevationMap:Normalize()
3556
3557 --attentuation should not break normalization
3558 for y = 0,height - 1 do
3559 for x = 0,width - 1 do
3560 local i = elevationMap:GetIndex(x,y)
3561 local attenuationFactor = GetAttenuationFactor(elevationMap,x,y)
3562 elevationMap.data[i] = elevationMap.data[i] * attenuationFactor
3563 end
3564 end
3565
3566 if debugTime then timeStart = os.clock() end
3567
3568 elevationMap.seaLevelThreshold = elevationMap:FindThresholdFromPercent(mg.landPercent + 0.05,true,false)
3569 log:Debug("seaLevelThreshold = %s", elevationMap.seaLevelThreshold)
3570
3571 if debugTime then print(string.format("%5s ms, GenerateElevationMap %s", math.floor((os.clock() - timeStart) * 1000), "End")) end
3572 return elevationMap
3573end
3574
3575function GenerateTempMaps(elevationMap)
3576 local timeStart = debugTime and os.clock() or 0
3577 local aboveSeaLevelMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3578 for y = 0,elevationMap.height - 1 do
3579 for x = 0,elevationMap.width - 1 do
3580 local i = aboveSeaLevelMap:GetIndex(x,y)
3581 if elevationMap:IsBelowSeaLevel(x,y) then
3582 aboveSeaLevelMap.data[i] = 0.0
3583 else
3584 aboveSeaLevelMap.data[i] = elevationMap.data[i] - elevationMap.seaLevelThreshold
3585 end
3586 end
3587 end
3588 aboveSeaLevelMap:Normalize()
3589 --aboveSeaLevelMap:Save("aboveSeaLevelMap.csv")
3590
3591 local summerMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3592 local zenith = mg.tropicLatitudes
3593 local topTempLat = mg.topLatitude + zenith
3594 local bottomTempLat = mg.bottomLatitude
3595 local latRange = topTempLat - bottomTempLat
3596 for y = 0,elevationMap.height - 1 do
3597 for x = 0,elevationMap.width - 1 do
3598 local i = summerMap:GetIndex(x,y)
3599 local lat = summerMap:GetLatitudeForY(y)
3600 --print("y=" .. y ..",lat=" .. lat)
3601 local latPercent = (lat - bottomTempLat)/latRange
3602 --print("latPercent=" .. latPercent)
3603 local temp = (math.sin(latPercent * math.pi * 2 - math.pi * 0.5) * 0.5 + 0.5)
3604 if elevationMap:IsBelowSeaLevel(x,y) then
3605 temp = temp * mg.maxWaterTemp + mg.minWaterTemp
3606 end
3607 summerMap.data[i] = temp
3608 end
3609 end
3610 if debugTime then timeStart = os.clock() end
3611 summerMap:Smooth(math.min(mg.tempBlendMaxRange, math.floor(elevationMap.width/8)))
3612 if debugTime then print(string.format("%5s ms, GenerateTempMaps %s", math.floor((os.clock() - timeStart) * 1000), "Smooth")) end
3613 if debugTime then timeStart = os.clock() end
3614 summerMap:Normalize()
3615 local winterMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3616 zenith = -mg.tropicLatitudes
3617 topTempLat = mg.topLatitude
3618 bottomTempLat = mg.bottomLatitude + zenith
3619 latRange = topTempLat - bottomTempLat
3620 for y = 0,elevationMap.height - 1 do
3621 for x = 0,elevationMap.width - 1 do
3622 local i = winterMap:GetIndex(x,y)
3623 local lat = winterMap:GetLatitudeForY(y)
3624 local latPercent = (lat - bottomTempLat)/latRange
3625 local temp = math.sin(latPercent * math.pi * 2 - math.pi * 0.5) * 0.5 + 0.5
3626 if elevationMap:IsBelowSeaLevel(x,y) then
3627 temp = temp * mg.maxWaterTemp + mg.minWaterTemp
3628 end
3629 winterMap.data[i] = temp
3630 end
3631 end
3632 winterMap:Smooth(math.min(mg.tempBlendMaxRange, math.floor(elevationMap.width/8)))
3633 winterMap:Normalize()
3634
3635 local temperatureMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3636 for y = 0,elevationMap.height - 1 do
3637 for x = 0,elevationMap.width - 1 do
3638 local i = temperatureMap:GetIndex(x,y)
3639 temperatureMap.data[i] = (winterMap.data[i] + summerMap.data[i]) * (1.0 - aboveSeaLevelMap.data[i])
3640 end
3641 end
3642 temperatureMap:Normalize()
3643
3644 return summerMap,winterMap,temperatureMap
3645end
3646
3647function GenerateRainfallMap(elevationMap)
3648 local summerMap,winterMap,temperatureMap = GenerateTempMaps(elevationMap)
3649
3650 --summerMap:Save("summerMap.csv")
3651 --winterMap:Save("winterMap.csv")
3652 --temperatureMap:Save("temperatureMap.csv")
3653 local geoMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3654 for y = 0,elevationMap.height - 1 do
3655 for x = 0,elevationMap.width - 1 do
3656 local i = elevationMap:GetIndex(x,y)
3657 local lat = elevationMap:GetLatitudeForY(y)
3658 local pressure = elevationMap:GetGeostrophicPressure(lat)
3659 geoMap.data[i] = pressure
3660 end
3661 end
3662 geoMap:Normalize()
3663 --geoMap:Save("geoMap.csv")
3664
3665 local sortedSummerMap = {}
3666 local sortedWinterMap = {}
3667 for y = 0,elevationMap.height - 1 do
3668 for x = 0,elevationMap.width - 1 do
3669 local i = elevationMap:GetIndex(x,y)
3670 sortedSummerMap[i + 1] = {x,y,summerMap.data[i]}
3671 sortedWinterMap[i + 1] = {x,y,winterMap.data[i]}
3672 end
3673 end
3674 table.sort(sortedSummerMap, function (a,b) return a[3] < b[3] end)
3675 table.sort(sortedWinterMap, function (a,b) return a[3] < b[3] end)
3676
3677 local sortedGeoMap = {}
3678 local xStart = 0
3679 local xStop = 0
3680 local yStart = 0
3681 local yStop = 0
3682 local incX = 0
3683 local incY = 0
3684 local geoIndex = 1
3685 local str = ""
3686 for zone=0,5 do
3687 local topY = elevationMap:GetYFromZone(zone,true)
3688 local bottomY = elevationMap:GetYFromZone(zone,false)
3689 if not (topY == -1 and bottomY == -1) then
3690 if topY == -1 then
3691 topY = elevationMap.height - 1
3692 end
3693 if bottomY == -1 then
3694 bottomY = 0
3695 end
3696 --str = string.format("topY = %d, bottomY = %d",topY,bottomY)
3697 --print(str)
3698 local dir1,dir2 = elevationMap:GetGeostrophicWindDirections(zone)
3699 --str = string.format("zone = %d, dir1 = %d",zone,dir1)
3700 --print(str)
3701 if (dir1 == mg.SW) or (dir1 == mg.SE) then
3702 yStart = topY
3703 yStop = bottomY --- 1
3704 incY = -1
3705 else
3706 yStart = bottomY
3707 yStop = topY --+ 1
3708 incY = 1
3709 end
3710 if dir2 == mg.W then
3711 xStart = elevationMap.width - 1
3712 xStop = 0---1
3713 incX = -1
3714 else
3715 xStart = 0
3716 xStop = elevationMap.width
3717 incX = 1
3718 end
3719 --str = string.format("yStart = %d, yStop = %d, incY = %d",yStart,yStop,incY)
3720 --print(str)
3721 --str = string.format("xStart = %d, xStop = %d, incX = %d",xStart,xStop,incX)
3722 --print(str)
3723
3724 for y = yStart,yStop ,incY do
3725 --str = string.format("y = %d",y)
3726 --print(str)
3727 --each line should start on water to avoid vast areas without rain
3728 local xxStart = xStart
3729 local xxStop = xStop
3730 for xx = xStart,xStop - incX, incX do
3731 local i = elevationMap:GetIndex(xx,y)
3732 if elevationMap:IsBelowSeaLevel(xx,y) then
3733 xxStart = xx
3734 xxStop = xx + elevationMap.width * incX
3735 break
3736 end
3737 end
3738 for x = xxStart,xxStop - incX,incX do
3739 local i = elevationMap:GetIndex(x,y)
3740 sortedGeoMap[geoIndex] = {x,y,geoMap.data[i]}
3741 geoIndex = geoIndex + 1
3742 end
3743 end
3744 end
3745 end
3746-- table.sort(sortedGeoMap, function (a,b) return a[3] < b[3] end)
3747 --print(#sortedGeoMap)
3748 --print(#geoMap.data)
3749
3750 local rainfallSummerMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3751 local moistureMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3752 for i = 1,#sortedSummerMap do
3753 local x = sortedSummerMap[i][1]
3754 local y = sortedSummerMap[i][2]
3755 local pressure = sortedSummerMap[i][3]
3756 DistributeRain(x,y,elevationMap,temperatureMap,summerMap,rainfallSummerMap,moistureMap,false)
3757 end
3758
3759 local rainfallWinterMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3760 local moistureMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3761 for i = 1,#sortedWinterMap do
3762 local x = sortedWinterMap[i][1]
3763 local y = sortedWinterMap[i][2]
3764 local pressure = sortedWinterMap[i][3]
3765 DistributeRain(x,y,elevationMap,temperatureMap,winterMap,rainfallWinterMap,moistureMap,false)
3766 end
3767
3768 local rainfallGeostrophicMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3769 moistureMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3770
3771 --print("----------------------------------------------------------------------------------------")
3772 --print("--GEOSTROPHIC---------------------------------------------------------------------------")
3773 --print("----------------------------------------------------------------------------------------")
3774 for i = 1,#sortedGeoMap do
3775 local x = sortedGeoMap[i][1]
3776 local y = sortedGeoMap[i][2]
3777--~ if y == 35 or y == 40 then
3778--~ str = string.format("x = %d, y = %d",x,y)
3779--~ print(str)
3780--~ end
3781 DistributeRain(x,y,elevationMap,temperatureMap,geoMap,rainfallGeostrophicMap,moistureMap,true)
3782 end
3783 --zero below sea level for proper percent threshold finding
3784 for y = 0,elevationMap.height - 1 do
3785 for x = 0,elevationMap.width - 1 do
3786 local i = elevationMap:GetIndex(x,y)
3787 if elevationMap:IsBelowSeaLevel(x,y) then
3788 rainfallSummerMap.data[i] = 0.0
3789 rainfallWinterMap.data[i] = 0.0
3790 rainfallGeostrophicMap.data[i] = 0.0
3791 end
3792 end
3793 end
3794
3795 rainfallSummerMap:Normalize()
3796 --rainfallSummerMap:Save("rainFallSummerMap.csv")
3797 rainfallWinterMap:Normalize()
3798 --rainfallWinterMap:Save("rainFallWinterMap.csv")
3799 rainfallGeostrophicMap:Normalize()
3800 --rainfallGeostrophicMap:Save("rainfallGeostrophicMap.csv")
3801
3802 local rainfallMap = FloatMap:New(elevationMap.width,elevationMap.height,elevationMap.xWrap,elevationMap.yWrap)
3803 for y = 0,elevationMap.height - 1 do
3804 for x = 0,elevationMap.width - 1 do
3805 local i = elevationMap:GetIndex(x,y)
3806 rainfallMap.data[i] = rainfallSummerMap.data[i] + rainfallWinterMap.data[i] + (rainfallGeostrophicMap.data[i] * mg.geostrophicFactor)
3807 end
3808 end
3809 rainfallMap:Normalize()
3810
3811 return rainfallMap, temperatureMap
3812end
3813
3814function DistributeRain(x,y,elevationMap,temperatureMap,pressureMap,rainfallMap,moistureMap,boolGeostrophic)
3815
3816 local i = elevationMap:GetIndex(x,y)
3817 local upLiftSource = math.max(math.pow(pressureMap.data[i],mg.upLiftExponent),1.0 - temperatureMap.data[i])
3818 --local str = string.format("geo=%s,x=%d, y=%d, srcPressure uplift = %f, upliftSource = %f",tostring(boolGeostrophic),x,y,math.pow(pressureMap.data[i],mg.upLiftExponent),upLiftSource)
3819 --print(str)
3820 if elevationMap:IsBelowSeaLevel(x,y) then
3821 moistureMap.data[i] = math.max(moistureMap.data[i], temperatureMap.data[i])
3822 --print("water tile = true")
3823 end
3824 --if debugTime then print(string.format("moistureMap.data[i] = %f",moistureMap.data[i])) end
3825
3826 --make list of neighbors
3827 local nList = {}
3828 if boolGeostrophic then
3829 local zone = elevationMap:GetZone(y)
3830 local dir1,dir2 = elevationMap:GetGeostrophicWindDirections(zone)
3831 local x1,y1 = elevationMap:GetNeighbor(x,y,dir1)
3832 local ii = elevationMap:GetIndex(x1,y1)
3833 --neighbor must be on map and in same wind zone
3834 if ii >= 0 and (elevationMap:GetZone(y1) == elevationMap:GetZone(y)) then
3835 table.insert(nList,{x1,y1})
3836 end
3837 local x2,y2 = elevationMap:GetNeighbor(x,y,dir2)
3838 ii = elevationMap:GetIndex(x2,y2)
3839 if ii >= 0 then
3840 table.insert(nList,{x2,y2})
3841 end
3842 else
3843 for dir = 0, 5 do
3844 local xx,yy = elevationMap:GetNeighbor(x,y,dir)
3845 local ii = elevationMap:GetIndex(xx,yy)
3846 if ii >= 0 and pressureMap.data[i] <= pressureMap.data[ii] then
3847 table.insert(nList,{xx,yy})
3848 end
3849 end
3850 end
3851 if #nList == 0 or boolGeostrophic and #nList == 1 then
3852 local cost = moistureMap.data[i]
3853 rainfallMap.data[i] = cost
3854 return
3855 end
3856 local moisturePerNeighbor = moistureMap.data[i]/#nList
3857 --drop rain and pass moisture to neighbors
3858 for n = 1,#nList do
3859 local xx = nList[n][1]
3860 local yy = nList[n][2]
3861 local ii = elevationMap:GetIndex(xx,yy)
3862 local upLiftDest = math.max(math.pow(pressureMap.data[ii],mg.upLiftExponent),1.0 - temperatureMap.data[ii])
3863 local cost = GetRainCost(upLiftSource,upLiftDest)
3864 local bonus = 0.0
3865 if (elevationMap:GetZone(y) == mg.NPOLAR or elevationMap:GetZone(y) == mg.SPOLAR) then
3866 bonus = mg.polarRainBoost
3867 end
3868 if boolGeostrophic and #nList == 2 then
3869 if n == 1 then
3870 moisturePerNeighbor = (1.0 - mg.geostrophicLateralWindStrength) * moistureMap.data[i]
3871 else
3872 moisturePerNeighbor = mg.geostrophicLateralWindStrength * moistureMap.data[i]
3873 end
3874 end
3875 --if debugTime then print(string.format("---xx=%d, yy=%d, destPressure uplift = %f, upLiftDest = %f, cost = %f, moisturePerNeighbor = %f, bonus = %f",xx,yy,math.pow(pressureMap.data[ii],mg.upLiftExponent),upLiftDest,cost,moisturePerNeighbor,bonus)) end
3876 rainfallMap.data[i] = rainfallMap.data[i] + cost * moisturePerNeighbor + bonus
3877 --pass to neighbor.
3878 --if debugTime then print(string.format("---moistureMap.data[ii] = %f",moistureMap.data[ii])) end
3879 moistureMap.data[ii] = moistureMap.data[ii] + moisturePerNeighbor - (cost * moisturePerNeighbor)
3880 --if debugTime then print(string.format("---dropping %f rain",cost * moisturePerNeighbor + bonus)) end
3881 --if debugTime then print(string.format("---passing on %f moisture",moisturePerNeighbor - (cost * moisturePerNeighbor))) end
3882 end
3883
3884end
3885
3886function GetRainCost(upLiftSource,upLiftDest)
3887 local cost = mg.minimumRainCost
3888 cost = math.max(mg.minimumRainCost, cost + upLiftDest - upLiftSource)
3889 if cost < 0.0 then
3890 cost = 0.0
3891 end
3892 return cost
3893end
3894
3895function GetDifferenceAroundHex(x,y)
3896 local avg = elevationMap:GetAverageInHex(x,y,1)
3897 local i = elevationMap:GetIndex(x,y)
3898 return elevationMap.data[i] - avg
3899--~ local nList = elevationMap:GetRadiusAroundHex(x,y,1)
3900--~ local i = elevationMap:GetIndex(x,y)
3901--~ local biggestDiff = 0.0
3902--~ for n=1,#nList do
3903--~ local xx = nList[n][1]
3904--~ local yy = nList[n][2]
3905--~ local ii = elevationMap:GetIndex(xx,yy)
3906--~ local diff = nil
3907--~ if elevationMap:IsBelowSeaLevel(x,y) then
3908--~ diff = elevationMap.data[i] - elevationMap.seaLevelThreshold
3909--~ else
3910--~ diff = elevationMap.data[i] - elevationMap.data[ii]
3911--~ end
3912--~ if diff > biggestDiff then
3913--~ biggestDiff = diff
3914--~ end
3915--~ end
3916--~ if biggestDiff < 0.0 then
3917--~ biggestDiff = 0.0
3918--~ end
3919--~ return biggestDiff
3920end
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933--
3934-- Plot functions
3935--
3936
3937function Plot_GetID(plot)
3938 if not plot then
3939 error("plot:GetID plot=nil")
3940 return nil
3941 end
3942 local iW, iH = Map.GetGridSize()
3943 return plot:GetY() * iW + plot:GetX()
3944end
3945
3946function Plot_GetFertilityInRange(plot, range, yieldID)
3947 local value = 0
3948 for nearPlot, distance in Plot_GetPlotsInCircle(plot, range, yieldID) do
3949 value = value + Plot_GetFertility(nearPlot, yieldID) / math.max(1, distance)
3950 end
3951 return value
3952end
3953
3954function Plot_GetFertility(plot, yieldID, ignoreStrategics)
3955 if plot:IsImpassable() or plot:GetTerrainType() == TerrainTypes.TERRAIN_OCEAN then
3956 return 0
3957 end
3958
3959 local value = 0
3960 local featureID = plot:GetFeatureType()
3961 local terrainID = plot:GetTerrainType()
3962 local resID = plot:GetResourceType()
3963
3964 if yieldID then
3965 value = value + plot:CalculateYield(yieldID, true)
3966 else
3967 for _, yieldID in pairs(mg.basicYields) do
3968 value = value + plot:CalculateYield(yieldID, true)
3969 end
3970 end
3971
3972 if plot:IsFreshWater() then
3973 value = value + 0.25
3974 end
3975
3976 if plot:IsLake() then
3977 -- can't improve lakes
3978 value = value - 1
3979 end
3980
3981 if featureID == FeatureTypes.FEATURE_FOREST and terrainID ~= TerrainTypes.TERRAIN_TUNDRA then
3982 value = value + 0.5
3983 end
3984
3985 if resID == -1 then
3986 if featureID == -1 and terrainID == TerrainTypes.TERRAIN_COAST then
3987 -- can't do much with these tiles in BNW
3988 value = value - 0.75
3989 end
3990 else
3991 local resInfo = GameInfo.Resources[resID]
3992 value = value + 4 * resInfo.Happiness
3993 if resInfo.ResourceClassType == "RESOURCECLASS_RUSH" and not ignoreStrategics then
3994 value = value + math.ceil(5 * math.sqrt(plot:GetNumResource()))
3995 elseif resInfo.ResourceClassType == "RESOURCECLASS_BONUS" then
3996 value = value + 2
3997 end
3998 end
3999 --]]
4000 return value
4001end
4002----------------------------------------------------------------
4003function Plot_GetPlotsInCircle(plot, minR, maxR)
4004 if not plot then
4005 print("plot:GetPlotsInCircle plot=nil")
4006 return
4007 end
4008 if not maxR then
4009 maxR = minR
4010 minR = 1
4011 end
4012
4013 local mapW, mapH = Map.GetGridSize()
4014 local isWrapX = Map:IsWrapX()
4015 local isWrapY = Map:IsWrapY()
4016 local centerX = plot:GetX()
4017 local centerY = plot:GetY()
4018
4019 leftX = isWrapX and ((centerX-maxR) % mapW) or Constrain(0, centerX-maxR, mapW-1)
4020 rightX = isWrapX and ((centerX+maxR) % mapW) or Constrain(0, centerX+maxR, mapW-1)
4021 bottomY = isWrapY and ((centerY-maxR) % mapH) or Constrain(0, centerY-maxR, mapH-1)
4022 topY = isWrapY and ((centerY+maxR) % mapH) or Constrain(0, centerY+maxR, mapH-1)
4023
4024 local nearX = leftX
4025 local nearY = bottomY
4026 local stepX = 0
4027 local stepY = 0
4028 local rectW = rightX-leftX
4029 local rectH = topY-bottomY
4030
4031 if rectW < 0 then
4032 rectW = rectW + mapW
4033 end
4034
4035 if rectH < 0 then
4036 rectH = rectH + mapH
4037 end
4038
4039 local nextPlot = Map.GetPlot(nearX, nearY)
4040
4041 return function ()
4042 while (stepY < 1 + rectH) and nextPlot do
4043 while (stepX < 1 + rectW) and nextPlot do
4044 local plot = nextPlot
4045 local distance = Map.PlotDistance(nearX, nearY, centerX, centerY)
4046
4047 nearX = (nearX + 1) % mapW
4048 stepX = stepX + 1
4049 nextPlot = Map.GetPlot(nearX, nearY)
4050
4051 if IsBetween(minR, distance, maxR) then
4052 return plot, distance
4053 end
4054 end
4055 nearX = leftX
4056 nearY = (nearY + 1) % mapH
4057 stepX = 0
4058 stepY = stepY + 1
4059 nextPlot = Map.GetPlot(nearX, nearY)
4060 end
4061 end
4062end
4063
4064function Plot_GetPlotsInCircleFast(x, y, radius)
4065 -- assumes X wrap
4066
4067 local plotIDs = {}
4068 local W, H = Map.GetGridSize()
4069 local odd = y % 2
4070 local topY = radius
4071 local bottomY = radius
4072 local currentY = nil
4073 local len = 1+radius
4074 local i = (y % H) * W + (x % W)
4075
4076 --constrain the top of our circle to be on the map
4077 if y+radius > H-1 then
4078 for r=0,radius,1 do
4079 if y+r == H-1 then
4080 topY = r
4081 break
4082 end
4083 end
4084 end
4085
4086 --constrain the bottom of our circle to be on the map
4087 if y-radius < 0 then
4088 for r=0,radius,1 do
4089 if y-r == 0 then
4090 bottomY = r
4091 break
4092 end
4093 end
4094 end
4095
4096 --adjust starting length, apply the top and bottom limits, and correct odd for the starting point
4097 len = len+(radius-bottomY)
4098 currentY = y - bottomY
4099 topY = y + topY
4100 odd = (odd+bottomY)%2
4101
4102 --set starting point
4103 if x-(radius-bottomY) - math.floor((bottomY+odd)/2) < 0 then
4104 i = i - (W*bottomY) + (W-(radius-bottomY)) - math.floor((bottomY+odd)/2)
4105 x = x + (W-(radius-bottomY)) - math.floor((bottomY+odd)/2)
4106 -- print(string.format("i for (%d,%d) WOULD have been in outer space. x is (%d,%d) i is (%d)",xx,y,x,y-bottomY,i))
4107 else
4108 i = i - (W*bottomY) - (radius-bottomY) - math.floor((bottomY+odd)/2)
4109 x = x - (radius-bottomY) - math.floor((bottomY+odd)/2)
4110 end
4111
4112 --cycle through the plot indexes and add them to a table
4113 --local str = ""
4114 --local iters = 0
4115 while currentY <= topY do
4116 --insert the start value, scan left to right adding each index in the line to our table
4117 --str = str..i..","
4118 table.insert(plotIDs,i)
4119 local wrapped = false
4120 for n=1,len-1,1 do
4121 if x ~= (W-1) then
4122 i = i + 1
4123 x = x + 1
4124 else
4125 i = i-(W-1)
4126 x = 0
4127 wrapped = true
4128 end
4129 --str = str..i..","
4130 table.insert(plotIDs,i)
4131 end
4132 if currentY < y then
4133 --move i NW and increment the length to scan
4134 if not wrapped then
4135 i = i+W-len+odd
4136 x = x-len+odd
4137 else
4138 i = i+W+(W-len+odd)
4139 x = x+(W-len+odd)
4140 end
4141 len = len+1
4142 else
4143 --move i NE and decrement the length to scan
4144 if not wrapped then
4145 i = i+W-len+1+odd
4146 x = x-len+1+odd
4147 else
4148 i = i+W+(W-len+1+odd)
4149 x = x+(W-len+1+odd)
4150 end
4151 len = len-1
4152 end
4153 currentY = currentY+1
4154 odd = (odd+1)%2
4155 -- iters = iters+1
4156 -- if iters > 300 then
4157 -- print("infinite loop in GetCircle")
4158 -- break
4159 -- end
4160 end
4161 -- print(string.format("added "..str.." to table for circle starting at(%d,%d)",xx,y))
4162 return plotIDs
4163end
4164
4165local plotTypeName = {}-- -1="NO_PLOT"}
4166local terrainTypeName = {}-- -1="NO_TERRAIN"}
4167local featureTypeName = {}-- -1="NO_FEATURE"}
4168function Plot_GetCirclePercents(plot, minR, maxR)
4169 --[[ Plot_GetCirclePercents(centerPlot, minRadius, maxRadius) usage example:
4170
4171 plotPercent = Plot_GetCirclePercents(plot, 2, 2)
4172 if (plotPercent.PLOT_LAND + plotPercent.PLOT_HILLS) <= 0.25 then
4173 return
4174 end
4175 ]]
4176 for k, v in pairs(PlotTypes) do
4177 plotTypeName[v] = k
4178 end
4179 for itemInfo in GameInfo.Terrains() do
4180 terrainTypeName[itemInfo.ID] = itemInfo.Type
4181 end
4182 for itemInfo in GameInfo.Features() do
4183 featureTypeName[itemInfo.ID] = itemInfo.Type
4184 end
4185
4186 local weights = {TOTAL=0, SEA=0, NO_PLOT=0, NO_TERRAIN=0, NO_FEATURE=0}
4187
4188 for k, v in pairs(PlotTypes) do
4189 weights[k] = 0
4190 end
4191 for itemInfo in GameInfo.Terrains() do
4192 weights[itemInfo.Type] = 0
4193 end
4194 for itemInfo in GameInfo.Features() do
4195 weights[itemInfo.Type] = 0
4196 end
4197
4198 for nearPlot, distance in Plot_GetPlotsInCircle(plot, minR, maxR) do
4199 local nearWeight = (distance == 0) and 6 or (1/distance)
4200 local plotType = plotTypeName[nearPlot:GetPlotType()]
4201 local terrainType = terrainTypeName[nearPlot:GetTerrainType()]
4202 local featureType = featureTypeName[nearPlot:GetFeatureType()] or "NO_FEATURE"
4203
4204 weights.TOTAL = weights.TOTAL + nearWeight
4205 weights[plotType] = weights[plotType] + nearWeight
4206 weights[terrainType] = weights[terrainType] + nearWeight
4207 weights[featureType] = weights[featureType] + nearWeight
4208
4209 if plotType == "PLOT_OCEAN" then
4210 if not nearPlot:IsLake() and featureType ~= "FEATURE_ICE" then
4211 weights.SEA = weights.SEA + nearWeight
4212 end
4213 end
4214 end
4215
4216 if weights.TOTAL == 0 then
4217 print("plot:GetAreaWeights Total=0! x=%s y=%s", x, y)
4218 end
4219 for k, v in pairs(weights) do
4220 if k ~= "TOTAL" then
4221 weights[k] = weights[k] / weights.TOTAL
4222 end
4223 end
4224
4225 return weights
4226end
4227
4228function Plot_GetElevation(plot, ignoreSeas)
4229 if ignoreSeas and Contains(mg.seaPlots, plot) then
4230 -- try to preserve inland seas
4231 return elevationMap.seaLevelThreshold
4232 end
4233 return elevationMap.data[elevationMap:GetIndex(plot:GetX(), plot:GetY())]
4234end
4235
4236function GetElevationByPlotID(plotID)
4237 local elevation = elevationMap.data[plotID]
4238 if not elevation then
4239 log:Warn("GetElevationByPlotID elevationMap.data[%s] = %s", plotID, elevation)
4240 return 0
4241 end
4242 return elevation
4243end
4244
4245function Plot_IsWater(plot, useElevation, ignoreSeas)
4246 if useElevation then
4247 return elevationMap.data[elevationMap:GetIndex(plot:GetX(), plot:GetY())] < elevationMap.seaLevelThreshold
4248 end
4249 if ignoreSeas and Contains(mg.seaPlots, plot) then
4250 return false
4251 end
4252 return (plot:GetPlotType() == PlotTypes.PLOT_OCEAN) or Contains(mg.lakePlots, plot)
4253end
4254
4255function Plot_IsLake(plot)
4256 return plot:IsLake() or Contains(mg.lakePlots, plot)
4257end
4258
4259function waterMatch(x,y)
4260 return elevationMap:IsBelowSeaLevel(x,y)
4261end
4262
4263function landMatch(x,y)
4264 return not elevationMap:IsBelowSeaLevel(x,y)
4265end
4266
4267function oceanMatch(x,y)
4268 local plot = Map.GetPlot(x,y)
4269 if plot:GetPlotType() == PlotTypes.PLOT_OCEAN then
4270 return true
4271 end
4272 return false
4273end
4274
4275function jungleMatch(x,y)
4276 local plot = Map.GetPlot(x,y)
4277 if plot:GetFeatureType() == FeatureTypes.FEATURE_JUNGLE or Contains(mg.tropicalPlots, plot) then
4278 return true
4279 --include any mountains on the border as part of the desert.
4280 elseif (plot:GetFeatureType() == FeatureTypes.FEATURE_MARSH or plot:GetFeatureType() == FeatureTypes.FEATURE_FOREST) and plot:GetTerrainType() == TerrainTypes.TERRAIN_GRASS then
4281 local nList = elevationMap:GetRadiusAroundHex(x,y,1)
4282 for n=1,#nList do
4283 local xx = nList[n][1]
4284 local yy = nList[n][2]
4285 local ii = elevationMap:GetIndex(xx,yy)
4286 if 11 ~= -1 then
4287 local nPlot = Map.GetPlot(xx,yy)
4288 if nPlot:GetFeatureType() == FeatureTypes.FEATURE_JUNGLE then
4289 return true
4290 end
4291 end
4292 end
4293 end
4294 return false
4295end
4296
4297function desertMatch(x,y)
4298 local plot = Map.GetPlot(x,y)
4299 if plot:GetTerrainType() == TerrainTypes.TERRAIN_DESERT then
4300 return true
4301 --include any mountains on the border as part of the desert.
4302 elseif IsMountain(plot) then
4303 local nList = elevationMap:GetRadiusAroundHex(x,y,1)
4304 for n=1,#nList do
4305 local xx = nList[n][1]
4306 local yy = nList[n][2]
4307 local ii = elevationMap:GetIndex(xx,yy)
4308 if 11 ~= -1 then
4309 local nPlot = Map.GetPlot(xx,yy)
4310 if not IsMountain(nPlot) and nPlot:GetTerrainType() == TerrainTypes.TERRAIN_DESERT then
4311 return true
4312 end
4313 end
4314 end
4315 end
4316 return false
4317end
4318
4319function IsMountain(plot)
4320 return plot:IsMountain() or Contains(mg.MountainPasses, plot)
4321end
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334--
4335-- Utilities
4336--
4337---------------------------------------------------------------------
4338function Round(num, places)
4339 local mult = 10^(places or 0)
4340 return math.floor(num * mult + 0.5) / mult
4341end
4342
4343function ShuffleList(list)
4344 local len = #list
4345 for i=0,len - 1 do
4346 local k = PWRandint(0,len-1)
4347 list[i], list[k] = list[k], list[i]
4348 end
4349end
4350
4351function IsBetween(lower, mid, upper)
4352 return ((lower <= mid) and (mid <= upper))
4353end
4354
4355function Contains(list, value)
4356 for k, v in pairs(list) do
4357 if v == value then
4358 return true
4359 end
4360 end
4361 return false
4362end
4363
4364function DeepCopy(object)
4365 -- DeepCopy(object) copies all elements of a table
4366 local lookup_table = {}
4367 local function _copy(object)
4368 if type(object) ~= "table" then
4369 return object
4370 elseif lookup_table[object] then
4371 return lookup_table[object]
4372 end
4373 local new_table = {}
4374 lookup_table[object] = new_table
4375 for index, value in pairs(object) do
4376 new_table[_copy(index)] = _copy(value)
4377 end
4378 return setmetatable(new_table, getmetatable(object))
4379 end
4380 return _copy(object)
4381end
4382
4383function Constrain(lower, mid, upper)
4384 return math.max(lower, math.min(mid, upper))
4385end
4386
4387function Push(a,item)
4388 table.insert(a,item)
4389end
4390
4391function Pop(a)
4392 return table.remove(a)
4393end
4394
4395function GetRandomWeighted(list, size)
4396 -- GetRandomWeighted(list, size) returns a key from a list of (key, weight) pairs
4397 size = size or 100
4398 local chanceIDs = GetWeightedTable(list, size)
4399
4400 if chanceIDs == -1 then
4401 return -1
4402 end
4403 local randomID = 1 + Map.Rand(size, "GetRandomWeighted")
4404 if not chanceIDs[randomID] then
4405 print("GetRandomWeighted: invalid random index selected = %s", randomID)
4406 chanceIDs[randomID] = -1
4407 end
4408 return chanceIDs[randomID]
4409end
4410
4411function GetWeightedTable(list, size)
4412 -- GetWeightedTable(list, size) returns a table with key blocks sized proportionately to a weighted list
4413 local totalWeight = 0
4414 local chanceIDs = {}
4415 local position = 1
4416
4417 for key, weight in pairs(list) do
4418 totalWeight = totalWeight + weight
4419 end
4420
4421 if totalWeight == 0 then
4422 for key, weight in pairs(list) do
4423 list[key] = 1
4424 totalWeight = totalWeight + 1
4425 end
4426 if totalWeight == 0 then
4427 print("GetWeightedTable: empty list")
4428 --print(debug.traceback())
4429 return -1
4430 end
4431 end
4432
4433 for key, weight in pairs(list) do
4434 local positionNext = position + size * weight / totalWeight
4435 for i = math.floor(position), math.floor(positionNext) do
4436 chanceIDs[i] = key
4437 end
4438 position = positionNext
4439 end
4440 return chanceIDs
4441end
4442
4443function GetOppositeDir(dir)
4444 return ((dir + 2) % 6) + 1
4445end
4446
4447function GetBellCurve(value, normalize)
4448 --Returns a value along a bell curve from a 0 - 1 range
4449 if normalize then
4450 value = 1 - math.abs(value - normalize) / normalize
4451 end
4452 return math.sin(value * math.pi * 2 - math.pi/2)/2 + 0.5
4453end
4454
4455function GetSinCurve(value, normalize, pushEdges)
4456 --Returns a value along a sin curve from a 0 - 1 range
4457 if not pushEdges then pushEdges = 0 end
4458 if normalize then
4459 value = 1 - ((normalize - pushEdges*2) - (value - pushEdges)) / (normalize - pushEdges*2)
4460 --value = 1 - math.abs(value - normalize) / normalize
4461 end
4462 return math.sin(value * math.pi * 2)
4463end
4464
4465-----------------------------------------------------------------------------
4466--Interpolation and Perlin functions
4467function CubicInterpolate(v0,v1,v2,v3,mu)
4468 local mu2 = mu * mu
4469 local a0 = v3 - v2 - v0 + v1
4470 local a1 = v0 - v1 - a0
4471 local a2 = v2 - v0
4472 local a3 = v1
4473
4474 return (a0 * mu * mu2 + a1 * mu2 + a2 * mu + a3)
4475end
4476
4477function BicubicInterpolate(v,muX,muY)
4478 local a0 = CubicInterpolate(v[1],v[2],v[3],v[4],muX)
4479 local a1 = CubicInterpolate(v[5],v[6],v[7],v[8],muX)
4480 local a2 = CubicInterpolate(v[9],v[10],v[11],v[12],muX)
4481 local a3 = CubicInterpolate(v[13],v[14],v[15],v[16],muX)
4482
4483 return CubicInterpolate(a0,a1,a2,a3,muY)
4484end
4485
4486function CubicDerivative(v0,v1,v2,v3,mu)
4487 local mu2 = mu * mu
4488 local a0 = v3 - v2 - v0 + v1
4489 local a1 = v0 - v1 - a0
4490 local a2 = v2 - v0
4491 --local a3 = v1
4492
4493 return (3 * a0 * mu2 + 2 * a1 * mu + a2)
4494end
4495
4496function BicubicDerivative(v,muX,muY)
4497 local a0 = CubicInterpolate(v[1],v[2],v[3],v[4],muX)
4498 local a1 = CubicInterpolate(v[5],v[6],v[7],v[8],muX)
4499 local a2 = CubicInterpolate(v[9],v[10],v[11],v[12],muX)
4500 local a3 = CubicInterpolate(v[13],v[14],v[15],v[16],muX)
4501
4502 return CubicDerivative(a0,a1,a2,a3,muY)
4503end
4504
4505function GetInterpolatedValue(X,Y,srcMap)
4506 --This function gets a smoothly interpolated value from srcMap.
4507 --x and y are non-integer coordinates of where the value is to
4508 --be calculated, and wrap in both directions. srcMap is an bject
4509 --of type FloatMap.
4510 local points = {}
4511 local fractionX = X - math.floor(X)
4512 local fractionY = Y - math.floor(Y)
4513
4514 --wrappedX and wrappedY are set to -1,-1 of the sampled area
4515 --so that the sample area is in the middle quad of the 4x4 grid
4516 local wrappedX = ((math.floor(X) - 1) % srcMap.rectWidth) + srcMap.rectX
4517 local wrappedY = ((math.floor(Y) - 1) % srcMap.rectHeight) + srcMap.rectY
4518
4519 local x
4520 local y
4521
4522 for pY = 0, 4-1 do
4523 y = pY + wrappedY
4524 for pX = 0,4-1 do
4525 x = pX + wrappedX
4526 local srcIndex = srcMap:GetRectIndex(x, y)
4527 points[(pY * 4 + pX) + 1] = srcMap.data[srcIndex]
4528 end
4529 end
4530
4531 local finalValue = BicubicInterpolate(points,fractionX,fractionY)
4532
4533 return finalValue
4534
4535end
4536
4537function GetDerivativeValue(X,Y,srcMap)
4538 local points = {}
4539 local fractionX = X - math.floor(X)
4540 local fractionY = Y - math.floor(Y)
4541
4542 --wrappedX and wrappedY are set to -1,-1 of the sampled area
4543 --so that the sample area is in the middle quad of the 4x4 grid
4544 local wrappedX = ((math.floor(X) - 1) % srcMap.rectWidth) + srcMap.rectX
4545 local wrappedY = ((math.floor(Y) - 1) % srcMap.rectHeight) + srcMap.rectY
4546
4547 local x
4548 local y
4549
4550 for pY = 0, 4-1 do
4551 y = pY + wrappedY
4552 for pX = 0,4-1 do
4553 x = pX + wrappedX
4554 local srcIndex = srcMap:GetRectIndex(x, y)
4555 points[(pY * 4 + pX) + 1] = srcMap.data[srcIndex]
4556 end
4557 end
4558
4559 local finalValue = BicubicDerivative(points,fractionX,fractionY)
4560
4561 return finalValue
4562
4563end
4564
4565function GetPerlinNoise(x,y,destMapWidth,destMapHeight,initialFrequency,initialAmplitude,amplitudeChange,octaves,noiseMap)
4566 --This function gets Perlin noise for the destination coordinates. Note
4567 --that in order for the noise to wrap, the area sampled on the noise map
4568 --must change to fit each octave.
4569 local finalValue = 0.0
4570 local frequency = initialFrequency
4571 local amplitude = initialAmplitude
4572 local frequencyX --slight adjustment for seamless wrapping
4573 local frequencyY --''
4574 for i = 1,octaves do
4575 if noiseMap.wrapX then
4576 noiseMap.rectX = math.floor(noiseMap.width/2 - (destMapWidth * frequency)/2)
4577 noiseMap.rectWidth = math.max(math.floor(destMapWidth * frequency),1)
4578 frequencyX = noiseMap.rectWidth/destMapWidth
4579 else
4580 noiseMap.rectX = 0
4581 noiseMap.rectWidth = noiseMap.width
4582 frequencyX = frequency
4583 end
4584 if noiseMap.wrapY then
4585 noiseMap.rectY = math.floor(noiseMap.height/2 - (destMapHeight * frequency)/2)
4586 noiseMap.rectHeight = math.max(math.floor(destMapHeight * frequency),1)
4587 frequencyY = noiseMap.rectHeight/destMapHeight
4588 else
4589 noiseMap.rectY = 0
4590 noiseMap.rectHeight = noiseMap.height
4591 frequencyY = frequency
4592 end
4593
4594 finalValue = finalValue + GetInterpolatedValue(x * frequencyX, y * frequencyY, noiseMap) * amplitude
4595 frequency = frequency * 2.0
4596 amplitude = amplitude * amplitudeChange
4597 end
4598 finalValue = finalValue/octaves
4599 return finalValue
4600end
4601
4602function GetPerlinDerivative(x,y,destMapWidth,destMapHeight,initialFrequency,initialAmplitude,amplitudeChange,octaves,noiseMap)
4603 local finalValue = 0.0
4604 local frequency = initialFrequency
4605 local amplitude = initialAmplitude
4606 local frequencyX --slight adjustment for seamless wrapping
4607 local frequencyY --''
4608 for i = 1,octaves do
4609 if noiseMap.wrapX then
4610 noiseMap.rectX = math.floor(noiseMap.width/2 - (destMapWidth * frequency)/2)
4611 noiseMap.rectWidth = math.floor(destMapWidth * frequency)
4612 frequencyX = noiseMap.rectWidth/destMapWidth
4613 else
4614 noiseMap.rectX = 0
4615 noiseMap.rectWidth = noiseMap.width
4616 frequencyX = frequency
4617 end
4618 if noiseMap.wrapY then
4619 noiseMap.rectY = math.floor(noiseMap.height/2 - (destMapHeight * frequency)/2)
4620 noiseMap.rectHeight = math.floor(destMapHeight * frequency)
4621 frequencyY = noiseMap.rectHeight/destMapHeight
4622 else
4623 noiseMap.rectY = 0
4624 noiseMap.rectHeight = noiseMap.height
4625 frequencyY = frequency
4626 end
4627
4628 finalValue = finalValue + GetDerivativeValue(x * frequencyX, y * frequencyY, noiseMap) * amplitude
4629 frequency = frequency * 2.0
4630 amplitude = amplitude * amplitudeChange
4631 end
4632 finalValue = finalValue/octaves
4633 return finalValue
4634end
4635
4636
4637---------------------------------------------------------------------
4638
4639--ShiftMap Class
4640function ShiftMaps()
4641 --local stripRadius = self.stripRadius
4642 local shift_x = 0
4643 local shift_y = 0
4644
4645 shift_x = DetermineXShift()
4646
4647 ShiftMapsBy(shift_x, shift_y)
4648end
4649
4650function ShiftMapsBy(xshift, yshift)
4651 local W, H = Map.GetGridSize()
4652 if(xshift > 0 or yshift > 0) then
4653 local Shift = {}
4654 local iDestI = 0
4655 for iDestY = 0, H-1 do
4656 for iDestX = 0, W-1 do
4657 local iSourceX = (iDestX + xshift) % W
4658
4659 --local iSourceY = (iDestY + yshift) % H -- If using yshift, enable this and comment out the faster line below. - Bobert13
4660 local iSourceY = iDestY
4661
4662 local iSourceI = W * iSourceY + iSourceX
4663 Shift[iDestI] = elevationMap.data[iSourceI]
4664 --if debugTime then print(string.format("Shift:%d, %f | eMap:%d, %f",iDestI,Shift[iDestI],iSourceI,elevationMap.data[iSourceI])) end
4665 iDestI = iDestI + 1
4666 end
4667 end
4668 elevationMap.data = Shift --It's faster to do one large table operation here than it is to do thousands of small operations to set up a copy of the input table at the beginning. -Bobert13
4669 end
4670 return elevationMap
4671end
4672
4673function DetermineXShift()
4674 --[[ This function will align the most water-heavy vertical portion of the map with the
4675 vertical map edge. This is a form of centering the landmasses, but it emphasizes the
4676 edge not the middle. If there are columns completely empty of land, these will tend to
4677 be chosen as the new map edge, but it is possible for a narrow column between two large
4678 continents to be passed over in favor of the thinnest section of a continent, because
4679 the operation looks at a group of columns not just a single column, then picks the
4680 center of the most water heavy group of columns to be the new vertical map edge. ]]--
4681
4682 -- First loop through the map columns and record land plots in each column.
4683 local mapW, mapH = Map.GetGridSize()
4684 local land_totals = {}
4685 for x = 0, mapW - 1 do
4686 local current_column = 0
4687 for y = 0, mapH - 1 do
4688 local i = y * mapW + x + 1
4689 if not elevationMap:IsBelowSeaLevel(x,y) then
4690 current_column = current_column + 1
4691 end
4692 end
4693 table.insert(land_totals, current_column)
4694 end
4695
4696 -- Now evaluate column groups, each record applying to the center column of the group.
4697 local column_groups = {}
4698 -- Determine the group size in relation to map width.
4699 local group_radius = 3
4700 -- Measure the groups.
4701 for column_index = 1, mapW do
4702 local current_group_total = 0
4703 --for current_column = column_index - group_radius, column_index + group_radius do
4704 --Changed how group_radius works to get groups of four. -Bobert13
4705 for current_column = column_index, column_index + group_radius do
4706 local current_index = current_column % mapW
4707 if current_index == 0 then -- Modulo of the last column will be zero this repairs the issue.
4708 current_index = mapW
4709 end
4710 current_group_total = current_group_total + land_totals[current_index]
4711 end
4712 table.insert(column_groups, current_group_total)
4713 end
4714
4715 -- Identify the group with the least amount of land in it.
4716 local best_value = mapH * (group_radius + 1) -- Set initial value to max possible.
4717 local best_group = 1 -- Set initial best group as current map edge.
4718 for column_index, group_land_plots in ipairs(column_groups) do
4719 if group_land_plots < best_value then
4720 best_value = group_land_plots
4721 best_group = column_index
4722 end
4723 end
4724
4725 -- Determine X Shift
4726 local x_shift = best_group + 2
4727
4728 return x_shift
4729end
4730
4731-----------------------------------------------------------------------------
4732-- Random functions will use lua rands for stand alone script running and Map.rand for in game.
4733function PWRand()
4734 if Map then
4735 return Map.Rand(10000, "Random - Lua") / 10000
4736 end
4737 return math.random()
4738end
4739
4740function PWRandSeed(fixedseed)
4741 local seed
4742 if fixedseed == nil then
4743 seed = (Map.Rand(32767,"") * 65536) + Map.Rand(65535,"")
4744 else
4745 seed = fixedseed
4746 end
4747 math.randomseed(seed)
4748 print("random seed for this map is " .. seed)
4749end
4750
4751function PWRandint(low, high)
4752 --range is inclusive, low and high are possible results
4753 return math.random(low, high)
4754end
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768--
4769-- FloatMap
4770--
4771
4772-----------------------------------------------------------------------------
4773-- FloatMap class
4774-- This is for storing 2D map data. The 'data' field is a zero based, one
4775-- dimensional array. To access map data by x and y coordinates, use the
4776-- GetIndex method to obtain the 1D index, which will handle any needs for
4777-- wrapping in the x and y directions.
4778-----------------------------------------------------------------------------
4779FloatMap = inheritsFrom(nil)
4780
4781function FloatMap:New(width, height, wrapX, wrapY)
4782 local new_inst = {}
4783 setmetatable(new_inst, {__index = FloatMap}) --setup metatable
4784
4785 new_inst.width = width
4786 new_inst.height = height
4787 new_inst.wrapX = wrapX
4788 new_inst.wrapY = wrapY
4789 new_inst.length = width*height
4790
4791 --These fields are used to access only a subset of the map
4792 --with the GetRectIndex function. This is useful for
4793 --making Perlin noise wrap without generating separate
4794 --noise fields for each octave
4795 new_inst.rectX = 0
4796 new_inst.rectY = 0
4797 new_inst.rectWidth = width
4798 new_inst.rectHeight = height
4799
4800 new_inst.data = {}
4801 for i = 0,width*height - 1 do
4802 new_inst.data[i] = 0.0
4803 end
4804
4805 return new_inst
4806end
4807
4808function FloatMap:GetNeighbor(x,y,dir)
4809 local xx
4810 local yy
4811 local odd = y % 2
4812 if dir == mg.C then
4813 return x,y
4814 elseif dir == mg.W then
4815 xx = x - 1
4816 yy = y
4817 return xx,yy
4818 elseif dir == mg.NW then
4819 xx = x - 1 + odd
4820 yy = y + 1
4821 return xx,yy
4822 elseif dir == mg.NE then
4823 xx = x + odd
4824 yy = y + 1
4825 return xx,yy
4826 elseif dir == mg.E then
4827 xx = x + 1
4828 yy = y
4829 return xx,yy
4830 elseif dir == mg.SE then
4831 xx = x + odd
4832 yy = y - 1
4833 return xx,yy
4834 elseif dir == mg.SW then
4835 xx = x - 1 + odd
4836 yy = y - 1
4837 return xx,yy
4838 else
4839 print(string.format("Bad direction %s in FloatMap:GetNeighbor", dir))
4840 end
4841 return -1,-1
4842end
4843
4844function FloatMap:GetIndex(x,y)
4845 if not self.wrapY and (y < 0 or y > self.height-1) then
4846 return -1
4847 elseif not self.wrapX and (x < 0 or x > self.width-1) then
4848 return -1
4849 end
4850
4851 return (y % self.height) * self.width + (x % self.width)
4852end
4853
4854function FloatMap:GetXYFromIndex(i)
4855 local x = i % self.width
4856 local y = (i - x)/self.width
4857 return x,y
4858end
4859
4860--quadrants are labeled
4861--A B
4862--D C
4863function FloatMap:GetQuadrant(x,y)
4864 if x < self.width/2 then
4865 if y < self.height/2 then
4866 return "A"
4867 else
4868 return "D"
4869 end
4870 else
4871 if y < self.height/2 then
4872 return "B"
4873 else
4874 return "C"
4875 end
4876 end
4877end
4878
4879--Gets an index for x and y based on the current rect settings. x and y are local to the defined rect.
4880--Wrapping is assumed in both directions
4881function FloatMap:GetRectIndex(x,y)
4882 local xx = x % self.rectWidth
4883 local yy = y % self.rectHeight
4884
4885 xx = self.rectX + xx
4886 yy = self.rectY + yy
4887
4888 return self:GetIndex(xx,yy)
4889end
4890
4891function FloatMap:Normalize()
4892 --find highest and lowest values
4893 local maxAlt = -1000.0
4894 local minAlt = 1000.0
4895 for i = 0,self.length - 1 do
4896 local alt = self.data[i]
4897 if alt > maxAlt then
4898 maxAlt = alt
4899 end
4900 if alt < minAlt then
4901 minAlt = alt
4902 end
4903
4904 end
4905 --subtract minAlt from all values so that
4906 --all values are zero and above
4907 for i = 0, self.length - 1, 1 do
4908 self.data[i] = self.data[i] - minAlt
4909 end
4910
4911 --subract minAlt also from maxAlt
4912 maxAlt = maxAlt - minAlt
4913
4914 --determine and apply scaler to whole map
4915 local scaler
4916 if maxAlt == 0.0 then
4917 scaler = 0.0
4918 else
4919 scaler = 1.0/maxAlt
4920 end
4921
4922 -- for i = 0,self.length - 1,1 do
4923 -- self.data[i] = self.data[i] * scaler
4924 -- if i == self.length/2 then
4925 -- print("normalized: "..self.data[i])
4926 -- end
4927 -- end
4928
4929 for i=0,self.length-1,1 do
4930 self.data[i],expo = math.frexp(self.data[i])
4931 self.data[i] = (self.data[i]*scaler)
4932 self.data[i] = math.ldexp(self.data[i],expo)
4933 -- if i == self.length/2 then
4934 -- print("normalized: "..self.data[i].." expo: "..expo)
4935 -- end
4936 end
4937end
4938
4939function FloatMap:GenerateNoise()
4940 for i = 0,self.length - 1 do
4941 self.data[i] = PWRand()
4942 end
4943
4944end
4945
4946function FloatMap:GenerateBinaryNoise()
4947 for i = 0,self.length - 1 do
4948 if PWRand() > 0.5 then
4949 self.data[i] = 1
4950 else
4951 self.data[i] = 0
4952 end
4953 end
4954
4955end
4956
4957function FloatMap:FindThresholdFromPercent(percent, greaterThan, excludeZeros)
4958 local mapList = {}
4959 local percentage = percent * 100
4960
4961 if greaterThan then
4962 percentage = 100 - percentage
4963 end
4964
4965 if percentage >= 100 then
4966 return 1.01 --whole map
4967 elseif percentage <= 0 then
4968 return -0.01 --none of the map
4969 end
4970
4971 for i = 0,self.length - 1 do
4972 if not (self.data[i] == 0.0 and excludeZeros) then
4973 table.insert(mapList,self.data[i])
4974 end
4975 end
4976
4977 table.sort(mapList, function (a,b) return a < b end)
4978 local threshIndex = math.floor((#mapList * percentage)/100)
4979
4980 log:Debug("threshIndex %s = math.floor((%s * %s)/100)", threshIndex, #mapList, percentage)
4981
4982 return mapList[threshIndex - 1]
4983
4984end
4985
4986function FloatMap:GetLatitudeForY(y)
4987 local range = mg.topLatitude - mg.bottomLatitude
4988 local lat = nil
4989 if y < self.height/2 then
4990 lat = (y+1) / self.height * range + (mg.bottomLatitude - mg.topLatitude / self.height)
4991 else
4992 lat = y / self.height * range + (mg.bottomLatitude + mg.topLatitude / self.height)
4993 end
4994 return lat
4995end
4996
4997function FloatMap:GetYForLatitude(lat)
4998 local range = mg.topLatitude - mg.bottomLatitude
4999 return math.floor(((lat - mg.bottomLatitude) /range * self.height) + 0.5)
5000end
5001
5002function FloatMap:GetZone(y)
5003 local lat = self:GetLatitudeForY(y)
5004 if y < 0 or y >= self.height then
5005 return mg.NOZONE
5006 end
5007 if lat > mg.polarFrontLatitude then
5008 return mg.NPOLAR
5009 elseif lat >= mg.horseLatitudes then
5010 return mg.NTEMPERATE
5011 elseif lat >= 0.0 then
5012 return mg.NEQUATOR
5013 elseif lat > -mg.horseLatitudes then
5014 return mg.SEQUATOR
5015 elseif lat >= -mg.polarFrontLatitude then
5016 return mg.STEMPERATE
5017 else
5018 return mg.SPOLAR
5019 end
5020end
5021
5022function FloatMap:GetYFromZone(zone, bTop)
5023 if bTop then
5024 for y=self.height - 1,0,-1 do
5025 if zone == self:GetZone(y) then
5026 return y
5027 end
5028 end
5029 else
5030 for y=0,self.height - 1 do
5031 if zone == self:GetZone(y) then
5032 return y
5033 end
5034 end
5035 end
5036 return -1
5037end
5038
5039function FloatMap:GetGeostrophicWindDirections(zone)
5040
5041 if zone == mg.NPOLAR then
5042 return mg.SW,mg.W
5043 elseif zone == mg.NTEMPERATE then
5044 return mg.NE,mg.E
5045 elseif zone == mg.NEQUATOR then
5046 return mg.SW,mg.W
5047 elseif zone == mg.SEQUATOR then
5048 return mg.NW,mg.W
5049 elseif zone == mg.STEMPERATE then
5050 return mg.SE, mg.E
5051 else
5052 return mg.NW,mg.W
5053 end
5054 return -1,-1
5055end
5056
5057function FloatMap:GetGeostrophicPressure(lat)
5058 local latRange = nil
5059 local latPercent = nil
5060 local pressure = nil
5061 if lat > mg.polarFrontLatitude then
5062 latRange = 90.0 - mg.polarFrontLatitude
5063 latPercent = (lat - mg.polarFrontLatitude)/latRange
5064 pressure = 1.0 - latPercent
5065 elseif lat >= mg.horseLatitudes then
5066 latRange = mg.polarFrontLatitude - mg.horseLatitudes
5067 latPercent = (lat - mg.horseLatitudes)/latRange
5068 pressure = latPercent
5069 elseif lat >= 0.0 then
5070 latRange = mg.horseLatitudes - 0.0
5071 latPercent = (lat - 0.0)/latRange
5072 pressure = 1.0 - latPercent
5073 elseif lat > -mg.horseLatitudes then
5074 latRange = 0.0 + mg.horseLatitudes
5075 latPercent = (lat + mg.horseLatitudes)/latRange
5076 pressure = latPercent
5077 elseif lat >= -mg.polarFrontLatitude then
5078 latRange = -mg.horseLatitudes + mg.polarFrontLatitude
5079 latPercent = (lat + mg.polarFrontLatitude)/latRange
5080 pressure = 1.0 - latPercent
5081 else
5082 latRange = -mg.polarFrontLatitude + 90.0
5083 latPercent = (lat + 90)/latRange
5084 pressure = latPercent
5085 end
5086-- Prevents excessively high and low pressures which helps distribute rain more evenly in the affected areas. -Bobert13
5087 pressure = pressure + 1
5088 if pressure > 1.5 then
5089 pressure = pressure * mg.pressureNorm
5090 else
5091 pressure = pressure / mg.pressureNorm
5092 end
5093 pressure = pressure - 1
5094-- FIN -Bobert13
5095 --print(pressure)
5096 return pressure
5097end
5098
5099function FloatMap:ApplyFunction(func)
5100 for i = 0,self.length - 1 do
5101 self.data[i] = func(self.data[i])
5102 end
5103end
5104
5105function FloatMap:GetRadiusAroundHex(x,y,radius)
5106 local list = {}
5107 table.insert(list,{x,y})
5108 if radius == 0 then
5109 return list
5110 end
5111
5112 local hereX = x
5113 local hereY = y
5114
5115 --make a circle for each radius
5116 for r = 1,radius do
5117 --start 1 to the west
5118 hereX,hereY = self:GetNeighbor(hereX,hereY,mg.W)
5119 if self:IsOnMap(hereX,hereY) then
5120 table.insert(list,{hereX,hereY})
5121 end
5122 --Go r times to the NE
5123 for z = 1,r do
5124 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.NE)
5125 if self:IsOnMap(hereX,hereY) then
5126 table.insert(list,{hereX,hereY})
5127 end
5128 end
5129 --Go r times to the E
5130 for z = 1,r do
5131 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.E)
5132 if self:IsOnMap(hereX,hereY) then
5133 table.insert(list,{hereX,hereY})
5134 end
5135 end
5136 --Go r times to the SE
5137 for z = 1,r do
5138 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.SE)
5139 if self:IsOnMap(hereX,hereY) then
5140 table.insert(list,{hereX,hereY})
5141 end
5142 end
5143 --Go r times to the SW
5144 for z = 1,r do
5145 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.SW)
5146 if self:IsOnMap(hereX,hereY) then
5147 table.insert(list,{hereX,hereY})
5148 end
5149 end
5150 --Go r times to the W
5151 for z = 1,r do
5152 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.W)
5153 if self:IsOnMap(hereX,hereY) then
5154 table.insert(list,{hereX,hereY})
5155 end
5156 end
5157 --Go r - 1 times to the NW!!!!!
5158 for z = 1,r - 1 do
5159 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.NW)
5160 if self:IsOnMap(hereX,hereY) then
5161 table.insert(list,{hereX,hereY})
5162 end
5163 end
5164 --one extra NW to set up for next circle
5165 hereX, hereY = self:GetNeighbor(hereX,hereY,mg.NW)
5166 end
5167 return list
5168end
5169
5170function FloatMap:GetAverageInHex(x,y,radius)
5171 local sum = 0
5172 local numPlots = 0
5173 --print("GetAverageInHex A")
5174 local nearPlotIDs = Plot_GetPlotsInCircleFast(x, y, radius)
5175 --print("GetAverageInHex B")
5176 for _, nearPlotID in pairs(nearPlotIDs) do
5177 sum = sum + self.data[nearPlotID]
5178 numPlots = numPlots + 1
5179 end
5180 return sum / numPlots
5181end
5182
5183function FloatMap:GetStdDevInHex(x,y,radius)
5184 local average = 0
5185 local numPlots = 0
5186 --print("GetStdDevInHex A")
5187 local nearPlotIDs = Plot_GetPlotsInCircleFast(x, y, radius)
5188 --print("GetStdDevInHex B")
5189
5190 for _, nearPlotID in ipairs(nearPlotIDs) do
5191 average = average + self.data[nearPlotID]
5192 numPlots = numPlots + 1
5193 end
5194 average = average / numPlots
5195
5196 local deviation = 0.0
5197 for _, nearPlotID in ipairs(nearPlotIDs) do
5198 deviation = deviation + (self.data[nearPlotID] - average) ^ 2
5199 end
5200 return math.sqrt(deviation/numPlots)
5201end
5202
5203function FloatMap:Smooth(radius)
5204 --log:Info("FloatMap:Smooth(%s)", radius)
5205 local dataCopy = {}
5206 local i = 0
5207 for y = 0, self.height - 1 do
5208 for x = 0, self.width - 1 do
5209-- local i = self:GetIndex(x,y)
5210 dataCopy[i] = self:GetAverageInHex(x,y,radius)
5211 i = i + 1
5212 end
5213 end
5214 self.data = dataCopy
5215end
5216
5217function FloatMap:Deviate(radius)
5218 local dataCopy = {}
5219 local i = 0
5220 for y = 0, self.height - 1 do
5221 for x = 0, self.width - 1 do
5222-- local i = self:GetIndex(x,y)
5223 dataCopy[i] = self:GetStdDevInHex(x,y,radius)
5224 i = i + 1
5225 end
5226 end
5227 self.data = dataCopy
5228end
5229
5230function FloatMap:IsOnMap(x,y)
5231 local i = self:GetIndex(x,y)
5232 if i == -1 then
5233 return false
5234 end
5235 return true
5236end
5237
5238function FloatMap:Save(name)
5239 print("saving " .. name .. "..")
5240 local str = self.width .. "," .. self.height
5241 for i = 0,self.length - 1 do
5242 str = str .. "," .. self.data[i]
5243 end
5244 local file = io.open(name,"w+")
5245 file:write(str)
5246 file:close()
5247 print("bitmap saved as " .. name .. ".")
5248end
5249
5250
5251
5252
5253
5254
5255
5256
5257
5258
5259
5260
5261
5262--
5263-- AreaMap
5264--
5265
5266PWAreaMap = inheritsFrom(FloatMap)
5267
5268function PWAreaMap:New(width,height,wrapX,wrapY)
5269 local new_inst = FloatMap:New(width,height,wrapX,wrapY)
5270 setmetatable(new_inst, {__index = PWAreaMap}) --setup metatable
5271
5272 new_inst.areaList = {}
5273 new_inst.segStack = {}
5274 return new_inst
5275end
5276
5277function PWAreaMap:DefineAreas(matchFunction)
5278 --zero map data
5279 for i = 0,self.width*self.height - 1 do
5280 self.data[i] = 0.0
5281 end
5282
5283 self.areaList = {}
5284 local currentAreaID = 0
5285 for y = 0, self.height - 1 do
5286 for x = 0, self.width - 1 do
5287 local i = self:GetIndex(x,y)
5288 if self.data[i] == 0 then
5289 currentAreaID = currentAreaID + 1
5290 local area = PWArea:New(currentAreaID,x,y,matchFunction(x,y))
5291 --str = string.format("Filling area %d, matchFunction(x = %d,y = %d) = %s",area.id,x,y,tostring(matchFunction(x,y)))
5292 --print(str)
5293 self:FillArea(x,y,area,matchFunction)
5294 table.insert(self.areaList, area)
5295
5296 end
5297 end
5298 end
5299end
5300
5301function PWAreaMap:FillArea(x,y,area,matchFunction)
5302 self.segStack = {}
5303 local seg = LineSeg:New(y,x,x,1)
5304 Push(self.segStack,seg)
5305 seg = LineSeg:New(y + 1,x,x,-1)
5306 Push(self.segStack,seg)
5307 while #self.segStack > 0 do
5308 seg = Pop(self.segStack)
5309 self:ScanAndFillLine(seg,area,matchFunction)
5310 end
5311end
5312
5313function PWAreaMap:ScanAndFillLine(seg,area,matchFunction)
5314
5315 --str = string.format("Processing line y = %d, xLeft = %d, xRight = %d, dy = %d -------",seg.y,seg.xLeft,seg.xRight,seg.dy)
5316 --print(str)
5317 if self:ValidateY(seg.y + seg.dy) == -1 then
5318 return
5319 end
5320
5321 local odd = (seg.y + seg.dy) % 2
5322 local notOdd = seg.y % 2
5323 --str = string.format("odd = %d, notOdd = %d",odd,notOdd)
5324 --print(str)
5325
5326 local lineFound = 0
5327 local xStop = nil
5328 if self.wrapX then
5329 xStop = 0 - (self.width * 30)
5330 else
5331 xStop = -1
5332 end
5333 local leftExtreme = nil
5334 for leftExt = seg.xLeft - odd,xStop + 1,-1 do
5335 leftExtreme = leftExt --need this saved
5336 --str = string.format("leftExtreme = %d",leftExtreme)
5337 --print(str)
5338 local x = self:ValidateX(leftExtreme)
5339 local y = self:ValidateY(seg.y + seg.dy)
5340 local i = self:GetIndex(x,y)
5341 --str = string.format("x = %d, y = %d, area.trueMatch = %s, matchFunction(x,y) = %s",x,y,tostring(area.trueMatch),tostring(matchFunction(x,y)))
5342 --print(str)
5343 if self.data[i] == 0 and area.trueMatch == matchFunction(x,y) then
5344 self.data[i] = area.id
5345 area.size = area.size + 1
5346 --print("adding to area")
5347 lineFound = 1
5348 else
5349 --if no line was found, then leftExtreme is fine, but if
5350 --a line was found going left, then we need to increment
5351 --xLeftExtreme to represent the inclusive end of the line
5352 if lineFound == 1 then
5353 leftExtreme = leftExtreme + 1
5354 --print("line found, adding 1 to leftExtreme")
5355 end
5356 break
5357 end
5358 end
5359 --str = string.format("leftExtreme = %d",leftExtreme)
5360 --print(str)
5361 local rightExtreme = nil
5362 --now scan right to find extreme right, place each found segment on stack
5363 if self.wrapX then
5364 xStop = self.width * 20
5365 else
5366 xStop = self.width
5367 end
5368 for rightExt = seg.xLeft + lineFound - odd,xStop - 1 do
5369 rightExtreme = rightExt --need this saved
5370 --str = string.format("rightExtreme = %d",rightExtreme)
5371 --print(str)
5372 local x = self:ValidateX(rightExtreme)
5373 local y = self:ValidateY(seg.y + seg.dy)
5374 local i = self:GetIndex(x,y)
5375 --str = string.format("x = %d, y = %d, area.trueMatch = %s, matchFunction(x,y) = %s",x,y,tostring(area.trueMatch),tostring(matchFunction(x,y)))
5376 --print(str)
5377 if self.data[i] == 0 and area.trueMatch == matchFunction(x,y) then
5378 self.data[i] = area.id
5379 area.size = area.size + 1
5380 --print("adding to area")
5381 if lineFound == 0 then
5382 lineFound = 1 --starting new line
5383 leftExtreme = rightExtreme
5384 end
5385 elseif lineFound == 1 then --found the right end of a line segment
5386 --print("found right end of line")
5387 lineFound = 0
5388 --put same direction on stack
5389 local newSeg = LineSeg:New(y,leftExtreme,rightExtreme - 1,seg.dy)
5390 Push(self.segStack,newSeg)
5391 --str = string.format(" pushing y = %d, xLeft = %d, xRight = %d, dy = %d",y,leftExtreme,rightExtreme - 1,seg.dy)
5392 --print(str)
5393 --determine if we must put reverse direction on stack
5394 if leftExtreme < seg.xLeft - odd or rightExtreme >= seg.xRight + notOdd then
5395 --out of shadow so put reverse direction on stack
5396 newSeg = LineSeg:New(y,leftExtreme,rightExtreme - 1,-seg.dy)
5397 Push(self.segStack,newSeg)
5398 --str = string.format(" pushing y = %d, xLeft = %d, xRight = %d, dy = %d",y,leftExtreme,rightExtreme - 1,-seg.dy)
5399 --print(str)
5400 end
5401 if(rightExtreme >= seg.xRight + notOdd) then
5402 break
5403 end
5404 elseif lineFound == 0 and rightExtreme >= seg.xRight + notOdd then
5405 break --past the end of the parent line and no line found
5406 end
5407 --continue finding segments
5408 end
5409 if lineFound == 1 then --still needing a line to be put on stack
5410 print("still need line segments")
5411 lineFound = 0
5412 --put same direction on stack
5413 local newSeg = LineSeg:New(seg.y + seg.dy,leftExtreme,rightExtreme - 1,seg.dy)
5414 Push(self.segStack,newSeg)
5415 str = string.format(" pushing y = %d, xLeft = %d, xRight = %d, dy = %d",seg.y + seg.dy,leftExtreme,rightExtreme - 1,seg.dy)
5416 print(str)
5417 --determine if we must put reverse direction on stack
5418 if leftExtreme < seg.xLeft - odd or rightExtreme >= seg.xRight + notOdd then
5419 --out of shadow so put reverse direction on stack
5420 newSeg = LineSeg:New(seg.y + seg.dy,leftExtreme,rightExtreme - 1,-seg.dy)
5421 Push(self.segStack,newSeg)
5422 str = string.format(" pushing y = %d, xLeft = %d, xRight = %d, dy = %d",seg.y + seg.dy,leftExtreme,rightExtreme - 1,-seg.dy)
5423 print(str)
5424 end
5425 end
5426end
5427
5428function PWAreaMap:GetAreaByID(id)
5429 for i = 1,#self.areaList do
5430 if self.areaList[i].id == id then
5431 return self.areaList[i]
5432 end
5433 end
5434 error("Can't find area id in AreaMap.areaList")
5435end
5436
5437function PWAreaMap:ValidateY(y)
5438 local yy = nil
5439 if self.wrapY then
5440 yy = y % self.height
5441 elseif y < 0 or y >= self.height then
5442 return -1
5443 else
5444 yy = y
5445 end
5446 return yy
5447end
5448
5449function PWAreaMap:ValidateX(x)
5450 local xx = nil
5451 if self.wrapX then
5452 xx = x % self.width
5453 elseif x < 0 or x >= self.width then
5454 return -1
5455 else
5456 xx = x
5457 end
5458 return xx
5459end
5460
5461function PWAreaMap:PrintAreaList()
5462 for i=1,#self.areaList do
5463 local id = self.areaList[i].id
5464 local seedx = self.areaList[i].seedx
5465 local seedy = self.areaList[i].seedy
5466 local size = self.areaList[i].size
5467 local trueMatch = self.areaList[i].trueMatch
5468 local str = string.format("area id = %d, trueMatch = %s, size = %d, seedx = %d, seedy = %d",id,tostring(trueMatch),size,seedx,seedy)
5469 print(str)
5470 end
5471end
5472
5473--Area class
5474PWArea = inheritsFrom(nil)
5475function PWArea:New(id,seedx,seedy,trueMatch)
5476 local new_inst = {}
5477 setmetatable(new_inst, {__index = PWArea}) --setup metatable
5478
5479 new_inst.id = id
5480 new_inst.seedx = seedx
5481 new_inst.seedy = seedy
5482 new_inst.trueMatch = trueMatch
5483 new_inst.size = 0
5484
5485 return new_inst
5486end
5487
5488--LineSeg class
5489LineSeg = inheritsFrom(nil)
5490function LineSeg:New(y,xLeft,xRight,dy)
5491 local new_inst = {}
5492 setmetatable(new_inst, {__index = LineSeg}) --setup metatable
5493
5494 new_inst.y = y
5495 new_inst.xLeft = xLeft
5496 new_inst.xRight = xRight
5497 new_inst.dy = dy
5498
5499 return new_inst
5500end
5501
5502
5503
5504
5505
5506
5507
5508
5509
5510
5511
5512
5513
5514--
5515-- ElevationMap
5516--
5517
5518ElevationMap = inheritsFrom(FloatMap)
5519
5520function ElevationMap:New(width, height, wrapX, wrapY)
5521 local new_inst = FloatMap:New(width,height,wrapX,wrapY)
5522 setmetatable(new_inst, {__index = ElevationMap}) --setup metatable
5523 return new_inst
5524end
5525function ElevationMap:IsBelowSeaLevel(x,y)
5526 local i = self:GetIndex(x,y)
5527 if self.data[i] < self.seaLevelThreshold then
5528 return true
5529 else
5530 return false
5531 end
5532end
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
5543
5544
5545
5546--
5547-- Rivers
5548--
5549
5550
5551function AddRivers()
5552 local mapW, mapH = Map.GetGridSize()
5553
5554
5555 for y = 0, mapH-1 do
5556 for x = 0,mapW-1 do
5557 local plot = Map.GetPlot(x, y)
5558
5559 local WOfRiver, NWOfRiver, NEOfRiver = riverMap:GetFlowDirections(x,y)
5560
5561 if WOfRiver == mg.flowNONE then
5562 plot:SetWOfRiver(false,WOfRiver)
5563 else
5564 local xx,yy = elevationMap:GetNeighbor(x,y,mg.E)
5565 local nPlot = Map.GetPlot(xx,yy)
5566 if plot:IsMountain() and nPlot:IsMountain() then
5567 plot:SetPlotType(PlotTypes.PLOT_HILLS,false,false)
5568 end
5569 plot:SetWOfRiver(true,WOfRiver)
5570 --if debugTime then print(string.format("(%d,%d)WOfRiver = true dir=%d",x,y,WOfRiver)) end
5571 end
5572
5573 if NWOfRiver == mg.flowNONE then
5574 plot:SetNWOfRiver(false,NWOfRiver)
5575 else
5576 local xx,yy = elevationMap:GetNeighbor(x,y,mg.SE)
5577 local nPlot = Map.GetPlot(xx,yy)
5578 if plot:IsMountain() and nPlot:IsMountain() then
5579 plot:SetPlotType(PlotTypes.PLOT_HILLS,false,false)
5580 end
5581 plot:SetNWOfRiver(true,NWOfRiver)
5582 --if debugTime then print(string.format("(%d,%d)NWOfRiver = true dir=%d",x,y,NWOfRiver)) end
5583 end
5584
5585 if NEOfRiver == mg.flowNONE then
5586 plot:SetNEOfRiver(false,NEOfRiver)
5587 else
5588 local xx,yy = elevationMap:GetNeighbor(x,y,mg.SW)
5589 local nPlot = Map.GetPlot(xx,yy)
5590 if plot:IsMountain() and nPlot:IsMountain() then
5591 plot:SetPlotType(PlotTypes.PLOT_HILLS,false,false)
5592 end
5593 plot:SetNEOfRiver(true,NEOfRiver)
5594 --if debugTime then print(string.format("(%d,%d)NEOfRiver = true dir=%d",x,y,NEOfRiver)) end
5595 end
5596 end
5597 end
5598end
5599
5600function Plot_GetPreviousRiverPlot(plot, edgeDirection)
5601 local flowDirection = Plot_GetRiverFlowDirection(plot, edgeDirection)
5602 local nearDirection = mg.C
5603 if edgeDirection == (flowDirection + 1) % 6 then
5604 nearDirection = (flowDirection + 2) % 6
5605 else
5606 nearDirection = (flowDirection + 3) % 6
5607 end
5608 local nearPlot = Map.PlotDirection(plot:GetX(), plot:GetY(), nearDirection)
5609 local edgeA = (flowDirection - 1) % 6
5610 local edgeB = (flowDirection + 0) % 6
5611 local flowA = (flowDirection + 1) % 6
5612 local flowB = (flowDirection - 1) % 6
5613 return nearPlot, edgeA, edgeB, flowA, flowB
5614end
5615
5616function Plot_GetNextRiverPlot(plot, edgeDirection)
5617 local flowDirection = Plot_GetRiverFlowDirection(plot, edgeDirection)
5618 local nearDirection = mg.C
5619 if edgeDirection == (flowDirection + 1) % 6 then
5620 nearDirection = (flowDirection + 0) % 6
5621 else
5622 nearDirection = (flowDirection - 1) % 6
5623 end
5624 local nearPlot = Map.PlotDirection(plot:GetX(), plot:GetY(), nearDirection)
5625 local edgeA = (flowDirection + 2) % 6
5626 local edgeB = (flowDirection + 3) % 6
5627 local flowA = (flowDirection + 4) % 6
5628 local flowB = (flowDirection + 2) % 6
5629 return nearPlot, edgeA, flowA, edgeB, flowB
5630end
5631
5632function Plot_GetRiverFlowDirection(plot, edgeDirection)
5633 if edgeDirection == mg.E then
5634 return plot:GetRiverEFlowDirection()
5635 elseif edgeDirection == mg.SW then
5636 return plot:GetRiverSWFlowDirection()
5637 elseif edgeDirection == mg.SE then
5638 return plot:GetRiverSEFlowDirection()
5639 else
5640 nextPlot = Map.PlotDirection(plot:GetX(), plot:GetY(), edgeDirection)
5641 if not nextPlot then
5642 return mg.flowNONE
5643 end
5644 if edgeDirection == mg.W then
5645 return nextPlot:GetRiverEFlowDirection()
5646 elseif edgeDirection == mg.NE then
5647 return nextPlot:GetRiverSWFlowDirection()
5648 elseif edgeDirection == mg.NW then
5649 return nextPlot:GetRiverSEFlowDirection()
5650 end
5651 end
5652 return mg.flowNONE
5653end
5654
5655function Plot_GetRiverRotation(plot, edgeDirection)
5656 if not Plot_IsRiver(plot, edgeDirection) then
5657 return 0 -- no river
5658 end
5659 if Plot_GetRiverFlowDirection(plot, edgeDirection) == (edgeDirection + 2) % 6 then
5660 return 1 -- clockwise
5661 end
5662 return -1 -- counterclockwise
5663end
5664
5665function Plot_IsRiver(plot, edgeDirection)
5666 if edgeDirection then
5667 return Plot_IsRiverInDirection(plot, edgeDirection)
5668 end
5669 for _, edgeDirection in pairs(mg.edgeDirections) do
5670 if Plot_IsRiverInDirection(plot, edgeDirection) then
5671 return true
5672 end
5673 end
5674 return false
5675end
5676
5677function Plot_IsRiverInDirection(plot, edgeDirection)
5678 if edgeDirection == mg.E then
5679 return plot:IsWOfRiver()
5680 elseif edgeDirection == mg.SE then
5681 return plot:IsNWOfRiver()
5682 elseif edgeDirection == mg.SW then
5683 return plot:IsNEOfRiver()
5684 else
5685 nextPlot = Map.PlotDirection(plot:GetX(), plot:GetY(), edgeDirection)
5686 if not nextPlot then
5687 return false
5688 end
5689 if edgeDirection == mg.W then
5690 return nextPlot:IsWOfRiver()
5691 elseif edgeDirection == mg.NW then
5692 return nextPlot:IsNWOfRiver()
5693 elseif edgeDirection == mg.NE then
5694 return nextPlot:IsNEOfRiver()
5695 end
5696 end
5697 return false
5698end
5699
5700function Plot_SetRiver(plot, edgeDirection, flowDirection)
5701 local isRiver = (flowDirection ~= mg.flowNONE)
5702 if edgeDirection == mg.E then
5703 plot:SetWOfRiver(isRiver, flowDirection)
5704 elseif edgeDirection == mg.SE then
5705 plot:SetNWOfRiver(isRiver, flowDirection)
5706 elseif edgeDirection == mg.SW then
5707 plot:SetNEOfRiver(isRiver, flowDirection)
5708 else
5709 plot = Map.PlotDirection(plot:GetX(), plot:GetY(), edgeDirection)
5710 if not plot then
5711 return false
5712 end
5713 if edgeDirection == mg.W then
5714 plot:SetWOfRiver(isRiver, flowDirection)
5715 elseif edgeDirection == mg.NW then
5716 plot:SetNWOfRiver(isRiver, flowDirection)
5717 elseif edgeDirection == mg.NE then
5718 plot:SetNEOfRiver(isRiver, flowDirection)
5719 end
5720 end
5721 --plot:SetFeatureType(FeatureTypes.FEATURE_ICE, -1)
5722 return true
5723end
5724
5725RiverMap = inheritsFrom(nil)
5726
5727function RiverMap:New(elevationMap)
5728 local new_inst = {}
5729 setmetatable(new_inst, {__index = RiverMap})
5730
5731 new_inst.elevationMap = elevationMap
5732 new_inst.riverData = {}
5733 for y = 0,new_inst.elevationMap.height - 1 do
5734 for x = 0,new_inst.elevationMap.width - 1 do
5735 local i = new_inst.elevationMap:GetIndex(x,y)
5736 new_inst.riverData[i] = RiverHex:New(x,y)
5737 end
5738 end
5739
5740 return new_inst
5741end
5742
5743function RiverMap:GetJunction(x,y,isNorth)
5744 local i = self.elevationMap:GetIndex(x,y)
5745 if isNorth then
5746 return self.riverData[i].northJunction
5747 else
5748 return self.riverData[i].southJunction
5749 end
5750end
5751
5752function RiverMap:GetJunctionNeighbor(direction,junction)
5753 local xx = nil
5754 local yy = nil
5755 local ii = nil
5756 local neighbor = nil
5757 local odd = junction.y % 2
5758 if direction == mg.NOFLOW then
5759 error("can't get junction neighbor in direction NOFLOW")
5760 elseif direction == mg.WESTFLOW then
5761 xx = junction.x + odd - 1
5762 if junction.isNorth then
5763 yy = junction.y + 1
5764 else
5765 yy = junction.y - 1
5766 end
5767 ii = self.elevationMap:GetIndex(xx,yy)
5768 if ii ~= -1 then
5769 neighbor = self:GetJunction(xx,yy,not junction.isNorth)
5770 return neighbor
5771 end
5772 elseif direction == mg.EASTFLOW then
5773 xx = junction.x + odd
5774 if junction.isNorth then
5775 yy = junction.y + 1
5776 else
5777 yy = junction.y - 1
5778 end
5779 ii = self.elevationMap:GetIndex(xx,yy)
5780 if ii ~= -1 then
5781 neighbor = self:GetJunction(xx,yy,not junction.isNorth)
5782 return neighbor
5783 end
5784 elseif direction == mg.VERTFLOW then
5785 xx = junction.x
5786 if junction.isNorth then
5787 yy = junction.y + 2
5788 else
5789 yy = junction.y - 2
5790 end
5791 ii = self.elevationMap:GetIndex(xx,yy)
5792 if ii ~= -1 then
5793 neighbor = self:GetJunction(xx,yy,not junction.isNorth)
5794 return neighbor
5795 end
5796 end
5797
5798 return nil --neighbor off map
5799end
5800
5801--Get the west or east hex neighboring this junction
5802function RiverMap:GetRiverHexNeighbor(junction,westNeighbor)
5803 local xx = nil
5804 local yy = nil
5805 local ii = nil
5806 local odd = junction.y % 2
5807 if junction.isNorth then
5808 yy = junction.y + 1
5809 else
5810 yy = junction.y - 1
5811 end
5812 if westNeighbor then
5813 xx = junction.x + odd - 1
5814 else
5815 xx = junction.x + odd
5816 end
5817
5818 ii = self.elevationMap:GetIndex(xx,yy)
5819 if ii ~= -1 then
5820 return self.riverData[ii]
5821 end
5822
5823 return nil
5824end
5825
5826function RiverMap:SetJunctionAltitudes()
5827 for y = 0,self.elevationMap.height - 1 do
5828 for x = 0,self.elevationMap.width - 1 do
5829 local i = self.elevationMap:GetIndex(x,y)
5830 local vertAltitude = self.elevationMap.data[i]
5831 local westAltitude = nil
5832 local eastAltitude = nil
5833 local vertNeighbor = self.riverData[i]
5834 local westNeighbor = nil
5835 local eastNeighbor = nil
5836 local xx = nil
5837 local yy = nil
5838 local ii = nil
5839
5840 --first do north
5841 westNeighbor = self:GetRiverHexNeighbor(vertNeighbor.northJunction,true)
5842 eastNeighbor = self:GetRiverHexNeighbor(vertNeighbor.northJunction,false)
5843
5844 if westNeighbor ~= nil then
5845 ii = self.elevationMap:GetIndex(westNeighbor.x,westNeighbor.y)
5846 else
5847 ii = -1
5848 end
5849
5850 if ii ~= -1 then
5851 westAltitude = self.elevationMap.data[ii]
5852 else
5853 westAltitude = vertAltitude
5854 end
5855
5856 if eastNeighbor ~= nil then
5857 ii = self.elevationMap:GetIndex(eastNeighbor.x, eastNeighbor.y)
5858 else
5859 ii = -1
5860 end
5861
5862 if ii ~= -1 then
5863 eastAltitude = self.elevationMap.data[ii]
5864 else
5865 eastAltitude = vertAltitude
5866 end
5867
5868 vertNeighbor.northJunction.altitude = math.min(math.min(vertAltitude,westAltitude),eastAltitude)
5869
5870 --then south
5871 westNeighbor = self:GetRiverHexNeighbor(vertNeighbor.southJunction,true)
5872 eastNeighbor = self:GetRiverHexNeighbor(vertNeighbor.southJunction,false)
5873
5874 if westNeighbor ~= nil then
5875 ii = self.elevationMap:GetIndex(westNeighbor.x,westNeighbor.y)
5876 else
5877 ii = -1
5878 end
5879
5880 if ii ~= -1 then
5881 westAltitude = self.elevationMap.data[ii]
5882 else
5883 westAltitude = vertAltitude
5884 end
5885
5886 if eastNeighbor ~= nil then
5887 ii = self.elevationMap:GetIndex(eastNeighbor.x, eastNeighbor.y)
5888 else
5889 ii = -1
5890 end
5891
5892 if ii ~= -1 then
5893 eastAltitude = self.elevationMap.data[ii]
5894 else
5895 eastAltitude = vertAltitude
5896 end
5897
5898 vertNeighbor.southJunction.altitude = math.min(math.min(vertAltitude,westAltitude),eastAltitude)
5899 end
5900 end
5901end
5902
5903function RiverMap:isLake(junction)
5904
5905 --first exclude the map edges that don't have neighbors
5906 if junction.y == 0 and junction.isNorth == false then
5907 return false
5908 elseif junction.y == self.elevationMap.height - 1 and junction.isNorth == true then
5909 return false
5910 end
5911
5912 --exclude altitudes below sea level
5913 if junction.altitude < self.elevationMap.seaLevelThreshold then
5914 return false
5915 end
5916
5917 --if debugTime then print(string.format("junction = (%d,%d) N = %s, alt = %f",junction.x,junction.y,tostring(junction.isNorth),junction.altitude)) end
5918
5919 local vertNeighbor = self:GetJunctionNeighbor(mg.VERTFLOW,junction)
5920 local vertAltitude = nil
5921 if vertNeighbor == nil then
5922 vertAltitude = junction.altitude
5923 --print("--vertNeighbor == nil")
5924 else
5925 vertAltitude = vertNeighbor.altitude
5926 --if debugTime then print(string.format("--vertNeighbor = (%d,%d) N = %s, alt = %f",vertNeighbor.x,vertNeighbor.y,tostring(vertNeighbor.isNorth),vertNeighbor.altitude)) end
5927 end
5928
5929 local westNeighbor = self:GetJunctionNeighbor(mg.WESTFLOW,junction)
5930 local westAltitude = nil
5931 if westNeighbor == nil then
5932 westAltitude = junction.altitude
5933 --print("--westNeighbor == nil")
5934 else
5935 westAltitude = westNeighbor.altitude
5936 --if debugTime then print(string.format("--westNeighbor = (%d,%d) N = %s, alt = %f",westNeighbor.x,westNeighbor.y,tostring(westNeighbor.isNorth),westNeighbor.altitude)) end
5937 end
5938
5939 local eastNeighbor = self:GetJunctionNeighbor(mg.EASTFLOW,junction)
5940 local eastAltitude = nil
5941 if eastNeighbor == nil then
5942 eastAltitude = junction.altitude
5943 --print("--eastNeighbor == nil")
5944 else
5945 eastAltitude = eastNeighbor.altitude
5946 --if debugTime then print(string.format("--eastNeighbor = (%d,%d) N = %s, alt = %f",eastNeighbor.x,eastNeighbor.y,tostring(eastNeighbor.isNorth),eastNeighbor.altitude)) end
5947 end
5948
5949 local lowest = math.min(vertAltitude,math.min(westAltitude,math.min(eastAltitude,junction.altitude)))
5950
5951 if lowest == junction.altitude then
5952 --print("--is lake")
5953 return true
5954 end
5955 --print("--is not lake")
5956 return false
5957end
5958
5959--get the average altitude of the two lowest neighbors that are higher than the junction altitude.
5960function RiverMap:GetLowerNeighborAverage(junction)
5961 local vertNeighbor = self:GetJunctionNeighbor(mg.VERTFLOW,junction)
5962 local vertAltitude = nil
5963 if vertNeighbor == nil then
5964 vertAltitude = junction.altitude
5965 else
5966 vertAltitude = vertNeighbor.altitude
5967 end
5968
5969 local westNeighbor = self:GetJunctionNeighbor(mg.WESTFLOW,junction)
5970 local westAltitude = nil
5971 if westNeighbor == nil then
5972 westAltitude = junction.altitude
5973 else
5974 westAltitude = westNeighbor.altitude
5975 end
5976
5977 local eastNeighbor = self:GetJunctionNeighbor(mg.EASTFLOW,junction)
5978 local eastAltitude = nil
5979 if eastNeighbor == nil then
5980 eastAltitude = junction.altitude
5981 else
5982 eastAltitude = eastNeighbor.altitude
5983 end
5984
5985 local nList = {vertAltitude,westAltitude,eastAltitude}
5986 table.sort(nList)
5987 local avg = nil
5988 if nList[1] > junction.altitude then
5989 avg = (nList[1] + nList[2])/2.0
5990 elseif nList[2] > junction.altitude then
5991 avg = (nList[2] + nList[3])/2.0
5992 elseif nList[3] > junction.altitude then
5993 avg = (nList[3] + junction.altitude)/2.0
5994 else
5995 avg = junction.altitude --all neighbors are the same height. Dealt with later
5996 end
5997 return avg
5998end
5999
6000--this function alters the drainage pattern
6001function RiverMap:SiltifyLakes()
6002 local lakeList = {}
6003 local onQueueMapNorth = {}
6004 local onQueueMapSouth = {}
6005 for y = 0,self.elevationMap.height - 1 do
6006 for x = 0,self.elevationMap.width - 1 do
6007 local i = self.elevationMap:GetIndex(x,y)
6008 onQueueMapNorth[i] = false
6009 onQueueMapSouth[i] = false
6010 if self:isLake(self.riverData[i].northJunction) then
6011 Push(lakeList,self.riverData[i].northJunction)
6012 onQueueMapNorth[i] = true
6013 end
6014 if self:isLake(self.riverData[i].southJunction) then
6015 Push(lakeList,self.riverData[i].southJunction)
6016 onQueueMapSouth[i] = true
6017 end
6018 end
6019 end
6020
6021 local longestLakeList = #lakeList
6022 local shortestLakeList = #lakeList
6023 local iterations = 0
6024 local debugOn = false
6025 --if debugTime then print(string.format("initial lake count = %d",longestLakeList)) end
6026 while #lakeList > 0 do
6027 --if debugTime then print(string.format("length of lakeList = %d",#lakeList)) end
6028 iterations = iterations + 1
6029 if #lakeList > longestLakeList then
6030 longestLakeList = #lakeList
6031 end
6032
6033 if #lakeList < shortestLakeList then
6034 shortestLakeList = #lakeList
6035 --if debugTime then print(string.format("shortest lake list = %d, iterations = %d",shortestLakeList,iterations)) end
6036 iterations = 0
6037 end
6038
6039 if iterations > 1000000 then
6040 debugOn = true
6041 end
6042
6043 if iterations > 1001000 then
6044 error("endless loop in lake siltification. check logs")
6045 end
6046
6047 local junction = Pop(lakeList)
6048 local i = self.elevationMap:GetIndex(junction.x,junction.y)
6049 if junction.isNorth then
6050 onQueueMapNorth[i] = false
6051 else
6052 onQueueMapSouth[i] = false
6053 end
6054
6055 if debugOn then
6056 if debugTime then print(string.format("processing (%d,%d) N=%s alt=%f",junction.x,junction.y,tostring(junction.isNorth),junction.altitude)) end
6057 end
6058
6059 local avgLowest = self:GetLowerNeighborAverage(junction)
6060
6061 if debugOn then
6062 if debugTime then print(string.format("--avgLowest == %f",avgLowest)) end
6063 end
6064
6065 if avgLowest < junction.altitude + 0.005 then --cant use == in fp comparison
6066 junction.altitude = avgLowest + 0.005
6067 if debugOn then
6068 print("--adding 0.005 to avgLowest")
6069 end
6070 else
6071 junction.altitude = avgLowest
6072 end
6073
6074 if debugOn then
6075 if debugTime then print(string.format("--changing altitude to %f",junction.altitude)) end
6076 end
6077
6078 for dir = mg.WESTFLOW,mg.VERTFLOW do
6079 local neighbor = self:GetJunctionNeighbor(dir,junction)
6080 if debugOn and neighbor == nil then
6081 if debugTime then print(string.format("--nil neighbor at direction = %d",dir)) end
6082 end
6083 if neighbor ~= nil and self:isLake(neighbor) then
6084 local i = self.elevationMap:GetIndex(neighbor.x,neighbor.y)
6085 if neighbor.isNorth == true and onQueueMapNorth[i] == false then
6086 Push(lakeList,neighbor)
6087 onQueueMapNorth[i] = true
6088 if debugOn then
6089 if debugTime then print(string.format("--pushing (%d,%d) N=%s alt=%f",neighbor.x,neighbor.y,tostring(neighbor.isNorth),neighbor.altitude)) end
6090 end
6091 elseif neighbor.isNorth == false and onQueueMapSouth[i] == false then
6092 Push(lakeList,neighbor)
6093 onQueueMapSouth[i] = true
6094 if debugOn then
6095 if debugTime then print(string.format("--pushing (%d,%d) N=%s alt=%f",neighbor.x,neighbor.y,tostring(neighbor.isNorth),neighbor.altitude)) end
6096 end
6097 end
6098 end
6099 end
6100 end
6101 --if debugTime then print(string.format("longestLakeList = %d",longestLakeList)) end
6102
6103 --if debugTime then print(string.format("sea level = %f",self.elevationMap.seaLevelThreshold)) end
6104
6105 --[[
6106 local belowSeaLevelCount = 0
6107 local riverTest = FloatMap:New(self.elevationMap.width,self.elevationMap.height,self.elevationMap.xWrap,self.elevationMap.yWrap)
6108 local lakesFound = false
6109 for y = 0,self.elevationMap.height - 1 do
6110 for x = 0,self.elevationMap.width - 1 do
6111 local i = self.elevationMap:GetIndex(x,y)
6112
6113 local northAltitude = self.riverData[i].northJunction.altitude
6114 local southAltitude = self.riverData[i].southJunction.altitude
6115 if northAltitude < self.elevationMap.seaLevelThreshold then
6116 belowSeaLevelCount = belowSeaLevelCount + 1
6117 end
6118 if southAltitude < self.elevationMap.seaLevelThreshold then
6119 belowSeaLevelCount = belowSeaLevelCount + 1
6120 end
6121 riverTest.data[i] = (northAltitude + southAltitude)/2.0
6122
6123 if self:isLake(self.riverData[i].northJunction) then
6124 local junction = self.riverData[i].northJunction
6125 if debugTime then print(string.format("lake found at (%d, %d) isNorth = %s, altitude = %f!",junction.x,junction.y,tostring(junction.isNorth),junction.altitude)) end
6126 riverTest.data[i] = 1.0
6127 lakesFound = true
6128 end
6129 if self:isLake(self.riverData[i].southJunction) then
6130 local junction = self.riverData[i].southJunction
6131 if debugTime then print(string.format("lake found at (%d, %d) isNorth = %s, altitude = %f!",junction.x,junction.y,tostring(junction.isNorth),junction.altitude)) end
6132 riverTest.data[i] = 1.0
6133 lakesFound = true
6134 end
6135 end
6136 end
6137
6138 if lakesFound then
6139 --error("Failed to siltify lakes. check logs")
6140 end
6141 --]]
6142 --riverTest:Normalize()
6143-- riverTest:Save("riverTest.csv")
6144end
6145
6146function RiverMap:SetFlowDestinations()
6147 junctionList = {}
6148 for y = 0,self.elevationMap.height - 1 do
6149 for x = 0,self.elevationMap.width - 1 do
6150 local i = self.elevationMap:GetIndex(x,y)
6151 table.insert(junctionList,self.riverData[i].northJunction)
6152 table.insert(junctionList,self.riverData[i].southJunction)
6153 end
6154 end
6155
6156 table.sort(junctionList,function (a,b) return a.altitude > b.altitude end)
6157
6158 for n=1,#junctionList do
6159 local junction = junctionList[n]
6160 local validList = self:GetValidFlows(junction)
6161 if #validList > 0 then
6162 local choice = PWRandint(1,#validList)
6163 junction.flow = validList[choice]
6164 else
6165 junction.flow = mg.NOFLOW
6166 end
6167 end
6168end
6169
6170function RiverMap:GetValidFlows(junction)
6171 local validList = {}
6172 for dir = mg.WESTFLOW,mg.VERTFLOW do
6173 neighbor = self:GetJunctionNeighbor(dir,junction)
6174 if neighbor ~= nil and neighbor.altitude < junction.altitude then
6175 table.insert(validList,dir)
6176 end
6177 end
6178 return validList
6179end
6180
6181function RiverMap:IsTouchingOcean(junction)
6182
6183 if elevationMap:IsBelowSeaLevel(junction.x,junction.y) then
6184 return true
6185 end
6186 local westNeighbor = self:GetRiverHexNeighbor(junction,true)
6187 local eastNeighbor = self:GetRiverHexNeighbor(junction,false)
6188
6189 if westNeighbor == nil or elevationMap:IsBelowSeaLevel(westNeighbor.x,westNeighbor.y) then
6190 return true
6191 end
6192 if eastNeighbor == nil or elevationMap:IsBelowSeaLevel(eastNeighbor.x,eastNeighbor.y) then
6193 return true
6194 end
6195 return false
6196end
6197
6198function RiverMap:SetRiverSizes(rainfallMap)
6199 local junctionList = {} --only include junctions not touching ocean in this list
6200 for y = 0,self.elevationMap.height - 1 do
6201 for x = 0,self.elevationMap.width - 1 do
6202 local i = self.elevationMap:GetIndex(x,y)
6203 if not self:IsTouchingOcean(self.riverData[i].northJunction) then
6204 table.insert(junctionList,self.riverData[i].northJunction)
6205 end
6206 if not self:IsTouchingOcean(self.riverData[i].southJunction) then
6207 table.insert(junctionList,self.riverData[i].southJunction)
6208 end
6209 end
6210 end
6211
6212 table.sort(junctionList,function (a,b) return a.altitude > b.altitude end)
6213
6214 for n=1,#junctionList do
6215 local junction = junctionList[n]
6216 local nextJunction = junction
6217 local i = self.elevationMap:GetIndex(junction.x,junction.y)
6218 while true do
6219 nextJunction.size = (nextJunction.size + rainfallMap.data[i]) * mg.riverRainCheatFactor
6220 if nextJunction.flow == mg.NOFLOW or self:IsTouchingOcean(nextJunction) then
6221 nextJunction.size = 0.0
6222 break
6223 end
6224 nextJunction = self:GetJunctionNeighbor(nextJunction.flow,nextJunction)
6225 end
6226 end
6227
6228 --now sort by river size to find river threshold
6229 table.sort(junctionList,function (a,b) return a.size > b.size end)
6230 local riverIndex = math.floor(mg.riverPercent * #junctionList)
6231 self.riverThreshold = junctionList[riverIndex].size
6232 if debugTime then print(string.format("river threshold = %f",self.riverThreshold)) end
6233
6234--~ local riverMap = FloatMap:New(self.elevationMap.width,self.elevationMap.height,self.elevationMap.xWrap,self.elevationMap.yWrap)
6235--~ for y = 0,self.elevationMap.height - 1 do
6236--~ for x = 0,self.elevationMap.width - 1 do
6237--~ local i = self.elevationMap:GetIndex(x,y)
6238--~ riverMap.data[i] = math.max(self.riverData[i].northJunction.size,self.riverData[i].southJunction.size)
6239--~ end
6240--~ end
6241--~ riverMap:Normalize()
6242 --riverMap:Save("riverSizeMap.csv")
6243end
6244
6245--This function returns the flow directions needed by civ
6246function RiverMap:GetFlowDirections(x,y)
6247 --if debugTime then print(string.format("Get flow dirs for %d,%d",x,y)) end
6248 local i = elevationMap:GetIndex(x,y)
6249
6250 local WOfRiver = mg.flowNONE
6251 local xx,yy = elevationMap:GetNeighbor(x,y,mg.NE)
6252 local ii = elevationMap:GetIndex(xx,yy)
6253 if ii ~= -1 and self.riverData[ii].southJunction.flow == mg.VERTFLOW and self.riverData[ii].southJunction.size > self.riverThreshold then
6254 --if debugTime then print(string.format("--NE(%d,%d) south flow=%d, size=%f",xx,yy,self.riverData[ii].southJunction.flow,self.riverData[ii].southJunction.size)) end
6255 WOfRiver = mg.flowS
6256 end
6257 xx,yy = elevationMap:GetNeighbor(x,y,mg.SE)
6258 ii = elevationMap:GetIndex(xx,yy)
6259 if ii ~= -1 and self.riverData[ii].northJunction.flow == mg.VERTFLOW and self.riverData[ii].northJunction.size > self.riverThreshold then
6260 --if debugTime then print(string.format("--SE(%d,%d) north flow=%d, size=%f",xx,yy,self.riverData[ii].northJunction.flow,self.riverData[ii].northJunction.size)) end
6261 WOfRiver = mg.flowN
6262 end
6263
6264 local NWOfRiver = mg.flowNONE
6265 xx,yy = elevationMap:GetNeighbor(x,y,mg.SE)
6266 ii = elevationMap:GetIndex(xx,yy)
6267 if ii ~= -1 and self.riverData[ii].northJunction.flow == mg.WESTFLOW and self.riverData[ii].northJunction.size > self.riverThreshold then
6268 NWOfRiver = mg.flowSW
6269 end
6270 if self.riverData[i].southJunction.flow == mg.EASTFLOW and self.riverData[i].southJunction.size > self.riverThreshold then
6271 NWOfRiver = mg.flowNE
6272 end
6273
6274 local NEOfRiver = mg.flowNONE
6275 xx,yy = elevationMap:GetNeighbor(x,y,mg.SW)
6276 ii = elevationMap:GetIndex(xx,yy)
6277 if ii ~= -1 and self.riverData[ii].northJunction.flow == mg.EASTFLOW and self.riverData[ii].northJunction.size > self.riverThreshold then
6278 NEOfRiver = mg.flowSE
6279 end
6280 if self.riverData[i].southJunction.flow == mg.WESTFLOW and self.riverData[i].southJunction.size > self.riverThreshold then
6281 NEOfRiver = mg.flowNW
6282 end
6283
6284 return WOfRiver,NWOfRiver,NEOfRiver
6285end
6286
6287--RiverHex class
6288RiverHex = inheritsFrom(nil)
6289function RiverHex:New(x, y)
6290 local new_inst = {}
6291 setmetatable(new_inst, {__index = RiverHex})
6292
6293 new_inst.x = x
6294 new_inst.y = y
6295 new_inst.northJunction = RiverJunction:New(x,y,true)
6296 new_inst.southJunction = RiverJunction:New(x,y,false)
6297
6298 return new_inst
6299end
6300
6301--RiverJunction class
6302RiverJunction = inheritsFrom(nil)
6303function RiverJunction:New(x,y,isNorth)
6304 local new_inst = {}
6305 setmetatable(new_inst, {__index = RiverJunction})
6306
6307 new_inst.x = x
6308 new_inst.y = y
6309 new_inst.isNorth = isNorth
6310 new_inst.altitude = 0.0
6311 new_inst.flow = mg.NOFLOW
6312 new_inst.size = 0.0
6313
6314 return new_inst
6315end
6316
6317
6318
6319
6320
6321
6322
6323
6324
6325--
6326-- Override AssignStartingPlots functions
6327--
6328
6329if overrideAssignStartingPlots then
6330------------------------------------------------------------------------------
6331function AssignStartingPlots:__CustomInit()
6332 -- This function included to provide a quick and easy override for changing
6333 -- any initial settings. Add your customized version to the map script.
6334 --
6335 if not debugPrint then
6336 print = function() end
6337 end
6338 --]]
6339 self.AdjustTiles = AssignStartingPlots.AdjustTiles
6340 self.BuffIslands = AssignStartingPlots.BuffIslands
6341 self.PlaceBonusResources = AssignStartingPlots.PlaceBonusResources
6342 --self.CalculateStrategicPlotWeights = AssignStartingPlots.CalculateStrategicPlotWeights
6343 --self.PlaceStrategicAndBonusResourcesCEP = AssignStartingPlots.PlaceStrategicAndBonusResourcesCEP
6344 self.islandAreaBuffed = {}
6345 --self.plotResInfo = {}
6346 --self.impactData = {}
6347end
6348------------------------------------------------------------------------------
6349function AssignStartingPlots:MeasureStartPlacementFertilityOfPlot(x, y, checkForCoastalLand)
6350 return Plot_GetFertility(Map.GetPlot(x, y))
6351end
6352------------------------------------------------------------------------------
6353function AssignStartingPlots:GenerateRegions(args)
6354 --print("Map Generation - Dividing the map in to Regions");
6355 -- This function stores its data in the instance (self) data table.
6356 --
6357 -- The "Three Methods" of regional division:
6358 -- 1. Biggest Landmass: All civs start on the biggest landmass.
6359 -- 2. Continental: Civs are assigned to continents. Any continents with more than one civ are divided.
6360 -- 3. Rectangular: Civs start within a given rectangle that spans the whole map, without regard to landmass sizes.
6361 -- This method is primarily applied to Archipelago and other maps with lots of tiny islands.
6362 -- 4. Rectangular: Civs start within a given rectangle defined by arguments passed in on the function call.
6363 -- Arguments required for this method: iWestX, iSouthY, iWidth, iHeight
6364 local args = args or {};
6365 local iW, iH = Map.GetGridSize();
6366 self.method = args.method or self.method; -- Continental method is default.
6367 self.resource_setting = args.resources or 2; -- Each map script has to pass in parameter for Resource setting chosen by user.
6368
6369 -- Determine number of civilizations and city states present in this game.
6370 self.iNumCivs, self.iNumCityStates, self.player_ID_list, self.bTeamGame, self.teams_with_major_civs, self.number_civs_per_team = GetPlayerAndTeamInfo()
6371 self.iNumCityStatesUnassigned = self.iNumCityStates;
6372 --print("-"); print("Civs:", self.iNumCivs); print("City States:", self.iNumCityStates);
6373
6374 if self.method == 1 then -- Biggest Landmass
6375 -- Identify the biggest landmass.
6376 local biggest_area = Map.FindBiggestArea(False);
6377 local iAreaID = biggest_area:GetID();
6378 -- We'll need all eight data fields returned in the results table from the boundary finder:
6379 local landmass_data = ObtainLandmassBoundaries(iAreaID);
6380 local iWestX = landmass_data[1];
6381 local iSouthY = landmass_data[2];
6382 local iEastX = landmass_data[3];
6383 local iNorthY = landmass_data[4];
6384 local iWidth = landmass_data[5];
6385 local iHeight = landmass_data[6];
6386 local wrapsX = landmass_data[7];
6387 local wrapsY = landmass_data[8];
6388
6389 -- Obtain "Start Placement Fertility" of the landmass. (This measurement is customized for start placement).
6390 -- This call returns a table recording fertility of all plots within a rectangle that contains the landmass,
6391 -- with a zero value for any plots not part of the landmass -- plus a fertility sum and plot count.
6392 local fert_table, fertCount, plotCount = self:MeasureStartPlacementFertilityOfLandmass(iAreaID,
6393 iWestX, iEastX, iSouthY, iNorthY, wrapsX, wrapsY);
6394 -- Now divide this landmass in to regions, one per civ.
6395 -- The regional divider requires three arguments:
6396 -- 1. Number of divisions. (For "Biggest Landmass" this means number of civs in the game).
6397 -- 2. Fertility table. (This was obtained from the last call.)
6398 -- 3. Rectangle table. This table includes seven data fields:
6399 -- westX, southY, width, height, AreaID, fertilityCount, plotCount
6400 -- This is why we got the fertCount and plotCount from the fertility function.
6401 --
6402 -- Assemble the Rectangle data table:
6403 local rect_table = {iWestX, iSouthY, iWidth, iHeight, iAreaID, fertCount, plotCount};
6404 -- The data from this call is processed in to self.regionData during the process.
6405 self:DivideIntoRegions(self.iNumCivs, fert_table, rect_table)
6406 -- The regions have been defined.
6407
6408 elseif self.method == 3 or self.method == 4 then -- Rectangular
6409 -- Obtain the boundaries of the rectangle to be processed.
6410 -- If no coords were passed via the args table, default to processing the entire map.
6411 -- Note that it matters if method 3 or 4 is designated, because the difference affects
6412 -- how city states are placed, whether they look for any uninhabited lands outside the rectangle.
6413 self.inhabited_WestX = args.iWestX or 0;
6414 self.inhabited_SouthY = args.iSouthY or 0;
6415 self.inhabited_Width = args.iWidth or iW;
6416 self.inhabited_Height = args.iHeight or iH;
6417
6418 -- Obtain "Start Placement Fertility" inside the rectangle.
6419 -- Data returned is: fertility table, sum of all fertility, plot count.
6420 local fert_table, fertCount, plotCount = self:MeasureStartPlacementFertilityInRectangle(self.inhabited_WestX,
6421 self.inhabited_SouthY, self.inhabited_Width, self.inhabited_Height)
6422 -- Assemble the Rectangle data table:
6423 local rect_table = {self.inhabited_WestX, self.inhabited_SouthY, self.inhabited_Width,
6424 self.inhabited_Height, -1, fertCount, plotCount}; -- AreaID -1 means ignore area IDs.
6425 -- Divide the rectangle.
6426 self:DivideIntoRegions(self.iNumCivs, fert_table, rect_table)
6427 -- The regions have been defined.
6428
6429 else -- Continental.
6430 --[[ Loop through all plots on the map, measuring fertility of each land
6431 plot, identifying its AreaID, building a list of landmass AreaIDs, and
6432 tallying the Start Placement Fertility for each landmass. ]]--
6433
6434 -- region_data: [WestX, EastX, SouthY, NorthY,
6435 -- numLandPlotsinRegion, numCoastalPlotsinRegion,
6436 -- numOceanPlotsinRegion, iRegionNetYield,
6437 -- iNumLandAreas, iNumPlotsinRegion]
6438 local best_areas = {};
6439 local globalFertilityOfLands = {};
6440
6441 -- Obtain info on all landmasses for comparision purposes.
6442 local iGlobalFertilityOfLands = 0;
6443 local iNumLandPlots = 0;
6444 local iNumLandAreas = 0;
6445 local land_area_IDs = {};
6446 local land_area_plots = {};
6447 local land_area_fert = {};
6448 -- Cycle through all plots in the world, checking their Start Placement Fertility and AreaID.
6449 for x = 0, iW - 1 do
6450 for y = 0, iH - 1 do
6451 local i = y * iW + x + 1;
6452 local plot = Map.GetPlot(x, y);
6453 if not plot:IsWater() then -- Land plot, process it.
6454 iNumLandPlots = iNumLandPlots + 1;
6455 local iArea = plot:GetArea();
6456 local plotFertility = self:MeasureStartPlacementFertilityOfPlot(x, y, true); -- Check for coastal land is enabled.
6457 iGlobalFertilityOfLands = iGlobalFertilityOfLands + plotFertility;
6458 --
6459 if TestMembership(land_area_IDs, iArea) == false then -- This plot is the first detected in its AreaID.
6460 iNumLandAreas = iNumLandAreas + 1;
6461 table.insert(land_area_IDs, iArea);
6462 land_area_plots[iArea] = 1;
6463 land_area_fert[iArea] = plotFertility;
6464 else -- This AreaID already known.
6465 land_area_plots[iArea] = land_area_plots[iArea] + 1;
6466 land_area_fert[iArea] = land_area_fert[iArea] + plotFertility;
6467 end
6468 end
6469 end
6470 end
6471
6472 --[[ Debug printout
6473 print("* * * * * * * * * *");
6474 for area_loop, AreaID in ipairs(land_area_IDs) do
6475 print("Area ID " .. AreaID .. " is land.");
6476 end
6477 --
6478 print("* * * * * * * * * *");
6479 for AreaID, fert in pairs(land_area_fert) do
6480 print("Area ID " .. AreaID .. " has fertility of " .. fert);
6481 end
6482 print("* * * * * * * * * *");
6483 --]]
6484
6485 -- Sort areas, achieving a list of AreaIDs with best areas first.
6486 --
6487 -- Fertility data in land_area_fert is stored with areaID index keys.
6488 -- Need to generate a version of this table with indices of 1 to n, where n is number of land areas.
6489 local interim_table = {};
6490 for loop_index, data_entry in pairs(land_area_fert) do
6491 table.insert(interim_table, data_entry);
6492 end
6493
6494 --[[for AreaID, fert in ipairs(interim_table) do
6495 print("Interim Table ID " .. AreaID .. " has fertility of " .. fert);
6496 end
6497 print("* * * * * * * * * *"); ]]--
6498
6499 -- Sort the fertility values stored in the interim table. Sort order in Lua is lowest to highest.
6500 table.sort(interim_table);
6501
6502 --[[
6503 for AreaID, fert in ipairs(interim_table) do
6504 print("Interim Table ID " .. AreaID .. " has fertility of " .. fert);
6505 end
6506 print("* * * * * * * * * *");
6507 --]]
6508
6509 -- If less players than landmasses, we will ignore the extra landmasses.
6510 local iNumRelevantLandAreas = math.min(iNumLandAreas, self.iNumCivs);
6511 -- Now re-match the AreaID numbers with their corresponding fertility values
6512 -- by comparing the original fertility table with the sorted interim table.
6513 -- During this comparison, best_areas will be constructed from sorted AreaIDs, richest stored first.
6514 local best_areas = {};
6515 -- Currently, the best yields are at the end of the interim table. We need to step backward from there.
6516 local end_of_interim_table = table.maxn(interim_table);
6517 -- We may not need all entries in the table. Process only iNumRelevantLandAreas worth of table entries.
6518 local fertility_value_list = {};
6519 local fertility_value_tie = false;
6520 for tableConstructionLoop = end_of_interim_table, (end_of_interim_table - iNumRelevantLandAreas + 1), -1 do
6521 if TestMembership(fertility_value_list, interim_table[tableConstructionLoop]) == true then
6522 fertility_value_tie = true;
6523 print("*** WARNING: Fertility Value Tie exists! ***");
6524 else
6525 table.insert(fertility_value_list, interim_table[tableConstructionLoop]);
6526 end
6527 end
6528
6529 if fertility_value_tie == false then -- No ties, so no need of special handling for ties.
6530 for areaTestLoop = end_of_interim_table, (end_of_interim_table - iNumRelevantLandAreas + 1), -1 do
6531 for loop_index, AreaID in ipairs(land_area_IDs) do
6532 if interim_table[areaTestLoop] == land_area_fert[land_area_IDs[loop_index]] then
6533 table.insert(best_areas, AreaID);
6534 break
6535 end
6536 end
6537 end
6538 else -- Ties exist! Special handling required to protect against a shortfall in the number of defined regions.
6539 local iNumUniqueFertValues = table.maxn(fertility_value_list);
6540 for fertLoop = 1, iNumUniqueFertValues do
6541 for AreaID, fert in pairs(land_area_fert) do
6542 if fert == fertility_value_list[fertLoop] then
6543 -- Add ties only if there is room!
6544 local best_areas_length = table.maxn(best_areas);
6545 if best_areas_length < iNumRelevantLandAreas then
6546 table.insert(best_areas, AreaID);
6547 else
6548 break
6549 end
6550 end
6551 end
6552 end
6553 end
6554
6555 --[[ Debug printout
6556 print("-"); print("--- Continental Division, Initial Readout ---"); print("-");
6557 print("- Global Fertility:", iGlobalFertilityOfLands);
6558 print("- Total Land Plots:", iNumLandPlots);
6559 print("- Total Areas:", iNumLandAreas);
6560 print("- Relevant Areas:", iNumRelevantLandAreas); print("-");
6561 --]]
6562
6563 --[[ Debug printout
6564 print("* * * * * * * * * *");
6565 for area_loop, AreaID in ipairs(best_areas) do
6566 print("Area ID " .. AreaID .. " has fertility of " .. land_area_fert[AreaID]);
6567 end
6568 print("* * * * * * * * * *");
6569 --]]
6570
6571 -- Assign continents to receive start plots. Record number of civs assigned to each landmass.
6572 local inhabitedAreaIDs = {};
6573 local numberOfCivsPerArea = table.fill(0, iNumRelevantLandAreas); -- Indexed in synch with best_areas. Use same index to match values from each table.
6574 for civToAssign = 1, self.iNumCivs do
6575 local bestRemainingArea;
6576 local bestRemainingFertility = 0;
6577 local bestAreaTableIndex;
6578 -- Loop through areas, find the one with the best remaining fertility (civs added
6579 -- to a landmass reduces its fertility rating for subsequent civs).
6580 --
6581 --print("- - Searching landmasses in order to place Civ #", civToAssign); print("-");
6582 for area_loop, AreaID in ipairs(best_areas) do
6583 local thisLandmassCurrentFertility = land_area_fert[AreaID] / (1 + numberOfCivsPerArea[area_loop]);
6584 if thisLandmassCurrentFertility > bestRemainingFertility then
6585 bestRemainingArea = AreaID;
6586 bestRemainingFertility = thisLandmassCurrentFertility;
6587 bestAreaTableIndex = area_loop;
6588 --
6589 --print("- Found new candidate landmass with Area ID#:", bestRemainingArea, " with fertility of ", bestRemainingFertility);
6590 end
6591 end
6592 -- Record results for this pass. (A landmass has been assigned to receive one more start point than it previously had).
6593 numberOfCivsPerArea[bestAreaTableIndex] = numberOfCivsPerArea[bestAreaTableIndex] + 1;
6594 if TestMembership(inhabitedAreaIDs, bestRemainingArea) == false then
6595 table.insert(inhabitedAreaIDs, bestRemainingArea);
6596 end
6597 --print("Civ #", civToAssign, "has been assigned to Area#", bestRemainingArea); print("-");
6598 end
6599 --print("-"); print("--- End of Initial Readout ---"); print("-");
6600
6601 --[[print("*** Number of Civs per Landmass - Table Readout ***");
6602 PrintContentsOfTable(numberOfCivsPerArea)
6603 print("--- End of Civs per Landmass readout ***"); print("-"); print("-");
6604 --]]
6605 -- Loop through the list of inhabited landmasses, dividing each landmass in to regions.
6606 -- Note that it is OK to divide a continent with one civ on it: this will assign the whole
6607 -- of the landmass to a single region, and is the easiest method of recording such a region.
6608 local iNumInhabitedLandmasses = table.maxn(inhabitedAreaIDs);
6609 for loop, currentLandmassID in ipairs(inhabitedAreaIDs) do
6610 -- Obtain the boundaries of and data for this landmass.
6611 local landmass_data = ObtainLandmassBoundaries(currentLandmassID);
6612 local iWestX = landmass_data[1];
6613 local iSouthY = landmass_data[2];
6614 local iEastX = landmass_data[3];
6615 local iNorthY = landmass_data[4];
6616 local iWidth = landmass_data[5];
6617 local iHeight = landmass_data[6];
6618 local wrapsX = landmass_data[7];
6619 local wrapsY = landmass_data[8];
6620 -- Obtain "Start Placement Fertility" of the current landmass. (Necessary to do this
6621 -- again because the fert_table can't be built prior to finding boundaries, and we had
6622 -- to ID the proper landmasses via fertility to be able to figure out their boundaries.
6623 local fert_table, fertCount, plotCount = self:MeasureStartPlacementFertilityOfLandmass(currentLandmassID,
6624 iWestX, iEastX, iSouthY, iNorthY, wrapsX, wrapsY);
6625 -- Assemble the rectangle data for this landmass.
6626 local rect_table = {iWestX, iSouthY, iWidth, iHeight, currentLandmassID, fertCount, plotCount};
6627 -- Divide this landmass in to number of regions equal to civs assigned here.
6628 iNumCivsOnThisLandmass = numberOfCivsPerArea[loop];
6629 if iNumCivsOnThisLandmass > 0 and iNumCivsOnThisLandmass <= 22 then -- valid number of civs.
6630
6631 --[[ Debug printout for regional division inputs.
6632 print("-"); print("- Region #: ", loop);
6633 print("- Civs on this landmass: ", iNumCivsOnThisLandmass);
6634 print("- Area ID#: ", currentLandmassID);
6635 print("- Fertility: ", fertCount);
6636 print("- Plot Count: ", plotCount); print("-");
6637 --]]
6638
6639 self:DivideIntoRegions(iNumCivsOnThisLandmass, fert_table, rect_table)
6640 else
6641 print("Invalid number of civs assigned to a landmass: ", iNumCivsOnThisLandmass);
6642 end
6643 end
6644 --
6645 -- The regions have been defined.
6646 end
6647
6648 -- Entry point for easier overrides.
6649 self:CustomOverride()
6650
6651 --[[ Printout is for debugging only. Deactivate otherwise.
6652 local tempRegionData = self.regionData;
6653 for i, data in ipairs(tempRegionData) do
6654 print("-");
6655 print("Data for Start Region #", i);
6656 print("WestX: ", data[1]);
6657 print("SouthY: ", data[2]);
6658 print("Width: ", data[3]);
6659 print("Height: ", data[4]);
6660 print("AreaID: ", data[5]);
6661 print("Fertility:", data[6]);
6662 print("Plots: ", data[7]);
6663 print("Fert/Plot:", data[8]);
6664 print("-");
6665 end
6666 --]]
6667end
6668------------------------------------------------------------------------------
6669function AssignStartingPlots:ExaminePlotForNaturalWondersEligibility(x, y)
6670 -- This function checks only for eligibility requirements applicable to all
6671 -- Natural Wonders. If a candidate plot passes all such checks, we will move
6672 -- on to checking it against specific needs for each particular wonderID.
6673 --
6674 -- Update, May 2011: Control over wonderID placement is being migrated to XML. Some checks here moved to there.
6675 local iW, iH = Map.GetGridSize();
6676 local plotIndex = iW * y + x + 1;
6677
6678 -- Check for collision with player starts
6679 if self.naturalWondersData[plotIndex] > 0 then
6680 return false
6681 end
6682
6683 -- Check the location is a decent city site, otherwise the wonderID is pointless
6684 local plot = Map.GetPlot(x, y);
6685 if Plot_GetFertilityInRange(plot, 3) < 12 then
6686 return false
6687 end
6688 return true
6689end
6690------------------------------------------------------------------------------
6691function AssignStartingPlots:PlaceNaturalWonders()
6692 local NW_eligibility_order = self:GenerateNaturalWondersCandidatePlotLists()
6693 local iNumNWCandidates = table.maxn(NW_eligibility_order);
6694 if iNumNWCandidates == 0 then
6695 print("No Natural Wonders placed, no eligible sites found for any of them.");
6696 return
6697 end
6698
6699 --[[ Debug printout
6700 print("-"); print("--- Readout of wonderID Assignment Priority ---");
6701 for loop, wonderID in ipairs(NW_eligibility_order) do
6702 print("wonderID Assignment Priority#", loop, "goes to wonderID ", self.wonder_list[wonderID]);
6703 end
6704 print("-"); print("-"); --]]
6705
6706 -- Determine how many NWs to attempt to place. Target is regulated per map size.
6707 -- The final number cannot exceed the number the map has locations to support.
6708 local target_number = mg.numNaturalWonders;
6709 local iNumNWtoPlace = math.min(target_number, iNumNWCandidates);
6710 local selected_NWs, fallback_NWs = {}, {};
6711 for loop, wonderID in ipairs(NW_eligibility_order) do
6712 if loop <= iNumNWtoPlace then
6713 table.insert(selected_NWs, wonderID);
6714 else
6715 table.insert(fallback_NWs, wonderID);
6716 end
6717 end
6718
6719 --[[
6720 print("-");
6721 for loop, wonderID in ipairs(selected_NWs) do
6722 print("Natural Wonder ", self.wonder_list[wonderID], "has been selected for placement.");
6723 end
6724 print("-");
6725 for loop, wonderID in ipairs(fallback_NWs) do
6726 print("Natural Wonder ", self.wonder_list[wonderID], "has been selected as fallback.");
6727 end
6728 print("-");
6729 --
6730 print("--- Placing Natural Wonders! ---");
6731 --]]
6732
6733 -- Place the NWs
6734 local iNumPlaced = 0;
6735 for loop, nw_number in ipairs(selected_NWs) do
6736 local nw_type = self.wonder_list[nw_number];
6737 -- Obtain the correct Row number from the xml Placement table.
6738 local row_number;
6739 for row in GameInfo.Natural_Wonder_Placement() do
6740 if row.NaturalWonderType == nw_type then
6741 row_number = row.ID;
6742 end
6743 end
6744 -- Place the wonder, using the correct row data from XML.
6745 local bSuccess = self:AttemptToPlaceNaturalWonder(nw_number, row_number)
6746 if bSuccess then
6747 iNumPlaced = iNumPlaced + 1;
6748 end
6749 end
6750 if iNumPlaced < iNumNWtoPlace then
6751 for loop, nw_number in ipairs(fallback_NWs) do
6752 if iNumPlaced >= iNumNWtoPlace then
6753 break
6754 end
6755 local nw_type = self.wonder_list[nw_number];
6756 -- Obtain the correct Row number from the xml Placement table.
6757 local row_number;
6758 for row in GameInfo.Natural_Wonder_Placement() do
6759 if row.NaturalWonderType == nw_type then
6760 row_number = row.ID;
6761 end
6762 end
6763 -- Place the wonder, using the correct row data from XML.
6764 local bSuccess = self:AttemptToPlaceNaturalWonder(nw_number, row_number)
6765 if bSuccess then
6766 iNumPlaced = iNumPlaced + 1;
6767 end
6768 end
6769 end
6770
6771 --
6772 if iNumPlaced >= iNumNWtoPlace then
6773 print("-- Placed all Natural Wonders --"); print("-"); print("-");
6774 else
6775 print("-- Not all Natural Wonders targeted got placed --"); print("-"); print("-");
6776 end
6777 --
6778
6779end
6780------------------------------------------------------------------------------
6781function AssignStartingPlots:CanPlaceCityStateAt(x, y, area_ID, force_it, ignore_collisions)
6782 local iW, iH = Map.GetGridSize();
6783 local plot = Map.GetPlot(x, y)
6784 local area = plot:GetArea()
6785 if area ~= area_ID and area_ID ~= -1 then
6786 return false
6787 end
6788
6789 if plot:IsWater() or plot:IsMountain() then
6790 return false
6791 end
6792
6793 -- Avoid natural wonders
6794 for nearPlot in Plot_GetPlotsInCircle(plot, 1, 4) do
6795 local featureInfo = GameInfo.Features[nearPlot:GetFeatureType()]
6796 if featureInfo and featureInfo.NaturalWonder then
6797 log:Debug("CanPlaceCityStateAt: avoided natural wonder %s", featureInfo.Type)
6798 return false
6799 end
6800 end
6801
6802 -- Reserve the best city sites for major civs
6803 local fertility = Plot_GetFertilityInRange(plot, 2)
6804 if fertility > 28 then
6805 log:Trace("CanPlaceCityStateAt: avoided fertility %s", fertility)
6806 return false
6807 end
6808
6809 local plotIndex = y * iW + x + 1;
6810 if self.cityStateData[plotIndex] > 0 and force_it == false then
6811 return false
6812 end
6813 local plotIndex = y * iW + x + 1;
6814 if self.playerCollisionData[plotIndex] == true and ignore_collisions == false then
6815 --print("-"); print("City State candidate plot rejected: collided with already-placed civ or City State at", x, y);
6816 return false
6817 end
6818 return true
6819end
6820------------------------------------------------------------------------------
6821function AssignStartingPlots:PlaceCityStateInRegion(city_state_number, region_number)
6822 --print("Place City State in Region called for City State", city_state_number, "Region", region_number);
6823 local iW, iH = Map.GetGridSize();
6824 local placed_city_state = false;
6825 local reached_middle = false;
6826 local region_data_table = self.regionData[region_number];
6827 local iWestX = region_data_table[1];
6828 local iSouthY = region_data_table[2];
6829 local iWidth = region_data_table[3];
6830 local iHeight = region_data_table[4];
6831 local iAreaID = region_data_table[5];
6832
6833 local eligible_coastal, eligible_inland = {}, {};
6834
6835 -- Main loop, first pass, unforced
6836 local x, y;
6837 local curWX = iWestX;
6838 local curSY = iSouthY;
6839 local curWid = iWidth;
6840 local curHei = iHeight;
6841 while placed_city_state == false and reached_middle == false do
6842 -- Send the remaining unprocessed portion of the region to be processed.
6843 local nextWX, nextSY, nextWid, nextHei;
6844 eligible_coastal, eligible_inland, nextWX, nextSY, nextWid, nextHei,
6845 reached_middle = self:ObtainNextSectionInRegion(curWX, curSY, curWid, curHei, iAreaID, false, false) -- Don't force it. Yet.
6846 curWX, curSY, curWid, curHei = nextWX, nextSY, nextWid, nextHei;
6847 -- Attempt to place city state using the two plot lists received from the last call.
6848 x, y, placed_city_state = self:PlaceCityState(eligible_coastal, eligible_inland, false, false) -- Don't need to re-check collisions.
6849 end
6850
6851 if placed_city_state == true then
6852 -- Record and enact the placement.
6853 self.cityStatePlots[city_state_number] = {x, y, region_number};
6854 self.city_state_validity_table[city_state_number] = true; -- This is the line that marks a city state as valid to be processed by the rest of the system.
6855 local city_state_ID = city_state_number + GameDefines.MAX_MAJOR_CIVS - 1;
6856 local cityState = Players[city_state_ID];
6857 local cs_start_plot = Map.GetPlot(x, y)
6858 cityState:SetStartingPlot(cs_start_plot)
6859 self:GenerateLuxuryPlotListsAtCitySite(x, y, 1, true) -- Removes Feature Ice from coasts near to the city state's new location
6860 self:PlaceResourceImpact(x, y, 5, 4) -- City State layer
6861 self:PlaceResourceImpact(x, y, 2, 3) -- Luxury layer
6862 self:PlaceResourceImpact(x, y, 3, 3) -- Bonus layer
6863 self:PlaceResourceImpact(x, y, 4, 3) -- Fish layer
6864 self:PlaceResourceImpact(x, y, 7, 3) -- Marble layer
6865 if cityState:GetMinorCivTrait() == MinorCivTraitTypes.MINOR_CIV_TRAIT_MILITARISTIC then
6866 self:PlaceResourceImpact(x, y, 1, 0) -- Strategic layer, at start point only.
6867 else
6868 self:PlaceResourceImpact(x, y, 1, 3) -- Strategic layer
6869 end
6870 local impactPlotIndex = y * iW + x + 1;
6871 self.playerCollisionData[impactPlotIndex] = true;
6872 --print("-"); print("City State", city_state_number, "has been started at Plot", x, y, "in Region#", region_number);
6873 else
6874 --print("-"); print("WARNING: Crowding issues for City State #", city_state_number, " - Could not find valid site in Region#", region_number);
6875 self.iNumCityStatesDiscarded = self.iNumCityStatesDiscarded + 1;
6876 end
6877end
6878--[[----------------------------------------------------------------------------
6879function AssignStartingPlots:AssignCityStatesToRegionsOrToUninhabited(args)
6880 -- Assign city states away from the main continent on Terra maps.
6881 local iW, iH = Map.GetGridSize()
6882 self.iNumCityStatesPerRegion = 0;
6883 local current_cs_index = 1;
6884
6885 -- Determine how many City States to place on uninhabited landmasses.
6886 -- Also generate lists of candidate plots from uninhabited areas.
6887 local iNumLandAreas = 0;
6888 local iNumCivLandmassPlots = 0;
6889 local iNumUninhabitedLandmassPlots = 0;
6890 local land_area_IDs = {};
6891 local land_area_plot_count = {};
6892 local land_area_plot_tables = {};
6893 local areas_inhabited_by_civs = {};
6894 local areas_too_small = {};
6895 local areas_uninhabited = {};
6896 --
6897 if self.method == 3 then -- Rectangular regional division spanning the entire globe, ALL plots belong to inhabited regions.
6898 self.iNumCityStatesUninhabited = 0;
6899 --print("Rectangular regional division spanning the whole world: all city states must belong to a region!");
6900 else -- Possibility of plots that do not belong to any civ's Region. Evaluate these plots and assign an appropriate number of City States to them.
6901 -- Generate list of inhabited area IDs.
6902 if self.method == 1 or self.method == 2 then
6903 for index, region_data in ipairs(self.regionData) do
6904 local region_areaID = region_data[5];
6905 if TestMembership(areas_inhabited_by_civs, region_areaID) == false then
6906 table.insert(areas_inhabited_by_civs, region_areaID);
6907 end
6908 end
6909 end
6910 -- Iterate through plots and, for each land area, generate a list of all its member plots
6911 for x = 0, iW - 1 do
6912 for y = 0, iH - 1 do
6913 local plotIndex = y * iW + x + 1;
6914 local plot = Map.GetPlot(x, y);
6915 local plotType = plot:GetPlotType()
6916 local terrainType = plot:GetTerrainType()
6917 if (plotType == PlotTypes.PLOT_LAND or plotType == PlotTypes.PLOT_HILLS) and terrainType ~= TerrainTypes.TERRAIN_SNOW then -- Habitable land plot, process it.
6918 local iArea = plot:GetArea();
6919 if self.method == 4 then -- Determine if plot is inside or outside the regional rectangle
6920 if (x >= self.inhabited_WestX and x <= self.inhabited_WestX + self.inhabited_Width - 1) and
6921 (y >= self.inhabited_SouthY and y <= self.inhabited_SouthY + self.inhabited_Height - 1) then -- Civ-inhabited rectangle
6922 iNumCivLandmassPlots = iNumCivLandmassPlots + 1;
6923 else
6924 iNumUninhabitedLandmassPlots = iNumUninhabitedLandmassPlots + plot_count;
6925 if self.plotDataIsCoastal[i] == true then
6926 table.insert(self.uninhabited_areas_coastal_plots, i);
6927 else
6928 table.insert(self.uninhabited_areas_inland_plots, i);
6929 end
6930 end
6931 else -- AreaID-based method must be applied, which cannot all be done in this loop
6932 if TestMembership(land_area_IDs, iArea) == false then -- This plot is the first detected in its AreaID.
6933 iNumLandAreas = iNumLandAreas + 1;
6934 table.insert(land_area_IDs, iArea);
6935 land_area_plot_count[iArea] = 1;
6936 land_area_plot_tables[iArea] = {plotIndex};
6937 else -- This AreaID already known.
6938 land_area_plot_count[iArea] = land_area_plot_count[iArea] + 1;
6939 table.insert(land_area_plot_tables[iArea], plotIndex);
6940 end
6941 end
6942 end
6943 end
6944 end
6945 -- Complete the AreaID-based method.
6946 if self.method == 1 or self.method == 2 then
6947 -- Obtain counts of inhabited and uninhabited plots. Identify areas too small to use for City States.
6948 for areaID, plot_count in pairs(land_area_plot_count) do
6949 if TestMembership(areas_inhabited_by_civs, areaID) == true then
6950 iNumCivLandmassPlots = iNumCivLandmassPlots + plot_count;
6951 else
6952 iNumUninhabitedLandmassPlots = iNumUninhabitedLandmassPlots + plot_count;
6953 if plot_count < 2 then
6954 table.insert(areas_too_small, areaID);
6955 else
6956 table.insert(areas_uninhabited, areaID);
6957 end
6958 end
6959 end
6960 -- Now loop through all Uninhabited Areas and append their plots to the candidates tables.
6961 for areaID, area_plot_list in pairs(land_area_plot_tables) do
6962 if TestMembership(areas_uninhabited, areaID) == true then
6963 for loop, plotIndex in ipairs(area_plot_list) do
6964 local x = (plotIndex - 1) % iW;
6965 local y = (plotIndex - x - 1) / iW;
6966 local plot = Map.GetPlot(x, y);
6967 local terrainType = plot:GetTerrainType();
6968 if terrainType ~= TerrainTypes.TERRAIN_SNOW then
6969 if self.plotDataIsCoastal[plotIndex] == true then
6970 table.insert(self.uninhabited_areas_coastal_plots, plotIndex);
6971 else
6972 table.insert(self.uninhabited_areas_inland_plots, plotIndex);
6973 end
6974 end
6975 end
6976 end
6977 end
6978 end
6979 -- Determine the number of City States to assign to uninhabited areas.
6980
6981 self.iNumCityStatesUninhabited = mg.offshoreCS * self.iNumCityStates;
6982 self.iNumCityStatesUnassigned = self.iNumCityStatesUnassigned - self.iNumCityStatesUninhabited;
6983 end
6984 --print("-"); print("City States assigned to Uninhabited Areas: ", self.iNumCityStatesUninhabited);
6985 -- Update the city state number.
6986 current_cs_index = current_cs_index + self.iNumCityStatesUninhabited;
6987
6988 if self.iNumCityStatesUnassigned > 0 then
6989 -- Determine how many to place in support of regions that share their luxury type with two other regions.
6990 local iNumRegionsSharedLux = 0;
6991 local shared_lux_IDs = {};
6992 for resource_ID, amount_assigned_to_regions in ipairs(self.luxury_assignment_count) do
6993 if amount_assigned_to_regions == 3 then
6994 iNumRegionsSharedLux = iNumRegionsSharedLux + 3;
6995 table.insert(shared_lux_IDs, resource_ID);
6996 end
6997 end
6998 if iNumRegionsSharedLux > 0 and iNumRegionsSharedLux <= self.iNumCityStatesUnassigned then
6999 self.iNumCityStatesSharedLux = iNumRegionsSharedLux;
7000 self.iNumCityStatesLowFertility = self.iNumCityStatesUnassigned - self.iNumCityStatesSharedLux;
7001 else
7002 self.iNumCityStatesLowFertility = self.iNumCityStatesUnassigned;
7003 end
7004 --print("CS Shared Lux: ", self.iNumCityStatesSharedLux, " CS Low Fert: ", self.iNumCityStatesLowFertility);
7005 -- Assign remaining types to their respective regions.
7006 if self.iNumCityStatesSharedLux > 0 then
7007 for loop, res_ID in ipairs(shared_lux_IDs) do
7008 for loop, region_lux_data in ipairs(self.regions_sorted_by_type) do
7009 local this_region_res = region_lux_data[2];
7010 if this_region_res == res_ID then
7011 self.city_state_region_assignments[current_cs_index] = region_lux_data[1];
7012 --print("-"); print("City State", current_cs_index, "assigned to Region#", region_lux_data[1], " to compensate for Shared Luxury ID#", res_ID);
7013 current_cs_index = current_cs_index + 1;
7014 self.iNumCityStatesUnassigned = self.iNumCityStatesUnassigned - 1;
7015 end
7016 end
7017 end
7018 end
7019 if self.iNumCityStatesLowFertility > 0 then
7020 -- If more to assign than number of regions, assign per region.
7021 while self.iNumCityStatesUnassigned >= self.iNumCivs do
7022 for current_region = 1, self.iNumCivs do
7023 self.city_state_region_assignments[current_cs_index] = current_region;
7024 --print("-"); print("City State", current_cs_index, "assigned to Region#", current_region, " to compensate for Low Fertility");
7025 current_cs_index = current_cs_index + 1;
7026 self.iNumCityStatesUnassigned = self.iNumCityStatesUnassigned - 1;
7027 end
7028 end
7029 if self.iNumCityStatesUnassigned > 0 then
7030 local fert_unsorted, fert_sorted, region_list = {}, {}, {};
7031 for region_num = 1, self.iNumCivs do
7032 local area_plots = self.regionTerrainCounts[region_num][2];
7033 local region_fertility = self.regionData[region_num][6];
7034 local fertility_per_land_plot = region_fertility / area_plots;
7035 --print("-"); print("Region#", region_num, "AreaPlots:", area_plots, "Region Fertility:", region_fertility, "Per Plot:", fertility_per_land_plot);
7036
7037 table.insert(fert_unsorted, {region_num, fertility_per_land_plot});
7038 table.insert(fert_sorted, fertility_per_land_plot);
7039 end
7040 table.sort(fert_sorted);
7041 for current_lowest_fertility, fert_value in ipairs(fert_sorted) do
7042 for loop, data_pair in ipairs(fert_unsorted) do
7043 local this_region_fert = data_pair[2];
7044 if this_region_fert == fert_value then
7045 local regionNum = data_pair[1];
7046 table.insert(region_list, regionNum);
7047 table.remove(fert_unsorted, loop);
7048 break
7049 end
7050 end
7051 end
7052 for loop = 1, self.iNumCityStatesUnassigned do
7053 self.city_state_region_assignments[current_cs_index] = region_list[loop];
7054 --print("-"); print("City State", current_cs_index, "assigned to Region#", region_list[loop], " to compensate for Low Fertility");
7055 current_cs_index = current_cs_index + 1;
7056 self.iNumCityStatesUnassigned = self.iNumCityStatesUnassigned - 1;
7057 end
7058 end
7059 end
7060 end
7061
7062 -- Debug check
7063 if self.iNumCityStatesUnassigned ~= 0 then
7064 print("Wrong number of City States assigned at end of assignment process. This number unassigned: ", self.iNumCityStatesUnassigned);
7065 else
7066 --print("All city states assigned.");
7067 end
7068end
7069--]]----------------------------------------------------------------------------
7070function AssignStartingPlots:BuffIslands()
7071 print("Buffing Tiny Islands")
7072 local biggestAreaSize = Map.FindBiggestArea(false):GetNumTiles()
7073 if biggestAreaSize < 20 then
7074 -- Skip on archipalego maps
7075 return
7076 end
7077 local resWeights = {
7078 [self.stone_ID] = 4,
7079 [self.coal_ID] = 4,
7080 [self.oil_ID] = 1,
7081 [self.aluminum_ID] = 1,
7082 [self.uranium_ID] = 2
7083 }
7084 for plotID, plot in Plots(Shuffle) do
7085 local plotType = plot:GetPlotType()
7086 local terrainType = plot:GetTerrainType()
7087 local area = plot:Area()
7088 local areaSize = area:GetNumTiles()
7089 if ((plotType == PlotTypes.PLOT_HILLS or plotType == PlotTypes.PLOT_LAND )
7090 and plot:GetResourceType() == -1
7091 and IsBetween(1, areaSize, 0.1 * biggestAreaSize)
7092 and not self.islandAreaBuffed[area:GetID()]
7093 )then
7094 local resID = GetRandomWeighted(resWeights)
7095 local resNum = 1
7096 if resID ~= self.stone_ID then
7097 resNum = resNum + Map.Rand(2, "BuffIslands Random Resource Quantity - Lua")
7098 if resID ~= self.uranium_ID then
7099 resNum = resNum + 1
7100 end
7101 end
7102 if resNum > 0 then
7103 self.islandAreaBuffed[area:GetID()] = true
7104 if 75 >= Map.Rand(100, "BuffIslands Chance - Lua") then
7105 if resID == self.coal_ID and plotType == PlotTypes.PLOT_LAND then
7106 if terrainType == TerrainTypes.TERRAIN_TUNDRA then
7107 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST, -1)
7108 elseif terrainType == TerrainTypes.TERRAIN_GRASS or terrainType == TerrainTypes.TERRAIN_PLAINS then
7109 plot:SetFeatureType(FeatureTypes.FEATURE_JUNGLE, -1)
7110 end
7111 end
7112 plot:SetResourceType(resID, resNum)
7113 self.amounts_of_resources_placed[resID + 1] = self.amounts_of_resources_placed[resID + 1] + resNum
7114 end
7115 end
7116 end
7117 end
7118end
7119------------------------------------------------------------------------------
7120function AssignStartingPlots:AdjustTiles()
7121 -- Sugar could not be made to look good in both jungle and open/marsh at the same time.
7122 -- Jon and I decided the best workaround would be to turn any Sugar/Jungle in to Marsh.
7123 local iW, iH = Map.GetGridSize()
7124 for y = 0, iH - 1 do
7125 for x = 0, iW - 1 do
7126 local plot = Map.GetPlot(x, y)
7127 local resID = plot:GetResourceType()
7128 local featureType = plot:GetFeatureType();
7129 if resID == self.sugar_ID then
7130 if featureType == FeatureTypes.FEATURE_JUNGLE then
7131 local plotID = plot:GetPlotType()
7132 if plotID ~= PlotTypes.PLOT_LAND then
7133 plot:SetPlotType(PlotTypes.PLOT_LAND, false, true)
7134 end
7135 plot:SetFeatureType(FeatureTypes.FEATURE_MARSH, -1)
7136 plot:SetTerrainType(TerrainTypes.TERRAIN_GRASS, false, true)
7137 --
7138 --print("-"); print("Fixed a Sugar/Jungle at plot", x, y);
7139 end
7140 elseif resID == self.deer_ID then
7141 if featureType == FeatureTypes.NO_FEATURE then
7142 local plotID = plot:GetPlotType()
7143 if plotID ~= PlotTypes.PLOT_LAND then
7144 plot:SetPlotType(PlotTypes.PLOT_LAND, false, true)
7145 end
7146 plot:SetFeatureType(FeatureTypes.FEATURE_FOREST, -1)
7147 plot:SetTerrainType(TerrainTypes.TERRAIN_GRASS, false, true)
7148 --
7149 --print("-"); print("Added forest to deer at plot", x, y);
7150 end
7151 end
7152 if plot:GetTerrainType() == TerrainTypes.TERRAIN_SNOW then
7153 if Plot_IsRiver(plot) then
7154 plot:SetTerrainType(TerrainTypes.TERRAIN_TUNDRA,false,true)
7155 end
7156 end
7157
7158 if plot:IsHills() and featureType == FeatureTypes.FEATURE_JUNGLE then
7159 plot:SetTerrainType(TerrainTypes.TERRAIN_GRASS, false, true)
7160 Game.SetPlotExtraYield( x, y, YieldTypes.YIELD_FOOD, -1)
7161 end
7162
7163 --BuffDeserts(plot)
7164 end
7165 end
7166end
7167------------------------------------------------------------------------------
7168function BuffDeserts(plot)
7169 if Cep then
7170 return
7171 end
7172 if plot:GetTerrainType() ~= TerrainTypes.TERRAIN_DESERT or plot:IsHills() or plot:GetFeatureType() ~= -1 then
7173 return
7174 end
7175
7176 local resInfo = GameInfo.Resources[plot:GetResourceType()]
7177 if plot:IsFreshWater() then
7178 Game.SetPlotExtraYield( x, y, YieldTypes.YIELD_FOOD, 1)
7179 elseif resInfo then
7180 if resInfo.Type == "RESOURCE_STONE" then
7181 Game.SetPlotExtraYield( x, y, YieldTypes.YIELD_PRODUCTION, 2)
7182 elseif resInfo.ResourceClassType == "RESOURCECLASS_BONUS" then
7183 Game.SetPlotExtraYield( x, y, YieldTypes.YIELD_FOOD, 1)
7184 elseif resInfo.Happiness > 0 then
7185 Game.SetPlotExtraYield( x, y, YieldTypes.YIELD_GOLD, 1)
7186 elseif not resInfo.TechReveal then
7187 Game.SetPlotExtraYield( x, y, YieldTypes.YIELD_PRODUCTION, 1)
7188 end
7189 end
7190end
7191------------------------------------------------------------------------------
7192function AssignStartingPlots:ProcessResourceList(frequency, impact_table_number, plot_list, resources_to_place)
7193 -- Added a random factor to strategic resources - Thalassicus
7194
7195 -- This function needs to receive two numbers and two tables.
7196 -- Length of the plotlist is divided by frequency to get the number of
7197 -- resources to place. .. The first table is a list of plot indices.
7198 -- The second table contains subtables, one per resource type, detailing the
7199 -- resource ID number, quantity, weighting, and impact radius of each applicable
7200 -- resource. If radius min and max are different, the radius length is variable
7201 -- and a die roll will determine a value >= min and <= max.
7202 --
7203 -- The system may be easiest to manage if the weightings add up to 100, so they
7204 -- can be handled as percentages, but this is not required.
7205 --
7206 -- Impact #s - 1 strategic - 2 luxury - 3 bonus
7207 -- Res data - 1 ID - 2 quantity - 3 weight - 4 radius min - 5 radius max
7208 --
7209 -- The plot list will be processed sequentially, so randomize it in advance.
7210 -- The default lists are terrain-oriented and are randomized during __Init
7211 if plot_list == nil then
7212 --print("Plot list was nil! -ProcessResourceList");
7213 return
7214 end
7215 local iW, iH = Map.GetGridSize();
7216 local iNumTotalPlots = table.maxn(plot_list);
7217 local iNumResourcesToPlace = math.ceil(iNumTotalPlots / frequency);
7218 local iNumResourcesTypes = table.maxn(resources_to_place);
7219 local res_ID, res_quantity, res_weight, res_min, res_max, res_range, res_threshold = {}, {}, {}, {}, {}, {}, {};
7220 local totalWeight, accumulatedWeight = 0, 0;
7221 for index, resource_data in ipairs(resources_to_place) do
7222 res_ID[index] = resource_data[1];
7223 res_quantity[index] = resource_data[2];
7224 res_weight[index] = resource_data[3];
7225 totalWeight = totalWeight + resource_data[3];
7226 res_min[index] = resource_data[4];
7227 res_max[index] = resource_data[5];
7228 if res_max[index] > res_min[index] then
7229 res_range[index] = res_max[index] - res_min[index] + 1;
7230 else
7231 res_range[index] = -1;
7232 end
7233 end
7234 for index = 1, iNumResourcesTypes do
7235 -- We'll roll a die and check each resource in turn to see if it is
7236 -- the one to get placed in that particular case. The weightings are
7237 -- used to decide how much percentage of the total each represents.
7238 -- This chunk sets the threshold for each resource in turn.
7239 local threshold = (res_weight[index] + accumulatedWeight) * 10000 / totalWeight;
7240 table.insert(res_threshold, threshold);
7241 accumulatedWeight = accumulatedWeight + res_weight[index];
7242 end
7243 -- Main loop
7244 local current_index = 1;
7245 local avoid_ripples = true;
7246 for place_resource = 1, iNumResourcesToPlace do
7247 local placed_this_res = false;
7248 local use_this_res_index = 1;
7249 local diceroll = Map.Rand(10000, "Choose resource type - Distribute Resources - Lua");
7250 for index, threshold in ipairs(res_threshold) do
7251 if diceroll < threshold then -- Choose this resource type.
7252 use_this_res_index = index;
7253 break
7254 end
7255 end
7256 if avoid_ripples == true then -- Still on first pass through plot_list, seek first eligible 0 value on impact matrix.
7257 for index_to_check = current_index, iNumTotalPlots do
7258 if index_to_check == iNumTotalPlots then -- Completed first pass of plot_list, now change to seeking lowest value instead of zero value.
7259 avoid_ripples = false;
7260 end
7261 if placed_this_res == true then
7262 break
7263 else
7264 current_index = current_index + 1;
7265 end
7266 local plotIndex = plot_list[index_to_check];
7267 if impact_table_number == 1 then
7268 if self.strategicData[plotIndex] == 0 then
7269 local x = (plotIndex - 1) % iW;
7270 local y = (plotIndex - x - 1) / iW;
7271 local res_plot = Map.GetPlot(x, y)
7272 if res_plot:GetResourceType() == -1 then -- Placing this strategic resource in this plot.
7273 local res_addition = 0;
7274 if res_range[use_this_res_index] ~= -1 then
7275 res_addition = Map.Rand(res_range[use_this_res_index], "Resource Radius - Place Resource LUA");
7276 end
7277 local randValue = Map.Rand(self.resource_setting + 1, "Place Strategic Resource - Lua")
7278 local quantity = res_quantity[use_this_res_index] + randValue
7279 --print(string.format("ProcessResourceList table 1, Resource: %20s, Quantity: %s + %s - 1", GameInfo.Resources[res_ID[use_this_res_index]].Type, res_quantity[use_this_res_index], randValue));
7280 res_plot:SetResourceType(res_ID[use_this_res_index], quantity);
7281 if (Game.GetResourceUsageType(res_ID[use_this_res_index]) == ResourceUsageTypes.RESOURCEUSAGE_LUXURY) then
7282 self.totalLuxPlacedSoFar = self.totalLuxPlacedSoFar + 1;
7283 end
7284 self:PlaceResourceImpact(x, y, impact_table_number, res_min[use_this_res_index] + res_addition);
7285 placed_this_res = true;
7286 self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] = self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] + res_quantity[use_this_res_index];
7287 end
7288 end
7289 elseif impact_table_number == 2 then
7290 if self.luxuryData[plotIndex] == 0 then
7291 local x = (plotIndex - 1) % iW;
7292 local y = (plotIndex - x - 1) / iW;
7293 local res_plot = Map.GetPlot(x, y)
7294 if res_plot:GetResourceType() == -1 then -- Placing this luxury resource in this plot.
7295 local res_addition = 0;
7296 if res_range[use_this_res_index] ~= -1 then
7297 res_addition = Map.Rand(res_range[use_this_res_index], "Resource Radius - Place Resource LUA");
7298 end
7299 --print("ProcessResourceList table 2, Resource: " .. res_ID[use_this_res_index] .. ", Quantity: " .. res_quantity[use_this_res_index]);
7300 res_plot:SetResourceType(res_ID[use_this_res_index], res_quantity[use_this_res_index]);
7301 self:PlaceResourceImpact(x, y, impact_table_number, res_min[use_this_res_index] + res_addition);
7302 placed_this_res = true;
7303 self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] = self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] + 1;
7304 end
7305 end
7306 elseif impact_table_number == 3 then
7307 if self.bonusData[plotIndex] == 0 then
7308 local x = (plotIndex - 1) % iW;
7309 local y = (plotIndex - x - 1) / iW;
7310 local res_plot = Map.GetPlot(x, y)
7311 if res_plot:GetResourceType() == -1 then -- Placing this bonus resource in this plot.
7312 local res_addition = 0;
7313 if res_range[use_this_res_index] ~= -1 then
7314 res_addition = Map.Rand(res_range[use_this_res_index], "Resource Radius - Place Resource LUA");
7315 end
7316 --print("ProcessResourceList table 3, Resource: " .. res_ID[use_this_res_index] .. ", Quantity: " .. res_quantity[use_this_res_index]);
7317 res_plot:SetResourceType(res_ID[use_this_res_index], res_quantity[use_this_res_index]);
7318 self:PlaceResourceImpact(x, y, impact_table_number, res_min[use_this_res_index] + res_addition);
7319 placed_this_res = true;
7320 self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] = self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] + 1;
7321 if res_ID[use_this_res_index] == self.stone_ID then
7322 self.islandAreaBuffed[res_plot:GetArea()] = true
7323 end
7324 end
7325 end
7326 end
7327 end
7328 end
7329 if avoid_ripples == false then -- Completed first pass through plot_list, so use backup method.
7330 local lowest_impact = 98;
7331 local best_plot;
7332 for loop, plotIndex in ipairs(plot_list) do
7333 if impact_table_number == 1 then
7334 if lowest_impact > self.strategicData[plotIndex] then
7335 local x = (plotIndex - 1) % iW;
7336 local y = (plotIndex - x - 1) / iW;
7337 local res_plot = Map.GetPlot(x, y)
7338 if res_plot:GetResourceType() == -1 then
7339 lowest_impact = self.strategicData[plotIndex];
7340 best_plot = plotIndex;
7341 end
7342 end
7343 elseif impact_table_number == 2 then
7344 if lowest_impact > self.luxuryData[plotIndex] then
7345 local x = (plotIndex - 1) % iW;
7346 local y = (plotIndex - x - 1) / iW;
7347 local res_plot = Map.GetPlot(x, y)
7348 if res_plot:GetResourceType() == -1 then
7349 lowest_impact = self.luxuryData[plotIndex];
7350 best_plot = plotIndex;
7351 end
7352 end
7353 elseif impact_table_number == 3 then
7354 if lowest_impact > self.bonusData[plotIndex] then
7355 local x = (plotIndex - 1) % iW;
7356 local y = (plotIndex - x - 1) / iW;
7357 local res_plot = Map.GetPlot(x, y)
7358 if res_plot:GetResourceType() == -1 then
7359 lowest_impact = self.bonusData[plotIndex];
7360 best_plot = plotIndex;
7361 end
7362 end
7363 end
7364 end
7365 if best_plot ~= nil then
7366 local x = (best_plot - 1) % iW;
7367 local y = (best_plot - x - 1) / iW;
7368 local res_plot = Map.GetPlot(x, y)
7369 local res_addition = 0;
7370 if res_range[use_this_res_index] ~= -1 then
7371 res_addition = Map.Rand(res_range[use_this_res_index], "Resource Radius - Place Resource LUA");
7372 end
7373 --print("ProcessResourceList backup, Resource: " .. res_ID[use_this_res_index] .. ", Quantity: " .. res_quantity[use_this_res_index]);
7374 res_plot:SetResourceType(res_ID[use_this_res_index], res_quantity[use_this_res_index]);
7375 self:PlaceResourceImpact(x, y, impact_table_number, res_min[use_this_res_index] + res_addition);
7376 self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] = self.amounts_of_resources_placed[res_ID[use_this_res_index] + 1] + res_quantity[use_this_res_index];
7377 end
7378 end
7379 end
7380end
7381------------------------------------------------------------------------------
7382function AssignStartingPlots:PlaceSpecificNumberOfResources(resource_ID, quantity, amount,
7383 ratio, impact_table_number, min_radius, max_radius, plot_list)
7384 -- This function needs to receive seven numbers and one table.
7385 --
7386 -- Resource_ID is the type of resource to place.
7387 -- Quantity is the in-game quantity of the resource, or 0 if unquantified resource type.
7388 -- Amount is the number of plots intended to receive an assignment of this resource.
7389 --
7390 -- Ratio should be > 0 and <= 1 and is what determines when secondary and tertiary lists
7391 -- come in to play. The actual ratio is (AmountOfResource / PlotsInList). For instance,
7392 -- if we are assigning Sugar resources to Marsh, then if we are to assign eight Sugar
7393 -- resources, but there are only four Marsh plots in the list, a ratio of 1 would assign
7394 -- a Sugar to every single marsh plot, and then have to return an unplaced value of 4;
7395 -- but a ratio of 0.5 would assign only two Sugars to the four marsh plots, and return a
7396 -- value of 6. Any ratio less than or equal to 0.25 would assign one Sugar and return
7397 -- seven, as the ratio results will be rounded up not down, to the nearest integer.
7398 --
7399 -- Impact tables: -1 = ignore, 1 = strategic, 2 = luxury, 3 = bonus, 4 = fish
7400 -- Radius is amount of impact to place on this table when placing a resource.
7401 --
7402 -- nil tables are not acceptable but empty tables are fine
7403 --
7404 -- The plot lists will be processed sequentially, so randomize them in advance.
7405 --
7406
7407 --print("-"); print("PlaceSpecificResource called. ResID:", resource_ID, "Quantity:", quantity, "Amount:", amount, "Ratio:", ratio);
7408
7409 if plot_list == nil then
7410 --print("Plot list was nil! -PlaceSpecificNumberOfResources");
7411 return
7412 end
7413 local bCheckImpact = false;
7414 local impact_table = {};
7415 if impact_table_number == 1 then
7416 bCheckImpact = true;
7417 impact_table = self.strategicData;
7418 elseif impact_table_number == 2 then
7419 bCheckImpact = true;
7420 impact_table = self.luxuryData;
7421 elseif impact_table_number == 3 then
7422 bCheckImpact = true;
7423 impact_table = self.bonusData;
7424 elseif impact_table_number == 4 then
7425 bCheckImpact = true;
7426 impact_table = self.fishData;
7427 elseif impact_table_number ~= -1 then
7428 bCheckImpact = true;
7429 impact_table = self.impactData[impact_table_number];
7430 end
7431 local iW, iH = Map.GetGridSize();
7432 local iNumLeftToPlace = amount;
7433 local iNumPlots = table.maxn(plot_list);
7434 local iNumResources = math.min(amount, math.ceil(ratio * iNumPlots));
7435 -- Main loop
7436 for place_resource = 1, iNumResources do
7437 for loop, plotIndex in ipairs(plot_list) do
7438 if not bCheckImpact or impact_table[plotIndex] == 0 then
7439 local x = (plotIndex - 1) % iW;
7440 local y = (plotIndex - x - 1) / iW;
7441 local res_plot = Map.GetPlot(x, y)
7442 if res_plot:GetResourceType(-1) == -1 then -- Placing this resource in this plot.
7443 res_plot:SetResourceType(resource_ID, quantity);
7444 self.amounts_of_resources_placed[resource_ID + 1] = self.amounts_of_resources_placed[resource_ID + 1] + quantity;
7445 --print("-"); print("Placed Resource#", resource_ID, "at Plot", x, y);
7446 self.totalLuxPlacedSoFar = self.totalLuxPlacedSoFar + 1;
7447 iNumLeftToPlace = iNumLeftToPlace - 1;
7448 if bCheckImpact == true then
7449 local res_addition = 0;
7450 if max_radius > min_radius then
7451 res_addition = Map.Rand(1 + (max_radius - min_radius), "Resource Radius - Place Resource LUA");
7452 end
7453 local rad = min_radius + res_addition;
7454 self:PlaceResourceImpact(x, y, impact_table_number, rad)
7455 end
7456 break
7457 end
7458 end
7459 end
7460 end
7461 return iNumLeftToPlace
7462end
7463------------------------------------------------------------------------------
7464function AssignStartingPlots:GetMajorStrategicResourceQuantityValues()
7465 -- This function determines quantity per tile for each strategic resource's major deposit size.
7466 -- Note: scripts that cannot place Oil in the sea need to increase amounts on land to compensate.
7467 local uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = 4, 4, 7, 6, 7, 8;
7468 -- Check the resource setting.
7469 if self.resource_setting == 1 then -- Sparse
7470 uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = 2, 4, 5, 4, 5, 5;
7471 elseif self.resource_setting == 3 then -- Abundant
7472 uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = 4, 6, 9, 9, 10, 10;
7473 end
7474 return uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt
7475end
7476------------------------------------------------------------------------------
7477function AssignStartingPlots:GetSmallStrategicResourceQuantityValues()
7478 -- This function determines quantity per tile for each strategic resource's small deposit size.
7479 local uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = 2, 2, 2, 2, 2, 3;
7480 -- Check the resource setting.
7481 if self.resource_setting == 1 then -- Sparse
7482 uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = 1, 1, 2, 1, 2, 2;
7483 elseif self.resource_setting == 3 then -- Abundant
7484 uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = 2, 3, 3, 3, 3, 3;
7485 end
7486 return uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt
7487end
7488------------------------------------------------------------------------------
7489function AssignStartingPlots:PlaceOilInTheSea()
7490 -- Places sources of Oil in Coastal waters, equal to half what's on the
7491 -- land. If the map has too little ocean, then whatever will fit.
7492 --
7493 -- WARNING: This operation will render the Strategic Resource Impact Table useless for
7494 -- further operations, so should always be called last, even after minor placements.
7495 local sea_oil_amt = 2 --+ Map.Rand(self.resource_setting, "PlaceOilInTheSea - Lua");
7496 local iNumLandOilUnits = self.amounts_of_resources_placed[self.oil_ID + 1];
7497 local iNumToPlace = math.floor((iNumLandOilUnits / 2) / sea_oil_amt);
7498
7499 --print("Adding Oil resources to the Sea.");
7500 self:PlaceSpecificNumberOfResources(self.oil_ID, sea_oil_amt, iNumToPlace, 0.2, 1, 4, 7, self.coast_list)
7501end
7502--[=[----------------------------------------------------------------------------
7503function AssignStartingPlots:CalculateStrategicPlotWeights()
7504 local iW, iH = Map.GetGridSize()
7505 local resIDs = Game.GetResourceIDsOfUsage(ResourceUsageTypes.RESOURCEUSAGE_STRATEGIC)
7506 local plotTypeStrings = {}
7507 plotTypeStrings[-1] = "NO_PLOT"
7508 plotTypeStrings[0] = "PLOT_MOUNTAIN"
7509 plotTypeStrings[1] = "PLOT_HILLS"
7510 plotTypeStrings[2] = "PLOT_LAND"
7511 plotTypeStrings[3] = "PLOT_OCEAN"
7512
7513 print("CalculateStrategicPlotWeights")
7514
7515 -- Initialize tables
7516 for resInfo in GameInfo.Resources() do
7517 local resourceID = resInfo.ID
7518 self.plotResInfo[resourceID] = {}
7519 for x = 0, iW - 1 do
7520 for y = 0, iH - 1 do
7521 local plotID = y * iW + x + 1
7522 self.plotResInfo[resourceID][plotID] = {}
7523 self.plotResInfo[resourceID][plotID].numPlots = 0
7524 self.plotResInfo[resourceID][plotID].weight = 1
7525 self.plotResInfo[resourceID][plotID].weightAvg = 0
7526 self.impactData [100+resourceID] = {}
7527 self.impactData [100+resourceID][plotID] = 0
7528 end
7529 end
7530 end
7531
7532 -- Pass 1: Plot Weights
7533 for x = 0, iW - 1 do
7534 for y = 0, iH - 1 do
7535 local plot = Map.GetPlot(x, y)
7536 local plotID = y * iW + x + 1
7537 local typeTable = {}
7538 typeTable.PlotType = plotTypeStrings[plot:GetPlotType()]
7539 typeTable.NotLake = not plot:IsLake()
7540 typeTable.Freshwater = plot:IsFreshWater()
7541 typeTable.TerrainType = plot:GetTerrainType()
7542 typeTable.FeatureType = plot:GetFeatureType()
7543 typeTable.TerrainType = (typeTable.TerrainType == TerrainTypes.NO_TERRAIN) and "NO_TERRAIN" or GameInfo.Terrains[typeTable.TerrainType].Type
7544 typeTable.FeatureType = (typeTable.FeatureType == FeatureTypes.NO_FEATURE) and "NO_FEATURE" or GameInfo.Features[typeTable.FeatureType].Type
7545
7546 for resInfo in GameInfo.Resource_TerrainWeights() do
7547 local resID = GameInfo.Resources[resInfo.ResourceType].ID
7548 local useWeight = true
7549 for k, v in pairs(typeTable) do
7550 if resInfo[k] and resInfo[k] ~= v then
7551 useWeight = false
7552 end
7553 end
7554 if useWeight then
7555 self.plotResInfo[resID][plotID].weight = self.plotResInfo[resID][plotID].weight * resInfo.Weight
7556 end
7557 end
7558 end
7559 end
7560 --
7561
7562 -- Pass 2: Plot Weight Averages
7563 local randVariance = 100
7564 for x = 0, iW - 1 do
7565 for y = 0, iH - 1 do
7566 local plot = Map.GetPlot(x, y)
7567 local plotID = y * iW + x + 1
7568 local terrainType = plot:GetTerrainType()
7569 for nearPlot, distance in Plot_GetPlotsInCircle(plot, 0, Cep.RESOURCE_PLOT_BLUR_DISTANCE) do
7570 local nearPlotID = Plot_GetID(nearPlot)
7571 local nearPlotType = nearPlot:GetPlotType()
7572 for _, resID in pairs(resIDs) do
7573 print(string.format(("self.plotResInfo[resID]=%s nearPlotID=%s", nearPlotID, tostring(self.plotResInfo[resID]))))
7574 local nearWeight = self.plotResInfo[resID]
7575 nearWeight = nearWeight[nearPlotID]
7576 nearWeight = nearWeight.weight
7577 if (nearPlotType == PlotTypes.PLOT_LAND
7578 or nearPlotType == PlotTypes.PLOT_HILLS
7579 or (resID ~= self.oil_ID and self.plotResInfo[resID][nearPlotID].weight ~= 1)
7580 )then
7581 self.plotResInfo[resID][plotID].weightAvg = self.plotResInfo[resID][plotID].weightAvg + nearWeight / (distance + 1)
7582 self.plotResInfo[resID][plotID].numPlots = self.plotResInfo[resID][plotID].numPlots + 1 / (distance + 1)
7583 end
7584 --
7585 if resID == self.oil_ID and plot:GetTerrainType() == TerrainTypes.TERRAIN_COAST and not plot:IsLake() then
7586 if nearPlotType ~= PlotTypes.PLOT_OCEAN then
7587 -- Sea oil favors terrain where land oil avoids
7588 if nearWeight == 0 then
7589 nearWeight = 0.1
7590 end
7591 nearWeight = 1 / nearWeight
7592 end
7593 self.plotResInfo[resID][plotID].weightAvg = self.plotResInfo[resID][plotID].weightAvg + nearWeight / (distance + 1)
7594 self.plotResInfo[resID][plotID].numPlots = self.plotResInfo[resID][plotID].numPlots + 1 / (distance + 1)
7595 end
7596 end
7597 end
7598 for _, resID in pairs(resIDs) do
7599 if self.plotResInfo[resID][plotID].numPlots == 0 then
7600 self.plotResInfo[resID][plotID].numPlots = 1
7601 end
7602 local rand = 0.01 * Map.Rand(randVariance, "AssignStartingPlots:PlaceStrategicAndBonusResources()") / randVariance
7603 self.plotResInfo[resID][plotID].weightAvg = rand + self.plotResInfo[resID][plotID].weightAvg / self.plotResInfo[resID][plotID].numPlots --* self.plotResInfo[resID][plotID].weight
7604 end
7605 local testNum = 0--math.min(120, math.floor(self.plotResInfo[self.oil_ID][plotID].weightAvg * 10))
7606 if testNum >= 1 then
7607 self:PlaceSpecificNumberOfResources(self.iron_ID, testNum, 1, 1, -1, 0, 0, {plotID})
7608 end
7609 end
7610 end
7611end
7612------------------------------------------------------------------------------
7613function AssignStartingPlots:GetRegionStrategicPlotList(regionInfo)
7614 local iW, iH = Map.GetGridSize()
7615 local plotList = {}
7616 local iWestX = regionInfo[1]
7617 local iSouthY = regionInfo[2]
7618 local iWidth = regionInfo[3]
7619 local iHeight = regionInfo[4]
7620 local resIDs = Game.GetResourceIDsOfUsage(ResourceUsageTypes.RESOURCEUSAGE_STRATEGIC)
7621
7622 for _, resID in pairs(resIDs) do
7623 plotList[resID] = {}
7624 end
7625
7626 for region_loop_y = 0, iHeight - 1 do
7627 for region_loop_x = 0, iWidth - 1 do
7628 local x = (region_loop_x + iWestX) % iW
7629 local y = (region_loop_y + iSouthY) % iH
7630 local plotID = y * iW + x + 1
7631 local plot = Map.GetPlot(x, y)
7632 local plotType = plot:GetPlotType()
7633 local featureType = plot:GetFeatureType()
7634 for _, resID in pairs(resIDs) do
7635 if self.plotResInfo[resID][plotID].weightAvg > 0 then
7636 if plotType == PlotTypes.PLOT_HILLS or plotType == PlotTypes.PLOT_LAND then
7637 if plotType == PlotTypes.PLOT_LAND and (resID == self.aluminum_ID or resID == self.coal_ID) then
7638 self.plotResInfo[resID][plotID].weightAvg = self.plotResInfo[resID][plotID].weightAvg * 0.1
7639 end
7640 if featureType == FeatureTypes.NO_FEATURE then
7641 table.insert(plotList[resID], plotID)
7642 else
7643 local feature = GameInfo.Features[featureType]
7644 if not (feature.Impassable or feature.NoImprovement or feature.NaturalWonder) then
7645 table.insert(plotList[resID], plotID)
7646 end
7647 end
7648 elseif resID == self.oil_ID and plot:GetTerrainType() == TerrainTypes.TERRAIN_COAST and plot:IsAdjacentToLand() and plot:GetFeatureType() == FeatureTypes.NO_FEATURE and not plot:IsLake() then
7649 table.insert(plotList[resID], plotID)
7650 end
7651 end
7652 end
7653 end
7654 end
7655 return plotList
7656end
7657------------------------------------------------------------------------------
7658function AssignStartingPlots:PlaceStrategicResourceInPlots(plotList, resID, resRemaining, maxDepositSize)
7659 local iW, iH = Map.GetGridSize()
7660 local depositSize = math.min(resRemaining, 1 + Map.Rand(maxDepositSize, "Strategic Resource Placement"))
7661 for _, plotID in ipairs(plotList) do
7662 local x = (plotID - 1) % iW;
7663 local y = (plotID - x - 1) / iW;
7664 local plot = Map.GetPlot(x, y)
7665 if plot:GetResourceType() == -1 then
7666 --print(" placed %s", depositSize)
7667 plot:SetResourceType(resID, depositSize);
7668 self.amounts_of_resources_placed[resID + 1] = self.amounts_of_resources_placed[resID + 1] + depositSize;
7669 resRemaining = resRemaining - depositSize;
7670
7671 for nearPlot, distance in Plot_GetPlotsInCircle(plot, 0, Cep.RESOURCE_PLOT_BLUR_DISTANCE) do
7672 local nearPlotID = Plot_GetID(nearPlot)
7673 self.plotResInfo[resID][nearPlotID].weightAvg = self.plotResInfo[resID][nearPlotID].weightAvg * (1 - 1/(distance/depositSize * (1+Map.Rand(maxDepositSize, "Strategic Resource Placement"))))
7674 end
7675
7676 break
7677 end
7678 end
7679 return resRemaining
7680end
7681------------------------------------------------------------------------------
7682function AssignStartingPlots:GetResourceQuantities(resIDs)
7683 local resNum = {}
7684 local resLower = Cep.STRATEGIC_RESOURCE_LOWER_BOUND
7685 local resUpper = Cep.STRATEGIC_RESOURCE_UPPER_BOUND
7686 local maxDepositSize = 1
7687 local stratMultiplier = 1
7688 local resGroups = {}
7689
7690 if self.resource_setting == 1 then
7691 stratMultiplier = 0.66667
7692 elseif self.resource_setting == 3 then
7693 stratMultiplier = 1.66667
7694 end
7695
7696 for _, resID in pairs(resIDs) do
7697 resInfo = GameInfo.Resources[resID]
7698 if resInfo.MutuallyExclusiveGroup == -1 then
7699 resNum[resID] = resInfo.NumPerTerritory * stratMultiplier * 0.01 * (resLower + Map.Rand(resUpper - resLower, "Strategic Resource Placement"))
7700 if resNum[resID] < 0 then
7701 local worldInfo = GameInfo.Worlds[Map.GetWorldSize()]
7702 local resourceMod = 100
7703 --[[
7704 if not worldInfo.ResourceMod then
7705 print("Worlds[%s] has no ResourceMod column", worldInfo.Type)
7706 else
7707 resourceMod = worldInfo.ResourceMod
7708 end
7709 --]]
7710 resNum[resID] = -1 * resNum[resID] * resourceMod / 100
7711 end
7712 resNum[resID] = Game.Round(resNum[resID])
7713 else
7714 resGroups[resInfo.MutuallyExclusiveGroup] = resGroups[resInfo.MutuallyExclusiveGroup] or {}
7715 resGroups[resInfo.MutuallyExclusiveGroup][resID] = resInfo.NumPerTerritory
7716 end
7717 end
7718
7719 for resGroupID, resGroup in pairs(resGroups) do
7720 local groupSize = 0
7721 local groupTotal = 0
7722 local groupNormTotal = 0
7723 local resBoostIndex = 0
7724 local resWeight = {}
7725
7726 -- total for the group
7727 for resID, resNum in pairs(resGroup) do
7728 groupTotal = groupTotal + resNum
7729 groupSize = groupSize + 1
7730 end
7731 groupTotal = Game.Round(groupTotal * stratMultiplier * 0.01 * (resLower + Map.Rand(resUpper - resLower, "Strategic Resource Placement")))
7732
7733 -- pick a random resource to boost
7734 resBoostIndex = Map.Rand(groupSize, "Strategic Resource Placement")
7735 local i = 0
7736
7737 for resID, resNum in pairs(resGroup) do
7738 if i == resBoostIndex then
7739 resWeight[resID] = 2 * resNum
7740 else
7741 resWeight[resID] = resNum
7742 end
7743 groupNormTotal = groupNormTotal + resWeight[resID]
7744 i = i + 1
7745 end
7746
7747 -- normalize weights to get real quantity
7748 for resID, resNum in pairs(resWeight) do
7749 resNum[resID] = resWeight[resID] * groupTotal/groupNormTotal
7750 end
7751 end
7752
7753 return resNum
7754end
7755------------------------------------------------------------------------------
7756function AssignStartingPlots:PlaceStrategicAndBonusResourcesCEP()
7757 print("PlaceStrategicAndBonusResourcesCEP")
7758 local resIDs = Game.GetResourceIDsOfUsage(ResourceUsageTypes.RESOURCEUSAGE_STRATEGIC)
7759
7760 -- Place Strategic Resources
7761 print("Placing Strategic Resources")
7762 local placedStrategics = false
7763
7764 self:CalculateStrategicPlotWeights() -- Must do this before strategic resource placement!
7765
7766
7767
7768 for regionID, regionInfo in ipairs(self.regionData) do
7769 local plotList = self:GetRegionStrategicPlotList(regionInfo)
7770 local resNum = self:GetResourceQuantities(resIDs)
7771 for _, resID in pairs(resIDs) do
7772 local resRemaining = resNum[resID]
7773 local maxDepositSize = math.ceil(resRemaining * 80 / #(plotList[resID]))
7774 local passes = 0
7775 --print("Placing %2s %9s in Region %2s", resRemaining, Locale.ConvertTextKey(GameInfo.Resources[resID].Description), regionID)
7776 while resRemaining > 0 and passes < 30 do
7777 passes = passes + 1
7778 table.sort(plotList[resID], function (a,b)
7779 return self.plotResInfo[resID][a].weightAvg > self.plotResInfo[resID][b].weightAvg
7780 end)
7781 resRemaining = self:PlaceStrategicResourceInPlots(plotList[resID], resID, resRemaining, maxDepositSize)
7782 end
7783
7784 if resRemaining <= 0 then
7785 placedStrategics = true
7786 else
7787 --print("Unable to place %i %s in region %i after %i passes.", resRemaining, GameInfo.Resources[resID].Type, regionID, passes)
7788 end
7789 end
7790 end
7791
7792 local resMultiplier = 0.66667
7793 if self.resource_setting == 1 then -- Sparse, so increase the number of tiles per bonus.
7794 resMultiplier = resMultiplier * 1.5
7795 elseif self.resource_setting == 3 then -- Abundant, so reduce the number of tiles per bonus.
7796 resMultiplier = resMultiplier * 0.66667
7797 end
7798
7799 -- If territorial placement failed, fall back on vanilla system.
7800 if not placedStrategics then
7801 print("Map Generation - Strategic resource placement failed, falling back to vanilla.")
7802 -- Adjust amounts, if applicable, based on Resource Setting.
7803 local uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = self:GetMajorStrategicResourceQuantityValues()
7804
7805 -- Place Strategic resources.
7806 print("Map Generation - Placing Strategics");
7807 local resources_to_place = {
7808 {self.oil_ID, oil_amt, 65, 1, 1},
7809 {self.uranium_ID, uran_amt, 35, 0, 1} };
7810 self:ProcessResourceList(9, 1, self.marsh_list, resources_to_place)
7811
7812 local resources_to_place = {
7813 {self.oil_ID, oil_amt, 40, 1, 2},
7814 {self.aluminum_ID, alum_amt, 15, 1, 2},
7815 {self.iron_ID, iron_amt, 45, 1, 2} };
7816 self:ProcessResourceList(16, 1, self.tundra_flat_no_feature, resources_to_place)
7817
7818 local resources_to_place = {
7819 {self.oil_ID, oil_amt, 60, 1, 1},
7820 {self.aluminum_ID, alum_amt, 15, 2, 3},
7821 {self.iron_ID, iron_amt, 25, 2, 3} };
7822 self:ProcessResourceList(17, 1, self.snow_flat_list, resources_to_place)
7823
7824 local resources_to_place = {
7825 {self.oil_ID, oil_amt, 65, 0, 1},
7826 {self.iron_ID, iron_amt, 35, 1, 1} };
7827 self:ProcessResourceList(13, 1, self.desert_flat_no_feature, resources_to_place)
7828
7829 local resources_to_place = {
7830 {self.iron_ID, iron_amt, 26, 0, 2},
7831 {self.coal_ID, coal_amt, 35, 1, 3},
7832 {self.aluminum_ID, alum_amt, 39, 2, 3} };
7833 self:ProcessResourceList(22, 1, self.hills_list, resources_to_place)
7834
7835 local resources_to_place = {
7836 {self.coal_ID, coal_amt, 30, 1, 2},
7837 {self.uranium_ID, uran_amt, 70, 1, 2} };
7838 self:ProcessResourceList(33, 1, self.jungle_flat_list, resources_to_place)
7839
7840 local resources_to_place = {
7841 {self.coal_ID, coal_amt, 30, 1, 2},
7842 {self.uranium_ID, uran_amt, 70, 1, 1} };
7843 self:ProcessResourceList(39, 1, self.forest_flat_list, resources_to_place)
7844
7845 local resources_to_place = {
7846 {self.horse_ID, horse_amt, 100, 2, 5} };
7847 self:ProcessResourceList(33, 1, self.dry_grass_flat_no_feature, resources_to_place)
7848 local resources_to_place = {
7849 {self.horse_ID, horse_amt, 100, 1, 4} };
7850 self:ProcessResourceList(33, 1, self.plains_flat_no_feature, resources_to_place)
7851
7852 self:AddModernMinorStrategicsToCityStates() -- Added spring 2011
7853
7854 self:PlaceSmallQuantitiesOfStrategics(23 * resMultiplier, self.land_list);
7855
7856 self:PlaceOilInTheSea();
7857 end
7858
7859
7860 -- Place Bonus Resources
7861 --print("Placing Bonus Resources")
7862
7863
7864
7865 print("Map Generation - Placing Bonuses");
7866 self:PlaceFish(10 * resMultiplier, self.coast_list);
7867 self:PlaceSexyBonusAtCivStarts()
7868 self:AddExtraBonusesToHillsRegions()
7869
7870 self:PlaceBonusResources()
7871end
7872--]=]----------------------------------------------------------------------------
7873function AssignStartingPlots:PlaceStrategicAndBonusResources()
7874 print("PlaceStrategicAndBonusResources")
7875 -- KEY: {Resource ID, Quantity (0 = unquantified), weighting, minimum radius, maximum radius}
7876 -- KEY: (frequency (1 per n plots in the list), impact list number, plot list, resource data)
7877 --
7878 -- The radius creates a zone around the plot that other resources of that
7879 -- type will avoid if possible. See ProcessResourceList for impact numbers.
7880 --
7881 -- Order of placement matters, so changing the order may affect a later dependency.
7882
7883 -- Adjust amounts, if applicable, based on Resource Setting.
7884 local uran_amt, horse_amt, oil_amt, iron_amt, coal_amt, alum_amt = self:GetMajorStrategicResourceQuantityValues()
7885 local resources_to_place = {}
7886
7887 -- Adjust appearance rate per Resource Setting chosen by user.
7888 local resMultiplier = 1;
7889 if self.resource_setting == 1 then -- Sparse, so increase the number of tiles per bonus.
7890 resMultiplier = 1.5;
7891 elseif self.resource_setting == 3 then -- Abundant, so reduce the number of tiles per bonus.
7892 resMultiplier = 0.66666667;
7893 end
7894
7895 print("self.resource_setting = " .. self.resource_setting)
7896
7897 -- Place Strategic resources.
7898 do
7899 print("Map Generation - Placing Strategics");
7900
7901 resources_to_place = {
7902 {self.iron_ID, iron_amt, 10, 0, 2}, --26
7903 {self.coal_ID, coal_amt, 80, 1, 3}, -- 39
7904 {self.aluminum_ID, alum_amt, 10, 2, 3} }; -- 35
7905 self:ProcessResourceList(15, 1, self.hills_list, resources_to_place)
7906 -- 22
7907
7908 resources_to_place = {
7909 {self.coal_ID, coal_amt, 50, 1, 2}, -- 30
7910 {self.uranium_ID, uran_amt, 50, 1, 2} }; --70
7911 self:ProcessResourceList(20, 1, self.jungle_flat_list, resources_to_place)
7912 -- 33
7913
7914 resources_to_place = {
7915 {self.coal_ID, coal_amt, 80, 1, 2}, --70
7916 {self.uranium_ID, uran_amt, 20, 1, 1}, --30
7917 {self.iron_ID, iron_amt, 100, 0, 2} };
7918 self:ProcessResourceList(30, 1, self.forest_flat_list, resources_to_place)
7919 -- 39
7920
7921 resources_to_place = {
7922 {self.oil_ID, oil_amt, 65, 1, 1},
7923 {self.uranium_ID, uran_amt, 35, 0, 1} };
7924 self:ProcessResourceList(30, 1, self.jungle_flat_list, resources_to_place)
7925 -- 9
7926
7927 resources_to_place = {
7928 {self.oil_ID, oil_amt, 45, 1, 2},
7929 {self.aluminum_ID, alum_amt, 45, 1, 2},
7930 {self.iron_ID, iron_amt, 10, 1, 2} };
7931 self:ProcessResourceList(16, 1, self.tundra_flat_no_feature, resources_to_place)
7932 -- 16
7933
7934 resources_to_place = {
7935 {self.oil_ID, oil_amt, 20, 1, 1},
7936 {self.aluminum_ID, alum_amt, 20, 2, 3},
7937 {self.uranium_ID, alum_amt, 20, 2, 3},
7938 {self.coal_ID, alum_amt, 20, 2, 3},
7939 {self.iron_ID, iron_amt, 20, 2, 3} };
7940 self:ProcessResourceList(5, 1, self.snow_flat_list, resources_to_place)
7941 -- 17
7942
7943 resources_to_place = {
7944 {self.oil_ID, oil_amt, 80, 0, 1},
7945 {self.iron_ID, iron_amt, 20, 1, 1} };
7946 self:ProcessResourceList(10, 1, self.desert_flat_no_feature, resources_to_place)
7947 -- 13
7948
7949 resources_to_place = {
7950 {self.iron_ID, iron_amt, 100, 0, 2} };
7951 self:ProcessResourceList(10, 1, self.hills_jungle_list, resources_to_place)
7952 -- 99
7953
7954-- resources_to_place = {
7955-- {self.iron_ID, iron_amt, 100, 0, 2} };
7956-- self:ProcessResourceList(5, 1, self.marsh_list, resources_to_place)
7957 -- 99
7958
7959 resources_to_place = {
7960 {self.horse_ID, horse_amt, 100, 2, 5} };
7961 self:ProcessResourceList(33, 1, self.grass_flat_no_feature, resources_to_place)
7962 -- 33
7963
7964 resources_to_place = {
7965 {self.horse_ID, horse_amt, 100, 1, 4} };
7966 self:ProcessResourceList(33, 1, self.plains_flat_no_feature, resources_to_place)
7967 -- 33
7968
7969 resources_to_place = {
7970 {self.horse_ID, horse_amt, 100, 1, 4} };
7971 self:ProcessResourceList(10, 1, self.flood_plains_list, resources_to_place)
7972 end
7973 -- 33
7974
7975 self:AddModernMinorStrategicsToCityStates() -- Added spring 2011
7976
7977 self:PlaceSmallQuantitiesOfStrategics(23 * resMultiplier, self.land_list);
7978
7979 self:PlaceOilInTheSea();
7980
7981
7982 -- Check for low or missing Strategic resources
7983 do
7984 if self.amounts_of_resources_placed[self.iron_ID + 1] < 8 then
7985 --print("Map has very low iron, adding another.");
7986 local resources_to_place = { {self.iron_ID, iron_amt, 100, 0, 0} };
7987 self:ProcessResourceList(99999, 1, self.hills_list, resources_to_place) -- 99999 means one per that many tiles: a single instance.
7988 end
7989 if self.amounts_of_resources_placed[self.iron_ID + 1] < 4 * self.iNumCivs then
7990 --print("Map has very low iron, adding another.");
7991 local resources_to_place = { {self.iron_ID, iron_amt, 100, 0, 0} };
7992 self:ProcessResourceList(99999, 1, self.land_list, resources_to_place)
7993 end
7994 if self.amounts_of_resources_placed[self.horse_ID + 1] < 4 * self.iNumCivs then
7995 --print("Map has very low horse, adding another.");
7996 local resources_to_place = { {self.horse_ID, horse_amt, 100, 0, 0} };
7997 self:ProcessResourceList(99999, 1, self.plains_flat_no_feature, resources_to_place)
7998 end
7999 if self.amounts_of_resources_placed[self.horse_ID + 1] < 4 * self.iNumCivs then
8000 --print("Map has very low horse, adding another.");
8001 local resources_to_place = { {self.horse_ID, horse_amt, 100, 0, 0} };
8002 self:ProcessResourceList(99999, 1, self.dry_grass_flat_no_feature, resources_to_place)
8003 end
8004 if self.amounts_of_resources_placed[self.coal_ID + 1] < 8 then
8005 --print("Map has very low coal, adding another.");
8006 local resources_to_place = { {self.coal_ID, coal_amt, 100, 0, 0} };
8007 self:ProcessResourceList(99999, 1, self.hills_list, resources_to_place)
8008 end
8009 if self.amounts_of_resources_placed[self.coal_ID + 1] < 4 * self.iNumCivs then
8010 --print("Map has very low coal, adding another.");
8011 local resources_to_place = { {self.coal_ID, coal_amt, 100, 0, 0} };
8012 self:ProcessResourceList(99999, 1, self.land_list, resources_to_place)
8013 end
8014 if self.amounts_of_resources_placed[self.oil_ID + 1] < 4 * self.iNumCivs then
8015 --print("Map has very low oil, adding another.");
8016 local resources_to_place = { {self.oil_ID, oil_amt, 100, 0, 0} };
8017 self:ProcessResourceList(99999, 1, self.land_list, resources_to_place)
8018 end
8019 if self.amounts_of_resources_placed[self.aluminum_ID + 1] < 4 * self.iNumCivs then
8020 --print("Map has very low aluminum, adding another.");
8021 local resources_to_place = { {self.aluminum_ID, alum_amt, 100, 0, 0} };
8022 self:ProcessResourceList(99999, 1, self.hills_list, resources_to_place)
8023 end
8024 if self.amounts_of_resources_placed[self.uranium_ID + 1] < 2 * self.iNumCivs then
8025 --print("Map has very low uranium, adding another.");
8026 local resources_to_place = { {self.uranium_ID, uran_amt, 100, 0, 0} };
8027 self:ProcessResourceList(99999, 1, self.land_list, resources_to_place)
8028 end
8029 end
8030
8031 self:PlaceBonusResources()
8032end
8033------------------------------------------------------------------------------
8034function AssignStartingPlots:PlaceFish()
8035 print("AssignStartingPlots:PlaceFish()")
8036 for plotID, plot in Plots(Shuffle) do
8037 PlacePossibleFish(plot)
8038 end
8039end
8040function PlacePossibleFish(plot)
8041 if plot:GetTerrainType() ~= TerrainTypes.TERRAIN_COAST or plot:IsLake() or plot:GetFeatureType() ~= FeatureTypes.NO_FEATURE or plot:GetResourceType() ~= -1 then
8042 return
8043 end
8044 local x, y = plot:GetX(), plot:GetY()
8045 local landDistance = 999
8046 local sumFertility = 0
8047 local nearFish = 0
8048 local odds = 0
8049 local fishID = GameInfo.Resources.RESOURCE_FISH.ID
8050 local fishMod = 0
8051
8052 for nearPlot, distance in Plot_GetPlotsInCircle(plot, 1, 3) do
8053 distance = math.max(1, distance)
8054 if not nearPlot:IsWater() and distance < landDistance then
8055 landDistance = distance
8056 end
8057 sumFertility = sumFertility + Plot_GetFertility(nearPlot, false, true)
8058 if nearPlot:GetResourceType() == fishID then
8059 odds = odds - 100 / distance
8060 end
8061 end
8062 if landDistance >= 3 then
8063 return
8064 end
8065
8066 fishMod = odds
8067 odds = odds + 100 * (1 - sumFertility/(mg.fishTargetFertility * 2))
8068 odds = odds / landDistance
8069
8070 if odds >= Map.Rand(100, "PlacePossibleFish - Lua") then
8071 plot:SetResourceType(fishID, 1)
8072 --print(string.format( "PlacePossibleFish fertility=%-3s odds=%-3s fishMod=%-3s", Round(sumFertility), Round(odds), Round(fishMod) ))
8073 end
8074end
8075------------------------------------------------------------------------------
8076function AssignStartingPlots:PlaceBonusResources()
8077 local resMultiplier = 1;
8078 if self.resource_setting == 1 then -- Sparse, so increase the number of tiles per bonus.
8079 resMultiplier = 1.5;
8080 elseif self.resource_setting == 3 then -- Abundant, so reduce the number of tiles per bonus.
8081 resMultiplier = 0.66666667;
8082 end
8083
8084 -- Place Bonus Resources
8085 print("Map Generation - Placing Bonuses");
8086 self:PlaceFish(10 * resMultiplier, self.coast_list);
8087 self:PlaceSexyBonusAtCivStarts()
8088 self:AddExtraBonusesToHillsRegions()
8089 local resources_to_place = {}
8090
8091 resources_to_place = {
8092 {self.deer_ID, 1, 100, 1, 2} };
8093 self:ProcessResourceList(6 * resMultiplier, 3, self.extra_deer_list, resources_to_place)
8094 -- 8
8095
8096 resources_to_place = {
8097 {self.deer_ID, 1, 100, 1, 2} };
8098 self:ProcessResourceList(8 * resMultiplier, 3, self.tundra_flat_no_feature, resources_to_place)
8099 -- 12
8100
8101 resources_to_place = {
8102 {self.wheat_ID, 1, 100, 0, 2} };
8103 self:ProcessResourceList(10 * resMultiplier, 3, self.desert_wheat_list, resources_to_place)
8104 -- 10
8105
8106 resources_to_place = {
8107 {self.wheat_ID, 1, 100, 2, 3} };
8108 self:ProcessResourceList(10 * resMultiplier, 3, self.plains_flat_no_feature, resources_to_place)
8109 -- 27
8110
8111 resources_to_place = {
8112 {self.banana_ID, 1, 100, 0, 3} };
8113 self:ProcessResourceList(14 * resMultiplier, 3, self.banana_list, resources_to_place)
8114 -- 14
8115
8116 resources_to_place = {
8117 {self.cow_ID, 1, 100, 1, 2} };
8118 self:ProcessResourceList(18 * resMultiplier, 3, self.grass_flat_no_feature, resources_to_place)
8119 -- 18
8120
8121-- CBP
8122 resources_to_place = {
8123 {self.bison_ID, 1, 100, 1, 2} };
8124 self:ProcessResourceList(15 * resMultiplier, 3, self.plains_flat_no_feature, resources_to_place)
8125-- END
8126
8127 resources_to_place = {
8128 {self.sheep_ID, 1, 100, 1, 1} };
8129 self:ProcessResourceList(8 * resMultiplier, 3, self.hills_open_list, resources_to_place)
8130 -- 13
8131
8132 resources_to_place = {
8133 {self.stone_ID, 1, 100, 1, 1} };
8134 self:ProcessResourceList(10 * resMultiplier, 3, self.grass_flat_no_feature, resources_to_place)
8135 -- 20
8136
8137 resources_to_place = {
8138 {self.stone_ID, 1, 100, 1, 2} };
8139 self:ProcessResourceList(15 * resMultiplier, 3, self.tundra_flat_no_feature, resources_to_place)
8140 -- 15
8141
8142 resources_to_place = {
8143 {self.stone_ID, 1, 100, 1, 2} };
8144 self:ProcessResourceList(5 * resMultiplier, 3, self.desert_flat_no_feature, resources_to_place)
8145 -- 19
8146
8147 resources_to_place = {
8148 {self.stone_ID, 1, 100, 1, 2} };
8149 self:ProcessResourceList(50 * resMultiplier, 3, self.marsh_list, resources_to_place)
8150 -- 99
8151
8152 resources_to_place = {
8153 {self.stone_ID, 1, 100, 1, 2} };
8154 self:ProcessResourceList(8 * resMultiplier, 3, self.snow_flat_list, resources_to_place)
8155 -- 99
8156
8157 resources_to_place = {
8158 {self.deer_ID, 1, 100, 3, 4} };
8159 self:ProcessResourceList(25 * resMultiplier, 3, self.forest_flat_that_are_not_tundra, resources_to_place)
8160end
8161------------------------------------------------------------------------------
8162function AssignStartingPlots:PlaceResourcesAndCityStates()
8163 -- This function controls nearly all resource placement. Only resources
8164 -- placed during Normalization operations are handled elsewhere.
8165 --
8166 -- Luxury resources are placed in relationship to Regions, adapting to the
8167 -- details of the given map instance, including number of civs and city
8168 -- states present. At Jon's direction, Luxuries have been implemented to
8169 -- be diplomatic widgets for trading, in addition to sources of Happiness.
8170 --
8171 -- Strategic and Bonus resources are terrain-adjusted. They will customize
8172 -- to each map instance. Each terrain type has been measured and has certain
8173 -- resource types assigned to it. You can customize resource placement to
8174 -- any degree desired by controlling generation of plot groups to feed in
8175 -- to the process. The default plot groups are terrain-based, but any
8176 -- criteria you desire could be used to determine plot group membership.
8177 --
8178 -- If any default methods fail to meet a specific need, don't hesitate to
8179 -- replace them with custom methods. I have labored to make this new
8180 -- system as accessible and powerful as any ever before offered.
8181
8182 print("Map Generation - Assigning Luxury Resource Distribution");
8183 self:AssignLuxuryRoles()
8184
8185 print("Map Generation - Placing City States");
8186 self:PlaceCityStates()
8187
8188 -- Generate global plot lists for resource distribution.
8189 self:GenerateGlobalResourcePlotLists()
8190
8191 print("Map Generation - Placing Luxuries");
8192 self:PlaceLuxuries()
8193
8194 --print("Map Generation - Placing Stone on Islands");
8195 self:BuffIslands()
8196
8197 -- Place Strategic and Bonus resources.
8198 --[[
8199 if GameInfo.Cep then
8200 self:PlaceStrategicAndBonusResourcesCEP()
8201 else
8202 self:PlaceStrategicAndBonusResources()
8203 end
8204 --]]
8205 self:PlaceStrategicAndBonusResources()
8206
8207 --print("Map Generation - Normalize City State Locations");
8208 self:NormalizeCityStateLocations()
8209
8210 -- Fix Sugar graphics
8211 self:AdjustTiles()
8212
8213 local largestLand = Map.FindBiggestArea(false)
8214 if Map.GetCustomOption(6) == 2 then
8215 -- Biggest continent placement
8216 if largestLand:GetNumTiles() < 0.25 * Map.GetLandPlots() then
8217 print("AI Map Strategy - Offshore expansion with navy bias")
8218 -- Tell the AI that we should treat this as a offshore expansion map with naval bias
8219 Map.ChangeAIMapHint(4+1)
8220 else
8221 print("AI Map Strategy - Offshore expansion")
8222 -- Tell the AI that we should treat this as a offshore expansion map
8223 Map.ChangeAIMapHint(4)
8224 end
8225 elseif largestLand:GetNumTiles() < 0.25 * Map.GetLandPlots() then
8226 print("AI Map Strategy - Navy bias")
8227 -- Tell the AI that we should treat this as a map with naval bias
8228 Map.ChangeAIMapHint(1)
8229 else
8230 print("AI Map Strategy - Normal")
8231 end
8232
8233 -- Necessary to implement placement of Natural Wonders, and possibly other plot-type changes.
8234 -- This operation must be saved for last, as it invalidates all regional data by resetting Area IDs.
8235 Map.RecalculateAreas();
8236
8237 -- Activate for debug only
8238 self:PrintFinalResourceTotalsToLog()
8239 --
8240end
8241------------------------------------------------------------------------------
8242function AssignStartingPlots:NormalizeStartLocation(region_number)
8243 --[[ This function measures the value of land in two rings around a given start
8244 location, primarily for the purpose of determining how much support the site
8245 requires in the form of Bonus Resources. Numerous assumptions are built in
8246 to this operation that would need to be adjusted for any modifications to
8247 terrain or resources types and yields, or to game rules about rivers and
8248 other map elements. Nothing is hardcoded in a way that puts it out of the
8249 reach of modders, but any mods including changes to map elements may have a
8250 significant workload involved with rebalancing the start finder and the
8251 resource distribution to fit them properly to a mod's custom needs. I have
8252 labored to document every function and method in detail to make it as easy
8253 as possible to modify this system. -- Bob Thomas - April 15, 2010 ]]--
8254 --
8255 local iW, iH = Map.GetGridSize();
8256 local start_point_data = self.startingPlots[region_number];
8257 local x = start_point_data[1];
8258 local y = start_point_data[2];
8259 local plot = Map.GetPlot(x, y);
8260 local plotIndex = y * iW + x + 1;
8261 local isEvenY = true;
8262 if y / 2 > math.floor(y / 2) then
8263 isEvenY = false;
8264 end
8265 local wrapX = Map:IsWrapX();
8266 local wrapY = Map:IsWrapY();
8267 local innerFourFood, innerThreeFood, innerTwoFood, innerHills, innerForest, innerOneHammer, innerOcean = 0, 0, 0, 0, 0, 0, 0;
8268 local outerFourFood, outerThreeFood, outerTwoFood, outerHills, outerForest, outerOneHammer, outerOcean = 0, 0, 0, 0, 0, 0, 0;
8269 local innerCanHaveBonus, outerCanHaveBonus, innerBadTiles, outerBadTiles = 0, 0, 0, 0;
8270 local iNumFoodBonusNeeded = 0;
8271 local iNumNativeTwoFoodFirstRing, iNumNativeTwoFoodSecondRing = 0, 0; -- Cities must begin the game with at least three native 2F tiles, one in first ring.
8272 local search_table = {};
8273
8274 -- Remove any feature Ice from the first ring.
8275 self:GenerateLuxuryPlotListsAtCitySite(x, y, 1, true)
8276
8277 -- Set up Conditions checks.
8278 local alongOcean = false;
8279 local nextToLake = false;
8280 local isRiver = false;
8281 local nearRiver = false;
8282 local nearMountain = false;
8283 local forestCount, jungleCount = 0, 0;
8284
8285 -- Check start plot to see if it's adjacent to saltwater.
8286 if self.plotDataIsCoastal[plotIndex] == true then
8287 alongOcean = true;
8288 end
8289
8290 -- Check start plot to see if it's on a river.
8291 if plot:IsRiver() then
8292 isRiver = true;
8293 end
8294
8295 -- Data Chart for early game tile potentials
8296 --
8297 -- 4F: Flood Plains, Grass on fresh water (includes forest and marsh).
8298 -- 3F: Dry Grass, Plains on fresh water (includes forest and jungle), Tundra on fresh water (includes forest), Oasis
8299 -- 2F: Dry Plains, Lake, all remaining Jungles.
8300 --
8301 -- 1H: Plains, Jungle on Plains
8302
8303 -- Adding evaluation of grassland and plains for balance boost of bonus Cows for heavy grass starts. -1/26/2011 BT
8304 local iNumGrass, iNumPlains = 0, 0;
8305
8306 -- Evaluate First Ring
8307 if isEvenY then
8308 search_table = self.firstRingYIsEven;
8309 else
8310 search_table = self.firstRingYIsOdd;
8311 end
8312
8313 for loop, plot_adjustments in ipairs(search_table) do
8314 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8315 --
8316 if searchX < 0 or searchX >= iW or searchY < 0 or searchY >= iH then
8317 -- This plot does not exist. It's off the map edge.
8318 innerBadTiles = innerBadTiles + 1;
8319 else
8320 local searchPlot = Map.GetPlot(searchX, searchY)
8321 local plotType = searchPlot:GetPlotType()
8322 local terrainType = searchPlot:GetTerrainType()
8323 local featureType = searchPlot:GetFeatureType()
8324 --
8325 if plotType == PlotTypes.PLOT_MOUNTAIN then
8326 local nearMountain = true;
8327 innerBadTiles = innerBadTiles + 1;
8328 elseif plotType == PlotTypes.PLOT_OCEAN then
8329 if searchPlot:IsLake() then
8330 nextToLake = true;
8331 if featureType == FeatureTypes.FEATURE_ICE then
8332 innerBadTiles = innerBadTiles + 1;
8333 else
8334 innerTwoFood = innerTwoFood + 1;
8335 iNumNativeTwoFoodFirstRing = iNumNativeTwoFoodFirstRing + 1;
8336 end
8337 else
8338 if featureType == FeatureTypes.FEATURE_ICE then
8339 innerBadTiles = innerBadTiles + 1;
8340 else
8341 innerOcean = innerOcean + 1;
8342 innerCanHaveBonus = innerCanHaveBonus + 1;
8343 end
8344 end
8345 else -- Habitable plot.
8346 if featureType == FeatureTypes.FEATURE_JUNGLE then
8347 jungleCount = jungleCount + 1;
8348 iNumNativeTwoFoodFirstRing = iNumNativeTwoFoodFirstRing + 1;
8349 elseif featureType == FeatureTypes.FEATURE_FOREST then
8350 forestCount = forestCount + 1;
8351 end
8352 if searchPlot:IsRiver() then
8353 nearRiver = true;
8354 end
8355 if plotType == PlotTypes.PLOT_HILLS then
8356 innerHills = innerHills + 1;
8357 if featureType == FeatureTypes.FEATURE_JUNGLE then
8358 innerTwoFood = innerTwoFood + 1;
8359 innerCanHaveBonus = innerCanHaveBonus + 1;
8360 elseif featureType == FeatureTypes.FEATURE_FOREST then
8361 innerCanHaveBonus = innerCanHaveBonus + 1;
8362 elseif terrainType == TerrainTypes.TERRAIN_GRASS then
8363 iNumGrass = iNumGrass + 1;
8364 elseif terrainType == TerrainTypes.TERRAIN_PLAINS then
8365 iNumPlains = iNumPlains + 1;
8366 end
8367 elseif featureType == FeatureTypes.FEATURE_OASIS then
8368 innerThreeFood = innerThreeFood + 1;
8369 iNumNativeTwoFoodFirstRing = iNumNativeTwoFoodFirstRing + 1;
8370 elseif searchPlot:IsFreshWater() then
8371 if terrainType == TerrainTypes.TERRAIN_GRASS then
8372 innerFourFood = innerFourFood + 1;
8373 iNumGrass = iNumGrass + 1;
8374 if featureType ~= FeatureTypes.FEATURE_MARSH then
8375 innerCanHaveBonus = innerCanHaveBonus + 1;
8376 end
8377 if featureType == FeatureTypes.FEATURE_FOREST then
8378 innerForest = innerForest + 1;
8379 end
8380 if featureType == FeatureTypes.NO_FEATURE then
8381 iNumNativeTwoFoodFirstRing = iNumNativeTwoFoodFirstRing + 1;
8382 end
8383 elseif featureType == FeatureTypes.FEATURE_FLOOD_PLAINS then
8384 innerFourFood = innerFourFood + 1;
8385 innerCanHaveBonus = innerCanHaveBonus + 1;
8386 iNumNativeTwoFoodFirstRing = iNumNativeTwoFoodFirstRing + 1;
8387 elseif terrainType == TerrainTypes.TERRAIN_PLAINS then
8388 innerThreeFood = innerThreeFood + 1;
8389 innerCanHaveBonus = innerCanHaveBonus + 1;
8390 iNumPlains = iNumPlains + 1;
8391 if featureType == FeatureTypes.FEATURE_FOREST then
8392 innerForest = innerForest + 1;
8393 else
8394 innerOneHammer = innerOneHammer + 1;
8395 end
8396 elseif terrainType == TerrainTypes.TERRAIN_TUNDRA then
8397 innerThreeFood = innerThreeFood + 1;
8398 innerCanHaveBonus = innerCanHaveBonus + 1;
8399 if featureType == FeatureTypes.FEATURE_FOREST then
8400 innerForest = innerForest + 1;
8401 end
8402 elseif terrainType == TerrainTypes.TERRAIN_DESERT then
8403 innerBadTiles = innerBadTiles + 1;
8404 innerCanHaveBonus = innerCanHaveBonus + 1; -- Can have Oasis.
8405 else -- Snow
8406 innerBadTiles = innerBadTiles + 1;
8407 end
8408 else -- Dry Flatlands
8409 if terrainType == TerrainTypes.TERRAIN_GRASS then
8410 innerThreeFood = innerThreeFood + 1;
8411 iNumGrass = iNumGrass + 1;
8412 if featureType ~= FeatureTypes.FEATURE_MARSH then
8413 innerCanHaveBonus = innerCanHaveBonus + 1;
8414 end
8415 if featureType == FeatureTypes.FEATURE_FOREST then
8416 innerForest = innerForest + 1;
8417 end
8418 if featureType == FeatureTypes.NO_FEATURE then
8419 iNumNativeTwoFoodFirstRing = iNumNativeTwoFoodFirstRing + 1;
8420 end
8421 elseif terrainType == TerrainTypes.TERRAIN_PLAINS then
8422 innerTwoFood = innerTwoFood + 1;
8423 innerCanHaveBonus = innerCanHaveBonus + 1;
8424 iNumPlains = iNumPlains + 1;
8425 if featureType == FeatureTypes.FEATURE_FOREST then
8426 innerForest = innerForest + 1;
8427 else
8428 innerOneHammer = innerOneHammer + 1;
8429 end
8430 elseif terrainType == TerrainTypes.TERRAIN_TUNDRA then
8431 innerCanHaveBonus = innerCanHaveBonus + 1;
8432 if featureType == FeatureTypes.FEATURE_FOREST then
8433 innerForest = innerForest + 1;
8434 else
8435 innerBadTiles = innerBadTiles + 1;
8436 end
8437 elseif terrainType == TerrainTypes.TERRAIN_DESERT then
8438 innerBadTiles = innerBadTiles + 1;
8439 innerCanHaveBonus = innerCanHaveBonus + 1; -- Can have Oasis.
8440 else -- Snow
8441 innerBadTiles = innerBadTiles + 1;
8442 end
8443 end
8444 end
8445 end
8446 end
8447
8448 -- Evaluate Second Ring
8449 if isEvenY then
8450 search_table = self.secondRingYIsEven;
8451 else
8452 search_table = self.secondRingYIsOdd;
8453 end
8454
8455 for loop, plot_adjustments in ipairs(search_table) do
8456 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8457 local plot = Map.GetPlot(x, y);
8458 --
8459 --
8460 if searchX < 0 or searchX >= iW or searchY < 0 or searchY >= iH then
8461 -- This plot does not exist. It's off the map edge.
8462 outerBadTiles = outerBadTiles + 1;
8463 else
8464 local searchPlot = Map.GetPlot(searchX, searchY)
8465 local plotType = searchPlot:GetPlotType()
8466 local terrainType = searchPlot:GetTerrainType()
8467 local featureType = searchPlot:GetFeatureType()
8468 --
8469 if plotType == PlotTypes.PLOT_MOUNTAIN then
8470 local nearMountain = true;
8471 outerBadTiles = outerBadTiles + 1;
8472 elseif plotType == PlotTypes.PLOT_OCEAN then
8473 if searchPlot:IsLake() then
8474 if featureType == FeatureTypes.FEATURE_ICE then
8475 outerBadTiles = outerBadTiles + 1;
8476 else
8477 outerTwoFood = outerTwoFood + 1;
8478 iNumNativeTwoFoodSecondRing = iNumNativeTwoFoodSecondRing + 1;
8479 end
8480 else
8481 if featureType == FeatureTypes.FEATURE_ICE then
8482 outerBadTiles = outerBadTiles + 1;
8483 elseif terrainType == TerrainTypes.TERRAIN_COAST then
8484 outerCanHaveBonus = outerCanHaveBonus + 1;
8485 outerOcean = outerOcean + 1;
8486 end
8487 end
8488 else -- Habitable plot.
8489 if featureType == FeatureTypes.FEATURE_JUNGLE then
8490 jungleCount = jungleCount + 1;
8491 iNumNativeTwoFoodSecondRing = iNumNativeTwoFoodSecondRing + 1;
8492 elseif featureType == FeatureTypes.FEATURE_FOREST then
8493 forestCount = forestCount + 1;
8494 end
8495 if searchPlot:IsRiver() then
8496 nearRiver = true;
8497 end
8498 if plotType == PlotTypes.PLOT_HILLS then
8499 outerHills = outerHills + 1;
8500 if featureType == FeatureTypes.FEATURE_JUNGLE then
8501 outerTwoFood = outerTwoFood + 1;
8502 outerCanHaveBonus = outerCanHaveBonus + 1;
8503 elseif featureType == FeatureTypes.FEATURE_FOREST then
8504 outerCanHaveBonus = outerCanHaveBonus + 1;
8505 elseif terrainType == TerrainTypes.TERRAIN_GRASS then
8506 iNumGrass = iNumGrass + 1;
8507 elseif terrainType == TerrainTypes.TERRAIN_PLAINS then
8508 iNumPlains = iNumPlains + 1;
8509 end
8510 elseif featureType == FeatureTypes.FEATURE_OASIS then
8511 innerThreeFood = innerThreeFood + 1;
8512 iNumNativeTwoFoodSecondRing = iNumNativeTwoFoodSecondRing + 1;
8513 elseif searchPlot:IsFreshWater() then
8514 if terrainType == TerrainTypes.TERRAIN_GRASS then
8515 outerFourFood = outerFourFood + 1;
8516 iNumGrass = iNumGrass + 1;
8517 if featureType ~= FeatureTypes.FEATURE_MARSH then
8518 outerCanHaveBonus = outerCanHaveBonus + 1;
8519 end
8520 if featureType == FeatureTypes.FEATURE_FOREST then
8521 outerForest = outerForest + 1;
8522 end
8523 if featureType == FeatureTypes.NO_FEATURE then
8524 iNumNativeTwoFoodSecondRing = iNumNativeTwoFoodSecondRing + 1;
8525 end
8526 elseif featureType == FeatureTypes.FEATURE_FLOOD_PLAINS then
8527 outerFourFood = outerFourFood + 1;
8528 outerCanHaveBonus = outerCanHaveBonus + 1;
8529 iNumNativeTwoFoodSecondRing = iNumNativeTwoFoodSecondRing + 1;
8530 elseif terrainType == TerrainTypes.TERRAIN_PLAINS then
8531 outerThreeFood = outerThreeFood + 1;
8532 outerCanHaveBonus = outerCanHaveBonus + 1;
8533 iNumPlains = iNumPlains + 1;
8534 if featureType == FeatureTypes.FEATURE_FOREST then
8535 outerForest = outerForest + 1;
8536 else
8537 outerOneHammer = outerOneHammer + 1;
8538 end
8539 elseif terrainType == TerrainTypes.TERRAIN_TUNDRA then
8540 outerThreeFood = outerThreeFood + 1;
8541 outerCanHaveBonus = outerCanHaveBonus + 1;
8542 if featureType == FeatureTypes.FEATURE_FOREST then
8543 outerForest = outerForest + 1;
8544 end
8545 elseif terrainType == TerrainTypes.TERRAIN_DESERT then
8546 outerBadTiles = outerBadTiles + 1;
8547 outerCanHaveBonus = outerCanHaveBonus + 1; -- Can have Oasis.
8548 else -- Snow
8549 outerBadTiles = outerBadTiles + 1;
8550 end
8551 else -- Dry Flatlands
8552 if terrainType == TerrainTypes.TERRAIN_GRASS then
8553 outerThreeFood = outerThreeFood + 1;
8554 iNumGrass = iNumGrass + 1;
8555 if featureType ~= FeatureTypes.FEATURE_MARSH then
8556 outerCanHaveBonus = outerCanHaveBonus + 1;
8557 end
8558 if featureType == FeatureTypes.FEATURE_FOREST then
8559 outerForest = outerForest + 1;
8560 end
8561 if featureType == FeatureTypes.NO_FEATURE then
8562 iNumNativeTwoFoodSecondRing = iNumNativeTwoFoodSecondRing + 1;
8563 end
8564 elseif terrainType == TerrainTypes.TERRAIN_PLAINS then
8565 outerTwoFood = outerTwoFood + 1;
8566 outerCanHaveBonus = outerCanHaveBonus + 1;
8567 iNumPlains = iNumPlains + 1;
8568 if featureType == FeatureTypes.FEATURE_FOREST then
8569 outerForest = outerForest + 1;
8570 else
8571 outerOneHammer = outerOneHammer + 1;
8572 end
8573 elseif terrainType == TerrainTypes.TERRAIN_TUNDRA then
8574 outerCanHaveBonus = outerCanHaveBonus + 1;
8575 if featureType == FeatureTypes.FEATURE_FOREST then
8576 outerForest = outerForest + 1;
8577 else
8578 outerBadTiles = outerBadTiles + 1;
8579 end
8580 elseif terrainType == TerrainTypes.TERRAIN_DESERT then
8581 outerBadTiles = outerBadTiles + 1;
8582 outerCanHaveBonus = outerCanHaveBonus + 1; -- Can have Oasis.
8583 else -- Snow
8584 outerBadTiles = outerBadTiles + 1;
8585 end
8586 end
8587 end
8588 end
8589 end
8590
8591 -- Adjust the hammer situation, if needed.
8592 local innerHammerScore = (4 * innerHills) + (2 * innerForest) + innerOneHammer;
8593 local outerHammerScore = (2 * outerHills) + outerForest + outerOneHammer;
8594 local earlyHammerScore = (2 * innerForest) + outerForest + innerOneHammer + outerOneHammer;
8595 -- If drastic shortage, attempt to add a hill to first ring.
8596 if (outerHammerScore < 8 and innerHammerScore < 2) or innerHammerScore == 0 then -- Change a first ring plot to Hills.
8597 if isEvenY then
8598 randomized_first_ring_adjustments = GetShuffledCopyOfTable(self.firstRingYIsEven);
8599 else
8600 randomized_first_ring_adjustments = GetShuffledCopyOfTable(self.firstRingYIsOdd);
8601 end
8602 for attempt = 1, 6 do
8603 local plot_adjustments = randomized_first_ring_adjustments[attempt];
8604 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8605 -- Attempt to place a Hill at the currently chosen plot.
8606 local placedHill = self:AttemptToPlaceHillsAtPlot(searchX, searchY);
8607 if placedHill == true then
8608 innerHammerScore = innerHammerScore + 4;
8609 --print("Added hills next to hammer-poor start plot at ", x, y);
8610 break
8611 elseif attempt == 6 then
8612 --print("FAILED to add hills next to hammer-poor start plot at ", x, y);
8613 end
8614 end
8615 end
8616
8617 -- Add mandatory Iron, Horse, Oil to every start if Strategic Balance option is enabled.
8618 if self.resource_setting == 5 then
8619 self:AddStrategicBalanceResources(region_number)
8620 end
8621
8622 -- If early hammers will be too short, attempt to add a small Horse or Iron to second ring.
8623 if innerHammerScore < 3 and earlyHammerScore < 6 then -- Add a small Horse or Iron to second ring.
8624 if isEvenY then
8625 randomized_second_ring_adjustments = GetShuffledCopyOfTable(self.secondRingYIsEven);
8626 else
8627 randomized_second_ring_adjustments = GetShuffledCopyOfTable(self.secondRingYIsOdd);
8628 end
8629 for attempt = 1, 12 do
8630 local plot_adjustments = randomized_second_ring_adjustments[attempt];
8631 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8632 -- Attempt to place a Hill at the currently chosen plot.
8633 local placedStrategic = self:AttemptToPlaceSmallStrategicAtPlot(searchX, searchY);
8634 if placedStrategic == true then
8635 break
8636 elseif attempt == 12 then
8637 --print("FAILED to add small strategic resource near hammer-poor start plot at ", x, y);
8638 end
8639 end
8640 end
8641
8642 -- Rate the food situation.
8643 local innerFoodScore = (4 * innerFourFood) + (2 * innerThreeFood) + innerTwoFood;
8644 local outerFoodScore = (4 * outerFourFood) + (2 * outerThreeFood) + outerTwoFood;
8645 local totalFoodScore = innerFoodScore + outerFoodScore;
8646 local nativeTwoFoodTiles = iNumNativeTwoFoodFirstRing + iNumNativeTwoFoodSecondRing;
8647
8648 --[[ Debug printout of food scores.
8649 print("-");
8650 print("-- - Start Point in Region #", region_number, " has Food Score of ", totalFoodScore, " with rings of ", innerFoodScore, outerFoodScore);
8651 ]]--
8652
8653 -- Six levels for Bonus Resource support, from zero to five.
8654 if totalFoodScore < 4 and innerFoodScore == 0 then
8655 iNumFoodBonusNeeded = 5;
8656 elseif totalFoodScore < 6 then
8657 iNumFoodBonusNeeded = 4;
8658 elseif totalFoodScore < 8 then
8659 iNumFoodBonusNeeded = 3;
8660 elseif totalFoodScore < 12 and innerFoodScore < 5 then
8661 iNumFoodBonusNeeded = 3;
8662 elseif totalFoodScore < 17 and innerFoodScore < 9 then
8663 iNumFoodBonusNeeded = 2;
8664 elseif nativeTwoFoodTiles <= 1 then
8665 iNumFoodBonusNeeded = 2;
8666 elseif totalFoodScore < 24 and innerFoodScore < 11 then
8667 iNumFoodBonusNeeded = 1;
8668 elseif nativeTwoFoodTiles == 2 or iNumNativeTwoFoodFirstRing == 0 then
8669 iNumFoodBonusNeeded = 1;
8670 elseif totalFoodScore < 20 then
8671 iNumFoodBonusNeeded = 1;
8672 end
8673
8674 -- Check for Legendary Start resource option.
8675 if self.resource_setting == 4 then
8676 iNumFoodBonusNeeded = iNumFoodBonusNeeded + 2;
8677 end
8678
8679 -- Check to see if a Grass tile needs to be added at an all-plains site with zero native 2-food tiles in first two rings.
8680 if nativeTwoFoodTiles == 0 and iNumFoodBonusNeeded < 3 then
8681 local odd = self.firstRingYIsOdd;
8682 local even = self.firstRingYIsEven;
8683 local plot_list = {};
8684 -- For notes on how the hex-iteration works, refer to PlaceResourceImpact()
8685 local ripple_radius = 2;
8686 local currentX = x - ripple_radius;
8687 local currentY = y;
8688 for direction_index = 1, 6 do
8689 for plot_to_handle = 1, ripple_radius do
8690 if currentY / 2 > math.floor(currentY / 2) then
8691 plot_adjustments = odd[direction_index];
8692 else
8693 plot_adjustments = even[direction_index];
8694 end
8695 nextX = currentX + plot_adjustments[1];
8696 nextY = currentY + plot_adjustments[2];
8697 if wrapX == false and (nextX < 0 or nextX >= iW) then
8698 -- X is out of bounds.
8699 elseif wrapY == false and (nextY < 0 or nextY >= iH) then
8700 -- Y is out of bounds.
8701 else
8702 local realX = nextX;
8703 local realY = nextY;
8704 if wrapX then
8705 realX = realX % iW;
8706 end
8707 if wrapY then
8708 realY = realY % iH;
8709 end
8710 -- We've arrived at the correct x and y for the current plot.
8711 local plot = Map.GetPlot(realX, realY);
8712 if plot:GetResourceType(-1) == -1 then -- No resource here, safe to proceed.
8713 local plotType = plot:GetPlotType()
8714 local terrainType = plot:GetTerrainType()
8715 local featureType = plot:GetFeatureType()
8716 local plotIndex = realY * iW + realX + 1;
8717 -- Now check this plot for eligibility to be converted to flat open grassland.
8718 if plotType == PlotTypes.PLOT_LAND then
8719 if terrainType == TerrainTypes.TERRAIN_PLAINS then
8720 if featureType == FeatureTypes.NO_FEATURE then
8721 table.insert(plot_list, plotIndex);
8722 end
8723 end
8724 end
8725 end
8726 end
8727 currentX, currentY = nextX, nextY;
8728 end
8729 end
8730 local iNumConversionCandidates = table.maxn(plot_list);
8731 if iNumConversionCandidates == 0 then
8732 iNumFoodBonusNeeded = 3;
8733 else
8734 --print("-"); print("*** START HAD NO 2-FOOD TILES, YET ONLY QUALIFIED FOR 2 BONUS; CONVERTING A PLAINS TO GRASS! ***"); print("-");
8735 local diceroll = 1 + Map.Rand(iNumConversionCandidates, "Choosing plot to convert to Grass near food-poor Plains start - LUA");
8736 local conversionPlotIndex = plot_list[diceroll];
8737 local conv_x = (conversionPlotIndex - 1) % iW;
8738 local conv_y = (conversionPlotIndex - conv_x - 1) / iW;
8739 local plot = Map.GetPlot(conv_x, conv_y);
8740 plot:SetTerrainType(TerrainTypes.TERRAIN_GRASS, false, false)
8741 self:PlaceResourceImpact(conv_x, conv_y, 1, 0) -- Disallow strategic resources at this plot, to keep it a farm plot.
8742 end
8743 end
8744 -- Add Bonus Resources to food-poor start positions.
8745 if iNumFoodBonusNeeded > 0 then
8746 local maxBonusesPossible = innerCanHaveBonus + outerCanHaveBonus;
8747
8748 --print("-");
8749 --print("Food-Poor start ", x, y, " needs ", iNumFoodBonusNeeded, " Bonus, with ", maxBonusesPossible, " eligible plots.");
8750 --print("-");
8751
8752 local innerPlaced, outerPlaced = 0, 0;
8753 local randomized_first_ring_adjustments, randomized_second_ring_adjustments, randomized_third_ring_adjustments;
8754 if isEvenY then
8755 randomized_first_ring_adjustments = GetShuffledCopyOfTable(self.firstRingYIsEven);
8756 randomized_second_ring_adjustments = GetShuffledCopyOfTable(self.secondRingYIsEven);
8757 randomized_third_ring_adjustments = GetShuffledCopyOfTable(self.thirdRingYIsEven);
8758 else
8759 randomized_first_ring_adjustments = GetShuffledCopyOfTable(self.firstRingYIsOdd);
8760 randomized_second_ring_adjustments = GetShuffledCopyOfTable(self.secondRingYIsOdd);
8761 randomized_third_ring_adjustments = GetShuffledCopyOfTable(self.thirdRingYIsOdd);
8762 end
8763 local tried_all_first_ring = false;
8764 local tried_all_second_ring = false;
8765 local tried_all_third_ring = false;
8766 local allow_oasis = true; -- Permanent flag. (We don't want to place more than one Oasis per location).
8767 local placedOasis; -- Records returning result from each attempt.
8768 while iNumFoodBonusNeeded > 0 do
8769 if ((innerPlaced < 2 and innerCanHaveBonus > 0) or (self.resource_setting == 4 and innerPlaced < 3 and innerCanHaveBonus > 0))
8770 and tried_all_first_ring == false then
8771 -- Add bonus to inner ring.
8772 for attempt = 1, 6 do
8773 local plot_adjustments = randomized_first_ring_adjustments[attempt];
8774 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8775 -- Attempt to place a Bonus at the currently chosen plot.
8776 local placedBonus, placedOasis = self:AttemptToPlaceBonusResourceAtPlot(searchX, searchY, allow_oasis);
8777 if placedBonus == true then
8778 if allow_oasis == true and placedOasis == true then -- First oasis was placed on this pass, so change permission.
8779 allow_oasis = false;
8780 end
8781 --print("Placed a Bonus in first ring at ", searchX, searchY);
8782 innerPlaced = innerPlaced + 1;
8783 innerCanHaveBonus = innerCanHaveBonus - 1;
8784 iNumFoodBonusNeeded = iNumFoodBonusNeeded - 1;
8785 break
8786 elseif attempt == 6 then
8787 tried_all_first_ring = true;
8788 end
8789 end
8790
8791 elseif ((innerPlaced + outerPlaced < 5 and outerCanHaveBonus > 0) or (self.resource_setting == 4 and innerPlaced + outerPlaced < 4 and outerCanHaveBonus > 0))
8792 and tried_all_second_ring == false then
8793 -- Add bonus to second ring.
8794 for attempt = 1, 12 do
8795 local plot_adjustments = randomized_second_ring_adjustments[attempt];
8796 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8797 -- Attempt to place a Bonus at the currently chosen plot.
8798 local placedBonus, placedOasis = self:AttemptToPlaceBonusResourceAtPlot(searchX, searchY, allow_oasis);
8799 if placedBonus == true then
8800 if allow_oasis == true and placedOasis == true then -- First oasis was placed on this pass, so change permission.
8801 allow_oasis = false;
8802 end
8803 --print("Placed a Bonus in second ring at ", searchX, searchY);
8804 outerPlaced = outerPlaced + 1;
8805 outerCanHaveBonus = outerCanHaveBonus - 1;
8806 iNumFoodBonusNeeded = iNumFoodBonusNeeded - 1;
8807 break
8808 elseif attempt == 12 then
8809 tried_all_second_ring = true;
8810 end
8811 end
8812
8813 elseif tried_all_third_ring == false then
8814 -- Add bonus to third ring.
8815 for attempt = 1, 18 do
8816 local plot_adjustments = randomized_third_ring_adjustments[attempt];
8817 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8818 -- Attempt to place a Bonus at the currently chosen plot.
8819 local placedBonus, placedOasis = self:AttemptToPlaceBonusResourceAtPlot(searchX, searchY, allow_oasis);
8820 if placedBonus == true then
8821 if allow_oasis == true and placedOasis == true then -- First oasis was placed on this pass, so change permission.
8822 allow_oasis = false;
8823 end
8824 --print("Placed a Bonus in third ring at ", searchX, searchY);
8825 iNumFoodBonusNeeded = iNumFoodBonusNeeded - 1;
8826 break
8827 elseif attempt == 18 then
8828 tried_all_third_ring = true;
8829 end
8830 end
8831
8832 else -- Tried everywhere, have to give up.
8833 break
8834 end
8835 end
8836 end
8837
8838 -- Check for heavy grass and light plains. Adding Stone if grass count is high and plains count is low. - May 2011, BT
8839 local iNumStoneNeeded = 0;
8840 if iNumGrass >= 9 and iNumPlains == 0 then
8841 iNumStoneNeeded = 2;
8842 elseif iNumGrass >= 6 and iNumPlains <= 4 then
8843 iNumStoneNeeded = 1;
8844 end
8845 if iNumStoneNeeded > 0 then -- Add Stone to this grass start.
8846 local stonePlaced, innerPlaced = 0, 0;
8847 local randomized_first_ring_adjustments, randomized_second_ring_adjustments;
8848 if isEvenY then
8849 randomized_first_ring_adjustments = GetShuffledCopyOfTable(self.firstRingYIsEven);
8850 randomized_second_ring_adjustments = GetShuffledCopyOfTable(self.secondRingYIsEven);
8851 else
8852 randomized_first_ring_adjustments = GetShuffledCopyOfTable(self.firstRingYIsOdd);
8853 randomized_second_ring_adjustments = GetShuffledCopyOfTable(self.secondRingYIsOdd);
8854 end
8855 local tried_all_first_ring = false;
8856 local tried_all_second_ring = false;
8857 while iNumStoneNeeded > 0 do
8858 if innerPlaced < 1 and tried_all_first_ring == false then
8859 -- Add bonus to inner ring.
8860 for attempt = 1, 6 do
8861 local plot_adjustments = randomized_first_ring_adjustments[attempt];
8862 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8863 -- Attempt to place Cows at the currently chosen plot.
8864 local placedBonus = self:AttemptToPlaceStoneAtGrassPlot(searchX, searchY);
8865 if placedBonus == true then
8866 --print("Placed Stone in first ring at ", searchX, searchY);
8867 innerPlaced = innerPlaced + 1;
8868 iNumStoneNeeded = iNumStoneNeeded - 1;
8869 break
8870 elseif attempt == 6 then
8871 tried_all_first_ring = true;
8872 end
8873 end
8874
8875 elseif tried_all_second_ring == false then
8876 -- Add bonus to second ring.
8877 for attempt = 1, 12 do
8878 local plot_adjustments = randomized_second_ring_adjustments[attempt];
8879 local searchX, searchY = self:ApplyHexAdjustment(x, y, plot_adjustments)
8880 -- Attempt to place Stone at the currently chosen plot.
8881 local placedBonus = self:AttemptToPlaceStoneAtGrassPlot(searchX, searchY);
8882 if placedBonus == true then
8883 --print("Placed Stone in second ring at ", searchX, searchY);
8884 iNumStoneNeeded = iNumStoneNeeded - 1;
8885 break
8886 elseif attempt == 12 then
8887 tried_all_second_ring = true;
8888 end
8889 end
8890
8891 else -- Tried everywhere, have to give up.
8892 break
8893 end
8894 end
8895 end
8896
8897 -- Record conditions at this start location.
8898 local results_table = {alongOcean, nextToLake, isRiver, nearRiver, nearMountain, forestCount, jungleCount};
8899 self.startLocationConditions[region_number] = results_table;
8900end
8901------------------------------------------------------------------------------
8902function AssignStartingPlots:BalanceAndAssign()
8903 -- This function determines what level of Bonus Resource support a location
8904 -- may need, identifies compatibility with civ-specific biases, and places starts.
8905
8906 -- Normalize each start plot location.
8907 local iNumStarts = table.maxn(self.startingPlots);
8908 for region_number = 1, iNumStarts do
8909 self:NormalizeStartLocation(region_number)
8910 end
8911
8912 -- Check Game Option for disabling civ-specific biases.
8913 -- If they are to be disabled, then all civs are simply assigned to start plots at random.
8914 local bDisableStartBias = Game.GetCustomOption("GAMEOPTION_DISABLE_START_BIAS");
8915 if bDisableStartBias == 1 then
8916 --print("-"); print("ALERT: Civ Start Biases have been selected to be Disabled!"); print("-");
8917 local playerList = {};
8918 for loop = 1, self.iNumCivs do
8919 local player_ID = self.player_ID_list[loop];
8920 table.insert(playerList, player_ID);
8921 end
8922 local playerListShuffled = GetShuffledCopyOfTable(playerList)
8923 for region_number, player_ID in ipairs(playerListShuffled) do
8924 local x = self.startingPlots[region_number][1];
8925 local y = self.startingPlots[region_number][2];
8926 local start_plot = Map.GetPlot(x, y)
8927 local player = Players[player_ID]
8928 player:SetStartingPlot(start_plot)
8929 end
8930 -- If this is a team game (any team has more than one Civ in it) then make
8931 -- sure team members start near each other if possible. (This may scramble
8932 -- Civ biases in some cases, but there is no cure).
8933 if self.bTeamGame == true then
8934 self:NormalizeTeamLocations()
8935 end
8936 -- Done with un-biased Civ placement.
8937 return
8938 end
8939
8940 -- If the process reaches here, civ-specific start-location biases are enabled. Handle them now.
8941 -- Create a randomized list of all regions. As a region gets assigned, we'll remove it from the list.
8942 local all_regions = {};
8943 for loop = 1, self.iNumCivs do
8944 table.insert(all_regions, loop);
8945 end
8946 local regions_still_available = GetShuffledCopyOfTable(all_regions)
8947
8948 local civs_needing_coastal_start = {};
8949 local civs_priority_coastal_start = {};
8950 local civs_needing_river_start = {};
8951 local civs_needing_region_priority = {};
8952 local civs_needing_region_avoid = {};
8953 local regions_with_coastal_start = {};
8954 local regions_with_lake_start = {};
8955 local regions_with_river_start = {};
8956 local regions_with_near_river_start = {};
8957 local civ_status = table.fill(false, GameDefines.MAX_MAJOR_CIVS); -- Have to account for possible gaps in player ID numbers, for MP.
8958 local region_status = table.fill(false, self.iNumCivs);
8959 local priority_lists = {};
8960 local avoid_lists = {};
8961 local iNumCoastalCivs, iNumRiverCivs, iNumPriorityCivs, iNumAvoidCivs = 0, 0, 0, 0;
8962 local iNumCoastalCivsRemaining, iNumRiverCivsRemaining, iNumPriorityCivsRemaining, iNumAvoidCivsRemaining = 0, 0, 0, 0;
8963
8964 --print("-"); print("-"); print("--- DEBUG READOUT OF PLAYER START ASSIGNMENTS ---"); print("-");
8965
8966 -- Generate lists of player needs. Each additional need type is subordinate to those
8967 -- that come before. In other words, each Civ can have only one need type.
8968 for loop = 1, self.iNumCivs do
8969 local playerNum = self.player_ID_list[loop]; -- MP games can have gaps between player numbers, so we cannot assume a sequential set of IDs.
8970 local player = Players[playerNum];
8971 local civType = GameInfo.Civilizations[player:GetCivilizationType()].Type;
8972 --print("Player", playerNum, "of Civ Type", civType);
8973 local bNeedsCoastalStart = CivNeedsCoastalStart(civType)
8974 if bNeedsCoastalStart == true then
8975 --print("- - - - - - - needs Coastal Start!"); print("-");
8976 iNumCoastalCivs = iNumCoastalCivs + 1;
8977 iNumCoastalCivsRemaining = iNumCoastalCivsRemaining + 1;
8978 table.insert(civs_needing_coastal_start, playerNum);
8979 if CivNeedsPlaceFirstCoastalStart then
8980 local bPlaceFirst = CivNeedsPlaceFirstCoastalStart(civType);
8981 if bPlaceFirst then
8982 --print("- - - - - - - needs to Place First!"); --print("-");
8983 table.insert(civs_priority_coastal_start, playerNum);
8984 end
8985 end
8986 else
8987 local bNeedsRiverStart = CivNeedsRiverStart(civType)
8988 if bNeedsRiverStart == true then
8989 --print("- - - - - - - needs River Start!"); print("-");
8990 iNumRiverCivs = iNumRiverCivs + 1;
8991 iNumRiverCivsRemaining = iNumRiverCivsRemaining + 1;
8992 table.insert(civs_needing_river_start, playerNum);
8993 else
8994 local iNumRegionPriority = GetNumStartRegionPriorityForCiv(civType)
8995 if iNumRegionPriority > 0 then
8996 --print("- - - - - - - needs Region Priority!"); print("-");
8997 local table_of_this_civs_priority_needs = GetStartRegionPriorityListForCiv_GetIDs(civType)
8998 iNumPriorityCivs = iNumPriorityCivs + 1;
8999 iNumPriorityCivsRemaining = iNumPriorityCivsRemaining + 1;
9000 table.insert(civs_needing_region_priority, playerNum);
9001 priority_lists[playerNum] = table_of_this_civs_priority_needs;
9002 else
9003 local iNumRegionAvoid = GetNumStartRegionAvoidForCiv(civType)
9004 if iNumRegionAvoid > 0 then
9005 --print("- - - - - - - needs Region Avoid!"); print("-");
9006 local table_of_this_civs_avoid_needs = GetStartRegionAvoidListForCiv_GetIDs(civType)
9007 iNumAvoidCivs = iNumAvoidCivs + 1;
9008 iNumAvoidCivsRemaining = iNumAvoidCivsRemaining + 1;
9009 table.insert(civs_needing_region_avoid, playerNum);
9010 avoid_lists[playerNum] = table_of_this_civs_avoid_needs;
9011 end
9012 end
9013 end
9014 end
9015 end
9016
9017 --[[ Debug printout
9018 print("Civs with Coastal Bias:", iNumCoastalCivs);
9019 print("Civs with River Bias:", iNumRiverCivs);
9020 print("Civs with Region Priority:", iNumPriorityCivs);
9021 print("Civs with Region Avoid:", iNumAvoidCivs); print("-");
9022 --]]
9023
9024 -- Handle Coastal Start Bias
9025 if iNumCoastalCivs > 0 then
9026 -- Generate lists of regions eligible to support a coastal start.
9027 local iNumRegionsWithCoastalStart, iNumRegionsWithLakeStart, iNumUnassignableCoastStarts = 0, 0, 0;
9028 for region_number, bAlreadyAssigned in ipairs(region_status) do
9029 if bAlreadyAssigned == false then
9030 if self.startLocationConditions[region_number][1] == true then
9031 --print("Region#", region_number, "has a Coastal Start.");
9032 iNumRegionsWithCoastalStart = iNumRegionsWithCoastalStart + 1;
9033 table.insert(regions_with_coastal_start, region_number);
9034 end
9035 end
9036 end
9037 if iNumRegionsWithCoastalStart < iNumCoastalCivs then
9038 for region_number, bAlreadyAssigned in ipairs(region_status) do
9039 if bAlreadyAssigned == false then
9040 if self.startLocationConditions[region_number][2] == true and
9041 self.startLocationConditions[region_number][1] == false then
9042 --print("Region#", region_number, "has a Lake Start.");
9043 iNumRegionsWithLakeStart = iNumRegionsWithLakeStart + 1;
9044 table.insert(regions_with_lake_start, region_number);
9045 end
9046 end
9047 end
9048 end
9049 if iNumRegionsWithCoastalStart + iNumRegionsWithLakeStart < iNumCoastalCivs then
9050 iNumUnassignableCoastStarts = iNumCoastalCivs - (iNumRegionsWithCoastalStart + iNumRegionsWithLakeStart);
9051 end
9052 -- Now assign those with coastal bias to start locations, where possible.
9053 if iNumCoastalCivs - iNumUnassignableCoastStarts > 0 then
9054 -- create non-priority coastal start list
9055 local non_priority_coastal_start = {};
9056 for loop1, iPlayerNum1 in ipairs(civs_needing_coastal_start) do
9057 local bAdd = true;
9058 for loop2, iPlayerNum2 in ipairs(civs_priority_coastal_start) do
9059 if (iPlayerNum1 == iPlayerNum2) then
9060 bAdd = false;
9061 end
9062 end
9063 if bAdd then
9064 table.insert(non_priority_coastal_start, iPlayerNum1);
9065 end
9066 end
9067
9068 local shuffled_priority_coastal_start = GetShuffledCopyOfTable(civs_priority_coastal_start);
9069 local shuffled_non_priority_coastal_start = GetShuffledCopyOfTable(non_priority_coastal_start);
9070 local shuffled_coastal_civs = {};
9071
9072 -- insert priority coastal starts first
9073 for loop, iPlayerNum in ipairs(shuffled_priority_coastal_start) do
9074 table.insert(shuffled_coastal_civs, iPlayerNum);
9075 end
9076
9077 -- insert non-priority coastal starts second
9078 for loop, iPlayerNum in ipairs(shuffled_non_priority_coastal_start) do
9079 table.insert(shuffled_coastal_civs, iPlayerNum);
9080 end
9081
9082 for loop, iPlayerNum in ipairs(shuffled_coastal_civs) do
9083 --print("shuffled_coastal_civs[" .. loop .. "]: " .. iPlayerNum);
9084 end
9085
9086 local shuffled_coastal_civs = GetShuffledCopyOfTable(civs_needing_coastal_start);
9087 local shuffled_coastal_regions, shuffled_lake_regions;
9088 local current_lake_index = 1;
9089 if iNumRegionsWithCoastalStart > 0 then
9090 shuffled_coastal_regions = GetShuffledCopyOfTable(regions_with_coastal_start);
9091 end
9092 if iNumRegionsWithLakeStart > 0 then
9093 shuffled_lake_regions = GetShuffledCopyOfTable(regions_with_lake_start);
9094 end
9095 for loop, playerNum in ipairs(shuffled_coastal_civs) do
9096 if loop > iNumCoastalCivs - iNumUnassignableCoastStarts then
9097 print("Ran out of Coastal and Lake start locations to assign to Coastal Bias.");
9098 break
9099 end
9100 -- Assign next randomly chosen civ in line to next randomly chosen eligible region.
9101 if loop <= iNumRegionsWithCoastalStart then
9102 -- Assign this civ to a region with coastal start.
9103 local choose_this_region = shuffled_coastal_regions[loop];
9104 local x = self.startingPlots[choose_this_region][1];
9105 local y = self.startingPlots[choose_this_region][2];
9106 local plot = Map.GetPlot(x, y);
9107 local player = Players[playerNum];
9108 player:SetStartingPlot(plot);
9109 --print("Player Number", playerNum, "assigned a COASTAL START BIAS location in Region#", choose_this_region, "at Plot", x, y);
9110 region_status[choose_this_region] = true;
9111 civ_status[playerNum + 1] = true;
9112 iNumCoastalCivsRemaining = iNumCoastalCivsRemaining - 1;
9113 local a, b, c = IdentifyTableIndex(civs_needing_coastal_start, playerNum)
9114 if a then
9115 table.remove(civs_needing_coastal_start, c[1]);
9116 end
9117 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9118 if a then
9119 table.remove(regions_still_available, c[1]);
9120 end
9121 else
9122 -- Out of coastal starts, assign this civ to region with lake start.
9123 local choose_this_region = shuffled_lake_regions[current_lake_index];
9124 local x = self.startingPlots[choose_this_region][1];
9125 local y = self.startingPlots[choose_this_region][2];
9126 local plot = Map.GetPlot(x, y);
9127 local player = Players[playerNum];
9128 player:SetStartingPlot(plot);
9129 --print("Player Number", playerNum, "with Coastal Bias assigned a fallback Lake location in Region#", choose_this_region, "at Plot", x, y);
9130 region_status[choose_this_region] = true;
9131 civ_status[playerNum + 1] = true;
9132 iNumCoastalCivsRemaining = iNumCoastalCivsRemaining - 1;
9133 local a, b, c = IdentifyTableIndex(civs_needing_coastal_start, playerNum)
9134 if a then
9135 table.remove(civs_needing_coastal_start, c[1]);
9136 end
9137 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9138 if a then
9139 table.remove(regions_still_available, c[1]);
9140 end
9141 current_lake_index = current_lake_index + 1;
9142 end
9143 end
9144 --else
9145 --print("Either no civs required a Coastal Start, or no Coastal Starts were available.");
9146 end
9147 end
9148
9149 -- Handle River bias
9150 if iNumRiverCivs > 0 or iNumCoastalCivsRemaining > 0 then
9151 -- Generate lists of regions eligible to support a river start.
9152 local iNumRegionsWithRiverStart, iNumRegionsNearRiverStart, iNumUnassignableRiverStarts = 0, 0, 0;
9153 for region_number, bAlreadyAssigned in ipairs(region_status) do
9154 if bAlreadyAssigned == false then
9155 if self.startLocationConditions[region_number][3] == true then
9156 iNumRegionsWithRiverStart = iNumRegionsWithRiverStart + 1;
9157 table.insert(regions_with_river_start, region_number);
9158 end
9159 end
9160 end
9161 for region_number, bAlreadyAssigned in ipairs(region_status) do
9162 if bAlreadyAssigned == false then
9163 if self.startLocationConditions[region_number][4] == true and
9164 self.startLocationConditions[region_number][3] == false then
9165 iNumRegionsNearRiverStart = iNumRegionsNearRiverStart + 1;
9166 table.insert(regions_with_near_river_start, region_number);
9167 end
9168 end
9169 end
9170 if iNumRegionsWithRiverStart + iNumRegionsNearRiverStart < iNumRiverCivs then
9171 iNumUnassignableRiverStarts = iNumRiverCivs - (iNumRegionsWithRiverStart + iNumRegionsNearRiverStart);
9172 end
9173 -- Now assign those with river bias to start locations, where possible.
9174 -- Also handle fallback placement for coastal bias that failed to find a match.
9175 if iNumRiverCivs - iNumUnassignableRiverStarts > 0 then
9176 local shuffled_river_civs = GetShuffledCopyOfTable(civs_needing_river_start);
9177 local shuffled_river_regions, shuffled_near_river_regions;
9178 if iNumRegionsWithRiverStart > 0 then
9179 shuffled_river_regions = GetShuffledCopyOfTable(regions_with_river_start);
9180 end
9181 if iNumRegionsNearRiverStart > 0 then
9182 shuffled_near_river_regions = GetShuffledCopyOfTable(regions_with_near_river_start);
9183 end
9184 for loop, playerNum in ipairs(shuffled_river_civs) do
9185 if loop > iNumRiverCivs - iNumUnassignableRiverStarts then
9186 print("Ran out of River and Near-River start locations to assign to River Bias.");
9187 break
9188 end
9189 -- Assign next randomly chosen civ in line to next randomly chosen eligible region.
9190 if loop <= iNumRegionsWithRiverStart then
9191 -- Assign this civ to a region with river start.
9192 local choose_this_region = shuffled_river_regions[loop];
9193 local x = self.startingPlots[choose_this_region][1];
9194 local y = self.startingPlots[choose_this_region][2];
9195 local plot = Map.GetPlot(x, y);
9196 local player = Players[playerNum];
9197 player:SetStartingPlot(plot);
9198 --print("Player Number", playerNum, "assigned a RIVER START BIAS location in Region#", choose_this_region, "at Plot", x, y);
9199 region_status[choose_this_region] = true;
9200 civ_status[playerNum + 1] = true;
9201 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9202 if a then
9203 table.remove(regions_still_available, c[1]);
9204 end
9205 else
9206 -- Assign this civ to a region where a river is near the start.
9207 local choose_this_region = shuffled_near_river_regions[loop - iNumRegionsWithRiverStart];
9208 local x = self.startingPlots[choose_this_region][1];
9209 local y = self.startingPlots[choose_this_region][2];
9210 local plot = Map.GetPlot(x, y);
9211 local player = Players[playerNum];
9212 player:SetStartingPlot(plot);
9213 --print("Player Number", playerNum, "with River Bias assigned a fallback 'near river' location in Region#", choose_this_region, "at Plot", x, y);
9214 region_status[choose_this_region] = true;
9215 civ_status[playerNum + 1] = true;
9216 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9217 if a then
9218 table.remove(regions_still_available, c[1]);
9219 end
9220 end
9221 end
9222 end
9223 -- Now handle any fallbacks for unassigned coastal bias.
9224 if iNumCoastalCivsRemaining > 0 and iNumRiverCivs < iNumRegionsWithRiverStart + iNumRegionsNearRiverStart then
9225 local iNumFallbacksWithRiverStart, iNumFallbacksNearRiverStart = 0, 0;
9226 local fallbacks_with_river_start, fallbacks_with_near_river_start = {}, {};
9227 for region_number, bAlreadyAssigned in ipairs(region_status) do
9228 if bAlreadyAssigned == false then
9229 if self.startLocationConditions[region_number][3] == true then
9230 iNumFallbacksWithRiverStart = iNumFallbacksWithRiverStart + 1;
9231 table.insert(fallbacks_with_river_start, region_number);
9232 end
9233 end
9234 end
9235 for region_number, bAlreadyAssigned in ipairs(region_status) do
9236 if bAlreadyAssigned == false then
9237 if self.startLocationConditions[region_number][4] == true and
9238 self.startLocationConditions[region_number][3] == false then
9239 iNumFallbacksNearRiverStart = iNumFallbacksNearRiverStart + 1;
9240 table.insert(fallbacks_with_near_river_start, region_number);
9241 end
9242 end
9243 end
9244 if iNumFallbacksWithRiverStart + iNumFallbacksNearRiverStart > 0 then
9245
9246 local shuffled_coastal_fallback_civs = GetShuffledCopyOfTable(civs_needing_coastal_start);
9247 local shuffled_river_fallbacks, shuffled_near_river_fallbacks;
9248 if iNumFallbacksWithRiverStart > 0 then
9249 shuffled_river_fallbacks = GetShuffledCopyOfTable(fallbacks_with_river_start);
9250 end
9251 if iNumFallbacksNearRiverStart > 0 then
9252 shuffled_near_river_fallbacks = GetShuffledCopyOfTable(fallbacks_with_near_river_start);
9253 end
9254 for loop, playerNum in ipairs(shuffled_coastal_fallback_civs) do
9255 if loop > iNumFallbacksWithRiverStart + iNumFallbacksNearRiverStart then
9256 print("Ran out of River and Near-River start locations to assign as fallbacks for Coastal Bias.");
9257 break
9258 end
9259 -- Assign next randomly chosen civ in line to next randomly chosen eligible region.
9260 if loop <= iNumFallbacksWithRiverStart then
9261 -- Assign this civ to a region with river start.
9262 local choose_this_region = shuffled_river_fallbacks[loop];
9263 local x = self.startingPlots[choose_this_region][1];
9264 local y = self.startingPlots[choose_this_region][2];
9265 local plot = Map.GetPlot(x, y);
9266 local player = Players[playerNum];
9267 player:SetStartingPlot(plot);
9268 --print("Player Number", playerNum, "with Coastal Bias assigned a fallback river location in Region#", choose_this_region, "at Plot", x, y);
9269 region_status[choose_this_region] = true;
9270 civ_status[playerNum + 1] = true;
9271 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9272 if a then
9273 table.remove(regions_still_available, c[1]);
9274 end
9275 else
9276 -- Assign this civ to a region where a river is near the start.
9277 local choose_this_region = shuffled_near_river_fallbacks[loop - iNumRegionsWithRiverStart];
9278 local x = self.startingPlots[choose_this_region][1];
9279 local y = self.startingPlots[choose_this_region][2];
9280 local plot = Map.GetPlot(x, y);
9281 local player = Players[playerNum];
9282 player:SetStartingPlot(plot);
9283 --print("Player Number", playerNum, "with Coastal Bias assigned a fallback 'near river' location in Region#", choose_this_region, "at Plot", x, y);
9284 region_status[choose_this_region] = true;
9285 civ_status[playerNum + 1] = true;
9286 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9287 if a then
9288 table.remove(regions_still_available, c[1]);
9289 end
9290 end
9291 end
9292 end
9293 end
9294 end
9295
9296 -- Handle Region Priority
9297 if iNumPriorityCivs > 0 then
9298 --print("-"); print("-"); print("--- REGION PRIORITY READOUT ---"); print("-");
9299 local iNumSinglePriority, iNumMultiPriority, iNumNeedFallbackPriority = 0, 0, 0;
9300 local single_priority, multi_priority, fallback_priority = {}, {}, {};
9301 local single_sorted, multi_sorted = {}, {};
9302 -- Separate priority civs in to two categories: single priority, multiple priority.
9303 for playerNum, priority_needs in pairs(priority_lists) do
9304 local len = table.maxn(priority_needs)
9305 if len == 1 then
9306 --print("Player#", playerNum, "has a single Region Priority of type", priority_needs[1]);
9307 local priority_data = {playerNum, priority_needs[1]};
9308 table.insert(single_priority, priority_data)
9309 iNumSinglePriority = iNumSinglePriority + 1;
9310 else
9311 --print("Player#", playerNum, "has multiple Region Priority, this many types:", len);
9312 local priority_data = {playerNum, len};
9313 table.insert(multi_priority, priority_data)
9314 iNumMultiPriority = iNumMultiPriority + 1;
9315 end
9316 end
9317 -- Single priority civs go first, and will engage fallback methods if no match found.
9318 if iNumSinglePriority > 0 then
9319 -- Sort the list so that proper order of execution occurs. (Going to use a blunt method for easy coding.)
9320 for region_type = 1, 8 do -- Must expand if new region types are added.
9321 for loop, data in ipairs(single_priority) do
9322 if data[2] == region_type then
9323 --print("Adding Player#", data[1], "to sorted list of single Region Priority.");
9324 table.insert(single_sorted, data);
9325 end
9326 end
9327 end
9328 -- Match civs who have a single Region Priority to the region type they need, if possible.
9329 for loop, data in ipairs(single_sorted) do
9330 local iPlayerNum = data[1];
9331 local iPriorityType = data[2];
9332 --print("* Attempting to assign Player#", iPlayerNum, "to a region of Type#", iPriorityType);
9333 local bFoundCandidate, candidate_regions = false, {};
9334 for test_loop, region_number in ipairs(regions_still_available) do
9335 if self.regionTypes[region_number] == iPriorityType then
9336 table.insert(candidate_regions, region_number);
9337 bFoundCandidate = true;
9338 --print("- - Found candidate: Region#", region_number);
9339 end
9340 end
9341 if bFoundCandidate then
9342 local diceroll = 1 + Map.Rand(table.maxn(candidate_regions), "Choosing from among Candidate Regions for start bias - LUA");
9343 local choose_this_region = candidate_regions[diceroll];
9344 local x = self.startingPlots[choose_this_region][1];
9345 local y = self.startingPlots[choose_this_region][2];
9346 local plot = Map.GetPlot(x, y);
9347 local player = Players[iPlayerNum];
9348 player:SetStartingPlot(plot);
9349 --print("Player Number", iPlayerNum, "with single Region Priority assigned to Region#", choose_this_region, "at Plot", x, y);
9350 region_status[choose_this_region] = true;
9351 civ_status[iPlayerNum + 1] = true;
9352 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9353 if a then
9354 table.remove(regions_still_available, c[1]);
9355 end
9356 else
9357 table.insert(fallback_priority, data)
9358 iNumNeedFallbackPriority = iNumNeedFallbackPriority + 1;
9359 --print("Player Number", iPlayerNum, "with single Region Priority was UNABLE to be matched to its type. Added to fallback list.");
9360 end
9361 end
9362 end
9363 -- Multiple priority civs go next, with fewest regions of priority going first.
9364 if iNumMultiPriority > 0 then
9365 for iNumPriorities = 2, 8 do -- Must expand if new region types are added.
9366 for loop, data in ipairs(multi_priority) do
9367 if data[2] == iNumPriorities then
9368 --print("Adding Player#", data[1], "to sorted list of multi Region Priority.");
9369 table.insert(multi_sorted, data);
9370 end
9371 end
9372 end
9373 -- Match civs who have mulitple Region Priority to one of the region types they need, if possible.
9374 for loop, data in ipairs(multi_sorted) do
9375 local iPlayerNum = data[1];
9376 local iNumPriorityTypes = data[2];
9377 --print("* Attempting to assign Player#", iPlayerNum, "to one of its Priority Region Types.");
9378 local bFoundCandidate, candidate_regions = false, {};
9379 for test_loop, region_number in ipairs(regions_still_available) do
9380 for inner_loop = 1, iNumPriorityTypes do
9381 local region_type_to_test = priority_lists[iPlayerNum][inner_loop];
9382 if self.regionTypes[region_number] == region_type_to_test then
9383 table.insert(candidate_regions, region_number);
9384 bFoundCandidate = true;
9385 --print("- - Found candidate: Region#", region_number);
9386 end
9387 end
9388 end
9389 if bFoundCandidate then
9390 local diceroll = 1 + Map.Rand(table.maxn(candidate_regions), "Choosing from among Candidate Regions for start bias - LUA");
9391 local choose_this_region = candidate_regions[diceroll];
9392 local x = self.startingPlots[choose_this_region][1];
9393 local y = self.startingPlots[choose_this_region][2];
9394 local plot = Map.GetPlot(x, y);
9395 local player = Players[iPlayerNum];
9396 player:SetStartingPlot(plot);
9397 --print("Player Number", iPlayerNum, "with multiple Region Priority assigned to Region#", choose_this_region, "at Plot", x, y);
9398 region_status[choose_this_region] = true;
9399 civ_status[iPlayerNum + 1] = true;
9400 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9401 if a then
9402 table.remove(regions_still_available, c[1]);
9403 end
9404 --else
9405 --print("Player Number", iPlayerNum, "with multiple Region Priority was unable to be matched.");
9406 end
9407 end
9408 end
9409 -- Fallbacks are done (if needed) after multiple-region priority is handled. The list is pre-sorted.
9410 if iNumNeedFallbackPriority > 0 then
9411 for loop, data in ipairs(fallback_priority) do
9412 local iPlayerNum = data[1];
9413 local iPriorityType = data[2];
9414 --print("* Attempting to assign Player#", iPlayerNum, "to a fallback region as similar as possible to Region Type#", iPriorityType);
9415 local choose_this_region = self:FindFallbackForUnmatchedRegionPriority(iPriorityType, regions_still_available)
9416 if choose_this_region == -1 then
9417 --print("FAILED to find fallback region bias for player#", iPlayerNum);
9418 else
9419 local x = self.startingPlots[choose_this_region][1];
9420 local y = self.startingPlots[choose_this_region][2];
9421 local plot = Map.GetPlot(x, y);
9422 local player = Players[iPlayerNum];
9423 player:SetStartingPlot(plot);
9424 --print("Player Number", iPlayerNum, "with single Region Priority assigned to FALLBACK Region#", choose_this_region, "at Plot", x, y);
9425 region_status[choose_this_region] = true;
9426 civ_status[iPlayerNum + 1] = true;
9427 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9428 if a then
9429 table.remove(regions_still_available, c[1]);
9430 end
9431 end
9432 end
9433 end
9434 end
9435
9436 -- Handle Region Avoid
9437 if iNumAvoidCivs > 0 then
9438 --print("-"); print("-"); print("--- REGION AVOID READOUT ---"); print("-");
9439 local avoid_sorted, avoid_unsorted, avoid_counts = {}, {}, {};
9440 -- Sort list of civs with Avoid needs, then process in reverse order, so most needs goes first.
9441 for playerNum, avoid_needs in pairs(avoid_lists) do
9442 local len = table.maxn(avoid_needs)
9443 --print("- Player#", playerNum, "has this number of Region Avoid needs:", len);
9444 local avoid_data = {playerNum, len};
9445 table.insert(avoid_unsorted, avoid_data)
9446 table.insert(avoid_counts, len)
9447 end
9448 table.sort(avoid_counts)
9449 for loop, avoid_count in ipairs(avoid_counts) do
9450 for test_loop, avoid_data in ipairs(avoid_unsorted) do
9451 if avoid_count == avoid_data[2] then
9452 table.insert(avoid_sorted, avoid_data[1])
9453 table.remove(avoid_unsorted, test_loop)
9454 end
9455 end
9456 end
9457 -- Process the Region Avoid needs.
9458 for loop = iNumAvoidCivs, 1, -1 do
9459 local iPlayerNum = avoid_sorted[loop];
9460 local candidate_regions = {};
9461 for test_loop, region_number in ipairs(regions_still_available) do
9462 local bFoundCandidate = true;
9463 for inner_loop, region_type_to_avoid in ipairs(avoid_lists[iPlayerNum]) do
9464 if self.regionTypes[region_number] == region_type_to_avoid then
9465 bFoundCandidate = false;
9466 end
9467 end
9468 if bFoundCandidate == true then
9469 table.insert(candidate_regions, region_number);
9470 --print("- - Found candidate: Region#", region_number)
9471 end
9472 end
9473 if table.maxn(candidate_regions) > 0 then
9474 local diceroll = 1 + Map.Rand(table.maxn(candidate_regions), "Choosing from among Candidate Regions for start bias - LUA");
9475 local choose_this_region = candidate_regions[diceroll];
9476 local x = self.startingPlots[choose_this_region][1];
9477 local y = self.startingPlots[choose_this_region][2];
9478 local plot = Map.GetPlot(x, y);
9479 local player = Players[iPlayerNum];
9480 player:SetStartingPlot(plot);
9481 --print("Player Number", iPlayerNum, "with Region Avoid assigned to allowed region type in Region#", choose_this_region, "at Plot", x, y);
9482 region_status[choose_this_region] = true;
9483 civ_status[iPlayerNum + 1] = true;
9484 local a, b, c = IdentifyTableIndex(regions_still_available, choose_this_region)
9485 if a then
9486 table.remove(regions_still_available, c[1]);
9487 end
9488 --else
9489 --print("Player Number", iPlayerNum, "with Region Avoid was unable to avoid the undesired region types.");
9490 end
9491 end
9492 end
9493
9494 -- Assign remaining civs to start plots.
9495 local playerList, regionList = {}, {};
9496 for loop = 1, self.iNumCivs do
9497 local player_ID = self.player_ID_list[loop];
9498 if civ_status[player_ID + 1] == false then -- Using C++ player ID, which starts at zero. Add 1 for Lua indexing.
9499 table.insert(playerList, player_ID);
9500 end
9501 if region_status[loop] == false then
9502 table.insert(regionList, loop);
9503 end
9504 end
9505 local iNumRemainingPlayers = table.maxn(playerList);
9506 local iNumRemainingRegions = table.maxn(regionList);
9507 if iNumRemainingPlayers > 0 or iNumRemainingRegions > 0 then
9508 --print("-"); print("Table of players with no start bias:");
9509 --PrintContentsOfTable(playerList);
9510 --print("-"); print("Table of regions still available after bias handling:");
9511 --PrintContentsOfTable(regionList);
9512 if iNumRemainingPlayers ~= iNumRemainingRegions then
9513 print("-"); print("ERROR: Number of civs remaining after handling biases does not match number of regions remaining!"); print("-");
9514 end
9515 local playerListShuffled = GetShuffledCopyOfTable(playerList)
9516 for index, player_ID in ipairs(playerListShuffled) do
9517 local region_number = regionList[index];
9518 local x = self.startingPlots[region_number][1];
9519 local y = self.startingPlots[region_number][2];
9520 --print("Now placing Player#", player_ID, "in Region#", region_number, "at start plot:", x, y);
9521 local start_plot = Map.GetPlot(x, y)
9522 local player = Players[player_ID]
9523 player:SetStartingPlot(start_plot)
9524 end
9525 end
9526
9527 -- If this is a team game (any team has more than one Civ in it) then make
9528 -- sure team members start near each other if possible. (This may scramble
9529 -- Civ biases in some cases, but there is no cure).
9530 if self.bTeamGame == true then
9531 self:NormalizeTeamLocations()
9532 end
9533 --
9534end
9535------------------------------------------------------------------------------
9536end
9537
9538
9539
9540
9541
9542
9543
9544
9545
9546
9547
9548
9549
9550
9551
9552
9553
9554
9555
9556
9557
9558
9559
9560--
9561-- RouteConnections.lua
9562--
9563-- Copyright 2011 (c) William Howard
9564--
9565-- Determines if a route exists between two plots/cities
9566--
9567-- Permission granted to re-distribute this file as part of a mod
9568-- on the condition that this comment block is preserved in its entirity
9569--
9570
9571
9572----- PUBLIC METHODS -----
9573
9574
9575
9576--
9577-- pPlayer - player object (not ID) or nil
9578-- pStartPlot, pTargetPlot - plot objects (not IDs)
9579-- sRoute - one of mg.routes (see above)
9580-- bShortestRoute - true to find the shortest route
9581-- sHighlight - one of the highlight keys (see above)
9582-- fBlockaded - call-back function of the form f(pPlot, pPlayer) to determine if a plot is blocked for this player (return true if blocked)
9583--
9584
9585function isCityConnected(pPlayer, pStartCity, pTargetCity, sRoute, bShortestRoute, sHighlight, fBlockaded)
9586 return isPlotConnected(pPlayer, pStartCity:Plot(), pTargetCity:Plot(), sRoute, bShortestRoute, sHighlight, fBlockaded)
9587end
9588
9589function isPlotConnected(pPlayer, pStartPlot, pTargetPlot, sRoute, bShortestRoute, sHighlight, fBlockaded)
9590 if (bShortestRoute) then
9591 mg.lastRouteLength = plotToPlotShortestRoute(pPlayer, pStartPlot, pTargetPlot, sRoute, mg.highlights[sHighlight], fBlockaded)
9592 else
9593 mg.lastRouteLength = plotToPlotConnection(pPlayer, pStartPlot, pTargetPlot, sRoute, 1, mg.highlights[sHighlight], listAddPlot(pStartPlot, {}), fBlockaded)
9594 end
9595
9596 return (mg.lastRouteLength ~= 0)
9597end
9598
9599function getRouteLength()
9600 return mg.lastRouteLength
9601end
9602
9603function getDistance(pPlot1, pPlot2)
9604 return distanceBetween(pPlot1, pPlot2)
9605end
9606
9607
9608----- PRIVATE DATA AND METHODS -----
9609
9610--
9611-- Check if pStartPlot is connected to pTargetPlot
9612--
9613-- NOTE: This is a recursive method
9614--
9615-- Returns the length of the route between the start and target plots (inclusive) - so 0 if no route
9616--
9617
9618function plotToPlotConnection(pPlayer, pStartPlot, pTargetPlot, sRoute, iLength, highlight, listVisitedPlots, fBlockaded)
9619 if (highlight ~= nil) then
9620 Events.SerialEventHexHighlight(PlotToHex(pStartPlot), true, highlight)
9621 end
9622
9623 -- Have we got there yet?
9624 if (isSamePlot(pStartPlot, pTargetPlot)) then
9625 return iLength
9626 end
9627
9628 -- Find any new plots we can visit from here
9629 local listRoutes = listFilter(reachablePlots(pPlayer, pStartPlot, sRoute, fBlockaded), listVisitedPlots)
9630
9631 -- New routes to check, so there is an onward path
9632 if (listRoutes ~= nil) then
9633 -- Covert the associative array into a linear array so it can be sorted
9634 local array = {}
9635 for sId, pPlot in pairs(listRoutes) do
9636 table.insert(array, pPlot)
9637 end
9638
9639 -- Now sort the linear array by distance from the target plot
9640 table.sort(array, function(x, y) return (distanceBetween(x, pTargetPlot) < distanceBetween(y, pTargetPlot)) end)
9641
9642 -- Now check each onward plot in turn to see if that is connected
9643 for i, pPlot in ipairs(array) do
9644 -- Check that a prior route didn't visit this plot
9645 if (not listContainsPlot(pPlot, listVisitedPlots)) then
9646 -- Add this plot to the list of visited plots
9647 listAddPlot(pPlot, listVisitedPlots)
9648
9649 -- If there's a route, we're done
9650 local iLen = plotToPlotConnection(pPlayer, pPlot, pTargetPlot, sRoute, iLength+1, highlight, listVisitedPlots, fBlockaded)
9651 if (iLen > 0) then
9652 return iLen
9653 end
9654 end
9655 end
9656 end
9657
9658 if (highlight ~= nil) then
9659 Events.SerialEventHexHighlight(PlotToHex(pStartPlot), false)
9660 end
9661
9662 -- No connection found
9663 return 0
9664end
9665
9666
9667--
9668-- Find the shortest route between two plots
9669--
9670-- We start at the TARGET plot - as the path length from here to the target plot is 1,
9671-- we will call this "ring 1". We then find all reachable near plots and place them in "ring 2".
9672-- If the START plot is in "ring 2", we have a route, if "ring 2" is empty, there is no route,
9673-- otherwise find all reachable near plots that have not already been seen and place those in "ring 3"
9674-- We then loop, checking "ring N" otherwise generating "ring N+1"
9675--
9676-- Once we have found a route, the path length will be of length N and we know that there must be at
9677-- least one route by picking a plot from each ring. The plot needed from "ring N" is the START plot,
9678-- we then need ANY plot from "ring N-1" that is near to the start plot. And in general we need
9679-- any plot from "ring M-1" that is near to the plot choosen from "ring M". The final plot in
9680-- the path will always be the target plot as that is the only plot in "ring 1"
9681--
9682-- Returns the length of the route between the start and target plots (inclusive) - so 0 if no route
9683--
9684
9685function plotToPlotShortestRoute(pPlayer, pStartPlot, pTargetPlot, sRoute, highlight, fBlockaded)
9686 local rings = {}
9687
9688 local iRing = 1
9689 rings[iRing] = listAddPlot(pTargetPlot, {})
9690
9691 repeat
9692 iRing = generateNextRing(pPlayer, sRoute, rings, iRing, fBlockaded)
9693
9694 bFound = listContainsPlot(pStartPlot, rings[iRing])
9695 bNoRoute = (rings[iRing] == nil)
9696 until (bFound or bNoRoute)
9697
9698 if (bFound and highlight ~= nil) then
9699 Events.SerialEventHexHighlight(PlotToHex(pStartPlot), true, highlight)
9700
9701 local pLastPlot = pStartPlot
9702
9703 for i = iRing - 1, 1, -1 do
9704 pNextPlot = listFirstAdjacentPlot(pLastPlot, rings[i])
9705
9706 -- Check should be completely unnecessary
9707 if (pNextPlot == nil) then
9708 return 0
9709 end
9710
9711 Events.SerialEventHexHighlight(PlotToHex(pNextPlot), true, highlight)
9712
9713 pLastPlot = pNextPlot
9714 end
9715 end
9716
9717 return (bFound) and iRing or 0
9718end
9719
9720-- Helper method to find all plots near to the plots in the specified ring
9721function generateNextRing(pPlayer, sRoute, rings, iRing, fBlockaded)
9722 local nextRing = nil
9723
9724 --print("generateNextRing " .. iRing)
9725 for k, pPlot in pairs(rings[iRing]) do
9726 -- Consider two near tiles A and B, if A is in ring N, B must either be unvisited or in ring N-1
9727 -- for if B was in ring N-2, A would have to be in ring N-1 - which it is not
9728 local listRoutes = listFilter(reachablePlots(pPlayer, pPlot, sRoute, fBlockaded), ((iRing > 1) and rings[iRing-1] or {}))
9729
9730 if (listRoutes ~= nil) then
9731 for sId, pPlot in pairs(listRoutes) do
9732 nextRing = nextRing or {}
9733 listAddPlot(pPlot, nextRing)
9734 end
9735 end
9736 end
9737
9738 rings[iRing+1] = nextRing
9739
9740 return iRing+1
9741end
9742
9743
9744--
9745-- Methods dealing with finding all near tiles that can be reached by the specified route type
9746--
9747
9748-- Return a list of (up to 6) reachable plots from this one by route type
9749function reachablePlots(pPlayer, pPlot, sRoute, fBlockaded)
9750 local list = nil
9751 for pDestPlot in Plot_GetPlotsInCircle(pPlot, 1) do
9752 -- Don't let submarines fall over the edge!
9753 if (pDestPlot ~= nil) then
9754 if (pPlayer == nil or pDestPlot:IsRevealed(pPlayer:GetTeam())) then
9755 local bAdd = false
9756 -- Be careful of order, must check for road before rail, and coastal before ocean
9757 if (sRoute == mg.routes[1] and (pDestPlot:GetPlotType() == PlotTypes.PLOT_LAND or pDestPlot:GetPlotType() == PlotTypes.PLOT_HILLS)) then
9758 bAdd = true
9759 elseif (sRoute == mg.routes[2] and pDestPlot:GetRouteType() >= 0) then
9760 bAdd = true
9761 elseif (sRoute == mg.routes[3] and pDestPlot:GetRouteType() >= 1) then
9762 bAdd = true
9763 elseif (sRoute == mg.routes[4] and pDestPlot:GetTerrainType() == TerrainTypes.TERRAIN_COAST) then
9764 bAdd = true
9765 elseif (sRoute == mg.routes[5] and pDestPlot:IsWater()) then
9766 bAdd = true
9767 elseif (sRoute == mg.routes[6] and pDestPlot:IsWater()) then
9768 bAdd = true
9769 end
9770 -- Special case for water, a city on the coast counts as water
9771 if (not bAdd and (sRoute == mg.routes[4] or sRoute == mg.routes[5] or sRoute == mg.routes[6])) then
9772 bAdd = pDestPlot:IsCity()
9773 end
9774 -- Check for impassable and blockaded tiles
9775 bAdd = bAdd and isPassable(pDestPlot, sRoute) and not isBlockaded(pDestPlot, pPlayer, fBlockaded)
9776 if (bAdd) then
9777 list = list or {}
9778 listAddPlot(pDestPlot, list)
9779 end
9780 end
9781 end
9782 end
9783 return list
9784end
9785
9786-- Is the plot passable for this route type ..
9787function isPassable(pPlot, sRoute)
9788 bPassable = true
9789
9790 -- .. due to terrain, eg natural wonders and those covered in ice
9791 iFeature = pPlot:GetFeatureType()
9792 if (iFeature > 0 and GameInfo.Features[iFeature].NaturalWonder == true) then
9793 bPassable = false
9794 elseif (iFeature == FeatureTypes.FEATURE_ICE and sRoute ~= mg.routes[6]) then
9795 bPassable = false
9796 end
9797
9798 return bPassable
9799end
9800
9801-- Is the plot blockaded for this player ..
9802function isBlockaded(pPlot, pPlayer, fBlockaded)
9803 bBlockaded = false
9804
9805 if (fBlockaded ~= nil) then
9806 bBlockaded = fBlockaded(pPlot, pPlayer)
9807 end
9808
9809 return bBlockaded
9810end
9811
9812
9813
9814--
9815-- Calculate the distance between two plots
9816--
9817-- See http://www-cs-students.stanford.edu/~amitp/Articles/HexLOS.html
9818-- Also http://keekerdc.com/2011/03/hexagon-grids-coordinate-systems-and-distance-calculations/
9819--
9820function distanceBetween(pPlot1, pPlot2)
9821 local mapX, mapY = Map.GetGridSize()
9822
9823 -- Need to work on a hex based grid
9824 local hex1 = PlotToHex(pPlot1)
9825 local hex2 = PlotToHex(pPlot2)
9826
9827 -- Calculate the distance between the x and z co-ordinate pairs
9828 -- allowing for the East-West wrap, (ie shortest route may be by going backwards!)
9829 local deltaX = math.min(math.abs(hex2.x - hex1.x), mapX - math.abs(hex2.x - hex1.x))
9830 local deltaZ = math.min(math.abs(hex2.z - hex1.z), mapX - math.abs(hex2.z - hex1.z))
9831
9832 -- Calculate the distance between the y co-ordinates
9833 -- there is no North-South wrap, so this is easy
9834 local deltaY = math.abs(hex2.y - hex1.y)
9835
9836 -- Calculate the distance between the plots
9837 local distance = math.max(deltaX, deltaY, deltaZ)
9838
9839 -- Allow for both end points in the distance calculation
9840 return distance + 1
9841end
9842
9843-- Get the hex co-ordinates of a plot
9844function PlotToHex(pPlot)
9845 local hex = ToHexFromGrid(Vector2(pPlot:GetX(), pPlot:GetY()))
9846
9847 -- X + y + z = 0, hence z = -(x+y)
9848 hex.z = -(hex.x + hex.y)
9849
9850 return hex
9851end
9852
9853
9854--
9855-- List (associative arrays) helper methods
9856--
9857
9858-- Return a list formed by removing all entries from list1 which are in list2
9859function listFilter(list1, list2)
9860 local list = nil
9861
9862 if (list1 ~= nil) then
9863 for sKey, pPlot in pairs(list1) do
9864 if (list2 == nil or list2[sKey] == nil) then
9865 list = list or {}
9866 list[sKey] = pPlot
9867 end
9868 end
9869 end
9870
9871 return list
9872end
9873
9874-- Return true if pPlot is in list
9875function listContainsPlot(pPlot, list)
9876 return (list ~= nil and list[getPlotKey(pPlot)] ~= nil)
9877end
9878
9879-- Add the plot to the list
9880function listAddPlot(pPlot, list)
9881 if (list ~= nil) then
9882 list[getPlotKey(pPlot)] = pPlot
9883 end
9884
9885 return list
9886end
9887
9888function listFirstAdjacentPlot(pPlot, list)
9889 for key, plot in pairs(list) do
9890 if (distanceBetween(pPlot, plot) == 2) then
9891 return plot
9892 end
9893 end
9894
9895 -- We should NEVER reach here
9896 return nil
9897end
9898
9899
9900--
9901-- Plot helper methods
9902--
9903
9904-- Are the plots one and the same?
9905function isSamePlot(pPlot1, pPlot2)
9906 return (pPlot1:GetX() == pPlot2:GetX() and pPlot1:GetY() == pPlot2:GetY())
9907end
9908
9909-- Get a unique key for the plot
9910function getPlotKey(pPlot)
9911 return string.format("%d:%d", pPlot:GetX(), pPlot:GetY())
9912end
9913
9914-- Get the grid-based (x, y) co-ordinates of the plot as a string
9915function plotToGridStr(pPlot)
9916 if (pPlot == nil) then return "" end
9917
9918 return string.format("(%d, %d)", pPlot:GetX(), pPlot:GetY())
9919end
9920
9921-- Get the hex-based (x, y, z) co-ordinates of the plot as a string
9922function plotToHexStr(pPlot)
9923 if (pPlot == nil) then return "" end
9924
9925 local hex = PlotToHex(pPlot)
9926
9927 return string.format("(%d, %d, %d)", hex.x, hex.y, hex.z)
9928end