1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-05-13 22:06:58 +02:00

Merge pull request #4440 from IvanSavenko/building_fixes

Fixes for configurable buildings
This commit is contained in:
Ivan Savenko 2024-08-15 22:21:41 +03:00 committed by GitHub
commit 6426d24feb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 689 additions and 175 deletions

View File

@ -174,7 +174,20 @@
"horde1": { "id" : 18, "upgrades" : "dwellingLvl3" }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl3" },
"horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl3", "requires" : [ "horde1" ], "mode" : "auto" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl3", "requires" : [ "horde1" ], "mode" : "auto" },
"ship": { "id" : 20, "upgrades" : "shipyard" }, "ship": { "id" : 20, "upgrades" : "shipyard" },
"special2": { "type" : "stables", "requires" : [ "dwellingLvl4" ] }, "special2": {
"type" : "configurable",
"requires" : [ "dwellingLvl4" ],
"configuration" : {
"visitMode" : "bonus",
"rewards" : [
{
"message" : "@core.genrltxt.580",
"movePoints" : 400,
"bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ]
}
]
}
},
"special3": { "type" : "brotherhoodOfSword", "upgrades" : "tavern" }, "special3": { "type" : "brotherhoodOfSword", "upgrades" : "tavern" },
"grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "MORALE", "val": 2, "propagator": "PLAYER_PROPAGATOR" } ] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ { "type": "MORALE", "val": 2, "propagator": "PLAYER_PROPAGATOR" } ] },

View File

@ -174,9 +174,39 @@
"special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ] }, "special1": { "type" : "artifactMerchant", "requires" : [ "marketplace" ] },
"horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" },
"horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
"special2": { "type" : "manaVortex", "requires" : [ "mageGuild1" ] }, "special2": {
"type" : "configurable",
"requires" : [ "mageGuild1" ],
"configuration" : {
"resetParameters" : {
"period" : 7,
"visitors" : true
},
"visitMode" : "hero", // Should be 'once' to match (somewhat buggy) H3 logic
"rewards" : [
{
"limiter" : {
"noneOf" : [ { "manaPercentage" : 200 } ]
},
"message" : "@core.genrltxt.579",
"manaPercentage" : 200
}
]
}
},
"special3": { "type" : "portalOfSummoning" }, "special3": { "type" : "portalOfSummoning" },
"special4": { "type" : "experienceVisitingBonus" }, "special4": {
"type" : "configurable",
"configuration" : {
"visitMode" : "hero",
"rewards" : [
{
"message" : "@core.genrltxt.583",
"heroExperience" : 1000
}
]
}
},
"grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 },
"bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.spellpower", "val": 12 } ] }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.spellpower", "val": 12 } ] },

View File

@ -169,7 +169,19 @@
"resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "wood": 1, "ore": 1 } }, "resourceSilo": { "id" : 15, "requires" : [ "marketplace" ], "produce": { "wood": 1, "ore": 1 } },
"blacksmith": { "id" : 16 }, "blacksmith": { "id" : 16 },
"special1": { "type" : "defenceVisitingBonus", "requires" : [ "allOf", [ "townHall" ], [ "special2" ] ] }, "special1": {
"type" : "configurable",
"requires" : [ "allOf", [ "townHall" ], [ "special2" ] ],
"configuration" : {
"visitMode" : "hero",
"rewards" : [
{
"message" : "@core.genrltxt.585",
"primary" : { "defence" : 1 }
}
]
}
},
"horde1": { "id" : 18, "upgrades" : "dwellingLvl1" }, "horde1": { "id" : 18, "upgrades" : "dwellingLvl1" },
"horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
"ship": { "id" : 20, "upgrades" : "shipyard" }, "ship": { "id" : 20, "upgrades" : "shipyard" },

View File

@ -175,7 +175,19 @@
"horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
"special2": { "type" : "spellPowerGarrisonBonus", "requires" : [ "fort" ] }, "special2": { "type" : "spellPowerGarrisonBonus", "requires" : [ "fort" ] },
"special3": { "type" : "castleGate", "requires" : [ "citadel" ] }, "special3": { "type" : "castleGate", "requires" : [ "citadel" ] },
"special4": { "type" : "spellPowerVisitingBonus", "requires" : [ "mageGuild1" ] }, "special4": {
"type" : "configurable",
"requires" : [ "mageGuild1" ],
"configuration" : {
"visitMode" : "hero",
"rewards" : [
{
"message" : "@core.genrltxt.582",
"primary" : { "spellpower" : 1 }
}
]
}
},
"horde2": { "id" : 24, "upgrades" : "dwellingLvl3" }, "horde2": { "id" : 24, "upgrades" : "dwellingLvl3" },
"horde2Upgr": { "id" : 25, "upgrades" : "dwellingUpLvl3", "requires" : [ "horde2" ], "mode" : "auto" }, "horde2Upgr": { "id" : 25, "upgrades" : "dwellingUpLvl3", "requires" : [ "horde2" ], "mode" : "auto" },
"grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }}, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }},

