Newer
Older
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
--- Interface to add ToME combat system
module(..., package.seeall, class.make)
--- Checks what to do with the target
-- Talk ? attack ? displace ?
function _M:bumpInto(target, x, y)
local reaction = self:reactionToward(target)
if reaction < 0 then
if target.encounterAttack and self.player then self:onWorldEncounter(target, x, y) return end
if game.player == self and ((not config.settings.tome.actor_based_movement_mode and game.bump_attack_disabled) or (config.settings.tome.actor_based_movement_mode and self.bump_attack_disabled)) then return end
return self:useTalent(self.T_ATTACK, nil, nil, nil, target)
-- Talk ? Bump ?
if self.player and target.on_bump then
target:on_bump(self)
elseif self.player and target.can_talk then
if target.can_talk_only_once then target.can_talk = nil end
if target.can_talk_only_once then target.can_talk = nil end
elseif self.move_others and not target.cant_be_moved then
if target.move_others and self ~= game.player then return end
dg
committed
-- Check we can both walk in the tile we will end up in
local blocks = game.level.map:checkAllEntitiesLayersNoStop(target.x, target.y, "block_move", self)
for kind, v in pairs(blocks) do if kind[1] ~= Map.ACTOR and v then return end end
blocks = game.level.map:checkAllEntitiesLayersNoStop(self.x, self.y, "block_move", target)
for kind, v in pairs(blocks) do if kind[1] ~= Map.ACTOR and v then return end end
local tx, ty, sx, sy = target.x, target.y, self.x, self.y
target.x = nil target.y = nil
self.x = nil self.y = nil
if target.describeFloor then target:describeFloor(target.x, target.y, true) end
if self.describeFloor then self:describeFloor(self.x, self.y, true) end
if self:attr("bump_swap_speed_divide") then
self:useEnergy(game.energy_to_act * self:combatMovementSpeed(x, y) / self:attr("bump_swap_speed_divide"))
self.did_energy = true
end
end
end
end
--- Makes the death happen!
- attack: increases chances to hit against high defense
- defense: increases chances to miss against high attack power
- armor: direct reduction of damage done
- armor penetration: reduction of target's armor
- damage: raw damage done
]]
function _M:attackTarget(target, damtype, mult, noenergy, force_unharmed)
-- Break before we do the blow, because it might start step up, we dont want to insta-cancel it
self:breakStepUp()
if self:attr("feared") then
if not noenergy then
self:useEnergy(game.energy_to_act * speed)
self.did_energy = true
end
game.logSeen(self, "%s is too afraid to attack.", self.name:capitalize())
return false
end
dg
committed
if self:attr("terrified") and rng.percent(self:attr("terrified")) then
if not noenergy then
self:useEnergy(game.energy_to_act)
self.did_energy = true
end
game.logSeen(self, "%s is too terrified to attack.", self.name:capitalize())
return false
end
-- Cancel stealth early if we are noticed
if self:isTalentActive(self.T_STEALTH) and target:canSee(self) then
self:useTalent(self.T_STEALTH)
self.changed = true
game.logPlayer(self, "%s notices you at the last moment!", target.name:capitalize())
end
-- Change attack type if using gems
if not damtype and self:getInven(self.INVEN_GEM) then
local gems = self:getInven(self.INVEN_GEM)
local types = {}
for i = 1, #gems do
if gems[i] and gems[i].attack_type then types[#types+1] = gems[i].attack_type end
end
if #types > 0 then
damtype = rng.table(types)
end
elseif not damtype and self:attr("force_melee_damage_type") then
damtype = self:attr("force_melee_damage_type")
local hd = {"Combat:attackTarget", target=target, damtype=damtype, mult=mult, noenergy=noenergy}
if self:triggerHook(hd) then
speed, hit, damtype, mult = hd.speed, hd.hit, hd.damtype, hd.mult
end
if not speed and self:isTalentActive(self.T_GESTURE_OF_PAIN) then
dg
committed
print("[ATTACK] attacking with Gesture of Pain")
local t = self:getTalentFromId(self.T_GESTURE_OF_PAIN)
dg
committed
speed, hit = t.attack(self, t, target)
break_stealth = true
end
local mean
if not speed and not self:attr("disarmed") and not self:isUnarmed() and not force_unharmed then
-- All weapons in main hands
if self:getInven(self.INVEN_MAINHAND) then
for i, o in ipairs(self:getInven(self.INVEN_MAINHAND)) do
local combat = self:getObjectCombat(o, "mainhand")
if combat and not o.archery then
speed = math.max(speed or 0, s)
hit = hit or h
if hit and not sound then sound = combat.sound
elseif not hit and not sound_miss then sound_miss = combat.sound_miss end
if not combat.no_stealth_break then break_stealth = true end
-- All weapons in off hands
-- Offhand attacks are with a damage penalty, that can be reduced by talents
if self:getInven(self.INVEN_OFFHAND) then
for i, o in ipairs(self:getInven(self.INVEN_OFFHAND)) do
if o.special_combat and o.subtype == "shield" and self:knowTalent(self.T_STONESHIELD) then combat = o.special_combat end
if combat and not o.archery then
local s, h = self:attackTargetWith(target, combat, damtype, offmult)
speed = math.max(speed or 0, s)
hit = hit or h
if hit and not sound then sound = combat.sound
elseif not hit and not sound_miss then sound_miss = combat.sound_miss end
if not combat.no_stealth_break then break_stealth = true end
local combat = self:getObjectCombat(nil, "barehand")
if hit and not sound then sound = combat.sound
elseif not hit and not sound_miss then sound_miss = combat.sound_miss end
if not combat.no_stealth_break then break_stealth = true end
if speed and not noenergy then
self:useEnergy(game.energy_to_act * speed)
self.did_energy = true
end
if sound then game:playSoundNear(self, sound)
elseif sound_miss then game:playSoundNear(self, sound_miss) end
game:playSoundNear(self, self.on_hit_sound or "actions/melee_hit_squish")
if self.sound_moam and rng.chance(7) then game:playSoundNear(self, self.sound_moam) end
if self:isTalentActive(self.T_CLEAVE) then
if self:attr("unharmed_attack_on_hit") then
local v = self:attr("unharmed_attack_on_hit")
self:attr("unharmed_attack_on_hit", -v)
if rng.percent(60) then self:attackTarget(target, nil, 1, true, true) end
--- Determines the combat field to use for this item
function _M:getObjectCombat(o, kind)
if kind == "barehand" then return self.combat end
if kind == "mainhand" then return o.combat end
if kind == "offhand" then return o.combat end
return nil
end
--- Computes a logarithmic chance to hit, opposing chance to hit to chance to miss
-- This will be used for melee attacks, physical and spell resistance
if atk < 0 then atk = 0 end
if def < 0 then def = 0 end
if atk == 0 then atk = 1 end
local hit = nil
factor = factor or 5
local one = 1 / (1 + math.exp(-(atk - def) / 7))
local two = 0
hit = 50 * (one + two)
--Tells the tier difference between two values
function _M:crossTierEffect(eff_id, apply_power, apply_save, use_given_e)
local q = game.player:hasQuest("tutorial-combat-stats")
if q and not q:isCompleted("final-lesson")then
return
end
local ct_effect
local save_for_effects = {
physical = "combatPhysicalResist",
magical = "combatSpellResist",
mental = "combatMentalResist",
}
local cross_tier_effects = {
combatPhysicalResist = self.EFF_OFFBALANCE,
combatSpellResist = self.EFF_SPELLSHOCKED,
combatMentalResist = self.EFF_BRAINLOCKED,
}
local e = self.tempeffect_def[eff_id]
if not apply_power or not save_for_effects[e.type] then return end
local save = self[apply_save or save_for_effects[e.type]](self, true)
if use_given_e then
ct_effect = self["EFF_"..e.name]
else
ct_effect = cross_tier_effects[save_for_effects[e.type]]
end
local dur = self:getTierDiff(apply_power, save)
end
function _M:getTierDiff(atk, def)
atk = math.floor(atk)
def = math.floor(def)
return math.max(0, math.max(math.ceil(atk/20), 1) - math.max(math.ceil(def/20), 1))
end
--New, simpler checkHit that relies on rescaleCombatStats() being used elsewhere
function _M:checkHit(atk, def, min, max, factor, p)
if atk < 0 then atk = 0 end
if def < 0 then def = 0 end
if game.player:hasQuest("tutorial-combat-stats") then
min = 0
max = 100
end --ensures predictable combat for the tutorial
print("checkHit", atk, def)
local hit = math.ceil(50 + 2.5 * (atk - def))
hit = util.bound(hit, min, max)
print("=> chance to hit", hit)
return rng.percent(hit), hit
end
if not target:attr("evasion") or self == target then return end
local evasion = target:attr("evasion")
print("checkEvasion", evasion, target.level, self.level)
evasion = evasion * (target.level / self.level)
print("=> evasion chance", evasion)
return rng.percent(evasion)
end
function _M:attackTargetWith(target, weapon, damtype, mult, force_dam)
damtype = damtype or (weapon and weapon.damtype) or DamageType.PHYSICAL
local mode = "other"
if self:hasShield() then mode = "shield"
elseif self:hasTwoHandedWeapon() then mode = "twohanded"
elseif self:hasDualWeapon() then mode = "dualwield"
end
self.turn_procs.weapon_type = {kind=weapon and weapon.talented or "unknown", mode=mode}
local atk, def = self:combatAttack(weapon), target:combatDefense()
-- add stalker damage and attack bonus
local effStalker = self:hasEffect(self.EFF_STALKER)
if effStalker and effStalker.target == target then
local t = self:getTalentFromId(self.T_STALK)
atk = atk + t.getAttackChange(self, t, effStalker.bonus)
mult = mult * t.getStalkedDamageMultiplier(self, t, effStalker.bonus)
end
dg
committed
-- add marked prey damage and attack bonus
local effPredator = self:hasEffect(self.EFF_PREDATOR)
if effPredator and effPredator.type == target.type then
if effPredator.subtype == target.subtype then
mult = mult + effPredator.subtypeDamageChange
atk = atk + effPredator.subtypeAttackChange
else
mult = mult + effPredator.typeDamageChange
atk = atk + effPredator.typeAttackChange
end
end
if target.knowTalent and target:knowTalent(target.T_GESTURE_OF_GUARDING) then
local t = target:getTalentFromId(target.T_GESTURE_OF_GUARDING)
mult = mult * (100 + t.getDamageChange(target, t)) / 100
end
dg
committed
-- track weakness for hate bonus before the target removes it
local effGloomWeakness = target:hasEffect(target.EFF_GLOOM_WEAKNESS)
local dam, apr, armor = force_dam or self:combatDamage(weapon), self:combatAPR(weapon), target:combatArmor()
print("[ATTACK] to ", target.name, " :: ", dam, apr, armor, def, "::", mult)
-- check repel
local repelled = false
if target:isTalentActive(target.T_REPEL) then
local t = target:getTalentFromId(target.T_REPEL)
repelled = t.isRepelled(target, t)
end
if repelled then
game.logSeen(target, "%s repels an attack from %s.", target.name:capitalize(), self.name)
elseif self:checkEvasion(target) then
evaded = true
game.logSeen(target, "%s evades %s.", target.name:capitalize(), self.name)
elseif self:checkHit(atk, def) and (self:canSee(target) or self:attr("blind_fight") or rng.chance(3)) then
print("[ATTACK] raw dam", dam, "versus", armor, pres, "with APR", apr)
dam = math.max(dam * pres - armor, 0) + (dam * (1 - pres))
local damrange = self:combatDamageRange(weapon)
dam = rng.range(dam, dam * damrange)
print("[ATTACK] after range", dam)
if target:hasEffect(target.EFF_COUNTERSTRIKE) then
dam = dam * 2
local eff = target.tmp[target.EFF_COUNTERSTRIKE]
eff.nb = eff.nb - 1
if eff.nb == 0 then target:removeEffect(target.EFF_COUNTERSTRIKE) end
print("[ATTACK] after counterstrike", dam)
end
if weapon and weapon.inc_damage_type then
for t, idt in pairs(weapon.inc_damage_type) do
if target.type.."/"..target.subtype == t or target.type == t then dam = dam + dam * idt / 100 break end
end
print("[ATTACK] after inc by type", dam)
end
if crit then game.logSeen(self, "#{bold}#%s performs a critical strike!#{normal}#", self.name:capitalize()) end
-- Phasing, percent of weapon damage bypasses shields
dg
committed
-- It's done like this because onTakeHit has no knowledge of the weapon
if weapon and weapon.phasing then
self:attr("damage_shield_penetrate", weapon.phasing)
end
local oldproj = DamageType:getProjectingFor(self)
if self.__talent_running then DamageType:projectingFor(self, {project_type={talent=self.__talent_running}}) end
-- Damage conversion?
-- Reduces base damage but converts it into another damage type
local conv_dam
local conv_damtype
dg
committed
local total_conversion = 0
if weapon and weapon.convert_damage then
for typ, conv in pairs(weapon.convert_damage) do
if dam > 0 then
conv_dam = math.min(dam, dam * (conv / 100))
dg
committed
total_conversion = total_conversion + conv_dam
conv_damtype = typ
dam = dam - conv_dam
if conv_dam > 0 then
DamageType:get(conv_damtype).projector(self, target.x, target.y, conv_damtype, math.max(0, conv_dam))
end
end
end
end
if dam > 0 then
DamageType:get(damtype).projector(self, target.x, target.y, damtype, math.max(0, dam))
end
if self.__talent_running then DamageType:projectingFor(self, oldproj) end
dg
committed
-- remove phasing
if weapon and weapon.phasing then
self:attr("damage_shield_penetrate", -weapon.phasing)
end
dg
committed
-- add damage conversion back in so the total damage still gets passed
if total_conversion > 0 then
dam = dam + total_conversion
end
local srcname = game.level.map.seens(self.x, self.y) and self.name:capitalize() or "Something"
game.logSeen(target, "%s misses %s.", srcname, target.name)
-- cross-tier effect for accuracy vs. defense
local tier_diff = self:getTierDiff(atk, target:combatDefense(false, target:attr("combat_def_ct")))
if hitted and not target.dead and tier_diff > 0 then
local reapplied = false
-- silence the apply message it if the target already has the effect
for eff_id, p in pairs(target.tmp) do
local e = target.tempeffect_def[eff_id]
if e.desc == "Off-guard" then
reapplied = true
end
end
target:setEffect(target.EFF_OFFGUARD, tier_diff, {}, reapplied)
-- handle stalk targeting for hits (also handled in Actor for turn end effects)
if hitted and target ~= self then
if effStalker then
-- mark if stalkee was hit
effStalker.hit = effStalker.hit or effStalker.target == target
elseif self:isTalentActive(self.T_STALK) then
local stalk = self:isTalentActive(self.T_STALK)
if not stalk.hit then
-- mark a new target
stalk.hit = true
stalk.hit_target = target
elseif stalk.hit_target ~= target then
-- more than one target; clear it
stalk.hit_target = nil
end
end
end
-- Spread diseases
if hitted and self:knowTalent(self.T_CARRIER) and rng.percent(4 * self:getTalentLevelRaw(self.T_CARRIER)) then
-- Use epidemic talent spreading
local t = self:getTalentFromId(self.T_EPIDEMIC)
t.do_spread(self, t, target)
end
if hitted and not target.dead and weapon and weapon.melee_project then for typ, dam in pairs(weapon.melee_project) do
if dam > 0 then
DamageType:get(typ).projector(self, target.x, target.y, typ, dam)
end
end end
if hitted and not target.dead then for typ, dam in pairs(self.melee_project) do
if dam > 0 then
DamageType:get(typ).projector(self, target.x, target.y, typ, dam)
end
if hitted and not target.dead and self:knowTalent(self.T_WEAPON_OF_LIGHT) and self:isTalentActive(self.T_WEAPON_OF_LIGHT) then
if self:getPositive() >= 3 then
local dam = 7 + self:getTalentLevel(self.T_WEAPON_OF_LIGHT) * self:combatSpellpower(0.092)
DamageType:get(DamageType.LIGHT).projector(self, target.x, target.y, DamageType.LIGHT, dam)
self:incPositive(-3)
if hitted and not target.dead and self:knowTalent(self.T_SHADOW_COMBAT) and self:isTalentActive(self.T_SHADOW_COMBAT) and self:getMana() > 0 then
local dam = 2 + self:combatTalentSpellDamage(self.T_SHADOW_COMBAT, 2, 50)
local mana = 2
if self:getMana() > mana then
DamageType:get(DamageType.DARKNESS).projector(self, target.x, target.y, DamageType.DARKNESS, dam)
self:incMana(-mana)
end
if hitted and not target.dead and self:knowTalent(self.T_WEAPON_FOLDING) and self:isTalentActive(self.T_WEAPON_FOLDING) then
local t = self:getTalentFromId(self.T_WEAPON_FOLDING)
local dam = t.getDamage(self, t)
DamageType:get(DamageType.TEMPORAL).projector(self, target.x, target.y, DamageType.TEMPORAL, dam)
self:incParadox(- t.getParadoxReduction(self, t))
-- Ruin
if hitted and not target.dead and self:knowTalent(self.T_RUIN) and self:isTalentActive(self.T_RUIN) then
local t = self:getTalentFromId(self.T_RUIN)
local dam = t.getDamage(self, t)
DamageType:get(DamageType.DRAINLIFE).projector(self, target.x, target.y, DamageType.DRAINLIFE, dam)
end
dg
committed
if hitted and not target.dead and self:knowTalent(self.T_ARCANE_COMBAT) and self:isTalentActive(self.T_ARCANE_COMBAT) then
local t = self:getTalentFromId(self.T_ARCANE_COMBAT)
t.do_trigger(self, t, target)
if hitted and not target.dead and weapon and weapon.talent_on_hit and next(weapon.talent_on_hit) and not self.turn_procs.melee_talent then
for tid, data in pairs(weapon.talent_on_hit) do
if rng.percent(data.chance) then
self.turn_procs.melee_talent = true
self:forceUseTalent(tid, {ignore_cd=true, ignore_energy=true, force_target=target, force_level=data.level, ignore_ressources=true})
-- On crit talent
if hitted and crit and not target.dead and weapon and weapon.talent_on_crit and next(weapon.talent_on_crit) and not self.turn_procs.melee_talent then
for tid, data in pairs(weapon.talent_on_crit) do
if rng.percent(data.chance) then
self.turn_procs.melee_talent = true
self:forceUseTalent(tid, {ignore_cd=true, ignore_energy=true, force_target=target, force_level=data.level, ignore_ressources=true})
end
end
end
if hitted and self:attr("shattering_impact") and (not self.shattering_impact_last_turn or self.shattering_impact_last_turn < game.turn) then
local dam = dam * self.shattering_impact
self:project({type="ball", radius=1, selffire=false}, target.x, target.y, DamageType.PHYSICAL, dam)
self.shattering_impact_last_turn = game.turn
if hitted and weapon and weapon.burst_on_hit then
for typ, dam in pairs(weapon.burst_on_hit) do
if dam > 0 then
self:project({type="ball", radius=1, friendlyfire=false}, target.x, target.y, typ, dam)
end
end
end
-- Critical Burst (generally more damage then burst on hit and larger radius)
if hitted and crit and weapon and weapon.burst_on_crit then
for typ, dam in pairs(weapon.burst_on_crit) do
if dam > 0 then
self:project({type="ball", radius=2, friendlyfire=false}, target.x, target.y, typ, dam)
end
if hitted and crit and weapon and self:knowTalent(self.T_ARCANE_DESTRUCTION) then
local typ = rng.table{{DamageType.FIRE,"ball_fire"}, {DamageType.LIGHTNING,"ball_lightning_beam"}, {DamageType.ARCANE,"ball_arcane"}}
self:project({type="ball", radius=2, friendlyfire=false}, target.x, target.y, typ[1], self:combatSpellpower() * 2)
game.level.map:particleEmitter(target.x, target.y, 2, typ[2], {radius=2, tx=target.x, ty=target.y})
-- Onslaught
if hitted and self:attr("onslaught") then
local dir = util.getDir(target.x, target.y, self.x, self.y) or 6
local lx, ly = util.coordAddDir(self.x, self.y, util.dirSides(dir, self.x, self.y).left)
local rx, ry = util.coordAddDir(self.x, self.y, util.dirSides(dir, self.x, self.y).right)
local lt, rt = game.level.map(lx, ly, Map.ACTOR), game.level.map(rx, ry, Map.ACTOR)
if target:checkHit(self:combatAttack(weapon), target:combatPhysicalResist(), 0, 95, 10) and target:canBe("knockback") then
target:knockback(self.x, self.y, self:attr("onslaught"))
target:crossTierEffect(target.EFF_OFFBALANCE, self:combatAttack())
if lt and lt:checkHit(self:combatAttack(weapon), lt:combatPhysicalResist(), 0, 95, 10) and lt:canBe("knockback") then
lt:knockback(self.x, self.y, self:attr("onslaught"))
if rt and rt:checkHit(self:combatAttack(weapon), rt:combatPhysicalResist(), 0, 95, 10) and rt:canBe("knockback") then
rt:knockback(self.x, self.y, self:attr("onslaught"))
-- Reactive target on hit damage
if hitted then for typ, dam in pairs(target.on_melee_hit) do
if type(dam) == "number" then if dam > 0 then DamageType:get(typ).projector(target, self.x, self.y, typ, dam) end
elseif dam.dam and dam.dam > 0 then DamageType:get(typ).projector(target, self.x, self.y, typ, dam)
-- Acid splash
if hitted and target:knowTalent(target.T_ACID_BLOOD) then
local t = target:getTalentFromId(target.T_ACID_BLOOD)
t.do_splash(target, t, self)
end
-- Bloodbath
if hitted and crit and self:knowTalent(self.T_BLOODBATH) then
local t = self:getTalentFromId(self.T_BLOODBATH)
t.do_bloodbath(self, t)
end
-- Mortal Terror
if hitted and not target.dead and self:knowTalent(self.T_MORTAL_TERROR) then
local t = self:getTalentFromId(self.T_MORTAL_TERROR)
t.do_terror(self, t, target, dam)
end
-- Dwarves stoneskin
if hitted and not target.dead and target:attr("auto_stoneskin") and rng.percent(15) then
target:setEffect(target.EFF_STONE_SKIN, 5, {power=target:attr("auto_stoneskin")})
if hitted and not target.dead and self:knowTalent(self.T_CONDUIT) and self:isTalentActive(self.T_CONDUIT) and self.use_psi_combat then
local t = self:getTalentFromId(self.T_CONDUIT)
-- Exploit Weakness
if hitted and not target.dead and self:knowTalent(self.T_EXPLOIT_WEAKNESS) and self:isTalentActive(self.T_EXPLOIT_WEAKNESS) then
local t = self:getTalentFromId(self.T_EXPLOIT_WEAKNESS)
t.do_weakness(self, t, target)
end
-- Lacerating Strikes
if hitted and not target.dead and self:knowTalent(self.T_LACERATING_STRIKES) then
local t = self:getTalentFromId(self.T_LACERATING_STRIKES)
t.do_cut(self, t, target, dam)
end
-- Scoundrel's Strategies
if hitted and not target.dead and self:knowTalent(self.T_SCOUNDREL) and target:hasEffect(target.EFF_CUT) then
local t = self:getTalentFromId(self.T_SCOUNDREL)
t.do_scoundrel(self, t, target)
end
if hitted and weapon and weapon.special_on_hit and weapon.special_on_hit.fct and (not target.dead or weapon.special_on_hit.on_kill) then
if hitted and crit and weapon and weapon.special_on_crit and weapon.special_on_crit.fct and (not target.dead or weapon.special_on_crit.on_kill) then
weapon.special_on_crit.fct(weapon, self, target)
end
if hitted and weapon and weapon.special_on_kill and weapon.special_on_kill.fct and target.dead then
weapon.special_on_kill.fct(weapon, self, target)
end
dg
committed
if hitted and crit and not target.dead and self:knowTalent(self.T_BACKSTAB) and not target:attr("stunned") and rng.percent(self:getTalentLevel(self.T_BACKSTAB) * 3) then
if target:canBe("stun") then
target:setEffect(target.EFF_STUNNED, 3, {apply_power=self:combatAttack()})
end
end
if hitted and not target.dead and self.vile_poisons and next(self.vile_poisons) and target:canBe("poison") then
local tid = rng.table(table.keys(self.vile_poisons))
if tid then
local t = self:getTalentFromId(tid)
t.proc(self, t, target, weapon)
end
end
if hitted and not target.dead and target:attr("stamina_regen_when_hit") then target:incStamina(target.stamina_regen_when_hit) end
if hitted and not target.dead and target:attr("mana_regen_when_hit") then target:incMana(target.mana_regen_when_hit) end
if hitted and not target.dead and target:attr("equilibrium_regen_when_hit") then target:incEquilibrium(-target.equilibrium_regen_when_hit) end
if hitted and not target.dead and target:attr("psi_regen_when_hit") then target:incPsi(target.psi_regen_when_hit) end
if hitted and not target.dead and target:attr("hate_regen_when_hit") then target:incHate(target.hate_regen_when_hit) end
if hitted and self:attr("stamina_regen_on_hit") then self:incStamina(self.stamina_regen_on_hit) end
if hitted and self:attr("mana_regen_on_hit") then self:incMana(self.mana_regen_on_hit) end
if hitted and not target.dead and target:knowTalent(target.T_STONESHIELD) then
local t = target:getTalentFromId(target.T_STONESHIELD)
local m, mm, e, em = t.getValues(self, t)
target:incMana(math.min(dam * m, mm))
target:incEquilibrium(-math.min(dam * e, em))
end
-- Ablative Armor
if hitted and not target.dead and target:attr("carbon_spikes") then
local t = target:getTalentFromId(target.T_CARBON_SPIKES)
t.do_carbonLoss(target, t)
-- Set Up
if not hitted and not target.dead and not evaded and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:hasEffect(target.EFF_DEFENSIVE_MANEUVER) then
local t = target:getTalentFromId(target.T_SET_UP)
local power = t.getPower(target, t)
self:setEffect(self.EFF_SET_UP, 2, {src = target, power=power})
end
if not hitted and not target.dead and not evaded and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:knowTalent(target.T_COUNTER_ATTACK) and rng.percent(target:getTalentLevel(target.T_COUNTER_ATTACK) * (5 + target:getCun(5, true))) then
game.logSeen(self, "%s counters the attack!", target.name:capitalize())
local t = target:getTalentFromId(target.T_COUNTER_ATTACK)
target:attackTarget(self, nil, t.getDamage(target, t), true)
-- Gesture of Guarding counterattack
if hitted and not target.dead and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:knowTalent(target.T_GESTURE_OF_GUARDING) then
local t = target:getTalentFromId(target.T_GESTURE_OF_GUARDING)
t.on_hit(target, t, self)
end
if not hitted and not target.dead and not evaded and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:knowTalent(target.T_DEFENSIVE_THROW) and rng.percent(target:getTalentLevel(target.T_DEFENSIVE_THROW) * (5 + target:getCun(5, true))) then
local t = target:getTalentFromId(target.T_DEFENSIVE_THROW)
t.do_throw(target, self, t)
end
-- Greater Weapon Focus
local gwf = self:hasEffect(self.EFF_GREATER_WEAPON_FOCUS)
if hitted and not target.dead and weapon and gwf and not gwf.inside and rng.percent(gwf.chance) then
game.logSeen(self, "%s focuses and gains an extra blow!", self.name:capitalize())
self:attackTargetWith(target, weapon, damtype, mult)
gwf.inside = nil
end
if hitted and game.level.data.zero_gravity and rng.percent(util.bound(dam, 0, 100)) then
-- Roll with it
if hitted and target:attr("knockback_on_hit") and not target.turn_procs.roll_with_it and rng.percent(util.bound(dam, 0, 100)) then
local ox, oy = self.x, self.y
game:onTickEnd(function()
target:knockback(ox, oy, 1)
if not target:hasEffect(target.EFF_WILD_SPEED) then target:setEffect(target.EFF_WILD_SPEED, 1, {power=200}) end
end)
dg
committed
-- Weakness hate bonus
if hitted and effGloomWeakness and effGloomWeakness.hateBonus or 0 > 0 then
self:incHate(effGloomWeakness.hateBonus)
game.logPlayer(self, "#F53CBE#You revel in attacking a weakened foe! (+%d hate)", effGloomWeakness.hateBonus)
effGloomWeakness.hateBonus = nil
end
dg
committed
-- Rampage
if hitted and crit then
local eff = self:hasEffect(self.EFF_RAMPAGE)
if eff and not eff.critHit and eff.actualDuration < eff.maxDuration and self:knowTalent(self.T_BRUTALITY) then
dg
committed
game.logPlayer(self, "#F53CBE#Your rampage is invigorated by your fierce attack! (+1 duration)")
eff.critHit = true
eff.actualDuration = eff.actualDuration + 1
eff.dur = eff.dur + 1
end
end
dg
committed
-- Marked Prey
if hitted and not target.dead and effPredator and effPredator.type == target.type then
if effPredator.subtype == target.subtype then
-- Anatomy stun
if effPredator.subtypeStunChance > 0 and rng.percent(effPredator.subtypeStunChance) then
if target:canBe("stun") then
target:setEffect(target.EFF_STUNNED, 3, {})
else
game.logSeen(target, "%s resists the stun!", target.name:capitalize())
end
end
dg
committed
-- Outmaneuver
if effPredator.subtypeOutmaneuverChance > 0 and rng.percent(effPredator.subtypeOutmaneuverChance) then
local t = self:getTalentFromId(self.T_OUTMANEUVER)
target:setEffect(target.EFF_OUTMANEUVERED, t.getDuration(self, t), { physicalResistChange=t.getPhysicalResistChange(self, t), statReduction=t.getStatReduction(self, t) })
dg
committed
end
else
-- Outmaneuver
if effPredator.typeOutmaneuverChance > 0 and rng.percent(effPredator.typeOutmaneuverChance) then
local t = self:getTalentFromId(self.T_OUTMANEUVER)
target:setEffect(target.EFF_OUTMANEUVERED, t.getDuration(self, t), { physicalResistChange=t.getPhysicalResistChange(self, t), statReduction=t.getStatReduction(self, t) })
dg
committed
end
end
end
dg
committed
if hitted and crit and target:hasEffect(target.EFF_DISMAYED) then
target:removeEffect(target.EFF_DISMAYED)
end
if hitted and not target.dead then
-- Curse of Madness: Twisted Mind
if self.hasEffect and self:hasEffect(self.EFF_CURSE_OF_MADNESS) then
local eff = self:hasEffect(self.EFF_CURSE_OF_MADNESS)
local def = self.tempeffect_def[self.EFF_CURSE_OF_MADNESS]
def.doConspirator(self, eff, target)
end
if target.hasEffect and target:hasEffect(target.EFF_CURSE_OF_MADNESS) then
local eff = target:hasEffect(target.EFF_CURSE_OF_MADNESS)
local def = target.tempeffect_def[target.EFF_CURSE_OF_MADNESS]
def.doConspirator(target, eff, self)
end
-- Curse of Nightmares: Suffocate
if self.hasEffect and self:hasEffect(self.EFF_CURSE_OF_NIGHTMARES) then
local eff = self:hasEffect(self.EFF_CURSE_OF_NIGHTMARES)
local def = self.tempeffect_def[self.EFF_CURSE_OF_NIGHTMARES]
def.doSuffocate(self, eff, target)
end
if target.hasEffect and target:hasEffect(target.EFF_CURSE_OF_NIGHTMARES) then
local eff = target:hasEffect(target.EFF_CURSE_OF_NIGHTMARES)
local def = target.tempeffect_def[target.EFF_CURSE_OF_NIGHTMARES]
def.doSuffocate(target, eff, self)
end
end
local hd = {"Combat:attackTargetWith", hitted=hitted, target=target, weapon=weapon, damtype=damtype, mult=mult, dam=dam}
if self:triggerHook(hd) then hitted = hd.hitted end
-- Visual feedback
if hitted then game.level.map:particleEmitter(target.x, target.y, 1, "melee_attack", {color=target.blood_color}) end
self.turn_procs.weapon_type = nil
sword = Talents.T_WEAPONS_MASTERY,
axe = Talents.T_WEAPONS_MASTERY,
mace = Talents.T_WEAPONS_MASTERY,
knife = Talents.T_KNIFE_MASTERY,
whip = Talents.T_EXOTIC_WEAPONS_MASTERY,
trident = Talents.T_EXOTIC_WEAPONS_MASTERY,
bow = Talents.T_BOW_MASTERY,
sling = Talents.T_SLING_MASTERY,
staff = Talents.T_STAFF_MASTERY,
dg
committed
dream = Talents.T_DREAM_CRUSHER,
}
--- Checks weapon training
function _M:combatCheckTraining(weapon)
if not weapon_talents[weapon.talented] then return 0 end
--- Fake denotes a check not actually being made, used by character sheets etc.
local add = 0
if self:hasDualWeapon() and self:knowTalent(self.T_DUAL_WEAPON_DEFENSE) then
add = add + 4 + (self:getTalentLevel(self.T_DUAL_WEAPON_DEFENSE) * self:getDex()) / 12
end
add = add + (self:checkOnDefenseCall("defense") or 0)
end
if self:knowTalent(self.T_TACTICAL_EXPERT) then
local t = self:getTalentFromId(self.T_TACTICAL_EXPERT)
add = add + t.do_tact_update(self, t)
end
if self:knowTalent(self.T_CORRUPTED_SHELL) then
add = add + self:getCon() / 3
end
if self:knowTalent(self.T_STEADY_MIND) then
local t = self:getTalentFromId(self.T_STEADY_MIND)
add = add + t.getDefense(self, t)
end
if self:isTalentActive(Talents.T_SURGE) then
local t = self:getTalentFromId(self.T_SURGE)
add = add + t.getDefenseChange(self, t)
end
local d = math.max(0, self.combat_def + (self:getDex() - 10) * 0.35 + add + (self:getLck() - 50) * 0.4)
if self:hasLightArmor() and self:knowTalent(self.T_MOBILE_DEFENCE) then
d = d * (1 + self:getTalentLevel(self.T_MOBILE_DEFENCE) * 0.08)
end
if self:knowTalent(self.T_MISDIRECTION) then
d = d * (1 + self:getTalentLevel(self.T_MISDIRECTION) * (0.02 * (1 + self:getCun() / 85)))
end
function _M:combatDefense(fake, add)
local base_defense = self:combatDefenseBase(true)
if not fake then base_defense = self:combatDefenseBase() end
local d = math.max(0, base_defense + (add or 0))
return self:rescaleCombatStats(d)
end
--- Gets the defense ranged
function _M:combatDefenseRanged(fake, add)
local base_defense = self:combatDefenseBase(true)
if not fake then base_defense = self:combatDefenseBase() end
local d = math.max(0, base_defense + (self.combat_def_ranged or 0) + (add or 0))
if self:hasHeavyArmor() and self:knowTalent(self.T_ARMOUR_TRAINING) then
local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING)
add = add + at.getArmor(self, at)
if self:knowTalent(self.T_GOLEM_ARMOUR) then
local ga = Talents:getTalentFromId(Talents.T_GOLEM_ARMOUR)
add = add + ga.getArmor(self, ga)
end
if self:knowTalent(self.T_CARBON_SPIKES) and self:isTalentActive(self.T_CARBON_SPIKES) then
add = add + self.carbon_armor
end
if self:knowTalent(self.T_ARMOUR_OF_SHADOWS) and not game.level.map.lites(self.x, self.y) then
add = add + 30
end
--- Gets armor hardiness
-- This is how much % of a blow we can reduce with armor
function _M:combatArmorHardiness()
local add = 0
if self:hasHeavyArmor() and self:knowTalent(self.T_ARMOUR_TRAINING) then
local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING)
add = add + at.getArmorHardiness(self, at)
if self:knowTalent(self.T_GOLEM_ARMOUR) then
local ga = Talents:getTalentFromId(Talents.T_GOLEM_ARMOUR)
add = add + ga.getArmorHardiness(self, ga)
end
if self:hasLightArmor() and self:knowTalent(self.T_MOBILE_DEFENCE) then
add = add + self:getTalentLevel(self.T_MOBILE_DEFENCE) * 6
end
if self:knowTalent(self.T_ARMOUR_OF_SHADOWS) and not game.level.map.lites(self.x, self.y) then
add = add + 50
end
function _M:combatAttackBase(weapon, ammo)
return 4 + self.combat_atk + self:getTalentLevel(Talents.T_WEAPON_COMBAT) * 10 + (weapon.atk or 0) + (ammo and ammo.atk or 0) + (self:getLck() - 50) * 0.4
end
function _M:combatAttack(weapon, ammo)
if self.use_psi_combat then stats = self:getCun(100, true) - 10
elseif weapon and weapon.wil_attack then stats = self:getWil(100, true) - 10
local d = self:combatAttackBase(weapon, ammo) + stats
if self:attr("dazed") then d = d / 2 end
return self:rescaleCombatStats(d)
function _M:combatAttackRanged(weapon, ammo)
local stats
if self.use_psi_combat then stats = self:getCun(100, true) - 10
elseif weapon and weapon.wil_attack then stats = self:getWil(100, true) - 10
else stats = self:getDex(100, true) - 10
end
local d = self:combatAttackBase(weapon, ammo) + stats + (self.combat_atk_ranged or 0)
function _M:combatAttackStr(weapon, ammo)
local d = self:combatAttackBase(weapon, ammo) + (self:getStr(100, true) - 10)
if self:attr("dazed") then d = d / 2 end
return self:rescaleCombatStats(d)
function _M:combatAttackDex(weapon, ammo)
local d = self:combatAttackBase(weapon, ammo) + (self:getDex(100, true) - 10)
if self:attr("dazed") then d = d / 2 end
return self:rescaleCombatStats(d)
function _M:combatAttackMag(weapon, ammo)
local d = self:combatAttackBase(weapon, ammo) + (self:getMag(100, true) - 10)
if self:attr("dazed") then d = d / 2 end
return self:rescaleCombatStats(d)
--- Gets the armor penetration
function _M:combatAPR(weapon)
local addapr = 0
return self.combat_apr + (weapon.apr or 0) + addapr
end
--- Gets the weapon speed
function _M:combatSpeed(weapon)
return (weapon.physspeed or 1) / self.combat_physspeed
end
--- Gets the crit rate
function _M:combatCrit(weapon)
if weapon.talented and self:knowTalent(Talents.T_LETHALITY) then
dg
committed
local crit = self.combat_physcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + (weapon.physcrit or 1) + addcrit
-- This currently beefs up high-end damage values to make up for the combat stat rescale nerf.
function _M:rescaleDamage(dam)
if dam <= 0 then return dam end
-- return dam * (1 - math.log10(dam * 2) / 7) --this is the old version, pre-combat-stat-rescale
return dam ^ 1.04
end
--Diminishing-returns method of scaling combat stats, observing this rule: the first twenty ranks cost 1 point each, the second twenty cost two each, and so on. This is much, much better for players than some logarithmic mess, since they always know exactly what's going on, and there are nice breakpoints to strive for.
function _M:rescaleCombatStats(raw_combat_stat_value)
local x = raw_combat_stat_value
local tiers = 5 -- Just increase this if you want to add high-level content that allows for combat stat scores over 100.
--return math.floor(math.min(x, 20) + math.min(math.max((x-20), 0)/2, 20) + math.min(math.max((x-60), 0)/3, 20) + math.min(math.max((x-120), 0)/4, 20) + math.min(math.max((x-200), 0)/5, 20)) --Five terms of the summation below.
local total = 0
for i = 1, tiers do
local sub = 20*(i*(i-1)/2)
total = total + math.min(math.max(x-sub, 0)/i, 20)
end
return total
--- Gets the damage
function _M:combatDamage(weapon)
local sub_cun_to_str = false
if weapon.talented and weapon.talented == "knife" and self:knowTalent(Talents.T_LETHALITY) then sub_cun_to_str = true end
if self.use_psi_combat and stat == "str" then stat = "wil" end
if self.use_psi_combat and stat == "dex" then stat = "cun" end
totstat = totstat + self:getStat(stat) * mod
if self:knowTalent(self.T_GREATER_TELEKINETIC_GRASP) then
local g = self:getTalentFromId(self.T_GREATER_TELEKINETIC_GRASP)
totstat = totstat * g.stat_sub(self, g)
else
if self:knowTalent(self.T_SUPERPOWER) then
totstat = totstat + self:getStat("wil") * 0.3
end
if self:knowTalent(self.T_ARCANE_MIGHT) then
totstat = totstat + self:getStat("mag") * 0.5
end
local talented_mod = math.sqrt(self:combatCheckTraining(weapon) / 5) / 2 + 1
local power = math.max((weapon.dam or 1), 1)
power = (math.sqrt(power / 10) - 1) * 0.5 + 1
--print(("[COMBAT DAMAGE] power(%f) totstat(%f) talent_mod(%f)"):format(power, totstat, talented_mod))
return self:rescaleDamage(0.3*(self:combatPhysicalpower(nil, weapon) + totstat) * power * talented_mod)
function _M:combatPhysicalpower(mod, weapon, add)
add = add or 0
if self:knowTalent(Talents.T_ARCANE_DESTRUCTION) then
add = add + self:combatSpellpower() * self:getTalentLevel(Talents.T_ARCANE_DESTRUCTION) / 7
if self:isTalentActive(Talents.T_BLOOD_FRENZY) then
add = add + self.blood_frenzy
end
if self:knowTalent(self.T_EMPTY_HAND) and self:isUnarmed() then
local t = self:getTalentFromId(self.T_EMPTY_HAND)
add = add + t.getDamage(self, t)
end
if self:attr("psychometry_power") then
add = add + self:attr("psychometry_power")
end
if not weapon then
local inven = self:getInven(self.INVEN_MAINHAND)
if inven and inven[1] then weapon = self:getObjectCombat(inven[1], "mainhand") else weapon = self.combat end
add = add + 10 * self:combatCheckTraining(weapon)
local d = (self.combat_dam > 0 and self.combat_dam or 0) + add + self:getStr()
--- Gets damage based on talent
function _M:combatTalentPhysicalDamage(t, base, max)
-- Compute at "max"
local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
-- Compute real
return self:rescaleDamage((base + (self:combatPhysicalpower())) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod)
end
function _M:combatSpellpower(mod, add)
add = add or 0
if self:knowTalent(self.T_ARCANE_CUNNING) then
add = add + (15 + self:getTalentLevel(self.T_ARCANE_CUNNING) * 5) * self:getCun() / 100
if self:knowTalent(self.T_SHADOW_CUNNING) then
add = add + (15 + self:getTalentLevel(self.T_SHADOW_CUNNING) * 5) * self:getCun() / 100
end
if self:hasEffect(self.EFF_BLOODLUST) then
add = add + self:hasEffect(self.EFF_BLOODLUST).dur
local am = 1
if self:attr("spellpower_reduction") then am = 1 / (1 + self:attr("spellpower_reduction")) end
local d = (self.combat_spellpower > 0 and self.combat_spellpower or 0) + add + self:getMag()
return self:rescaleCombatStats(d) * mod * am
end
--- Gets damage based on talent
function _M:combatTalentSpellDamage(t, base, max, spellpower_override)
-- Compute at "max"
local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
-- Compute real
return self:rescaleDamage((base + (spellpower_override or self:combatSpellpower())) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod)
--- Gets weapon damage mult based on talent
function _M:combatTalentWeaponDamage(t, base, max, t2)
if t2 then t2 = t2 / 2 else t2 = 0 end
local diff = max - base
local mult = base + diff * math.sqrt((self:getTalentLevel(t) + t2) / 5)
-- print("[TALENT WEAPON MULT]", self:getTalentLevel(t), base, max, t2, mult)
local offmult = (mult or 1) / 2
if self:knowTalent(Talents.T_DUAL_WEAPON_TRAINING) then
offmult = math.max(offmult, (mult or 1) / (2 - (math.min(self:getTalentLevel(Talents.T_DUAL_WEAPON_TRAINING), 8) / 6)))
end
if self:knowTalent(Talents.T_CORRUPTED_STRENGTH) then
offmult = math.max(offmult, (mult or 1) / (2 - (math.min(self:getTalentLevel(Talents.T_CORRUPTED_STRENGTH), 8) / 9)))
if self:hasEffect(self.EFF_CURSE_OF_MADNESS) then
local eff = self:hasEffect(self.EFF_CURSE_OF_MADNESS)
if eff.level >= 1 and eff.unlockLevel >= 1 then
local def = self.tempeffect_def[self.EFF_CURSE_OF_MADNESS]
offmult = offmult + ((mult or 1) * def.getOffHandMultChange(eff.level) / 100)
end
end
if combat and combat.no_offhand_penalty then
return math.max(1, offmult)
else
return offmult
end
--- Gets fatigue
function _M:combatFatigue()
if self.fatigue < 0 then return 0 end
dg
committed
local crit = self.combat_spellcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + 1
dg
committed
--- Gets mindcrit
dg
committed
function _M:combatMindCrit(add)
local add = add or 0
dg
committed
if self:knowTalent(self.T_GESTURE_OF_POWER) then
local t = self:getTalentFromId(self.T_GESTURE_OF_POWER)
add = t.getMindCritChange(self, t)
end
dg
committed
local crit = self.combat_mindcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + 1 + add
dg
committed
end
return 1 / self.combat_spellspeed
function _M:combatMindSpeed()
return 1 / self.combat_mindspeed
--- Gets summon speed
function _M:combatSummonSpeed()
return math.max(1 - ((self:attr("fast_summons") or 0) / 100), 0.1)
end
--- Computes physical crit chance reduction
function _M:combatCritReduction()
local crit_reduction = 0
if self:hasHeavyArmor() and self:knowTalent(self.T_ARMOUR_TRAINING) then
local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING)
crit_reduction = crit_reduction + at.getCriticalChanceReduction(self, at)
if self:knowTalent(self.T_GOLEM_ARMOUR) then
local ga = Talents:getTalentFromId(Talents.T_GOLEM_ARMOUR)
crit_reduction = crit_reduction + ga.getCriticalChanceReduction(self, ga)
end
end
if self:attr("combat_crit_reduction") then
crit_reduction = crit_reduction + self:attr("combat_crit_reduction")
end
return crit_reduction
end
function _M:physicalCrit(dam, weapon, target, atk, def, add_chance, crit_power_add)
local chance = self:combatCrit(weapon) + (add_chance or 0)
crit_power_add = crit_power_add or 0
if target:hasEffect(target.EFF_DISMAYED) then
chance = 100
end
if self:knowTalent(self.T_BACKSTAB) and target:attr("stunned") then chance = chance + self:getTalentLevel(self.T_BACKSTAB) * 10 end
if target:attr("combat_crit_vulnerable") then
chance = chance + target:attr("combat_crit_vulnerable")
if target:hasEffect(target.EFF_SET_UP) then
local p = target:hasEffect(target.EFF_SET_UP)
if p and p.src == self then
chance = chance - target:combatCritReduction()
-- Scoundrel's Strategies
if self:attr("cut") and target:knowTalent(self.T_SCOUNDREL) then
chance = chance - (5 + (target:getTalentLevel(self.T_SCOUNDREL)*5))
end
if self:isTalentActive(self.T_STEALTH) and self:knowTalent(self.T_SHADOWSTRIKE) then
chance = 100
crit_power_add = crit_power_add + self:getTalentLevel(self.T_SHADOWSTRIKE) / 7
end
if target:hasEffect(target.EFF_OFFGUARD) then
crit_power_add = crit_power_add + 0.1
self.turn_procs.is_crit = "physical"
self.turn_procs.crit_power = (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dam = dam * (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
if self:knowTalent(self.T_EYE_OF_THE_TIGER) then self:triggerTalent(self.T_EYE_OF_THE_TIGER, nil, "physical") end
local chance = self:combatSpellCrit() + (add_chance or 0)
if self:isTalentActive(self.T_STEALTH) and self:knowTalent(self.T_SHADOWSTRIKE) then
chance = 100
crit_power_add = crit_power_add + self:getTalentLevel(self.T_SHADOWSTRIKE) / 7
end
self.turn_procs.crit_power = (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dam = dam * (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dg
committed
game.logSeen(self, "#{bold}#%s's spell attains critical power!#{normal}#", self.name:capitalize())
if self:attr("mana_on_crit") then self:incMana(self:attr("mana_on_crit")) end
if self:attr("vim_on_crit") then self:incVim(self:attr("vim_on_crit")) end
if self:attr("paradox_on_crit") then self:incParadox(self:attr("paradox_on_crit")) end
if self:attr("positive_on_crit") then self:incPositive(self:attr("positive_on_crit")) end
if self:attr("negative_on_crit") then self:incNegative(self:attr("negative_on_crit")) end
if self:attr("spellsurge_on_crit") then
local power = self:attr("spellsurge_on_crit")
self:setEffect(self.EFF_SPELLSURGE, 10, {power=power, max=power*3})
end
if self:isTalentActive(self.T_BLOOD_FURY) then
local t = self:getTalentFromId(self.T_BLOOD_FURY)
t.on_crit(self, t)
end
if self:isTalentActive(self.T_CORONA) then
local t = self:getTalentFromId(self.T_CORONA)
if self:knowTalent(self.T_EYE_OF_THE_TIGER) then self:triggerTalent(self.T_EYE_OF_THE_TIGER, nil, "spell") end
local chance = self:combatMindCrit() + (add_chance or 0)
local crit = false
if self:isTalentActive(self.T_STEALTH) and self:knowTalent(self.T_SHADOWSTRIKE) then
chance = 100
crit_power_add = crit_power_add + self:getTalentLevel(self.T_SHADOWSTRIKE) / 7
end
print("[MIND CRIT %]", chance)
if rng.percent(chance) then
self.turn_procs.crit_power = (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dam = dam * (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dg
committed
game.logSeen(self, "#{bold}#%s's mind surges with critical power!#{normal}#", self.name:capitalize())
if self:attr("hate_on_crit") then self:incHate(self:attr("hate_on_crit")) end
if self:attr("psi_on_crit") then self:incPsi(self:attr("psi_on_crit")) end
if self:attr("equilibrium_on_crit") then self:incEquilibrium(self:attr("equilibrium_on_crit")) end
if self:knowTalent(self.T_EYE_OF_THE_TIGER) then self:triggerTalent(self.T_EYE_OF_THE_TIGER, nil, "mind") end
if self:knowTalent(self.T_LIVING_MUCUS) then self:callTalent(self.T_LIVING_MUCUS, "on_crit") end
dg
committed
--- Do we get hit by our own AOE ?
function _M:spellFriendlyFire()
local chance = (self:getLck() - 50) * 0.2
if self:isTalentActive(self.T_SPELLCRAFT) then chance = chance + self:getTalentLevelRaw(self.T_SPELLCRAFT) * 20 end
dg
committed
chance = 100 - chance
print("[SPELL] friendly fire chance", chance)
dg
committed
end
dg
committed
function _M:combatMindpower(mod, add)
dg
committed
add = add or 0
dg
committed
if self:knowTalent(self.T_SUPERPOWER) then
add = add + 25 * self:getStr() / 100
end
if self:knowTalent(self.T_GESTURE_OF_POWER) then
local t = self:getTalentFromId(self.T_GESTURE_OF_POWER)
dg
committed
add = add + t.getMindpowerChange(self, t)
dg
committed
end
if self:attr("psychometry_power") then
add = add + self:attr("psychometry_power")
end
dg
committed
local d = (self.combat_mindpower > 0 and self.combat_mindpower or 0) + add + self:getWil() * 0.7 + self:getCun() * 0.4
end
--- Gets damage based on talent
function _M:combatTalentMindDamage(t, base, max)
-- Compute at "max"
local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
-- Compute real
return self:rescaleDamage((base + (self:combatMindpower())) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod)
--- Gets damage based on talent
function _M:combatTalentStatDamage(t, stat, base, max)
-- Compute at "max"
local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
-- Compute real
local dam = (base + (self:getStat(stat))) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod
dam = dam * (1 - math.log10(dam * 2) / 7)
dam = dam ^ (1 / 1.04)
return self:rescaleDamage(dam)
function _M:combatTalentIntervalDamage(t, stat, min, max, stat_weight)
local stat_weight = stat_weight or 0.5
local dam = min + (max - min)*((stat_weight * self:getStat(stat)/100) + (1 - stat_weight) * self:getTalentLevel(t)/6.5)
dam = dam * (1 - math.log10(dam * 2) / 7)
dam = dam ^ (1 / 1.04)
return self:rescaleDamage(dam)
end
--- Gets damage based on talent, stat, and interval
function _M:combatStatTalentIntervalDamage(t, stat, min, max, stat_weight)
local stat_weight = stat_weight or 0.5
scaled_stat = self[stat](self)
return self:rescaleDamage(min + (max - min)*((stat_weight * self[stat](self)/100) + (1 - stat_weight) * self:getTalentLevel(t)/6.5))
--- Fake denotes a check not actually being made, used by character sheets etc.
function _M:combatPhysicalResist(fake)
add = add + (self:checkOnDefenseCall("physical") or 0)
end
if self:knowTalent(self.T_CORRUPTED_SHELL) then
add = add + self:getCon() / 3
end
add = add + util.bound(self.money / (90 - self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 5), 0, self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 7)
local d = self.combat_physresist + (self:getCon() + self:getStr() + (self:getLck() - 50) * 0.5) * 0.35 + add
if self:attr("dazed") then d = d / 2 end
local total = self:rescaleCombatStats(d)
-- Psionic Balance
if self:knowTalent(self.T_BALANCE) then
local t = self:getTalentFromId(self.T_BALANCE)
local ratio = t.getBalanceRatio(self, t)
total = (1 - ratio)*total + self:combatMentalResist(fake)*ratio
end
return total
--- Fake denotes a check not actually being made, used by character sheets etc.
function _M:combatSpellResist(fake)
add = add + (self:checkOnDefenseCall("spell") or 0)
end
if self:knowTalent(self.T_CORRUPTED_SHELL) then
add = add + self:getCon() / 3
end
add = add + util.bound(self.money / (90 - self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 5), 0, self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 7)
local d = self.combat_spellresist + (self:getMag() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.35 + add
if self:attr("dazed") then d = d / 2 end
local total = self:rescaleCombatStats(d)
-- Psionic Balance
if self:knowTalent(self.T_BALANCE) then
local t = self:getTalentFromId(self.T_BALANCE)
local ratio = t.getBalanceRatio(self, t)
total = (1 - ratio)*total + self:combatMentalResist(fake)*ratio
end
return total
--- Fake denotes a check not actually being made, used by character sheets etc.
function _M:combatMentalResist(fake)
add = add + (self:checkOnDefenseCall("mental") or 0)
end
if self:knowTalent(self.T_CORRUPTED_SHELL) then
add = add + self:getCon() / 3
end
if self:knowTalent(self.T_STEADY_MIND) then
local t = self:getTalentFromId(self.T_STEADY_MIND)
add = add + t.getMental(self, t)
end
add = add + util.bound(self.money / (90 - self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 5), 0, self:getTalentLevelRaw(self.T_POWER_IS_MONEY) * 7)
local d = self.combat_mentalresist + (self:getCun() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.35 + add
if self:attr("dazed") then d = d / 2 end
return self:rescaleCombatStats(d)
-- Called when a Save or Defense is checked
function _M:checkOnDefenseCall(type)
local add = 0
if self:knowTalent(self.T_SPIN_FATE) then
print("Spin Fate", type)
local t = self:getTalentFromId(self.T_SPIN_FATE)
t.do_spin_fate(self, t, type)
end
return add
end
--- Returns the resistance
function _M:combatGetResist(type)
local power = 100
if self.force_use_resist and self.force_use_resist ~= type then
type = self.force_use_resist
power = self.force_use_resist_percent or 100
end
local a = (self.resists.all or 0) / 100
local b = (self.resists[type] or 0) / 100
local r = math.min(100 * (1 - (1 - a) * (1 - b)), (self.resists_cap.all or 0) + (self.resists_cap[type] or 0))
return r * power / 100
function _M:combatMovementSpeed(x, y)
if game.level and game.level.data.zero_gravity then
local movement_speed = self.movement_speed
if x and y and game.level.map:checkAllEntities(x, y, "creepingDark") and self:knowTalent(self.T_DARK_VISION) then
local t = self:getTalentFromId(self.T_DARK_VISION)
movement_speed = movement_speed + t.getMovementSpeedChange(self, t)
end
return mult * (self.base_movement_speed or 1) / movement_speed
--- Computes see stealth
function _M:combatSeeStealth()
local bonus = 0
if self:knowTalent(self.T_PIERCING_SIGHT) then bonus = bonus + 5 + self:getTalentLevel(self.T_PIERCING_SIGHT) * self:getCun(15, true) end
return self.level / 2 + self:getCun(25, true) + (self:attr("see_stealth") or 0) + bonus
end
--- Computes see invisible
function _M:combatSeeInvisible()
local bonus = 0
if self:knowTalent(self.T_PIERCING_SIGHT) then bonus = bonus + 5 + self:getTalentLevel(self.T_PIERCING_SIGHT) * self:getCun(15, true) end
return (self:attr("see_invisible") or 0) + bonus
end
--- Check if the actor has a gem bomb in quiver
function _M:hasAlchemistWeapon()
if not self:getInven("QUIVER") then return nil, "no ammo" end
local ammo = self:getInven("QUIVER")[1]
if not ammo or not ammo.alchemist_power then
return nil, "bad or no ammo"
end
return ammo
end
if self:attr("disarmed") then
return nil, "disarmed"
end
if not self:getInven("MAINHAND") then return end
local weapon = self:getInven("MAINHAND")[1]
if not weapon or weapon.subtype ~= "staff" then
return nil
end
return weapon
end
--- Check if the actor has an axe weapon
function _M:hasAxeWeapon()
if self:attr("disarmed") then
return nil, "disarmed"
end
if not self:getInven("MAINHAND") then return end
local weapon = self:getInven("MAINHAND")[1]
if not weapon or (weapon.subtype ~= "battleaxe" and weapon.subtype ~= "waraxe") then
return nil
end
return weapon
end
--- Check if the actor has a cursed weapon
function _M:hasCursedWeapon()
if self:attr("disarmed") then
return nil, "disarmed"
end
if not self:getInven("MAINHAND") then return end
local weapon = self:getInven("MAINHAND")[1]
if not weapon or not weapon.curse then
local t = self:getTalentFromId(self.T_DEFILING_TOUCH)
if not t.canCurseItem(self, t, weapon) then return nil end
return weapon
end
--- Check if the actor has a cursed weapon
function _M:hasCursedOffhandWeapon()
if self:attr("disarmed") then
return nil, "disarmed"
end
if not self:getInven("OFFHAND") then return end
local weapon = self:getInven("OFFHAND")[1]
if not weapon or not weapon.combat or not weapon.curse then
local t = self:getTalentFromId(self.T_DEFILING_TOUCH)
if not t.canCurseItem(self, t, weapon) then return nil end
--- Check if the actor has a two handed weapon
if self:attr("disarmed") then
return nil, "disarmed"
end
local weapon = self:getInven("MAINHAND")[1]
if not weapon or not weapon.twohanded then
return nil
end
return weapon
end
--- Check if the actor has a shield
function _M:hasShield()
if self:attr("disarmed") then
return nil, "disarmed"
end
if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return end
local shield = self:getInven("OFFHAND")[1]
if not shield or not shield.special_combat then
return nil
end
return shield
-- Check if actor is unarmed
function _M:isUnarmed()
local unarmed = true
if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return end
local weapon = self:getInven("MAINHAND")[1]
local offweapon = self:getInven("OFFHAND")[1]
if weapon or offweapon then
unarmed = false
end
return unarmed
end
dg
committed
-- Get the number of free hands the actor has
function _M:getFreeHands()
if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return 0 end
dg
committed
local weapon = self:getInven("MAINHAND")[1]
local offweapon = self:getInven("OFFHAND")[1]
if weapon and offweapon then return 0 end
dg
committed
if weapon or offweapon then return 1 end
return 2
end
if self:attr("disarmed") then
return nil, "disarmed"
end
if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return end
local weapon = self:getInven("MAINHAND")[1]
local offweapon = self:getInven("OFFHAND")[1]
if not weapon or not offweapon or not weapon.combat or not offweapon.combat then
return nil
end
return weapon, offweapon
end
--- Check if the actor uses psiblades
function _M:hasPsiblades(main, off)
if self:attr("disarmed") then
return nil, "disarmed"
end
local weapon, offweapon = nil, nil
if main then
if not self:getInven("MAINHAND") then return end
weapon = self:getInven("MAINHAND")[1]
if not weapon or not weapon.combat or not weapon.psiblade_active then return nil, "unactivated psiblade" end
end
if off then
if not self:getInven("OFFHAND") then return end
offweapon = self:getInven("OFFHAND")[1]
if not offweapon or not offweapon.combat or not offweapon.psiblade_active then return nil, "unactivated psiblade" end
end
return weapon, offweapon
end
--- Check if the actor has a light armor
function _M:hasLightArmor()
if not self:getInven("BODY") then return end
local armor = self:getInven("BODY")[1]
if not armor or (armor.subtype ~= "cloth" and armor.subtype ~= "light") then
return nil
end
return armor
end
--- Check if the actor has a heavy armor
function _M:hasHeavyArmor()
if not self:getInven("BODY") then return end
local armor = self:getInven("BODY")[1]
if not armor or (armor.subtype ~= "heavy" and armor.subtype ~= "massive") then
end
--- Check if the actor has a massive armor
function _M:hasMassiveArmor()
if not self:getInven("BODY") then return end
local armor = self:getInven("BODY")[1]
if not armor or armor.subtype ~= "massive" then
return nil
end
-- Unarmed Combat; this handles grapple checks and building combo points
-- Builds Comob; reduces the cooldown on all unarmed abilities on cooldown by one
function _M:buildCombo()
local duration = 3
local power = 1
-- Combo String bonuses
if self:knowTalent(self.T_COMBO_STRING) then
local t = self:getTalentFromId(self.T_COMBO_STRING)
local t = self:getTalentFromId(self.T_RELENTLESS_STRIKES)
self:setEffect(self.EFF_COMBO, duration, {power=power})
end
function _M:getCombo(combo)
local combo = 0
local p = self:hasEffect(self.EFF_COMBO)
combo = p.cur_power
end
return combo
end
function _M:clearCombo()
if self:hasEffect(self.EFF_COMBO) then
self:removeEffect(self.EFF_COMBO)
end
end
-- Check to see if the target is already being grappled; many talents have extra effects on grappled targets
function _M:isGrappled(source)
local p = self:hasEffect(self.EFF_GRAPPLED)
if p and p.src == source then
return true
else
return false
end
end
-- Breaks active grapples; called by a few talents that involve a lot of movement
function _M:breakGrapples()
if self:hasEffect(self.EFF_GRAPPLING) then
local p = self:hasEffect(self.EFF_GRAPPLING)
if p.trgt then
p.trgt:removeEffect(p.trgt.EFF_GRAPPLED)
end
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
self:removeEffect(self.EFF_GRAPPLING)
end
end
-- grapple size check; compares attackers size and targets size
function _M:grappleSizeCheck(target)
size = target.size_category - self.size_category
if size > 1 then
game.logSeen(target, "%s fails because %s is too big!", self.name:capitalize(), target.name:capitalize())
return true
else
return false
end
end
-- Starts the grapple
function _M:startGrapple(target)
-- pulls boosted grapple effect from the clinch talent if known
if self:knowTalent(self.T_CLINCH) then
local t = self:getTalentFromId(self.T_CLINCH)
power = t.getPower(self, t)
duration = t.getDuration(self, t)
hitbonus = self:getTalentLevel(t)/2
else
power = 5
duration = 4
hitbonus = 0
end
-- Breaks the grapple before reapplying
if self:hasEffect(self.EFF_GRAPPLING) then
self:removeEffect(self.EFF_GRAPPLING, true)
target:setEffect(target.EFF_GRAPPLED, duration, {src=self, power=power}, true)
self:setEffect(self.EFF_GRAPPLING, duration, {trgt=target}, true)
elseif target:canBe("pin") then
target:setEffect(target.EFF_GRAPPLED, duration, {src=self, power=power, apply_power=self:combatPhysicalpower()})
target:crossTierEffect(target.EFF_GRAPPLED, self:combatPhysicalpower())
self:setEffect(self.EFF_GRAPPLING, duration, {trgt=target})
return true
else
game.logSeen(target, "%s resists the grapple!", target.name:capitalize())
return false
end