diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 0aae6d558..6c189492d 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -20,6 +20,7 @@ #include "../../../lib/CCreatureHandler.h" #include "../../../lib/GameLibrary.h" #include "../../../lib/StartInfo.h" +#include "../../../lib/GameSettings.h" #include "../../../lib/filesystem/Filesystem.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" @@ -581,7 +582,7 @@ float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, return role == HeroRole::SCOUT ? 2 : 0; if(hero->getSecSkillLevel(skill) != MasteryLevel::NONE - || hero->secSkills.size() >= GameConstants::SKILL_PER_HERO) + || static_cast(hero->secSkills.size()) >= cb->getSettings().getInteger(EGameSettings::HEROES_SKILL_PER_HERO)) return 0; auto score = ai->heroManager->evaluateSecSkill(skill, hero); diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index 50f9e7fb8..5adb9b366 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -435,6 +435,9 @@ "vcmi.townWindow.upgradeAll.notAllUpgradable" : "Not enough resources to upgrade all creatures. Do you want to upgrade following creatures?", "vcmi.townWindow.upgradeAll.notUpgradable" : "Not enough resources to upgrade any creature.", + "vcmi.kingdomOverview.secSkillOverflow.hover" : "More skills", + "vcmi.kingdomOverview.secSkillOverflow.help" : "{More skills}\n\nThis hero has more skills.\nYou can see all of them in the hero overview.", + "vcmi.logicalExpressions.anyOf" : "Any of the following:", "vcmi.logicalExpressions.allOf" : "All of the following:", "vcmi.logicalExpressions.noneOf" : "None of the following:", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index a2f8bade1..aaa82c3d1 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -425,6 +425,9 @@ "vcmi.townWindow.upgradeAll.notAllUpgradable" : "Nicht genügend Ressourcen um alle Kreaturen aufzurüsten. Folgende Kreaturen aufrüsten?", "vcmi.townWindow.upgradeAll.notUpgradable" : "Nicht genügend Ressourcen um mindestens eine Kreatur aufzurüsten.", + "vcmi.kingdomOverview.secSkillOverflow.hover" : "Mehr skills", + "vcmi.kingdomOverview.secSkillOverflow.help" : "{Mehr skills}\n\nDieser Held hat mehr Skills.\nIn der Heldenübersicht können alle eingesehen werden.", + "vcmi.logicalExpressions.anyOf" : "Eines der folgenden:", "vcmi.logicalExpressions.allOf" : "Alles der folgenden:", "vcmi.logicalExpressions.noneOf" : "Keines der folgenden:", diff --git a/client/windows/CExchangeWindow.cpp b/client/windows/CExchangeWindow.cpp index bb886a5d4..9234b5ec7 100644 --- a/client/windows/CExchangeWindow.cpp +++ b/client/windows/CExchangeWindow.cpp @@ -81,7 +81,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, primSkillValues[leftRight].push_back(std::make_shared(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE)); - for(int m=0; m < hero->secSkills.size(); ++m) + for(int m=0; m < std::min(static_cast(hero->secSkills.size()), 8); ++m) secSkills[leftRight].push_back(std::make_shared(Point(32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88), CSecSkillPlace::ImageSize::SMALL, hero->secSkills[m].first, hero->secSkills[m].second)); @@ -377,6 +377,10 @@ void CExchangeWindow::questLogShortcut() void CExchangeWindow::update() { + const bool qeLayout = isQuickExchangeLayoutAvailable(); + + OBJECT_CONSTRUCTION; + CWindowWithArtifacts::update(); for(size_t leftRight : {0, 1}) @@ -389,8 +393,27 @@ void CExchangeWindow::update() primSkillValues[leftRight][m]->setText(std::to_string(value)); } - for(int m=0; m < hero->secSkills.size(); ++m) + int slots = 8; + bool isMoreSkillsThanSlots = hero->secSkills.size() > slots; + for(int m=0; m < std::min(static_cast(hero->secSkills.size()), 8); ++m) { + if(m == slots - 1) + { + if(isMoreSkillsThanSlots) + { + Rect r(Point(32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88), Point(34, 28)); + secSkillsFull[leftRight] = std::make_shared(r, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "..."); + secSkillsFullArea[leftRight] = std::make_shared(r, LIBRARY->generaltexth->translate("vcmi.kingdomOverview.secSkillOverflow.hover"), LIBRARY->generaltexth->translate("vcmi.kingdomOverview.secSkillOverflow.help")); + secSkills[leftRight][m]->setSkill(SecondarySkill::NONE); + continue; + } + else + { + secSkillsFull[leftRight].reset(); + secSkillsFullArea[leftRight].reset(); + } + } + int id = hero->secSkills[m].first; int level = hero->secSkills[m].second; diff --git a/client/windows/CExchangeWindow.h b/client/windows/CExchangeWindow.h index e1c31d3e2..499eeb3a4 100644 --- a/client/windows/CExchangeWindow.h +++ b/client/windows/CExchangeWindow.h @@ -13,6 +13,8 @@ #include "../widgets/CExchangeController.h" class CGarrisonSlot; +class CMultiLineLabel; +class LRClickableAreaWText; class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts { @@ -27,6 +29,8 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::vector> primSkillAreas; std::array>, 2> secSkills; + std::array, 2> secSkillsFull; + std::array, 2> secSkillsFullArea; std::array, 2> heroAreas; std::array, 2> specialtyAreas; diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 091db70a8..d806de045 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -28,6 +28,7 @@ #include "../widgets/CGarrisonInt.h" #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" +#include "../widgets/Slider.h" #include "../render/IRenderHandler.h" #include "../lib/CConfigHandler.h" @@ -148,16 +149,27 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) expValue = std::make_shared(68, 252); manaValue = std::make_shared(211, 252); + if(hero->secSkills.size() > 8) + { + auto divisionRoundUp = [](int x, int y){ return (x + (y - 1)) / y; }; + int lines = divisionRoundUp(hero->secSkills.size(), 2); + secSkillSlider = std::make_shared(Point(284, 276), 189, [this](int val){ CHeroWindow::update(); }, 4, lines, 0, Orientation::VERTICAL, CSlider::BROWN); + secSkillSlider->setPanningStep(48); + secSkillSlider->setScrollBounds(Rect(-266, 0, secSkillSlider->pos.x - pos.x + secSkillSlider->pos.w, secSkillSlider->pos.h)); + } + for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) { - Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); + bool isSmallBox = (secSkillSlider && i%2 == 1); + Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), isSmallBox ? 120 : 136, 42); secSkills.emplace_back(std::make_shared(r.topLeft(), CSecSkillPlace::ImageSize::MEDIUM)); int x = (i % 2) ? 212 : 68; int y = 280 + 48 * (i/2); + int width = isSmallBox ? 71 : 87; - secSkillValues.push_back(std::make_shared(x, y, FONT_SMALL, ETextAlignment::TOPLEFT)); - secSkillNames.push_back(std::make_shared(x, y+20, FONT_SMALL, ETextAlignment::TOPLEFT)); + secSkillValues.push_back(std::make_shared(x, y, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, "", width)); + secSkillNames.push_back(std::make_shared(x, y+20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, "", width)); } // various texts @@ -175,6 +187,8 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) void CHeroWindow::update() { + OBJECT_CONSTRUCTION; + CWindowWithArtifacts::update(); auto & heroscrn = LIBRARY->generaltexth->heroscrn; assert(curHero); @@ -195,7 +209,6 @@ void CHeroWindow::update() portraitImage->setFrame(curHero->getIconIndex()); { - OBJECT_CONSTRUCTION; if(!garr) { bool removableTroops = curHero->getOwner() == GAME->interface()->playerID; @@ -233,9 +246,17 @@ void CHeroWindow::update() } //secondary skills support - for(size_t g=0; g< secSkills.size(); ++g) + for(size_t g=0; g < secSkills.size(); ++g) { - SecondarySkill skill = curHero->secSkills[g].first; + int offset = secSkillSlider ? secSkillSlider->getValue() * 2 : 0; + if(curHero->secSkills.size() < g + offset + 1) + { + secSkillNames[g]->setText(""); + secSkillValues[g]->setText(""); + secSkills[g]->setSkill(SecondarySkill::NONE); + break; + } + SecondarySkill skill = curHero->secSkills[g + offset].first; int level = curHero->getSecSkillLevel(skill); std::string skillName = skill.toEntity(LIBRARY)->getNameTranslated(); std::string skillValue = LIBRARY->generaltexth->levels[level-1]; diff --git a/client/windows/CHeroWindow.h b/client/windows/CHeroWindow.h index fd33df05d..9524262ff 100644 --- a/client/windows/CHeroWindow.h +++ b/client/windows/CHeroWindow.h @@ -33,6 +33,7 @@ class CToggleGroup; class CGStatusBar; class CTextBox; class CGarrisonInt; +class CSlider; /// Button which switches hero selection class CHeroSwitcher : public CIntObject @@ -77,6 +78,7 @@ class CHeroWindow : public CStatusbarWindow, public IGarrisonHolder, public CWin std::vector< std::shared_ptr> secSkills; std::vector> secSkillNames; std::vector> secSkillValues; + std::shared_ptr secSkillSlider; std::shared_ptr quitButton; std::shared_ptr dismissLabel; diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index d01711562..856f903be 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -41,7 +41,8 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/texts/CGeneralTextHandler.h" +#include "texts/CGeneralTextHandler.h" +#include "../../lib/GameSettings.h" static const std::string OVERVIEW_BACKGROUND = "OvCast.pcx"; static const size_t OVERVIEW_SIZE = 4; @@ -983,8 +984,17 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) heroInfo.push_back(std::make_shared(Point(78+(int)i*36, 26), InfoBox::POS_DOWN, InfoBox::SIZE_SMALL, data)); } - for(size_t i=0; isecSkills.size() > slots; + for(size_t i=0; i(r, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "..."); + heroInfoFullArea = std::make_shared(r, LIBRARY->generaltexth->translate("vcmi.kingdomOverview.secSkillOverflow.hover"), LIBRARY->generaltexth->translate("vcmi.kingdomOverview.secSkillOverflow.help")); + continue; + } auto data = std::make_shared(IInfoBoxData::HERO_SECONDARY_SKILL, hero, (int)i); heroInfo.push_back(std::make_shared(Point(410+(int)i*36, 5), InfoBox::POS_NONE, InfoBox::SIZE_SMALL, data)); } diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index fc519197c..b96bbe609 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -32,6 +32,7 @@ class CTabbedInt; class CGStatusBar; class CGarrisonInt; class CMultiLineLabel; +class LRClickableAreaWText; class CKingdHeroList; class CKingdTownList; @@ -310,6 +311,8 @@ class CHeroItem : public CIntObject, public IGarrisonHolder std::shared_ptr artButtons; std::vector> heroInfo; + std::shared_ptr heroInfoFull; + std::shared_ptr heroInfoFullArea; std::shared_ptr morale; std::shared_ptr luck; diff --git a/config/gameConfig.json b/config/gameConfig.json index d348c302a..769a748c9 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -507,6 +507,9 @@ "movementPointsLand" : [ 1500, 1500, 1500, 1500, 1560, 1630, 1700, 1760, 1830, 1900, 1960, 2000 ], /// movement points hero can get on start of the turn when on sea, depending on speed of slowest creature (0-based list) "movementPointsSea" : [ 1500 ], + + /// maximal secondary skills per hero + "skillPerHero" : 8, /// Base scouting range for hero without any range modifiers "baseScoutingRange" : 5, diff --git a/config/schemas/gameSettings.json b/config/schemas/gameSettings.json index 671caf4e9..0b1b093a8 100644 --- a/config/schemas/gameSettings.json +++ b/config/schemas/gameSettings.json @@ -48,6 +48,7 @@ "movementCostBase" : { "type" : "number" }, "movementPointsLand" : { "type" : "array" }, "movementPointsSea" : { "type" : "array" }, + "skillPerHero" : { "type" : "number" }, "specialtyCreatureGrowth" : { "type" : "number" }, "specialtySecondarySkillGrowth" : { "type" : "number" }, "baseScoutingRange" : { "type" : "number" } diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index 0f5a42f5f..27a12b270 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -10,6 +10,17 @@ To use cheat code, press `Tab` key or click/tap on status bar to open game chat `nwcthereisnospoon`, `nwcmidichlorians`, `nwctim`, `vcmiistari` or `vcmispells` - give a spell book, all spells and 999 mana to currently selected hero. Also allows casting spell up to 100 times per combat round +### Secondary Skills + +`vcmiskill ` - give a secondary skill to currently selected hero + +Examples: +`vcmiskill learning` - give expert level learning skill +`vcmiskill leadership 2` - give advanced level leadership skill +`vcmiskill wisdom 0` - remove wisdom skill +`vcmiskill every` - give all skills on expert level +`vcmiskill every 0` - remove all skills + ### Army `nwctrinity`, `nwcpadme`, `nwcavertingoureyes`, `vcmiainur` or `vcmiarchangel` - give 5 Archangels in every empty slot (to currently selected hero) diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 8086c1416..a3132be39 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -87,6 +87,7 @@ const std::vector GameSettings::settingProperties = {EGameSettings::HEROES_MOVEMENT_COST_BASE, "heroes", "movementCostBase" }, {EGameSettings::HEROES_MOVEMENT_POINTS_LAND, "heroes", "movementPointsLand" }, {EGameSettings::HEROES_MOVEMENT_POINTS_SEA, "heroes", "movementPointsSea" }, + {EGameSettings::HEROES_SKILL_PER_HERO, "heroes", "skillPerHero" }, {EGameSettings::HEROES_SPECIALTY_CREATURE_GROWTH, "heroes", "specialtyCreatureGrowth" }, {EGameSettings::HEROES_SPECIALTY_SECONDARY_SKILL_GROWTH, "heroes", "specialtySecondarySkillGrowth" }, {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, diff --git a/lib/IGameSettings.h b/lib/IGameSettings.h index 56af7b711..693c30fd0 100644 --- a/lib/IGameSettings.h +++ b/lib/IGameSettings.h @@ -60,6 +60,7 @@ enum class EGameSettings HEROES_MOVEMENT_COST_BASE, HEROES_MOVEMENT_POINTS_LAND, HEROES_MOVEMENT_POINTS_SEA, + HEROES_SKILL_PER_HERO, HEROES_SPECIALTY_CREATURE_GROWTH, HEROES_SPECIALTY_SECONDARY_SKILL_GROWTH, INTERFACE_PLAYER_COLORED_BACKGROUND, diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index 6579a0c08..09c408a6d 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -33,7 +33,6 @@ namespace GameConstants constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit constexpr int ARMY_SIZE = 7; - constexpr int SKILL_PER_HERO = 8; constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order constexpr int SKILL_QUANTITY=28; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 75ceb8099..7efbf9a76 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -31,6 +31,7 @@ #include "../CCreatureHandler.h" #include "../mapping/CMap.h" #include "../StartInfo.h" +#include "../GameSettings.h" #include "CGTownInstance.h" #include "../entities/artifact/ArtifactUtils.h" #include "../entities/artifact/CArtifact.h" @@ -167,7 +168,7 @@ int3 CGHeroInstance::convertFromVisitablePos(const int3 & position) const bool CGHeroInstance::canLearnSkill() const { - return secSkills.size() < GameConstants::SKILL_PER_HERO; + return secSkills.size() < cb->getSettings().getInteger(EGameSettings::HEROES_SKILL_PER_HERO); } bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 8ea67c9d1..07bb9911a 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -17,7 +17,9 @@ #include "../TurnTimerHandler.h" #include "../../lib/CPlayerState.h" +#include "../../lib/CSkillHandler.h" #include "../../lib/StartInfo.h" +#include "../../lib/constants/Enumerations.h" #include "../../lib/entities/artifact/CArtHandler.h" #include "../../lib/entities/building/CBuilding.h" #include "../../lib/entities/hero/CHeroHandler.h" @@ -694,6 +696,46 @@ void PlayerMessageProcessor::cheatMaxMorale(PlayerColor player, const CGHeroInst gameHandler->giveHeroBonus(&gb); } +void PlayerMessageProcessor::cheatSkill(PlayerColor player, const CGHeroInstance * hero, std::vector words) +{ + if (!hero) + return; + + std::string identifier; + try + { + identifier = words.at(0); + } + catch(std::logic_error&) + { + return; + } + + MasteryLevel::Type mastery; + try + { + mastery = static_cast(std::stoi(words.at(1))); + } + catch(std::logic_error&) + { + mastery = MasteryLevel::Type::EXPERT; + } + + if(identifier == "every") + { + for(const auto & skill : LIBRARY->skillh->objects) + gameHandler->changeSecSkill(hero, SecondarySkill(skill->getId()), mastery, ChangeValueMode::ABSOLUTE); + return; + } + + std::optional skillId = LIBRARY->identifiers()->getIdentifier(ModScope::scopeGame(), "skill", identifier, false); + if(!skillId.has_value()) + return; + + auto skill = SecondarySkill(skillId.value()); + gameHandler->changeSecSkill(hero, skill, mastery, ChangeValueMode::ABSOLUTE); +} + bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerColor player, ObjectInstanceID currObj) { std::vector words; @@ -736,7 +778,8 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo "vcmiluck", "nwcfollowthewhiterabbit", "nwccastleanthrax", "vcmimorale", "nwcmorpheus", "nwcmuchrejoicing", "vcmigod", "nwctheone", - "vcmiscrolls" + "vcmiscrolls", + "vcmiskill" }; if(vstd::contains(localCheats, cheatName)) @@ -829,7 +872,8 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla cheatMovement(player, hero, { }); cheatFly(player, hero); }; - const auto & doColorSchemeChange = [&](ColorScheme filter) { cheatColorSchemeChange(player, filter); }; + const auto & doCheatColorSchemeChange = [&](ColorScheme filter) { cheatColorSchemeChange(player, filter); }; + const auto & doCheatSkill = [&]() { cheatSkill(player, hero, words); }; std::map> callbacks = { {"vcmiainur", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, @@ -909,9 +953,10 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla {"vcmigod", doCheatTheOne }, {"nwctheone", doCheatTheOne }, {"vcmiscrolls", doCheatGiveScrolls }, - {"vcmicolor", [&] () {doColorSchemeChange(ColorScheme::H2_SCHEME);} }, - {"nwcphisherprice", [&] () {doColorSchemeChange(ColorScheme::H2_SCHEME);} }, - {"vcmigray", [&] () {doColorSchemeChange(ColorScheme::GRAYSCALE);} }, + {"vcmicolor", [&] () {doCheatColorSchemeChange(ColorScheme::H2_SCHEME);} }, + {"nwcphisherprice", [&] () {doCheatColorSchemeChange(ColorScheme::H2_SCHEME);} }, + {"vcmigray", [&] () {doCheatColorSchemeChange(ColorScheme::GRAYSCALE);} }, + {"vcmiskill", doCheatSkill }, }; assert(callbacks.count(cheatName)); diff --git a/server/processors/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index 2438ef9eb..a0e44109a 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -59,6 +59,7 @@ class PlayerMessageProcessor void cheatMaxLuck(PlayerColor player, const CGHeroInstance * hero); void cheatMaxMorale(PlayerColor player, const CGHeroInstance * hero); void cheatFly(PlayerColor player, const CGHeroInstance * hero); + void cheatSkill(PlayerColor player, const CGHeroInstance * hero, std::vector words); void commandExit(PlayerColor player, const std::vector & words); void commandKick(PlayerColor player, const std::vector & words);