View File

@ -171,7 +171,19 @@
"horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl1", "requires" : [ "horde1" ], "mode" : "auto" },
"special2": { "type" : "freelancersGuild", "requires" : [ "marketplace" ] }, "special2": { "type" : "freelancersGuild", "requires" : [ "marketplace" ] },
"special3": { "type" : "ballistaYard", "requires" : [ "blacksmith" ] }, "special3": { "type" : "ballistaYard", "requires" : [ "blacksmith" ] },
"special4": { "type" : "attackVisitingBonus", "requires" : [ "fort" ] }, "special4": {
"type" : "configurable",
"requires" : [ "fort" ],
"configuration" : {
"visitMode" : "hero",
"rewards" : [
{
"message" : "@core.genrltxt.584",
"primary" : { "attack" : 1 }
}
]
}
},
"grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 },
"bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 20 } ] }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 20 } ] },

View File

@ -174,7 +174,19 @@
"horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" }, "horde1Upgr": { "id" : 19, "upgrades" : "dwellingUpLvl2", "requires" : [ "horde1" ], "mode" : "auto" },
"special2": { "type" : "lookoutTower", "height" : "high", "requires" : [ "fort" ] }, "special2": { "type" : "lookoutTower", "height" : "high", "requires" : [ "fort" ] },
"special3": { "type" : "library", "requires" : [ "mageGuild1" ] }, "special3": { "type" : "library", "requires" : [ "mageGuild1" ] },
"special4": { "type" : "knowledgeVisitingBonus", "requires" : [ "mageGuild1" ] }, "special4": {
"type" : "configurable",
"requires" : [ "mageGuild1" ],
"configuration" : {
"visitMode" : "hero",
"rewards" : [
{
"message" : "@core.genrltxt.581",
"primary" : { "knowledge" : 1 }
}
]
}
},
"grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] }, "grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] },
"dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] },

View File

@ -179,7 +179,10 @@
"description" : "stacking" "description" : "stacking"
}, },
"description" : { "description" : {
"type" : "string", "anyOf" : [
{ "type" : "string" },
{ "type" : "number" }
],
"description" : "description" "description" : "description"
} }
} }

View File

