Okay,
Version 2 of the RubbleMaker is below. The class is no longer static and includes 2 constructors. If the default constructor is used, the RubbleMaker uses stock Outpost2 tiles. The second constructor allows passing the class TileIndices that contains the required tile index data to use the class. New instances of TileIndices can be created and modified to represent custom tile sets.
This is my first time using a C++ variadic function and my first time writing my own class constructors outside of a book exercise. I'm probably completely abusing the header file placing all the classes in it? I'm proud of the code, especially since it works as advertised, but I also recognize that it is my first time doing a couple of things, so there is a good chance it could be better. Definitely interested in header/cpp code separation discussion.
I'd like to move the Bulldozer code inside the RubbleMaker, which would require renaming RubbleMaker. It would fit nicely and allow defining the required Bulldozer TileSubSets inside the TileIndices Class where the rubble and blast indices are already located. This way Dozing terrain on custom tile sets could be done. RubbleMaker is calling Bulldozer anyways to doze the footprint of destroyed buildings.
Trying to think of a decent folder name to place the code inside the repo LevelsAndMods folder. OutpostSTL? I've got some other code to place next to it that is probably too 'heavy' for OP2Helper.
Still wouldn't mind eliminating HFL dependency. If someone wants to use the code without HFL, let me know and I'll make the modifications.
The code below is pretty long, so I understand if it doesn't really get read in its entirety. I'll try to keep the next code sample a little smaller!
RubbleMaker.h#pragma once
#include "OP2Helper\Bulldozer.h"
#include "HFL\Source\HFL.h"
#include "Outpost2DLL\Outpost2DLL.h"
#include <vector>
class TileGroup;
class TileSubSet;
class TileChangeSet;
class TileIndices;
/*Automates placing Rubble, Blast Marks (meteor strikes or destroyed vehicles) and Destroyed
Buildings. When attempting to place rubble or blast marks, LOCATIONs are checked to ensure
they are valid before placement. When placing multiple blast marks or rubble, if not enough
valid LOCATIONs are provided, the function will place as many as possible and then exit.
BLAST MARK - Will not be added to transition tiles (like mud/dirt), special objects (like
conestoga debris tiles), or special terrain features (like multi-tile craters and cliffs).
RUBBLE - Will only be placed on tiles with a unit passable CellType.
DESTROYED BUILDING - Destroyed buildings include the properly sized extended
dozed footprint, 2 tubes if appropriate, and proper number of common and rare rubble. No
error checking is done to ensure LOCATION of destroyed building is valid (IE no cliffs in
the middle).*/
class RubbleMaker
{
public:
// Default constructor used stock Outpost 2 Tile Set.
RubbleMaker()
{
TileIndices = class TileIndices();
}
// Pass modified TileIndices class for custom Tile Sets.
RubbleMaker(const TileIndices &tileIndices)
{
TileIndices = tileIndices;
};
// Attempts to place blasted tile. Returns true if placement successful.
bool CreateBlastedTile(const LOCATION &loc);
//Attempts to randomly place blasted tiles within a MAP_RECT.
void CreateBlastedTiles(const MAP_RECT &mapRect, int count);
//Attempts to place blasted tiles at each location.
template<typename LOCATION, size_t N>
void CreateBlastedTiles(const std::array<LOCATION, N> &locs)
{
for (const LOCATION &loc : locs)
{
CreateBlastedTile(loc);
}
}
// Attempts to place blasted tiles at each location.
void CreateBlastedTiles(const std::vector<LOCATION> &locs);
bool RubbleAllowed(const LOCATION &loc);
// Attempts to place common rubble. Returns true if placement successful.
bool CreateCommonRubble(const LOCATION &loc);
// Attempts to place rare rubble. Returns true if placement successful.
bool CreateRareRubble(const LOCATION &loc);
// Attempts to place rubble randomly over provided MAP_RECT.
void CreateRandomRubble(const MAP_RECT &mapRect, int commonRubbleAmount, int rareRubbleAmount);
// Places a destroyed building of given map_id type. LOCATION specifies the position
// where the North/South and East/West tubes would meet underneath the structure.
void CreateDestroyedBuilding(map_id buildingType, const LOCATION &loc);
// Places a destroyed building with custom parameters.
void CreateDestroyedBuilding(const LOCATION &loc, int buildingWidth, int buildingHeight,
bool includeTubes, int commonRubbleAmount, int rareRubbleAmount);
private:
TileIndices TileIndices;
void GetAllLocationsFromRect(std::vector<LOCATION> &locations, const MAP_RECT &mapRect);
MAP_RECT GetBuildingDozedRect(const LOCATION &loc, int buildingWidth, int buildingHeight); //Returned MAP_RECT does not include dozed perimeter tiles of building.
MAP_RECT GetBuildingFootprint(const MAP_RECT &buildingFootprint);
void RubbleMaker::CreateRandomCommonRubble(int commonRubbleAmount, std::vector<LOCATION> &possRubbleLocs);
void RubbleMaker::CreateRandomRareRubble(int rareRubbleAmount, std::vector<LOCATION> &possRubbleLocs);
void RubbleMaker::CreateDestroyedBuildingTubes(const LOCATION &buildingLoc, const MAP_RECT &buildingFootprint);
};
// Defines noteorthy tile groupings for editing map tiles. The default TileGroups represent the stock Outpost 2 TileSet.
// Values may be replaced for custom tileSets.
class TileIndices
{
public:
// Common Rubble TileGroups and TileGroups that common rubble may replace.
std::vector<TileChangeSet> CommonRubbleSets = std::vector<TileChangeSet>{
TileChangeSet(TileSubSet("Common Mud Rubble", 323, 326), TileSubSet("Mud Replaceable By Rubble", 0, 438)),
TileChangeSet(TileSubSet("Common Rock Rubble", 830, 833), TileSubSet("Rock Replaceable By Rubble", 439, 1205)),
TileChangeSet(TileSubSet("Common Sand Rubble", 1585, 1588), TileSubSet("Sand Replaceable by Rubble", 1206, 2011)) };
// Rare Rubble TileGroups and TileGroups that rare rubble may replace.
std::vector<TileChangeSet> RareRubbleSets = std::vector<TileChangeSet>{
TileChangeSet(TileSubSet("Rare Mud Rubble", 327, 330), TileSubSet("Mud Replaceable By Rubble", 0, 438)),
TileChangeSet(TileSubSet("Rare Rock Rubble", 834, 837), TileSubSet("Rock Replaceable By Rubble", 439, 1205)),
TileChangeSet(TileSubSet("Rare Sand Rubble", 1589, 1592), TileSubSet("Sand Replaceable by Rubble", 1206, 2011)),
};
// Blast Mark TileGroups and TileGroups that blast marks may replace.
std::vector<TileChangeSet> BlastSets = std::vector<TileChangeSet>
{
TileChangeSet(TileSubSet("Rough Mud Blasts", 267, 269), TileSubSet("Rough Mud Replaceable By Blasts", 0, 32)),
TileChangeSet(TileSubSet("Dozed Mud Blasts", 264, 266), TileSubSet("Dozed Mud Replaceable By Blasts", 409, 416)),
TileChangeSet(TileSubSet("Rough Rock Blasts", 792, 794), TileSubSet("Rough Rock Replaceable By Blasts", 439, 504)),
TileChangeSet(TileSubSet("Dozed Rock Blasts", 789, 791), TileSubSet("Dozed Rock Replaceable By Blasts", 921, 928)),
TileChangeSet(TileSubSet("Rough Sand Blasts", 1550, 1552), TileSubSet("Rough Sand Replaceable By Blasts", 2, TileGroup{ 1206, 1269 }, TileGroup{ 1286, 1295 })),
TileChangeSet(TileSubSet("Dozed Sand Blasts", 1547, 1549), TileSubSet("Dozed Sand Replaceable By Blasts", 1670, 1677))
};
};
// Includes 2 TileGroups. A Replaceable TileGroup that indicates which tiles may be replaced and a
// Replacement TileGroup that indicates which tiles are valid replacement tiles.
class TileChangeSet
{
public:
TileChangeSet() {}
TileChangeSet(TileSubSet replacementTiles, TileSubSet replaceableTiles)
{
ReplacementTiles = replacementTiles;
ReplaceableTiles = replaceableTiles;
}
// Returns if the replaceable TileGroup contains the tileID.
bool IsTileReplaceable(int tileID)
{
return ReplaceableTiles.ContainsTile(tileID);
};
// Returns the total number of possible replacement tiles.
int ReplacementTileRange()
{
return ReplacementTiles.Range();
};
// Returns a replacement tileID based on it's range in the Replacement TileGroup.
int ReplacementTileID(int replacementRange)
{
return ReplacementTiles.GetTile(replacementRange);
};
private:
TileSubSet ReplacementTiles;
TileSubSet ReplaceableTiles;
};
// One or more TileGroups that should be treated the same.
class TileSubSet
{
public:
TileSubSet() {}
TileSubSet(char* name, int firstIndex, int lastIndex)
{
Name = name;
TileGroups.push_back(TileGroup(firstIndex, lastIndex));
}
TileSubSet(char* name, int numbGroups, ...)
{
Name = name;
va_list tileGroups;
va_start(tileGroups, numbGroups);
for (int i = 0; i < numbGroups; i++)
{
TileGroups.push_back(va_arg(tileGroups, TileGroup));
}
va_end(tileGroups);
}
char* GetName() { return Name; };
// Returns if any of the member TileGroups contains the TileID
bool ContainsTile(int tileID)
{
for (TileGroup tileGroup : TileGroups)
{
if (tileGroup.ContainsTile(tileID))
{
return true;
}
}
return false;
}
// The total number of tiles included in all member TileGroups.
int Range()
{
int range = 0;
for (TileGroup tileGroup : TileGroups)
{
range += tileGroup.Range();
}
return range;
};
// Returns a TileID based on the combined range of all member TileGroups.
int GetTile(int range)
{
int tileID = 0;
for (TileGroup tileGroup : TileGroups)
{
if (tileGroup.Range() < range)
{
range -= tileGroup.Range();
}
else
{
return tileGroup.GetTile(range);
}
}
return -1;
};
private:
char* Name;
std::vector<TileGroup> TileGroups;
};
// Inclusive group of consectutive tiles defined by mapping ID.
class TileGroup
{
public:
TileGroup() {}
TileGroup(int firstIndex, int lastIndex)
{
FirstIndex = firstIndex;
LastIndex = lastIndex;
}
bool ContainsTile(int tileIndex)
{
return tileIndex >= FirstIndex && tileIndex <= LastIndex;
};
int Range()
{
return LastIndex - FirstIndex + 1;
};
int GetTile(int range)
{
return FirstIndex + range;
};
private:
int FirstIndex;
int LastIndex;
};
RubbleMaker.cpp#include "RubbleMaker.h"
bool RubbleMaker::RubbleAllowed(const LOCATION &loc)
{
int currentCellType = GameMap::GetCellType(loc);
return
currentCellType == CellTypes::cellDozedArea ||
currentCellType == CellTypes::cellFastPassible1 ||
currentCellType == CellTypes::cellFastPassible2 ||
currentCellType == CellTypes::cellMediumPassible1 ||
currentCellType == CellTypes::cellMediumPassible2 ||
currentCellType == CellTypes::cellSlowPassible1 ||
currentCellType == CellTypes::cellSlowPassible2;
}
bool RubbleMaker::CreateBlastedTile(const LOCATION &loc)
{
int originalTileIndex = GameMap::GetTile(loc);
for (TileChangeSet changeSet : TileIndices.BlastSets)
{
if (changeSet.IsTileReplaceable(originalTileIndex))
{
GameMap::SetTile(loc, changeSet.ReplacementTileID(TethysGame::GetRand(changeSet.ReplacementTileRange())));
return true;
}
}
return false;
}
void RubbleMaker::CreateBlastedTiles(const std::vector<LOCATION> &locs)
{
for (const LOCATION &loc : locs)
{
CreateBlastedTile(loc);
}
}
void RubbleMaker::CreateBlastedTiles(const MAP_RECT &mapRect, int count)
{
std::vector<LOCATION> possibleBlastLocs;
GetAllLocationsFromRect(possibleBlastLocs, mapRect);
int placedBlastCount = 0;
while (placedBlastCount < count)
{
if (possibleBlastLocs.size() == 0)
{
break;
}
int locIndex = TethysGame::GetRand(possibleBlastLocs.size());
if (CreateBlastedTile(possibleBlastLocs[locIndex]))
{
++placedBlastCount;
}
possibleBlastLocs.erase(possibleBlastLocs.begin() + locIndex);
}
}
bool RubbleMaker::CreateCommonRubble(const LOCATION &loc)
{
if (!RubbleAllowed(loc))
{
return false;
}
GameMap::SetCellType(loc, CellTypes::cellRubble);
int originalTileIndex = GameMap::GetTile(loc);
for (TileChangeSet changeSet : TileIndices.CommonRubbleSets)
{
if (changeSet.IsTileReplaceable(originalTileIndex))
{
GameMap::SetTile(loc, changeSet.ReplacementTileID(TethysGame::GetRand(changeSet.ReplacementTileRange())));
return true;
}
}
return false;
}
bool RubbleMaker::CreateRareRubble(const LOCATION &loc)
{
if (!RubbleAllowed(loc))
{
return false;
}
GameMap::SetCellType(loc, CellTypes::cellRubble);
int originalTileIndex = GameMap::GetTile(loc);
for (TileChangeSet changeSet : TileIndices.RareRubbleSets)
{
if (changeSet.IsTileReplaceable(originalTileIndex))
{
GameMap::SetTile(loc, changeSet.ReplacementTileID(TethysGame::GetRand(changeSet.ReplacementTileRange())));
return true;
}
}
return false;
}
void RubbleMaker::CreateRandomRubble(const MAP_RECT &mapRect, int commonRubbleAmount, int rareRubbleAmount)
{
std::vector<LOCATION> possRubbleLocs;
GetAllLocationsFromRect(possRubbleLocs, mapRect);
CreateRandomCommonRubble(commonRubbleAmount, possRubbleLocs);
CreateRandomRareRubble(rareRubbleAmount, possRubbleLocs);
}
void RubbleMaker::CreateDestroyedBuilding(const LOCATION &loc, int buildingWidth, int buildingHeight,
bool includeTubes, int commonRubbleAmount, int rareRubbleAmount)
{
MAP_RECT buildingDozedRect = GetBuildingDozedRect(loc, buildingWidth, buildingHeight);
MAP_RECT buildingFootprint = GetBuildingFootprint(buildingDozedRect);
Bulldozer::Doze(buildingDozedRect);
if (includeTubes)
{
CreateDestroyedBuildingTubes(loc, buildingDozedRect);
}
CreateRandomRubble(buildingFootprint, commonRubbleAmount, rareRubbleAmount);
}
void RubbleMaker::CreateDestroyedBuilding(map_id buildingType, const LOCATION &loc)
{
UnitInfo unitInfo = UnitInfo(buildingType);
bool includeTubes = true;
int commonRubbleCount = 1;
int rareRubbleCount = 0;
switch (buildingType)
{
case map_id::mapLightTower:
includeTubes = false;
commonRubbleCount = 0;
break;
case map_id::mapResidence:
case map_id::mapCommonStorage:
case map_id::mapRareStorage:
case map_id::mapAgridome:
case map_id::mapRecreationFacility:
case map_id::mapNursery:
case map_id::mapGuardPost:
case map_id::mapForum:
break;
case map_id::mapCommonOreMine:
case map_id::mapRareOreMine:
case map_id::mapMagmaWell:
includeTubes = false;
break;
case map_id::mapSolarPowerArray:
includeTubes = false;
commonRubbleCount = 1;
rareRubbleCount = 1;
case map_id::mapUniversity:
case map_id::mapGORF:
case map_id::mapTradeCenter:
case map_id::mapAdvancedResidence:
case map_id::mapReinforcedResidence:
commonRubbleCount = 2;
break;
case map_id::mapGeothermalPlant:
commonRubbleCount = 2;
includeTubes = false;
case map_id::mapMHDGenerator:
includeTubes = false;
commonRubbleCount = 2;
rareRubbleCount = 1;
break;
case map_id::mapTokamak:
includeTubes = false;
commonRubbleCount = 3;
break;
case map_id::mapArachnidFactory:
case map_id::mapGarage:
case map_id::mapBasicLab:
case map_id::mapStandardLab:
case map_id::mapRobotCommand:
case map_id::mapConsumerFactory:
case map_id::mapDIRT:
commonRubbleCount = 3;
break;
case map_id::mapObservatory:
case map_id::mapMeteorDefense:
commonRubbleCount = 3;
rareRubbleCount = 1;
break;
case map_id::mapAdvancedLab:
commonRubbleCount = 4;
break;
case map_id::mapVehicleFactory:
case map_id::mapCommonOreSmelter:
commonRubbleCount = 5;
break;
case map_id::mapCommandCenter:
case map_id::mapStructureFactory:
case map_id::mapRareOreSmelter:
commonRubbleCount = 6;
break;
case map_id::mapSpaceport:
commonRubbleCount = 7;
rareRubbleCount = 1;
break;
}
CreateDestroyedBuilding(loc, unitInfo.GetXSize(), unitInfo.GetYSize(), includeTubes, commonRubbleCount, rareRubbleCount);
}
// PRIVATE FUNCTIONS
void RubbleMaker::GetAllLocationsFromRect(std::vector<LOCATION> &locations, const MAP_RECT &mapRect)
{
for (int y = 0; y < mapRect.Height(); ++y)
{
for (int x = 0; x < mapRect.Width(); ++x)
{
locations.push_back(LOCATION(mapRect.x1 + x, mapRect.y1 + y));
}
}
}
MAP_RECT RubbleMaker::GetBuildingDozedRect(const LOCATION &buildingLoc, int buildingWidth, int buildingHeight)
{
LOCATION topLeftDozedTile(
buildingLoc.x - buildingWidth / 2,
buildingLoc.y - buildingHeight / 2);
LOCATION bottomRightOfFootprint(topLeftDozedTile.x + buildingWidth, topLeftDozedTile.y + buildingHeight);
return MAP_RECT(LOCATION(topLeftDozedTile.x - 1, topLeftDozedTile.y - 1), bottomRightOfFootprint);
}
MAP_RECT RubbleMaker::GetBuildingFootprint(const MAP_RECT &buildingFootprint)
{
return MAP_RECT(
buildingFootprint.x1 + 1,
buildingFootprint.y1 + 1,
buildingFootprint.x2 - 1,
buildingFootprint.y2 - 1);
}
// Removes LOCATIONS from possRubbleLocs where rubble is now added.
void RubbleMaker::CreateRandomCommonRubble(int commonRubbleAmount, std::vector<LOCATION> &possRubbleLocs)
{
for (int i = 0; i < commonRubbleAmount; ++i)
{
if (possRubbleLocs.size() == 0)
{
return;
}
int rubbleLocIndex = TethysGame::GetRand(possRubbleLocs.size());
LOCATION rubbleLoc = possRubbleLocs[rubbleLocIndex];
CreateCommonRubble(rubbleLoc);
possRubbleLocs.erase(possRubbleLocs.begin() + rubbleLocIndex);
}
}
// Removes LOCATIONS from possRubbleLocs where rubble is now added.
void RubbleMaker::CreateRandomRareRubble(int rareRubbleAmount, std::vector<LOCATION> &possRubbleLocs)
{
for (int i = 0; i < rareRubbleAmount; ++i)
{
if (possRubbleLocs.size() == 0)
{
return;
}
int rubbleLocIndex = TethysGame::GetRand(possRubbleLocs.size());
LOCATION rubbleLoc = possRubbleLocs[rubbleLocIndex];
CreateRareRubble(rubbleLoc);
possRubbleLocs.erase(possRubbleLocs.begin() + rubbleLocIndex);
}
}
void RubbleMaker::CreateDestroyedBuildingTubes(const LOCATION &buildingLoc, const MAP_RECT &buildingFootprint)
{
LOCATION southTubeLoc(buildingLoc.x, buildingFootprint.y2);
LOCATION eastTubeLoc(buildingFootprint.x2, buildingLoc.y);
if (RubbleAllowed(southTubeLoc))
{
// The tile's celltype North of the Southern tube is temporarily set to tube to ensure the tube is orientied North/South.
LOCATION fauxTubeCellLoc(southTubeLoc.x, southTubeLoc.y - 1);
CellTypes fauxTubeCellOriginalType = (CellTypes)GameMap::GetCellType(fauxTubeCellLoc);
GameMap::SetCellType(fauxTubeCellLoc, CellTypes::cellTube0);
TethysGame::CreateWallOrTube(southTubeLoc.x, southTubeLoc.y, -1, map_id::mapTube);
GameMap::SetCellType(fauxTubeCellLoc, fauxTubeCellOriginalType);
}
if (RubbleAllowed(eastTubeLoc))
{
TethysGame::CreateWallOrTube(eastTubeLoc.x, eastTubeLoc.y, -1, map_id::mapTube);
}
}