CRewardableObject.cpp 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150
  1. /*
  2. *
  3. * CRewardableObject.cpp, part of VCMI engine
  4. *
  5. * Authors: listed in file AUTHORS in main folder
  6. *
  7. * License: GNU General Public License v2.0 or later
  8. * Full text of license available in license.txt file, in main folder
  9. *
  10. */
  11. #include "StdInc.h"
  12. #include "CRewardableObject.h"
  13. #include "../CHeroHandler.h"
  14. #include "../CGeneralTextHandler.h"
  15. #include "../CSoundBase.h"
  16. #include "../NetPacks.h"
  17. #include "../IGameCallback.h"
  18. #include "../CGameState.h"
  19. #include "../CPlayerState.h"
  20. #include "CObjectClassesHandler.h"
  21. bool CRewardLimiter::heroAllowed(const CGHeroInstance * hero) const
  22. {
  23. if(dayOfWeek != 0)
  24. {
  25. if (IObjectInterface::cb->getDate(Date::DAY_OF_WEEK) != dayOfWeek)
  26. return false;
  27. }
  28. for(auto & reqStack : creatures)
  29. {
  30. size_t count = 0;
  31. for (auto slot : hero->Slots())
  32. {
  33. const CStackInstance * heroStack = slot.second;
  34. if (heroStack->type == reqStack.type)
  35. count += heroStack->count;
  36. }
  37. if (count < reqStack.count) //not enough creatures of this kind
  38. return false;
  39. }
  40. if(!IObjectInterface::cb->getPlayer(hero->tempOwner)->resources.canAfford(resources))
  41. return false;
  42. if(minLevel > hero->level)
  43. return false;
  44. for(size_t i=0; i<primary.size(); i++)
  45. {
  46. if (primary[i] > hero->getPrimSkillLevel(PrimarySkill::PrimarySkill(i)))
  47. return false;
  48. }
  49. for(auto & skill : secondary)
  50. {
  51. if (skill.second > hero->getSecSkillLevel(skill.first))
  52. return false;
  53. }
  54. for(auto & art : artifacts)
  55. {
  56. if (!hero->hasArt(art))
  57. return false;
  58. }
  59. return true;
  60. }
  61. std::vector<ui32> CRewardableObject::getAvailableRewards(const CGHeroInstance * hero) const
  62. {
  63. std::vector<ui32> ret;
  64. for(size_t i=0; i<info.size(); i++)
  65. {
  66. const CVisitInfo & visit = info[i];
  67. if((visit.limiter.numOfGrants == 0 || visit.numOfGrants < visit.limiter.numOfGrants) // reward has unlimited uses or some are still available
  68. && visit.limiter.heroAllowed(hero))
  69. {
  70. logGlobal->debugStream() << "Reward " << i << " is allowed";
  71. ret.push_back(i);
  72. }
  73. }
  74. return ret;
  75. }
  76. CVisitInfo CRewardableObject::getVisitInfo(int index, const CGHeroInstance *) const
  77. {
  78. return info[index];
  79. }
  80. void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
  81. {
  82. auto grantRewardWithMessage = [&](int index) -> void
  83. {
  84. auto vi = getVisitInfo(index, h);
  85. logGlobal->debugStream() << "Granting reward " << index << ". Message says: " << vi.message.toString();
  86. // show message only if it is not empty
  87. if (!vi.message.toString().empty())
  88. {
  89. InfoWindow iw;
  90. iw.player = h->tempOwner;
  91. iw.soundID = soundID;
  92. iw.text = vi.message;
  93. vi.reward.loadComponents(iw.components, h);
  94. cb->showInfoDialog(&iw);
  95. }
  96. // grant reward afterwards. Note that it may remove object
  97. grantReward(index, h);
  98. };
  99. auto selectRewardsMessage = [&](std::vector<ui32> rewards) -> void
  100. {
  101. BlockingDialog sd(canRefuse, rewards.size() > 1);
  102. sd.player = h->tempOwner;
  103. sd.soundID = soundID;
  104. sd.text = onSelect;
  105. for (auto index : rewards)
  106. sd.components.push_back(getVisitInfo(index, h).reward.getDisplayedComponent(h));
  107. cb->showBlockingDialog(&sd);
  108. };
  109. if(!wasVisited(h))
  110. {
  111. auto rewards = getAvailableRewards(h);
  112. logGlobal->debugStream() << "Visiting object with " << rewards.size() << " possible rewards";
  113. switch (rewards.size())
  114. {
  115. case 0: // no available rewards, e.g. empty flotsam
  116. {
  117. InfoWindow iw;
  118. iw.player = h->tempOwner;
  119. iw.soundID = soundID;
  120. if (!onEmpty.toString().empty())
  121. iw.text = onEmpty;
  122. else
  123. iw.text = onVisited;
  124. cb->showInfoDialog(&iw);
  125. break;
  126. }
  127. case 1: // one reward. Just give it with message
  128. {
  129. if (canRefuse)
  130. selectRewardsMessage(rewards);
  131. else
  132. grantRewardWithMessage(rewards[0]);
  133. break;
  134. }
  135. default: // multiple rewards. Act according to select mode
  136. {
  137. switch (selectMode) {
  138. case SELECT_PLAYER: // player must select
  139. selectRewardsMessage(rewards);
  140. break;
  141. case SELECT_FIRST: // give first available
  142. grantRewardWithMessage(rewards[0]);
  143. break;
  144. case SELECT_RANDOM: // select one randomly //TODO: use weights
  145. grantRewardWithMessage(rewards[CRandomGenerator::getDefault().nextInt(rewards.size()-1)]);
  146. break;
  147. }
  148. break;
  149. }
  150. }
  151. if (getAvailableRewards(h).size() == 0)
  152. {
  153. ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id);
  154. cb->sendAndApply(&cov);
  155. }
  156. }
  157. else
  158. {
  159. logGlobal->debugStream() << "Revisiting already visited object";
  160. InfoWindow iw;
  161. iw.player = h->tempOwner;
  162. iw.soundID = soundID;
  163. if (!onVisited.toString().empty())
  164. iw.text = onVisited;
  165. else
  166. iw.text = onEmpty;
  167. cb->showInfoDialog(&iw);
  168. }
  169. }
  170. void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const
  171. {
  172. grantRewardAfterLevelup(getVisitInfo(selectedReward, hero), hero);
  173. }
  174. void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const
  175. {
  176. if(answer == 0)
  177. return; // player refused
  178. if(answer > 0 && answer-1 < info.size())
  179. {
  180. auto list = getAvailableRewards(hero);
  181. grantReward(list[answer - 1], hero);
  182. }
  183. else
  184. {
  185. throw std::runtime_error("Unhandled choice");
  186. }
  187. }
  188. void CRewardableObject::onRewardGiven(const CGHeroInstance * hero) const
  189. {
  190. // no implementation, virtual function for overrides
  191. }
  192. void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const
  193. {
  194. ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id);
  195. cb->sendAndApply(&cov);
  196. cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID);
  197. grantRewardBeforeLevelup(getVisitInfo(rewardID, hero), hero);
  198. }
  199. void CRewardableObject::grantRewardBeforeLevelup(const CVisitInfo & info, const CGHeroInstance * hero) const
  200. {
  201. assert(hero);
  202. assert(hero->tempOwner.isValidPlayer());
  203. assert(stacks.empty());
  204. assert(info.reward.creatures.size() <= GameConstants::ARMY_SIZE);
  205. assert(!cb->isVisitCoveredByAnotherQuery(this, hero));
  206. cb->giveResources(hero->tempOwner, info.reward.resources);
  207. for(auto & entry : info.reward.secondary)
  208. {
  209. int current = hero->getSecSkillLevel(entry.first);
  210. if( (current != 0 && current < entry.second) ||
  211. (hero->canLearnSkill() ))
  212. {
  213. cb->changeSecSkill(hero, entry.first, entry.second);
  214. }
  215. }
  216. for(int i=0; i< info.reward.primary.size(); i++)
  217. if(info.reward.primary[i] > 0)
  218. cb->changePrimSkill(hero, static_cast<PrimarySkill::PrimarySkill>(i), info.reward.primary[i], false);
  219. si64 expToGive = 0;
  220. expToGive += VLC->heroh->reqExp(hero->level+info.reward.gainedLevels) - VLC->heroh->reqExp(hero->level);
  221. expToGive += hero->calculateXp(info.reward.gainedExp);
  222. if(expToGive)
  223. cb->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive);
  224. // hero is not blocked by levelup dialog - grant remainer immediately
  225. if(!cb->isVisitCoveredByAnotherQuery(this, hero))
  226. {
  227. grantRewardAfterLevelup(info, hero);
  228. }
  229. }
  230. void CRewardableObject::grantRewardAfterLevelup(const CVisitInfo & info, const CGHeroInstance * hero) const
  231. {
  232. if(info.reward.manaDiff || info.reward.manaPercentage >= 0)
  233. {
  234. si32 mana = hero->mana;
  235. if (info.reward.manaPercentage >= 0)
  236. mana = hero->manaLimit() * info.reward.manaPercentage / 100;
  237. cb->setManaPoints(hero->id, mana + info.reward.manaDiff);
  238. }
  239. if(info.reward.movePoints || info.reward.movePercentage >= 0)
  240. {
  241. SetMovePoints smp;
  242. smp.hid = hero->id;
  243. smp.val = hero->movement;
  244. if (info.reward.movePercentage >= 0) // percent from max
  245. smp.val = hero->maxMovePoints(hero->boat != nullptr) * info.reward.movePercentage / 100;
  246. smp.val = std::max<si32>(0, smp.val + info.reward.movePoints);
  247. cb->setMovePoints(&smp);
  248. }
  249. for(const Bonus & bonus : info.reward.bonuses)
  250. {
  251. assert(bonus.source == Bonus::OBJECT);
  252. assert(bonus.sid == ID);
  253. GiveBonus gb;
  254. gb.who = GiveBonus::HERO;
  255. gb.bonus = bonus;
  256. gb.id = hero->id.getNum();
  257. cb->giveHeroBonus(&gb);
  258. }
  259. for(ArtifactID art : info.reward.artifacts)
  260. cb->giveHeroNewArtifact(hero, VLC->arth->artifacts[art],ArtifactPosition::FIRST_AVAILABLE);
  261. if(!info.reward.spells.empty())
  262. {
  263. std::set<SpellID> spellsToGive(info.reward.spells.begin(), info.reward.spells.end());
  264. cb->changeSpells(hero, true, spellsToGive);
  265. }
  266. if(!info.reward.creatures.empty())
  267. {
  268. CCreatureSet creatures;
  269. for (auto & crea : info.reward.creatures)
  270. creatures.addToSlot(creatures.getFreeSlot(), new CStackInstance(crea.type, crea.count));
  271. cb->giveCreatures(this, hero, creatures, false);
  272. }
  273. onRewardGiven(hero);
  274. if(info.reward.removeObject)
  275. cb->removeObject(this);
  276. }
  277. bool CRewardableObject::wasVisited(PlayerColor player) const
  278. {
  279. switch (visitMode)
  280. {
  281. case VISIT_UNLIMITED:
  282. case VISIT_BONUS:
  283. return false;
  284. case VISIT_ONCE:
  285. return vstd::contains(cb->getPlayer(player)->visitedObjects, ObjectInstanceID(id));
  286. case VISIT_HERO:
  287. return false;
  288. case VISIT_PLAYER:
  289. return vstd::contains(cb->getPlayer(player)->visitedObjects, ObjectInstanceID(id));
  290. default:
  291. return false;
  292. }
  293. }
  294. bool CRewardableObject::wasVisited(const CGHeroInstance * h) const
  295. {
  296. switch (visitMode)
  297. {
  298. case VISIT_UNLIMITED:
  299. return false;
  300. case VISIT_BONUS:
  301. return h->hasBonusFrom(Bonus::OBJECT, ID);
  302. case VISIT_HERO:
  303. return h->visitedObjects.count(ObjectInstanceID(id));
  304. default:
  305. return wasVisited(h->tempOwner);
  306. }
  307. }
  308. void CRewardInfo::loadComponents(std::vector<Component> & comps,
  309. const CGHeroInstance * h) const
  310. {
  311. for (auto comp : extraComponents)
  312. comps.push_back(comp);
  313. if (gainedExp)
  314. {
  315. comps.push_back(Component(
  316. Component::EXPERIENCE, 0, h->calculateXp(gainedExp), 0));
  317. }
  318. if (gainedLevels) comps.push_back(Component(Component::EXPERIENCE, 0, gainedLevels, 0));
  319. if (manaDiff) comps.push_back(Component(Component::PRIM_SKILL, 5, manaDiff, 0));
  320. for (size_t i=0; i<primary.size(); i++)
  321. {
  322. if (primary[i] != 0)
  323. comps.push_back(Component(Component::PRIM_SKILL, i, primary[i], 0));
  324. }
  325. for (auto & entry : secondary)
  326. comps.push_back(Component(Component::SEC_SKILL, entry.first, entry.second, 0));
  327. for (auto & entry : artifacts)
  328. comps.push_back(Component(Component::ARTIFACT, entry, 1, 0));
  329. for (auto & entry : spells)
  330. comps.push_back(Component(Component::SPELL, entry, 1, 0));
  331. for (auto & entry : creatures)
  332. comps.push_back(Component(Component::CREATURE, entry.type->idNumber, entry.count, 0));
  333. for (size_t i=0; i<resources.size(); i++)
  334. {
  335. if (resources[i] !=0)
  336. comps.push_back(Component(Component::RESOURCE, i, resources[i], 0));
  337. }
  338. }
  339. Component CRewardInfo::getDisplayedComponent(const CGHeroInstance * h) const
  340. {
  341. std::vector<Component> comps;
  342. loadComponents(comps, h);
  343. assert(!comps.empty());
  344. return comps.front();
  345. }
  346. // FIXME: copy-pasted from CObjectHandler
  347. static std::string & visitedTxt(const bool visited)
  348. {
  349. int id = visited ? 352 : 353;
  350. return VLC->generaltexth->allTexts[id];
  351. }
  352. std::string CRewardableObject::getHoverText(PlayerColor player) const
  353. {
  354. if(visitMode == VISIT_PLAYER || visitMode == VISIT_ONCE)
  355. return getObjectName() + " " + visitedTxt(wasVisited(player));
  356. return getObjectName();
  357. }
  358. std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const
  359. {
  360. if(visitMode != VISIT_UNLIMITED)
  361. return getObjectName() + " " + visitedTxt(wasVisited(hero));
  362. return getObjectName();
  363. }
  364. void CRewardableObject::setPropertyDer(ui8 what, ui32 val)
  365. {
  366. switch (what)
  367. {
  368. case ObjProperty::REWARD_RESET:
  369. for (auto & visit : info)
  370. visit.numOfGrants = 0;
  371. break;
  372. case ObjProperty::REWARD_SELECT:
  373. selectedReward = val;
  374. info[val].numOfGrants++;
  375. break;
  376. }
  377. }
  378. void CRewardableObject::newTurn(CRandomGenerator & rand) const
  379. {
  380. if (resetDuration != 0 && cb->getDate(Date::DAY) > 1 && (cb->getDate(Date::DAY) % resetDuration) == 1)
  381. cb->setObjProperty(id, ObjProperty::REWARD_RESET, 0);
  382. }
  383. CRewardableObject::CRewardableObject():
  384. soundID(soundBase::invalid),
  385. selectMode(0),
  386. selectedReward(0),
  387. resetDuration(0),
  388. canRefuse(false)
  389. {}
  390. ///////////////////////////////////////////////////////////////////////////////////////////////////
  391. /// END OF CODE FOR CREWARDABLEOBJECT AND RELATED CLASSES ///
  392. ///////////////////////////////////////////////////////////////////////////////////////////////////
  393. /// Helper, selects random art class based on weights
  394. static int selectRandomArtClass(CRandomGenerator & rand, int treasure, int minor, int major, int relic)
  395. {
  396. int total = treasure + minor + major + relic;
  397. assert(total != 0);
  398. int hlp = rand.nextInt(total - 1);
  399. if(hlp < treasure)
  400. return CArtifact::ART_TREASURE;
  401. if(hlp < treasure + minor)
  402. return CArtifact::ART_MINOR;
  403. if(hlp < treasure + minor + major)
  404. return CArtifact::ART_MAJOR;
  405. return CArtifact::ART_RELIC;
  406. }
  407. /// Helper, adds random artifact to reward selecting class based on weights
  408. static void loadRandomArtifact(CRandomGenerator & rand, CVisitInfo & info, int treasure, int minor, int major, int relic)
  409. {
  410. int artClass = selectRandomArtClass(rand, treasure, minor, major, relic);
  411. ArtifactID artID = VLC->arth->pickRandomArtifact(rand, artClass);
  412. info.reward.artifacts.push_back(artID);
  413. }
  414. CGPickable::CGPickable()
  415. {
  416. visitMode = VISIT_UNLIMITED;
  417. selectMode = SELECT_PLAYER;
  418. }
  419. void CGPickable::initObj(CRandomGenerator & rand)
  420. {
  421. blockVisit = true;
  422. switch(ID)
  423. {
  424. case Obj::CAMPFIRE:
  425. {
  426. soundID = soundBase::experience;
  427. int givenRes = rand.nextInt(5);
  428. int givenAmm = rand.nextInt(4, 6);
  429. info.resize(1);
  430. info[0].reward.resources[givenRes] = givenAmm;
  431. info[0].reward.resources[Res::GOLD]= givenAmm * 100;
  432. info[0].message.addTxt(MetaString::ADVOB_TXT,23);
  433. info[0].reward.removeObject = true;
  434. break;
  435. }
  436. case Obj::FLOTSAM:
  437. {
  438. int type = rand.nextInt(3);
  439. soundID = soundBase::GENIE;
  440. switch(type)
  441. {
  442. case 0:
  443. info.resize(1);
  444. info[0].message.addTxt(MetaString::ADVOB_TXT, 51);
  445. info[0].reward.removeObject = true;
  446. break;
  447. case 1:
  448. {
  449. info.resize(1);
  450. info[0].reward.resources[Res::WOOD] = 5;
  451. info[0].message.addTxt(MetaString::ADVOB_TXT, 52);
  452. info[0].reward.removeObject = true;
  453. break;
  454. }
  455. case 2:
  456. {
  457. info.resize(1);
  458. info[0].reward.resources[Res::WOOD] = 5;
  459. info[0].reward.resources[Res::GOLD] = 200;
  460. info[0].message.addTxt(MetaString::ADVOB_TXT, 53);
  461. info[0].reward.removeObject = true;
  462. break;
  463. }
  464. case 3:
  465. {
  466. info.resize(1);
  467. info[0].reward.resources[Res::WOOD] = 10;
  468. info[0].reward.resources[Res::GOLD] = 500;
  469. info[0].message.addTxt(MetaString::ADVOB_TXT, 54);
  470. info[0].reward.removeObject = true;
  471. break;
  472. }
  473. }
  474. break;
  475. }
  476. case Obj::SEA_CHEST:
  477. {
  478. soundID = soundBase::chest;
  479. int hlp = rand.nextInt(99);
  480. if(hlp < 20)
  481. {
  482. info.resize(1);
  483. info[0].message.addTxt(MetaString::ADVOB_TXT, 116);
  484. info[0].reward.removeObject = true;
  485. }
  486. else if(hlp < 90)
  487. {
  488. info.resize(1);
  489. info[0].reward.resources[Res::GOLD] = 1500;
  490. info[0].message.addTxt(MetaString::ADVOB_TXT, 118);
  491. info[0].reward.removeObject = true;
  492. }
  493. else
  494. {
  495. info.resize(1);
  496. loadRandomArtifact(rand, info[0], 100, 0, 0, 0);
  497. info[0].reward.resources[Res::GOLD] = 1000;
  498. info[0].message.addTxt(MetaString::ADVOB_TXT, 117);
  499. info[0].message.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
  500. info[0].reward.removeObject = true;
  501. }
  502. }
  503. break;
  504. case Obj::SHIPWRECK_SURVIVOR:
  505. {
  506. soundID = soundBase::experience;
  507. info.resize(1);
  508. loadRandomArtifact(rand, info[0], 55, 20, 20, 5);
  509. info[0].message.addTxt(MetaString::ADVOB_TXT, 125);
  510. info[0].message.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
  511. info[0].reward.removeObject = true;
  512. }
  513. break;
  514. case Obj::TREASURE_CHEST:
  515. {
  516. int hlp = rand.nextInt(99);
  517. if(hlp >= 95)
  518. {
  519. soundID = soundBase::treasure;
  520. info.resize(1);
  521. loadRandomArtifact(rand, info[0], 100, 0, 0, 0);
  522. info[0].message.addTxt(MetaString::ADVOB_TXT,145);
  523. info[0].message.addReplacement(MetaString::ART_NAMES, info[0].reward.artifacts.back());
  524. info[0].reward.removeObject = true;
  525. return;
  526. }
  527. else if (hlp >= 65)
  528. {
  529. soundID = soundBase::chest;
  530. onSelect.addTxt(MetaString::ADVOB_TXT,146);
  531. info.resize(2);
  532. info[0].reward.resources[Res::GOLD] = 2000;
  533. info[1].reward.gainedExp = 1500;
  534. info[0].reward.removeObject = true;
  535. info[1].reward.removeObject = true;
  536. }
  537. else if(hlp >= 33)
  538. {
  539. soundID = soundBase::chest;
  540. onSelect.addTxt(MetaString::ADVOB_TXT,146);
  541. info.resize(2);
  542. info[0].reward.resources[Res::GOLD] = 1500;
  543. info[1].reward.gainedExp = 1000;
  544. info[0].reward.removeObject = true;
  545. info[1].reward.removeObject = true;
  546. }
  547. else
  548. {
  549. soundID = soundBase::chest;
  550. onSelect.addTxt(MetaString::ADVOB_TXT,146);
  551. info.resize(2);
  552. info[0].reward.resources[Res::GOLD] = 1000;
  553. info[1].reward.gainedExp = 500;
  554. info[0].reward.removeObject = true;
  555. info[1].reward.removeObject = true;
  556. }
  557. }
  558. break;
  559. }
  560. }
  561. ///////////////////////////////////////////////////////////////////////////////////////////////////
  562. CGBonusingObject::CGBonusingObject()
  563. {
  564. visitMode = VISIT_BONUS;
  565. selectMode = SELECT_FIRST;
  566. }
  567. void CGBonusingObject::initObj(CRandomGenerator & rand)
  568. {
  569. auto configureBonusDuration = [&](CVisitInfo & visit, Bonus::BonusDuration duration, Bonus::BonusType type, si32 value, si32 descrID)
  570. {
  571. Bonus b(duration, type, Bonus::OBJECT, value, ID, descrID != 0 ? VLC->generaltexth->advobtxt[descrID] : "");
  572. visit.reward.bonuses.push_back(b);
  573. if (type == Bonus::MORALE)
  574. visit.reward.extraComponents.push_back(Component(Component::MORALE, 0, value, 0));
  575. if (type == Bonus::LUCK)
  576. visit.reward.extraComponents.push_back(Component(Component::LUCK, 0, value, 0));
  577. };
  578. auto configureBonus = [&](CVisitInfo & visit, Bonus::BonusType type, si32 value, si32 descrID)
  579. {
  580. configureBonusDuration(visit, Bonus::ONE_BATTLE, type, value, descrID);
  581. };
  582. auto configureMessage = [&](CVisitInfo & visit, int onGrantID, int onVisitedID, soundBase::soundID sound)
  583. {
  584. visit.message.addTxt(MetaString::ADVOB_TXT, onGrantID);
  585. onVisited.addTxt(MetaString::ADVOB_TXT, onVisitedID);
  586. soundID = sound;
  587. };
  588. info.resize(1);
  589. switch(ID)
  590. {
  591. case Obj::BUOY:
  592. blockVisit = true;
  593. configureMessage(info[0], 21, 22, soundBase::MORALE);
  594. configureBonus(info[0], Bonus::MORALE, +1, 94);
  595. break;
  596. case Obj::SWAN_POND:
  597. configureMessage(info[0], 29, 30, soundBase::LUCK);
  598. configureBonus(info[0], Bonus::LUCK, 2, 67);
  599. info[0].reward.movePercentage = 0;
  600. break;
  601. case Obj::FAERIE_RING:
  602. configureMessage(info[0], 49, 50, soundBase::LUCK);
  603. configureBonus(info[0], Bonus::LUCK, 1, 71);
  604. break;
  605. case Obj::FOUNTAIN_OF_FORTUNE:
  606. selectMode = SELECT_RANDOM;
  607. info.resize(5);
  608. for (int i=0; i<5; i++)
  609. {
  610. configureBonus(info[i], Bonus::LUCK, i-1, 69); //NOTE: description have %d that should be replaced with value
  611. info[i].message.addTxt(MetaString::ADVOB_TXT, 55);
  612. soundID = soundBase::LUCK;
  613. }
  614. onVisited.addTxt(MetaString::ADVOB_TXT, 56);
  615. break;
  616. case Obj::IDOL_OF_FORTUNE:
  617. info.resize(7);
  618. for (int i=0; i<6; i++)
  619. {
  620. info[i].limiter.dayOfWeek = i+1;
  621. configureBonus(info[i], (i%2) ? Bonus::MORALE : Bonus::LUCK, 1, 68);
  622. info[i].message.addTxt(MetaString::ADVOB_TXT, 62);
  623. soundID = soundBase::experience;
  624. }
  625. info.back().limiter.dayOfWeek = 7;
  626. configureBonus(info.back(), Bonus::MORALE, 1, 68); // on last day of week
  627. configureBonus(info.back(), Bonus::LUCK, 1, 68);
  628. configureMessage(info.back(), 62, 63, soundBase::experience);
  629. break;
  630. case Obj::MERMAID:
  631. blockVisit = true;
  632. configureMessage(info[0], 83, 82, soundBase::LUCK);
  633. configureBonus(info[0], Bonus::LUCK, 1, 72);
  634. break;
  635. case Obj::RALLY_FLAG:
  636. configureMessage(info[0], 111, 110, soundBase::MORALE);
  637. configureBonus(info[0], Bonus::MORALE, 1, 102);
  638. configureBonus(info[0], Bonus::LUCK, 1, 102);
  639. info[0].reward.movePoints = 400;
  640. break;
  641. case Obj::OASIS:
  642. configureMessage(info[0], 95, 94, soundBase::MORALE);
  643. configureBonus(info[0], Bonus::MORALE, 1, 95);
  644. info[0].reward.movePoints = 800;
  645. break;
  646. case Obj::TEMPLE:
  647. info[0].limiter.dayOfWeek = 7;
  648. info.resize(2);
  649. configureBonus(info[0], Bonus::MORALE, 2, 96);
  650. configureBonus(info[1], Bonus::MORALE, 1, 97);
  651. info[0].message.addTxt(MetaString::ADVOB_TXT, 140);
  652. info[1].message.addTxt(MetaString::ADVOB_TXT, 140);
  653. onVisited.addTxt(MetaString::ADVOB_TXT, 141);
  654. soundID = soundBase::temple;
  655. break;
  656. case Obj::WATERING_HOLE:
  657. configureMessage(info[0], 166, 167, soundBase::MORALE);
  658. configureBonus(info[0], Bonus::MORALE, 1, 100);
  659. info[0].reward.movePoints = 400;
  660. break;
  661. case Obj::FOUNTAIN_OF_YOUTH:
  662. configureMessage(info[0], 57, 58, soundBase::MORALE);
  663. configureBonus(info[0], Bonus::MORALE, 1, 103);
  664. info[0].reward.movePoints = 400;
  665. break;
  666. case Obj::STABLES:
  667. configureMessage(info[0], 137, 136, soundBase::STORE);
  668. configureBonusDuration(info[0], Bonus::ONE_WEEK, Bonus::LAND_MOVEMENT, 400, 0);
  669. info[0].reward.movePoints = 400;
  670. break;
  671. }
  672. }
  673. CVisitInfo CGBonusingObject::getVisitInfo(int index, const CGHeroInstance *h) const
  674. {
  675. if(ID == Obj::STABLES)
  676. {
  677. assert(index == 0);
  678. for(auto& slot : h->Slots())
  679. {
  680. if(slot.second->type->idNumber == CreatureID::CAVALIER)
  681. {
  682. CVisitInfo vi(info[0]);
  683. vi.message.clear();
  684. vi.message.addTxt(MetaString::ADVOB_TXT, 138);
  685. vi.reward.extraComponents.push_back(Component(
  686. Component::CREATURE, CreatureID::CHAMPION, 0, 1));
  687. return std::move(vi);
  688. }
  689. }
  690. }
  691. return info[index];
  692. }
  693. void CGBonusingObject::onHeroVisit(const CGHeroInstance *h) const
  694. {
  695. CRewardableObject::onHeroVisit(h);
  696. if(ID == Obj::STABLES)
  697. {
  698. //regardless of whether this hero visited stables or not, cavaliers must be upgraded
  699. for(auto& slot : h->Slots())
  700. {
  701. if(slot.second->type->idNumber == CreatureID::CAVALIER)
  702. {
  703. cb->changeStackType(StackLocation(h, slot.first),
  704. VLC->creh->creatures[CreatureID::CHAMPION]);
  705. }
  706. }
  707. }
  708. }
  709. bool CGBonusingObject::wasVisited(const CGHeroInstance * h) const
  710. {
  711. if(ID == Obj::STABLES)
  712. {
  713. for(auto& slot : h->Slots())
  714. {
  715. if(slot.second->type->idNumber == CreatureID::CAVALIER)
  716. {
  717. // always display the reward message if the hero got cavaliers
  718. return false;
  719. }
  720. }
  721. }
  722. return CRewardableObject::wasVisited(h);
  723. }
  724. void CGBonusingObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const
  725. {
  726. if(ID == Obj::STABLES && CRewardableObject::wasVisited(hero))
  727. {
  728. // reward message has been displayed - do not give the actual bonus
  729. return;
  730. }
  731. CRewardableObject::grantReward(rewardID, hero);
  732. }
  733. ///////////////////////////////////////////////////////////////////////////////////////////////////
  734. CGOnceVisitable::CGOnceVisitable()
  735. {
  736. visitMode = VISIT_ONCE;
  737. selectMode = SELECT_FIRST;
  738. }
  739. void CGOnceVisitable::initObj(CRandomGenerator & rand)
  740. {
  741. switch(ID)
  742. {
  743. case Obj::CORPSE:
  744. {
  745. onEmpty.addTxt(MetaString::ADVOB_TXT, 38);
  746. soundID = soundBase::MYSTERY;
  747. blockVisit = true;
  748. if(rand.nextInt(99) < 20)
  749. {
  750. info.resize(1);
  751. loadRandomArtifact(rand, info[0], 10, 10, 10, 0);
  752. info[0].message.addTxt(MetaString::ADVOB_TXT, 37);
  753. info[0].limiter.numOfGrants = 1;
  754. }
  755. }
  756. break;
  757. case Obj::LEAN_TO:
  758. {
  759. soundID = soundBase::GENIE;
  760. onEmpty.addTxt(MetaString::ADVOB_TXT, 65);
  761. info.resize(1);
  762. int type = rand.nextInt(5); //any basic resource without gold
  763. int value = rand.nextInt(1, 4);
  764. info[0].reward.resources[type] = value;
  765. info[0].message.addTxt(MetaString::ADVOB_TXT, 64);
  766. info[0].limiter.numOfGrants = 1;
  767. }
  768. break;
  769. case Obj::WARRIORS_TOMB:
  770. {
  771. soundID = soundBase::GRAVEYARD;
  772. onSelect.addTxt(MetaString::ADVOB_TXT, 161);
  773. info.resize(2);
  774. loadRandomArtifact(rand, info[0], 30, 50, 25, 5);
  775. Bonus bonus(Bonus::ONE_BATTLE, Bonus::MORALE, Bonus::OBJECT, -3, ID);
  776. info[0].reward.bonuses.push_back(bonus);
  777. info[1].reward.bonuses.push_back(bonus);
  778. info[0].limiter.numOfGrants = 1;
  779. info[0].message.addTxt(MetaString::ADVOB_TXT, 162);
  780. info[0].message.addReplacement(VLC->arth->artifacts[info[0].reward.artifacts.back()]->Name());
  781. info[1].message.addTxt(MetaString::ADVOB_TXT, 163);
  782. }
  783. break;
  784. case Obj::WAGON:
  785. {
  786. soundID = soundBase::GENIE;
  787. onVisited.addTxt(MetaString::ADVOB_TXT, 156);
  788. int hlp = rand.nextInt(99);
  789. if(hlp < 40) //minor or treasure art
  790. {
  791. info.resize(1);
  792. loadRandomArtifact(rand, info[0], 10, 10, 0, 0);
  793. info[0].limiter.numOfGrants = 1;
  794. info[0].message.addTxt(MetaString::ADVOB_TXT, 155);
  795. }
  796. else if(hlp < 90) //2 - 5 of non-gold resource
  797. {
  798. info.resize(1);
  799. int type = rand.nextInt(5);
  800. int value = rand.nextInt(2, 5);
  801. info[0].reward.resources[type] = value;
  802. info[0].limiter.numOfGrants = 1;
  803. info[0].message.addTxt(MetaString::ADVOB_TXT, 154);
  804. }
  805. // or nothing
  806. }
  807. break;
  808. }
  809. }
  810. ///////////////////////////////////////////////////////////////////////////////////////////////////
  811. CGVisitableOPH::CGVisitableOPH()
  812. {
  813. visitMode = VISIT_HERO;
  814. selectMode = SELECT_PLAYER;
  815. }
  816. void CGVisitableOPH::initObj(CRandomGenerator & rand)
  817. {
  818. switch(ID)
  819. {
  820. case Obj::ARENA:
  821. soundID = soundBase::NOMAD;
  822. info.resize(2);
  823. info[0].reward.primary[PrimarySkill::ATTACK] = 2;
  824. info[1].reward.primary[PrimarySkill::DEFENSE] = 2;
  825. onSelect.addTxt(MetaString::ADVOB_TXT, 0);
  826. onVisited.addTxt(MetaString::ADVOB_TXT, 1);
  827. canRefuse = true;
  828. break;
  829. case Obj::MERCENARY_CAMP:
  830. info.resize(1);
  831. info[0].reward.primary[PrimarySkill::ATTACK] = 1;
  832. soundID = soundBase::NOMAD;
  833. info[0].message.addTxt(MetaString::ADVOB_TXT, 80);
  834. onVisited.addTxt(MetaString::ADVOB_TXT, 81);
  835. break;
  836. case Obj::MARLETTO_TOWER:
  837. info.resize(1);
  838. info[0].reward.primary[PrimarySkill::DEFENSE] = 1;
  839. soundID = soundBase::NOMAD;
  840. info[0].message.addTxt(MetaString::ADVOB_TXT, 39);
  841. onVisited.addTxt(MetaString::ADVOB_TXT, 40);
  842. break;
  843. case Obj::STAR_AXIS:
  844. info.resize(1);
  845. info[0].reward.primary[PrimarySkill::SPELL_POWER] = 1;
  846. soundID = soundBase::gazebo;
  847. info[0].message.addTxt(MetaString::ADVOB_TXT, 100);
  848. onVisited.addTxt(MetaString::ADVOB_TXT, 101);
  849. break;
  850. case Obj::GARDEN_OF_REVELATION:
  851. info.resize(1);
  852. info[0].reward.primary[PrimarySkill::KNOWLEDGE] = 1;
  853. soundID = soundBase::GETPROTECTION;
  854. info[0].message.addTxt(MetaString::ADVOB_TXT, 59);
  855. onVisited.addTxt(MetaString::ADVOB_TXT, 60);
  856. break;
  857. case Obj::LEARNING_STONE:
  858. info.resize(1);
  859. info[0].reward.gainedExp = 1000;
  860. soundID = soundBase::gazebo;
  861. info[0].message.addTxt(MetaString::ADVOB_TXT, 143);
  862. onVisited.addTxt(MetaString::ADVOB_TXT, 144);
  863. break;
  864. case Obj::TREE_OF_KNOWLEDGE:
  865. soundID = soundBase::gazebo;
  866. info.resize(1);
  867. canRefuse = true;
  868. info[0].reward.gainedLevels = 1;
  869. onVisited.addTxt(MetaString::ADVOB_TXT, 147);
  870. info.resize(1);
  871. switch (rand.nextInt(2))
  872. {
  873. case 0: // free
  874. onSelect.addTxt(MetaString::ADVOB_TXT, 148);
  875. break;
  876. case 1:
  877. info[0].limiter.resources[Res::GOLD] = 2000;
  878. info[0].reward.resources[Res::GOLD] = -2000;
  879. onSelect.addTxt(MetaString::ADVOB_TXT, 149);
  880. onEmpty.addTxt(MetaString::ADVOB_TXT, 150);
  881. break;
  882. case 2:
  883. info[0].limiter.resources[Res::GEMS] = 10;
  884. info[0].reward.resources[Res::GEMS] = -10;
  885. onSelect.addTxt(MetaString::ADVOB_TXT, 151);
  886. onEmpty.addTxt(MetaString::ADVOB_TXT, 152);
  887. break;
  888. }
  889. break;
  890. case Obj::LIBRARY_OF_ENLIGHTENMENT:
  891. {
  892. onVisited.addTxt(MetaString::ADVOB_TXT, 67);
  893. onEmpty.addTxt(MetaString::ADVOB_TXT, 68);
  894. // Don't like this one but don't see any easier approach
  895. CVisitInfo visit;
  896. visit.reward.primary[PrimarySkill::ATTACK] = 2;
  897. visit.reward.primary[PrimarySkill::DEFENSE] = 2;
  898. visit.reward.primary[PrimarySkill::KNOWLEDGE] = 2;
  899. visit.reward.primary[PrimarySkill::SPELL_POWER] = 2;
  900. static_assert(SecSkillLevel::LEVELS_SIZE == 4, "Behavior of Library of Enlignment may not be correct");
  901. for (int i=0; i<SecSkillLevel::LEVELS_SIZE; i++)
  902. {
  903. visit.limiter.minLevel = 10 - i * 2;
  904. visit.limiter.secondary[SecondarySkill::DIPLOMACY] = i;
  905. visit.message.addTxt(MetaString::ADVOB_TXT, 66);
  906. info.push_back(visit);
  907. }
  908. soundID = soundBase::gazebo;
  909. break;
  910. }
  911. case Obj::SCHOOL_OF_MAGIC:
  912. info.resize(2);
  913. info[0].reward.primary[PrimarySkill::SPELL_POWER] = 1;
  914. info[1].reward.primary[PrimarySkill::KNOWLEDGE] = 1;
  915. info[0].reward.resources[Res::GOLD] = -1000;
  916. info[1].reward.resources[Res::GOLD] = -1000;
  917. onSelect.addTxt(MetaString::ADVOB_TXT, 71);
  918. onVisited.addTxt(MetaString::ADVOB_TXT, 72);
  919. onEmpty.addTxt(MetaString::ADVOB_TXT, 73);
  920. soundID = soundBase::faerie;
  921. canRefuse = true;
  922. break;
  923. case Obj::SCHOOL_OF_WAR:
  924. info.resize(2);
  925. info[0].reward.primary[PrimarySkill::ATTACK] = 1;
  926. info[1].reward.primary[PrimarySkill::DEFENSE] = 1;
  927. info[0].reward.resources[Res::GOLD] = -1000;
  928. info[1].reward.resources[Res::GOLD] = -1000;
  929. onSelect.addTxt(MetaString::ADVOB_TXT, 158);
  930. onVisited.addTxt(MetaString::ADVOB_TXT, 159);
  931. onEmpty.addTxt(MetaString::ADVOB_TXT, 160);
  932. soundID = soundBase::MILITARY;
  933. canRefuse = true;
  934. break;
  935. }
  936. }
  937. ///////////////////////////////////////////////////////////////////////////////////////////////////
  938. CGVisitableOPW::CGVisitableOPW()
  939. {
  940. visitMode = VISIT_ONCE;
  941. selectMode = SELECT_RANDOM;
  942. resetDuration = 7;
  943. }
  944. void CGVisitableOPW::initObj(CRandomGenerator & rand)
  945. {
  946. switch (ID)
  947. {
  948. case Obj::MYSTICAL_GARDEN:
  949. soundID = soundBase::experience;
  950. onEmpty.addTxt(MetaString::ADVOB_TXT, 93);
  951. info.resize(2);
  952. info[0].reward.resources[Res::GEMS] = 5;
  953. info[1].reward.resources[Res::GOLD] = 500;
  954. info[0].message.addTxt(MetaString::ADVOB_TXT, 92);
  955. info[1].message.addTxt(MetaString::ADVOB_TXT, 92);
  956. break;
  957. case Obj::WINDMILL:
  958. soundID = soundBase::GENIE;
  959. onEmpty.addTxt(MetaString::ADVOB_TXT, 169);
  960. // 3-6 of any resource but wood and gold
  961. for (int resID = Res::MERCURY; resID < Res::GOLD; resID++)
  962. {
  963. for (int val = 3; val <=6; val++)
  964. {
  965. CVisitInfo visit;
  966. visit.reward.resources[resID] = val;
  967. visit.message.addTxt(MetaString::ADVOB_TXT, 92);
  968. info.push_back(visit);
  969. }
  970. }
  971. break;
  972. case Obj::WATER_WHEEL:
  973. soundID = soundBase::GENIE;
  974. onEmpty.addTxt(MetaString::ADVOB_TXT, 165);
  975. info.resize(1);
  976. info[0].reward.resources[Res::GOLD] = 500;
  977. info[0].message.addTxt(MetaString::ADVOB_TXT, 164);
  978. break;
  979. }
  980. }
  981. void CGVisitableOPW::setPropertyDer(ui8 what, ui32 val)
  982. {
  983. if(ID == Obj::WATER_WHEEL && what == ObjProperty::REWARD_RESET)
  984. {
  985. auto& reward = info[0].reward.resources[Res::GOLD];
  986. if(info[0].numOfGrants == 0)
  987. {
  988. reward = 1000;
  989. }
  990. else
  991. {
  992. reward = 500;
  993. }
  994. }
  995. CRewardableObject::setPropertyDer(what, val);
  996. }
  997. ///////////////////////////////////////////////////////////////////////////////////////////////////
  998. void CGMagicSpring::initObj(CRandomGenerator & rand)
  999. {
  1000. CVisitInfo visit; // TODO: "player above max mana" limiter. Use logical expressions for limiters?
  1001. visit.reward.manaPercentage = 200;
  1002. visit.message.addTxt(MetaString::ADVOB_TXT, 74);
  1003. info.push_back(visit); // two rewards, one for each entrance
  1004. info.push_back(visit);
  1005. onEmpty.addTxt(MetaString::ADVOB_TXT, 75);
  1006. soundID = soundBase::GENIE;
  1007. }
  1008. std::vector<int3> CGMagicSpring::getVisitableOffsets() const
  1009. {
  1010. std::vector <int3> visitableTiles;
  1011. for(int y = 0; y < 6; y++)
  1012. for (int x = 0; x < 8; x++) //starting from left
  1013. if (appearance.isVisitableAt(x, y))
  1014. visitableTiles.push_back (int3(x, y , 0));
  1015. return visitableTiles;
  1016. }
  1017. int3 CGMagicSpring::getVisitableOffset() const
  1018. {
  1019. auto visitableTiles = getVisitableOffsets();
  1020. if (visitableTiles.size() != info.size())
  1021. {
  1022. logGlobal->warnStream() << "Unexpected number of visitable tiles of Magic Spring at " << pos << "!";
  1023. return int3(-1,-1,-1);
  1024. }
  1025. for (size_t i=0; i<visitableTiles.size(); i++)
  1026. {
  1027. if (info[i].numOfGrants == 0)
  1028. return visitableTiles[i];
  1029. }
  1030. return visitableTiles[0]; // return *something*. This is valid visitable tile but already used
  1031. }
  1032. std::vector<ui32> CGMagicSpring::getAvailableRewards(const CGHeroInstance * hero) const
  1033. {
  1034. auto tiles = getVisitableOffsets();
  1035. for (size_t i=0; i<tiles.size(); i++)
  1036. {
  1037. if (pos - tiles[i] == hero->getPosition() && info[i].numOfGrants == 0)
  1038. {
  1039. return std::vector<ui32>(1, i);
  1040. }
  1041. }
  1042. // hero is either not on visitable tile (should not happen) or tile is already used
  1043. return std::vector<ui32>();
  1044. }