@ -0,0 +1,326 @@
{
"type" : "object",
"$schema" : "http://json-schema.org/draft-04/schema",
"title" : "VCMI map object format",
"description" : "Description of map object class",
"required" : [ "rewards" ],
"additionalProperties" : false,
"definitions" : {
"value" : {
"anyOf" : [
{
"type" : "number"
},
{
"type" : "string" // variable name
},
{
"type" : "array",
"items" : {
"$ref" : "#/definitions/value"
}
},
{
"type" : "object",
"additionalProperties" : true,
"properties" : {
"amount" : { "$ref" : "#/definitions/value" },
"min" : { "$ref" : "#/definitions/value" },
"max" : { "$ref" : "#/definitions/value" }
}
}
]
},
"identifier" : {
"anyOf" : [
{
"type" : "string"
},
{
"type" : "object",
"additionalProperties" : true,
"properties" : {
"type" : {
"$ref" : "#/definitions/identifier"
},
"anyOf" : {
"type" : "array",
"items" : {
"$ref" : "#/definitions/identifier"
}
},
"noneOf" : {
"type" : "array",
"items" : {
"$ref" : "#/definitions/identifier"
}
}
}
}
]
},
"identifierList" : {
"type" : "array",
"items" : {
"$ref" : "#/definitions/identifier"
}
},
"identifierWithValueList" : {
"anyOf" : [
{
"type" : "array",
"items" : {
"allOf" : [
{ "$ref" : "#/definitions/identifier" },
{ "$ref" : "#/definitions/value" }
]
}
},
{
"type" : "object",
"additionalProperties" : {
"$ref" : "#/definitions/value"
}
},
],
},
"reward" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"appearChance" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"dice" : { "type" : "number" },
"min" : { "type" : "number", "minimum" : 0, "exclusiveMaximum" : 100 },
"max" : { "type" : "number", "exclusiveMinimum" : 0, "maximum" : 100 }
}
},
"limiter" : { "$ref" : "#/definitions/limiter" },
"message" : { "$ref" : "#/definitions/message" },
"description" : { "$ref" : "#/definitions/message" },
"heroExperience" : { "$ref" : "#/definitions/value" },
"heroLevel" : { "$ref" : "#/definitions/value" },
"movePercentage" : { "$ref" : "#/definitions/value" },
"movePoints" : { "$ref" : "#/definitions/value" },
"manaPercentage" : { "$ref" : "#/definitions/value" },
"manaPoints" : { "$ref" : "#/definitions/value" },
"manaOverflowFactor" : { "$ref" : "#/definitions/value" },
"removeObject" : { "type" : "boolean" },
"bonuses" : {
"type":"array",
"description": "List of bonuses that will be granted to visiting hero",
"items": { "$ref" : "bonus.json" }
},
"resources" : { "$ref" : "#/definitions/identifierWithValueList" },
"secondary" : { "$ref" : "#/definitions/identifierWithValueList" },
"creatures" : { "$ref" : "#/definitions/identifierWithValueList" },
"primary" : { "$ref" : "#/definitions/identifierWithValueList" },
"artifacts" : { "$ref" : "#/definitions/identifierList" },
"spells" : { "$ref" : "#/definitions/identifierList" },
"spellCast" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"spell" : { "$ref" : "#/definitions/identifier" },
"schoolLevel" : { "type" : "number" }
}
},
"revealTiles" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"hide" : { "type" : "boolean" },
"radius" : { "type" : "number" },
"surface" : { "type" : "number" },
"subterra" : { "type" : "number" },
"water" : { "type" : "number" },
"rock" : { "type" : "number" }
}
},
"changeCreatures" : {
"type" : "object",
"additionalProperties" : { "type" : "string" }
}
}
},
"limiter" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"dayOfWeek" : { "$ref" : "#/definitions/value" },
"daysPassed" : { "$ref" : "#/definitions/value" },
"heroExperience" : { "$ref" : "#/definitions/value" },
"heroLevel" : { "$ref" : "#/definitions/value" },
"manaPercentage" : { "$ref" : "#/definitions/value" },
"manaPoints" : { "$ref" : "#/definitions/value" },
"canLearnSkills" : { "type" : "boolean" },
"resources" : { "$ref" : "#/definitions/identifierWithValueList" },
"secondary" : { "$ref" : "#/definitions/identifierWithValueList" },
"creatures" : { "$ref" : "#/definitions/identifierWithValueList" },
"primary" : { "$ref" : "#/definitions/identifierWithValueList" },
"canLearnSpells" : { "$ref" : "#/definitions/identifierList" },
"heroClasses" : { "$ref" : "#/definitions/identifierList" },
"artifacts" : { "$ref" : "#/definitions/identifierList" },
"spells" : { "$ref" : "#/definitions/identifierList" },
"colors" : { "$ref" : "#/definitions/identifierList" },
"heroes" : { "$ref" : "#/definitions/identifierList" },
"anyOf" : {
"type" : "array",
"items" : { "$ref" : "#/definitions/limiter" }
},
"allOf" : {
"type" : "array",
"items" : { "$ref" : "#/definitions/limiter" }
},
"noneOf" : {
"type" : "array",
"items" : { "$ref" : "#/definitions/limiter" }
},
}
},
"message" : {
"anyOf" : [
{
"type" : "array",
"items" : {
"anyOf" : [
{ "type" : "number" },
{ "type" : "string" }
]
}
},
{
"type" : "number"
},
{
"type" : "string"
}
]
},
"variableList" : {
"type" : "object",
"additionalProperties" : {
"$ref" : "#/definitions/identifier"
}
}
},
"properties" : {
"rewards" : {
"type" : "array",
"items" : { "$ref" : "#/definitions/reward" }
},
"onVisited" : {
"type" : "array",
"items" : { "$ref" : "#/definitions/reward" }
},
"onEmpty" : {
"type" : "array",
"items" : { "$ref" : "#/definitions/reward" }
},
"variables" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"number" : {
"type" : "object",
"additionalProperties" : {
"$ref" : "#/definitions/value"
}
},
"artifact" : {
"$ref" : "#/definitions/variableList"
},
"spell" : {
"$ref" : "#/definitions/variableList"
},
"primarySkill" : {
"$ref" : "#/definitions/variableList"
},
"secondarySkill" : {
"$ref" : "#/definitions/variableList"
},
},
},
"onSelectMessage" : {
"$ref" : "#/definitions/message"
},
"description" : {
"$ref" : "#/definitions/message"
},
"notVisitedTooltip" : {
"$ref" : "#/definitions/message"
},
"visitedTooltip" : {
"$ref" : "#/definitions/message"
},
"onVisitedMessage" : {
"$ref" : "#/definitions/message"
},
"onEmptyMessage" : {
"$ref" : "#/definitions/message"
},
"canRefuse": {
"type" : "boolean"
},
"showScoutedPreview": {
"type" : "boolean"
},
"showInInfobox": {
"type" : "boolean"
},
"visitMode": {
"enum" : [ "unlimited", "once", "hero", "bonus", "limiter", "player" ],
"type" : "string"
},
"visitLimiter": {
"type" : "object"
},
"selectMode": {
"enum" : [ "selectFirst", "selectPlayer", "selectRandom", "selectAll" ],
"type" : "string"
},
"resetParameters" : {
"type" : "object",
"additionalProperties" : false,
"properties" : {
"visitors" : { "type" : "boolean" },
"rewards" : { "type" : "boolean" },
"period" : { "type" : "number" }
}
},
// Properties that might appear since this node is shared with object config
"compatibilityIdentifiers" : { },
"blockedVisitable" : { },
"removable" : { },
"aiValue" : { },
"index" : { },
"base" : { },
"rmg" : { },
"templates" : { },
"battleground" : { },
"sounds" : { }
}
}

