1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-01-12 02:28:11 +02:00

Merge pull request #4680 from Laserlicht/spell

Basic spell research
This commit is contained in:
Ivan Savenko 2024-10-07 15:22:04 +03:00 committed by GitHub
commit cf685e4bfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 375 additions and 85 deletions

View File

@ -249,6 +249,12 @@ int CBattleCallback::sendRequest(const CPackForServer * request)
return requestID;
}
void CCallback::spellResearch( const CGTownInstance *town, SpellID spellAtSlot, bool accepted )
{
SpellResearch pack(town->id, spellAtSlot, accepted);
sendRequest(&pack);
}
void CCallback::swapGarrisonHero( const CGTownInstance *town )
{
if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player ))

View File

@ -78,6 +78,7 @@ public:
virtual bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
virtual void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted)=0;
virtual void swapGarrisonHero(const CGTownInstance *town)=0;
virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
@ -187,6 +188,7 @@ public:
bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
void endTurn() override;
void spellResearch(const CGTownInstance *town, SpellID spellAtSlot, bool accepted) override;
void swapGarrisonHero(const CGTownInstance *town) override;
void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -61,6 +61,13 @@
"vcmi.spellBook.search" : "search...",
"vcmi.spellResearch.canNotAfford" : "You can't afford to replace {%SPELL1} with {%SPELL2}. But you can still discard this spell and continue spell research.",
"vcmi.spellResearch.comeAgain" : "Research has already been done today. Come back tomorrow.",
"vcmi.spellResearch.pay" : "Would you like to replace {%SPELL1} with {%SPELL2}? Or discard this spell and continue spell research?",
"vcmi.spellResearch.research" : "Research this Spell",
"vcmi.spellResearch.skip" : "Skip this Spell",
"vcmi.spellResearch.abort" : "Abort",
"vcmi.mainMenu.serverConnecting" : "Connecting...",
"vcmi.mainMenu.serverAddressEnter" : "Enter address:",
"vcmi.mainMenu.serverConnectionFailed" : "Failed to connect",

View File

@ -60,6 +60,13 @@
"vcmi.spellBook.search" : "suchen...",
"vcmi.spellResearch.canNotAfford" : "Ihr könnt es Euch nicht leisten, {%SPELL1} durch {%SPELL2} zu ersetzen. Aber Ihr könnt diesen Zauberspruch trotzdem verwerfen und die Zauberspruchforschung fortsetzen.",
"vcmi.spellResearch.comeAgain" : "Die Forschung wurde heute bereits abgeschlossen. Kommt morgen wieder.",
"vcmi.spellResearch.pay" : "Möchtet Ihr {%SPELL1} durch {%SPELL2} ersetzen? Oder diesen Zauberspruch verwerfen und die Zauberspruchforschung fortsetzen?",
"vcmi.spellResearch.research" : "Erforsche diesen Zauberspruch",
"vcmi.spellResearch.skip" : "Überspringe diesen Zauberspruch",
"vcmi.spellResearch.abort" : "Abbruch",
"vcmi.mainMenu.serverConnecting" : "Verbinde...",
"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
"vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen",

View File

@ -159,6 +159,7 @@ public:
friend class CBattleCallback; //handling players actions
void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> & spells) override {};
void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted) override {};
bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;};
void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {};
void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {};

View File

@ -37,6 +37,7 @@ public:
void visitHeroVisitCastle(HeroVisitCastle & pack) override;
void visitSetMana(SetMana & pack) override;
void visitSetMovePoints(SetMovePoints & pack) override;
void visitSetResearchedSpells(SetResearchedSpells & pack) override;
void visitFoWChange(FoWChange & pack) override;
void visitChangeStackCount(ChangeStackCount & pack) override;
void visitSetStackType(SetStackType & pack) override;

View File

