CGameStateTest.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. /*
  2. * CGameStateTest.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "mock/mock_Services.h"
  12. #include "mock/mock_MapService.h"
  13. #include "mock/mock_IGameEventCallback.h"
  14. #include "mock/mock_spells_Problem.h"
  15. #include "../../lib/VCMIDirs.h"
  16. #include "../../lib/json/JsonBonus.h"
  17. #include "../../lib/json/JsonUtils.h"
  18. #include "../../lib/gameState/CGameState.h"
  19. #include "../../lib/modding/ModScope.h"
  20. #include "../../lib/networkPacks/PacksForClient.h"
  21. #include "../../lib/networkPacks/PacksForClientBattle.h"
  22. #include "../../lib/networkPacks/SetStackEffect.h"
  23. #include "../../lib/CRandomGenerator.h"
  24. #include "../../lib/StartInfo.h"
  25. #include "../../lib/TerrainHandler.h"
  26. #include "../../lib/battle/BattleInfo.h"
  27. #include "../../lib/battle/BattleLayout.h"
  28. #include "../../lib/callback/GameRandomizer.h"
  29. #include "../../lib/CStack.h"
  30. #include "../../lib/filesystem/ResourcePath.h"
  31. #include "../../lib/mapping/CMap.h"
  32. #include "../../lib/spells/CSpellHandler.h"
  33. #include "../../lib/spells/ISpellMechanics.h"
  34. #include "../../lib/spells/AbilityCaster.h"
  35. class CGameStateTest : public ::testing::Test, public SpellCastEnvironment, public MapListener
  36. {
  37. public:
  38. CGameStateTest()
  39. : gameEventCallback(std::make_shared<GameEventCallbackMock>(this)),
  40. mapService("test/MiniTest/", this),
  41. map(nullptr)
  42. {
  43. }
  44. void SetUp() override
  45. {
  46. gameState = std::make_shared<CGameState>();
  47. gameState->preInit(&services);
  48. }
  49. void TearDown() override
  50. {
  51. gameState.reset();
  52. }
  53. bool describeChanges() const override
  54. {
  55. return true;
  56. }
  57. void apply(CPackForClient & pack) override
  58. {
  59. gameState->apply(pack);
  60. }
  61. void apply(BattleLogMessage & pack) override
  62. {
  63. gameState->apply(pack);
  64. }
  65. void apply(BattleStackMoved & pack) override
  66. {
  67. gameState->apply(pack);
  68. }
  69. void apply(BattleUnitsChanged & pack) override
  70. {
  71. gameState->apply(pack);
  72. }
  73. void apply(SetStackEffect & pack) override
  74. {
  75. gameState->apply(pack);
  76. }
  77. void apply(StacksInjured & pack) override
  78. {
  79. gameState->apply(pack);
  80. }
  81. void apply(BattleObstaclesChanged & pack) override
  82. {
  83. gameState->apply(pack);
  84. }
  85. void apply(CatapultAttack & pack) override
  86. {
  87. gameState->apply(pack);
  88. }
  89. void complain(const std::string & problem) override
  90. {
  91. FAIL() << "Server-side assertion: " << problem;
  92. };
  93. vstd::RNG * getRNG() override
  94. {
  95. return &randomGenerator;//todo: mock this
  96. }
  97. const CMap * getMap() const override
  98. {
  99. return map;
  100. }
  101. const IGameInfoCallback * getCb() const override
  102. {
  103. return gameState.get();
  104. }
  105. void createBoat(const int3 & visitablePosition, BoatId type, PlayerColor initiator) override
  106. {
  107. }
  108. bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode) override
  109. {
  110. return false;
  111. }
  112. void genericQuery(Query * request, PlayerColor color, std::function<void(std::optional<int32_t>)> callback) override
  113. {
  114. //todo:
  115. }
  116. void mapLoaded(CMap * map) override
  117. {
  118. EXPECT_EQ(this->map, nullptr);
  119. this->map = map;
  120. }
  121. void startTestGame()
  122. {
  123. StartInfo si;
  124. si.mapname = "anything";//does not matter, map service mocked
  125. si.difficulty = 0;
  126. si.mode = EStartMode::NEW_GAME;
  127. std::unique_ptr<CMapHeader> header = mapService.loadMapHeader(ResourcePath(si.mapname));
  128. ASSERT_NE(header.get(), nullptr);
  129. //FIXME: this has been copied from CPreGame, but should be part of StartInfo
  130. for(int i = 0; i < header->players.size(); i++)
  131. {
  132. const PlayerInfo & pinfo = header->players[i];
  133. //neither computer nor human can play - no player
  134. if (!(pinfo.canHumanPlay || pinfo.canComputerPlay))
  135. continue;
  136. PlayerSettings & pset = si.playerInfos[PlayerColor(i)];
  137. pset.color = PlayerColor(i);
  138. pset.connectedPlayerIDs.insert(static_cast<PlayerConnectionID>(i));
  139. pset.name = "Player";
  140. pset.castle = pinfo.defaultCastle();
  141. pset.hero = pinfo.defaultHero();
  142. if(pset.hero != HeroTypeID::RANDOM && pinfo.hasCustomMainHero())
  143. {
  144. pset.hero = pinfo.mainCustomHeroId;
  145. pset.heroNameTextId = pinfo.mainCustomHeroNameTextId;
  146. pset.heroPortrait = HeroTypeID(pinfo.mainCustomHeroPortrait);
  147. }
  148. }
  149. GameRandomizer randomizer(*gameState);
  150. Load::ProgressAccumulator progressTracker;
  151. gameState->init(&mapService, &si, randomizer, progressTracker, false);
  152. ASSERT_NE(map, nullptr);
  153. ASSERT_EQ(map->getHeroesOnMap().size(), 2);
  154. }
  155. void startTestBattle(const CGHeroInstance * attacker, const CGHeroInstance * defender)
  156. {
  157. BattleSideArray<const CGHeroInstance *> heroes = {attacker, defender};
  158. BattleSideArray<const CArmedInstance *> armedInstancies = {attacker, defender};
  159. int3 tile(4,4,0);
  160. const auto & t = *gameState->getTile(tile);
  161. auto terrain = t.getTerrainID();
  162. BattleField terType(0);
  163. BattleLayout layout = BattleLayout::createDefaultLayout(*gameState, attacker, defender);
  164. //send info about battles
  165. auto battle = BattleInfo::setupBattle(gameState.get(), tile, terrain, terType, armedInstancies, heroes, layout, nullptr);
  166. BattleStart bs;
  167. bs.info = std::move(battle);
  168. bs.battleID = BattleID(0);
  169. ASSERT_EQ(gameState->currentBattles.size(), 0);
  170. gameEventCallback->sendAndApply(bs);
  171. ASSERT_EQ(gameState->currentBattles.size(), 1);
  172. }
  173. std::shared_ptr<CGameState> gameState;
  174. std::shared_ptr<GameEventCallbackMock> gameEventCallback;
  175. MapServiceMock mapService;
  176. ServicesMock services;
  177. CMap * map;
  178. CRandomGenerator randomGenerator;
  179. };
  180. //Issue #2765, Ghost Dragons can cast Age on Catapults
  181. TEST_F(CGameStateTest, issue2765)
  182. {
  183. startTestGame();
  184. auto attackerID = map->getHeroesOnMap()[0];
  185. auto defenderID = map->getHeroesOnMap()[1];
  186. auto attacker = dynamic_cast<CGHeroInstance *>(map->getObject(attackerID));
  187. auto defender = dynamic_cast<CGHeroInstance *>(map->getObject(defenderID));
  188. ASSERT_NE(attacker->tempOwner, defender->tempOwner);
  189. {
  190. NewArtifact na;
  191. na.artHolder = defender->id;
  192. na.artId = ArtifactID::BALLISTA;
  193. na.pos = ArtifactPosition::MACH1;
  194. gameEventCallback->sendAndApply(na);
  195. }
  196. startTestBattle(attacker, defender);
  197. {
  198. battle::UnitInfo info;
  199. info.id = gameState->currentBattles.front()->battleNextUnitId();
  200. info.count = 1;
  201. info.type = CreatureID(69);
  202. info.side = BattleSide::ATTACKER;
  203. info.position = gameState->currentBattles.front()->getAvailableHex(info.type, info.side);
  204. info.summoned = false;
  205. BattleUnitsChanged pack;
  206. pack.battleID = BattleID(0);
  207. pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
  208. info.save(pack.changedStacks.back().data);
  209. gameEventCallback->sendAndApply(pack);
  210. }
  211. const CStack * att = nullptr;
  212. const CStack * def = nullptr;
  213. for(const auto & s : gameState->currentBattles.front()->stacks)
  214. {
  215. if(s->unitType()->getId() == CreatureID::BALLISTA && s->unitSide() == BattleSide::DEFENDER)
  216. def = s.get();
  217. else if(s->unitType()->getId() == CreatureID(69) && s->unitSide() == BattleSide::ATTACKER)
  218. att = s.get();
  219. }
  220. ASSERT_NE(att, nullptr);
  221. ASSERT_NE(def, nullptr);
  222. ASSERT_NE(att, def);
  223. EXPECT_NE(att->getMyHero(), defender);
  224. EXPECT_NE(def->getMyHero(), attacker);
  225. EXPECT_EQ(att->getMyHero(), attacker) << att->nodeName();
  226. EXPECT_EQ(def->getMyHero(), defender) << def->nodeName();
  227. {
  228. using namespace ::testing;
  229. spells::ProblemMock problemMock;
  230. // EXPECT_CALL(problemMock, add(_));
  231. const CSpell * age = SpellID(SpellID::AGE).toSpell();
  232. ASSERT_NE(age, nullptr);
  233. spells::AbilityCaster caster(att, 3);
  234. //here tested ballista, but this applied to all war machines
  235. spells::BattleCast cast(gameState->currentBattles.front().get(), &caster, spells::Mode::PASSIVE, age);
  236. spells::Target target;
  237. target.emplace_back(def);
  238. auto m = age->battleMechanics(&cast);
  239. EXPECT_FALSE(m->canBeCastAt(target, problemMock));
  240. EXPECT_TRUE(cast.castIfPossible(this, target));//should be possible, but with no effect (change to aimed cast check?)
  241. EXPECT_TRUE(def->activeSpells().empty());
  242. }
  243. }
  244. TEST_F(CGameStateTest, battleResurrection)
  245. {
  246. startTestGame();
  247. auto attackerID = map->getHeroesOnMap()[0];
  248. auto defenderID = map->getHeroesOnMap()[1];
  249. auto attacker = dynamic_cast<CGHeroInstance *>(map->getObject(attackerID));
  250. auto defender = dynamic_cast<CGHeroInstance *>(map->getObject(defenderID));
  251. ASSERT_NE(attacker->tempOwner, defender->tempOwner);
  252. attacker->setSecSkillLevel(SecondarySkill::EARTH_MAGIC, 3, ChangeValueMode::ABSOLUTE);
  253. attacker->addSpellToSpellbook(SpellID::RESURRECTION);
  254. attacker->setPrimarySkill(PrimarySkill::SPELL_POWER, 100, ChangeValueMode::ABSOLUTE);
  255. attacker->setPrimarySkill(PrimarySkill::KNOWLEDGE, 20, ChangeValueMode::ABSOLUTE);
  256. attacker->mana = attacker->manaLimit();
  257. {
  258. NewArtifact na;
  259. na.artHolder = attacker->id;
  260. na.artId = ArtifactID::SPELLBOOK;
  261. na.pos = ArtifactPosition::SPELLBOOK;
  262. gameEventCallback->sendAndApply(na);
  263. }
  264. startTestBattle(attacker, defender);
  265. uint32_t unitId = gameState->currentBattles.front()->battleNextUnitId();
  266. {
  267. battle::UnitInfo info;
  268. info.id = unitId;
  269. info.count = 10;
  270. info.type = CreatureID(13);
  271. info.side = BattleSide::ATTACKER;
  272. info.position = gameState->currentBattles.front()->getAvailableHex(info.type, info.side);
  273. info.summoned = false;
  274. BattleUnitsChanged pack;
  275. pack.battleID = BattleID(0);
  276. pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
  277. info.save(pack.changedStacks.back().data);
  278. gameEventCallback->sendAndApply(pack);
  279. }
  280. {
  281. battle::UnitInfo info;
  282. info.id = gameState->currentBattles.front()->battleNextUnitId();
  283. info.count = 10;
  284. info.type = CreatureID(13);
  285. info.side = BattleSide::DEFENDER;
  286. info.position = gameState->currentBattles.front()->getAvailableHex(info.type, info.side);
  287. info.summoned = false;
  288. BattleUnitsChanged pack;
  289. pack.battleID = BattleID(0);
  290. pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD);
  291. info.save(pack.changedStacks.back().data);
  292. gameEventCallback->sendAndApply(pack);
  293. }
  294. CStack * unit = gameState->currentBattles.front()->getStack(unitId);
  295. ASSERT_NE(unit, nullptr);
  296. int64_t damage = unit->getMaxHealth() + 1;
  297. unit->damage(damage);
  298. EXPECT_EQ(unit->getCount(), 9);
  299. {
  300. using namespace ::testing;
  301. spells::ProblemMock problemMock;
  302. EXPECT_CALL(problemMock, add(_)).Times(AnyNumber()); //todo: do smth with problems of optional effects
  303. const CSpell * spell = SpellID(SpellID::RESURRECTION).toSpell();
  304. ASSERT_NE(spell, nullptr);
  305. spells::BattleCast cast(gameState->currentBattles.front().get(), attacker, spells::Mode::HERO, spell);
  306. spells::Target target;
  307. target.emplace_back(unit);
  308. auto m = spell->battleMechanics(&cast);
  309. EXPECT_TRUE(m->canBeCast(problemMock));
  310. EXPECT_TRUE(m->canBeCastAt(target, problemMock));
  311. cast.cast(this, target);
  312. //
  313. // std::vector<std::string> expLog;
  314. //
  315. // EXPECT_THAT(problemMock.log, ContainerEq(expLog));
  316. }
  317. EXPECT_EQ(unit->health.getCount(), 10);
  318. EXPECT_EQ(unit->health.getResurrected(), 0);
  319. }
  320. TEST_F(CGameStateTest, battleInterference)
  321. {
  322. static const char skillText[] = R"(
  323. {
  324. "type" : "PRIMARY_SKILL",
  325. "subtype" : "spellpower",
  326. "val" : -10,
  327. "valueType" : "PERCENT_TO_ALL",
  328. "propagator" : "BATTLE_WIDE",
  329. "sourceType" : "SECONDARY_SKILL",
  330. "sourceID" : "wisdom",
  331. "propagationUpdater" : "BONUS_OWNER_UPDATER",
  332. "limiters" : [ "OPPOSITE_SIDE" ]
  333. }
  334. )";
  335. static const char specialtyText[] = R"(
  336. {
  337. "type" : "PRIMARY_SKILL",
  338. "subtype" : "spellpower",
  339. "val" : 5,
  340. "sourceType" : "HERO_SPECIAL",
  341. "sourceID" : "lordHaart",
  342. "targetSourceType" : "SECONDARY_SKILL",
  343. "valueType" : "PERCENT_TO_TARGET_TYPE",
  344. "propagator" : "BATTLE_WIDE",
  345. "propagationUpdater" : [ "TIMES_HERO_LEVEL", "BONUS_OWNER_UPDATER" ]
  346. "limiters" : [ "OPPOSITE_SIDE" ]
  347. }
  348. )";
  349. JsonNode skillJson(skillText, std::size(skillText), "testBattleInterferenceSkillText");
  350. JsonNode specialtyJson(specialtyText, std::size(specialtyText), "testBattleInterferenceSpecialtyTextA");
  351. skillJson.setModScope(ModScope::scopeGame());
  352. specialtyJson.setModScope(ModScope::scopeGame());
  353. auto skillBonus = JsonUtils::parseBonus(skillJson);
  354. auto specialtyBonus = JsonUtils::parseBonus(specialtyJson);
  355. startTestGame();
  356. auto attackerID = map->getHeroesOnMap()[0];
  357. auto defenderID = map->getHeroesOnMap()[1];
  358. auto attacker = dynamic_cast<CGHeroInstance *>(map->getObject(attackerID));
  359. auto defender = dynamic_cast<CGHeroInstance *>(map->getObject(defenderID));
  360. ASSERT_NE(attacker->tempOwner, defender->tempOwner);
  361. attacker->setPrimarySkill(PrimarySkill::SPELL_POWER, 100, ChangeValueMode::ABSOLUTE);
  362. attacker->addNewBonus(skillBonus);
  363. attacker->addNewBonus(specialtyBonus);
  364. attacker->level = 20;
  365. defender->setPrimarySkill(PrimarySkill::SPELL_POWER, 100, ChangeValueMode::ABSOLUTE);
  366. defender->level = 10;
  367. startTestBattle(attacker, defender);
  368. EXPECT_EQ(attacker->getPrimSkillLevel(PrimarySkill::SPELL_POWER), 100);
  369. EXPECT_EQ(defender->getPrimSkillLevel(PrimarySkill::SPELL_POWER), 80);
  370. }