1 #include "stdafx.h"
2 #include "model_builder.h"
3 #include "level.h"
4 #include "tribe.h"
5 #include "inventory.h"
6 #include "collective_builder.h"
7 #include "options.h"
8 #include "player_control.h"
9 #include "spectator.h"
10 #include "creature.h"
11 #include "square.h"
12 #include "progress_meter.h"
13 #include "collective.h"
14 #include "level_maker.h"
15 #include "model.h"
16 #include "level_builder.h"
17 #include "monster_ai.h"
18 #include "game.h"
19 #include "campaign.h"
20 #include "creature_name.h"
21 #include "villain_type.h"
22 #include "enemy_factory.h"
23 #include "view_object.h"
24 #include "item.h"
25 #include "furniture.h"
26 #include "sokoban_input.h"
27 #include "external_enemies.h"
28 #include "immigration.h"
29 #include "technology.h"
30 #include "keybinding.h"
31 #include "tutorial.h"
32 #include "settlement_info.h"
33 #include "enemy_info.h"
34 
35 using namespace std::chrono;
36 
ModelBuilder(ProgressMeter * m,RandomGen & r,Options * o,SokobanInput * sok)37 ModelBuilder::ModelBuilder(ProgressMeter* m, RandomGen& r, Options* o, SokobanInput* sok) : random(r), meter(m), options(o),
38   enemyFactory(EnemyFactory(random)), sokobanInput(sok) {
39 }
40 
~ModelBuilder()41 ModelBuilder::~ModelBuilder() {
42 }
43 
getPigstyPopulationIncrease()44 int ModelBuilder::getPigstyPopulationIncrease() {
45   return 4;
46 }
47 
getStatuePopulationIncrease()48 int ModelBuilder::getStatuePopulationIncrease() {
49   return 1;
50 }
51 
getThronePopulationIncrease()52 int ModelBuilder::getThronePopulationIncrease() {
53   return 10;
54 }
55 
getKeeperConfig(RandomGen & random,bool fastImmigration,bool regenerateMana)56 static CollectiveConfig getKeeperConfig(RandomGen& random, bool fastImmigration, bool regenerateMana) {
57   return CollectiveConfig::keeper(
58       fastImmigration ? 10 : 140,
59       10,
60       regenerateMana,
61       {
62       CONSTRUCT(PopulationIncrease,
63         c.type = FurnitureType::PIGSTY;
64         c.increasePerSquare = 0.25;
65         c.maxIncrease = ModelBuilder::getPigstyPopulationIncrease();),
66       CONSTRUCT(PopulationIncrease,
67         c.type = FurnitureType::MINION_STATUE;
68         c.increasePerSquare = ModelBuilder::getStatuePopulationIncrease();
69         c.maxIncrease = 1000;),
70       CONSTRUCT(PopulationIncrease,
71         c.type = FurnitureType::THRONE;
72         c.increasePerSquare = ModelBuilder::getThronePopulationIncrease();
73         c.maxIncrease = c.increasePerSquare;),
74       },
75       {
76       ImmigrantInfo(CreatureId::IMP, {MinionTrait::WORKER, MinionTrait::NO_LIMIT, MinionTrait::NO_EQUIPMENT})
77           .setSpawnLocation(NearLeader{})
78           .setKeybinding(Keybinding::CREATE_IMP)
79           .setSound(Sound(SoundId::CREATE_IMP).setPitch(2))
80           .setNoAuto()
81           .setInitialRecruitment(4)
82           .addRequirement(ExponentialCost{ CostInfo(CollectiveResourceId::GOLD, 6), 5, 4 }),
83       ImmigrantInfo(CreatureId::GOBLIN, {MinionTrait::FIGHTER, MinionTrait::NO_EQUIPMENT})
84           .setFrequency(0.7)
85           .addRequirement(0.1, AttractionInfo{1, vector<AttractionType>(
86                {FurnitureType::FORGE, FurnitureType::WORKSHOP, FurnitureType::JEWELER})}),
87       ImmigrantInfo(CreatureId::ORC, {MinionTrait::FIGHTER})
88           .setFrequency(0.7)
89           .addRequirement(0.1, AttractionInfo{1, FurnitureType::TRAINING_WOOD}),
90       ImmigrantInfo(CreatureId::ORC_SHAMAN, {MinionTrait::FIGHTER})
91           .setFrequency(0.6)
92           .addRequirement(0.1, AttractionInfo{1, {FurnitureType::BOOKCASE_WOOD, FurnitureType::LABORATORY}}),
93       ImmigrantInfo(CreatureId::OGRE, {MinionTrait::FIGHTER})
94           .setFrequency(0.3)
95           .addRequirement(0.1, AttractionInfo{1, FurnitureType::TRAINING_IRON}),
96       ImmigrantInfo(CreatureId::HARPY, {MinionTrait::FIGHTER})
97           .setFrequency(0.3)
98           .addRequirement(0.1, AttractionInfo{1, FurnitureType::TRAINING_WOOD})
99           .addRequirement(0.3, AttractionInfo{1, ItemIndex::RANGED_WEAPON}),
100       ImmigrantInfo(CreatureId::ZOMBIE, {MinionTrait::FIGHTER})
101           .setFrequency(0.5)
102           .setSpawnLocation(FurnitureType::GRAVE)
103           .addRequirement(0.0, CostInfo(CollectiveResourceId::CORPSE, 1)),
104       ImmigrantInfo(CreatureId::SKELETON, {MinionTrait::FIGHTER})
105           .setFrequency(0.5)
106           .setSpawnLocation(FurnitureType::GRAVE)
107           .addRequirement(0.1, AttractionInfo{1, FurnitureType::TRAINING_IRON})
108           .addRequirement(0.0, CostInfo(CollectiveResourceId::CORPSE, 1)),
109       ImmigrantInfo(CreatureId::VAMPIRE, {MinionTrait::FIGHTER})
110           .setFrequency(0.2)
111           .setSpawnLocation(FurnitureType::GRAVE)
112           .addRequirement(0.1, AttractionInfo{1, FurnitureType::TRAINING_IRON})
113           .addRequirement(0.0, CostInfo(CollectiveResourceId::CORPSE, 1)),
114       ImmigrantInfo(CreatureId::LOST_SOUL, {MinionTrait::FIGHTER})
115           .setFrequency(0.3)
116           .setSpawnLocation(FurnitureType::DEMON_SHRINE)
117           .addRequirement(0.3, AttractionInfo{1, FurnitureType::DEMON_SHRINE})
118           .addRequirement(0.0, FurnitureType::DEMON_SHRINE),
119       ImmigrantInfo(CreatureId::SUCCUBUS, {MinionTrait::FIGHTER, MinionTrait::NO_EQUIPMENT})
120           .setFrequency(0.3)
121           .setSpawnLocation(FurnitureType::DEMON_SHRINE)
122           .addRequirement(0.3, AttractionInfo{2, FurnitureType::DEMON_SHRINE})
123           .addRequirement(0.0, FurnitureType::DEMON_SHRINE),
124       ImmigrantInfo(CreatureId::DOPPLEGANGER, {MinionTrait::FIGHTER})
125           .setFrequency(0.3)
126           .setSpawnLocation(FurnitureType::DEMON_SHRINE)
127           .addRequirement(0.3, AttractionInfo{3, FurnitureType::DEMON_SHRINE})
128           .addRequirement(0.0, FurnitureType::DEMON_SHRINE),
129       ImmigrantInfo(CreatureId::RAVEN, {MinionTrait::FIGHTER, MinionTrait::NO_RETURNING})
130           .setFrequency(0.5)
131           .addRequirement(0.0, FurnitureType::BEAST_CAGE)
132           .addRequirement(0.0, SunlightState::DAY),
133       ImmigrantInfo(CreatureId::BAT, {MinionTrait::FIGHTER, MinionTrait::NO_RETURNING})
134           .setFrequency(0.5)
135           .addRequirement(0.0, FurnitureType::BEAST_CAGE)
136           .addRequirement(0.0, SunlightState::NIGHT),
137       ImmigrantInfo(CreatureId::WOLF, {MinionTrait::FIGHTER, MinionTrait::NO_RETURNING})
138           .setFrequency(0.15)
139           .addRequirement(0.0, FurnitureType::BEAST_CAGE)
140           .setGroupSize(Range(3, 9))
141           .setAutoTeam()
142           .addRequirement(0.0, SunlightState::NIGHT),
143       ImmigrantInfo(CreatureId::CAVE_BEAR, {MinionTrait::FIGHTER, MinionTrait::NO_RETURNING})
144           .addRequirement(0.0, FurnitureType::BEAST_CAGE)
145           .setFrequency(0.1),
146       ImmigrantInfo(CreatureId::WEREWOLF, {MinionTrait::FIGHTER, MinionTrait::NO_RETURNING})
147           .setFrequency(0.1)
148           .addRequirement(0.1, AttractionInfo{2, FurnitureType::TRAINING_IRON}),
149       ImmigrantInfo(CreatureId::DARK_ELF_WARRIOR, {MinionTrait::FIGHTER})
150           .addRequirement(0.0, RecruitmentInfo{{EnemyId::DARK_ELVES}, 3, MinionTrait::FIGHTER})
151           .addRequirement(CostInfo(CollectiveResourceId::GOLD, 20)),
152       ImmigrantInfo(CreatureId::ORC, {MinionTrait::FIGHTER})
153           .addRequirement(0.0, RecruitmentInfo{{EnemyId::ORC_VILLAGE}, 3, MinionTrait::FIGHTER})
154           .addRequirement(CostInfo(CollectiveResourceId::GOLD, 5)),
155       ImmigrantInfo(CreatureId::HARPY, {MinionTrait::FIGHTER})
156           .addRequirement(0.0, RecruitmentInfo{{EnemyId::HARPY_CAVE}, 3, MinionTrait::FIGHTER})
157           .addRequirement(CostInfo(CollectiveResourceId::GOLD, 12)),
158       ImmigrantInfo(CreatureId::OGRE, {MinionTrait::FIGHTER})
159           .addRequirement(0.0, RecruitmentInfo{{EnemyId::OGRE_CAVE, EnemyId::ORC_VILLAGE}, 3, MinionTrait::FIGHTER})
160           .addRequirement(CostInfo(CollectiveResourceId::GOLD, 12)),
161       ImmigrantInfo(random.permutation({CreatureId::SPECIAL_HMBN, CreatureId::SPECIAL_HMBW,
162               CreatureId::SPECIAL_HMGN, CreatureId::SPECIAL_HMGW}), {MinionTrait::FIGHTER})
163           .addRequirement(0.0, TechId::HUMANOID_MUT)
164           .addRequirement(0.0, Pregnancy {})
165           .addRequirement(CostInfo(CollectiveResourceId::GOLD, 100))
166           .setSpawnLocation(Pregnancy {}),
167       ImmigrantInfo(random.permutation({CreatureId::SPECIAL_BMBN, CreatureId::SPECIAL_BMBW,
168               CreatureId::SPECIAL_BMGN, CreatureId::SPECIAL_BMGW}), {MinionTrait::FIGHTER})
169           .addRequirement(0.0, TechId::BEAST_MUT)
170           .addRequirement(0.0, Pregnancy {})
171           .addRequirement(CostInfo(CollectiveResourceId::GOLD, 100))
172           .setSpawnLocation(Pregnancy {})
173   });
174 }
175 
makeExtraLevel(WModel model,EnemyInfo & enemy)176 SettlementInfo& ModelBuilder::makeExtraLevel(WModel model, EnemyInfo& enemy) {
177   const int towerHeight = random.get(7, 12);
178   const int gnomeHeight = random.get(3, 5);
179   SettlementInfo& mainSettlement = enemy.settlement;
180   SettlementInfo& extraSettlement = enemy.levelConnection->otherEnemy->settlement;
181   switch (enemy.levelConnection->type) {
182     case LevelConnection::TOWER: {
183       StairKey downLink = StairKey::getNew();
184       extraSettlement.upStairs = {downLink};
185       for (int i : Range(towerHeight - 1)) {
186         StairKey upLink = StairKey::getNew();
187         model->buildLevel(
188             LevelBuilder(meter, random, 4, 4, "Tower floor" + toString(i + 2)),
189             LevelMaker::towerLevel(random,
190                 CONSTRUCT(SettlementInfo,
191                   c.type = SettlementType::TOWER;
192                   c.inhabitants.fighters = CreatureList(
193                       random.get(1, 3),
194                       random.choose(
195                           CreatureId::WATER_ELEMENTAL, CreatureId::AIR_ELEMENTAL, CreatureId::FIRE_ELEMENTAL,
196                           CreatureId::EARTH_ELEMENTAL));
197                   c.tribe = enemy.settlement.tribe;
198                   c.collective = new CollectiveBuilder(CollectiveConfig::noImmigrants(), c.tribe);
199                   c.upStairs = {upLink};
200                   c.downStairs = {downLink};
201                   c.furniture = FurnitureFactory(TribeId::getHuman(), FurnitureType::GROUND_TORCH);
202                   c.buildingId = BuildingId::BRICK;)));
203         downLink = upLink;
204       }
205       mainSettlement.downStairs = {downLink};
206       model->buildLevel(
207          LevelBuilder(meter, random, 5, 5, "Tower top"),
208          LevelMaker::towerLevel(random, mainSettlement));
209       return extraSettlement;
210     }
211     case LevelConnection::CRYPT: {
212       StairKey key = StairKey::getNew();
213       extraSettlement.downStairs = {key};
214       mainSettlement.upStairs = {key};
215       model->buildLevel(
216          LevelBuilder(meter, random, 40, 40, "Crypt"),
217          LevelMaker::cryptLevel(random, mainSettlement));
218       return extraSettlement;
219     }
220     case LevelConnection::MAZE: {
221       StairKey key = StairKey::getNew();
222       extraSettlement.upStairs = {key};
223       mainSettlement.downStairs = {key};
224       model->buildLevel(
225          LevelBuilder(meter, random, 40, 40, "Maze"),
226          LevelMaker::mazeLevel(random, extraSettlement));
227       return mainSettlement;
228     }
229     case LevelConnection::GNOMISH_MINES: {
230       StairKey upLink = StairKey::getNew();
231       extraSettlement.downStairs = {upLink};
232       for (int i : Range(gnomeHeight - 1)) {
233         StairKey downLink = StairKey::getNew();
234         model->buildLevel(
235             LevelBuilder(meter, random, 60, 40, "Mines lvl " + toString(i + 1)),
236             LevelMaker::roomLevel(random, CreatureFactory::gnomishMines(
237                 mainSettlement.tribe, TribeId::getMonster(), 0),
238                 CreatureFactory::waterCreatures(TribeId::getMonster()),
239                 CreatureFactory::lavaCreatures(TribeId::getMonster()), {upLink}, {downLink},
240                 FurnitureFactory::roomFurniture(TribeId::getPest())));
241         upLink = downLink;
242       }
243       mainSettlement.upStairs = {upLink};
244       model->buildLevel(
245          LevelBuilder(meter, random, 60, 40, "Mine Town"),
246          LevelMaker::mineTownLevel(random, mainSettlement));
247       return extraSettlement;
248     }
249     case LevelConnection::SOKOBAN:
250       StairKey key = StairKey::getNew();
251       extraSettlement.upStairs = {key};
252       mainSettlement.downStairs = {key};
253       Table<char> sokoLevel = sokobanInput->getNext();
254       model->buildLevel(
255           LevelBuilder(meter, random, sokoLevel.getBounds().width(), sokoLevel.getBounds().height(), "Sokoban"),
256           LevelMaker::sokobanFromFile(random, mainSettlement, sokoLevel));
257       return extraSettlement;
258   }
259 }
260 
singleMapModel(const string & worldName)261 PModel ModelBuilder::singleMapModel(const string& worldName) {
262   return tryBuilding(10, [&] { return trySingleMapModel(worldName);});
263 }
264 
trySingleMapModel(const string & worldName)265 PModel ModelBuilder::trySingleMapModel(const string& worldName) {
266   vector<EnemyInfo> enemies;
267   for (int i : Range(random.get(5, 9)))
268     enemies.push_back(enemyFactory->get(EnemyId::HUMAN_COTTAGE));
269   for (int i : Range(random.get(1, 3)))
270     enemies.push_back(enemyFactory->get(EnemyId::KOBOLD_CAVE));
271   for (int i : Range(random.get(1, 3)))
272     enemies.push_back(enemyFactory->get(EnemyId::BANDITS).setVillainType(VillainType::LESSER));
273   enemies.push_back(enemyFactory->get(random.choose(EnemyId::GNOMES, EnemyId::DARK_ELVES)).setVillainType(VillainType::ALLY));
274   append(enemies, enemyFactory->getVaults());
275   if (random.roll(4))
276     enemies.push_back(enemyFactory->get(EnemyId::ANTS_CLOSED).setVillainType(VillainType::LESSER));
277   enemies.push_back(enemyFactory->get(EnemyId::KNIGHTS).setVillainType(VillainType::MAIN));
278   enemies.push_back(enemyFactory->get(random.choose(EnemyId::OGRE_CAVE, EnemyId::HARPY_CAVE))
279       .setVillainType(VillainType::ALLY));
280   for (auto& enemy : random.chooseN(3, {
281         EnemyId::ELEMENTALIST,
282         EnemyId::WARRIORS,
283         EnemyId::ELVES,
284         EnemyId::DWARVES,
285         EnemyId::VILLAGE}))
286     enemies.push_back(enemyFactory->get(enemy).setVillainType(VillainType::MAIN));
287   for (auto& enemy : random.chooseN(3, {
288         EnemyId::GREEN_DRAGON,
289         EnemyId::SHELOB,
290         EnemyId::HYDRA,
291         EnemyId::RED_DRAGON,
292         EnemyId::CYCLOPS,
293         EnemyId::DRIADS,
294         EnemyId::ENTS}))
295     enemies.push_back(enemyFactory->get(enemy).setVillainType(VillainType::LESSER));
296   for (auto& enemy : random.chooseN(1, {
297         EnemyId::KRAKEN,
298         EnemyId::WITCH,
299         EnemyId::CEMETERY}))
300     enemies.push_back(enemyFactory->get(enemy));
301   return tryModel(360, worldName, enemies, true, BiomeId::GRASSLAND, {}, true);
302 }
303 
addMapVillains(vector<EnemyInfo> & enemyInfo,BiomeId biomeId)304 void ModelBuilder::addMapVillains(vector<EnemyInfo>& enemyInfo, BiomeId biomeId) {
305   switch (biomeId) {
306     case BiomeId::GRASSLAND:
307       for (int i : Range(random.get(3, 5)))
308         enemyInfo.push_back(enemyFactory->get(EnemyId::HUMAN_COTTAGE));
309       break;
310     case BiomeId::MOUNTAIN:
311       for (int i : Range(random.get(1, 4)))
312         enemyInfo.push_back(enemyFactory->get(random.choose(EnemyId::DWARF_CAVE, EnemyId::KOBOLD_CAVE)));
313       for (int i : Range(random.get(0, 3)))
314         enemyInfo.push_back(enemyFactory->get(random.choose({EnemyId::BANDITS, EnemyId::NO_AGGRO_BANDITS}, {1, 4})));
315       break;
316     case BiomeId::FORREST:
317       for (int i : Range(random.get(3, 5)))
318         enemyInfo.push_back(enemyFactory->get(EnemyId::ELVEN_COTTAGE));
319       break;
320   }
321 }
322 
tryCampaignBaseModel(const string & siteName,bool addExternalEnemies)323 PModel ModelBuilder::tryCampaignBaseModel(const string& siteName, bool addExternalEnemies) {
324   vector<EnemyInfo> enemyInfo;
325   BiomeId biome = BiomeId::MOUNTAIN;
326   enemyInfo.push_back(random.choose(enemyFactory->get(EnemyId::DWARF_CAVE), enemyFactory->get(EnemyId::KOBOLD_CAVE)));
327   enemyInfo.push_back(enemyFactory->get(EnemyId::BANDITS));
328   enemyInfo.push_back(enemyFactory->get(EnemyId::TUTORIAL_VILLAGE));
329   if (random.chance(0.3))
330     enemyInfo.push_back(enemyFactory->get(EnemyId::KRAKEN));
331   optional<ExternalEnemies> externalEnemies;
332   if (addExternalEnemies)
333     externalEnemies = ExternalEnemies(random, enemyFactory->getExternalEnemies());
334   return tryModel(230, siteName, enemyInfo, true, biome, std::move(externalEnemies), true);
335 }
336 
tryTutorialModel(const string & siteName)337 PModel ModelBuilder::tryTutorialModel(const string& siteName) {
338   vector<EnemyInfo> enemyInfo;
339   BiomeId biome = BiomeId::MOUNTAIN;
340   enemyInfo.push_back(enemyFactory->get(EnemyId::TUTORIAL_VILLAGE).setVillainType(VillainType::LESSER));
341   return tryModel(230, siteName, enemyInfo, true, biome, {}, false);
342 }
343 
getBiome(EnemyId enemyId,RandomGen & random)344 static optional<BiomeId> getBiome(EnemyId enemyId, RandomGen& random) {
345   switch (enemyId) {
346     case EnemyId::KNIGHTS:
347     case EnemyId::WARRIORS:
348     case EnemyId::ELEMENTALIST:
349     case EnemyId::LIZARDMEN:
350     case EnemyId::HYDRA:
351     case EnemyId::VILLAGE:
352     case EnemyId::ORC_VILLAGE: return BiomeId::GRASSLAND;
353     case EnemyId::RED_DRAGON:
354     case EnemyId::GREEN_DRAGON:
355     case EnemyId::DWARVES:
356     case EnemyId::DARK_ELVES:
357     case EnemyId::OGRE_CAVE:
358     case EnemyId::HARPY_CAVE:
359     case EnemyId::SOKOBAN:
360     case EnemyId::GNOMES:
361     case EnemyId::UNICORN_HERD:
362     case EnemyId::CYCLOPS:
363     case EnemyId::SHELOB:
364     case EnemyId::DEMON_DEN:
365     case EnemyId::ANTS_OPEN: return BiomeId::MOUNTAIN;
366     case EnemyId::ELVES:
367     case EnemyId::DRIADS:
368     case EnemyId::ENTS: return BiomeId::FORREST;
369     case EnemyId::BANDITS: return random.choose<BiomeId>();
370     case EnemyId::CEMETERY: return random.choose(BiomeId::GRASSLAND, BiomeId::FORREST);
371     default: return none;
372   }
373 }
374 
tryCampaignSiteModel(const string & siteName,EnemyId enemyId,VillainType type)375 PModel ModelBuilder::tryCampaignSiteModel(const string& siteName, EnemyId enemyId, VillainType type) {
376   vector<EnemyInfo> enemyInfo { enemyFactory->get(enemyId).setVillainType(type)};
377   auto biomeId = getBiome(enemyId, random);
378   CHECK(biomeId) << "Unimplemented enemy in campaign " << EnumInfo<EnemyId>::getString(enemyId);
379   addMapVillains(enemyInfo, *biomeId);
380   return tryModel(170, siteName, enemyInfo, false, *biomeId, {}, true);
381 }
382 
tryBuilding(int numTries,function<PModel ()> buildFun)383 PModel ModelBuilder::tryBuilding(int numTries, function<PModel()> buildFun) {
384   for (int i : Range(numTries)) {
385     try {
386       if (meter)
387         meter->reset();
388       return buildFun();
389     } catch (LevelGenException) {
390       INFO << "Retrying level gen";
391     }
392   }
393   FATAL << "Couldn't generate a level";
394   return nullptr;
395 
396 }
397 
campaignBaseModel(const string & siteName,bool externalEnemies)398 PModel ModelBuilder::campaignBaseModel(const string& siteName, bool externalEnemies) {
399   return tryBuilding(20, [=] { return tryCampaignBaseModel(siteName, externalEnemies); });
400 }
401 
tutorialModel(const string & siteName)402 PModel ModelBuilder::tutorialModel(const string& siteName) {
403   return tryBuilding(20, [=] { return tryTutorialModel(siteName); });
404 }
405 
campaignSiteModel(const string & siteName,EnemyId enemyId,VillainType type)406 PModel ModelBuilder::campaignSiteModel(const string& siteName, EnemyId enemyId, VillainType type) {
407   return tryBuilding(20, [&] { return tryCampaignSiteModel(siteName, enemyId, type); });
408 }
409 
measureSiteGen(int numTries,vector<string> types)410 void ModelBuilder::measureSiteGen(int numTries, vector<string> types) {
411   if (types.empty()) {
412     types = {"single_map", "campaign_base"};
413     for (auto enemy : ENUM_ALL(EnemyId))
414       if (!!getBiome(enemy, random))
415         types.push_back(EnumInfo<EnemyId>::getString(enemy));
416   }
417   vector<function<void()>> tasks;
418   for (auto& type : types) {
419     if (type == "single_map")
420       tasks.push_back([=] { measureModelGen(type, numTries, [this] { trySingleMapModel("pok"); }); });
421     else if (type == "campaign_base")
422       tasks.push_back([=] { measureModelGen(type, numTries, [this] { tryCampaignBaseModel("pok", false); }); });
423     else if (auto id = EnumInfo<EnemyId>::fromStringSafe(type)) {
424       if (!!getBiome(*id, random))
425         tasks.push_back([=] { measureModelGen(type, numTries, [&] { tryCampaignSiteModel("", *id, VillainType::LESSER); }); });
426     } else {
427       std::cout << "Bad map type: " << type << std::endl;
428       return;
429     }
430   }
431   for (auto& t : tasks)
432     t();
433 }
434 
measureModelGen(const string & name,int numTries,function<void ()> genFun)435 void ModelBuilder::measureModelGen(const string& name, int numTries, function<void()> genFun) {
436   int numSuccess = 0;
437   int maxT = 0;
438   int minT = 1000000;
439   double sumT = 0;
440   std::cout << name;
441   for (int i : Range(numTries)) {
442 #ifndef OSX // this triggers some compiler errors OSX, I don't need it there anyway.
443     auto time = steady_clock::now();
444 #endif
445     try {
446       genFun();
447       ++numSuccess;
448       std::cout << ".";
449       std::cout.flush();
450     } catch (LevelGenException) {
451       std::cout << "x";
452       std::cout.flush();
453     }
454 #ifndef OSX
455     int millis = duration_cast<milliseconds>(steady_clock::now() - time).count();
456     sumT += millis;
457     maxT = max(maxT, millis);
458     minT = min(minT, millis);
459 #endif
460   }
461   std::cout << std::endl << numSuccess << " / " << numTries << ". MinT: " <<
462     minT << ". MaxT: " << maxT << ". AvgT: " << sumT / numSuccess << std::endl;
463 }
464 
spawnKeeper(WModel m,PCreature keeper,bool regenerateMana,vector<string> introText)465 WCollective ModelBuilder::spawnKeeper(WModel m, PCreature keeper, bool regenerateMana, vector<string> introText) {
466   WLevel level = m->getTopLevel();
467   WCreature keeperRef = keeper.get();
468   CHECK(level->landCreature(StairKey::keeperSpawn(), keeperRef)) << "Couldn't place keeper on level.";
469   m->addCreature(std::move(keeper));
470   m->collectives.push_back(CollectiveBuilder(
471         getKeeperConfig(random, options->getBoolValue(OptionId::FAST_IMMIGRATION), regenerateMana), TribeId::getKeeper())
472       .setLevel(level)
473       .addCreature(keeperRef, {MinionTrait::LEADER})
474       .build());
475   WCollective playerCollective = m->collectives.back().get();
476   playerCollective->setControl(PlayerControl::create(playerCollective, introText));
477   playerCollective->setVillainType(VillainType::PLAYER);
478   playerCollective->acquireInitialTech();
479   return playerCollective;
480 }
481 
tryModel(int width,const string & levelName,vector<EnemyInfo> enemyInfo,bool keeperSpawn,BiomeId biomeId,optional<ExternalEnemies> externalEnemies,bool hasWildlife)482 PModel ModelBuilder::tryModel(int width, const string& levelName, vector<EnemyInfo> enemyInfo, bool keeperSpawn,
483     BiomeId biomeId, optional<ExternalEnemies> externalEnemies, bool hasWildlife) {
484   auto model = Model::create();
485   vector<SettlementInfo> topLevelSettlements;
486   vector<EnemyInfo> extraEnemies;
487   for (auto& elem : enemyInfo) {
488     elem.settlement.collective = new CollectiveBuilder(elem.config, elem.settlement.tribe);
489     if (elem.levelConnection) {
490       elem.levelConnection->otherEnemy->settlement.collective =
491           new CollectiveBuilder(elem.levelConnection->otherEnemy->config,
492                                 elem.levelConnection->otherEnemy->settlement.tribe);
493       topLevelSettlements.push_back(makeExtraLevel(model.get(), elem));
494       extraEnemies.push_back(*elem.levelConnection->otherEnemy);
495     } else
496       topLevelSettlements.push_back(elem.settlement);
497   }
498   append(enemyInfo, extraEnemies);
499   optional<CreatureFactory> wildlife;
500   if (hasWildlife)
501     wildlife = CreatureFactory::forrest(TribeId::getWildlife());
502   WLevel top =  model->buildTopLevel(
503       LevelBuilder(meter, random, width, width, levelName, false),
504       LevelMaker::topLevel(random, wildlife, topLevelSettlements, width,
505         keeperSpawn, biomeId));
506   model->calculateStairNavigation();
507   for (auto& enemy : enemyInfo) {
508     if (enemy.settlement.locationName)
509       enemy.settlement.collective->setLocationName(*enemy.settlement.locationName);
510     if (auto race = enemy.settlement.race)
511       enemy.settlement.collective->setRaceName(*race);
512     if (enemy.discoverable)
513       enemy.settlement.collective->setDiscoverable();
514     PCollective collective = enemy.settlement.collective->build();
515     auto control = VillageControl::create(collective.get(), enemy.villain);
516     if (enemy.villainType)
517       collective->setVillainType(*enemy.villainType);
518     if (enemy.id)
519       collective->setEnemyId(*enemy.id);
520     collective->setControl(std::move(control));
521     model->collectives.push_back(std::move(collective));
522   }
523   if (externalEnemies)
524     model->addExternalEnemies(std::move(*externalEnemies));
525   return model;
526 }
527 
splashModel(const FilePath & splashPath)528 PModel ModelBuilder::splashModel(const FilePath& splashPath) {
529   auto m = Model::create();
530   WLevel l = m->buildTopLevel(
531       LevelBuilder(meter, Random, Level::getSplashBounds().width(), Level::getSplashBounds().height(), "Splash",
532         true, 1.0),
533       LevelMaker::splashLevel(
534           CreatureFactory::splashLeader(TribeId::getHuman()),
535           CreatureFactory::splashHeroes(TribeId::getHuman()),
536           CreatureFactory::splashMonsters(TribeId::getKeeper()),
537           CreatureFactory::singleType(TribeId::getKeeper(), CreatureId::IMP), splashPath));
538   m->topLevel = l;
539   return m;
540 }
541 
battleModel(const FilePath & levelPath,CreatureList allies,CreatureList enemies)542 PModel ModelBuilder::battleModel(const FilePath& levelPath, CreatureList allies, CreatureList enemies) {
543   auto m = Model::create();
544   ifstream stream(levelPath.getPath());
545   Table<char> level = *SokobanInput::readTable(stream);
546   WLevel l = m->buildTopLevel(
547       LevelBuilder(meter, Random, level.getBounds().width(), level.getBounds().height(), "Battle", true, 1.0),
548       LevelMaker::battleLevel(level, allies, enemies));
549   m->topLevel = l;
550   return m;
551 }
552