@ -14,6 +14,7 @@
#include "CPlayerInterface.h"
#include "CGameInfo.h"
#include "windows/GUIClasses.h"
#include "windows/CCastleInterface.h"
#include "mapView/mapHandler.h"
#include "adventureMap/AdventureMapInterface.h"
#include "adventureMap/CInGameConsole.h"
@ -172,6 +173,12 @@ void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
}
void ApplyClientNetPackVisitor::visitSetResearchedSpells(SetResearchedSpells & pack)
{
for(const auto & win : GH.windows().findWindows<CMageGuildScreen>())
win->updateSpells(pack.tid);
}
void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack)
{
for(auto &i : cl.playerint)

View File

@ -70,6 +70,7 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona
customSubtitle = ValText;
size = imageSize;
font = fnt;
newLine = false;
assert(size < sizeInvalid);
@ -471,7 +472,8 @@ void CComponentBox::placeComponents(bool selectable)
//start next row
if ((pos.w != 0 && rows.back().width + comp->pos.w + distance > pos.w) // row is full
|| rows.back().comps >= componentsInRow)
|| rows.back().comps >= componentsInRow
|| (prevComp && prevComp->newLine))
{
prevComp = nullptr;
rows.push_back (RowData (0,0,0));

View File

@ -52,6 +52,7 @@ public:
std::string customSubtitle;
ESize size; //component size.
EFonts font; //Font size of label
bool newLine; //Line break after component
std::string getDescription() const;
std::string getSubtitle() const;

View File

@ -1966,7 +1966,7 @@ void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition)
}
CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & imagename)
: CWindowObject(BORDERED, imagename)
: CWindowObject(BORDERED, imagename), townId(owner->town->id)
{
OBJECT_CONSTRUCTION;
@ -1982,6 +1982,15 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & i
exit = std::make_shared<CButton>(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN);
updateSpells(townId);
}
void CMageGuildScreen::updateSpells(ObjectInstanceID tID)
{
if(tID != townId)
return;
OBJECT_CONSTRUCTION;
static const std::vector<std::vector<Point> > positions =
{
{Point(222,445), Point(312,445), Point(402,445), Point(520,445), Point(610,445), Point(700,445)},
@ -1991,21 +2000,28 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & i
{Point(491,325), Point(591,325)}
};
for(size_t i=0; i<owner->town->town->mageLevel; i++)
spells.clear();
emptyScrolls.clear();
const CGTownInstance * town = LOCPLINT->cb->getTown(townId);
for(size_t i=0; i<town->town->mageLevel; i++)
{
size_t spellCount = owner->town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm?
size_t spellCount = town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm?
for(size_t j=0; j<spellCount; j++)
{
if(i<owner->town->mageGuildLevel() && owner->town->spells[i].size()>j)
spells.push_back(std::make_shared<Scroll>(positions[i][j], owner->town->spells[i][j].toSpell()));
if(i<town->mageGuildLevel() && town->spells[i].size()>j)
spells.push_back(std::make_shared<Scroll>(positions[i][j], town->spells[i][j].toSpell(), townId));
else
emptyScrolls.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y));
}
}
redraw();
}
CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell)
: spell(Spell)
CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell, ObjectInstanceID townId)
: spell(Spell), townId(townId)
{
OBJECT_CONSTRUCTION;
@ -2017,7 +2033,61 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell)
void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
{
LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(ComponentType::SPELL, spell->id));
const CGTownInstance * town = LOCPLINT->cb->getTown(townId);
if(LOCPLINT->cb->getSettings().getBoolean(EGameSettings::TOWNS_SPELL_RESEARCH) && town->spellResearchAllowed)
{
int level = -1;
for(int i = 0; i < town->spells.size(); i++)
if(vstd::find_pos(town->spells[i], spell->id) != -1)
level = i;
if(town->spellResearchCounterDay >= LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY).Vector()[level].Float())
{
LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.spellResearch.comeAgain"));
return;
}
auto costBase = TResources(LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]);
auto costExponent = LOCPLINT->cb->getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH).Vector()[level].Float();
auto cost = costBase * std::pow(town->spellResearchAcceptedCounter + 1, costExponent);
std::vector<std::shared_ptr<CComponent>> resComps;
auto newSpell = town->spells[level].at(town->spellsAtLevel(level, false));
resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, spell->id));
resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, newSpell));
resComps.back()->newLine = true;
for(TResources::nziterator i(cost); i.valid(); i++)
{
resComps.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, i->resType, i->resVal, CComponent::ESize::medium));
}
auto showSpellResearchDialog = [this, resComps, town, cost, newSpell](){
std::vector<std::pair<AnimationPath, CFunctionList<void()>>> pom;
for(int i = 0; i < 3; i++)
pom.emplace_back(AnimationPath::builtin("settingsWindow/button80"), nullptr);
auto text = CGI->generaltexth->translate(LOCPLINT->cb->getResourceAmount().canAfford(cost) ? "vcmi.spellResearch.pay" : "vcmi.spellResearch.canNotAfford");
boost::replace_first(text, "%SPELL1", spell->id.toSpell()->getNameTranslated());
boost::replace_first(text, "%SPELL2", newSpell.toSpell()->getNameTranslated());
auto temp = std::make_shared<CInfoWindow>(text, LOCPLINT->playerID, resComps, pom);
temp->buttons[0]->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("spellResearch/accept")));
temp->buttons[0]->addCallback([this, town](){ LOCPLINT->cb->spellResearch(town, spell->id, true); });
temp->buttons[0]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.research")); });
temp->buttons[0]->setEnabled(LOCPLINT->cb->getResourceAmount().canAfford(cost));
temp->buttons[1]->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("spellResearch/reroll")));
temp->buttons[1]->addCallback([this, town](){ LOCPLINT->cb->spellResearch(town, spell->id, false); });
temp->buttons[1]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.skip")); });
temp->buttons[2]->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("spellResearch/close")));
temp->buttons[2]->addPopupCallback([](){ CRClickPopup::createAndPush(CGI->generaltexth->translate("vcmi.spellResearch.abort")); });
GH.windows().pushWindow(temp);
};
showSpellResearchDialog();
}
else
LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(ComponentType::SPELL, spell->id));
}
void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition)