View File

@ -31,11 +31,12 @@
"type" : "string" "type" : "string"
}, },
"description" : { "description" : {
"description" : "Localizable decsription of this building", "description" : "Localizable description of this building",
"type" : "string" "type" : "string"
}, },
"type" : { "type" : {
"type" : "string", "type" : "string",
"enum" : [ "mysticPond", "artifactMerchant", "freelancersGuild", "magicUniversity", "castleGate", "creatureTransformer", "portalOfSummoning", "ballistaYard", "lookoutTower", "library", "brotherhoodOfSword", "fountainOfFortune", "spellPowerGarrisonBonus", "attackGarrisonBonus", "defenseGarrisonBonus", "escapeTunnel", "lighthouse", "treasury", "thievesGuild", "bank", "configurable" ],
"description" : "Subtype for some special buildings" "description" : "Subtype for some special buildings"
}, },
"mode" : { "mode" : {
@ -56,6 +57,10 @@
"description" : "Optional, indicates that this building upgrades another base building", "description" : "Optional, indicates that this building upgrades another base building",
"type" : "string" "type" : "string"
}, },
"configuration" : {
"description" : "Configuration of building. Only used if 'type' is set to 'configurable'",
"$ref" : "rewardable.json"
},
"cost" : { "cost" : {
"type" : "object", "type" : "object",
"additionalProperties" : false, "additionalProperties" : false,

View File

@ -341,100 +341,7 @@ Each town requires a set of buildings (Around 30-45 buildings)
``` ```
## Building node ## Building node
See [Town Building Format](Town_Building_Format.md)
```jsonc
{
// Numeric identifier of this building
"id" : 0,
// Localizable name of this building
"name" : "",
// Localizable decsription of this building
"description" : "",
// Optional, indicates that this building upgrades another base building
"upgrades" : "baseBuilding",
// List of town buildings that must be built before this one. See below for full format
"requires" : [ "allOf", [ "mageGuild1" ], [ "tavern" ] ],
// Resources needed to build building
"cost" : { ... },
// TODO: Document me: Subtype for some special buildings
"type" : "",
// TODO: Document me: Height for lookout towers and some grails
"height" : "average"
// Resources produced each day by this building
"produce" : { ... },
//determine how this building can be built. Possible values are:
// normal - default value. Fulfill requirements, use resources, spend one day
// auto - building appears when all requirements are built
// special - building can not be built manually
// grail - building requires grail to be built
"mode" : "auto",
// Buildings which bonuses should be overridden with bonuses of the current building
"overrides" : [ "anotherBuilding ]
// Bonuses, provided by this special building on build using bonus system
"bonuses" : BONUS_FORMAT
// Bonuses, provided by this special building on hero visit and applied to the visiting hero
"onVisitBonuses" : BONUS_FORMAT
}
```
Building requirements can be described using logical expressions:
```jsonc
"requires" :
[
"allOf", // Normal H3 "build all" mode
[ "mageGuild1" ],
[
"noneOf", // available only when none of these building are built
[ "dwelling5A" ],
[ "dwelling5AUpgrade" ]
],
[
"anyOf", // any non-zero number of these buildings must be built
[ "tavern" ],
[ "blacksmith" ]
]
]
```
## Structure node ## Structure node
See [Town Building Format](Town_Building_Format.md)
```jsonc
{
// Main animation file for this building
"animation" : "",
// Horizontal position on town screen
"x" : 0,
// Vertical position on town screen
"y" : 0,
// used for blit order. Higher value places structure close to screen and drawn on top of buildings with lower values
"z" : 0,
// Path to image with golden border around building, displayed when building is selected
"border" : "",
// Path to image with area that indicate when building is selected
"area" : "",
//TODO: describe me
"builds": "",
// If upgrade, this building will replace parent animation but will not alter its behaviour
"hidden" : false
}
```

View File

@ -0,0 +1,194 @@
# Town Building Format
# Required data
Each building requires following assets:
- Town animation file (1 animation file)
- Selection highlight (1 image)
- Selection area (1 image)
- Town hall icon (1 image)
## Town Building node
```jsonc
{
// Numeric identifier of this building
"id" : 0,
// Localizable name of this building
"name" : "",
// Localizable decsription of this building
"description" : "",
// Optional, indicates that this building upgrades another base building
"upgrades" : "baseBuilding",
// List of town buildings that must be built before this one. See below for full format
"requires" : [ "allOf", [ "mageGuild1" ], [ "tavern" ] ],
// Resources needed to build building
"cost" : {
"wood" : 20,
"ore" : 20,
"gold" : 10000
},
// Allows to define additional functionality of this building, usually using logic of one of original H3 town building
// Generally only needs to be specified for "special" buildings
// See 'List of unique town buildings' section below for detailed description of this field
"type" : "",
// If set, building will have Lookout Tower logic - extend sight radius of a town.
// Possible values:
// low - increases town sight radius by 5 tiles
// average - sight radius extended by 15 tiles
// high - sight radius extended by 20 tiles
// skyship - entire map will be revealed
// If not set, building will not affect sight radius of a town
"height" : "average"
// Resources produced each day by this building
"produce" : {
"sulfur" : 1,
"gold" : 2000
},
//determine how this building can be built. Possible values are:
// normal - default value. Fulfill requirements, use resources, spend one day
// auto - building appears when all requirements are built
// special - building can not be built manually
// grail - building requires grail to be built
"mode" : "auto",
// Buildings which bonuses should be overridden with bonuses of the current building
"overrides" : [ "anotherBuilding ]
// Bonuses, provided by this special building on build using bonus system
"bonuses" : BONUS_FORMAT
// Bonuses, provided by this special building on hero visit and applied to the visiting hero
"onVisitBonuses" : BONUS_FORMAT
}
```
Building requirements can be described using logical expressions:
```jsonc
"requires" :
[
"allOf", // Normal H3 "build all" mode
[ "mageGuild1" ],
[
"noneOf", // available only when none of these building are built
[ "dwelling5A" ],
[ "dwelling5AUpgrade" ]
],
[
"anyOf", // any non-zero number of these buildings must be built
[ "tavern" ],
[ "blacksmith" ]
]
]
```
### List of unique town buildings
Following Heroes III buildings can be used as unique buildings for a town. Their functionality should be identical to a corresponding H3 building:
- `mysticPond`
- `artifactMerchant`
- `freelancersGuild`
- `magicUniversity`
- `castleGate`
- `creatureTransformer`
- `portalOfSummoning`
- `ballistaYard`
- `stables`
- `manaVortex`
- `lookoutTower`
- `library`
- `brotherhoodOfSword`
- `fountainOfFortune`
- `escapeTunnel`
- `lighthouse`
- `treasury`
- `spellPowerGarrisonBonus`
- `attackGarrisonBonus`
- `defenseGarrisonBonus`
Following HotA buildings can be used as unique building for a town. Functionality should match corresponding HotA building:
- `thievesGuild`
- `bank`
In addition to above, it is possible to use same format as [Rewardable](../Map_Objects/Rewardable.md) map objects for town buildings. In order to do that, town building type must be set to `configurable` and configuration of a rewardable object must be placed into `configuration` node
Example 1 - Order of Fire from Inferno:
```jsonc
"special4": { //
"type" : "configurable",
"requires" : [ "mageGuild1" ],
"configuration" : {
"visitMode" : "hero",
"rewards" : [
{
"message" : "@core.genrltxt.582", // NOTE: this forces vcmi to load string from H3 text file. In order to define own string simply write your own message without '@' symbol
"primary" : { "spellpower" : 1 }
}
]
}
}
```
Example 2 - Mana Vortex from Dungeon
```jsonc
"special2": {
"type" : "configurable",
"requires" : [ "mageGuild1" ],
"configuration" : {
"resetParameters" : {
"period" : 7,
"visitors" : true
},
"visitMode" : "once",
"rewards" : [
{
"limiter" : {
"noneOf" : [ { "manaPercentage" : 200 } ]
},
"message" : "@core.genrltxt.579",
"manaPercentage" : 200
}
]
}
}
```
### Town Structure node
```jsonc
{
// Main animation file for this building
"animation" : "",
// Horizontal position on town screen
"x" : 0,
// Vertical position on town screen
"y" : 0,
// used for blit order. Higher value places structure close to screen and drawn on top of buildings with lower values
"z" : 0,
// Path to image with golden border around building, displayed when building is selected
"border" : "",
// Path to image with area that indicate when building is selected
"area" : "",
//TODO: describe me
"builds": "",
// If upgrade, this building will replace parent animation but will not alter its behaviour
"hidden" : false
}
```

View File

@ -84,11 +84,9 @@ bool IBonusBearer::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const
bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const
{ {
boost::format fmt("source_%did_%s"); return hasBonus(Selector::source(source,sourceID));
fmt % static_cast<int>(source) % sourceID.toString();
return hasBonus(Selector::source(source,sourceID), fmt.str());
} }
std::shared_ptr<const Bonus> IBonusBearer::getBonus(const CSelector &selector) const std::shared_ptr<const Bonus> IBonusBearer::getBonus(const CSelector &selector) const
{ {
auto bonuses = getAllBonuses(selector, Selector::all); auto bonuses = getAllBonuses(selector, Selector::all);

View File

@ -203,7 +203,8 @@ namespace MappedKeys
{ "lighthouse", BuildingSubID::LIGHTHOUSE }, { "lighthouse", BuildingSubID::LIGHTHOUSE },
{ "treasury", BuildingSubID::TREASURY }, { "treasury", BuildingSubID::TREASURY },
{ "thievesGuild", BuildingSubID::THIEVES_GUILD }, { "thievesGuild", BuildingSubID::THIEVES_GUILD },
{ "bank", BuildingSubID::BANK } { "bank", BuildingSubID::BANK },
{ "configurable", BuildingSubID::CUSTOM_VISITING_REWARD}
}; };
static const std::map<std::string, EMarketMode> MARKET_NAMES_TO_TYPES = static const std::map<std::string, EMarketMode> MARKET_NAMES_TO_TYPES =

View File

@ -80,7 +80,7 @@ BuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const
std::string CTown::getGreeting(BuildingSubID::EBuildingSubID subID) const std::string CTown::getGreeting(BuildingSubID::EBuildingSubID subID) const
{ {
return CTownHandler::getMappedValue<const std::string, BuildingSubID::EBuildingSubID>(subID, std::string(), specialMessages, false); return vstd::find_or(specialMessages, subID, std::string());
} }
void CTown::setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const void CTown::setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const

View File

@ -324,7 +324,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
assert(!source.getModScope().empty()); assert(!source.getModScope().empty());
auto * ret = new CBuilding(); auto * ret = new CBuilding();
ret->bid = getMappedValue<BuildingID, std::string>(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); ret->bid = vstd::find_or(MappedKeys::BUILDING_NAMES_TO_TYPES, stringID, BuildingID::NONE);
ret->subId = BuildingSubID::NONE; ret->subId = BuildingSubID::NONE;
if(ret->bid == BuildingID::NONE && !source["id"].isNull()) if(ret->bid == BuildingID::NONE && !source["id"].isNull())
@ -339,9 +339,9 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
ret->mode = ret->bid == BuildingID::GRAIL ret->mode = ret->bid == BuildingID::GRAIL
? CBuilding::BUILD_GRAIL ? CBuilding::BUILD_GRAIL
: getMappedValue<CBuilding::EBuildMode>(source["mode"], CBuilding::BUILD_NORMAL, CBuilding::MODES); : vstd::find_or(CBuilding::MODES, source["mode"].String(), CBuilding::BUILD_NORMAL);
ret->height = getMappedValue<CBuilding::ETowerHeight>(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); ret->height = vstd::find_or(CBuilding::TOWER_TYPES, source["height"].String(), CBuilding::HEIGHT_NO_TOWER);
ret->identifier = stringID; ret->identifier = stringID;
ret->modScope = source.getModScope(); ret->modScope = source.getModScope();
@ -361,7 +361,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
if(ret->buildingBonuses.empty()) if(ret->buildingBonuses.empty())
{ {
ret->subId = getMappedValue<BuildingSubID::EBuildingSubID>(source["type"], BuildingSubID::NONE, MappedKeys::SPECIAL_BUILDINGS); ret->subId = vstd::find_or(MappedKeys::SPECIAL_BUILDINGS, source["type"].String(), BuildingSubID::NONE);
addBonusesForVanilaBuilding(ret); addBonusesForVanilaBuilding(ret);
} }
@ -376,11 +376,8 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
bonus->sid = BonusSourceID(ret->getUniqueTypeID()); bonus->sid = BonusSourceID(ret->getUniqueTypeID());
} }
if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) if(ret->subId == BuildingSubID::CUSTOM_VISITING_REWARD)
{ ret->rewardableObjectInfo.init(source["configuration"], ret->getBaseTextID());
ret->subId = BuildingSubID::CUSTOM_VISITING_REWARD;
ret->rewardableObjectInfo.init(source, ret->getBaseTextID());
}
} }
//MODS COMPATIBILITY FOR 0.96 //MODS COMPATIBILITY FOR 0.96
if(!ret->produce.nonZero()) if(!ret->produce.nonZero())

