1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-08 22:26:51 +02:00

Fixes for handling of oversized map dwellings

- Marked large version of H3 Unicorn's Glade as not usable for random
dwelling replacement
- Shifted oversized dwellings - that have at most 2x2 as blocked tile,
but have non-blocked tile column will now be placed correctly
- This fixes incorrect random dwelling replacement of the only oversized
H3 dwelling - Portal of Glory
- Game will now detect & report invalid dwelling templates from mods
- Updated docs to clarify dwellings format
This commit is contained in:
Ivan Savenko
2025-05-12 17:50:36 +03:00
parent 29207c0b0f
commit cb70cc48d6
9 changed files with 102 additions and 3 deletions

View File

@ -377,6 +377,7 @@
},
"unicornGladeBig": {
"index": 51,
"bannedForRandomDwelling" : true,
"creatures": [["unicorn"]],
"sounds": {
"ambient": ["LOOPUNIC"]

View File

@ -9,6 +9,11 @@
[ "airElemental", "stormElemental" ],
[ "waterElemental" ]
],
/// If set to true, this dwelling will not be selected as a replacement for random dwelling on map
/// Such dwellings have no restrictions on which tiles are visitable or blocked
/// For dwelling to be usable as a replacement, it must follow some additional restrictions (see below)
"bannedForRandomDwelling" : true,
/// List of guards for this dwelling. Can have two possible values:
/// Boolean true/false - If set to "true", guards will be generated using H3 formula:
@ -20,3 +25,44 @@
]
}
```
## Replacement of random dwellings
Existing maps may contain random dwellings that will be replaced with concrete dwellings on map loading.
For dwelling to be a valid replacement for such random dwelling it must be:
- block at most 2x2 tile square
- one tile in bottom row must be visitable, and another - blocked
Visible tiles (`V` in map object template mask) don't have any restrictions and can have any layout
It is possible to make dwellings that don't fulfill this requirements, however such dwellings should only be used for custom maps or random maps. Mod that adds a new faction need to also provide a set of valid dwellings that can be used for replacement of random dwellings.
Examples of valid dwellings:
- minimal - bottom row contains one blocked and one visitable tile, second row fully passable
```json
"mask":[
"AB"
],
```
- maximal - bottom row contains one blocked and one visitable tile, both tiles on second row are blocked
```json
"mask":[
"BB"
"BA"
],
```
- extended visual - similar to maximal, but right-most column is fully passable. Note that blocked tiles still fit into 2x2 square
```json
"mask":[
"BBV"
"BAV"
],
```

View File

@ -175,6 +175,7 @@ void AObjectTypeHandler::clearTemplates()
void AObjectTypeHandler::addTemplate(const std::shared_ptr<const ObjectTemplate> & templ)
{
templates.push_back(templ);
onTemplateAdded(templ);
}
void AObjectTypeHandler::addTemplate(JsonNode config)

View File

@ -57,6 +57,8 @@ protected:
/// initialization for classes that inherit this one
virtual void initTypeData(const JsonNode & input);
virtual void onTemplateAdded(const std::shared_ptr<const ObjectTemplate>) {}
public:
AObjectTypeHandler();

View File

@ -15,7 +15,9 @@
#include "../json/JsonRandom.h"
#include "../GameLibrary.h"
#include "../mapObjects/CGDwelling.h"
#include "../mapObjects/ObjectTemplate.h"
#include "../modding/IdentifierStorage.h"
#include "../CConfigHandler.h"
VCMI_LIB_NAMESPACE_BEGIN
@ -52,6 +54,30 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input)
}
guards = input["guards"];
bannedForRandomDwelling = input["bannedForRandomDwelling"].Bool();
for (const auto & mapTemplate : getTemplates())
onTemplateAdded(mapTemplate);
}
void DwellingInstanceConstructor::onTemplateAdded(const std::shared_ptr<const ObjectTemplate> mapTemplate)
{
if (bannedForRandomDwelling || settings["mods"]["validation"].String() == "off")
return;
bool invalidForRandomDwelling = false;
int3 corner = mapTemplate->getCornerOffset();
for (const auto & tile : mapTemplate->getBlockedOffsets())
invalidForRandomDwelling |= (tile.x != -corner.x && tile.x != -corner.x-1) || (tile.y != -corner.y && tile.y != -corner.y-1);
for (const auto & tile : {mapTemplate->getVisitableOffset()})
invalidForRandomDwelling |= (tile.x != corner.x && tile.x != corner.x+1) || tile.y != corner.y;
invalidForRandomDwelling |= !mapTemplate->isBlockedAt(corner.x+0, corner.y) && !mapTemplate->isVisibleAt(corner.x+0, corner.y);
invalidForRandomDwelling |= !mapTemplate->isBlockedAt(corner.x+1, corner.y) && !mapTemplate->isVisibleAt(corner.x+1, corner.y);
if (invalidForRandomDwelling)
logMod->warn("Dwelling %s has template %s which is not valid for a random dwelling! Dwellings must not block tiles outside 2x2 range and must be visitable in bottom row. Change dwelling mask or mark dwelling as 'bannedForRandomDwelling'", getJsonKey(), mapTemplate->animationFile.getOriginalName());
}
bool DwellingInstanceConstructor::isBannedForRandomDwelling() const
@ -152,5 +178,4 @@ std::vector<const CCreature *> DwellingInstanceConstructor::getProducedCreatures
return creatures;
}
VCMI_LIB_NAMESPACE_END

View File

@ -28,6 +28,7 @@ class DwellingInstanceConstructor : public CDefaultObjectTypeHandler<CGDwelling>
protected:
bool objectFilter(const CGObjectInstance * obj, std::shared_ptr<const ObjectTemplate> tmpl) const override;
void initTypeData(const JsonNode & input) override;
void onTemplateAdded(const std::shared_ptr<const ObjectTemplate>) override;
public:
bool hasNameTextID() const override;

View File

@ -121,7 +121,7 @@ const std::set<int3> & CGObjectInstance::getBlockedOffsets() const
void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID)
{
auto position = visitablePos();
auto oldOffset = getVisitableOffset();
auto oldOffset = appearance->getCornerOffset();
auto &tile = cb->gameState().getMap().getTile(position);
//recalculate blockvis tiles - new appearance might have different blockmap than before
@ -144,11 +144,12 @@ void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID)
// instead, appearance update & pos adjustment occurs in GiveHero::applyGs
needToAdjustOffset |= this->ID == Obj::PRISON && newID == Obj::HERO;
needToAdjustOffset |= newID == Obj::MONSTER;
needToAdjustOffset |= newID == Obj::CREATURE_GENERATOR1 || newID == Obj::CREATURE_GENERATOR2 || newID == Obj::CREATURE_GENERATOR3 || newID == Obj::CREATURE_GENERATOR4;
if(needToAdjustOffset)
{
// adjust position since object visitable offset might have changed
auto newOffset = getVisitableOffset();
auto newOffset = appearance->getCornerOffset();
pos = pos - oldOffset + newOffset;
}

View File

@ -500,6 +500,23 @@ void ObjectTemplate::calculateVisitableOffset()
visitableOffset = int3(0, 0, 0);
}
int3 ObjectTemplate::getCornerOffset() const
{
assert(isVisitable());
int3 ret = visitableOffset;
for (const auto & tile : blockedOffsets)
{
ret = {
std::min(-tile.x, ret.x),
std::min(-tile.y, ret.y),
ret.z
};
}
return ret;
}
bool ObjectTemplate::canBePlacedAt(TerrainId terrainID) const
{
if (anyLandTerrain)

View File

@ -124,6 +124,11 @@ public:
// Checks if object can be placed on specific terrain
bool canBePlacedAt(TerrainId terrain) const;
/// Returns number of completely empty rows & columns in template
/// Such as shifted wandering monster def's from hota, or Portal of Glory dwelling from H3
/// object must be visitable
int3 getCornerOffset() const;
CompoundMapObjectID getCompoundID() const;
ObjectTemplate();