View File

@ -379,9 +379,10 @@ class CMageGuildScreen : public CStatusbarWindow
{
const CSpell * spell;
std::shared_ptr<CAnimImage> image;
ObjectInstanceID townId;
public:
Scroll(Point position, const CSpell *Spell);
Scroll(Point position, const CSpell *Spell, ObjectInstanceID townId);
void clickPressed(const Point & cursorPosition) override;
void showPopupWindow(const Point & cursorPosition) override;
void hover(bool on) override;
@ -393,8 +394,11 @@ class CMageGuildScreen : public CStatusbarWindow
std::shared_ptr<CMinorResDataBar> resdatabar;
ObjectInstanceID townId;
public:
CMageGuildScreen(CCastleInterface * owner, const ImagePath & image);
void updateSpells(ObjectInstanceID tID);
};
/// The blacksmith window where you can buy available in town war machine

View File

@ -302,7 +302,7 @@
"backpackSize" : -1,
// if heroes are invitable in tavern
"tavernInvite" : false,
// minimai primary skills for heroes
// minimal primary skills for heroes
"minimalPrimarySkills": [ 0, 0, 1, 1]
},
@ -311,7 +311,21 @@
// How many new building can be built in a town per day
"buildingsPerTurnCap" : 1,
// Chances for a town with default buildings to receive corresponding dwelling level built in start
"startingDwellingChances": [100, 50]
"startingDwellingChances": [100, 50],
// Enable spell research in mage guild
"spellResearch": false,
// Cost for an spell research (array index is spell tier)
"spellResearchCost": [
{ "gold": 1000, "wood" : 2, "mercury": 2, "ore": 2, "sulfur": 2, "crystal": 2, "gems": 2 },
{ "gold": 1000, "wood" : 4, "mercury": 4, "ore": 4, "sulfur": 4, "crystal": 4, "gems": 4 },
{ "gold": 1000, "wood" : 6, "mercury": 6, "ore": 6, "sulfur": 6, "crystal": 6, "gems": 6 },
{ "gold": 1000, "wood" : 8, "mercury": 8, "ore": 8, "sulfur": 8, "crystal": 8, "gems": 8 },
{ "gold": 1000, "wood" : 10, "mercury": 10, "ore": 10, "sulfur": 10, "crystal": 10, "gems": 10 }
],
// How much researchs/skips per day are possible? (array index is spell tier)
"spellResearchPerDay": [ 2, 2, 2, 2, 1 ],
// Exponent for increasing cost for each research (factor 1 disables this; array index is spell tier)
"spellResearchCostExponentPerResearch": [ 1.25, 1.25, 1.25, 1.25, 1.25 ]
},
"combat":

