Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[=[
- === UModManager Info Section ===
- id = "ExtendedHooks"
- name = "ExtendedHooks"
- description = [[Adds several new scripting hooks into existing functions.
- Made by 7Soul ([email protected])]]
- version = "0.1.0"
- priority = 5
- modifiedFields = { "defineSkill", "defineTrait", "defineCharClass", "defineCondition", "defineSpell", "Dungeon.loadInitFile", "Skill.init", "PartyMove.enter", "Champion.init", "Champion.saveState", "Champion.loadState", "Champion.recomputeStats", "RangedAttackComponent.init", "Condition.init", "Condition.loadState", "Condition.saveState", "CustomCondition.init", "NewGameMenu.startGame" }
- overwrittenFields = { "Stats", "StatNames", "Champion.getAccuracyWithAttacks","Champion.getCritChanceWithAttack", "Champion.getDamageText", "Champion.getDamageWithAttack", "Champion.updateHerbalism", "Champion.isDualWielding", "Champion.attack", "Champion.damage", "Champion.castSpell", "Champion.getLoad", "MonsterComponent.onAttackedByChampion", "MonsterComponent.damage", "computeDamage", "getDamageRange", "damageTile", "ItemComponent.dragItemToThrowZone", "ItemComponent.projectileHitEntity", "EquipmentItemComponent.recomputeStats", "EquipmentItemComponent.onComputeAccuracy, "EquipmentItemComponent.onComputeCritChance", "MeleeAttackComponent.start", "RangedAttackComponent.start", "ThrowAttackComponent.start", "FirearmAttackComponent.start", "BombItemComponent.explode", "CastSpellComponent.start", "CraftPotionComponent.brewPotion", "Champion.setConditionValue", "Condition.update", "CustomCondition.start", "CustomCondition.restart", "CustomCondition.stop", "CustomCondition.tick", "CustomCondition.recomputeStats", "ProtectiveShieldCondition.recomputeStats", "FireShieldCondition.recomputeStats", "FrostShieldCondition.recomputeStats", "ShockShieldCondition.recomputeStats", "PoisonShieldCondition.recomputeStats", "BearFormCondition.update", "Spell.castSpell", "BuiltInSpell.shield", "BuiltInSpell.light", "BuiltInSpell.darkness", "BuiltInSpell.darkbolt", "BuiltInSpell.forceField", "BuiltInSpell.fireburst", "BuiltInSpell.fireball", "Spell_meteorStorm", "BuiltInSpell.meteorStorm", "BuiltInSpell.fireShield", "BuiltInSpell.iceShards", "BuiltInSpell.frostbolt", "BuiltInSpell.frostShield", "BuiltInSpell.shock", "BuiltInSpell.invisibility", "BuiltInSpell.lightningBolt", "BuiltInSpell.shockShield", "BuiltInSpell.poisonCloud", "BuiltInSpell.poisonBolt", "BuiltInSpell.poisonShield", "BuiltInSpell.dispel", "Spell.elementalShield" }
- === End of Mod Info ===
- ]=]
- -- Stats = {
- -- "health",
- -- "max_health",
- -- "energy",
- -- "max_energy",
- -- "strength",
- -- "dexterity",
- -- "vitality",
- -- "willpower",
- -- "protection",
- -- "evasion",
- -- "resist_fire",
- -- "resist_cold",
- -- "resist_poison",
- -- "resist_shock",
- -- "max_load",
- -- "exp_rate",
- -- "food_rate",
- -- "health_regeneration_rate",
- -- "energy_regeneration_rate",
- -- "cooldown_rate",
- -- "critical_multiplier",
- -- "dual_wielding",
- -- }
- -- StatNames = {
- -- "Health",
- -- "Max Health",
- -- "Energy",
- -- "Max Energy",
- -- "Strength",
- -- "Dexterity",
- -- "Vitality",
- -- "Willpower",
- -- "Protection",
- -- "Evasion",
- -- "Resist Fire",
- -- "Resist Cold",
- -- "Resist Poison",
- -- "Resist Shock",
- -- "Max Load",
- -- "Experience Rate",
- -- "Food Consumption Rate",
- -- "Health Regeneration Rate",
- -- "Energy Regeneration Rate",
- -- "Cooldown Rate",
- -- "Critical Multiplier",
- -- "Dual Wielding",
- -- }
- config.developer = true
- defineProxyClass{
- class = "PartyComponent",
- description = "The singular party component that holds the four champions. Champion's position in the party can change when party formation is changed. However champions can be identified with their ordinal number that never changes.",
- baseClass = "Component",
- methods = {
- { "heal" },
- { "rest" },
- { "wakeUp", "boolean" },
- { "move", "number" },
- { "turn", "number" },
- { "isResting" },
- { "isMoving" },
- { "isFalling" },
- { "isClimbing" },
- { "isIdle" },
- { "isCarrying", "string" },
- { "setMovementSpeed", "number" },
- { "getMovementSpeed" },
- { "swapChampions", {"number", "number"} },
- { "getChampion", "number" },
- { "getChampionByOrdinal", "number" },
- { "playScreenEffect", "string" },
- { "shakeCamera", {"number", "number"} },
- { "knockback", "number" },
- { "grapple", "number" },
- },
- hooks = {
- "onCastSpell(self, champion, spell)",
- "onDamage(self, champion, damage, damageType)",
- "onDie(self, champion)",
- "onAttack(self, champion, action, slot)",
- "onLevelUp(self, champion)",
- "onReceiveCondition(self, champion, condition)",
- "onDrawGui(self, context)",
- "onDrawInventory(self, context, champion)",
- "onDrawStats(self, context, champion)",
- "onDrawSkills(self, context, champion)",
- "onDrawTraits(self, context, champion)",
- "onPickUpItem(self, item)",
- "onProjectileHit(self, champion, item, damage, damageType)",
- "onRest(self)",
- "onWakeUp(self)",
- "onTurn(self, direction)",
- "onMove(self, direction)",
- "onCalculateDamageWithAttack(self, champion, weapon, attack, power)", -- new
- "onBrewPotion(self, potion, champion)", -- new
- "onMultiplyHerbs(self, herbRates, champion)", -- new
- "onLoadDefaultParty(self)",
- },
- }
- defineProxyClass{
- class = "Champion",
- description = "Champion's attributes, skills, traits, conditions and other statistics can be accessed through this class.",
- methods = {
- { "setEnabled", "boolean" },
- { "setName", "string" },
- { "setRace", "string" },
- { "setSex", "string" },
- { "setClass", "string" },
- { "setHealth", "number" },
- { "setEnergy", "number" },
- { "getEnabled" },
- { "getName" },
- { "getRace" },
- { "getSex" },
- { "getClass" },
- { "getDualClass" },
- { "getLevel" },
- { "getExp" },
- { "getOrdinal" },
- { "setPortrait", "string" },
- { "isAlive" },
- { "gainExp", "number" },
- { "resetExp" },
- { "levelUp" },
- { "setSkillPoints", "number" },
- { "getSkillPoints" },
- { "addSkillPoints", "number" },
- { "getSkillLevel", {"string", "number"} },
- { "getSkillLevel", "string" },
- { "trainSkill", {"string", "number"} },
- { "addTrait", "string" },
- { "removeTrait", "string" },
- { "hasTrait", "string" },
- { "setFood", "number" },
- { "getFood" },
- { "consumeFood", "number" },
- { "modifyFood", "number" },
- { "setCondition", "string" },
- { "removeCondition", "string" },
- { "hasCondition", "string" },
- { "setConditionValue", {"string", "number"} },
- { "getConditionValue", "string" },
- { "isBodyPartWounded", "number" },
- { "damage", {"number", "string"} },
- { "playDamageSound" },
- { "playSound", "string" },
- { "showAttackResult", {"any", "string"} },
- { "setBaseStat", {"string", "number"} },
- { "modifyBaseStat", {"string", "number"} },
- { "upgradeBaseStat", {"string", "number"} },
- { "addStatModifier", {"string", "number"} },
- { "getBaseStat", "string" },
- { "getCurrentStat", "string" },
- { "regainHealth", "number" },
- { "regainEnergy", "number" },
- { "getHealth" },
- { "getMaxHealth" },
- { "getEnergy" },
- { "getMaxEnergy" },
- { "getProtection" },
- { "getEvasion" },
- { "getResistance", "string" },
- { "getLoad" },
- { "getMaxLoad" },
- { "getArmorSetPiecesEquipped", "string" },
- { "isArmorSetEquipped", "string" },
- { "isDualWielding" },
- { "isReadyToAttack", "number" },
- { "insertItem", {"number", "ItemComponent"} },
- { "removeItem", "ItemComponent" },
- { "removeItemFromSlot", "number" },
- { "swapItems", {"number", "number"} },
- { "swapWeaponSets" },
- { "getItem", "number" },
- { "getOtherHandItem", "number" },
- { "playHealingIndicator" },
- { "castSpell", "number" },
- { "attack", {"number", "boolean"} },
- { "getConditionStacks" }, -- new
- { "getData" }, -- new
- { "setData" }, -- new
- { "addData" }, -- new
- },
- }
- defineProxyClass{
- class = "MonsterComponent",
- baseClass = "Component",
- description = "Makes the object a monster. Requires Model, Animation and a brain component. Most monsters (depending on the brain) also require MonsterMove, MonsterTurn and MonsterAttack components.",
- methods = {
- { "setAIState", "string" },
- { "setHealth", "number" },
- { "setMaxHealth", "number" },
- { "setLevel", "number" },
- { "setCondition", "string" },
- { "setCapsuleHeight", "number" },
- { "setCapsuleRadius", "number" },
- { "setCollisionRadius", "number" },
- { "setDeathEffect", "string" },
- { "setDieSound" }, -- accepts string or nil
- { "setEvasion", "number" },
- { "setExp", "number" },
- { "setFlying", "boolean" },
- { "setFootstepSound", "string" },
- { "setHitEffect", "string" },
- { "setHitSound", "string" },
- { "setIdleAnimation", "string" },
- { "setImmunities", "table" },
- { "setLootDrop", "table" },
- { "setMeshName", "string" },
- { "setHeadRotation", "vec" },
- { "setProtection", "number" },
- { "setResistances", "table" },
- { "setShape", "string" },
- { "setSwarm", "boolean" },
- { "setMonsterFlag", {"string", "boolean"} },
- { "getHealth" },
- { "getMaxHealth" },
- { "getLevel" },
- { "getCapsuleHeight" },
- { "getCapsuleRadius" },
- { "getCollisionRadius" },
- { "getDeathEffect" },
- { "getDieSound" },
- { "getEvasion" },
- { "getExp" },
- { "getFlying" },
- { "getFootstepSound" },
- { "getGroupSize" },
- { "getHitEffect" },
- { "getHitSound" },
- { "getIdleAnimation" },
- { "getImmunities" },
- { "getLootDrop" },
- { "getMeshName" },
- { "getHeadRotation" },
- { "getProtection" },
- { "getResistance" },
- { "getShape" },
- { "getSwarm" },
- { "getCurrentAction" },
- { "getMonsterFlag", "string" },
- { "addItem", "ItemComponent" },
- { "removeItem", "ItemComponent" },
- { "dropAllItems" },
- { "performAction", "string" }, -- TODO: optional parameters!
- { "moveForward" },
- { "moveBackward" },
- { "turnLeft" },
- { "turnRight" },
- { "strafeLeft" },
- { "strafeRight" },
- { "attack" },
- { "shootProjectile", {"string", "number", "number"} },
- { "throwItem", {"string", "number", "number"} },
- { "showDamageText", {"string"} },
- { "die" }, -- optional arg: gainExp
- { "isInBackRow" },
- { "isIdle" },
- { "isAlive" },
- { "isChangingAltitude" },
- { "isFalling" },
- { "isGroupLeader" },
- { "isImmuneTo", "string" },
- { "isInvulnerable" },
- { "isMoving" },
- { "isPerformingAction", "string" },
- { "isReadyToAct" },
- { "knockback", "number" },
- { "setTraits", "table" },
- { "addTrait" , "string" },
- { "removeTrait" , "string" },
- { "hasTrait" , "string" },
- -- contents() is defined at the end of this file
- },
- hooks = {
- "onProjectileHit(self, item, champion, weapon, attack, damage, damageType, heading, crit)",
- "onPerformAction(self, name)",
- "onDamage(self, damage, damageType)",
- "onSpellDamage(self, damage, damageType, c, spell, heading)", -- new
- "onDie(self)",
- },
- }
- local oldDefineSkill = defineSkill
- function defineSkill(desc)
- local f = "defineSkill"
- checkNamedArgOpt("skillTraits", desc, f, "table") -- used to display traits in a more organized way
- oldDefineSkill(desc)
- end
- local oldDefineTrait = defineTrait
- function defineTrait(desc)
- local f = "defineTrait"
- checkNamedArgOpt("classEffect", desc, f, "table") -- used to display class hp/en growth separately
- checkNamedArgOpt("classAbility", desc, f, "table")
- oldDefineTrait(desc)
- end
- local oldDefineCharClass = defineCharClass
- function defineCharClass(desc)
- desc.order = desc.order or 99
- local f = "defineCharClass"
- checkNamedArgOpt("order", desc, f, "number") -- used to put classes in a specific order
- oldDefineCharClass(desc)
- end
- local oldDefineCondition = defineCondition
- function defineCondition(desc)
- local f = "defineCondition"
- checkNamedArgOpt("onRecomputeFinalStats", desc, f, "function")
- checkNamedArgOpt("maxStacks", desc, f, "number") --
- oldDefineCondition(desc)
- end
- defineProxyClass{
- class = "Condition",
- description = "Custom condition.",
- methods = {
- { "setDuration", "number" },
- { "getDuration" },
- -- { "getName" },
- { "getStacks" }, -- new
- },
- }
- local oldDefineSpell = defineSpell
- function defineSpell(desc)
- desc = table.copy(desc)
- local f = "defineSpell"
- checkNamedArgOpt("duration", desc, f, "number")
- checkNamedArgOpt("durationScaling", desc, f, "number")
- checkNamedArgOpt("power", desc, f, "number")
- checkNamedArgOpt("powerScaling", desc, f, "number")
- oldDefineSpell(desc)
- end
- local oldDungeonLoadInitFile = Dungeon.loadInitFile
- function Dungeon:loadInitFile()
- oldDungeonLoadInitFile(self)
- AddToolTips()
- AddStats()
- AddStatNames()
- end
- local oldNewGameMenuStartGame = NewGameMenu.startGame
- function NewGameMenu:startGame()
- if not modSystem:getCurrentMod() then
- oldDungeon = dungeon
- dungeon:redefineTraits()
- dungeon:redefineSkills()
- dungeon:redefineSpells()
- dungeon = oldDungeon
- end
- oldNewGameMenuStartGame(self)
- end
- function extendProxyClass(class, prop)
- class.__class.synthesizeProperty(prop)
- end
- function AddToolTips()
- local toolTips = ToolTip.toolTips
- toolTips["Critical Multiplier"] = "Multiplies damage dealt with criticals by the amount displayed."
- table.insert(ToolTip.toolTips, toolTips)
- end
- function AddStats()
- table.insert(Stats, "critical_multiplier")
- table.insert(Stats, "dual_wielding")
- end
- function AddStatNames()
- table.insert(ToolTip.toolTips, "Critical Damage")
- table.insert(ToolTip.toolTips, "Dual Wielding")
- end
- function GameMode:loadDefaultParty()
- local defaultParty = {
- {
- name = "Shadow",
- class = "fighter",
- race = "human",
- sex = "female",
- portrait = "assets/textures/portraits/human_female_01.tga",
- strength = 5,
- dexterity = 3,
- vitality = 2,
- willpower = 0,
- skills = { "light_weapons", 1, "armors", 1 },
- traits = { "tough", "agile" },
- },
- {
- name = "Mulrag",
- class = "barbarian",
- race = "minotaur",
- sex = "male",
- portrait = "assets/textures/portraits/minotaur_male_01.tga",
- strength = 5,
- dexterity = 1,
- vitality = 4,
- willpower = 0,
- skills = { "athletics", 1, "dodge", 1 },
- traits = { "aggressive", "head_hunter" },
- },
- {
- name = "Fang",
- class = "alchemist",
- race = "ratling",
- sex = "male",
- portrait = "assets/textures/portraits/ratling_male_01.tga",
- strength = 2,
- dexterity = 4,
- vitality = 2,
- willpower = 2,
- skills = { "alchemy", 1, "firearms", 1 },
- traits = { "mutation", "tough" },
- },
- {
- name = "Astaroth",
- class = "wizard",
- race = "human",
- sex = "male",
- portrait = "assets/textures/portraits/human_male_02.tga",
- strength = 0,
- dexterity = 2,
- vitality = 2,
- willpower = 6,
- skills = { "concentration", 1, "fire_magic", 1 },
- traits = { "aura", "strong_mind" },
- },
- }
- if party:isHookRegistered("onLoadDefaultParty") then
- newDefaultParty = party:callHook("onLoadDefaultParty", objectToProxy(self), defaultParty)
- if newDefaultParty then
- assert(newDefaultParty[4], "default party must be a table with 4 entries")
- defaultParty = newDefaultParty
- end
- end
- for i=1,4 do
- local champion = Champion.create()
- local def = defaultParty[i]
- champion.ordinal = i
- champion:setName(def.name)
- champion:setClass(def.class)
- champion:setRace(def.race)
- champion:setSex(def.sex)
- champion:setPortrait(def.portrait)
- champion:setBaseStat("strength", def.strength + 10)
- champion:setBaseStat("dexterity", def.dexterity + 10)
- champion:setBaseStat("vitality", def.vitality + 10)
- champion:setBaseStat("willpower", def.willpower + 10)
- party:setChampion(i, champion)
- champion:recomputeStats()
- champion:setBaseStat("health", champion:getMaxHealth())
- champion:setBaseStat("energy", champion:getMaxEnergy())
- for j=1,#def.skills,2 do
- champion:setSkillLevel(def.skills[j], 0) -- avoid gaining +1 skill every time we preview the game
- champion:trainSkill(def.skills[j], def.skills[j+1])
- end
- for j=1,#def.traits do
- champion:addTrait(def.traits[j])
- end
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- Tooltip Functions --
- -------------------------------------------------------------------------------------------------------
- -- Attack
- function ToolTip.drawAttack(attack, tx, ty, width, height, powerAttack)
- local actualWidth = width
- local font = ToolTip.normalFont
- local h = font:getLineHeight() --22
- -- charges
- if attack.charges and attack.maxCharges then
- local w = gui:drawText(string.format("Charges: ", attack.charges), tx, ty, font)
- actualWidth = math.max(actualWidth, w)
- local x,y = tx + w,ty - 13
- gui:drawRect(x, y, attack.maxCharges * 10 + 2, 12 + 4, {40,40,40,255})
- for i=1,attack.charges do
- gui:fillRect(x + 2, y + 2, 8, 12, {200, 50, 20, 255}) --{99,114,105,255})
- x = x + 8 + 2
- end
- actualWidth = math.max(actualWidth, attack.maxCharges * 10 + 65)
- ty = ty + h
- end
- -- energy cost
- if attack.energyCost and attack.energyCost ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Energy Cost: %d", attack.energyCost), tx, ty, font))
- ty = ty + h
- end
- if not powerAttack then
- if attack.attackPower and attack.attackPower ~= 0 then
- local mod = { attack.minDamageMod, attack.maxDamageMod }
- local min,max = getDamageRange(attack.attackPower, mod)
- local text
- if attack.damageType and attack.damageType ~= "physical" then
- local damageType = attack.damageType
- if damageType == "dispel" then
- damageType = "Ethereal"
- else
- damageType = string.capitalize(damageType)
- end
- text = string.format("Damage: %s %d - %d", damageType, min, max)
- else
- text = string.format("Damage: %d - %d", min, max)
- end
- -- bonus
- local baseStat = attack:getBaseDamageStat()
- local baseMulti = attack:getBaseDamageMultiplier()
- if baseStat then
- if baseMulti then
- text = string.format("%s + %s%% of user's %s", text, baseMulti*100, getStatName(baseStat))
- else
- text = string.format("%s + %s", text, getStatName(baseStat))
- end
- end
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
- ty = ty + h
- end
- if attack.accuracy then
- local text = "Accuracy:"
- local accuracy = attack.accuracy or 0
- if accuracy ~= 0 then
- text = string.format("%s %+d", text, accuracy)
- end
- if accuracy ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
- ty = ty + h
- end
- end
- if attack.critChance and attack.critChance ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Critical Chance: %+d%%", attack.critChance), tx, ty, font))
- ty = ty + h
- end
- if attack.critMultiplier and attack.critMultiplier ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Critical Damage: %+d%%", attack.critMultiplier * 100), tx, ty, font))
- ty = ty + h
- end
- if attack.range and attack.range ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Range: %d", attack.range), tx, ty, font))
- ty = ty + h
- end
- if attack.jamChance and attack.jamChance ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Chance to malfunction: %d%%", attack.jamChance), tx, ty, font))
- ty = ty + h
- end
- if attack.cooldown and attack.cooldown ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Cooldown: %.1f seconds", attack.cooldown), tx, ty, font))
- ty = ty + h
- end
- if attack.pierce and attack.pierce ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Ignores %d points of enemy's armor", attack.pierce), tx, ty, font))
- ty = ty + h
- end
- if attack.velocity and attack.velocity ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Projectiles are %d%% faster", (attack.velocity - 1) * 100), tx, ty, font))
- ty = ty + h
- end
- if attack and attack.reachWeapon then
- actualWidth = math.max(actualWidth, gui:drawText("Reach Weapon", tx, ty, font))
- ty = ty + h
- end
- end
- if attack.requirements then
- local champion = charSheet:getActiveChampion()
- local text = attack:getRequirementsText()
- local color = iff(attack:checkRequirements(champion), Color.White, Color.Red)
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font, color))
- ty = ty + h
- end
- if attack.gameEffect then
- actualWidth = math.max(actualWidth, gui:drawText(attack.gameEffect, tx, ty, font))
- ty = ty + h
- end
- return ty,actualWidth
- end
- -- EquipmentItem
- -- Shows some of the new stats, such as Min/Max Damage bonus and Crit Multiplier
- function ToolTip.drawEquipmentItem(item, tx, ty, width, height)
- local actualWidth = width
- local font = ToolTip.normalFont
- local h = font:getLineHeight()
- if item.protection and item.protection ~= 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Protection %+d", item.protection), tx, ty, font))
- ty = ty + h
- end
- if item.evasion then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Evasion %+d", item.evasion), tx, ty, font))
- ty = ty + h
- end
- if item.accuracy then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Accuracy %+d", item.accuracy), tx, ty, font))
- ty = ty + h
- end
- if item.critMultiplier then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Critical Damage %+d%%", item.critMultiplier * 100), tx, ty, font))
- ty = ty + h
- end
- if item.minDamageMod and not item.maxDamageMod then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Min Damage %+d", item.minDamageMod), tx, ty, font))
- ty = ty + h
- end
- if item.maxDamageMod and not item.minDamageMod then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Max Damage %+d", item.maxDamageMod), tx, ty, font))
- ty = ty + h
- end
- if item.maxDamageMod and item.minDamageMod then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Adds %d - %d damage to all attacks", item.minDamageMod, item.maxDamageMod), tx, ty, font))
- ty = ty + h
- end
- -- stat modifiers
- for i=1,#Stats do
- local stat = Stats[i]
- local modifier = item[stat]
- if modifier and stat ~= "protection" and stat ~= "evasion" then
- if modifier > 0 then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("%s +%d", StatNames[i], modifier), tx, ty, font))
- ty = ty + h
- else
- actualWidth = math.max(actualWidth, gui:drawText(string.format("%s %d", StatNames[i], modifier), tx, ty, font))
- ty = ty + h
- end
- end
- end
- -- resistances
- if item.resistAll then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Resist All %+d", item.resistAll), tx, ty, font))
- ty = ty + h
- end
- for i=1,#Elements do
- local resist = elementToResistance(Elements[i])
- if item[resist] then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Resist %s %+d", string.capitalize(Elements[i]), item[resist]), tx, ty, font))
- ty = ty + h
- end
- end
- -- skill modifiers
- if item.skillModifiers then
- for name,value in pairs(item.skillModifiers) do
- local skill = Skill.getSkill(name)
- if skill then
- name = skill.uiName
- else
- name = "???"
- end
- actualWidth = math.max(actualWidth, gui:drawText(string.format("%s %+d", name, value), tx, ty, font))
- ty = ty + h
- end
- end
- if item.go.item:hasTrait("light_armor") then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Evasion -5 without Light Armor proficiency"), tx, ty, font))
- ty = ty + h
- elseif item.go.item:hasTrait("heavy_armor") then
- actualWidth = math.max(actualWidth, gui:drawText(string.format("Evasion -10 without Heavy Armor proficiency"), tx, ty, font))
- ty = ty + h
- end
- if item.cooldownRate and item.cooldownRate ~= 0 then
- local text
- if item.cooldownRate > 0 then
- text = string.format("%d%% faster cooldown", item.cooldownRate)
- else
- text = string.format("%d%% slower cooldown", -item.cooldownRate)
- end
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
- ty = ty + h
- end
- if item.healthRegenerationRate and item.healthRegenerationRate ~= 0 then
- local text = string.format("Health Regeneration Rate %+d%%", item.healthRegenerationRate)
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
- ty = ty + h
- end
- if item.energyRegenerationRate and item.energyRegenerationRate ~= 0 then
- local text = string.format("Energy Regeneration Rate %+d%%", item.energyRegenerationRate)
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
- ty = ty + h
- end
- if item.expRate and item.expRate ~= 0 then
- local text = string.format( "Wearer gains experience points %d%% faster", item.expRate * 100)
- actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
- ty = ty + h
- end
- return ty,actualWidth
- end
- -- Conditions
- -- Draws number of stacks when appliable
- function ToolTip.drawCondition(cond, x, y, width, height)
- local actualWidth = 0
- local actualHeight = 0
- if ToolTip.style == "rounded_rect" then
- -- add little extra to width to make it look nicer
- gui:drawToolTipRect(x, y, width + 4, height)
- end
- local tx,ty = x + 16, y + 16
- -- draw caption
- if cond.iconIndex then
- cond:drawIcon(tx, ty)
- tx = tx + 36
- end
- local font = ToolTip.titleFont
- ty = ty + font:getMaxBearingY()
- local stacks = cond:getStacks()
- local stacksText = stacks and ("(" .. stacks .. " stacks)") or ""
- gui:drawText(cond.uiName .. stacksText, tx, ty + 3, font, Color.White)
- ty = ty + 26
- local font = ToolTip.normalFont
- -- description
- local desc = cond:getDescription()
- if desc then
- local tw,th = gui:drawTextParagraph(desc, tx, ty, 400, font)
- actualWidth = math.max(actualWidth, tw / gui.guiScale)
- ty = ty + th
- ty = ty + 4
- end
- -- debug info
- if config.developer then
- gui:drawText(string.format("value=%.1f timer=%.1f", cond.value, cond.timer), tx, ty, FontType.Default, Color.White)
- end
- actualWidth = actualWidth + 32
- if cond.iconIndex then
- actualWidth = actualWidth + 36
- end
- actualHeight = ty - y
- return actualWidth, actualHeight
- end
- -- Updated for aesthetic reasons
- -- When a trait has a description and a game effect, the desc shows in a blueish hue
- -- Skills have the option to show the traits separately from the gameEffect text
- function ToolTip.drawSkill(skill, x, y, width, height)
- local points = ToolTip.hints["skill_level"] or 0
- local compact = ToolTip.hints["compact"]
- local actualWidth = 0
- local actualHeight = 0
- local paragraphWidth = 450
- if ToolTip.style == "rounded_rect" then
- gui:drawToolTipRect(x, y, width, height)
- end
- local tx,ty = x + 16, y + 16
- gui:drawSkillIcon(skill, tx, ty, 75)
- tx = tx + 90
- -- draw caption
- local font = ToolTip.titleFont
- local maxBearing = font:getMaxBearingY()
- ty = ty + maxBearing
- actualWidth = math.max(actualWidth, gui:drawText(skill.uiName, tx, ty, font, Color.White))
- ty = ty + 24 + 5
- local font = ToolTip.normalFont
- if compact then font = FontType.PalatinoTiny end
- -- description
- local desc = skill:getDescriptionText()
- local descColor = iff(not skill.gameEffect, Color.White, {162,183,206,255})
- local tw,th = gui:drawTextParagraph(desc, tx, ty, paragraphWidth, font, descColor)
- actualWidth = math.max(actualWidth, tw / gui.guiScale)
- ty = ty + th / gui.guiScale
- local champion = charSheet:getActiveChampion()
- -- game effect
- if skill.gameEffect then
- ty = ty + 8
- local tw,th = gui:drawTextParagraph(skill.gameEffect, tx, ty, paragraphWidth, font)
- actualWidth = math.max(actualWidth, tw / gui.guiScale)
- ty = ty + th / gui.guiScale
- end
- -- skill traits
- if skill.skillTraits then
- ty = math.max(ty + 8, y + 16 + 75 + maxBearing )
- tx = tx - 88
- if skill.traits then
- for traitLevel = 0, 5 do
- if skill.traits[traitLevel] then
- local trait = Skill.getTrait(skill.traits[traitLevel])
- if trait then
- local color = Color.White
- local tw,th = gui:drawTextParagraph(" Level " .. traitLevel .. ": " .. trait.uiName, tx, ty, paragraphWidth, font, {153,244,124,255})
- ty = ty + th / gui.guiScale
- tw,th = gui:drawTextParagraph(skill.skillTraits[traitLevel], tx+12, ty, paragraphWidth+75, font, color)
- actualWidth = math.max(actualWidth, paragraphWidth)
- ty = ty + th / gui.guiScale
- end
- end
- end
- if skill.skillTraits[9] then
- ty = ty + 4
- local tw,th = gui:drawTextParagraph("*" .. skill.skillTraits[9], tx, ty, paragraphWidth, FontType.ScrollScaled, {162,183,206,255})
- actualWidth = math.max(actualWidth, tw / gui.guiScale)
- ty = ty + th / gui.guiScale
- end
- end
- tx = tx + 88
- end
- -- levels
- if skill.levels then
- for i=1,#skill.levels do
- local color = iff(champion:getSkillLevel(skill.name) >= i, Color.White, {110,110,110,255})
- local tw,th = gui:drawTextParagraph(skill.levels[i], tx, ty, paragraphWidth, font, color)
- actualWidth = math.max(actualWidth, tw / gui.guiScale)
- ty = ty + th / gui.guiScale
- end
- end
- -- requirements
- if skill.requirements then
- local champion = charSheet:getActiveChampion()
- local color
- if champion and not skill:checkRequirements(champion) then color = Color.Red end
- local text = skill:getRequirementsText(champion)
- local tw,th = gui:drawTextParagraph(text, tx, ty, 400, font, color)
- actualWidth = math.max(actualWidth, tw / gui.guiScale)
- ty = ty + th / gui.guiScale
- end
- -- draw spell gesture
- local spell = Spell.getSpell(skill.name)
- if spell then
- local width = actualWidth+90
- local w = 48
- local h = 40
- do
- local tx = tx + (width - 3*w)/2 - 90
- local ty = ty - 25
- gui:drawGuiItem(GuiItem.SpellPanelNoButtons, tx+10, ty+20)
- local maxLen = math.min(math.floor(ToolTip.time*5), ToolTip.runePanel:getGestureLength(spell.gesture))
- ToolTip.runePanel:drawGesture(spell.gesture, tx + w/2, ty + h/2, w, h, maxLen)
- end
- ty = ty + 3 * h + 10
- end
- ty = ty + 4
- actualWidth = actualWidth + 32 + 70 + 20
- actualHeight = math.max(ty - y + 2, 105)
- return actualWidth, actualHeight
- end
- local oldSkillInit = Skill.init
- function Skill:init(desc)
- oldSkillInit(self, desc)
- self.skillTraits = desc.skillTraits
- self.classEffect = desc.classEffect
- self.classAbility = desc.classAbility
- self.onComputeCritMultiplier = desc.onComputeCritMultiplier
- self.onComputeDamageModifier = desc.onComputeDamageModifier
- self.onComputeDamageMultiplier = desc.onComputeDamageMultiplier
- self.onComputeDualWieldingModifier = desc.onComputeDualWieldingModifier
- self.onComputeChampionAttackDamage = desc.onComputeChampionAttackDamage
- self.onComputeChampionSpellDamage = desc.onComputeChampionSpellDamage
- self.onComputeMonsterDamage = desc.onComputeMonsterDamage
- self.onCheckDualWielding = desc.onCheckDualWielding
- self.onComputePierce = desc.onComputePierce
- self.onCheckBackstab = desc.onCheckBackstab
- self.onComputeItemWeight = desc.onComputeItemWeight
- self.onComputeItemStats = desc.onComputeItemStats
- self.onComputeToHit = desc.onComputeToHit
- self.onRecomputeFinalStats = desc.onRecomputeFinalStats
- self.onComputeSpellCost = desc.onComputeSpellCost
- self.onComputeSpellCooldown = desc.onComputeSpellCooldown
- self.onComputeSpellDamage = desc.onComputeSpellDamage
- self.onComputeHerbMultiplicationRate = desc.onComputeHerbMultiplicationRate
- self.onComputeBearFormDuration = desc.onComputeBearFormDuration
- self.onComputeBombPower = desc.onComputeBombPower
- self.onComputeDamageTaken = desc.onComputeDamageTaken
- self.onComputeRange = desc.onComputeRange
- end
- -------------------------------------------------------------------------------------------------------
- -- Party Functions --
- -------------------------------------------------------------------------------------------------------
- local oldPartyMoveEnter = PartyMove.enter
- function PartyMove:enter(direction, speed, forcedMovement)
- oldPartyMoveEnter(self, direction, speed, forcedMovement)
- if not party:isUnderwater() then
- -- update herbalism
- for i=1,4 do
- local champ = party.champions[i]
- champ:updateHerbalismNew()
- end
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- Champion Functions --
- -------------------------------------------------------------------------------------------------------
- local oldInit = Champion.init
- function Champion:init()
- oldInit(self)
- self:setBaseStat("critical_multiplier", 2.5)
- self:setBaseStat("dual_wielding", 0.6)
- self.data = {} -- gonna be used to easily store new values into a champion
- end
- function Champion:setData(name, value)
- self.data[name] = value
- end
- function Champion:getData(name)
- return self.data[name]
- end
- function Champion:addData(name, value)
- self.data[name] = (self.data[name] or 0) + value
- end
- local oldChampionSaveState = Champion.saveState
- function Champion:saveState(file)
- oldChampionSaveState(self,file)
- file:openChunk("CHAM")
- file:writeValue(self.data)
- file:closeChunk()
- end
- local oldChampionLoadState = Champion.loadState
- function Champion:loadState(file, loadItems)
- oldChampionLoadState(self, file, loadItems)
- local chunkID = file:openChunk()
- assert(chunkID == "CHAM")
- self.data = file:readValue()
- file:closeChunk()
- end
- function Champion:setConditionValue(name, value, power, stacks)
- -- Now takes more optional parameters
- local curStacks = self:getConditionStacks(name)
- self:setCondition(name, value > 0)
- local cond = self.conditions[name]
- if cond then
- cond.value = value
- if power then cond.power = power end
- if stacks then cond.stacks = curStacks + 1 end
- end
- end
- function Champion:getAccuracyWithAttack(weapon, attack, target)
- -- Updated to having the target of the attack as an optional parameter
- -- check skill level requirement
- if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
- return nil
- end
- local accuracy = attack.accuracy or 0
- -- dexterity bonus
- accuracy = accuracy + self:getAccuracyFromDexterity()
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeAccuracy then
- local modifier = skill.onComputeAccuracy(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name), objectToProxy(target))
- accuracy = accuracy + (modifier or 0)
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeAccuracy then
- local modifier = trait.onComputeAccuracy(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0), objectToProxy(target))
- accuracy = accuracy + (modifier or 0)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeAccuracy then
- local modifier = comp:onComputeAccuracy(self, weapon, attack, attackType, target)
- accuracy = accuracy + (modifier or 0)
- end
- end
- end
- end
- end
- -- conditions
- if self:hasCondition("blind") or self:hasCondition("head_wound") then accuracy = accuracy - 50 end
- return math.floor(accuracy)
- end
- function Champion:getToHitChanceWithAttack(weapon, attack, target, accuracy, damageType)
- if not accuracy then accuracy = champion:getAccuracyWithAttack(weapon, attack) end
- local tohit = 60 + accuracy - (target.evasion or 0)
- tohit = tohit + self.luck
- tohit = math.clamp(tohit, 5, 95)
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeToHit then
- local modifier = skill.onComputeToHit(objectToProxy(target), objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), damageType, tohit, self:getSkillLevel(name))
- tohit = modifier or tohit
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeToHit then
- local modifier = trait.onComputeToHit(objectToProxy(target), objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), damageType, tohit, iff(self:hasTrait(name), 1, 0))
- tohit = modifier or tohit
- end
- end
- return tohit
- end
- function Champion:getAccuracyFromDexterity()
- return (self:getCurrentStat("dexterity") - 10) * 2
- end
- function Champion:getCritChanceWithAttack(weapon, attack, target)
- -- Updated to having the target of the attack as an optional parameter
- -- check skill level requirement
- if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
- return nil
- end
- local critChance = 5 + (attack.critChance or 0)
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeCritChance then
- local modifier = skill.onComputeCritChance(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name), objectToProxy(target), self:getAccuracyWithAttack(weapon, attack))
- critChance = critChance + (modifier or 0)
- end
- end
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeCritChance then
- local modifier = trait.onComputeCritChance(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0), objectToProxy(target), self:getAccuracyWithAttack(weapon, attack))
- critChance = critChance + (modifier or 0)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeCritChance then
- local modifier = comp:onComputeCritChance(self, weapon, attack, target)
- critChance = critChance + (modifier or 0)
- end
- end
- end
- end
- end
- critChance = math.clamp(critChance, 0, 100)
- return critChance
- end
- function Champion:getCritMultiplierWithAttack(weapon, attack, target)
- -- New function to deal with changing the crit damage multiplier
- -- check skill level requirement
- if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
- return nil
- end
- local critMulti = 2.5 + (attack.critMultiplier or 0)
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeCritMultiplier then
- local modifier = skill.onComputeCritMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name), target, self:getAccuracyWithAttack(weapon, attack))
- critMulti = critMulti + (modifier or 0)
- end
- end
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeCritMultiplier then
- local modifier = trait.onComputeCritMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0), target, self:getAccuracyWithAttack(weapon, attack))
- critMulti = critMulti + (modifier or 0)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeCritMultiplier then
- critMulti = critMulti + (comp:onComputeCritMultiplier(self, weapon, attack, target) or 0)
- end
- end
- end
- end
- end
- critMulti = math.clamp(critMulti, 0, 100)
- return critMulti
- end
- function Champion:getCritMultiplier(slot)
- assert(slot, "missing param 'slot'")
- local action = self:getPrimaryAction(slot)
- if action and action.getAttackType then
- local weapon = self:getItem(slot)
- if self:hasCondition("bear_form") then weapon = nil end
- return self:getCritMultiplierWithAttack(weapon, action)
- end
- end
- function Champion:getCritMultiplierText(slot)
- local crit = self:getCritMultiplier(slot)
- if crit then
- return crit.."x"
- else
- return "2.5x"
- end
- end
- function Champion:getTwoHanded(slot)
- local item = self:getItem(slot)
- return item and item:hasTrait("two_handed") and not self:hasTrait("two_handed_mastery")
- end
- function Champion:getDamageText(slot)
- -- when wielding a 2-handed weapon, the stats page should display same damage for the secondary hand
- local item = self:getItem(slot)
- if (slot == ItemSlot.Weapon or slot == ItemSlot.OffHand) and not item then
- local otherSlot = iff(slot == ItemSlot.Weapon, ItemSlot.OffHand, ItemSlot.Weapon)
- if otherItem and self:getTwoHanded(otherSlot) then
- slot = otherSlot
- end
- end
- local power,mod,var = self:getDamage(slot)
- if power then
- local min,max = getDamageRange(power, mod, var)
- return min.." - "..max
- else
- return "--"
- end
- end
- -- Returns attack power and damage modifier for an attack.
- function Champion:getDamageWithAttack(weapon, attack)
- -- Some hard coded effects were turned into trait/skill hooks such as weapon skill bonus and dual wielding penalty
- -- Added the ability to alter damage via hooks and to affect that variation of damage calculated, instead of being always -50% and +50%
- -- check skill level requirement
- if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
- return nil
- end
- local power = attack:getAttackPower()
- local mod = {attack:getMinDamageMod() or 0, attack:getMaxDamageMod() or 0}
- local variation = attack:getAttackPowerVariation() or 0.5
- if party:isHookRegistered("onCalculateDamageWithAttack") then
- power = party:callHook("onCalculateDamageWithAttack", objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), power)
- end
- -- dual wield penalty
- if weapon and self:isDualWielding() then
- dualWieldingMulti = self:getCurrentStat("dual_wielding")
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeDualWieldingModifier then
- local modifier = trait.onComputeDualWieldingModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
- dualWieldingMulti = dualWieldingMulti + (modifier or 0)
- end
- if trait.onComputeDualWieldingModifier then
- local modifier = trait.onComputeDualWieldingModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
- dualWieldingMulti = dualWieldingMulti + (modifier or 0)
- end
- end
- power = power * dualWieldingMulti
- end
- -- str/dex stat modifier
- local baseStat = attack:getBaseDamageStat()
- local baseMulti = attack:getBaseDamageMultiplier() or 1
- if baseStat then
- mod[1] = mod[1] + math.floor((self:getCurrentStat(baseStat) - 10) * baseMulti)
- mod[2] = mod[2] + math.floor((self:getCurrentStat(baseStat) - 10) * baseMulti)
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeDamageModifier then
- local modifier = skill.onComputeDamageModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name))
- if type(modifier) == "number" then
- mod[1] = mod[1] + (modifier or 0)
- mod[2] = mod[2] + (modifier or 0)
- elseif type(modifier) == "table" then
- mod[1] = mod[1] + (modifier[1] or 0)
- mod[2] = mod[2] + (modifier[2] or 0)
- end
- end
- if skill.onComputeDamageMultiplier then
- local modifier = skill.onComputeDamageMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name))
- power = power * (modifier or 1)
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeDamageModifier then
- local modifier = trait.onComputeDamageModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
- if type(modifier) == "number" then
- mod[1] = mod[1] + (modifier or 0)
- mod[2] = mod[2] + (modifier or 0)
- elseif type(modifier) == "table" then
- mod[1] = mod[1] + (modifier[1] or 0)
- mod[2] = mod[2] + (modifier[2] or 0)
- end
- end
- if trait.onComputeDamageMultiplier then
- local modifier = trait.onComputeDamageMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
- power = power * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeDamageModifier then
- local modifier = comp:onComputeDamageModifier(self, weapon, attack)
- mod[1] = mod[1] + (modifier[1] or 0)
- mod[2] = mod[2] + (modifier[2] or 0)
- end
- end
- end
- end
- end
- -- ammo bonus for missile weapons
- if attack:getAttackType() == "missile" then
- local slot = iff(self:getItem(ItemSlot.Weapon) == weapon, ItemSlot.Weapon, ItemSlot.OffHand)
- local ammo = self:getOtherHandItem(slot)
- if ammo and attack:checkAmmo(self, slot) then
- local ammoItem = ammo.go.ammoitem
- if ammoItem then
- --print("ammo bonus: ", ammoItem:getAttackPower() or 0)
- mod[1] = mod[1] + (ammoItem:getAttackPower() or 0)
- mod[2] = mod[2] + (ammoItem:getAttackPower() or 0)
- end
- end
- end
- -- conditions
- if self:hasCondition("starving") then power = power / 2 end
- power = math.max(math.floor(power), 0)
- return power,mod,variation
- end
- function Champion:updateHerbalism()
- -- We no longer use this function, since it's only called for the alchemist class
- return
- end
- function Champion:updateHerbalismNew()
- if not self:hasTrait("herb_multiplication") then return end -- herb multiplication no longer tied to alchemist class
- local multi = { 1, 1, 1, 1, 1, 1 }
- local herbRates = {
- ["blooddrop_cap"] = 850,
- ["etherweed"] = 930,
- ["mudwort"] = 1950,
- ["falconskyre"] = 2500,
- ["blackmoss"] = 3700,
- ["crystal_flower"] = 4500,
- }
- -- Herb multiplication can be canceled. For example, you could make them not multiply during the day, or increase the rate while under water
- local tilesMoved = party.go.statistics:getStat("tiles_moved")
- local returnVal = party:callHook("onMultiplyHerbs", herbRates, objectToProxy(champion))
- if returnVal then
- if returnVal[1] == false then return false end
- herbRates = returnVal[2] or herbRates
- end
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeHerbMultiplicationRate then
- local modifier = skill.onComputeHerbMultiplicationRate(objectToProxy(self), self:getSkillLevel(name))
- if modifier and modifier[1] then
- for i = 1, 6 do
- multi[i] = multi[i] * (modifier[i] or 1)
- end
- end
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeHerbMultiplicationRate then
- local modifier = trait.onComputeHerbMultiplicationRate(objectToProxy(self), iff(self:hasTrait(name), 1, 0))
- if modifier and modifier[1] then
- for i = 1, 6 do
- multi[i] = multi[i] * (modifier[i] or 1)
- end
- end
- end
- end
- local i = 1
- for herb,rate in pairs(herbRates) do
- -- check growth rate
- if (tilesMoved % math.floor(rate * multi[i])) == 0 then
- self:updateHerbalism2(herb)
- end
- i = i + 1
- end
- end
- function Champion:isDualWielding()
- -- Completely reworked to take this info from traits and equipment
- local isDualWielding = false
- local weapon1 = self:getItem(ItemSlot.Weapon)
- local weapon2 = self:getItem(ItemSlot.OffHand)
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onCheckDualWielding then
- isDualWielding = isDualWielding or skill.onCheckDualWielding(objectToProxy(self), objectToProxy(weapon1), objectToProxy(weapon2), self:getSkillLevel(name))
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onCheckDualWielding then
- isDualWielding = isDualWielding or trait.onCheckDualWielding(objectToProxy(self), objectToProxy(weapon1), objectToProxy(weapon2), iff(self:hasTrait(name), 1, 0))
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onCheckDualWielding then
- isDualWielding = isDualWielding or comp:onCheckDualWielding(self, weapon1, weapon2)
- end
- end
- end
- end
- end
- return isDualWielding
- end
- function Champion:attack(slot, powerAttack)
- local item = self:getItem(slot)
- if self:hasCondition("bear_form") then item = nil end
- local action
- if not item then
- action = self:getUnarmedAttack()
- elseif powerAttack then
- action = self:getSecondaryAction(slot)
- else
- action = self:getPrimaryAction(slot)
- end
- if not action then return end
- -- disable unarmed attacks when holding a two-handed weapon
- if not item then
- local otherItem = self:getOtherHandItem(slot)
- if otherItem and otherItem:hasTrait("two_handed") and not self:hasTrait("two_handed_mastery") then
- return
- end
- end
- -- check weapon skill level requirement
- if item and not item:canBeUsedByChampion(self, slot) then
- self:showAttackResult("Can't use")
- return
- end
- -- can't attack with a broken hand
- if (slot == ItemSlot.Weapon and self:hasCondition("right_hand_wound")) or (slot == ItemSlot.OffHand and self:hasCondition("left_hand_wound")) then
- self:showAttackResult("Can't use")
- return
- end
- -- can't attack with 2-handed weapon if one of the hands is broken
- if item and item:hasTrait("two_handed") and not self:hasTrait("two_handed_mastery") then
- if self:hasCondition("left_hand_wound") or self:hasCondition("right_hand_wound") then
- self:showAttackResult("Can't use")
- return
- end
- end
- local repeatCount = action.repeatCount
- local repeatDelay = action.repeatDelay
- if party:isHookRegistered("onAttack") and action ~= self:getUnarmedAttack() then
- local returnValue = party:callHook("onAttack", objectToProxy(self), objectToProxy(action), slot, repeatCount, repeatDelay)
- if returnValue then
- if returnValue[1] == false then return end
- repeatCount = returnValue[2] or repeatCount
- repeatDelay = returnValue[3] or repeatDelay
- end
- end
- if action ~= self:getUnarmedAttack() and action:callHook("onAttack", objectToProxy(self), slot, 0) == false then
- return
- end
- if action.start then action:start(self, slot, 0) end
- if powerAttack then party.go.statistics:increaseStat("power_attacks", 1) end
- self:addData("attack_count", 1)
- -- chain action
- if action.chainAction then
- local next = action:getNextChainAction()
- if next then
- self.pendingAttack = {
- action = next,
- slot = slot,
- time = action.chainActionDelay or 0.2,
- chainIndex = 1,
- }
- end
- end
- -- repeat action
- if action.repeatCount and action.repeatCount > 1 then
- local delay = action.repeatDelay or 0.2
- self.pendingAttack = {
- action = action,
- slot = slot,
- time = delay,
- chainIndex = 1,
- repeatCount = action.repeatCount - 1,
- repeatDelay = delay,
- }
- end
- -- double throw
- if self:hasTrait("double_throw") and action.getAttackType and action:getAttackType() == "throw" and self.pendingAttack == nil then
- local otherItem = self:getOtherHandItem(slot)
- if otherItem and otherItem.go.throwattack then
- self.pendingAttack = {
- action = otherItem.go.throwattack,
- slot = iff(slot == ItemSlot.Weapon, ItemSlot.OffHand, ItemSlot.Weapon),
- time = 0.2
- }
- end
- end
- end
- function Champion:damage(dmg, damageType, hitContext)
- damageType = damageType or "physical"
- dmg = math.floor(dmg)
- local isSpell = hitContext and (hitContext.go.tiledamager or hitContext.go.cloudspell)
- if damageType == "dispel" then return end
- if self:isAlive() and not self:hasCondition("petrified") then
- -- apply damage resistance
- if damageType ~= "physical" and damageType ~= "drowning" and damageType ~= "pure" then
- local resist = self:getResistance(damageType)
- if resist == 100 then
- dmg = 0
- elseif resist > 0 then
- dmg = math.floor(dmg * (100 - resist) / 100 + 0.5)
- end
- end
- local onDamageReturn = party:callHook("onDamage", objectToProxy(self), dmg, damageType, objectToProxy(hitContext))
- if onDamageReturn then
- dmg = onDamageReturn[2]
- if onDamageReturn[1] == false then
- return
- end
- end
- if dmg > 0 then
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeDamageTaken then
- dmg = skill.onComputeDamageTaken(objectToProxy(self), objectToProxy(hitContext), dmg, dmgType, isSpell, self:getSkillLevel(name)) or dmg
- end
- end
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeDamageTaken then
- dmg = trait.onComputeDamageTaken(objectToProxy(self), objectToProxy(hitContext), dmg, dmgType, isSpell, iff(self:hasTrait(name), 1, 0)) or dmg
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeDamageTaken then
- dmg = comp:onComputeDamageTaken(self, hitContext, dmg, dmgType, isSpell) or dmg
- end
- end
- end
- end
- end
- end
- dmg = self:triggerOnDamage(dmg, dmgType, isSpell, hitContext)
- dmg = math.floor(dmg)
- if dmg > 0 then
- self:modifyBaseStat("health", -dmg)
- self:showDamageIndicator(dmg)
- messageSystem:sendMessageNEW("onChampionDamaged", self, dmg, damageType)
- end
- -- champion died?
- if not self:isAlive() then
- if party:callHook("onDie", objectToProxy(self)) == false then
- if self:getBaseStat("health") < 1 then self:setBaseStat("health", 1) end
- return
- end
- self:die()
- end
- end
- end
- function Champion:triggerOnDamage(dmg, dmgType, isSpell, hitContext)
- -- trigger minotaur rage?
- if self:hasTrait("rage") then
- local oldHealth = self:getHealth() / self:getMaxHealth()
- local newHealth = (self:getHealth() - dmg) / self:getMaxHealth()
- local threshold = 0.2
- if oldHealth > threshold and newHealth <= threshold then
- self:setConditionValue("rage", self:getConditionValue("rage") + 20)
- end
- end
- return dmg
- end
- local oldChampionRecomputeStats = Champion.recomputeStats
- function Champion:recomputeStats()
- oldChampionRecomputeStats(self)
- -- Final stat modifiers
- -- apply item modifiers
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onRecomputeFinalStats then
- comp:onRecomputeFinalStats(self)
- end
- end
- end
- end
- end
- -- apply condition modifiers
- for _,cond in pairs(self.conditions) do
- if cond.recomputeFinalStats then cond:recomputeFinalStats(self) end
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onRecomputeFinalStats then
- local level = self:getSkillLevel(name)
- if level > 0 then
- skill.onRecomputeFinalStats(objectToProxy(self), level)
- end
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onRecomputeFinalStats and self:hasTrait(name) then
- trait.onRecomputeFinalStats(objectToProxy(self), 1)
- end
- end
- end
- function Champion:castSpell(gesture)
- -- Updated to allow traits and equipment to affect spell cost, cooldown and power
- -- can't cast spell with wounded head
- if self:hasCondition("head_wound") then
- self:showAttackResult("Fizzle", GuiItem.SpellFizzle)
- return false
- end
- -- find spell
- local spell = Spell.getSpellByGesture(gesture)
- if not spell then
- self:showAttackResult("Fizzle", GuiItem.SpellFizzle)
- soundSystem:playSound2D("spell_fizzle")
- self:spendEnergy(math.random(5,13))
- --self:clearRunes()
- return false
- end
- -- check skill level
- if not config.unlimitedSpells then
- -- check skill requirements
- if spell.requirements and not Skill.checkRequirements(self, spell.requirements) then
- self:showAttackResult("Fizzle", GuiItem.SpellFizzle)
- soundSystem:playSound2D("spell_fizzle")
- self:spendEnergy(math.random(5,13))
- self:clearRunes()
- return false
- end
- end
- -- spend energy
- if not config.unlimitedSpells then
- local cost = spell.manaCost
- -- console:print("cost before", cost)
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeSpellCost then
- local modifier = skill.onComputeSpellCost(objectToProxy(self), spell.name, spell.manaCost, spell.skill, self:getSkillLevel(name))
- cost = cost * (modifier or 1)
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeSpellCost then
- local modifier = trait.onComputeSpellCost(objectToProxy(self), spell.name, spell.manaCost, spell.skill, iff(self:hasTrait(name), 1, 0))
- cost = cost * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeSpellCost then
- local modifier = comp:onComputeSpellCost(self, spell.name, spell.manaCost, spell.skill)
- cost = cost * (modifier or 1)
- end
- end
- end
- end
- end
- if self:getEnergy() < cost then
- self:showAttackResult("Out of energy", GuiItem.SpellNoEnergy)
- soundSystem:playSound2D("spell_out_of_energy")
- return
- end
- self:spendEnergy(cost)
- end
- if party:callHook("onCastSpell", objectToProxy(self), spell.name) == false then
- return false
- end
- messageSystem:sendMessageNEW("onChampionCastSpell", self, spell)
- self:clearRunes()
- local skill = 0
- if spell.skill then skill = self:getSkillLevel(spell.skill) end
- --if config.unlimitedSpells then skill = math.max(skill, 3) end
- local pos = party.go:getWorldPositionFast()
- local x,y = party.go.map:worldToMap(pos)
- local spl = Spell.castSpell(spell, self, x, y, party.go.facing, party.go.elevation, skill)
- local dmg = 0
- local spellObject = nil
- if spl then
- if spl.tiledamager then
- dmg = spl.tiledamager.attackPower
- spellObject = spl.tiledamager
- elseif spl.projectile then
- dmg = spl.projectile.attackPower
- spellObject = spl.projectile
- elseif spl.cloudspell then
- dmg = spl.cloudspell.attackPower
- spellObject = spl.cloudspell
- end
- -- Spell damage modifiers
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeSpellDamage then
- local returnVal = skill.onComputeSpellDamage(objectToProxy(self), objectToProxy(spellObject), spell.name, spell.manaCost, spell.skill, self:getSkillLevel(name))
- if returnVal then
- if returnVal[1] == false then return end
- if returnVal[2] then
- local tempSpl = returnVal[2]
- spl = tempSpl.go
- if not (spl.tiledamager or spl.projectile or spl.cloudspell) then
- console:print("invalid onComputeSpellDamage return value 2")
- end
- end
- end
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeSpellDamage then
- local returnVal = trait.onComputeSpellDamage(objectToProxy(self), objectToProxy(spellObject), spell.name, spell.manaCost, spell.skill, iff(self:hasTrait(name), 1, 0))
- if returnVal then
- if returnVal[1] == false then return end
- if returnVal[2] then
- local tempSpl = returnVal[2]
- spl = tempSpl.go
- if not (spl.tiledamager or spl.projectile or spl.cloudspell) then
- console:print("invalid onComputeSpellDamage return value 2")
- end
- end
- end
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeSpellDamage then
- local returnVal = comp:onComputeSpellDamage(self, spellObject, spell.name, spell.manaCost, spell.skill)
- if returnVal then
- if returnVal[1] == false then return end
- if returnVal[2] then
- local tempSpl = returnVal[2]
- spl = tempSpl.go
- if not (spl.tiledamager or spl.projectile or spl.cloudspell) then
- console:print("invalid onComputeSpellDamage return value 2")
- end
- end
- end
- end
- end
- end
- end
- end
- end
- -- cool down
- local cooldown = 5
- -- console:print("cooldown before", cooldown)
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeSpellCooldown then
- local modifier = skill.onComputeSpellCooldown(objectToProxy(self), spell.name, spell.manaCost, spell.skill, self:getSkillLevel(name))
- cooldown = cooldown * (modifier or 1)
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeSpellCooldown then
- local modifier = trait.onComputeSpellCooldown(objectToProxy(self), spell.name, spell.manaCost, spell.skill, iff(self:hasTrait(name), 1, 0))
- cooldown = cooldown * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeSpellCooldown then
- local modifier = comp:onComputeSpellCooldown(self, spell.name, spell.manaCost, spell.skill)
- cooldown = cooldown * (modifier or 1)
- end
- end
- end
- end
- end
- -- console:print("cooldown after", cooldown)
- self.cooldownTimer[1] = cooldown
- self.cooldownTimer[2] = cooldown
- -- strenous activity consumes food
- self:consumeFood(math.random(4,9))
- -- learn new spell?
- if not spell.hidden and not self:hasTrait(spell.name) then
- self:addTrait(spell.name)
- gui:hudPrint(self.name.." learned a new spell!")
- soundSystem:playSound2D("discover_spell")
- end
- party.go.statistics:increaseStat("spells_cast", 1)
- return true
- end
- function Champion:getLoad()
- -- Updated to enable affecting item weight with traits
- local load = 0
- local armorWeightReductionEquipped = 1
- local armorWeightReduction = 1
- for i=1,ItemSlot.MaxSlots do
- local it = self.items[i]
- if it then
- local equipped = false
- if i < ItemSlot.BackpackFirst and i ~= ItemSlot.Weapon and i ~= ItemSlot.OffHand and (it:hasTrait("light_armor") or it:hasTrait("heavy_armor")) then
- equipped = true
- end
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeItemWeight then
- local modifier = skill.onComputeItemWeight(objectToProxy(self), equipped, self:getSkillLevel(name))
- armorWeightReduction = armorWeightReduction * (modifier or 1)
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeItemWeight then
- local modifier = trait.onComputeItemWeight(objectToProxy(self), equipped, iff(self:hasTrait(name), 1, 0))
- armorWeightReduction = armorWeightReduction * (modifier or 1)
- end
- end
- load = load + it:getTotalWeight() * armorWeightReduction
- end
- end
- return load
- end
- -------------------------------------------------------------------------------------------------------
- -- Monster Functions --
- -------------------------------------------------------------------------------------------------------
- function MonsterComponent:onAttackedByChampion(champion, weapon, attack, slot, dualWieldSide)
- if not self:isAlive() or self.go.elevation ~= party.go.elevation or self:getMonsterFlag(MonsterFlag.NonMaterial) then return end
- local target = self
- local damageType = attack.damageType or "physical"
- local gauntlets = champion:getItem(ItemSlot.Gloves)
- if gauntlets and gauntlets:hasTrait("fire_gauntlets") then damageType = "fire" end
- -- get target for monster groups
- if target.group then
- target = target:getNearestMonster(champion:getChampionPositionInWorld(0.5))
- if not target then return end
- end
- -- evasion
- if not self:hasCondition("sleep") and not self:hasCondition("frozen") and attack.name ~= "bonusAttack" then
- local accuracy = champion:getAccuracyWithAttack(weapon, attack)
- local tohit = champion:getToHitChanceWithAttack(weapon, attack, target, accuracy, damageType)
- if math.random() > tohit / 100 or target.evasion >= 1000 then
- target:showDamageText("miss", Color.Grey)
- champion.luck = math.min(champion.luck + 3, 15)
- champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
- return "miss"
- end
- champion.luck = 0
- end
- -- compute side
- local tside = (party.go.facing - target.go.facing + 6) % 4
- local side
- if tside == 0 then
- -- front left or right?
- local rightSide = (champion.championIndex == 1 or champion.championIndex == 3)
- side = iff(rightSide, "front_right", "front_left")
- elseif tside == 1 then
- side = "right"
- elseif tside == 2 then
- side = "back"
- else
- side = "left"
- end
- -- compute base damage
- local dmg = computeDamage(champion:getDamageWithAttack(weapon, attack))
- local damageFlags = DamageFlags.Impact
- -- crits & fumbles
- local crit = false
- local critChance = champion:getCritChanceWithAttack(weapon, attack, target) / 100
- if math.random() < critChance then
- if dmg > 0 then
- crit = true
- end
- elseif math.random() < 0.07 then
- dmg = dmg / 2
- end
- -- backstab
- local backstab = false
- local backStabMult = 0
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onCheckBackstab then
- local modifier = trait.onCheckBackstab(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, iff(champion:hasTrait(name), 1, 0))
- backStabMult = backStabMult + (modifier or 0)
- end
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onCheckBackstab then
- local modifier = skill.onCheckBackstab(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, champion:getSkillLevel(name))
- backStabMult = backStabMult + (modifier or 0)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeBackstabMultiplier then
- local modifier = comp:onComputeBackstabMultiplier(champion, weapon, attack)
- backStabMult = backStabMult + (modifier or 0)
- end
- end
- end
- end
- end
- if backStabMult > 0 and tside == 2 and not target:isImmuneTo("backstab") then
- local extraDamage = math.floor(dmg * (backStabMult-1) * (math.random() * 0.5 + 0.5))
- --print(string.format("backstab damage bonus +%d", extraDamage))
- dmg = dmg + extraDamage
- backstab = true
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeChampionAttackDamage then
- local modifier = skill.onComputeChampionAttackDamage(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, backstab, champion:getSkillLevel(name))
- if modifier then
- dmg = modifier[2] or dmg
- heading = modifier[3] ~= nil and modifier[3] or heading
- crit = modifier[4] ~= nil and modifier[4] or crit
- backstab = modifier[5] or backstab
- damageType = modifier[6] or damageType
- end
- end
- end
- -- trait modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeChampionAttackDamage then
- local modifier = trait.onComputeChampionAttackDamage(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, backstab, iff(champion:hasTrait(name), 1, 0))
- if modifier then
- dmg = modifier[2] or dmg
- heading = modifier[3] ~= nil and modifier[3] or heading
- crit = modifier[4] ~= nil and modifier[4] or crit
- backstab = modifier[5] or backstab
- damageType = modifier[6] or damageType
- end
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeChampionAttackDamage then
- local modifier = comp:onComputeChampionAttackDamage(self, champion, weapon, attack, dmg, damageType, crit, backstab, level)
- if modifier then
- dmg = modifier[2] or dmg
- heading = modifier[3] ~= nil and modifier[3] or heading
- crit = modifier[4] ~= nil and modifier[4] or crit
- backstab = modifier[5] or backstab
- damageType = modifier[6] or damageType
- end
- end
- end
- end
- end
- end
- if crit then
- local critMult = champion:getCritMultiplierWithAttack(weapon, attack, target)
- if champion:getSecondaryAction(slot) == attack then critMult = 1.5 end
- if attack.getCritMultiplier then critMult = critMult + (attack:getCritMultiplier()-1) end
- dmg = dmg * critMult
- if dmg <= 0 then
- crit = false
- end
- end
- -- damage reduction
- local protection = target:getMonsterProtectionWithAttack(champion, weapon, attack, dmg, damageType, crit, backstab)
- if protection > 0 then dmg = computeDamageReduction(dmg, protection) end
- -- invulnerability
- if target:isInvulnerable() then
- dmg = 0
- damageFlags = DamageFlags.Impact
- end
- -- damage source
- damageFlags = damageFlags + bit.lshift(DamageFlags.Champion1, champion.ordinal-1)
- -- compute impact position
- local impactPos = target:findImpactPosition(champion:getChampionPositionInWorld(0.7))
- -- heading
- local heading
- if backstab then
- heading = "Backstab"
- elseif crit then
- heading = "Critical"
- end
- dmg = math.max(math.floor(dmg), 0)
- if self.go.goromorgshield and self.go.goromorgshield:getEnergy() == math.huge then dmg = 0 end
- -- call hook
- if attack.go and attack:callHook("onHitMonster", objectToProxy(target), tside, dmg, objectToProxy(champion), crit, backstab) == false then
- return true
- end
- self:hitTriggers(champion, weapon, attack)
- if attack.knockback then
- self:knockback(party.go.facing)
- end
- -- deal damage to target
- local oldHealth = target:getHealth()
- target:damage(dmg, side, damageFlags, damageType, impactPos, heading)
- -- HACK: show zero damage in attack panel if monster is invulnerable to damage
- if target:getHealth() == oldHealth then dmg = 0 end
- if backstab and not target:isAlive() then
- steamContext:unlockAchievement("backstabber")
- end
- champion:showAttackResult(dmg, GuiItem.HitSplash, dualWieldSide)
- -- cause condition
- if attack.causeCondition then
- local chance = attack.conditionChance or 50
- if math.random(1,100) <= chance then
- target:setCondition(attack.causeCondition)
- -- mark condition so that exp is awarded if monster is killed by the condition
- local cond = target.go:getComponent(attack.causeCondition)
- if cond and cond.setCausedByChampion then
- cond:setCausedByChampion(champion.ordinal)
- end
- end
- end
- return true
- end
- function MonsterComponent:getMonsterProtectionWithAttack(champion, weapon, attack, dmg, damageType, crit, backstab, projectile)
- local pierce = 0
- local protection = self:getProtection()
- if attack and attack.pierce then pierce = pierce + attack.pierce end
- if projectile and projectile.projectilePierce then pierce = pierce + projectile.projectilePierce end
- if not champion then return math.max(protection - pierce, 0) end
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputePierce then
- local modifier = trait.onComputePierce(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(projectile), dmg, damageType, attack:getAttackType(), crit, backstab, iff(champion:hasTrait(name), 1, 0))
- pierce = pierce + (modifier or 0)
- end
- end
- -- skills modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputePierce then
- local modifier = skill.onComputePierce(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(projectile), dmg, damageType, attack:getAttackType(), crit, backstab, champion:getSkillLevel(name))
- pierce = pierce + (modifier or 0)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputePierce then
- local modifier = comp:onComputePierce(self, champion, weapon, attack, projectile, dmg, damageType, attackType, crit, backstab)
- pierce = pierce + (modifier or 0)
- end
- end
- end
- end
- end
- return math.max(protection - pierce, 0)
- end
- function MonsterComponent:hitTriggers(champion, weapon, attack)
- if not champion then return false end
- -- Triggers here
- end
- function MonsterComponent:damage(dmg, side, damageFlags, damageType, impactPos, heading, spell)
- damageFlags = damageFlags or 0
- if not self:isAlive() then return end
- -- tentacles hiding below ground ignore all damage
- if self.go.arch.name == "tentacles" and not self:getMonsterFlag(MonsterFlag.Collides) then return end
- if self:isInvulnerable() then dmg = 0 end
- -- resist
- local resist = self:getResistance(damageType)
- if resist then dmg = math.floor(dmg * getResistanceDamageMultiplier(resist)) end
- -- goromorg shield
- if self.go.goromorgshield and resist ~= "immune" and resist ~= "absorb" and self.go.goromorgshield:shieldHit(dmg) then return end
- -- dispel
- if damageType == "dispel" and not self:hasTrait("elemental") then return end
- local onDamageReturn = self:callHook("onDamage", dmg, damageType)
- if onDamageReturn then
- dmg = onDamageReturn[2]
- if onDamageReturn[1] == false then
- return
- end
- end
- -- remember who hit us
- local c = nil
- for i=0,3 do
- if bit.band(damageFlags, bit.lshift(DamageFlags.Champion1, i)) ~= 0 then
- c = i + 1
- self:setMonsterFlag(bit.lshift(MonsterFlag.DamagedByChampion1, i), true)
- end
- end
- local champion = party.champions[c]
- local onSpellDamageReturn = self:callHook("onSpellDamage", dmg, damageType, objectToProxy(champion), objectToProxy(spell), heading)
- if onSpellDamageReturn then
- if onSpellDamageReturn[1] == false then
- return
- end
- dmg = onSpellDamageReturn[2]
- heading = onSpellDamageReturn[3] or heading
- end
- -- trait modifiers
- if champion then
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeChampionSpellDamage then
- local modifier = trait.onComputeChampionSpellDamage(objectToProxy(self), objectToProxy(champion), objectToProxy(spell), dmg, damageType, iff(champion:hasTrait(name), 1, 0))
- if modifier then
- if modifier[1] == false then return end
- dmg = modifier[2]
- heading = modifier[3] or heading
- end
- end
- end
- end
- dmg = math.ceil(dmg)
- if dmg >= 0 then
- self.health = self.health - dmg
- else
- -- absorb
- self.health = math.min(self.health - dmg, self:getMaxHealth())
- end
- -- play get hit effects
- if bit.band(damageFlags, DamageFlags.Impact) ~= 0 then
- -- play get hit animation?
- if dmg > 0 and self.currentAction == nil then
- local anim
- if side == "front_left" then
- anim = "getHitFrontLeft"
- elseif side == "front_right" then
- anim = "getHitFrontRight"
- elseif side == "left" then
- anim = "getHitLeft"
- elseif side == "right" then
- anim = "getHitRight"
- elseif side == "back" then
- anim = "getHitBack"
- else
- error("invalid get hit side")
- end
- -- create get hit action
- if not self.go.damaged then
- self.go:createComponent(MonsterActionComponent, "damaged")
- end
- self.go.damaged:setAnimation(anim)
- self:performAction("damaged")
- end
- if self:hasCondition("frozen") then
- self.go:playSound("ice_hit")
- else
- if self.hitSound then self.go:playSound(self.hitSound) end
- end
- end
- -- show damage in hud
- local color
- if heading then
- color = Color.White
- else
- color = Color.Grey
- end
- if resist == "weak" or resist == "vulnerable" then color = Color.Red end
- local showDamage = true
- if dmg == 0 and bit.band(damageFlags, DamageFlags.OngoingDamage) ~= 0 then showDamage = false end
- if showDamage then
- if dmg >= 0 and dmg < 9999 then
- self:showDamageText(dmg, color, heading)
- elseif dmg < 0 then
- self:showDamageText(-dmg, {0,200,0,255}, heading)
- else
- self:showDamageText("", Color.Red, heading)
- end
- end
- if self.health > 0 then
- if dmg > 0 then self.go:sendMessage("onMonsterDamaged", dmg, damageType) end
- -- play get hit particle effect
- if bit.band(damageFlags, DamageFlags.Impact) ~= 0 and bit.band(damageFlags, DamageFlags.NoLingeringEffects) == 0 then
- if dmg > 0 then
- if damageType == "physical" then
- self:playHitEffect(impactPos)
- elseif damageType == "fire" then
- self:playParticleEffect("damage_fire")
- elseif damageType == "shock" then
- self:playParticleEffect("damage_shock")
- end
- end
- end
- else
- -- freeze monster if death from cold
- if not self:hasCondition("frozen") and damageType == "cold" then
- self:setCondition("frozen", 1)
- end
- local gainExp = false
- local anyChampion = DamageFlags.Champion1 + DamageFlags.Champion2 + DamageFlags.Champion3 + DamageFlags.Champion4
- local gainExp = bit.band(damageFlags, anyChampion) ~= 0
- if self:getHealth() - damage <= 0 then
- self:deathTriggers(champion, weapon, attack)
- end
- self:die(gainExp)
- end
- end
- function MonsterComponent:deathTriggers(champion, weapon, attack)
- if not champion then return false end
- -- Death triggers here
- end
- -------------------------------------------------------------------------------------------------------
- -- Combat Functions --
- -------------------------------------------------------------------------------------------------------
- function computeDamage(attackPower, modifier, variation)
- -- Updated in order to be able to affect the damage variation
- local min,max = getDamageRange(attackPower, modifier, variation)
- return math.max(math.random(min, max), 0)
- end
- -- Returns min and max damage.
- function getDamageRange(attackPower, modifier, variation)
- -- Updated in order to be able to affect the damage variation and to directly affect the damage modifier min/max
- variation = variation or 0.5
- modifier = modifier or {0,0}
- if attackPower == 0 then return modifier[1],modifier[2] end
- if attackPower then
- local power = attackPower
- local min = math.max(math.floor( (power - power * variation) + (modifier[1] or 0) ), 1)
- local max = math.max(math.floor( (power + power * variation) + (modifier[2] or 0) ), 1)
- local minModded = math.clamp(min, 1, math.max(max - 1, 1))
- min = minModded
- local maxModded = math.clamp(max, min + 1, 999)
- return minModded,maxModded
- end
- end
- -- Damages all creatures in a tile
- function damageTile(map, x, y, direction, elevation, damageFlags, damageType, power, screenEffect, hitCallback, hitContext)
- -- hit monster
- for _,monster in map:componentsAt(MonsterComponent, x, y) do
- local ent = monster.go
- if ent.elevation == elevation and not monster:getMonsterFlag(MonsterFlag.NonMaterial) and power and power > 0 then
- -- check temporal ice shards immunity
- local immune = bit.band(damageFlags, DamageFlags.DamageSourceIceShards) ~= 0 and monster.iceShardsImmunityTimer
- if not immune then
- local side = computeSide(direction, ent.facing)
- local dmg = computeDamage(power)
- if prob(0.08) then dmg = dmg * 2 end
- -- compute approximate impact position
- local impactPos
- if bit.band(damageFlags, DamageFlags.Impact) ~= 0 then
- local dx,dy = getDxDy(direction)
- local pos = map:mapToWorld(x + dx, y + dy)
- impactPos = monster:findImpactPosition(pos)
- end
- local status
- if hitCallback then status = hitCallback(hitContext, "monster", monster, dmg, damageType) end
- if status ~= false then
- monster:damage(dmg, side, damageFlags, damageType, impactPos, heading, hitContext)
- end
- end
- end
- end
- -- hit obstacle
- -- avoid hitting just spawned obstacles in reaction to getting obstacle destroyed
- local obstacles
- for _,obstacle in map:componentsAt(ObstacleComponent, x, y) do
- if obstacle.go.elevation == elevation then
- if obstacles == nil then obstacles = {} end
- obstacles[#obstacles+1] = obstacle
- end
- end
- if obstacles then
- for i=1, #obstacles do
- local obstacle = obstacles[i]
- local dmg = computeDamage(power)
- local status
- if hitCallback then status = hitCallback(hitContext, "obstacle", obstacle, dmg, damageType) end
- if status ~= false and obstacle.go:sendMessage("onDamage", dmg, damageType) then
- obstacle:playHitEffects()
- end
- end
- end
- -- hit party?
- for _,party in map:componentsAt(PartyComponent, x, y) do
- if party.go.elevation == elevation then
- -- check temporal ice shards immunity
- local immune = bit.band(damageFlags, DamageFlags.DamageSourceIceShards) ~= 0 and party.iceShardsImmunityTimer
- if not immune then
- local partyHit = false
- for i=1,4 do
- local dmg = 0
- if power and power > 0 then dmg = computeDamage(power) end
- -- halve damage for backrow
- if damageFlags and bit.band(damageFlags, DamageFlags.HalveBackRowDamage) ~= 0 and checkPartyCover(i, direction) then
- dmg = math.floor(dmg / 2)
- end
- local status
- if hitCallback then status = hitCallback(hitContext, "champion", party.champions[i], dmg, damageType) end
- if status ~= false then
- partyHit = true
- if dmg > 0 then party.champions[i]:damage(dmg, damageType, hitContext) end
- end
- end
- if partyHit then
- party:wakeUp(true)
- if screenEffect then
- party:playScreenEffect(screenEffect)
- end
- if bit.band(damageFlags, DamageFlags.CameraShake) ~= 0 then
- party:shakeCamera(0.5, 0.3)
- end
- end
- end
- end
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- Item Functions --
- -------------------------------------------------------------------------------------------------------
- function ItemComponent:dragItemToThrowZone(x, y)
- local champion = gameMode:getActiveChampion()
- if champion then
- local vx,vy,vw,vh = gameMode:getViewport()
- local side = iff(x < vw/2, 0, 1)
- local origin = party.champions[side+1]:getChampionPositionInWorld(0.4)
- -- push forward so that item won't collide against a door behind us
- origin = origin + party:getWorldForward() * self:getBoundingRadius()
- local power = math.max(14 - self.weight, 10)
- local gravity = math.clamp(2 + self.weight*1.5, 4, 10)
- local velocityUp = 0
- local weapon = nil
- local attack = nil
- if self:hasTrait("throwing_weapon") then
- power = 14
- gravity = 1
- velocityUp = 0
- weapon = self
- attack = self.go.throwattack
- end
- self:throw(party, origin, party.go.facing, power, gravity, velocityUp)
- self.thrownByChampion = champion.ordinal
- -- store original weapon and attack data in projectile
- self.thrownByWeapon = weapon
- self.thrownByAttack = attack
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], 1)
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], 1)
- soundSystem:playSound2D("swipe")
- return true
- end
- end
- function ItemComponent:projectileHitEntity(target)
- -- Updated for extra hook goodness
- -- compute damage
- local dmg = self.projectileDamage or math.random(1,3)
- local damageType = self.projectileDamageType or "physical"
- local pierce = self.projectilePierce or 0
- local accuracy = self.projectileAccuracy or 0
- local critChance = self.projectileCritChance or 5
- local weapon = self.thrownByWeapon
- local attack = self.thrownByAttack
- local thrownByChampion
- if self.thrownByChampion then
- thrownByChampion = party:getChampionByOrdinal(self.thrownByChampion)
- end
- -- crits & fumbles
- local crit = false
- if math.random() < critChance/100 then
- if dmg > 0 then
- crit = true
- end
- elseif math.random() < 0.1 then
- dmg = math.floor(dmg / 2)
- end
- local heading = nil
- if crit then heading = "Critical" end
- if target.monster then
- local target = target.monster
- -- evasion
- if not target:hasCondition("sleep") and not target:hasCondition("frozen") then
- local tohit = thrownByChampion:getToHitChanceWithAttack(weapon, attack, target, accuracy, damageType)
- if math.random() > tohit / 100 or target.evasion >= 1000 then
- target:showDamageText("miss", Color.Grey)
- if thrownByChampion then
- thrownByChampion.luck = math.min(thrownByChampion.luck + 3, 15)
- end
- self.go:playSound("impact_blunt")
- return "miss"
- end
- if thrownByChampion then thrownByChampion.luck = 0 end
- end
- if crit then
- local critMult = champion:getCritMultiplierWithAttack(weapon, attack, target)
- if champion:getSecondaryAction(slot) == attack then critMult = 1.5 end
- if attack.getCritMultiplier then critMult = critMult + (attack:getCritMultiplier()-1) end
- dmg = dmg * critMult
- if dmg <= 0 then
- crit = false
- end
- end
- -- damage reduction
- local protection = target:getMonsterProtectionWithAttack(champion, weapon, attack, dmg, damageType, crit, nil, projectile)
- if protection > 0 then dmg = computeDamageReduction(dmg, protection) end
- -- hit monster
- local side = "front_left" -- TODO: compute side
- local impactPos = target:findImpactPosition(self.go:getWorldPosition())
- local damageFlags = DamageFlags.Impact
- if self.thrownByChampion then damageFlags = damageFlags + bit.lshift(DamageFlags.Champion1, self.thrownByChampion-1) end
- local returnValue = target:callHook("onProjectileHit", objectToProxy(self), objectToProxy(thrownByChampion), objectToProxy(self.thrownByWeapon), objectToProxy(self.thrownByAttack), dmg, damageType, heading, crit)
- if returnValue then
- if returnValue[1] == false then return end
- dmg = returnValue[2] or dmg
- damageType = returnValue[3] or damageType
- heading = returnValue[4] or heading
- end
- target:damage(dmg, side, damageFlags, damageType, impactPos, heading)
- self:callHook("onThrowAttackHitMonster", objectToProxy(target))
- target:hitTriggers(thrownByChampion, self.thrownByWeapon, self.thrownByAttack)
- -- stick projectile into monster
- if target:isAlive() then
- local sharp = self:getItemFlag(ItemFlag.SharpProjectile)
- local fragile = self:getItemFlag(ItemFlag.FragileProjectile)
- if self.projectileDamage and sharp and not fragile then
- self.go.map:removeEntity(self.go)
- if self.convertToItemOnImpact then self.convertItem = true end
- target:addItem(self)
- end
- end
- if not target:isAlive() and self:hasTrait("leg_armor") then
- steamContext:unlockAchievement("full_monty")
- end
- elseif target.obstacle then
- -- hit obstacle
- target:sendMessage("onDamage", dmg)
- target.obstacle:playHitEffects()
- elseif target.party then
- -- hit party
- local target = party:getAttackTarget((self.go.facing+2) % 4, math.random(0,1))
- if target then
- if party:isHookRegistered("onProjectileHit") then
- if party:callHook("onProjectileHit", objectToProxy(target), objectToProxy(self), dmg, damageType) == false then
- return
- end
- end
- soundSystem:playSound2D("projectile_hit_party")
- party:wakeUp(true)
- target:damage(dmg, damageType)
- -- HACK: hard code medusa arrow, 20% chance to petrify
- if self.go.arch.name == "petrifying_arrow" then
- local petrified = math.random() < 0.2
- if petrified and target:isAlive() and not target:hasCondition("petrified") then
- target:setCondition("petrified")
- end
- end
- if target:isAlive() then target:playDamageSound() end
- end
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- EquipmentItem Functions --
- -------------------------------------------------------------------------------------------------------
- defineProxyClass{
- class = "EquipmentItemComponent",
- baseClass = "Component",
- description = "Implemented various modifiers to stats of an Champion when the item is equipped. The traits of an item define where the item can be equipped.",
- methods = {
- { "setSlot", "number" },
- { "setStrength", "number" },
- { "setDexterity", "number" },
- { "setVitality", "number" },
- { "setWillpower", "number" },
- { "setProtection", "number" },
- { "setEvasion", "number" },
- { "setResistFire", "number" },
- { "setResistCold", "number" },
- { "setResistShock", "number" },
- { "setResistPoison", "number" },
- { "setHealth", "number" },
- { "setEnergy", "number" },
- { "setExpRate", "number" },
- { "setFoodRate", "number" },
- { "setHealthRegenerationRate", "number" },
- { "setEnergyRegenerationRate", "number" },
- { "setCooldownRate", "number" },
- { "setAccuracy", "number" },
- { "setDamage", "number" },
- { "setCriticalChance", "number" },
- { "setCriticalMultiplier", "number" },
- { "setDualWielding", "number" },
- { "setPierceOnCrit", "number" },
- { "getSlot" },
- { "getStrength" },
- { "getDexterity" },
- { "getVitality" },
- { "getWillpower" },
- { "getProtection" },
- { "getEvasion" },
- { "getResistFire" },
- { "getResistCold" },
- { "getResistShock" },
- { "getResistPoison" },
- { "getHealth" },
- { "getEnergy" },
- { "getExpRate" },
- { "getFoodRate" },
- { "getHealthRegenerationRate" },
- { "getEnergyRegenerationRate" },
- { "getCooldownRate" },
- { "getAccuracy" },
- { "getDamage" },
- { "getCriticalChance" },
- { "getCriticalMultiplier" },
- { "getDualWielding" },
- { "getPierceOnCrit" },
- { "getMinDamageMod" },
- { "getMaxDamageMod" },
- { "setMinDamageMod", "number" },
- { "setMaxDamageMod", "number" },
- { "getBlockChance" },
- { "getBlockThreshhold" },
- { "setBlockChance", "number" },
- { "setBlockThreshhold", "number" },
- },
- hooks = {
- "onRecomputeStats(self, champion)",
- "onRecomputeFinalStats(self, champion)",
- "onComputeAccuracy(self, champion, weapon, attack, monster, attackType)",
- "onComputeCritChance(self, champion, weapon, attack, monster, attackType)",
- "onComputeDamageModifier(self, champion, weapon, attack, attackType)",
- "onCheckDualWielding(self, champion, weapon1, weapon2)",
- "onComputeBackstabMultiplier(self, champion, weapon, attack)",
- "onComputeCritMultiplier(self, champion, weapon, attack, monster)",
- "onComputeChampionAttackDamage(self, monster, champion, weapon, attack, dmg, damageType, crit, backstab)",
- "onComputePierce(self, monster, champion, weapon, attack, projectile, dmg, damageType, crit, backstab)",
- "onComputeSpellCost(self, champion, name, cost, skill)",
- "onComputeSpellCooldown(self, champion, name, cost, skill)",
- "onComputeSpellDamage(self, champion, spell, name, cost, skill)",
- "onComputeBombPower(self, bombItem, champion, power)",
- "onComputeBearFormDuration(self, champion)",
- "onComputeDamageTaken(self, champion, attack, dmg, dmgType, isSpell)",
- "onComputeMalfunctionChance(self, champion, weapon, attack, attackType)",
- "onComputeRange(self, champion, weapon, attack, attackType)",
- },
- }
- extendProxyClass(EquipmentItemComponent, "critMultiplier")
- extendProxyClass(EquipmentItemComponent, "dualWielding")
- extendProxyClass(EquipmentItemComponent, "minDamageMod")
- extendProxyClass(EquipmentItemComponent, "maxDamageMod")
- -- local oldRecomputeStats = EquipmentItemComponent.recomputeStats
- function EquipmentItemComponent:recomputeStats(champion, slot)
- -- oldRecomputeStats(self,champion,slot)
- -- called at the beginning of each frame, updates champions stats
- if not self.enabled then return end
- if self:isEquipped(champion, slot) then
- local stats = champion.stats
- stats.strength.current = stats.strength.current + (self.strength or 0)
- stats.dexterity.current = stats.dexterity.current + (self.dexterity or 0)
- stats.vitality.current = stats.vitality.current + (self.vitality or 0)
- stats.willpower.current = stats.willpower.current + (self.willpower or 0)
- stats.protection.current = stats.protection.current + (self.protection or 0)
- stats.evasion.current = stats.evasion.current + (self.evasion or 0)
- stats.resist_fire.current = stats.resist_fire.current + (self.resistFire or 0) + (self.resistAll or 0)
- stats.resist_cold.current = stats.resist_cold.current + (self.resistCold or 0) + (self.resistAll or 0)
- stats.resist_shock.current = stats.resist_shock.current + (self.resistShock or 0) + (self.resistAll or 0)
- stats.resist_poison.current = stats.resist_poison.current + (self.resistPoison or 0) + (self.resistAll or 0)
- stats.max_health.current = stats.max_health.current + (self.health or 0)
- stats.max_energy.current = stats.max_energy.current + (self.energy or 0)
- stats.exp_rate.current = stats.exp_rate.current + (self.expRate or 0)
- stats.food_rate.current = stats.food_rate.current + (self.foodRate or 0)
- stats.health_regeneration_rate.current = stats.health_regeneration_rate.current + (self.healthRegenerationRate or 0)
- stats.energy_regeneration_rate.current = stats.energy_regeneration_rate.current + (self.energyRegenerationRate or 0)
- stats.cooldown_rate.current = stats.cooldown_rate.current + (self.cooldownRate or 0)
- stats.critical_multiplier.current = stats.critical_multiplier.current + (self.critMultiplier or 0)
- stats.dual_wielding.current = stats.dual_wielding.current + (self.dualWielding or 0)
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeItemStats then
- skill.onComputeItemStats(objectToProxy(self), objectToProxy(champion), slot, champion:getSkillLevel(name))
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeItemStats then
- trait.onComputeItemStats(objectToProxy(self), objectToProxy(champion), slot, iff(champion:hasTrait(name), 1, 0))
- end
- end
- end
- self:callHook("onRecomputeStats", objectToProxy(champion))
- end
- function EquipmentItemComponent:onComputeAccuracy(champion, weapon, attack, attackType, monster)
- if self.enabled then
- local modifier = self:callHook("onComputeAccuracy", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), objectToProxy(monster))
- modifier = modifier or 0
- if self.accuracy then modifier = modifier + self.accuracy end
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeCritChance(champion, weapon, attack, monster)
- if self.enabled then
- local modifier = self:callHook("onComputeCritChance", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), objectToProxy(monster))
- modifier = modifier or 0
- if self.criticalChance then modifier = modifier + self.criticalChance end
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeDamageModifier(champion, weapon, attack)
- if self.enabled then
- local modifier = self:callHook("onComputeDamageModifier", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType())
- modifier = (modifier and type(modifier) == "table") and modifier or modifier and { modifier/2, modifier*2 } or {0,0}
- if self.minDamageMod then modifier[1] = modifier[1] + self.minDamageMod end
- if self.maxDamageMod then modifier[2] = modifier[2] + self.maxDamageMod end
- return modifier
- end
- end
- function EquipmentItemComponent:onCheckDualWielding(champion, weapon1, weapon2)
- if self.enabled then
- local modifier = self:callHook("onCheckDualWielding", objectToProxy(champion), objectToProxy(weapon1), objectToProxy(weapon2))
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeBackstabMultiplier(champion, weapon, attack)
- if self.enabled then
- local modifier = self:callHook("onComputeBackstabMultiplier", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType())
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeCritMultiplier(champion, weapon, attack, monster)
- if self.enabled then
- local modifier = self:callHook("onComputeCritMultiplier", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(monster))
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeChampionAttackDamage(monster, champion, weapon, attack, dmg, damageType, crit, backstab)
- if self.enabled then
- local modifier = self:callHook("onComputeChampionAttackDamage", objectToProxy(monster), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, backstab)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputePierce(monster, champion, weapon, attack, projectile, dmg, damageType, attackType, crit, backstab)
- if self.enabled then
- local modifier = self:callHook("onComputePierce", objectToProxy(monster), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(projectile), dmg, damageType, attack:getAttackType(), crit, backstab)
- return modifier
- end
- end
- function EquipmentItemComponent:onRecomputeFinalStats(champion)
- if self.enabled then
- local modifier = self:callHook("onRecomputeFinalStats", objectToProxy(champion))
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeSpellCost(champion, name, cost, skill)
- if self.enabled then
- local modifier = self:callHook("onComputeSpellCost", objectToProxy(champion), name, cost, skill)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeSpellCooldown(champion, name, cost, skill)
- if self.enabled then
- local modifier = self:callHook("onComputeSpellCooldown", objectToProxy(champion), name, cost, skill)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeSpellDamage(champion, spellObject, name, cost, skill)
- if self.enabled then
- local modifier = self:callHook("onComputeSpellDamage", objectToProxy(champion), objectToProxy(spellObject), name, cost, skill)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeBombPower(champion, power)
- if self.enabled then
- local modifier = self:callHook("onComputeBombPower", objectToProxy(bombItem), objectToProxy(champion), power)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeBearFormDuration(champion)
- if self.enabled then
- local modifier = self:callHook("onComputeBearFormDuration", objectToProxy(champion))
- return modifier
- end
- end
- -- Modifies damage taken by champion
- function EquipmentItemComponent:onComputeDamageTaken(self, champion, attack, dmg, dmgType, isSpell)
- if self.enabled then
- local modifier = self:callHook("onComputeDamageTaken", objectToProxy(champion), objectToProxy(hitContext), dmg, dmgType, isSpell)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeMalfunctionChance(self, champion, weapon, attack, attackType)
- if self.enabled then
- local modifier = self:callHook("onComputeMalfunctionChance", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attackType)
- return modifier
- end
- end
- function EquipmentItemComponent:onComputeRange(self, champion, weapon, attack, attackType)
- if self.enabled then
- local modifier = self:callHook("onComputeRange", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attackType)
- return modifier
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- MeleeAttack Functions --
- -------------------------------------------------------------------------------------------------------
- defineProxyClass{
- class = "MeleeAttackComponent",
- baseClass = "ItemActionComponent",
- description = "Implements melee attack action for items. Melee attacks can hit and damage a single target in front of the party.",
- methods = {
- { "setAttackPower", "number" },
- { "setAccuracy", "number" },
- { "setCooldown", "number" },
- { "setSwipe", "string" },
- { "setAttackSound", "string" },
- { "setDamageType", "string" },
- { "setReachWeapon", "boolean" },
- { "setSkill", "string" },
- { "setRequiredLevel", "number" },
- { "setBaseDamageStat", "string" },
- { "setCauseCondition", "string" },
- { "setConditionChance", "number" },
- { "setPierce", "number" },
- { "setUnarmedAttack", "boolean" },
- { "setCameraShake", "boolean" },
- { "getAttackPower" },
- { "getAccuracy" },
- { "getCooldown" },
- { "getSwipe" },
- { "getAttackSound" },
- { "getDamageType" },
- { "getReachWeapon" },
- { "getSkill" },
- { "getRequiredLevel" },
- { "getBaseDamageStat" },
- { "getCauseCondition" },
- { "getConditionChance" },
- { "getPierce" },
- { "getUnarmedAttack" },
- { "getCameraShake" },
- { "setBaseDamageMultiplier", "number" },
- { "getBaseDamageMultiplier" },
- { "getMinDamageMod" },
- { "getMaxDamageMod" },
- { "setMinDamageMod", "number" },
- { "setMaxDamageMod", "number" },
- { "setJamChance", "number" },
- { "getJamChance" },
- { "setJammed", "boolean" },
- { "getJammed" },
- },
- hooks = {
- "onPostAttack(self, champion, slot)",
- "onHitMonster(self, monster, tside, damage, champion, crit, backstab)",
- },
- }
- extendProxyClass(MeleeAttackComponent, "pierce")
- extendProxyClass(MeleeAttackComponent, "attackPowerVariation")
- extendProxyClass(MeleeAttackComponent, "baseDamageMultiplier")
- extendProxyClass(MeleeAttackComponent, "minDamageMod")
- extendProxyClass(MeleeAttackComponent, "maxDamageMod")
- extendProxyClass(MeleeAttackComponent, "jamChance")
- extendProxyClass(MeleeAttackComponent, "jamCount")
- extendProxyClass(MeleeAttackComponent, "jamText")
- extendProxyClass(MeleeAttackComponent, "velocity")
- function MeleeAttackComponent:start(champion, slot, chainIndex)
- local weapon = champion:getItem(slot)
- if champion:hasCondition("bear_form") then weapon = nil end
- if not champion:getDamage(slot) then
- console:warn("no attack power")
- return
- end
- if self.unarmedAttack then
- party.go.statistics:increaseStat("unarmed_attacks", 1)
- else
- party.go.statistics:increaseStat("melee_attacks", 1)
- end
- -- Melee weapons could jam if you wanted to
- local malfunctionModifier = 1
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeMalfunctionChance then
- local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", iff(champion:hasTrait(name), 1, 0))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeMalfunctionChance then
- local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", champion:getSkillLevel(name))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeMalfunctionChance then
- local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "melee")
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- end
- end
- end
- local dualWield = champion:isDualWielding()
- -- jam
- if self.jamChance then
- local chance = self.jamChance * malfunctionModifier
- if not weapon:getJammed() and math.random(1, 100) <= chance then
- weapon:setJammed(true)
- weapon.jamCount = math.random(2, 6)
- end
- end
- -- fix jammed weapon
- if weapon and weapon:getJammed() then
- weapon.jamCount = (weapon.jamCount or 3) - 1
- if weapon.jamCount < 0 then
- weapon:setJammed(false)
- weapon.jamCount = nil
- end
- end
- if weapon and weapon:getJammed() then
- soundSystem:playSound2D("firearm_jammed")
- champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
- return
- end
- -- consume food
- champion:consumeFood(0.5 + math.random() * 2)
- -- cooldown
- local cooldown = self.cooldown or 0
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeCooldown then
- local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", champion:getSkillLevel(name))
- cooldown = cooldown * (modifier or 1)
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeCooldown then
- local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", iff(champion:hasTrait(name), 1, 0))
- cooldown = cooldown * (modifier or 1)
- end
- end
- if dualWield then
- if slot == ItemSlot.Weapon then
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- else
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- end
- else
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- end
- party:endCondition("invisibility")
- -- play swipe effect
- if self.swipe then
- local swipe = self.swipe
- if swipe == "flurry" then
- swipe = iff(chainIndex == 1, "vertical", "horizontal")
- end
- local side = iff(champion.championIndex == 1 or champion.championIndex == 3, 0, 1)
- party.swipes[swipe]:play(side)
- end
- -- play sound
- if self.attackSound then
- champion:playSound(self.attackSound)
- end
- if self.cameraShake then
- party:shakeCamera(0.5, 0.3)
- end
- -- get source and target tiles for attack
- local x,y = party.go.map:worldToMap(party.go:getWorldPosition())
- local dx,dy = getDxDy(party.go.facing)
- local tx = x + dx
- local ty = y + dy
- local map = party.go.map
- local dualWieldSide = iff(dualWield, slot, nil)
- -- check reach
- if champion.championIndex == 3 or champion.championIndex == 4 then
- if not self.reachWeapon and not champion:hasTrait("reach") then
- champion:showAttackResult("Can't reach")
- return
- end
- end
- -- hit wall
- if map:isWall(tx,ty) or map:getElevation(tx,ty) > party.go.elevation or map:isForceField(x, y, party.go.elevation) then
- if self.unarmedAttack then
- soundSystem:playSound2D("impact_punch")
- else
- if self.go.item.impactSound then soundSystem:playSound2D(self.go.item.impactSound) end
- end
- champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
- return
- end
- -- hit door
- local door = map:findDoor(x, y, party.go.facing, party.go.elevation)
- if door and not door:isPassable() then
- if door:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
- return
- end
- end
- -- hit force field
- for _,f in map:componentsAt(ForceFieldComponent, tx, ty) do
- if f.enabled then
- if f:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
- return
- end
- end
- end
- -- hit obstacle first (so that monsters inside obstacles get cover)
- for _,obstacle in map:componentsAt(ObstacleComponent, tx, ty) do
- if obstacle.enabled then
- if obstacle:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
- return
- end
- end
- end
- -- hit monster
- for _,monster in map:componentsAt(MonsterComponent, tx, ty) do
- local fullHealth = monster:getHealth() == monster:getMaxHealth()
- if monster:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
- -- unstoppable achievement: kill monster with a single blow
- if fullHealth and not monster:isAlive() then
- steamContext:unlockAchievement("unstoppable")
- end
- if not monster:isAlive() and not weapon then
- steamContext:unlockAchievement("fist_fighter")
- end
- return
- end
- end
- champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
- -- HACK: self.go is nil for bear claw attacks
- if self.go then
- self:callHook("onPostAttack", objectToProxy(champion), slot)
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- RangedAttack Functions --
- -------------------------------------------------------------------------------------------------------
- defineProxyClass{
- class = "RangedAttackComponent",
- baseClass = "ItemActionComponent",
- description = "Implements missile attack action for items. Missile attacks require ammo that must be held in champion's other hand.",
- methods = {
- { "setAttackPower", "number" },
- { "setAttackPowerVariation", "number" },
- { "setCooldown", "number" },
- { "setSwipe", "string" },
- { "setAttackSound", "string" },
- { "setSkill", "string" },
- { "setRequiredLevel", "number" },
- { "setAmmo", "string" },
- { "setBaseDamageStat", "string" },
- { "setDamageType", "string" },
- { "setProjectileItem", "string" },
- { "getAttackPower" },
- { "getAttackPowerVariation" },
- { "getCooldown" },
- { "getSwipe" },
- { "getAttackSound" },
- { "getSkill" },
- { "getRequiredLevel" },
- { "getAmmo" },
- { "getBaseDamageStat" },
- { "getDamageType" },
- { "getProjectileItem" },
- { "setBaseDamageMultiplier", "number" },
- { "getBaseDamageMultiplier" },
- { "getMinDamageMod" },
- { "getMaxDamageMod" },
- { "setMinDamageMod", "number" },
- { "setMaxDamageMod", "number" },
- { "setPierce", "number" },
- { "getPierce" },
- { "setJamChance", "number" },
- { "getJamChance" },
- { "setJammed", "boolean" },
- { "getJammed" },
- { "setCritMultiplier", "number" },
- { "getCritMultiplier" },
- },
- hooks = {
- "onPostAttack(self, champion, slot)",
- },
- }
- extendProxyClass(RangedAttackComponent, "pierce")
- extendProxyClass(RangedAttackComponent, "critMultiplier")
- extendProxyClass(RangedAttackComponent, "attackPowerVariation")
- extendProxyClass(RangedAttackComponent, "baseDamageMultiplier")
- extendProxyClass(RangedAttackComponent, "minDamageMod")
- extendProxyClass(RangedAttackComponent, "maxDamageMod")
- extendProxyClass(RangedAttackComponent, "jamChance")
- extendProxyClass(RangedAttackComponent, "jamCount")
- extendProxyClass(RangedAttackComponent, "jamText")
- extendProxyClass(RangedAttackComponent, "velocity")
- local oldRangedAttackComponentInit = RangedAttackComponent.init
- function RangedAttackComponent:init(go)
- oldRangedAttackComponentInit(self, go)
- self.pierce = 0
- self.jamChance = 0
- end
- function RangedAttackComponent:start(champion, slot)
- local weapon = champion:getItem(slot)
- local ammoSlot = nil
- -- check ammo
- if not self:checkAmmo(champion, slot) then
- champion:showAttackResult("No ammo")
- return
- end
- -- cooldown
- local cooldown = self.cooldown or 0
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeCooldown then
- local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", champion:getSkillLevel(name))
- cooldown = cooldown * (modifier or 1)
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeCooldown then
- local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", iff(champion:hasTrait(name), 1, 0))
- cooldown = cooldown * (modifier or 1)
- end
- end
- -- check dual wielding
- local dualWield = champion:isDualWielding()
- if dualWield then
- if slot == ItemSlot.Weapon then
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- else
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- end
- else
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- end
- local dualWieldSide = iff(dualWield, slot, nil)
- -- check ammo
- if not (item and item.go.ammoitem and self.ammo == item.go.ammoitem:getAmmoType()) then
- champion:showAttackResult("No ammo", nil, dualWieldSide)
- return
- end
- -- Missile weapons could jam if you wanted to
- local malfunctionModifier = 1
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeMalfunctionChance then
- local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", iff(champion:hasTrait(name), 1, 0))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeMalfunctionChance then
- local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", champion:getSkillLevel(name))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeMalfunctionChance then
- local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "missile")
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- end
- end
- end
- local dualWieldSide = iff(dualWield, slot, nil)
- -- jam
- if self.jamChance then
- local chance = self.jamChance * malfunctionModifier
- if not weapon:getJammed() and math.random(1, 100) <= chance then
- weapon:setJammed(true)
- weapon.jamCount = math.random(2, 6)
- end
- end
- -- fix jammed weapon
- if weapon:getJammed() then
- weapon.jamCount = (weapon.jamCount or 3) - 1
- if weapon.jamCount < 0 then
- weapon:setJammed(false)
- weapon.jamCount = nil
- end
- end
- if weapon:getJammed() then
- soundSystem:playSound2D("firearm_jammed")
- champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
- return
- end
- -- consume energy
- if math.random() < 0.8 then
- local cost = self.cooldown * (math.random() * 0.6 + 0.4)
- cost = math.floor(cost + 0.5)
- --print("consume energy: "..cost)
- champion:modifyBaseStat("energy", -cost)
- end
- -- consume food
- champion:consumeFood(0.5 + math.random() * 2)
- party:endCondition("invisibility")
- party.go.statistics:increaseStat("missile_attacks", 1)
- -- play sound
- if self.attackSound then
- champion:playSound(self.attackSound)
- end
- local count = 1
- for i=1,count do
- if not (item and self.ammo == item.go.ammoitem:getAmmoType()) then return end
- local ammo = champion:getItem(ammoSlot)
- -- determine damage
- -- note: this has to be done before removing the ammo from hand!
- local dmg = computeDamage(champion:getDamageWithAttack(self.go.item, self))
- -- split stack
- if ammo.stackable and ammo.count > 1 then
- ammo = ammo:splitStack(1)
- else
- -- shoot last
- champion:removeItemFromSlot(ammoSlot)
- if ammoSlot == ItemSlot.Weapon then
- champion.autoEquipEmptyHand = ammo.go.arch.name
- else
- champion.autoEquipOffHand = ammo.go.arch.name
- end
- end
- local side = iff(champion.championIndex == 1 or champion.championIndex == 3, 0, 1)
- local power = 14
- local gravity = 1
- local pos = champion:getChampionPositionInWorld(0.4)
- -- push forward so that item won't collide against a door behind us
- pos = pos + party:getWorldForward() * ammo:getBoundingRadius()
- -- separate missiles if shooting multiple missiles
- if i > 1 then
- pos = pos + party:getWorldForward() * (i-1) * 0.9
- if i == 2 then
- pos.y = pos.y - 0.1
- elseif i == 3 then
- pos.y = pos.y + 0.1
- end
- end
- -- convert projectile type (e.g. normal arrow to fire arrow)
- if self.projectileItem then
- ammo = create(self.projectileItem).item
- end
- ammo:throw(party, pos, party.go.facing, power, gravity, 0)
- ammo:setItemFlag(ItemFlag.AutoPickUp, true)
- ammo.projectileDamage = dmg
- ammo.projectileDamageType = self.damageType
- ammo.projectileAccuracy = champion:getAccuracyWithAttack(weapon, self)
- ammo.projectileCritChance = champion:getCritChanceWithAttack(weapon, self)
- ammo.projectilePierce = self.pierce or 0
- ammo.go.projectile:setVelocity(ammo.go.projectile:getVelocity() * (self.velocity or 1))
- ammo.thrownByChampion = champion.ordinal
- -- store original weapon and attack data in projectile
- ammo.thrownByWeapon = weapon
- ammo.thrownByAttack = self
- end
- champion:showAttackResult("Shoot", nil, dualWieldSide)
- self:callHook("onPostAttack", objectToProxy(champion), slot)
- end
- -------------------------------------------------------------------------------------------------------
- -- ThrowAttack Functions --
- -------------------------------------------------------------------------------------------------------
- defineProxyClass{
- class = "ThrowAttackComponent",
- baseClass = "ItemActionComponent",
- description = "Implements throw attack action for items. When thrown, a Projectile component is dynamically created and added to the thrown item.",
- methods = {
- { "setAttackPower", "number" },
- { "setAttackPowerVariation", "number" },
- { "setCooldown", "number" },
- { "setSwipe", "string" },
- { "setAttackSound", "string" },
- { "setSkill", "string" },
- { "setRequiredLevel", "number" },
- { "setBaseDamageStat", "string" },
- { "getAttackPower" },
- { "getAttackPowerVariation" },
- { "getCooldown" },
- { "getSwipe" },
- { "getAttackSound" },
- { "getSkill" },
- { "getRequiredLevel" },
- { "getBaseDamageStat" },
- { "setBaseDamageMultiplier", "number" },
- { "getBaseDamageMultiplier" },
- { "getMinDamageMod" },
- { "getMaxDamageMod" },
- { "setMinDamageMod", "number" },
- { "setMaxDamageMod", "number" },
- { "setJamChance", "number" },
- { "getJamChance" },
- { "setJammed", "boolean" },
- { "getJammed" },
- { "setCritMultiplier", "number" },
- { "getCritMultiplier" },
- },
- hooks = {
- "onPostAttack(self, champion, slot)",
- }
- }
- extendProxyClass(ThrowAttackComponent, "critMultiplier")
- extendProxyClass(ThrowAttackComponent, "attackPowerVariation")
- extendProxyClass(ThrowAttackComponent, "baseDamageMultiplier")
- extendProxyClass(ThrowAttackComponent, "minDamageMod")
- extendProxyClass(ThrowAttackComponent, "maxDamageMod")
- extendProxyClass(ThrowAttackComponent, "jamChance")
- extendProxyClass(ThrowAttackComponent, "jamCount")
- extendProxyClass(ThrowAttackComponent, "jamText")
- function ThrowAttackComponent:start(champion, slot)
- local weapon = champion:getItem(slot)
- party.go.statistics:increaseStat("throw_attacks", 1)
- -- Throw weapons could jam if you wanted to
- local malfunctionModifier = 1
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeMalfunctionChance then
- local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", iff(champion:hasTrait(name), 1, 0))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- skills modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeMalfunctionChance then
- local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", champion:getSkillLevel(name))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeMalfunctionChance then
- local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "throw")
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- end
- end
- end
- local dualWield = champion:isDualWielding()
- local dualWieldSide = iff(dualWield, slot, nil)
- -- jam
- if self.jamChance then
- local chance = self.jamChance * malfunctionModifier
- if not weapon:getJammed() and math.random(1, 100) <= chance then
- weapon:setJammed(true)
- weapon.jamCount = math.random(2, 6)
- end
- end
- -- fix jammed weapon
- if weapon:getJammed() then
- weapon.jamCount = (weapon.jamCount or 3) - 1
- if weapon.jamCount < 0 then
- weapon:setJammed(false)
- weapon.jamCount = nil
- end
- end
- if weapon:getJammed() then
- soundSystem:playSound2D("firearm_jammed")
- champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
- return
- end
- -- consume energy
- if math.random() < 0.8 then
- local cost = self.cooldown * (math.random() * 0.6 + 0.4)
- cost = math.floor(cost + 0.5)
- --print("consume energy: "..cost)
- champion:modifyBaseStat("energy", -cost)
- end
- -- consume food
- champion:consumeFood(0.5 + math.random() * 2)
- -- cooldown
- local cooldown = self.cooldown or 0
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeCooldown then
- local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", champion:getSkillLevel(name))
- cooldown = cooldown * (modifier or 1)
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeCooldown then
- local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", iff(champion:hasTrait(name), 1, 0))
- cooldown = cooldown * (modifier or 1)
- end
- end
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- party:endCondition("invisibility")
- -- play sound
- if self.attackSound then
- champion:playSound(self.attackSound)
- end
- local count = 1
- --if champion:getSkillLevel("double_throw") > 0 then count = 2 end
- for i=1,count do
- -- weapons left to throw?
- local item = champion:getItem(slot)
- if not item then return end
- -- determine damage
- -- note: this has to be done before removing the item from hand!
- local dmg = computeDamage(champion:getDamageWithAttack(weapon, self))
- -- split stack
- local projectile
- if item.stackable and item.count > 1 then
- projectile = item:splitStack(1)
- else
- -- throw last
- if champion:getItem(ItemSlot.Weapon) == item then
- champion:removeItemFromSlot(ItemSlot.Weapon)
- champion.autoEquipEmptyHand = item.go.arch.name
- else
- champion:removeItemFromSlot(ItemSlot.OffHand)
- champion.autoEquipOffHand = item.go.arch.name
- end
- projectile = item
- end
- local pos = champion:getChampionPositionInWorld(0.4)
- -- push forward so that item won't collide against a door behind us
- pos = pos + party:getWorldForward() * item:getBoundingRadius()
- -- separate projectiles if shooting multiple projectiles
- if self.spread then
- pos.x = pos.x + (math.random() - 0.5) * self.spread
- pos.y = pos.y + (math.random() - 0.5) * self.spread
- pos.z = pos.z + (math.random() - 0.5) * self.spread
- end
- local power = 14
- local gravity = 1
- local velocityUp = 0
- if projectile.go.arch.bombType then
- local weight = 0.8
- power = math.max(14 - weight, 10)
- gravity = math.clamp(2 + weight*1.5, 4, 10)
- velocityUp = 0
- end
- projectile:throw(party, pos, party.go.facing, power, gravity, velocityUp)
- projectile:setItemFlag(ItemFlag.AutoPickUp, true)
- projectile.go.projectile:setVelocity(projectile.go.projectile:getVelocity() * (self.velocity or 1))
- projectile.projectileDamage = dmg
- projectile.projectileAccuracy = champion:getAccuracyWithAttack(weapon, self)
- projectile.projectileCritChance = champion:getCritChanceWithAttack(weapon, self)
- projectile.projectilePierce = self.pierce
- projectile.thrownByChampion = champion.ordinal
- projectile.thrownByWeapon = weapon
- projectile.thrownByAttack = self
- messageSystem:sendMessageNEW("onPartyThrowItem", projectile)
- end
- champion:showAttackResult("Throw")
- self:callHook("onPostAttack", objectToProxy(champion), slot)
- end
- -------------------------------------------------------------------------------------------------------
- -- FirearmAttack Functions --
- -------------------------------------------------------------------------------------------------------
- defineProxyClass{
- class = "FirearmAttackComponent",
- baseClass = "ItemActionComponent",
- description = "Implements firearm attacks. Firearm attacks need ammo.",
- methods = {
- { "setAttackPower", "number" },
- { "setAttackPowerVariation", "number" },
- { "setRange", "number" },
- { "setCooldown", "number" },
- { "setAttackSound", "string" },
- { "setSkill", "string" },
- { "setRequiredLevel", "number" },
- { "setAccuracy", "number" },
- { "setAmmo", "string" },
- { "setBaseDamageStat", "string" },
- { "setClipSize", "number" },
- { "setDamageType", "string" },
- { "setLoadedCount", "number" },
- { "setPierce", "number" },
- { "getAttackPower" },
- { "getAttackPowerVariation" },
- { "getRange" },
- { "getCooldown" },
- { "getAttackSound" },
- { "getSkill" },
- { "getRequiredLevel" },
- { "getAccuracy" },
- { "getAmmo" },
- { "getBaseDamageStat" },
- { "getClipSize" },
- { "getDamageType" },
- { "getLoadedCount" },
- { "getPierce" },
- { "setBaseDamageMultiplier", "number" },
- { "getBaseDamageMultiplier" },
- { "getMinDamageMod" },
- { "getMaxDamageMod" },
- { "setMinDamageMod", "number" },
- { "setMaxDamageMod", "number" },
- { "setJamChance", "number" },
- { "getJamChance" },
- { "setJammed", "boolean" },
- { "getJammed" },
- { "setCritMultiplier", "number" },
- { "getCritMultiplier" },
- },
- hooks = {
- "onBackfire(self, champion)",
- "onPostAttack(self, champion, slot)",
- },
- }
- extendProxyClass(FirearmAttackComponent, "critMultiplier")
- extendProxyClass(FirearmAttackComponent, "attackPowerVariation")
- extendProxyClass(FirearmAttackComponent, "baseDamageMultiplier")
- extendProxyClass(FirearmAttackComponent, "minDamageMod")
- extendProxyClass(FirearmAttackComponent, "maxDamageMod")
- extendProxyClass(FirearmAttackComponent, "jamText")
- function FirearmAttackComponent:start(champion, slot)
- local weapon = champion:getItem(slot)
- local ammoSlot = iff(slot == ItemSlot.Weapon, ItemSlot.OffHand, ItemSlot.Weapon)
- if not champion:getDamage(slot) then
- console:warn("no attack power")
- return
- end
- -- check ammo
- if self:checkAmmo(champion, ammoSlot) < 1 then
- if self.clipSize then
- champion:showAttackResult("Clip empty")
- --soundSystem:playSound2D("firearm_jammed")
- else
- champion:showAttackResult("No ammo")
- end
- return
- end
- local malfunctionModifier = 1
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeMalfunctionChance then
- local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", iff(champion:hasTrait(name), 1, 0))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeMalfunctionChance then
- local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", champion:getSkillLevel(name))
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeMalfunctionChance then
- local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "firearm")
- malfunctionModifier = malfunctionModifier * (modifier or 1)
- end
- end
- end
- end
- end
- local dualWield = champion:isDualWielding()
- -- jam
- if self.jamChance then
- local chance = self.jamChance * malfunctionModifier
- if not weapon:getJammed() and math.random(1, 100) <= chance then
- weapon:setJammed(true)
- weapon.jamCount = math.random(2, 6)
- end
- end
- -- fix jammed weapon
- if weapon:getJammed() then
- weapon.jamCount = (weapon.jamCount or 3) - 1
- if weapon.jamCount < 0 then
- weapon:setJammed(false)
- weapon.jamCount = nil
- end
- end
- if weapon:getJammed() then
- soundSystem:playSound2D("firearm_jammed")
- champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
- return
- end
- party.go.statistics:increaseStat("firearm_attacks", 1)
- -- consume ammo
- if self.clipSize then
- self.loadedCount = self.loadedCount - 1
- else
- self:consumeAmmo(champion, ammoSlot, 1)
- end
- champion:consumeFood(0.5 + math.random() * 2)
- -- cooldown
- local cooldown = self.cooldown or 0
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeCooldown then
- local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", champion:getSkillLevel(name))
- cooldown = cooldown * (modifier or 1)
- end
- end
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeCooldown then
- local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", iff(champion:hasTrait(name), 1, 0))
- cooldown = cooldown * (modifier or 1)
- end
- end
- -- backfire
- if self.backfireChance then
- local chance = self.backfireChance * malfunctionModifier
- --print("backfire chance = ", chance)
- if math.random(1, 100) <= chance then
- if self:callHook("onBackfire", objectToProxy(champion)) ~= false then
- party.go:spawn("fireburst")
- party:shakeCamera(1, 0.3)
- champion:showAttackResult("Backfire!")
- champion:playDamageSound()
- if self.attackSound then champion:playSound(self.attackSound) end
- weapon:setJammed(true)
- return
- end
- end
- end
- -- cooldown
- if dualWield then
- if slot == ItemSlot.Weapon then
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- else
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- end
- else
- champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
- champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
- end
- party:endCondition("invisibility")
- -- play sound
- if self.attackSound then
- champion:playSound(self.attackSound)
- end
- local dualWieldSide = iff(dualWield, slot, nil)
- -- trace ray
- local origin = party.go:getWorldPosition()
- origin.y = origin.y + 1.3
- local range = self.range
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeRange then
- local modifier = trait.onComputeRange(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", iff(champion:hasTrait(name), 1, 0))
- range = range + (modifier or 0)
- end
- end
- -- skill modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeRange then
- local modifier = skill.onComputeRange(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", champion:getSkillLevel(name))
- range = range + (modifier or 0)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeRange then
- local modifier = comp:onComputeRange(champion, weapon, self, "firearm")
- range = range + (modifier or 0)
- end
- end
- end
- end
- end
- local hitWhat, hitEntity, hitPos = self.raycast(origin, party.go.facing, range, party)
- local hitStatus
- if hitEntity then
- --print(hitWhat, hitEntity.arch.name)
- hitStatus = hitEntity:sendMessage("onAttackedByChampion", champion, weapon, self, slot, dualWieldSide)
- end
- -- spawn particle effect
- if hitWhat and hitStatus ~= "miss" then
- local map = party.go.map
- local x,y = map:worldToMap(hitPos)
- local fx = spawn(map, "particle_system", x, y, 0, party.go.elevation)
- fx.particle:setParticleSystem("hit_firearm")
- -- random spread
- local spread = 0.5
- local dx,dy = getDxDy(party.go.facing)
- local pos = vec(hitPos.x + (math.random() - 0.5) * spread * dy, hitPos.y + (math.random() - 0.5) * spread, hitPos.z + (math.random() - 0.5) * spread * dx)
- fx:setWorldPosition(pos)
- end
- if not hitStatus then
- champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
- end
- self:callHook("onPostAttack", objectToProxy(champion), slot)
- end
- -------------------------------------------------------------------------------------------------------
- -- BombItem Functions --
- -------------------------------------------------------------------------------------------------------
- function BombItemComponent:explode(map, x, y, facing)
- if self:callHook("onExplode", map.level, x, y, facing, self.go.elevation) == false then
- return
- end
- local power = self.bombPower or 0
- -- damage multiplier from multiple bombs
- local item = self.go.item
- if item and item.count and item.count > 1 then
- power = math.floor(power * (1 + (item.count-1) * 0.2))
- end
- local thrownByChampion = item.thrownByChampion
- -- bombs boost
- local champion = party:getChampionByOrdinal(thrownByChampion)
- if champion and champion:getClass() == "alchemist" then
- end
- -- traits modifiers
- for name,skill in pairs(dungeon.skills) do
- if skill.onComputeBombPower then
- power = skill.onComputeBombPower(objectToProxy(self), objectToProxy(champion), power, champion:getSkillLevel(name)) or power
- end
- end
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeBombPower then
- power = trait.onComputeBombPower(objectToProxy(self), objectToProxy(champion), power, iff(self:hasTrait(name), 1, 0)) or power
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = champion:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeBombPower then
- power = comp:onComputeBombPower(self, champion, power) or power
- end
- end
- end
- end
- end
- local elevation = self.go.elevation
- if self.bombType == "shock" then
- local ent = spawn(map, "shockburst", x, y, facing, elevation)
- ent.tiledamager:setAttackPower(power)
- ent.tiledamager:setCastByChampion(thrownByChampion)
- elseif self.bombType == "fire" then
- local ent = spawn(map, "fireburst", x, y, facing, elevation)
- ent.tiledamager:setAttackPower(power)
- ent.tiledamager:setCastByChampion(thrownByChampion)
- -- spawn wall of fire but only if there's ground or a platform underneath
- if elevation == map:getElevation(x, y) or PlatformComponent.getPlatformAt(map, x, y, elevation) then
- local ent = spawn(map, "wall_fire", x, y, facing, elevation)
- ent.tiledamager:setAttackPower(5)
- ent.tiledamager:setCastByChampion(thrownByChampion)
- end
- elseif self.bombType == "frost" then
- local ent = spawn(map, "frostburst", x, y, facing, elevation)
- ent.tiledamager:setAttackPower(power)
- ent.tiledamager:setCastByChampion(thrownByChampion)
- elseif self.bombType == "poison" then
- local ent = spawn(map, "poison_cloud_medium", x, y, 0, elevation)
- ent.cloudspell:setAttackPower(power)
- ent.cloudspell:setCastByChampion(thrownByChampion)
- else
- console:warn("unknown bomb type: "..tostring(self.bombType))
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- CastSpell Functions --
- -------------------------------------------------------------------------------------------------------
- function CastSpellComponent:start(champion, slot)
- local name = self.spell
- if not name then console:warn("unknown wand spell"); return end
- -- find spell
- local spell = Spell.getSpell(name)
- if not spell then
- console:warn("Unknown spell: "..name)
- return
- end
- if self:checkCharges(champion) == false then return end
- -- use wand's power as spell skill
- --if not self.power then console:warn("wand power not set for "..weapon.go.arch.name) end
- local skill = (self.power or 0)
- local pos = party.go:getWorldPositionFast()
- local x,y = party.go.map:worldToMap(pos)
- Spell.castSpell(spell, champion, x, y, party.go.facing, party.go.elevation, skill, spell)
- champion.cooldownTimer[1] = champion.cooldownTimer[1] + (self.cooldown or 0)
- champion.cooldownTimer[2] = champion.cooldownTimer[2] + (self.cooldown or 0)
- -- consume charges
- self:consumeCharges(champion)
- -- strenous activity consumes food
- champion:consumeFood(math.random(1,5))
- end
- function CastSpellComponent:checkCharges(champion)
- if self.charges == 0 then return false end
- end
- function CastSpellComponent:consumeCharges(champion)
- if self.charges then
- self.charges = self.charges - 1
- if self.charges < 1 then
- self:deplete()
- end
- end
- end
- -------------------------------------------------------------------------------------------------------
- -- CraftPotion Functions --
- -------------------------------------------------------------------------------------------------------
- function CraftPotionComponent:brewPotion(champion)
- if self.recipe == 0 then return end
- local alchemy = champion:getSkillLevel("alchemy")
- -- verify that champion has enough herbs
- local herbs = CraftPotionComponent.Herbs
- for i=1,#herbs do
- if herbs[i].count < herbs[i].reserved then
- gui:hudPrint(champion.name.." does not have enough herbs to craft this potion.")
- return
- end
- end
- -- get recipe
- local recipe = self:getPotionRecipe(self.recipe)
- if not recipe then
- gui:hudPrint(champion.name.." failed to brew a potion.")
- self.recipe = 0
- champion:showAttackPanel(nil)
- return
- end
- -- check alchemy skill
- if alchemy < (recipe.level or 0) then
- gui:hudPrint(champion.name.." is not skilled enough in Alchemy to brew this potion.")
- return
- end
- -- consume herbs
- local r = self.recipe
- for i=5,0,-1 do
- -- extract herb from recipe
- local h = math.floor(r / 10^i)
- r = r - h * 10^i
- if h ~= 0 then
- self:consumeHerb(champion, CraftPotionComponent.Herbs[h%10].name)
- end
- end
- local potion = recipe.potion
- if champion:hasTrait("improved_alchemy") then
- if potion == "potion_healing" then potion = "potion_greater_healing" end
- if potion == "potion_energy" then potion = "potion_greater_energy" end
- end
- local count = self:countPotions(recipe, champion)
- local returnVal = party:callHook("onBrewPotion", count, potion, objectToProxy(champion))
- if returnVal then
- if returnVal[1] == false then return false end
- count = returnVal[2] or count
- potion = returnVal[3] or potion
- end
- self:onBrewPotion(count, recipe, champion)
- local mouseItem = gui:getMouseItem()
- if mouseItem == nil then
- -- create new potion to mouse hand
- local item = create(potion).item
- item:setStackSize(count)
- gui:setMouseItem(item)
- elseif mouseItem.go.arch.name == potion then
- -- merge new potion to stack in hand
- mouseItem.count = mouseItem.count + count
- else
- -- create new potion on the ground
- local item = spawn(party.go.map, potion, party.go.x, party.go.y, party.go.facing, party.go.elevation).item
- item:setStackSize(count)
- end
- soundSystem:playSound2D("brew_potion")
- party.go.statistics:increaseStat("potions_mixed", 1)
- self.recipe = 0
- champion:showAttackPanel(nil)
- end
- function CraftPotionComponent:countPotions(recipe, champion)
- local potion = recipe.potion
- local count = 1
- if champion:hasTrait("bomb_expert") then
- if potion == "fire_bomb" or potion == "shock_bomb" or potion == "poison_bomb" or potion == "frost_bomb" then
- count = 3
- end
- end
- return count
- end
- function CraftPotionComponent:onBrewPotion(recipe, champion)
- -- Triggers on potion crafted
- end
- -------------------------------------------------------------------------------------------------------
- -- Condition Functions --
- -------------------------------------------------------------------------------------------------------
- local oldConditionInit = Condition.init
- function Condition:init(uiName, description, iconIndex, ...)
- oldConditionInit(self, uiName, description, iconIndex, ...)
- self.power = 0
- self.stacks = 1
- end
- function Condition:setPower(value)
- self.power = power
- end
- function Condition:getPower()
- return self.power
- end
- local oldConditionLoadState = Condition.loadState
- function Condition:loadState(file, ...)
- oldConditionLoadState(self, file, ...)
- self.power = file:readValue()
- self.stacks = file:readValue()
- end
- local oldConditionSaveState = Condition.saveState
- function Condition:saveState(file, ...)
- oldConditionSaveState(self, file, ...)
- file:writeValue(self.power)
- file:writeValue(self.stacks)
- end
- function Condition:update(champion)
- self.timer = self.timer - Time.deltaTime
- if self.timer <= 0 then
- self:tick(champion)
- end
- if self.value then
- self.value = self.value - Time.deltaTime
- if self.value <= 0 then
- if self.stacks > 1 then
- self.stacks = self.stacks - 1
- self.value = self.stackTimer
- elseif self.stacks == 1 then
- self.stacks = self.stacks - 1
- else
- return false
- end
- end
- end
- end
- local oldCustomConditionInit = CustomCondition.init
- function CustomCondition:init(name)
- oldCustomConditionInit(self, name)
- local desc = dungeon.conditions[name]
- self.maxStacks = desc.maxStacks
- self.stackTimer = desc.stackTimer
- self.name = desc.name
- end
- function CustomCondition:getStacks()
- return self.stacks
- end
- function CustomCondition:start(champion)
- if self.onStart then self.onStart(objectToProxy(self), objectToProxy(champion), true, self.power, self.stacks) end
- end
- function CustomCondition:restart(champion)
- if self.onStart then self.onStart(objectToProxy(self), objectToProxy(champion), false, self.power, self.stacks) end
- end
- function CustomCondition:stop(champion)
- if self.onStop then self.onStop(objectToProxy(self), objectToProxy(champion), self.power, self.stacks) end
- end
- function CustomCondition:tick(champion)
- if self.onTick then self.onTick(objectToProxy(self), objectToProxy(champion), self.power, self.stacks) end
- self.timer = self.tickInterval or 1
- end
- function CustomCondition:recomputeStats(champion)
- if self.onRecomputeStats then self.onRecomputeStats(objectToProxy(self), objectToProxy(champion), self.power, self.stacks) end
- end
- function ProtectiveShieldCondition:recomputeStats(champion)
- local stats = champion.stats
- stats.protection.current = stats.protection.current + champion.conditions.protective_shield.power
- end
- function FireShieldCondition:recomputeStats(champion)
- local stats = champion.stats
- stats.resist_fire.current = stats.resist_fire.current + champion.conditions.fire_shield.power
- end
- function FrostShieldCondition:recomputeStats(champion)
- local stats = champion.stats
- stats.resist_cold.current = stats.resist_cold.current + champion.conditions.frost_shield.power
- end
- function ShockShieldCondition:recomputeStats(champion)
- local stats = champion.stats
- stats.resist_shock.current = stats.resist_shock.current + champion.conditions.shock_shield.power
- end
- function PoisonShieldCondition:recomputeStats(champion)
- local stats = champion.stats
- stats.resist_poison.current = stats.resist_poison.current + champion.conditions.poison_shield.power
- end
- function BearFormCondition:update(champion)
- local multi = 1
- -- skill modifiers
- for name,skill in pairs(dungeon.skill) do
- if skill.onComputeBearFormDuration then
- local modifier = skill.onComputeBearFormDuration(objectToProxy(self), self:getSkillLevel(name))
- multi = multi * (modifier or 1)
- end
- end
- -- traits modifiers
- for name,trait in pairs(dungeon.traits) do
- if trait.onComputeBearFormDuration then
- local modifier = trait.onComputeBearFormDuration(objectToProxy(self), iff(self:hasTrait(name), 1, 0))
- multi = multi * (modifier or 1)
- end
- end
- -- equipment modifiers (equipped items only)
- for i=1,ItemSlot.BackpackFirst-1 do
- local it = self:getItem(i)
- if it then
- if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
- for i=1,it.go.components.length do
- local comp = it.go.components[i]
- if comp.onComputeBearFormDuration then
- multi = multi * (comp:onComputeBearFormDuration(self, champion) or 1)
- end
- end
- end
- end
- end
- return champion:spendEnergy(Time.deltaTime*multi)
- end
- -------------------------------------------------------------------------------------------------------
- -- Spell Functions --
- -------------------------------------------------------------------------------------------------------
- function Spell.castSpell(spell, caster, x, y, direction, elevation, skill)
- -- caster = champion casting the spell
- -- skill = caster's skill in spell's school of magic (or wand's strength for wand spells)
- if type(spell.onCast) == "string" then
- -- built-in spell
- local spellFunc = BuiltInSpell[spell.onCast]
- if spellFunc then
- spellFunc(caster, x, y, direction, elevation, skill, spell) -- adds spell data to call
- else
- console:warn("unknown built-in spell: "..spell.onCast)
- end
- else
- -- custom spell
- spell.onCast(objectToProxy(caster), x, y, direction, elevation, skill, spell) -- adds spell data to call
- end
- end
- BuiltInSpell = {}
- -- We redefine built-in spells so it can take the spell definition data instead of using hard-coded values
- function BuiltInSpell.shield(caster, x, y, direction, elevation, skill, spl)
- soundSystem:playSound2D("generic_spell")
- caster:setCondition("protective_shield")
- local duration = spl.duration + skill * (spl.durationScaling or 0)
- local power = spl.power + skill * (spl.powerScaling or 0)
- caster.conditions.protective_shield:setDuration(duration)
- caster.conditions.protective_shield:setPower(power)
- -- gui:hudPrint(caster.name.." conjures a magical shield.")
- end
- function BuiltInSpell.light(caster, x, y, direction, elevation, skill, spl)
- soundSystem:playSound2D("light")
- if party.lightSpell < 0 then
- -- light cancels darkness
- party.lightSpell = 0
- else
- party.lightSpell = spl.duration + level * spl.durationScaling
- end
- -- gui:hudPrint(caster.name.." conjures magical light.")
- end
- function BuiltInSpell.darkness(caster, x, y, direction, elevation, skill, spl)
- soundSystem:playSound2D("generic_spell")
- if party.lightSpell > 0 then
- -- darkness cancels light
- party.lightSpell = 0
- else
- party.lightSpell = (spl.duration + level * spl.durationScaling) * -1
- end
- end
- function BuiltInSpell.darkbolt(caster, x, y, direction, elevation, skill, spl)
- --soundSystem:playSound2D("generic_spell")
- local spell = spawn(party.go.map, "dark_bolt", x, y, direction, elevation)
- spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(caster.ordinal)
- party:endCondition("invisibility")
- end
- function BuiltInSpell.forceField(caster, x, y, direction, elevation, skill, spl)
- x,y = Spell.getBurstTargetTile(x, y, direction)
- local duration = math.max(spl.duration + skill * (spl.durationScaing or 0), 5)
- soundSystem:playSound2D("force_field_cast")
- for _,f in party.go.map:componentsAt(ForceFieldComponent, x, y) do
- if f.enabled then
- if not f.duration then
- -- square already has a permanent force field
- return
- else
- -- square already has a temporary force field -> reset duration
- f.duration = math.max(f.duration, duration)
- return
- end
- end
- end
- local spell = spawn(party.go.map, "force_field", x, y, direction, elevation)
- spell.forcefield:setDuration(duration)
- end
- function BuiltInSpell.fireburst(caster, x, y, direction, elevation, skill, spl)
- local map = party.go.map
- x,y = Spell.getBurstTargetTile(x, y, direction)
- local spell = spawn(map, "fireburst", x, y, direction, elevation)
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.tiledamager:setAttackPower(power)
- spell.tiledamager:setCastByChampion(caster.ordinal)
- if skill == 1 then spell.tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects) end
- -- cause burning condition
- local chance = spl.duration * (1 + skill * (spl.durationScaling or 0))
- if skill >= 3 and math.random() < chance then
- for _,monster in map:componentsAt(MonsterComponent, x, y) do
- if monster.go.elevation == elevation then
- monster:setCondition("burning")
- -- mark condition so that exp is awarded if monster is killed by the condition
- local burningCondition = monster.go.burning
- if burningCondition then
- burningCondition:setCausedByChampion(caster.ordinal)
- end
- end
- end
- end
- party:endCondition("invisibility")
- return spell
- end
- function BuiltInSpell.fireball(caster, x, y, direction, elevation, skill, spl)
- local spell
- if skill < 2 then
- spell = spawn(party.go.map, "fireball_small", x, y, direction, elevation)
- elseif skill == 2 then
- spell = spawn(party.go.map, "fireball_medium", x, y, direction, elevation)
- else
- spell = spawn(party.go.map, "fireball_large", x, y, direction, elevation)
- end
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(caster.ordinal)
- party:endCondition("invisibility")
- return spell
- end
- function Spell_meteorStorm(casterOrdinal, spreadX, spreadY, spl)
- if party:isUnderwater() then return end
- local meteorCount = 5
- local spell = spawn(party.go.map, "fireball_medium", party.go.x, party.go.y, party.go.facing, party.go.elevation)
- local caster = party:getChampionByOrdinal(casterOrdinal)
- local pos = Spell.getCasterPositionInWorld(caster)
- local skill = caster:getSkillLevel("fire_magic")
- -- offset position
- local rdx,rdy = getDxDy((party.go.facing+1)%4)
- pos.x = pos.x - rdx * spreadX
- pos.y = pos.y + spreadY
- pos.z = pos.z + rdy * spreadX
- spell:setWorldPosition(pos)
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(casterOrdinal)
- end
- function BuiltInSpell.meteorStorm(caster, x, y, direction, elevation, skill, spl)
- local meteorCount = 5
- for i=1,meteorCount do
- local spreadX = math.random() * 0.5 * iff((i % 2) == 0, 1, -1)
- local spreadY = -(i / meteorCount - 0.5)
- messageSystem:delayedFunctionCall("Spell_meteorStorm", (i-1) * 0.15, caster.ordinal, spreadX, spreadY, spl)
- end
- party:endCondition("invisibility")
- end
- function BuiltInSpell.fireShield(caster, x, y, direction, elevation, skill, spl)
- local duration = spl.duration + skill * (spl.durationScaling or 0)
- local power = spl.power + skill * (spl.powerScaling or 0)
- Spell.elementalShield("fire_shield", duration, power)
- end
- function BuiltInSpell.iceShards(caster, x, y, direction, elevation, skill, spl)
- local map = party.go.map
- x,y = Spell.getBurstTargetTile(x, y, direction)
- local spell = spawn(party.go.map, "ice_shards", x, y, direction, elevation)
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.tiledamager:setAttackPower(power)
- local range = spl.duration + skill * (spl.durationScaling or 0)
- spell.iceshards:setRange(range)
- spell.tiledamager:setCastByChampion(caster.ordinal)
- party:endCondition("invisibility")
- -- cast on invalid space (e.g. empty air)?
- if not spell.tiledamager:isEnabled() then
- soundSystem:playSound2D("spell_fizzle")
- end
- end
- function BuiltInSpell.frostbolt(caster, x, y, direction, elevation, skill, spl)
- local name = "frostbolt_"..math.clamp(skill, 1, 5)
- local spell = spawn(party.go.map, name, x, y, direction, elevation)
- spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(caster.ordinal)
- party:endCondition("invisibility")
- end
- function BuiltInSpell.frostShield(caster, x, y, direction, elevation, skill, spl)
- local duration = spl.duration + skill * (spl.durationScaling or 0)
- local power = spl.power + skill * (spl.powerScaling or 0)
- Spell.elementalShield("frost_shield", duration, power)
- end
- function BuiltInSpell.shock(caster, x, y, direction, elevation, skill, spl)
- local map = party.go.map
- x,y = Spell.getBurstTargetTile(x, y, direction)
- local spell = spawn(party.go.map, "shockburst", x, y, direction, elevation)
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.tiledamager:setAttackPower(power)
- spell.tiledamager:setCastByChampion(caster.ordinal)
- if skill == 1 then spell.tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects) end
- party:endCondition("invisibility")
- end
- function BuiltInSpell.invisibility(caster, x, y, direction, elevation, skill, spl)
- local duration = spl.duration + skill * (spl.durationScaling or 0)
- for i=1,4 do
- party.champions[i]:setConditionValue("invisibility", duration)
- end
- soundSystem:playSound2D("generic_spell")
- end
- function BuiltInSpell.lightningBolt(caster, x, y, direction, elevation, skill, spl)
- local spell
- if skill > 1 then
- spell = spawn(party.go.map, "lightning_bolt_greater", x, y, direction, elevation)
- else
- spell = spawn(party.go.map, "lightning_bolt", x, y, direction, elevation)
- end
- spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(caster.ordinal)
- party:endCondition("invisibility")
- end
- function BuiltInSpell.shockShield(caster, x, y, direction, elevation, skill, spl)
- local duration = spl.duration + skill * (spl.durationScaling or 0)
- local power = spl.power + skill * (spl.powerScaling or 0)
- Spell.elementalShield("shock_shield", duration, power)
- end
- function BuiltInSpell.poisonCloud(caster, x, y, direction, elevation, skill, spl)
- if caster:isEquipped("shaman_staff") then skill = skill + 1 end
- local map = party.go.map
- x,y = Spell.getBurstTargetTile(x, y, direction, true)
- local spell
- if skill >= 5 then
- spell = spawn(party.go.map, "poison_cloud_large", x, y, 0, elevation)
- elseif skill >= 3 then
- spell = spawn(party.go.map, "poison_cloud_medium", x, y, 0, elevation)
- else
- spell = spawn(party.go.map, "poison_cloud_small", x, y, 0, elevation)
- end
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.cloudspell:setAttackPower(power)
- spell.cloudspell:setCastByChampion(caster.ordinal)
- spell.cloudspell:setDamageInterval(math.max(0.8 - skill * 0.4 / 5, 0.2)) -- damage doubles with 5 skill levels
- spell.cloudspell:combineClouds()
- party:endCondition("invisibility")
- end
- function BuiltInSpell.poisonBolt(caster, x, y, direction, elevation, skill, spl)
- if caster:isEquipped("shaman_staff") then skill = skill + 1 end
- local spell
- if skill >= 3 then
- spell = spawn(party.go.map, "poison_bolt_greater", x, y, direction, elevation)
- else
- spell = spawn(party.go.map, "poison_bolt", x, y, direction, elevation)
- end
- spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(caster.ordinal)
- party:endCondition("invisibility")
- end
- function BuiltInSpell.poisonShield(caster, x, y, direction, elevation, skill, spl)
- local duration = spl.duration + skill * (spl.durationScaling or 0)
- local power = spl.power + skill * (spl.powerScaling or 0)
- Spell.elementalShield("poison_shield", duration, power)
- end
- function BuiltInSpell.dispel(caster, x, y, direction, elevation, skill, spl)
- local spell = spawn(party.go.map, "dispel_projectile", x, y, direction, elevation)
- spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
- local power = spl.power + skill * (spl.powerScaling or 0)
- spell.projectile:setAttackPower(power)
- spell.projectile:setIgnoreEntity(party.go)
- spell.projectile:setCastByChampion(caster.ordinal)
- end
- function BuiltInSpell.causeFear(caster, x, y, direction, elevation, skill, spl)
- local map = party.go.map
- x,y = Spell.getBurstTargetTile(x, y, direction, true)
- local power = spl.power + skill * (spl.powerScaling or 0)
- for _,monster in party.go.map:componentsAt(MonsterComponent, x, y) do
- if monster:isAlive() and not monster.fleeing then
- local brain = monster.go.brain
- if brain then
- if math.random(power, 100) > brain.morale then
- brain:startFleeing()
- else
- monster:showDamageText("Resists", Color.White)
- end
- end
- end
- end
- -- hit self?
- if x == party.go.x and y == party.go.y then
- for i=1,4 do
- local ch = party.champions[i]
- if ch and ch:isAlive() and not ch:hasCondition("paralyzed") and math.random() < 0.5 then
- ch:setCondition("paralyzed")
- end
- end
- end
- local fx = spawn(party.go.map, "particle_system", x, y, 0, elevation)
- fx.particle:setParticleSystem("fear_cloud")
- fx.particle:setOffset(vec(0, 1.5, 0))
- soundSystem:playSound2D("wand_fear")
- party:endCondition("invisibility")
- end
- function BuiltInSpell.heal(caster, x, y, direction, elevation, skill, spl)
- local power = spl.power + skill * (spl.powerScaling or 0)
- for i=1,4 do
- local champion = party:getChampion(i)
- if champion:isAlive() then
- champion:regainHealth(power)
- champion:playHealingIndicator()
- soundSystem:playSound2D("heal_party")
- end
- end
- end
- function Spell.elementalShield(condition, duration, power)
- -- cancel existing shields
- for i=1,4 do
- party.champions[i]:removeCondition("fire_shield")
- party.champions[i]:removeCondition("frost_shield")
- party.champions[i]:removeCondition("poison_shield")
- party.champions[i]:removeCondition("shock_shield")
- end
- for i=1,4 do
- party.champions[i]:setConditionValue(condition, duration, power)
- end
- soundSystem:playSound2D("generic_spell")
- end
- function Dungeon:redefineTraits()
- -------------------------------------------------------------------------------------------------------
- -- Redefining vanilla classes --
- -------------------------------------------------------------------------------------------------------
- defineCharClass{
- name = "alchemist",
- uiName = "Alchemist",
- traits = { "herb_multiplication", "firearm_expert" },
- optionalTraits = 2,
- }
- defineCharClass{
- name = "barbarian",
- uiName = "Barbarian",
- optionalTraits = 2,
- }
- defineCharClass{
- name = "battle_mage",
- uiName = "Battle Mage",
- traits = { "hand_caster", "armor_expert", "staff_defence" },
- optionalTraits = 2,
- }
- defineCharClass{
- name = "farmer",
- uiName = "Farmer",
- skillPoints = 0,
- optionalTraits = 2,
- }
- defineCharClass{
- name = "fighter",
- uiName = "Fighter",
- traits = { "melee_specialist" },
- optionalTraits = 2,
- }
- defineCharClass{
- name = "knight",
- uiName = "Knight",
- traits = { "armor_expert", "shield_expert" },
- optionalTraits = 2,
- }
- defineCharClass{
- name = "rogue",
- uiName = "Rogue",
- traits = { "rogue_dual_wield" },
- optionalTraits = 2,
- }
- defineCharClass{
- name = "wizard",
- uiName = "Wizard",
- traits = { "hand_caster" },
- optionalTraits = 2,
- }
- -------------------------------------------------------------------------------------------------------
- -- Redefining vanilla traits --
- -------------------------------------------------------------------------------------------------------
- -- Class traits
- defineTrait{
- name = "fighter",
- uiName = "Fighter",
- icon = 96,
- description = "As a fighter you are a master of close combat. You are trained to use a wide variety of weapons.",
- gameEffect = [[
- - Health 60 (+7 per level), Energy 30 (+3 per level)
- - Special attacks with melee weapons take half the time to build up and cost 25% less energy.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("max_health", 60 + (level-1) * 7)
- champion:addStatModifier("max_energy", 30 + (level-1) * 3)
- end
- end,
- }
- defineTrait{
- name = "barbarian",
- uiName = "Barbarian",
- icon = 94,
- description = "As a barbarian you do not care about finesse in combat. Instead you rely on raw power and speed.",
- gameEffect = [[
- - Health 80 (+10 per level), Energy 30 (+3 per level)
- - Strength +1 per level.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("strength", level)
- champion:addStatModifier("max_health", 80 + (level-1) * 10)
- champion:addStatModifier("max_energy", 30 + (level-1) * 3)
- end
- end,
- }
- defineTrait{
- name = "knight",
- uiName = "Knight",
- icon = 97,
- description = "As a knight you believe that good preparation is the key to triumph in combat. You are specialized in wielding armor and using the shield.",
- gameEffect = [[
- - Health 60 (+7 per level), Energy 30 (+3 per level)
- - Protection +1 per level.
- - Weight of equipped armor is reduced by 50%.
- - Evasion bonus of equipped shields is increased by 50%.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("max_health", 60 + (level-1) * 7)
- champion:addStatModifier("max_energy", 30 + (level-1) * 3)
- champion:addStatModifier("protection", level)
- end
- end,
- }
- defineTrait{
- name = "rogue",
- uiName = "Rogue",
- icon = 95,
- description = "As a rogue you are a stealthy warrior who prefers to use ranged weapons or light melee weapons.",
- gameEffect = [[
- - Health 45 (+5 per level), Energy 40 (+5 per level)
- - When dual wielding you suffer only 25% penalty to weapon damage (normally 40%).
- - +1% chance per level to score a critical hit with missile or throwing weapons.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("max_health", 45 + (level-1) * 5)
- champion:addStatModifier("max_energy", 40 + (level-1) * 5)
- end
- end,
- onComputeCritChance = function(champion, weapon, attack, attackType, level, monster, accuracy)
- if level > 0 and (attackType == "throw" or attackType == "missile") then return champion:getLevel() end
- end,
- }
- defineTrait{
- name = "wizard",
- uiName = "Wizard",
- icon = 33,
- description = "As a wizard you use enchanted staves and orbs to command great mystical powers that can be used to cause harm or to protect.",
- gameEffect = [[
- - Health 35 (+3 per level), Energy 50 (+7 per level)
- - Willpower +2
- - You can cast spells with bare hands.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("willpower", 2)
- champion:addStatModifier("max_health", 35 + (level-1) * 3)
- champion:addStatModifier("max_energy", 50 + (level-1) * 7)
- end
- end,
- }
- defineTrait{
- name = "battle_mage",
- uiName = "Battle Mage",
- icon = 98,
- description = "As a battle mage you are comfortable with fighting in the front row as well as blasting with spells from the back row.",
- gameEffect = [[
- - Health 50 (+5 per level), Energy 50 (+5 per level)
- - Weight of equipped armor is reduced by 50%.
- - You can cast spells with bare hands.
- - You gain Protection +10 and Resist All +10 when equipped with a magical staff or an orb.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("max_health", 50 + (level-1) * 5)
- champion:addStatModifier("max_energy", 50 + (level-1) * 5)
- end
- end,
- }
- defineTrait{
- name = "alchemist",
- uiName = "Alchemist",
- icon = 92,
- description = "As an alchemist you brew potions and defend yourself in combat by wielding firearms.",
- gameEffect = [[
- - Health 50 (+6 per level), Energy 50 (+4 per level)
- - Herbs in your inventory multiply. The growth rate is determined by the number of steps taken.
- - Firearms have 50% less chance to malfunction.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("max_health", 50 + (level-1) * 6)
- champion:addStatModifier("max_energy", 50 + (level-1) * 4)
- end
- end,
- }
- defineTrait{
- name = "farmer",
- uiName = "Farmer",
- icon = 93,
- description = "As a farmer you do not command great powers and do not know how to wield a sword. Instead you are familiar with digging ditches for irrigation and the growth cycles of pitroot plants, basically everything a successful adventurer would never need.",
- gameEffect = [[
- - Health 30 (+5 per level), Energy 30 (+5 per level)
- - You receive no skillpoints at first level.
- - Instead of slaying monsters you gain experience points by eating food.]],
- onRecomputeStats = function(champion, level)
- if level > 0 then
- level = champion:getLevel()
- champion:addStatModifier("max_health", 30 + (level-1) * 5)
- champion:addStatModifier("max_energy", 30 + (level-1) * 5)
- end
- end,
- }
- -- Race traits
- defineTrait{
- name = "human",
- uiName = "Human",
- icon = 37,
- description = "As a human you belong to the most populous sentient race in the known world. You are very adaptable and can excel in all professions.",
- gameEffect = "- You gain experience points 10% faster.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("exp_rate", 10)
- end
- end,
- }
- defineTrait{
- name = "minotaur",
- uiName = "Minotaur",
- icon = 38,
- description = "As a minotaur you are bulky, simple and quick to anger. Your incredible stubborness is tolerated by others only because of your incredible prowess in combat.",
- gameEffect = "- Strength +5, Dexterity -4, Vitality +4, Willpower -3.\n- Your food consumption rate is 25% higher than normal.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("strength", 5)
- champion:addStatModifier("dexterity", -4)
- champion:addStatModifier("vitality", 4)
- champion:addStatModifier("willpower", -3)
- champion:addStatModifier("food_rate", 25)
- end
- end,
- }
- defineTrait{
- name = "lizardman",
- uiName = "Lizardman",
- icon = 40,
- description = "As a lizardman you are a social outcast and are mistrusted by other races because of your capricious and deceitful nature. What you lack in social skills you greatly make up for in stealth and dexterity.",
- gameEffect = "- Dexterity +2, Willpower -2.\n- Resist All +25%.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("dexterity", 2)
- champion:addStatModifier("willpower", -2)
- champion:addStatModifier("resist_fire", 25)
- champion:addStatModifier("resist_cold", 25)
- champion:addStatModifier("resist_poison", 25)
- champion:addStatModifier("resist_shock", 25)
- end
- end,
- }
- defineTrait{
- name = "insectoid",
- uiName = "Insectoid",
- icon = 39,
- description = "As an insectoid, your thoughts are completely alien to other races. Your knowledge of the arcane is unrivaled. Insectoids come in many shapes and sizes but most often their bodies are covered with a thick shell.",
- gameEffect = "- Strength +1, Dexterity -2, Vitality -1, Willpower +2.\n- Your chance of getting body parts injured is reduced by 50%.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("strength", 1)
- champion:addStatModifier("dexterity", -2)
- champion:addStatModifier("vitality", -1)
- champion:addStatModifier("willpower", 2)
- end
- end,
- onReceiveCondition = function(champion, cond, level)
- if level > 0 and string.match(cond, "_wound$") and math.random() <= 0.5 then
- return false
- end
- end,
- }
- defineTrait{
- name = "ratling",
- uiName = "Ratling",
- icon = 41,
- description = "As a ratling you may seem weak and disease ridden on the surface, but you are actually one of the most adaptable and hardy creatures in the world. You are a hoarder by nature and greatly enjoy fiddling with mechanical contraptions.",
- gameEffect = "- Strength -4, Dexterity +2.\n- Evasion +2.\n- Max Load +15kg.\n- You are immune to diseases.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("strength", -4)
- champion:addStatModifier("dexterity", 2)
- champion:addStatModifier("evasion", 2)
- champion:addStatModifier("max_load", level * 15)
- end
- end,
- onReceiveCondition = function(champion, cond, level)
- if level > 0 and cond == "diseased" then
- return false
- end
- end,
- }
- -- Race-specific traits
- defineTrait{
- name = "skilled",
- uiName = "Skilled",
- icon = 42,
- charGen = true,
- requiredRace = "human",
- description = "You gain 1 extra skill point at first level. It may only be used to raise a skill from level 0 to level 1.",
- -- hardcoded
- }
- defineTrait{
- name = "fast_learner",
- uiName = "Fast Learner",
- icon = 43,
- charGen = true,
- requiredRace = "human",
- description = "You gain experience points 10% faster. This bonus is in addition to the experience bonus granted by your race.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("exp_rate", 10)
- end
- end,
- }
- defineTrait{
- name = "head_hunter",
- uiName = "Head Hunter",
- icon = 45,
- charGen = true,
- requiredRace = "minotaur",
- description = "Strength +1 for each skull carried.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- -- count skulls
- local skulls = 0
- for i=1,ItemSlot.MaxSlots do
- local item = champion:getItem(i)
- if item then
- if item:hasTrait("skull") then
- skulls = skulls + 1
- else
- local container = item.go.containeritem
- if container then
- local capacity = container:getCapacity()
- for j=1,capacity do
- local item2 = container:getItem(j)
- if item2 and item2:hasTrait("skull") then
- skulls = skulls + 1
- end
- end
- end
- end
- end
- end
- champion:addStatModifier("strength", skulls)
- end
- end,
- }
- defineTrait{
- name = "rage",
- uiName = "Rage",
- icon = 46,
- charGen = true,
- requiredRace = "minotaur",
- description = "Your strength is temporarily increased by 10 when you have less than 20% health remaining.",
- }
- defineTrait{
- name = "fast_metabolism",
- uiName = "Fast Metabolism",
- icon = 54,
- charGen = true,
- requiredRace = "lizardman",
- description = "Your Health and Energy regenerates 30% faster while your food consumption rate increases by 20%. Additionally, healing potions are twice as effective for you.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("food_rate", 20)
- champion:addStatModifier("health_regeneration_rate", 30)
- champion:addStatModifier("energy_regeneration_rate", 30)
- end
- end,
- }
- defineTrait{
- name = "endure_elements",
- uiName = "Endure Elements",
- icon = 55,
- charGen = true,
- requiredRace = "lizardman",
- description = "Years spent living in the wilderness have made you resistant to the forces of nature. Resist All +25%",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_fire", 25)
- champion:addStatModifier("resist_cold", 25)
- champion:addStatModifier("resist_shock", 25)
- champion:addStatModifier("resist_poison", 25)
- end
- end,
- }
- defineTrait{
- name = "poison_immunity",
- uiName = "Poison Immunity",
- icon = 56,
- charGen = true,
- requiredRace = "lizardman",
- description = "You are immune to poison.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_poison", 100)
- end
- end,
- }
- defineTrait{
- name = "chitin_armor",
- uiName = "Chitin Armor",
- icon = 51,
- charGen = true,
- requiredRace = "insectoid",
- description = "Your body is covered by a thick chitin shell. You gain Protection +10.",
- onRecomputeStats = function(champion, level)
- if level > 0 then champion:addStatModifier("protection", 10) end
- end,
- }
- defineTrait{
- name = "quick",
- uiName = "Quick",
- icon = 52,
- charGen = true,
- requiredRace = "insectoid",
- description = "The cooldown period for all actions you perform is decreased by 10%.",
- onComputeCooldown = function(champion, weapon, attack, attackType, level)
- if level > 0 then return 0.9 end
- end,
- }
- defineTrait{
- name = "mutation",
- uiName = "Mutation",
- icon = 48,
- charGen = true,
- requiredRace = "ratling",
- description = "One of your attribute scores, chosen randomly, increases by 1 when you gain a new level.",
- }
- -- Generic traits
- defineTrait{
- name = "athletic",
- uiName = "Muscular",
- icon = 12,
- charGen = true,
- description = "Your body is muscular. Strength +2",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("strength", 2)
- end
- end,
- }
- defineTrait{
- name = "agile",
- uiName = "Agile",
- icon = 81,
- charGen = true,
- description = "Your reflexes are exceptional. Dexterity +2",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("dexterity", 2)
- end
- end,
- }
- defineTrait{
- name = "healthy",
- uiName = "Healthy",
- icon = 77,
- charGen = true,
- description = "You are exceptionally healthy. Vitality +2",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("vitality", 2)
- end
- end,
- }
- defineTrait{
- name = "strong_mind",
- uiName = "Strong Mind",
- icon = 80,
- charGen = true,
- description = "Your mind is as sharp as a needle. Willpower +2",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("willpower", 2)
- end
- end,
- }
- defineTrait{
- name = "tough",
- uiName = "Tough",
- icon = 91,
- charGen = true,
- description = "You are resistant to physical punishment. Health +20",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("max_health", 20)
- end
- end,
- }
- defineTrait{
- name = "aura",
- uiName = "Aura",
- icon = 82,
- charGen = true,
- description = "You have a potent aura. Energy +20",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("max_energy", 20)
- end
- end,
- }
- defineTrait{
- name = "aggressive",
- uiName = "Aggressive",
- icon = 75,
- charGen = true,
- description = "You are full of rage. Damage +4",
- onComputeDamageModifier = function(champion, weapon, attack, attackType, level)
- if level > 0 then
- return 4
- end
- end,
- }
- defineTrait{
- name = "evasive",
- uiName = "Evasive",
- icon = 9,
- charGen = true,
- description = "You know how best to stay away from harm. Evasion +5",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("evasion", 5)
- end
- end,
- }
- defineTrait{
- name = "fire_resistant",
- uiName = "Daemon Ancestor",
- icon = 76,
- charGen = true,
- description = "Your Great Grandfather had fiery eyes. Resist Fire +25",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_fire", 25)
- end
- end,
- }
- defineTrait{
- name = "cold_resistant",
- uiName = "Cold-blooded",
- icon = 83,
- charGen = true,
- description = "You are naturally resistant to cold. Resist Cold +25",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_cold", 25)
- end
- end,
- }
- defineTrait{
- name = "poison_resistant",
- uiName = "Poison Resistant",
- icon = 79,
- charGen = true,
- description = "You are naturally resistant to poisons. Resist Poison +25",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_poison", 25)
- end
- end,
- }
- defineTrait{
- name = "natural_armor",
- uiName = "Natural Armor",
- icon = 78,
- charGen = true,
- description = "Your skin is very thick and armor-like. Protection +5",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("protection", 5)
- end
- end,
- }
- defineTrait{
- name = "endurance",
- uiName = "Endurance",
- icon = 11,
- charGen = true,
- description = "Max carrying capacity is increased by 25 kg and food consumption rate is decreased by 25%.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("max_load", 25)
- champion:addStatModifier("food_rate", -25)
- end
- end,
- }
- defineTrait{
- name = "weapon_specialization",
- uiName = "Martial Training",
- icon = 0,
- charGen = true,
- description = "You are trained to use a wide variety of melee weapons. Melee attacks gain Accuracy +7.",
- onComputeAccuracy = function(champion, weapon, attack, attackType, level)
- if level > 0 and attackType == "melee" then return 7 end
- end,
- }
- -- Skill traits
- defineTrait{
- name = "pack_mule",
- uiName = "Pack Mule",
- icon = 109,
- description = "Your max carrying capacity is increased by 15 kg.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("max_load", 15)
- end
- end,
- }
- defineTrait{
- name = "meditation",
- uiName = "Meditation",
- icon = 110,
- description = "Your Energy regeneration rate is increased by 25% while resting.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- if party.party:isResting() then
- champion:addStatModifier("energy_regeneration_rate", 25)
- end
- end
- end,
- }
- defineTrait{
- name = "two_handed_mastery",
- uiName = "Ogre's Grip",
- icon = 6,
- description = "You can wield two-handed weapons in one hand.",
- -- hardcoded trait
- }
- defineTrait{
- name = "light_armor_proficiency",
- uiName = "Light Armors",
- icon = 57,
- description = "You can wear Light Armor without penalties.",
- -- hardcoded trait
- }
- defineTrait{
- name = "heavy_armor_proficiency",
- uiName = "Heavy Armors",
- icon = 7,
- description = "You can wear Heavy Armor without penalties.",
- -- hardcoded trait
- }
- defineTrait{
- name = "armor_expert",
- uiName = "Armor Expert",
- icon = 7,
- description = "Weight of equipped armor is reduced by 50%.",
- onComputeItemWeight = function(champion, equipped, level)
- if level > 0 and equipped then
- return 1 - 0.5
- end
- end
- }
- defineTrait{
- name = "shield_expert",
- uiName = "Shield Expert",
- icon = 8,
- description = "Increases evasion bonus of equipped shields by 50%.",
- onComputeItemWeight = function(champion, equipped, level)
- if level > 0 and equipped then
- return 0.5
- end
- end
- }
- defineTrait{
- name = "staff_defence",
- uiName = "Staff Defence",
- icon = 34,
- description = "You gain Protection +10 and Resist All +10 when equipped with a staff or an orb.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- local item1 = champion:getItem(ItemSlot.Weapon)
- local item2 = champion:getItem(ItemSlot.OffHand)
- if (item1 and item1.go.runepanel) or (item2 and item2.go.runepanel) then
- champion:addStatModifier("protection", 10)
- champion:addStatModifier("resist_fire", 10)
- champion:addStatModifier("resist_cold", 10)
- champion:addStatModifier("resist_shock", 10)
- champion:addStatModifier("resist_poison", 10)
- end
- end
- end,
- }
- defineTrait{
- name = "backstab",
- uiName = "Backstab",
- icon = 103,
- description = "You do triple damage when you successfully backstab an enemy with a dagger.",
- onCheckBackstab = function(monster, champion, weapon, attack, dmg, dmgType, crit, level)
- if level > 0 and weapon and weapon:hasTrait("dagger") then
- return 3
- end
- end
- }
- defineTrait{
- name = "assassin",
- uiName = "Assassin",
- icon = 104,
- description = "You can backstab with any Light Weapon.",
- onCheckBackstab = function(monster, champion, weapon, attack, dmg, dmgType, crit, level)
- if level > 0 and weapon and weapon:hasTrait("light_weapon") then
- if weapon and weapon:hasTrait("dagger") then
- return 0 -- backstab is already 3 so we don't add both together
- else
- return 3 -- light weapon, not dagger
- end
- end
- end
- }
- defineTrait{
- name = "firearm_mastery",
- uiName = "Firearm Mastery",
- icon = 85,
- description = "Firearms never malfunction in your hands.",
- onComputeMalfunctionChance = function(champion, weapon, attack, attackType, level)
- if attackType == "firearm" then
- return 0
- end
- end,
- }
- defineTrait{
- name = "dual_wield",
- uiName = "Dual Wielding",
- icon = 19,
- description = "You can attack separately with Light Weapons in either hand. One of the weapons must be a dagger. Both weapons suffer a 40% penalty to the items' base damage when dual wielding.",
- onCheckDualWielding = function(champion, weapon1, weapon2, level)
- if level > 0 and weapon1 and weapon2 and weapon1:hasTrait("light_weapon") and weapon2:hasTrait("light_weapon") then
- if weapon1:hasTrait("dagger") or weapon2:hasTrait("dagger") then
- return true
- end
- end
- end
- }
- defineTrait{
- name = "improved_dual_wield",
- uiName = "Dual Wield Mastery",
- icon = 107,
- description = "You can dual wield any two Light Weapons. Both weapons still suffer a 40% penalty to the items' base damage when dual wielding.",
- onCheckDualWielding = function(champion, weapon1, weapon2, level)
- if level > 0 and weapon1 and weapon2 and weapon1:hasTrait("light_weapon") and weapon2:hasTrait("light_weapon") then
- return true
- end
- end
- }
- defineTrait{
- name = "piercing_arrows",
- uiName = "Piercing Arrows",
- icon = 23,
- description = "Your Missile Weapon attacks ignore 20 points of enemy's armor.",
- onComputePierce = function(monster, champion, weapon, attack, projectile, dmg, dmgType, attackType, crit, backstab, level)
- if level > 0 and (projectile.go.ammoitem:getAmmoType() == "arrow" or projectile.go.ammoitem:getAmmoType() == "quarrel") then
- return 20
- end
- end
- }
- defineTrait{
- name = "double_throw",
- uiName = "Double Throw",
- icon = 21,
- description = "You can throw weapons from both hands with one action.",
- -- hardcoded skill
- }
- defineTrait{
- name = "reach",
- uiName = "Reach",
- icon = 27,
- description = "You can perform melee attacks from the back row.",
- -- hardcoded skill
- }
- defineTrait{
- name = "uncanny_speed",
- uiName = "Uncanny Speed",
- icon = 108,
- description = "Cooldowns of all your actions are sped up by 10%.",
- onComputeCooldown = function(champion, weapon, attack, attackType, level)
- if level > 0 then return 0.9 end
- end,
- }
- defineTrait{
- name = "fire_mastery",
- uiName = "Fire Mastery",
- icon = 29,
- description = "You gain Resist Fire +50.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_fire", 50)
- end
- end,
- }
- defineTrait{
- name = "air_mastery",
- uiName = "Air Mastery",
- icon = 30,
- description = "You gain Resist Shock +50.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_shock", 50)
- end
- end,
- }
- defineTrait{
- name = "earth_mastery",
- uiName = "Earth Mastery",
- icon = 31,
- description = "You gain Resist Poison +50.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_poison", 50)
- end
- end,
- }
- defineTrait{
- name = "water_mastery",
- uiName = "Water Mastery",
- icon = 32,
- description = "You gain Resist Cold +50.",
- onRecomputeStats = function(champion, level)
- if level > 0 then
- champion:addStatModifier("resist_cold", 50)
- end
- end,
- }
- defineTrait{
- name = "rogue_dual_wield",
- uiName = "Rogue Dual Wield",
- icon = 95,
- description = "When dual wielding you suffer only 25% penalty to weapon damage (normally 40%).",
- onComputeDualWieldingModifier = function(champion, weapon, attack, attackType, level)
- if level > 0 then
- return 0.15
- end
- end,
- }
- defineTrait{
- name = "melee_specialist",
- uiName = "Melee Specialist",
- icon = 96,
- description = "Special attacks with melee weapons take half the time to build up and cost 25% less energy.",
- }
- defineTrait{
- name = "firearm_expert",
- uiName = "Firearms Expert",
- icon = 92,
- description = "Firearms have 50% less chance to malfunction.",
- onComputeMalfunctionChance = function(champion, weapon, attack, attackType, level)
- if attackType == "firearm" then
- return 0.5
- end
- end,
- }
- -- Tome traits
- defineTrait{
- name = "leadership",
- uiName = "Party Leader",
- icon = 13,
- description = "Other party members gain +1 bonus to all attributes as long as you are alive.",
- onRecomputeStats = function(champion, level)
- level = champion:getLevel()
- local leadership = 0
- for i=1,4 do
- local champ = party.party:getChampionByOrdinal(i)
- if champ:hasTrait("leadership") then
- leadership = 1
- break
- end
- end
- if leadership ~= 0 then
- champion:addStatModifier("strength", leadership)
- champion:addStatModifier("willpower", leadership)
- champion:addStatModifier("dexterity", leadership)
- champion:addStatModifier("vitality", leadership)
- end
- end,
- }
- defineTrait{
- name = "nightstalker",
- uiName = "Nightstalker",
- icon = 0,
- description = "",
- hidden = true,
- onRecomputeStats = function(champion, level)
- if level > 0 then
- local bonus = GameMode.getTimeOfDay() >= 1 and 5 or -5
- champion:addStatModifier("vitality", bonus)
- end
- end,
- }
- end
- -------------------------------------------------------------------------------------------------------
- -- Redefining vanilla skills --
- -------------------------------------------------------------------------------------------------------
- function Dungeon:redefineSkills()
- defineSkill{
- name = "alchemy",
- uiName = "Alchemy",
- priority = 10,
- icon = 20,
- description = "A higher skill level in Alchemy allows you to brew a wider range of potions. To craft potions you also need herbs and a Mortar and Pestle.",
- skillTraits = {
- [4] = "You brew stronger healing and energy potions.",
- [5] = "When you craft bombs you get three bombs instead of one."
- },
- traits = { [4] = "improved_alchemy", [5] = "bomb_expert" },
- }
- defineSkill{
- name = "athletics",
- uiName = "Athletics",
- priority = 20,
- icon = 12,
- description = "Increases your health by 20 for each skill point.",
- skillTraits = {
- [3] = "Your carrying capacity is increased by 15 kg.",
- },
- traits = { [3] = "pack_mule" },
- onRecomputeStats = function(champion, level)
- champion:addStatModifier("max_health", level*20)
- end,
- }
- defineSkill{
- name = "concentration",
- uiName = "Concentration",
- priority = 30,
- icon = 26,
- description = "Increases your energy by 20 for each skill point.",
- skillTraits = {
- [3] = "Your Energy regeneration rate is increased by 25% while resting.",
- },
- traits = { [3] = "meditation" },
- onRecomputeStats = function(champion, level)
- champion:addStatModifier("max_energy", level*20)
- end,
- }
- defineSkill{
- name = "light_weapons",
- uiName = "Light Weapons",
- priority = 40,
- icon = 106,
- description = "Increases damage of Light Weapons by 20% for each skill point.",
- skillTraits = {
- [3] = "You can dual wield* Light Weapons as long one of them is a dagger.",
- [5] = "You can dual wield* any Light Weapons.",
- [9] = "When dual wielding you suffer a 40% penalty to weapon damage.", -- a trait marked with [9] will display as a footnote
- },
- traits = { [3] = "dual_wield", [5] = "improved_dual_wield" },
- onComputeDamageMultiplier = function(champion, weapon, attack, attackType, level)
- if level > 0 and weapon and weapon:hasTrait("light_weapon") and attackType == "melee" then
- return 1 + level * 0.2
- end
- end,
- }
- defineSkill{
- name = "heavy_weapons",
- uiName = "Heavy Weapons",
- priority = 50,
- icon = 105,
- description = "Increases damage of Heavy Weapons by 20% for each skill point.",
- skillTraits = {
- [5] = "You can wield two-handed weapons in one hand.",
- },
- traits = { [5] = "two_handed_mastery" },
- onComputeDamageMultiplier = function(champion, weapon, attack, attackType, level)
- if level > 0 and weapon and weapon:hasTrait("heavy_weapon") and attackType == "melee" then
- return 1 + level * 0.2
- end
- end,
- }
- defineSkill{
- name = "missile_weapons",
- uiName = "Missile Weapons",
- priority = 60,
- icon = 17,
- description = "Increases damage of Missile Weapons by 20% for each skill point.",
- skillTraits = {
- [4] = "Your Missile Weapon attacks ignore 20 points of an enemy's armor.",
- },
- traits = { [4] = "piercing_arrows" },
- onComputeDamageMultiplier = function(champion, weapon, attack, attackType, level)
- if level > 0 and weapon and weapon:hasTrait("missile_weapon") and attackType == "missile" then
- return 1 + level * 0.2
- end
- end,
- }
- defineSkill{
- name = "throwing",
- uiName = "Throwing",
- priority = 70,
- icon = 16,
- description = "Increases damage of Throwing Weapons by 20% for each skill point.",
- skillTraits = {
- [5] = "You can throw weapons from both hands with one action.",
- },
- traits = { [5] = "double_throw" },
- onComputeDamageMultiplier = function(champion, weapon, attack, attackType, level)
- if level > 0 and weapon and weapon:hasTrait("throwing_weapon") and attackType == "throw" then
- return 1 + level * 0.2
- end
- end,
- }
- defineSkill{
- name = "firearms",
- uiName = "Firearms",
- priority = 80,
- icon = 90,
- description = "Increases range of firearm attacks by 1 square for each skill point.", -- idk why the original said it decreases malfunctioning chance, as it only the trait does that
- skillTraits = {
- [5] = "Firearms never malfunction in your hands.",
- },
- traits = { [5] = "firearm_mastery" },
- onComputeRange = function(champion, weapon, attack, attackType, level)
- if attackType == "firearm" then
- return level
- end
- end,
- }
- defineSkill{
- name = "accuracy",
- uiName = "Accuracy",
- priority = 90,
- icon = 86,
- description = "Increases your Accuracy by 10 for each skill point.",
- skillTraits = {
- [2] = "You can perform melee attacks from the back row.",
- },
- traits = { [2] = "reach" },
- onComputeAccuracy = function(champion, weapon, attack, attackType, level, monster)
- return level * 10
- end,
- }
- defineSkill{
- name = "critical",
- uiName = "Critical",
- priority = 100,
- icon = 10,
- description = "Improves your chance of scoring a critical hit with physical attacks by 3%.",
- skillTraits = {
- [3] = "You can backstab an enemy with a dagger and deal triple damage.",
- [5] = "You can backstab with any Light Weapon.",
- },
- traits = { [3] = "backstab", [5] = "assassin" },
- onComputeCritChance = function(champion, weapon, attack, attackType, level, monster, accuracy)
- return level * 3
- end,
- }
- defineSkill{
- name = "armors",
- uiName = "Armor",
- priority = 110,
- icon = 7,
- description = "Increases protection of armor pieces equipped by 5% for each skill point.",
- skillTraits = {
- [2] = "You are proficient with Light Armor and can wear it without penalties.",
- [4] = "You can wear Heavy Armor without penalties.",
- },
- traits = { [2] = "light_armor_proficiency", [4] = "heavy_armor_proficiency" },
- onComputeItemStats = function(equipmentitem, champion, slot, level)
- local item = equipmentitem.go.item
- if equipmentitem:getProtection() and slot ~= ItemSlot.Weapon and slot ~= ItemSlot.OffHand then
- champion:addStatModifier("protection", equipmentitem:getProtection() * level * 0.05)
- end
- end,
- }
- defineSkill{
- name = "dodge",
- uiName = "Dodge",
- priority = 120,
- icon = 9,
- description = "Increases evasion by 3 for each skill point.",
- skillTraits = {
- [3] = "The cooldown period for all of your actions is decreased by 10%.",
- },
- onRecomputeStats = function(champion, level)
- champion:addStatModifier("evasion", level * 3)
- end,
- traits = { [3] = "uncanny_speed" },
- }
- defineSkill{
- name = "fire_magic",
- uiName = "Fire Magic",
- priority = 130,
- icon = 29,
- description = "Increases damage of fire spells by 20% for each skill point.",
- skillTraits = {
- [5] = "You gain Resist Fire +50.",
- },
- traits = { [5] = "fire_mastery" },
- onComputeSpellDamage = function(champion, spell, name, cost, skill, level)
- if level > 0 and spell and skill == "fire_magic" then
- spell:setAttackPower(spell:getAttackPower() * (1 + 0.2 * level))
- return { true, spell }
- end
- end
- }
- defineSkill{
- name = "air_magic",
- uiName = "Air Magic",
- priority = 140,
- icon = 30,
- description = "Increases damage of air spells by 20% for each skill point.",
- skillTraits = {
- [5] = "You gain Resist Shock +50.",
- },
- traits = { [5] = "air_mastery" },
- onComputeSpellDamage = function(champion, spell, name, cost, skill, level)
- if level > 0 and spell and skill == "air_magic" then
- spell:setAttackPower(spell:getAttackPower() * (1 + 0.2 * level))
- return { true, spell }
- end
- end
- }
- defineSkill{
- name = "earth_magic",
- uiName = "Earth Magic",
- priority = 150,
- icon = 31,
- description = "Increases damage of earth spells by 20% for each skill point.",
- skillTraits = {
- [5] = "You gain Resist Poison +50.",
- },
- traits = { [5] = "earth_mastery" },
- onComputeSpellDamage = function(champion, spell, name, cost, skill, level)
- if level > 0 and spell and skill == "earth_magic" then
- spell:setAttackPower(spell:getAttackPower() * (1 + 0.2 * level))
- return { true, spell }
- end
- end
- }
- defineSkill{
- name = "water_magic",
- uiName = "Water Magic",
- priority = 160,
- icon = 32,
- description = "Increases damage of water spells by 20% for each skill point.",
- skillTraits = {
- [5] = "You gain Resist Cold +50.",
- },
- traits = { [5] = "water_mastery" },
- onComputeSpellDamage = function(champion, spell, name, cost, skill, level)
- if level > 0 and spell and skill == "water_magic" then
- spell:setAttackPower(spell:getAttackPower() * (1 + 0.2 * level))
- return { true, spell }
- end
- end
- }
- end
- -------------------------------------------------------------------------------------------------------
- -- Redefining vanilla spells --
- -------------------------------------------------------------------------------------------------------
- -- We redefine vanilla spells so we can have their power and skill scaling to not be hard-coded
- -- The 20% damage scaling from skills are done on the skills themselves
- function Dungeon:redefineSpells()
- -- utility spells
- defineSpell{
- name = "shield",
- uiName = "Shield",
- gesture = 456,
- manaCost = 35,
- duration = 30,
- durationScaling = 10, -- multiplies skill level
- power = 25, -- protection amount
- onCast = "shield",
- skill = "concentration",
- requirements = { "concentration", 1 },
- icon = 102,
- spellIcon = 19,
- description = "Creates a magical shield around you. The shield protects from physical damage by increasing your Protection by 25. Every point in concentration skill increases spell's duration by 10 seconds.",
- }
- defineSpell{
- name = "light",
- uiName = "Light",
- gesture = 25,
- manaCost = 35,
- duration = 600,
- onCast = "light",
- skill = "concentration",
- requirements = { "concentration", 2 },
- icon = 58,
- spellIcon = 18,
- description = "Conjures a dancing ball of light that illuminates your path.",
- }
- defineSpell{
- name = "darkness",
- uiName = "Darkness",
- gesture = 85,
- manaCost = 25,
- duration = 300,
- onCast = "darkness",
- skill = "concentration",
- requirements = { "concentration", 2 },
- icon = 59,
- spellIcon = 11,
- description = "Negates all magical and non-magical light sources carried by your party.",
- }
- defineSpell{
- name = "darkbolt",
- uiName = "Darkbolt",
- gesture = 854,
- manaCost = 25,
- power = 9,
- powerScaling = 2,
- onCast = "darkbolt",
- skill = "concentration",
- requirements = { "concentration", 3 },
- icon = 100,
- spellIcon = 20,
- description = "Shoots a ray that engulfs the target in magical darkness.",
- }
- defineSpell{
- name = "force_field",
- uiName = "Force Field",
- gesture = 123698741,
- manaCost = 35,
- duration = 0,
- durationScaling = 5,
- onCast = "forceField",
- skill = "concentration",
- requirements = { "concentration", 2 },
- icon = 101,
- spellIcon = 5,
- description = "Creates a magical barrier that blocks all movement. Every point in Concentration increases spell's duration by 2 seconds.",
- }
- -- fire magic
- defineSpell{
- name = "fireburst",
- uiName = "Fireburst",
- gesture = 1,
- manaCost = 25,
- duration = 0.4, -- used for burn chance
- power = 22,
- onCast = "fireburst",
- skill = "fire_magic",
- requirements = { "fire_magic", 1 },
- icon = 60,
- spellIcon = 1,
- description = "Conjures a blast of fire that deals fire damage to all foes directly in front of you.",
- }
- defineSpell{
- name = "fireball",
- uiName = "Fireball",
- gesture = 1236,
- manaCost = 43,
- power = 30,
- onCast = "fireball",
- skill = "fire_magic",
- requirements = { "fire_magic", 3, "air_magic", 1 },
- icon = 61,
- spellIcon = 7,
- description = "A flaming ball of fire shoots from your fingertips causing devastating damage to your foes.",
- }
- defineSpell{
- name = "meteor_storm",
- uiName = "Meteor Storm",
- gesture = 14563,
- manaCost = 80,
- power = 15,
- onCast = "meteorStorm",
- skill = "fire_magic",
- requirements = { "fire_magic", 5, "air_magic", 3 },
- icon = 99,
- spellIcon = 8,
- description = "Unleashes a devastating storm of meteors on your foes.",
- }
- defineSpell{
- name = "fire_shield",
- uiName = "Fire Shield",
- gesture = 52145,
- manaCost = 50,
- power = 35, -- resist amount
- duration = 50,
- onCast = "fireShield",
- skill = "fire_magic",
- requirements = { "fire_magic", 3, "concentration", 3 },
- icon = 66,
- spellIcon = 12,
- description = "Creates a magical shield reducing fire damage against the party.",
- }
- -- ice magic
- defineSpell{
- name = "ice_shards",
- uiName = "Ice Shards",
- gesture = 789,
- manaCost = 30,
- power = 18,
- duration = 2, -- used for base range
- durationScaling = 1, -- used for range scaling
- onCast = "iceShards",
- skill = "water_magic",
- requirements = { "water_magic", 1, "earth_magic", 1 },
- icon = 70,
- spellIcon = 3,
- description = "Deathly sharp spikes of ice thrust from the ground hitting your opponents in a line. Every point in Water Magic increases the spell's range by one.",
- }
- defineSpell{
- name = "dispel",
- uiName = "Dispel",
- gesture = 123654789,
- manaCost = 42,
- power = 25,
- onCast = "dispel",
- skill = "water_magic",
- requirements = { "water_magic", 1, "concentration", 1 },
- icon = 72,
- spellIcon = 13,
- description = "Shoots a ray that damages elementals.",
- }
- defineSpell{
- name = "frostbolt",
- uiName = "Frostbolt",
- gesture = 369,
- manaCost = 37,
- power = 15,
- onCast = "frostbolt",
- skill = "water_magic",
- requirements = { "water_magic", 3, "air_magic", 1 },
- icon = 71,
- spellIcon = 4,
- description = "You hurl a bolt of icy death dealing ranged damage and freezing your opponents. Every point in Water Magic increases the probability and duration of the freezing effect.",
- }
- defineSpell{
- name = "frost_shield",
- uiName = "Frost Shield",
- gesture = 58965,
- manaCost = 50,
- power = 35, -- resist amount
- duration = 50,
- onCast = "frostShield",
- skill = "water_magic",
- requirements = { "water_magic", 3, "concentration", 3 },
- icon = 68,
- spellIcon = 14,
- description = "Creates a magical shield reducing cold damage against the party.",
- }
- -- air magic
- defineSpell{
- name = "shock",
- uiName = "Shock",
- gesture = 3,
- manaCost = 25,
- power = 22,
- onCast = "shock",
- skill = "air_magic",
- requirements = { "air_magic", 1 },
- icon = 64,
- spellIcon = 6,
- description = "Conjures a blast of electricity that deals shock damage to all foes directly in front of you.",
- }
- defineSpell{
- name = "invisibility",
- uiName = "Invisibility",
- gesture = 3658,
- manaCost = 45,
- duration = 40,
- onCast = "invisibility",
- skill = "air_magic",
- requirements = { "air_magic", 3, "concentration", 2 },
- icon = 74,
- spellIcon = 15,
- description = "Turns yourself and your friends invisible.",
- }
- defineSpell{
- name = "lightning_bolt",
- uiName = "Lightning Bolt",
- gesture = 4523,
- manaCost = 50,
- power = 30,
- onCast = "lightningBolt",
- skill = "air_magic",
- requirements = { "air_magic", 4 },
- icon = 65,
- spellIcon = 9,
- description = "You channel the power of storms through your hands.",
- }
- defineSpell{
- name = "shock_shield",
- uiName = "Shock Shield",
- gesture = 52365,
- manaCost = 50,
- power = 35, -- resist amount
- duration = 50,
- onCast = "shockShield",
- skill = "air_magic",
- requirements = { "air_magic", 3, "concentration", 3 },
- icon = 69,
- spellIcon = 16,
- description = "Creates a magical shield reducing shock damage against the party.",
- }
- -- earth magic
- defineSpell{
- name = "poison_cloud",
- uiName = "Poison Cloud",
- gesture = 7,
- manaCost = 27,
- power = 5,
- onCast = "poisonCloud",
- skill = "earth_magic",
- requirements = { "earth_magic", 1 },
- icon = 62,
- spellIcon = 2,
- description = "Summon a toxic cloud of poison that deals damage over time.",
- }
- defineSpell{
- name = "poison_bolt",
- uiName = "Poison Bolt",
- gesture = 78963,
- manaCost = 32,
- power = 15,
- onCast = "poisonBolt",
- skill = "earth_magic",
- requirements = { "earth_magic", 2 },
- icon = 63,
- spellIcon = 10,
- description = "A sizzling venomous bolt of poison shoots from your hands.",
- }
- defineSpell{
- name = "poison_shield",
- uiName = "Poison Shield",
- gesture = 58745,
- manaCost = 50,
- power = 35, -- resist amount
- duration = 50,
- onCast = "poisonShield",
- skill = "earth_magic",
- requirements = { "earth_magic", 3, "concentration", 3 },
- icon = 67,
- spellIcon = 17,
- description = "Creates a magical shield reducing poison damage against the party.",
- }
- -- hidden spells
- defineSpell{
- name = "open_serpent_door",
- uiName = "Open Door",
- gesture = 0,
- manaCost = 25,
- onCast = "openDoor",
- skill = "concentration",
- requirements = { "concentration", 1 },
- icon = 59,
- spellIcon = 0,
- description = "",
- hidden = true,
- }
- defineSpell{
- name = "disintegrate",
- uiName = "Disintegrate",
- gesture = 0,
- manaCost = 0,
- onCast = "disintegrate",
- skill = "concentration",
- requirements = { "concentration", 5 },
- icon = 59,
- spellIcon = 0,
- description = "",
- hidden = true,
- }
- defineSpell{
- name = "balance",
- uiName = "Balance",
- gesture = 5,
- manaCost = 10,
- onCast = "balance",
- skill = "concentration",
- requirements = {},
- icon = 59,
- spellIcon = 0,
- description = "",
- hidden = true,
- }
- defineSpell{
- name = "cause_fear",
- uiName = "Cause Fear",
- gesture = 0,
- manaCost = 25,
- power = 0, -- increases chance to fear, up to 100
- onCast = "causeFear",
- skill = "concentration",
- requirements = { "concentration", 3 },
- icon = 33,
- spellIcon = 0,
- description = "",
- hidden = true,
- }
- defineSpell{
- name = "heal",
- uiName = "Heal",
- gesture = 0,
- manaCost = 50,
- power = 100, -- heal amount
- onCast = "heal",
- skill = "concentration",
- requirements = { "concentration", 3 },
- icon = 33,
- spellIcon = 0,
- description = "",
- hidden = true,
- }
- end
Advertisement
Add Comment
Please, Sign In to add comment