View File

@ -68,11 +68,6 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase<FactionID, Faction, CFactio
void loadRandomFaction(); void loadRandomFaction();
public: public:
template<typename R, typename K>
static R getMappedValue(const K key, const R defval, const std::map<K, R> & map, bool required = true);
template<typename R>
static R getMappedValue(const JsonNode & node, const R defval, const std::map<std::string, R> & map, bool required = true);
CTown * randomTown; CTown * randomTown;
CFaction * randomFaction; CFaction * randomFaction;
@ -98,25 +93,4 @@ protected:
std::shared_ptr<CFaction> loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; std::shared_ptr<CFaction> loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override;
}; };
template<typename R, typename K>
R CTownHandler::getMappedValue(const K key, const R defval, const std::map<K, R> & map, bool required)
{
auto it = map.find(key);
if(it != map.end())
return it->second;
if(required)
logMod->warn("Warning: Property: '%s' is unknown. Correct the typo or update VCMI.", key);
return defval;
}
template<typename R>
R CTownHandler::getMappedValue(const JsonNode & node, const R defval, const std::map<std::string, R> & map, bool required)
{
if(!node.isNull() && node.getType() == JsonNode::JsonType::DATA_STRING)
return getMappedValue<R, std::string>(node.String(), defval, map, required);
return defval;
}
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END