View File

@ -51,8 +51,12 @@
"type" : "object",
"additionalProperties" : false,
"properties" : {
"buildingsPerTurnCap" : { "type" : "number" },
"startingDwellingChances" : { "type" : "array" }
"buildingsPerTurnCap" : { "type" : "number" },
"startingDwellingChances" : { "type" : "array" },
"spellResearch" : { "type" : "boolean" },
"spellResearchCost" : { "type" : "array" },
"spellResearchPerDay" : { "type" : "array" },
"spellResearchCostExponentPerResearch" : { "type" : "array" }
}
},
"combat": {

View File

@ -37,71 +37,75 @@ GameSettings::GameSettings() = default;
GameSettings::~GameSettings() = default;
const std::vector<GameSettings::SettingOption> GameSettings::settingProperties = {
{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" },
{EGameSettings::BONUSES_GLOBAL, "bonuses", "global" },
{EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" },
{EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX, "combat", "areaShotCanTargetEmptyHex" },
{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" },
{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, "combat", "attackPointDamageFactorCap" },
{EGameSettings::COMBAT_BAD_LUCK_DICE, "combat", "badLuckDice" },
{EGameSettings::COMBAT_BAD_MORALE_DICE, "combat", "badMoraleDice" },
{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, "combat", "defensePointDamageFactor" },
{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap" },
{EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" },
{EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" },
{EGameSettings::COMBAT_LAYOUTS, "combat", "layouts" },
{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" },
{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" },
{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" },
{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" },
{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" },
{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" },
{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" },
{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" },
{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles"},
{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit"},
{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" },
{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" },
{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" },
{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" },
{EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" },
{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, "heroes", "minimalPrimarySkills" },
{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" },
{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" },
{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" },
{EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" },
{EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" },
{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" },
{EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" },
{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" },
{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" },
{EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" },
{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" },
{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" },
{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" },
{EGameSettings::MODULE_COMMANDERS, "modules", "commanders" },
{EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" },
{EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" },
{EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" },
{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" },
{EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" },
{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" },
{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" },
{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" },
{EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" },
{EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" },
{EGameSettings::TEXTS_CREATURE, "textData", "creature" },
{EGameSettings::TEXTS_FACTION, "textData", "faction" },
{EGameSettings::TEXTS_HERO, "textData", "hero" },
{EGameSettings::TEXTS_HERO_CLASS, "textData", "heroClass" },
{EGameSettings::TEXTS_OBJECT, "textData", "object" },
{EGameSettings::TEXTS_RIVER, "textData", "river" },
{EGameSettings::TEXTS_ROAD, "textData", "road" },
{EGameSettings::TEXTS_SPELL, "textData", "spell" },
{EGameSettings::TEXTS_TERRAIN, "textData", "terrain" },
{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" },
{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" },
{EGameSettings::INTERFACE_PLAYER_COLORED_BACKGROUND, "interface", "playerColoredBackground" },
{EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" },
{EGameSettings::BONUSES_GLOBAL, "bonuses", "global" },
{EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" },
{EGameSettings::COMBAT_AREA_SHOT_CAN_TARGET_EMPTY_HEX, "combat", "areaShotCanTargetEmptyHex" },
{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" },
{EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, "combat", "attackPointDamageFactorCap" },
{EGameSettings::COMBAT_BAD_LUCK_DICE, "combat", "badLuckDice" },
{EGameSettings::COMBAT_BAD_MORALE_DICE, "combat", "badMoraleDice" },
{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, "combat", "defensePointDamageFactor" },
{EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap" },
{EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" },
{EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" },
{EGameSettings::COMBAT_LAYOUTS, "combat", "layouts" },
{EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" },
{EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" },
{EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" },
{EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" },
{EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" },
{EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" },
{EGameSettings::DIMENSION_DOOR_EXPOSES_TERRAIN_TYPE, "spells", "dimensionDoorExposesTerrainType" },
{EGameSettings::DIMENSION_DOOR_FAILURE_SPENDS_POINTS, "spells", "dimensionDoorFailureSpendsPoints" },
{EGameSettings::DIMENSION_DOOR_ONLY_TO_UNCOVERED_TILES, "spells", "dimensionDoorOnlyToUncoveredTiles" },
{EGameSettings::DIMENSION_DOOR_TOURNAMENT_RULES_LIMIT, "spells", "dimensionDoorTournamentRulesLimit" },
{EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS, "spells", "dimensionDoorTriggersGuards" },
{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" },
{EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" },
{EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" },
{EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" },
{EGameSettings::HEROES_MINIMAL_PRIMARY_SKILLS, "heroes", "minimalPrimarySkills" },
{EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" },
{EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" },
{EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" },
{EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" },
{EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" },
{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" },
{EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" },
{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" },
{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" },
{EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" },
{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" },
{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" },
{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" },
{EGameSettings::MODULE_COMMANDERS, "modules", "commanders" },
{EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" },
{EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" },
{EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" },
{EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" },
{EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" },
{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" },
{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" },
{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" },
{EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" },
{EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" },
{EGameSettings::TEXTS_CREATURE, "textData", "creature" },
{EGameSettings::TEXTS_FACTION, "textData", "faction" },
{EGameSettings::TEXTS_HERO, "textData", "hero" },
{EGameSettings::TEXTS_HERO_CLASS, "textData", "heroClass" },
{EGameSettings::TEXTS_OBJECT, "textData", "object" },
{EGameSettings::TEXTS_RIVER, "textData", "river" },
{EGameSettings::TEXTS_ROAD, "textData", "road" },
{EGameSettings::TEXTS_SPELL, "textData", "spell" },
{EGameSettings::TEXTS_TERRAIN, "textData", "terrain" },
{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" },
{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" },
{EGameSettings::TOWNS_SPELL_RESEARCH, "towns", "spellResearch" },
{EGameSettings::TOWNS_SPELL_RESEARCH_COST, "towns", "spellResearchCost" },
{EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY, "towns", "spellResearchPerDay" },
{EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH, "towns", "spellResearchCostExponentPerResearch" },
{EGameSettings::INTERFACE_PLAYER_COLORED_BACKGROUND, "interface", "playerColoredBackground" },
};
void GameSettings::loadBase(const JsonNode & input)

View File

@ -94,6 +94,7 @@ public:
virtual void showInfoDialog(InfoWindow * iw) = 0;
virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells)=0;
virtual void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted)=0;
virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0;
virtual void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) = 0;
virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0;

View File

@ -80,6 +80,10 @@ enum class EGameSettings
TOWNS_BUILDINGS_PER_TURN_CAP,
TOWNS_STARTING_DWELLING_CHANCES,
INTERFACE_PLAYER_COLORED_BACKGROUND,
TOWNS_SPELL_RESEARCH,
TOWNS_SPELL_RESEARCH_COST,
TOWNS_SPELL_RESEARCH_PER_DAY,
TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH,
OPTIONS_COUNT,
OPTIONS_BEGIN = BONUSES_GLOBAL

View File

@ -268,7 +268,10 @@ CGTownInstance::CGTownInstance(IGameCallback *cb):
built(0),
destroyed(0),
identifier(0),
alignmentToPlayer(PlayerColor::NEUTRAL)
alignmentToPlayer(PlayerColor::NEUTRAL),
spellResearchCounterDay(0),
spellResearchAcceptedCounter(0),
spellResearchAllowed(true)
{
this->setNodeType(CBonusSystemNode::TOWN);
}

View File

@ -73,6 +73,9 @@ public:
std::vector<std::vector<SpellID> > spells; //spells[level] -> vector of spells, first will be available in guild
std::vector<CCastleEvent> events;
std::pair<si32, si32> bonusValue;//var to store town bonuses (rampart = resources from mystic pond, factory = save debts);
int spellResearchCounterDay;
int spellResearchAcceptedCounter;
bool spellResearchAllowed;
//////////////////////////////////////////////////////////////////////////
template <typename Handler> void serialize(Handler &h)
@ -93,6 +96,13 @@ public:
h & spells;
h & events;
if (h.version >= Handler::Version::SPELL_RESEARCH)
{
h & spellResearchCounterDay;
h & spellResearchAcceptedCounter;
h & spellResearchAllowed;
}
if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS)
{
h & rewardableBuildings;

View File

@ -2235,10 +2235,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt
}
if(features.levelHOTA1)
{
// TODO: HOTA support
[[maybe_unused]] bool spellResearchAvailable = reader->readBool();
}
object->spellResearchAllowed = reader->readBool();
// Read castle events
uint32_t eventsCount = reader->readUInt32();

View File

@ -42,6 +42,7 @@ public:
virtual void visitSetSecSkill(SetSecSkill & pack) {}
virtual void visitHeroVisitCastle(HeroVisitCastle & pack) {}
virtual void visitChangeSpells(ChangeSpells & pack) {}
virtual void visitSetResearchedSpells(SetResearchedSpells & pack) {}
virtual void visitSetMana(SetMana & pack) {}
virtual void visitSetMovePoints(SetMovePoints & pack) {}
virtual void visitFoWChange(FoWChange & pack) {}
@ -128,6 +129,7 @@ public:
virtual void visitBuildStructure(BuildStructure & pack) {}
virtual void visitVisitTownBuilding(VisitTownBuilding & pack) {}
virtual void visitRazeStructure(RazeStructure & pack) {}
virtual void visitSpellResearch(SpellResearch & pack) {}
virtual void visitRecruitCreatures(RecruitCreatures & pack) {}
virtual void visitUpgradeCreature(UpgradeCreature & pack) {}
virtual void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) {}

View File

@ -162,6 +162,10 @@ void ChangeSpells::visitTyped(ICPackVisitor & visitor)
visitor.visitChangeSpells(*this);
}
void SetResearchedSpells::visitTyped(ICPackVisitor & visitor)
{
visitor.visitSetResearchedSpells(*this);
}
void SetMana::visitTyped(ICPackVisitor & visitor)
{
visitor.visitSetMana(*this);
@ -592,6 +596,11 @@ void RazeStructure::visitTyped(ICPackVisitor & visitor)
visitor.visitRazeStructure(*this);
}
void SpellResearch::visitTyped(ICPackVisitor & visitor)
{
visitor.visitSpellResearch(*this);
}
void RecruitCreatures::visitTyped(ICPackVisitor & visitor)
{
visitor.visitRecruitCreatures(*this);
@ -930,6 +939,16 @@ void ChangeSpells::applyGs(CGameState *gs)
hero->removeSpellFromSpellbook(sid);
}
void SetResearchedSpells::applyGs(CGameState *gs)
{
CGTownInstance *town = gs->getTown(tid);
town->spells[level] = spells;
town->spellResearchCounterDay++;
if(accepted)
town->spellResearchAcceptedCounter++;
}
void SetMana::applyGs(CGameState *gs)
{
CGHeroInstance * hero = gs->getHero(hid);
@ -1914,7 +1933,10 @@ void NewTurn::applyGs(CGameState *gs)
creatureSet.applyGs(gs);
for(CGTownInstance* t : gs->map->towns)
{
t->built = 0;
t->spellResearchCounterDay = 0;
}
if(newRumor)
gs->currentRumor = *newRumor;

View File

@ -288,6 +288,26 @@ struct DLL_LINKAGE ChangeSpells : public CPackForClient
}
};
struct DLL_LINKAGE SetResearchedSpells : public CPackForClient
{
void applyGs(CGameState * gs) override;
void visitTyped(ICPackVisitor & visitor) override;
ui8 level = 0;
ObjectInstanceID tid;
std::vector<SpellID> spells;
bool accepted;
template <typename Handler> void serialize(Handler & h)
{
h & level;
h & tid;
h & spells;
h & accepted;
}
};
struct DLL_LINKAGE SetMana : public CPackForClient
{
void applyGs(CGameState * gs) override;

View File

@ -306,6 +306,28 @@ struct DLL_LINKAGE RazeStructure : public BuildStructure
void visitTyped(ICPackVisitor & visitor) override;
};
struct DLL_LINKAGE SpellResearch : public CPackForServer
{
SpellResearch() = default;
SpellResearch(const ObjectInstanceID & TID, SpellID spellAtSlot, bool accepted)
: tid(TID), spellAtSlot(spellAtSlot), accepted(accepted)
{
}
ObjectInstanceID tid;
SpellID spellAtSlot;
bool accepted;
void visitTyped(ICPackVisitor & visitor) override;
template <typename Handler> void serialize(Handler & h)
{
h & static_cast<CPackForServer &>(*this);
h & tid;
h & spellAtSlot;
h & accepted;
}
};
struct DLL_LINKAGE RecruitCreatures : public CPackForServer
{
RecruitCreatures() = default;

View File

@ -61,6 +61,7 @@ enum class ESerializationVersion : int32_t
CAMPAIGN_OUTRO_SUPPORT, // 862 - support for campaign outro video
REWARDABLE_BANKS, // 863 - team state contains list of scouted objects, coast visitable rewardable objects
REGION_LABEL, // 864 - labels for campaign regions
SPELL_RESEARCH, // 865 - spell research
CURRENT = REGION_LABEL
CURRENT = SPELL_RESEARCH
};

View File

@ -288,6 +288,8 @@ void registerTypes(Serializer &s)
s.template registerType<LobbySetDifficulty>(238);
s.template registerType<LobbyForceSetPlayer>(239);
s.template registerType<LobbySetExtraOptions>(240);
s.template registerType<SpellResearch>(241);
s.template registerType<SetResearchedSpells>(242);
}
VCMI_LIB_NAMESPACE_END

View File

@ -1253,6 +1253,16 @@ void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const st
sendAndApply(&cs);
}
void CGameHandler::setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted)
{
SetResearchedSpells cs;
cs.tid = town->id;
cs.spells = spells;
cs.level = level;
cs.accepted = accepted;
sendAndApply(&cs);
}
void CGameHandler::giveHeroBonus(GiveBonus * bonus)
{
sendAndApply(bonus);
@ -2251,6 +2261,60 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid)
return true;
}
bool CGameHandler::spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool accepted)
{
CGTownInstance *t = gs->getTown(tid);
if(!getSettings().getBoolean(EGameSettings::TOWNS_SPELL_RESEARCH) && complain("Spell research not allowed!"))
return false;
if (!t->spellResearchAllowed && complain("Spell research not allowed in this town!"))
return false;
int level = -1;
for(int i = 0; i < t->spells.size(); i++)
if(vstd::find_pos(t->spells[i], spellAtSlot) != -1)
level = i;
if(level == -1 && complain("Spell for replacement not found!"))
return false;
auto spells = t->spells.at(level);
bool researchLimitExceeded = t->spellResearchCounterDay >= getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_PER_DAY).Vector()[level].Float();
if(researchLimitExceeded && complain("Already researched today!"))
return false;
if(!accepted)
{
auto it = spells.begin() + t->spellsAtLevel(level, false);
std::rotate(it, it + 1, spells.end()); // move to end
setResearchedSpells(t, level, spells, accepted);
return true;
}
auto costBase = TResources(getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST).Vector()[level]);
auto costExponent = getSettings().getValue(EGameSettings::TOWNS_SPELL_RESEARCH_COST_EXPONENT_PER_RESEARCH).Vector()[level].Float();
auto cost = costBase * std::pow(t->spellResearchAcceptedCounter + 1, costExponent);
if(!getPlayerState(t->getOwner())->resources.canAfford(cost) && complain("Spell replacement cannot be afforded!"))
return false;
giveResources(t->getOwner(), -cost);
std::swap(spells.at(t->spellsAtLevel(level, false)), spells.at(vstd::find_pos(spells, spellAtSlot)));
auto it = spells.begin() + t->spellsAtLevel(level, false);
std::rotate(it, it + 1, spells.end()); // move to end
setResearchedSpells(t, level, spells, accepted);
if(t->visitingHero)
giveSpells(t, t->visitingHero);
if(t->garrisonHero)
giveSpells(t, t->garrisonHero);
return true;
}
bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl, PlayerColor player)
{
const CGDwelling * dwelling = dynamic_cast<const CGDwelling *>(getObj(objid));

View File

@ -107,6 +107,7 @@ public:
//from IGameCallback
//do sth
void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells) override;
void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted) override;
bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override;
void setOwner(const CGObjectInstance * obj, PlayerColor owner) override;
void giveExperience(const CGHeroInstance * hero, TExpType val) override;
@ -218,6 +219,7 @@ public:
bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings
bool visitTownBuilding(ObjectInstanceID tid, BuildingID bid);
bool razeStructure(ObjectInstanceID tid, BuildingID bid);
bool spellResearch(ObjectInstanceID tid, SpellID spellAtSlot, bool accepted);
bool disbandCreature( ObjectInstanceID id, SlotID pos );
bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player);
bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot);

View File

@ -138,6 +138,14 @@ void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack)
result = gh.buildStructure(pack.tid, pack.bid);
}
void ApplyGhNetPackVisitor::visitSpellResearch(SpellResearch & pack)
{
gh.throwIfWrongOwner(&pack, pack.tid);
gh.throwIfPlayerNotActive(&pack);
result = gh.spellResearch(pack.tid, pack.spellAtSlot, pack.accepted);
}
void ApplyGhNetPackVisitor::visitVisitTownBuilding(VisitTownBuilding & pack)
{
gh.throwIfWrongOwner(&pack, pack.tid);

View File

@ -41,6 +41,7 @@ public:
void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override;
void visitDisbandCreature(DisbandCreature & pack) override;
void visitBuildStructure(BuildStructure & pack) override;
void visitSpellResearch(SpellResearch & pack) override;
void visitVisitTownBuilding(VisitTownBuilding & pack) override;
void visitRecruitCreatures(RecruitCreatures & pack) override;
void visitUpgradeCreature(UpgradeCreature & pack) override;

View File

@ -44,6 +44,7 @@ public:
void showInfoDialog(InfoWindow * iw) override {}
void changeSpells(const CGHeroInstance * hero, bool give, const std::set<SpellID> &spells) override {}
void setResearchedSpells(const CGTownInstance * town, int level, const std::vector<SpellID> spells, bool accepted) override {}
bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}
void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override {}
void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {}