1/**
2 * The Forgotten Server - a free and open-source MMORPG server emulator
3 * Copyright (C) 2019 Mark Samman <mark.samman@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#include "otpch.h"
21
22#include "combat.h"
23#include "configmanager.h"
24#include "game.h"
25#include "pugicast.h"
26#include "weapons.h"
27
28extern Game g_game;
29extern Vocations g_vocations;
30extern ConfigManager g_config;
31extern Weapons *g_weapons;
32
33Weapons::Weapons() { scriptInterface.initState(); }
34
35Weapons::~Weapons() { clear(false); }
36
37const Weapon *Weapons::getWeapon(const Item *item) const {
38 if (!item) {
39 return nullptr;
40 }
41
42 auto it = weapons.find(item->getID());
43 if (it == weapons.end()) {
44 return nullptr;
45 }
46 return it->second;
47}
48
49void Weapons::clear(bool fromLua) {
50 for (auto it = weapons.begin(); it != weapons.end();) {
51 if (fromLua == it->second->fromLua) {
52 it = weapons.erase(it);
53 } else {
54 ++it;
55 }
56 }
57
58 reInitState(fromLua);
59}
60
61LuaScriptInterface &Weapons::getScriptInterface() { return scriptInterface; }
62
63std::string Weapons::getScriptBaseName() const { return "weapons"; }
64
65void Weapons::loadDefaults() {
66 for (size_t i = 100, size = Item::items.size(); i < size; ++i) {
67 const ItemType &it = Item::items.getItemType(i);
68 if (it.id == 0 || weapons.find(i) != weapons.end()) {
69 continue;
70 }
71
72 switch (it.weaponType) {
73 case WEAPON_AXE:
74 case WEAPON_SWORD:
75 case WEAPON_CLUB: {
76 WeaponMelee *weapon = new WeaponMelee(&scriptInterface);
77 weapon->configureWeapon(it);
78 weapons[i] = weapon;
79 break;
80 }
81
82 case WEAPON_AMMO:
83 case WEAPON_DISTANCE: {
84 if (it.weaponType == WEAPON_DISTANCE && it.ammoType != AMMO_NONE) {
85 continue;
86 }
87
88 WeaponDistance *weapon = new WeaponDistance(&scriptInterface);
89 weapon->configureWeapon(it);
90 weapons[i] = weapon;
91 break;
92 }
93
94 default:
95 break;
96 }
97 }
98}
99
100Event_ptr Weapons::getEvent(const std::string &nodeName) {
101 if (strcasecmp(nodeName.c_str(), "melee") == 0) {
102 return Event_ptr(new WeaponMelee(&scriptInterface));
103 } else if (strcasecmp(nodeName.c_str(), "distance") == 0) {
104 return Event_ptr(new WeaponDistance(&scriptInterface));
105 } else if (strcasecmp(nodeName.c_str(), "wand") == 0) {
106 return Event_ptr(new WeaponWand(&scriptInterface));
107 }
108 return nullptr;
109}
110
111bool Weapons::registerEvent(Event_ptr event, const pugi::xml_node &) {
112 Weapon *weapon = static_cast<Weapon *>(
113 event.release()); // event is guaranteed to be a Weapon
114
115 auto result = weapons.emplace(weapon->getID(), weapon);
116 if (!result.second) {
117 std::cout << "[Warning - Weapons::registerEvent] Duplicate registered item "
118 "with id: "
119 << weapon->getID() << std::endl;
120 }
121 return result.second;
122}
123
124bool Weapons::registerLuaEvent(Weapon *event) {
125 Weapon_ptr weapon{event};
126 weapons[weapon->getID()] = weapon.release();
127
128 return true;
129}
130
131// monsters
132int32_t Weapons::getMaxMeleeDamage(int32_t attackSkill, int32_t attackValue) {
133 return static_cast<int32_t>(
134 std::ceil((attackSkill * (attackValue * 0.05)) + (attackValue * 0.5)));
135}
136
137// players
138int32_t Weapons::getMaxWeaponDamage(uint32_t level, int32_t attackSkill,
139 int32_t attackValue, float attackFactor) {
140 return static_cast<int32_t>(std::round(
141 (level / 5) + (((((attackSkill / 4.) + 1) * (attackValue / 3.)) * 1.03) /
142 attackFactor)));
143}
144
145bool Weapon::configureEvent(const pugi::xml_node &node) {
146 pugi::xml_attribute attr;
147 if (!(attr = node.attribute("id"))) {
148 std::cout << "[Error - Weapon::configureEvent] Weapon without id."
149 << std::endl;
150 return false;
151 }
152 id = pugi::cast<uint16_t>(attr.value());
153
154 if ((attr = node.attribute("level"))) {
155 level = pugi::cast<uint32_t>(attr.value());
156 }
157
158 if ((attr = node.attribute("maglv")) || (attr = node.attribute("maglevel"))) {
159 magLevel = pugi::cast<uint32_t>(attr.value());
160 }
161
162 if ((attr = node.attribute("mana"))) {
163 mana = pugi::cast<uint32_t>(attr.value());
164 }
165
166 if ((attr = node.attribute("manapercent"))) {
167 manaPercent = pugi::cast<uint32_t>(attr.value());
168 }
169
170 if ((attr = node.attribute("soul"))) {
171 soul = pugi::cast<uint32_t>(attr.value());
172 }
173
174 if ((attr = node.attribute("prem"))) {
175 premium = attr.as_bool();
176 }
177
178 if ((attr = node.attribute("breakchance"))) {
179 breakChance = std::min<uint8_t>(100, pugi::cast<uint16_t>(attr.value()));
180 }
181
182 if ((attr = node.attribute("action"))) {
183 action = getWeaponAction(asLowerCaseString(attr.as_string()));
184 if (action == WEAPONACTION_NONE) {
185 std::cout << "[Warning - Weapon::configureEvent] Unknown action "
186 << attr.as_string() << std::endl;
187 }
188 }
189
190 if ((attr = node.attribute("enabled"))) {
191 enabled = attr.as_bool();
192 }
193
194 if ((attr = node.attribute("unproperly"))) {
195 wieldUnproperly = attr.as_bool();
196 }
197
198 std::list<std::string> vocStringList;
199 for (auto vocationNode : node.children()) {
200 if (!(attr = vocationNode.attribute("name"))) {
201 continue;
202 }
203
204 int32_t vocationId = g_vocations.getVocationId(attr.as_string());
205 if (vocationId != -1) {
206 vocWeaponMap[vocationId] = true;
207 int32_t promotedVocation = g_vocations.getPromotedVocation(vocationId);
208 if (promotedVocation != VOCATION_NONE) {
209 vocWeaponMap[promotedVocation] = true;
210 }
211
212 if (vocationNode.attribute("showInDescription").as_bool(true)) {
213 vocStringList.push_back(asLowerCaseString(attr.as_string()));
214 }
215 }
216 }
217
218 std::string vocationString;
219 for (const std::string &str : vocStringList) {
220 if (!vocationString.empty()) {
221 if (str != vocStringList.back()) {
222 vocationString.push_back(',');
223 vocationString.push_back(' ');
224 } else {
225 vocationString += " and ";
226 }
227 }
228
229 vocationString += str;
230 vocationString.push_back('s');
231 }
232
233 uint32_t wieldInfo = 0;
234 if (getReqLevel() > 0) {
235 wieldInfo |= WIELDINFO_LEVEL;
236 }
237
238 if (getReqMagLv() > 0) {
239 wieldInfo |= WIELDINFO_MAGLV;
240 }
241
242 if (!vocationString.empty()) {
243 wieldInfo |= WIELDINFO_VOCREQ;
244 }
245
246 if (isPremium()) {
247 wieldInfo |= WIELDINFO_PREMIUM;
248 }
249
250 if (wieldInfo != 0) {
251 ItemType &it = Item::items.getItemType(id);
252 it.wieldInfo = wieldInfo;
253 it.vocationString = vocationString;
254 it.minReqLevel = getReqLevel();
255 it.minReqMagicLevel = getReqMagLv();
256 }
257
258 configureWeapon(Item::items[id]);
259 return true;
260}
261
262void Weapon::configureWeapon(const ItemType &it) { id = it.id; }
263
264std::string Weapon::getScriptEventName() const { return "onUseWeapon"; }
265
266int32_t Weapon::playerWeaponCheck(Player *player, Creature *target,
267 uint8_t shootRange) const {
268 const Position &playerPos = player->getPosition();
269 const Position &targetPos = target->getPosition();
270 if (playerPos.z != targetPos.z) {
271 return 0;
272 }
273
274 uint8_t effectiveRange = shootRange;
275 const Outfit *outfit = Outfits::getInstance().getOutfitByLookType(
276 player->getSex(), player->getCurrentOutfit().lookType);
277 if (outfit && outfit->attackRange > effectiveRange) {
278 effectiveRange = outfit->attackRange;
279 }
280
281 if (std::max<uint32_t>(Position::getDistanceX(playerPos, targetPos),
282 Position::getDistanceY(playerPos, targetPos)) >
283 effectiveRange) {
284 return 0;
285 }
286
287 if (!player->hasFlag(PlayerFlag_IgnoreWeaponCheck)) {
288 if (!enabled) {
289 return 0;
290 }
291
292 if (player->getMana() < getManaCost(player)) {
293 return 0;
294 }
295
296 if (player->getHealth() < getHealthCost(player)) {
297 return 0;
298 }
299
300 if (player->getSoul() < soul) {
301 return 0;
302 }
303
304 if (isPremium() && !player->isPremium()) {
305 return 0;
306 }
307
308 if (!vocWeaponMap.empty()) {
309 if (vocWeaponMap.find(player->getVocationId()) == vocWeaponMap.end()) {
310 return 0;
311 }
312 }
313
314 int32_t damageModifier = 100;
315 if (player->getLevel() < getReqLevel()) {
316 damageModifier = (isWieldedUnproperly() ? damageModifier / 2 : 0);
317 }
318
319 if (player->getMagicLevel() < getReqMagLv()) {
320 damageModifier = (isWieldedUnproperly() ? damageModifier / 2 : 0);
321 }
322 return damageModifier;
323 }
324
325 return 100;
326}
327
328bool Weapon::useWeapon(Player *player, Item *item, Creature *target) const {
329 int32_t damageModifier =
330 playerWeaponCheck(player, target, item->getShootRange());
331 if (damageModifier == 0) {
332 return false;
333 }
334
335 internalUseWeapon(player, item, target, damageModifier);
336 return true;
337}
338
339bool Weapon::useFist(Player *player, Creature *target) {
340 uint8_t range = 1;
341 const Outfit *outfit = Outfits::getInstance().getOutfitByLookType(
342 player->getSex(), player->getCurrentOutfit().lookType);
343 if (outfit) {
344 range = outfit->attackRange;
345 }
346
347 if (std::max<uint32_t>(
348 Position::getDistanceX(player->getPosition(), target->getPosition()),
349 Position::getDistanceY(player->getPosition(),
350 target->getPosition())) > range) {
351 return false;
352 }
353
354 float attackFactor = player->getAttackFactor();
355 int32_t attackSkill = player->getSkillLevel(SKILL_FIST);
356 int32_t attackValue = 7;
357
358 int32_t maxDamage = Weapons::getMaxWeaponDamage(
359 player->getLevel(), attackSkill, attackValue, attackFactor);
360
361 CombatParams params;
362 params.combatType = COMBAT_PHYSICALDAMAGE;
363 params.blockedByArmor = true;
364 params.blockedByShield = true;
365
366 CombatDamage damage;
367 damage.origin = ORIGIN_MELEE;
368 damage.primary.type = params.combatType;
369 damage.primary.value = -normal_random(0, maxDamage);
370
371 Combat::doTargetCombat(player, target, damage, params);
372 if (!player->hasFlag(PlayerFlag_NotGainSkill) &&
373 player->getAddAttackSkill()) {
374 player->addSkillAdvance(SKILL_FIST, 1);
375 }
376
377 return true;
378}
379
380void Weapon::internalUseWeapon(Player *player, Item *item, Creature *target,
381 int32_t damageModifier) const {
382 if (scripted) {
383 LuaVariant var;
384 var.type = VARIANT_NUMBER;
385 var.number = target->getID();
386 executeUseWeapon(player, var);
387 } else {
388 CombatDamage damage;
389 WeaponType_t weaponType = item->getWeaponType();
390 if (weaponType == WEAPON_AMMO || weaponType == WEAPON_DISTANCE) {
391 damage.origin = ORIGIN_RANGED;
392 } else {
393 damage.origin = ORIGIN_MELEE;
394 }
395 damage.primary.type = params.combatType;
396 damage.primary.value =
397 (getWeaponDamage(player, target, item) * damageModifier) / 100;
398 damage.secondary.type = getElementType();
399 damage.secondary.value = getElementDamage(player, target, item);
400 Combat::doTargetCombat(player, target, damage, params);
401 }
402
403 onUsedWeapon(player, item, target->getTile());
404}
405
406void Weapon::internalUseWeapon(Player *player, Item *item, Tile *tile) const {
407 if (scripted) {
408 LuaVariant var;
409 var.type = VARIANT_TARGETPOSITION;
410 var.pos = tile->getPosition();
411 executeUseWeapon(player, var);
412 } else {
413 Combat::postCombatEffects(player, tile->getPosition(), params);
414 g_game.addMagicEffect(tile->getPosition(), CONST_ME_POFF);
415 }
416
417 onUsedWeapon(player, item, tile);
418}
419
420void Weapon::onUsedWeapon(Player *player, Item *item, Tile *destTile) const {
421 if (!player->hasFlag(PlayerFlag_NotGainSkill)) {
422 skills_t skillType;
423 uint32_t skillPoint;
424 if (getSkillType(player, item, skillType, skillPoint)) {
425 player->addSkillAdvance(skillType, skillPoint);
426 }
427 }
428
429 uint32_t manaCost = getManaCost(player);
430 if (manaCost != 0) {
431 player->addManaSpent(manaCost);
432 player->changeMana(-static_cast<int32_t>(manaCost));
433 }
434
435 uint32_t healthCost = getHealthCost(player);
436 if (healthCost != 0) {
437 player->changeHealth(-static_cast<int32_t>(healthCost));
438 }
439
440 if (!player->hasFlag(PlayerFlag_HasInfiniteSoul) && soul > 0) {
441 player->changeSoul(-static_cast<int32_t>(soul));
442 }
443
444 if (breakChance != 0 && uniform_random(1, 100) <= breakChance) {
445 Weapon::decrementItemCount(item);
446 return;
447 }
448
449 switch (action) {
450 case WEAPONACTION_REMOVECOUNT:
451 if (g_config.getBoolean(ConfigManager::REMOVE_WEAPON_AMMO)) {
452 Weapon::decrementItemCount(item);
453 }
454 break;
455
456 case WEAPONACTION_REMOVECHARGE: {
457 uint16_t charges = item->getCharges();
458 if (charges != 0 &&
459 g_config.getBoolean(ConfigManager::REMOVE_WEAPON_CHARGES)) {
460 g_game.transformItem(item, item->getID(), charges - 1);
461 }
462 break;
463 }
464
465 case WEAPONACTION_MOVE:
466 g_game.internalMoveItem(item->getParent(), destTile, INDEX_WHEREEVER, item,
467 1, nullptr, FLAG_NOLIMIT);
468 break;
469
470 default:
471 break;
472 }
473}
474
475uint32_t Weapon::getManaCost(const Player *player) const {
476 if (mana != 0) {
477 return mana;
478 }
479
480 if (manaPercent == 0) {
481 return 0;
482 }
483
484 return (player->getMaxMana() * manaPercent) / 100;
485}
486
487int32_t Weapon::getHealthCost(const Player *player) const {
488 if (health != 0) {
489 return health;
490 }
491
492 if (healthPercent == 0) {
493 return 0;
494 }
495
496 return (player->getMaxHealth() * healthPercent) / 100;
497}
498
499bool Weapon::executeUseWeapon(Player *player, const LuaVariant &var) const {
500 // onUseWeapon(player, var)
501 if (!scriptInterface->reserveScriptEnv()) {
502 std::cout << "[Error - Weapon::executeUseWeapon] Call stack overflow"
503 << std::endl;
504 return false;
505 }
506
507 ScriptEnvironment *env = scriptInterface->getScriptEnv();
508 env->setScriptId(scriptId, scriptInterface);
509
510 lua_State *L = scriptInterface->getLuaState();
511
512 scriptInterface->pushFunction(scriptId);
513 LuaScriptInterface::pushUserdata<Player>(L, player);
514 LuaScriptInterface::setMetatable(L, -1, "Player");
515 scriptInterface->pushVariant(L, var);
516
517 return scriptInterface->callFunction(2);
518}
519
520void Weapon::decrementItemCount(Item *item) {
521 uint16_t count = item->getItemCount();
522 if (count > 1) {
523 g_game.transformItem(item, item->getID(), count - 1);
524 } else {
525 g_game.internalRemoveItem(item);
526 }
527}
528
529WeaponMelee::WeaponMelee(LuaScriptInterface *interface) : Weapon(interface) {
530 params.blockedByArmor = true;
531 params.blockedByShield = true;
532 params.combatType = COMBAT_PHYSICALDAMAGE;
533}
534
535void WeaponMelee::configureWeapon(const ItemType &it) {
536 if (it.abilities) {
537 elementType = it.abilities->elementType;
538 elementDamage = it.abilities->elementDamage;
539 params.aggressive = true;
540 params.useCharges = true;
541 } else {
542 elementType = COMBAT_NONE;
543 elementDamage = 0;
544 }
545 Weapon::configureWeapon(it);
546}
547
548bool WeaponMelee::useWeapon(Player *player, Item *item,
549 Creature *target) const {
550 int32_t damageModifier =
551 playerWeaponCheck(player, target, item->getShootRange());
552 if (damageModifier == 0) {
553 return false;
554 }
555
556 internalUseWeapon(player, item, target, damageModifier);
557 return true;
558}
559
560bool WeaponMelee::getSkillType(const Player *player, const Item *item,
561 skills_t &skill, uint32_t &skillpoint) const {
562 if (player->getAddAttackSkill() &&
563 player->getLastAttackBlockType() != BLOCK_IMMUNITY) {
564 skillpoint = 1;
565 } else {
566 skillpoint = 0;
567 }
568
569 WeaponType_t weaponType = item->getWeaponType();
570 switch (weaponType) {
571 case WEAPON_SWORD: {
572 skill = SKILL_SWORD;
573 return true;
574 }
575
576 case WEAPON_CLUB: {
577 skill = SKILL_CLUB;
578 return true;
579 }
580
581 case WEAPON_AXE: {
582 skill = SKILL_AXE;
583 return true;
584 }
585
586 default:
587 break;
588 }
589 return false;
590}
591
592int32_t WeaponMelee::getElementDamage(const Player *player, const Creature *,
593 const Item *item) const {
594 if (elementType == COMBAT_NONE) {
595 return 0;
596 }
597
598 int32_t attackSkill = player->getWeaponSkill(item);
599 int32_t attackValue = elementDamage;
600 float attackFactor = player->getAttackFactor();
601
602 int32_t maxValue = Weapons::getMaxWeaponDamage(
603 player->getLevel(), attackSkill, attackValue, attackFactor);
604 return -normal_random(
605 0, static_cast<int32_t>(maxValue *
606 player->getVocation()->meleeDamageMultiplier));
607}
608
609int32_t WeaponMelee::getWeaponDamage(const Player *player, const Creature *,
610 const Item *item,
611 bool maxDamage /*= false*/) const {
612 int32_t attackSkill = player->getWeaponSkill(item);
613 int32_t attackValue = std::max<int32_t>(0, item->getAttack());
614 float attackFactor = player->getAttackFactor();
615
616 int32_t maxValue = static_cast<int32_t>(
617 Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue,
618 attackFactor) *
619 player->getVocation()->meleeDamageMultiplier);
620 if (maxDamage) {
621 return -maxValue;
622 }
623
624 return -normal_random(0, maxValue);
625}
626
627WeaponDistance::WeaponDistance(LuaScriptInterface *interface)
628 : Weapon(interface) {
629 params.blockedByArmor = true;
630 params.combatType = COMBAT_PHYSICALDAMAGE;
631}
632
633void WeaponDistance::configureWeapon(const ItemType &it) {
634 params.distanceEffect = it.shootType;
635
636 if (it.abilities) {
637 elementType = it.abilities->elementType;
638 elementDamage = it.abilities->elementDamage;
639 params.aggressive = true;
640 params.useCharges = true;
641 } else {
642 elementType = COMBAT_NONE;
643 elementDamage = 0;
644 }
645
646 Weapon::configureWeapon(it);
647}
648
649bool WeaponDistance::useWeapon(Player *player, Item *item,
650 Creature *target) const {
651 int32_t damageModifier = 0;
652 const ItemType &it = Item::items[id];
653 if (it.weaponType == WEAPON_AMMO) {
654 Item *mainWeaponItem = player->getWeapon(true);
655 const Weapon *mainWeapon = g_weapons->getWeapon(mainWeaponItem);
656 if (mainWeapon) {
657 damageModifier = mainWeapon->playerWeaponCheck(
658 player, target, mainWeaponItem->getShootRange());
659 } else if (mainWeaponItem) {
660 damageModifier =
661 playerWeaponCheck(player, target, mainWeaponItem->getShootRange());
662 }
663 } else {
664 damageModifier = playerWeaponCheck(player, target, item->getShootRange());
665 }
666
667 if (damageModifier == 0) {
668 return false;
669 }
670
671 int32_t chance;
672 if (it.hitChance == 0) {
673 // hit chance is based on distance to target and distance skill
674 uint32_t skill = player->getSkillLevel(SKILL_DISTANCE);
675 const Position &playerPos = player->getPosition();
676 const Position &targetPos = target->getPosition();
677 uint32_t distance =
678 std::max<uint32_t>(Position::getDistanceX(playerPos, targetPos),
679 Position::getDistanceY(playerPos, targetPos));
680
681 uint32_t maxHitChance;
682 if (it.maxHitChance != -1) {
683 maxHitChance = it.maxHitChance;
684 } else if (it.ammoType != AMMO_NONE) {
685 // hit chance on two-handed weapons is limited to 90%
686 maxHitChance = 90;
687 } else {
688 // one-handed is set to 75%
689 maxHitChance = 75;
690 }
691
692 if (maxHitChance == 75) {
693 // chance for one-handed weapons
694 switch (distance) {
695 case 1:
696 case 5:
697 chance = std::min<uint32_t>(skill, 74) + 1;
698 break;
699 case 2:
700 chance =
701 static_cast<uint32_t>(std::min<uint32_t>(skill, 28) * 2.40f) + 8;
702 break;
703 case 3:
704 chance =
705 static_cast<uint32_t>(std::min<uint32_t>(skill, 45) * 1.55f) + 6;
706 break;
707 case 4:
708 chance =
709 static_cast<uint32_t>(std::min<uint32_t>(skill, 58) * 1.25f) + 3;
710 break;
711 case 6:
712 chance =
713 static_cast<uint32_t>(std::min<uint32_t>(skill, 90) * 0.80f) + 3;
714 break;
715 case 7:
716 chance =
717 static_cast<uint32_t>(std::min<uint32_t>(skill, 104) * 0.70f) + 2;
718 break;
719 default:
720 chance = it.hitChance;
721 break;
722 }
723 } else if (maxHitChance == 90) {
724 // formula for two-handed weapons
725 switch (distance) {
726 case 1:
727 case 5:
728 chance =
729 static_cast<uint32_t>(std::min<uint32_t>(skill, 74) * 1.20f) + 1;
730 break;
731 case 2:
732 chance = static_cast<uint32_t>(std::min<uint32_t>(skill, 28) * 3.20f);
733 break;
734 case 3:
735 chance = std::min<uint32_t>(skill, 45) * 2;
736 break;
737 case 4:
738 chance = static_cast<uint32_t>(std::min<uint32_t>(skill, 58) * 1.55f);
739 break;
740 case 6:
741 case 7:
742 chance = std::min<uint32_t>(skill, 90);
743 break;
744 default:
745 chance = it.hitChance;
746 break;
747 }
748 } else if (maxHitChance == 100) {
749 switch (distance) {
750 case 1:
751 case 5:
752 chance =
753 static_cast<uint32_t>(std::min<uint32_t>(skill, 73) * 1.35f) + 1;
754 break;
755 case 2:
756 chance =
757 static_cast<uint32_t>(std::min<uint32_t>(skill, 30) * 3.20f) + 4;
758 break;
759 case 3:
760 chance =
761 static_cast<uint32_t>(std::min<uint32_t>(skill, 48) * 2.05f) + 2;
762 break;
763 case 4:
764 chance =
765 static_cast<uint32_t>(std::min<uint32_t>(skill, 65) * 1.50f) + 2;
766 break;
767 case 6:
768 chance =
769 static_cast<uint32_t>(std::min<uint32_t>(skill, 87) * 1.20f) - 4;
770 break;
771 case 7:
772 chance =
773 static_cast<uint32_t>(std::min<uint32_t>(skill, 90) * 1.10f) + 1;
774 break;
775 default:
776 chance = it.hitChance;
777 break;
778 }
779 } else {
780 chance = maxHitChance;
781 }
782 } else {
783 chance = it.hitChance;
784 }
785
786 if (item->getWeaponType() == WEAPON_AMMO) {
787 Item *bow = player->getWeapon(true);
788 if (bow && bow->getHitChance() != 0) {
789 chance += bow->getHitChance();
790 }
791 }
792
793 if (chance >= uniform_random(1, 100)) {
794 Weapon::internalUseWeapon(player, item, target, damageModifier);
795 } else {
796 // miss target
797 Tile *destTile = target->getTile();
798
799 if (!Position::areInRange<1, 1, 0>(player->getPosition(),
800 target->getPosition())) {
801 static std::vector<std::pair<int32_t, int32_t>> destList{
802 {-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {0, 0},
803 {1, 0}, {-1, 1}, {0, 1}, {1, 1}};
804 std::shuffle(destList.begin(), destList.end(), getRandomGenerator());
805
806 Position destPos = target->getPosition();
807
808 for (const auto &dir : destList) {
809 // Blocking tiles or tiles without ground ain't valid targets for spears
810 Tile *tmpTile = g_game.map.getTile(destPos.x + dir.first,
811 destPos.y + dir.second, destPos.z);
812 if (tmpTile && !tmpTile->hasFlag(TILESTATE_IMMOVABLEBLOCKSOLID) &&
813 tmpTile->getGround() != nullptr) {
814 destTile = tmpTile;
815 break;
816 }
817 }
818 }
819
820 Weapon::internalUseWeapon(player, item, destTile);
821 }
822 return true;
823}
824
825int32_t WeaponDistance::getElementDamage(const Player *player,
826 const Creature *target,
827 const Item *item) const {
828 if (elementType == COMBAT_NONE) {
829 return 0;
830 }
831
832 int32_t attackValue = elementDamage;
833 if (item->getWeaponType() == WEAPON_AMMO) {
834 Item *weapon = player->getWeapon(true);
835 if (weapon) {
836 attackValue += weapon->getAttack();
837 }
838 }
839
840 int32_t attackSkill = player->getSkillLevel(SKILL_DISTANCE);
841 float attackFactor = player->getAttackFactor();
842
843 int32_t minValue = 0;
844 int32_t maxValue = Weapons::getMaxWeaponDamage(
845 player->getLevel(), attackSkill, attackValue, attackFactor);
846 if (target) {
847 if (target->getPlayer()) {
848 minValue = static_cast<int32_t>(std::ceil(player->getLevel() * 0.1));
849 } else {
850 minValue = static_cast<int32_t>(std::ceil(player->getLevel() * 0.2));
851 }
852 }
853
854 return -normal_random(
855 minValue, static_cast<int32_t>(
856 maxValue * player->getVocation()->distDamageMultiplier));
857}
858
859int32_t WeaponDistance::getWeaponDamage(const Player *player,
860 const Creature *target,
861 const Item *item,
862 bool maxDamage /*= false*/) const {
863 int32_t attackValue = item->getAttack();
864
865 if (item->getWeaponType() == WEAPON_AMMO) {
866 Item *weapon = player->getWeapon(true);
867 if (weapon) {
868 attackValue += weapon->getAttack();
869 }
870 }
871
872 int32_t attackSkill = player->getSkillLevel(SKILL_DISTANCE);
873 float attackFactor = player->getAttackFactor();
874
875 int32_t maxValue = static_cast<int32_t>(
876 Weapons::getMaxWeaponDamage(player->getLevel(), attackSkill, attackValue,
877 attackFactor) *
878 player->getVocation()->distDamageMultiplier);
879 if (maxDamage) {
880 return -maxValue;
881 }
882
883 int32_t minValue;
884 if (target) {
885 if (target->getPlayer()) {
886 minValue = static_cast<int32_t>(std::ceil(player->getLevel() * 0.1));
887 } else {
888 minValue = static_cast<int32_t>(std::ceil(player->getLevel() * 0.2));
889 }
890 } else {
891 minValue = 0;
892 }
893 return -normal_random(minValue, maxValue);
894}
895
896bool WeaponDistance::getSkillType(const Player *player, const Item *,
897 skills_t &skill, uint32_t &skillpoint) const {
898 skill = SKILL_DISTANCE;
899
900 if (player->getAddAttackSkill()) {
901 switch (player->getLastAttackBlockType()) {
902 case BLOCK_NONE: {
903 skillpoint = 2;
904 break;
905 }
906
907 case BLOCK_DEFENSE:
908 case BLOCK_ARMOR: {
909 skillpoint = 1;
910 break;
911 }
912
913 default:
914 skillpoint = 0;
915 break;
916 }
917 } else {
918 skillpoint = 0;
919 }
920 return true;
921}
922
923bool WeaponWand::configureEvent(const pugi::xml_node &node) {
924 if (!Weapon::configureEvent(node)) {
925 return false;
926 }
927
928 pugi::xml_attribute attr;
929 if ((attr = node.attribute("min"))) {
930 minChange = pugi::cast<int32_t>(attr.value());
931 }
932
933 if ((attr = node.attribute("max"))) {
934 maxChange = pugi::cast<int32_t>(attr.value());
935 }
936
937 attr = node.attribute("type");
938 if (!attr) {
939 return true;
940 }
941
942 std::string tmpStrValue = asLowerCaseString(attr.as_string());
943 if (tmpStrValue == "earth") {
944 params.combatType = COMBAT_EARTHDAMAGE;
945 } else if (tmpStrValue == "ice") {
946 params.combatType = COMBAT_ICEDAMAGE;
947 } else if (tmpStrValue == "energy") {
948 params.combatType = COMBAT_ENERGYDAMAGE;
949 } else if (tmpStrValue == "fire") {
950 params.combatType = COMBAT_FIREDAMAGE;
951 } else if (tmpStrValue == "death") {
952 params.combatType = COMBAT_DEATHDAMAGE;
953 } else if (tmpStrValue == "holy") {
954 params.combatType = COMBAT_HOLYDAMAGE;
955 } else {
956 std::cout << "[Warning - WeaponWand::configureEvent] Type \""
957 << attr.as_string() << "\" does not exist." << std::endl;
958 }
959 return true;
960}
961
962void WeaponWand::configureWeapon(const ItemType &it) {
963 params.distanceEffect = it.shootType;
964
965 Weapon::configureWeapon(it);
966}
967
968int32_t WeaponWand::getWeaponDamage(const Player *, const Creature *,
969 const Item *,
970 bool maxDamage /*= false*/) const {
971 if (maxDamage) {
972 return -maxChange;
973 }
974 return -normal_random(minChange, maxChange);
975}
976