View File

@ -10,6 +10,7 @@
#include "StdInc.h" #include "StdInc.h"
#include "CRewardableConstructor.h" #include "CRewardableConstructor.h"
#include "../json/JsonUtils.h"
#include "../mapObjects/CRewardableObject.h" #include "../mapObjects/CRewardableObject.h"
#include "../texts/CGeneralTextHandler.h" #include "../texts/CGeneralTextHandler.h"
#include "../IGameCallback.h" #include "../IGameCallback.h"
@ -24,6 +25,8 @@ void CRewardableConstructor::initTypeData(const JsonNode & config)
if (!config["name"].isNull()) if (!config["name"].isNull())
VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"].String()); VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"].String());
JsonUtils::validate(config, "vcmi:rewardable", getJsonKey());
} }
bool CRewardableConstructor::hasNameTextID() const bool CRewardableConstructor::hasNameTextID() const

View File

@ -476,7 +476,7 @@ void CTownRewardableBuilding::onHeroVisit(const CGHeroInstance *h) const
cb->showBlockingDialog(&sd); cb->showBlockingDialog(&sd);
}; };
if(!town->hasBuilt(bID) || cb->isVisitCoveredByAnotherQuery(town, h)) if(!town->hasBuilt(bID))
return; return;
if(!wasVisitedBefore(h)) if(!wasVisitedBefore(h))

