diff --git a/data-canary/monster/trainer/training_machine.lua b/data-canary/monster/trainer/training_machine.lua new file mode 100644 index 00000000000..93d042efa5e --- /dev/null +++ b/data-canary/monster/trainer/training_machine.lua @@ -0,0 +1,76 @@ +local mType = Game.createMonsterType("Training Machine") +local monster = {} + +monster.description = "a training machine" +monster.experience = 0 +monster.outfit = { + lookType = 1142, +} + +monster.health = 1000000 +monster.maxHealth = monster.health +monster.race = "venom" +monster.corpse = 0 +monster.speed = 0 + +monster.changeTarget = { + interval = 1000, + chance = 0, +} + +monster.flags = { + summonable = false, + attackable = true, + hostile = true, + convinceable = false, + illusionable = false, + canPushItems = true, + canPushCreatures = true, + targetDistance = 1, + staticAttackChance = 100, +} + +monster.summons = {} + +monster.voices = { + interval = 5000, + chance = 10, + { text = "I hope you are enjoying your sparring Sir or Ma'am!", yell = false }, + { text = "Threat level rising!", yell = false }, + { text = "Engaging in hostile interaction!", yell = false }, + { text = "Rrrtttarrrttarrrtta", yell = false }, + { text = "Please feel free to hit me Sir or Ma'am!", yell = false }, + { text = "klonk klonk klonk", yell = false }, + { text = "Self-diagnosis running.", yell = false }, + { text = "Battle simulation proceeding.", yell = false }, + { text = "Repairs initiated!", yell = false }, +} + +monster.loot = {} + +monster.attacks = { + { name = "melee", interval = 2000, chance = 100, minDamage = -2, maxDamage = -7, attack = 130 }, +} + +monster.defenses = { + defense = 10, + armor = 7, + { name = "combat", type = COMBAT_HEALING, chance = 15, interval = 2000, minDamage = 10000, maxDamage = 50000, effect = CONST_ME_MAGIC_BLUE }, +} + +monster.elements = { + { type = COMBAT_PHYSICALDAMAGE, percent = 0 }, + { type = COMBAT_ENERGYDAMAGE, percent = 0 }, + { type = COMBAT_EARTHDAMAGE, percent = 0 }, + { type = COMBAT_FIREDAMAGE, percent = 0 }, + { type = COMBAT_LIFEDRAIN, percent = 0 }, + { type = COMBAT_MANADRAIN, percent = 0 }, + { type = COMBAT_DROWNDAMAGE, percent = 0 }, + { type = COMBAT_ICEDAMAGE, percent = 0 }, + { type = COMBAT_HOLYDAMAGE, percent = 0 }, + { type = COMBAT_DEATHDAMAGE, percent = 0 }, +} + +monster.immunities = {} + +mType:register(monster) diff --git a/data/scripts/talkactions/god/charms.lua b/data/scripts/talkactions/god/charms.lua index f06c17ac7e7..238bce7614f 100644 --- a/data/scripts/talkactions/god/charms.lua +++ b/data/scripts/talkactions/god/charms.lua @@ -117,7 +117,7 @@ function setBestiary.onSay(player, words, param) -- create log logCommand(player, words, param) - local usage = "/setbestiary PLAYER NAME,MONSTER NAME,AMOUNT" + local usage = "/setbestiary PLAYER NAME,MONSTER NAME/ALL,AMOUNT" if param == "" then player:sendCancelMessage("Command param required. Usage: " .. usage) return true @@ -136,21 +136,36 @@ function setBestiary.onSay(player, words, param) split[2] = split[2]:trimSpace() split[3] = split[3]:trimSpace() - local monsterName = split[2] - local mType = MonsterType(monsterName) - if not mType or (mType and mType:raceId() == 0) then - player:sendCancelMessage("This monster has no bestiary. Type the name exactly as in game.") - return true - end local amount = tonumber(split[3]) if not amount then - player:sendCancelMessage("Wrong kill amount") + player:sendCancelMessage("Wrong kill amount.") return true end - player:sendCancelMessage("Set bestiary kill of monster '" .. monsterName .. "' from player '" .. target:getName() .. "' to '" .. amount .. "'.") - target:sendCancelMessage("Updated kills of monster '" .. monsterName .. "'!") - target:addBestiaryKill(monsterName, amount) + local monsterName = split[2] + + -- If "all" is specified, iterate through all monsters + if monsterName:lower() == "all" then + local monsterList = Game.getMonsterTypes() -- Retrieves all available monsters + for _, mType in pairs(monsterList) do + if mType:raceId() > 0 then -- Ensure the monster has a bestiary entry + target:addBestiaryKill(mType:name(), amount) + end + end + player:sendCancelMessage("Set bestiary kill count to '" .. amount .. "' for all monsters for player '" .. target:getName() .. "'.") + target:sendCancelMessage("Updated kills for all monsters in the bestiary!") + else + local mType = MonsterType(monsterName) + if not mType or (mType and mType:raceId() == 0) then + player:sendCancelMessage("This monster has no bestiary. Type the name exactly as in the game.") + return true + end + + target:addBestiaryKill(monsterName, amount) + player:sendCancelMessage("Set bestiary kill of monster '" .. monsterName .. "' for player '" .. target:getName() .. "' to '" .. amount .. "'.") + target:sendCancelMessage("Updated kills of monster '" .. monsterName .. "'!") + end + target:getPosition():sendMagicEffect(CONST_ME_HOLYAREA) end diff --git a/src/creatures/combat/combat.cpp b/src/creatures/combat/combat.cpp index e1f764e0e29..b88d635bcb4 100644 --- a/src/creatures/combat/combat.cpp +++ b/src/creatures/combat/combat.cpp @@ -1202,7 +1202,11 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position const int32_t rangeX = maxX + MAP_MAX_VIEW_PORT_X; const int32_t rangeY = maxY + MAP_MAX_VIEW_PORT_Y; - int affected = 0; + std::vector> affectedTargets; + CombatDamage tmpDamage; + if (data) { + tmpDamage = *data; + } for (const auto &tile : tileList) { if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) { continue; @@ -1224,34 +1228,13 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position } if (!params.aggressive || (caster != creature && Combat::canDoCombat(caster, creature, params.aggressive) == RETURNVALUE_NOERROR)) { - affected++; + affectedTargets.push_back(creature); } } } } - CombatDamage tmpDamage; - if (data) { - tmpDamage.origin = data->origin; - tmpDamage.primary.type = data->primary.type; - tmpDamage.primary.value = data->primary.value; - tmpDamage.secondary.type = data->secondary.type; - tmpDamage.secondary.value = data->secondary.value; - tmpDamage.critical = data->critical; - tmpDamage.fatal = data->fatal; - tmpDamage.criticalDamage = data->criticalDamage; - tmpDamage.criticalChance = data->criticalChance; - tmpDamage.damageMultiplier = data->damageMultiplier; - tmpDamage.damageReductionMultiplier = data->damageReductionMultiplier; - tmpDamage.healingMultiplier = data->healingMultiplier; - tmpDamage.manaLeech = data->manaLeech; - tmpDamage.lifeLeech = data->lifeLeech; - tmpDamage.healingLink = data->healingLink; - tmpDamage.instantSpellName = data->instantSpellName; - tmpDamage.runeSpellName = data->runeSpellName; - tmpDamage.lifeLeechChance = data->lifeLeechChance; - tmpDamage.manaLeechChance = data->manaLeechChance; - } + applyExtensions(caster, affectedTargets, tmpDamage, params); // Wheel of destiny get beam affected total auto spectators = Spectators().find(pos, true, rangeX, rangeX, rangeY, rangeY); @@ -1259,7 +1242,11 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position uint8_t beamAffectedTotal = casterPlayer ? casterPlayer->wheel()->getBeamAffectedTotal(tmpDamage) : 0; uint8_t beamAffectedCurrent = 0; - tmpDamage.affected = affected; + tmpDamage.affected = affectedTargets.size(); + + // The apply extensions can't modifify the damage value, so we need to create a copy of the damage value + auto extensionsDamage = tmpDamage; + applyExtensions(caster, affectedTargets, extensionsDamage, params); for (const auto &tile : tileList) { if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) { continue; @@ -1285,7 +1272,17 @@ void Combat::CombatFunc(const std::shared_ptr &caster, const Position if (casterPlayer) { casterPlayer->wheel()->updateBeamMasteryDamage(tmpDamage, beamAffectedTotal, beamAffectedCurrent); } - func(caster, creature, params, &tmpDamage); + + if (func) { + auto creatureDamage = creature->getCombatDamage(); + if (!creatureDamage.isEmpty()) { + func(caster, creature, params, &creatureDamage); + // Reset the creature's combat damage + creature->setCombatDamage(CombatDamage()); + } else { + func(caster, creature, params, &tmpDamage); + } + } if (params.targetCallback) { params.targetCallback->onTargetCombat(caster, creature); } @@ -1320,7 +1317,9 @@ void Combat::doCombatHealth(const std::shared_ptr &caster, const std:: } } - applyExtensions(caster, target, damage, params); + std::vector> affectedTargets; + affectedTargets.push_back(target); + applyExtensions(caster, affectedTargets, damage, params); if (canCombat) { if (target && caster && params.distanceEffect != CONST_ANI_NONE) { @@ -1341,7 +1340,6 @@ void Combat::doCombatHealth(const std::shared_ptr &caster, const std:: } void Combat::doCombatHealth(const std::shared_ptr &caster, const Position &position, const std::unique_ptr &area, CombatDamage &damage, const CombatParams ¶ms) { - applyExtensions(caster, nullptr, damage, params); const auto origin = caster ? caster->getPosition() : Position(); CombatFunc(caster, origin, position, area, params, CombatHealthFunc, &damage); } @@ -1358,7 +1356,9 @@ void Combat::doCombatMana(const std::shared_ptr &caster, const std::sh g_game().addMagicEffect(target->getPosition(), params.impactEffect); } - applyExtensions(caster, target, damage, params); + std::vector> affectedTargets; + affectedTargets.push_back(target); + applyExtensions(caster, affectedTargets, damage, params); if (canCombat) { if (caster && target && params.distanceEffect != CONST_ANI_NONE) { @@ -1379,7 +1379,6 @@ void Combat::doCombatMana(const std::shared_ptr &caster, const std::sh } void Combat::doCombatMana(const std::shared_ptr &caster, const Position &position, const std::unique_ptr &area, CombatDamage &damage, const CombatParams ¶ms) { - applyExtensions(caster, nullptr, damage, params); const auto origin = caster ? caster->getPosition() : Position(); CombatFunc(caster, origin, position, area, params, CombatManaFunc, &damage); } @@ -2250,63 +2249,119 @@ void MagicField::onStepInField(const std::shared_ptr &creature) { } } -void Combat::applyExtensions(const std::shared_ptr &caster, const std::shared_ptr &target, CombatDamage &damage, const CombatParams ¶ms) { +void Combat::applyExtensions(const std::shared_ptr &caster, const std::vector> targets, CombatDamage &damage, const CombatParams ¶ms) { metrics::method_latency measure(__METRICS_METHOD_NAME__); if (damage.extension || !caster || damage.primary.type == COMBAT_HEALING) { return; } - g_logger().trace("[Combat::applyExtensions] - Applying extensions for {} on {}. Initial damage: {}", caster->getName(), target ? target->getName() : "null", damage.primary.value); - - // Critical hit - uint16_t chance = 0; - int32_t bonus = 50; const auto &player = caster->getPlayer(); const auto &monster = caster->getMonster(); + + uint16_t baseChance = 0; + int32_t baseBonus = 50; if (player) { - chance = player->getSkillLevel(SKILL_CRITICAL_HIT_CHANCE); - bonus = player->getSkillLevel(SKILL_CRITICAL_HIT_DAMAGE); - if (target && target->getMonster()) { - uint16_t playerCharmRaceid = player->parseRacebyCharm(CHARM_LOW, false, 0); - if (playerCharmRaceid != 0) { - const auto &mType = g_monsters().getMonsterType(target->getName()); - if (mType && playerCharmRaceid == mType->info.raceid) { - const auto charm = g_iobestiary().getBestiaryCharm(CHARM_LOW); - if (charm) { - chance += charm->percent; - g_game().sendDoubleSoundEffect(target->getPosition(), charm->soundCastEffect, charm->soundImpactEffect, caster); + baseChance = player->getSkillLevel(SKILL_CRITICAL_HIT_CHANCE); + baseBonus = player->getSkillLevel(SKILL_CRITICAL_HIT_DAMAGE); + + uint16_t lowBlowRaceid = player->parseRacebyCharm(CHARM_LOW, false, 0); + + baseBonus += damage.criticalDamage; + baseChance += static_cast(damage.criticalChance); + + bool canApplyCritical = false; + std::unordered_map lowBlowCrits; + canApplyCritical = (baseChance != 0 && uniform_random(1, 10000) <= baseChance); + + bool canApplyFatal = false; + if (const auto &playerWeapon = player->getInventoryItem(CONST_SLOT_LEFT); playerWeapon && playerWeapon->getTier() > 0) { + double fatalChance = playerWeapon->getFatalChance(); + canApplyFatal = (fatalChance > 0 && uniform_random(0, 10000) / 100.0 < fatalChance); + } + + if (!canApplyCritical && lowBlowRaceid != 0) { + const auto &charm = g_iobestiary().getBestiaryCharm(CHARM_LOW); + if (charm) { + uint16_t lowBlowChance = baseChance + charm->percent; + + for (const auto &target : targets) { + const auto &targetMonster = target->getMonster(); + if (!targetMonster) { + continue; + } + + const auto &mType = g_monsters().getMonsterType(targetMonster->getName()); + if (!mType) { + continue; + } + + uint16_t raceId = mType->info.raceid; + + if (raceId == lowBlowRaceid) { + if (!lowBlowCrits.contains(raceId)) { + lowBlowCrits[raceId] = (lowBlowChance != 0 && uniform_random(1, 10000) <= lowBlowChance); + } } } } } - } else if (monster) { - chance = monster->getCriticalChance() * 100; - bonus = monster->getCriticalDamage() * 100; - } - bonus += damage.criticalDamage; - double multiplier = 1.0 + static_cast(bonus) / 10000; - chance += static_cast(damage.criticalChance); + bool isSingleCombat = targets.size() == 1; + for (const auto &targetCreature : targets) { + CombatDamage targetDamage = damage; + int32_t finalBonus = baseBonus; + bool isTargetCritical = canApplyCritical; - if (chance != 0 && uniform_random(1, 10000) <= chance) { - damage.critical = true; - damage.primary.value *= multiplier; - damage.secondary.value *= multiplier; - } + const auto &targetMonster = targetCreature->getMonster(); + if (targetMonster) { + const auto &mType = g_monsters().getMonsterType(targetMonster->getName()); + if (!mType) { + continue; + } - if (player) { - // Fatal hit (onslaught) - if (const auto &playerWeapon = player->getInventoryItem(CONST_SLOT_LEFT); - playerWeapon != nullptr && playerWeapon->getTier() > 0) { - const double_t fatalChance = playerWeapon->getFatalChance(); - const double_t randomChance = uniform_random(0, 10000) / 100; - if (fatalChance > 0 && randomChance < fatalChance) { - damage.fatal = true; - damage.primary.value += static_cast(std::round(damage.primary.value * 0.6)); - damage.secondary.value += static_cast(std::round(damage.secondary.value * 0.6)); + uint16_t raceId = mType->info.raceid; + + if (!canApplyCritical && lowBlowCrits.contains(raceId) && lowBlowCrits[raceId]) { + isTargetCritical = true; + } } + + double targetMultiplier = 1.0 + static_cast(finalBonus) / 10000.0; + + if (isTargetCritical) { + targetDamage.critical = true; + targetDamage.primary.value *= targetMultiplier; + targetDamage.secondary.value *= targetMultiplier; + } + + if (canApplyFatal) { + targetDamage.fatal = true; + targetDamage.primary.value += static_cast(std::round(targetDamage.primary.value * 0.6)); + targetDamage.secondary.value += static_cast(std::round(targetDamage.secondary.value * 0.6)); + } + + // If is single target, apply the damage directly + if (isSingleCombat) { + damage = targetDamage; + continue; + } + + // If is multi target, apply the damage to each target + targetCreature->setCombatDamage(targetDamage); } } else if (monster) { + baseChance = monster->getCriticalChance() * 100; + baseBonus = monster->getCriticalDamage() * 100; + baseBonus += damage.criticalDamage; + double multiplier = 1.0 + static_cast(baseBonus) / 10000; + baseChance += static_cast(damage.criticalChance); + + if (baseChance != 0 && uniform_random(1, 10000) <= baseChance) { + damage.critical = true; + damage.primary.value *= multiplier; + damage.secondary.value *= multiplier; + } + damage.primary.value *= monster->getAttackMultiplier(); damage.secondary.value *= monster->getAttackMultiplier(); } diff --git a/src/creatures/combat/combat.hpp b/src/creatures/combat/combat.hpp index 3d269074dae..a3577146eef 100644 --- a/src/creatures/combat/combat.hpp +++ b/src/creatures/combat/combat.hpp @@ -183,7 +183,7 @@ class Combat { Combat(const Combat &) = delete; Combat &operator=(const Combat &) = delete; - static void applyExtensions(const std::shared_ptr &caster, const std::shared_ptr &target, CombatDamage &damage, const CombatParams ¶ms); + static void applyExtensions(const std::shared_ptr &caster, const std::vector> targets, CombatDamage &damage, const CombatParams ¶ms); static void doCombatHealth(const std::shared_ptr &caster, const std::shared_ptr &target, CombatDamage &damage, const CombatParams ¶ms); static void doCombatHealth(const std::shared_ptr &caster, const Position &position, const std::unique_ptr &area, CombatDamage &damage, const CombatParams ¶ms); diff --git a/src/creatures/creature.cpp b/src/creatures/creature.cpp index ca76b998093..e5eeed33af3 100644 --- a/src/creatures/creature.cpp +++ b/src/creatures/creature.cpp @@ -1890,3 +1890,11 @@ void Creature::safeCall(std::function &&action) const { action(); } } + +void Creature::setCombatDamage(const CombatDamage &damage) { + m_combatDamage = damage; +} + +CombatDamage Creature::getCombatDamage() const { + return m_combatDamage; +} diff --git a/src/creatures/creature.hpp b/src/creatures/creature.hpp index 2f33dbdfea7..b93f0ad3804 100644 --- a/src/creatures/creature.hpp +++ b/src/creatures/creature.hpp @@ -700,6 +700,9 @@ class Creature : virtual public Thing, public SharedObject { charmChanceModifier = value; } + void setCombatDamage(const CombatDamage &damage); + CombatDamage getCombatDamage() const; + protected: enum FlagAsyncClass_t : uint8_t { AsyncTaskRunning = 1 << 0, @@ -878,4 +881,5 @@ class Creature : virtual public Thing, public SharedObject { } uint8_t m_flagAsyncTask = 0; + CombatDamage m_combatDamage; }; diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index adafe28f550..7938b86bcc5 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -1582,6 +1582,10 @@ struct CombatDamage { std::string runeSpellName; CombatDamage() = default; + + bool isEmpty() const { + return primary.type == COMBAT_NONE && primary.value == 0 && secondary.type == COMBAT_NONE && secondary.value == 0 && origin == ORIGIN_NONE && critical == false && affected == 1 && extension == false && exString.empty() && fatal == false && criticalDamage == 0 && criticalChance == 0 && damageMultiplier == 0 && damageReductionMultiplier == 0 && healingMultiplier == 0 && manaLeech == 0 && manaLeechChance == 0 && lifeLeech == 0 && lifeLeechChance == 0 && healingLink == 0 && instantSpellName.empty() && runeSpellName.empty(); + } }; struct RespawnType { diff --git a/src/creatures/monsters/monster.cpp b/src/creatures/monsters/monster.cpp index c3c5b4975fd..3a49bc11657 100644 --- a/src/creatures/monsters/monster.cpp +++ b/src/creatures/monsters/monster.cpp @@ -20,6 +20,7 @@ #include "lua/callbacks/event_callback.hpp" #include "lua/callbacks/events_callbacks.hpp" #include "map/spectators.hpp" +#include "io/iobestiary.hpp" int32_t Monster::despawnRange; int32_t Monster::despawnRadius; @@ -2684,3 +2685,22 @@ void Monster::onExecuteAsyncTasks() { onThink_async(); } } + +bool Monster::checkCanApplyCharm(const std::shared_ptr &player, charmRune_t charmRune) const { + if (!player) { + return false; + } + + uint16_t playerCharmRaceid = player->parseRacebyCharm(charmRune, false, 0); + if (playerCharmRaceid != 0) { + const auto &monsterType = g_monsters().getMonsterType(getName()); + if (monsterType && playerCharmRaceid == monsterType->info.raceid) { + const auto &charm = g_iobestiary().getBestiaryCharm(charmRune); + if (charm) { + return true; + } + } + } + + return false; +} diff --git a/src/creatures/monsters/monster.hpp b/src/creatures/monsters/monster.hpp index 89305e7f5f7..6942d992baa 100644 --- a/src/creatures/monsters/monster.hpp +++ b/src/creatures/monsters/monster.hpp @@ -235,6 +235,7 @@ class Monster final : public Creature { void setCriticalDamage(uint16_t damage); uint16_t getCriticalDamage() const; + bool checkCanApplyCharm(const std::shared_ptr &player, charmRune_t charmRune) const; protected: void onExecuteAsyncTasks() override; diff --git a/src/game/game.cpp b/src/game/game.cpp index 2021182c68a..8b9dc713d55 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -7610,7 +7610,7 @@ void Game::sendMessages( } if (tmpPlayer == attackerPlayer && attackerPlayer != targetPlayer) { - buildMessageAsAttacker(target, damage, message, ss, damageString); + buildMessageAsAttacker(target, damage, message, ss, damageString, attackerPlayer); } else if (tmpPlayer == targetPlayer) { buildMessageAsTarget(attacker, damage, attackerPlayer, targetPlayer, message, ss, damageString); } else { @@ -7685,13 +7685,21 @@ void Game::buildMessageAsTarget( void Game::buildMessageAsAttacker( const std::shared_ptr &target, const CombatDamage &damage, TextMessage &message, - std::stringstream &ss, const std::string &damageString + std::stringstream &ss, const std::string &damageString, const std::shared_ptr &attackerPlayer ) const { ss.str({}); ss << ucfirst(target->getNameDescription()) << " loses " << damageString << " due to your " << (damage.critical ? "critical " : " ") << "attack."; if (damage.extension) { ss << " " << damage.exString; } + + if (damage.critical) { + const auto &targetMonster = target->getMonster(); + if (targetMonster && attackerPlayer && targetMonster->checkCanApplyCharm(attackerPlayer, CHARM_LOW)) { + ss << " (low blow charm)"; + } + } + if (damage.fatal) { ss << " (Onslaught)"; } diff --git a/src/game/game.hpp b/src/game/game.hpp index 0c58b5962de..9e02ae80e7a 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -901,7 +901,7 @@ class Game { void buildMessageAsAttacker( const std::shared_ptr &target, const CombatDamage &damage, TextMessage &message, - std::stringstream &ss, const std::string &damageString + std::stringstream &ss, const std::string &damageString, const std::shared_ptr &attackerPlayer ) const; void buildMessageAsTarget(