View File

@ -322,8 +322,9 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const
cb->heroVisitCastle(this, h); cb->heroVisitCastle(this, h);
} }
} }
else if(h->visitablePos() == visitablePos()) else
{ {
assert(h->visitablePos() == this->visitablePos());
bool commander_recover = h->commander && !h->commander->alive; bool commander_recover = h->commander && !h->commander->alive;
if (commander_recover) // rise commander from dead if (commander_recover) // rise commander from dead
{ {
@ -344,10 +345,6 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const
cb->showInfoDialog(&iw); cb->showInfoDialog(&iw);
} }
} }
else
{
logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), getNameTranslated());
}
} }
void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const
@ -556,14 +553,6 @@ void CGTownInstance::newTurn(vstd::RNG & rand) const
for(const auto * manaVortex : getBonusingBuildings(BuildingSubID::MANA_VORTEX)) for(const auto * manaVortex : getBonusingBuildings(BuildingSubID::MANA_VORTEX))
cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex
//get Mana Vortex or Stables bonuses
//same code is in the CGameHandler::buildStructure method
if (garrisonHero != nullptr) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
cb->visitCastleObjects(this, garrisonHero);
if (visitingHero != nullptr)
cb->visitCastleObjects(this, visitingHero);
if (tempOwner == PlayerColor::NEUTRAL) //garrison growth for neutral towns if (tempOwner == PlayerColor::NEUTRAL) //garrison growth for neutral towns
{ {
std::vector<SlotID> nativeCrits; //slots std::vector<SlotID> nativeCrits; //slots

View File

@ -2431,12 +2431,27 @@ void EntitiesChanged::applyGs(CGameState * gs)
void SetRewardableConfiguration::applyGs(CGameState * gs) void SetRewardableConfiguration::applyGs(CGameState * gs)
{ {
auto * objectPtr = gs->getObjInstance(objectID); auto * objectPtr = gs->getObjInstance(objectID);
if (!buildingID.hasValue())
{
auto * rewardablePtr = dynamic_cast<CRewardableObject *>(objectPtr); auto * rewardablePtr = dynamic_cast<CRewardableObject *>(objectPtr);
assert(rewardablePtr); assert(rewardablePtr);
rewardablePtr->configuration = configuration; rewardablePtr->configuration = configuration;
} }
else
{
auto * townPtr = dynamic_cast<CGTownInstance*>(objectPtr);
CGTownBuilding * buildingPtr = nullptr;
for (CGTownBuilding * building : townPtr->bonusingBuildings)
if (building->getBuildingType() == buildingID)
buildingPtr = building;
auto * rewardablePtr = dynamic_cast<CTownRewardableBuilding *>(buildingPtr);
assert(rewardablePtr);
rewardablePtr->configuration = configuration;
}
}
void SetBankConfiguration::applyGs(CGameState * gs) void SetBankConfiguration::applyGs(CGameState * gs)
{ {

View File

@ -631,10 +631,21 @@ void CGameHandler::onPlayerTurnStarted(PlayerColor which)
events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which); events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which);
turnTimerHandler->onPlayerGetTurn(which); turnTimerHandler->onPlayerGetTurn(which);
handleTimeEvents(which); const auto * playerState = gs->getPlayerState(which);
for (auto t : getPlayerState(which)->towns) handleTimeEvents(which);
for (auto t : playerState->towns)
handleTownEvents(t); handleTownEvents(t);
for (auto t : playerState->towns)
{
//garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
if (t->garrisonHero != nullptr)
objectVisited(t, t->garrisonHero);
if (t->visitingHero != nullptr)
objectVisited(t, t->visitingHero);
}
} }
void CGameHandler::onPlayerTurnEnded(PlayerColor which) void CGameHandler::onPlayerTurnEnded(PlayerColor which)
@ -1517,12 +1528,15 @@ void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector<CStac
} }
void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)
{
if (obj->visitingHero != hero && obj->garrisonHero != hero)
{ {
HeroVisitCastle vc; HeroVisitCastle vc;
vc.hid = hero->id; vc.hid = hero->id;
vc.tid = obj->id; vc.tid = obj->id;
vc.flags |= 1; vc.flags |= 1;
sendAndApply(&vc); sendAndApply(&vc);
}
visitCastleObjects(obj, hero); visitCastleObjects(obj, hero);
giveSpells (obj, hero); giveSpells (obj, hero);
@ -2487,9 +2501,9 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID,
changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED); changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED);
if(t->garrisonHero) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order if(t->garrisonHero) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
visitCastleObjects(t, t->garrisonHero); objectVisited(t, t->garrisonHero);
if(t->visitingHero) if(t->visitingHero)
visitCastleObjects(t, t->visitingHero); objectVisited(t, t->visitingHero);
checkVictoryLossConditionsForPlayer(t->tempOwner); checkVictoryLossConditionsForPlayer(t->tempOwner);
return true; return true;

View File

@ -242,10 +242,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST); gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST);
if(town) if(town)
{ gameHandler->objectVisited(town, recruitedHero);
gameHandler->visitCastleObjects(town, recruitedHero);
gameHandler->giveSpells(town, recruitedHero);
}
// If new hero has scouting he might reveal more terrain than we saw before // If new hero has scouting he might reveal more terrain than we saw before
gameHandler->changeFogOfWar(recruitedHero->getSightCenter(), recruitedHero->getSightRadius(), player, ETileVisibility::REVEALED); gameHandler->changeFogOfWar(recruitedHero->getSightCenter(), recruitedHero->getSightRadius(), player, ETileVisibility::REVEALED);