7Soul

Extended Hooks (Grimrock 2 UMod)

Jan 17th, 2021 (edited)
1,188
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. --[=[
  2. === UModManager Info Section ===
  3.  
  4. id = "ExtendedHooks"
  5.  
  6. name = "ExtendedHooks"
  7.  
  8. description = [[Adds several new scripting hooks into existing functions.
  9.  
  10. Made by 7Soul (henriquelazarini@gmail.com)]]
  11.  
  12. version = "0.1.0"
  13.  
  14. priority = 5
  15.  
  16. 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" }
  17.  
  18. 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" }
  19.  
  20. === End of Mod Info ===
  21. ]=]
  22.  
  23. -- Stats = {
  24. --  "health",
  25. --  "max_health",
  26. --  "energy",
  27. --  "max_energy",
  28. --  "strength",
  29. --  "dexterity",
  30. --  "vitality",
  31. --  "willpower",
  32. --  "protection",
  33. --  "evasion",
  34. --  "resist_fire",
  35. --  "resist_cold",
  36. --  "resist_poison",
  37. --  "resist_shock",
  38. --  "max_load",
  39. --  "exp_rate",
  40. --  "food_rate",
  41. --  "health_regeneration_rate",
  42. --  "energy_regeneration_rate",
  43. --  "cooldown_rate",
  44. --  "critical_multiplier",
  45. --  "dual_wielding",
  46. -- }
  47.  
  48. -- StatNames = {
  49. --  "Health",
  50. --  "Max Health",
  51. --  "Energy",
  52. --  "Max Energy",
  53. --  "Strength",
  54. --  "Dexterity",
  55. --  "Vitality",
  56. --  "Willpower",
  57. --  "Protection",
  58. --  "Evasion",
  59. --  "Resist Fire",
  60. --  "Resist Cold",
  61. --  "Resist Poison",
  62. --  "Resist Shock",
  63. --  "Max Load",
  64. --  "Experience Rate",
  65. --  "Food Consumption Rate",
  66. --  "Health Regeneration Rate",
  67. --  "Energy Regeneration Rate",
  68. --  "Cooldown Rate",
  69. --  "Critical Multiplier",
  70. --  "Dual Wielding",
  71. -- }
  72.  
  73. config.developer = true
  74.  
  75. defineProxyClass{
  76.     class = "PartyComponent",
  77.     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.",
  78.     baseClass = "Component",
  79.     methods = {
  80.         { "heal" },
  81.         { "rest" },
  82.         { "wakeUp", "boolean" },
  83.         { "move", "number" },
  84.         { "turn", "number" },
  85.         { "isResting" },
  86.         { "isMoving" },
  87.         { "isFalling" },
  88.         { "isClimbing" },
  89.         { "isIdle" },
  90.         { "isCarrying", "string" },
  91.         { "setMovementSpeed", "number" },
  92.         { "getMovementSpeed" },
  93.         { "swapChampions", {"number", "number"} },
  94.         { "getChampion", "number" },
  95.         { "getChampionByOrdinal", "number" },
  96.         { "playScreenEffect", "string" },
  97.         { "shakeCamera", {"number", "number"} },
  98.         { "knockback", "number" },
  99.         { "grapple", "number" },
  100.     },
  101.     hooks = {
  102.         "onCastSpell(self, champion, spell)",
  103.         "onDamage(self, champion, damage, damageType)",
  104.         "onDie(self, champion)",
  105.         "onAttack(self, champion, action, slot)",
  106.         "onLevelUp(self, champion)",
  107.         "onReceiveCondition(self, champion, condition)",
  108.         "onDrawGui(self, context)",    
  109.         "onDrawInventory(self, context, champion)",
  110.         "onDrawStats(self, context, champion)",
  111.         "onDrawSkills(self, context, champion)",
  112.         "onDrawTraits(self, context, champion)",
  113.         "onPickUpItem(self, item)",
  114.         "onProjectileHit(self, champion, item, damage, damageType)",
  115.         "onRest(self)",
  116.         "onWakeUp(self)",
  117.         "onTurn(self, direction)",
  118.         "onMove(self, direction)",
  119.         "onCalculateDamageWithAttack(self, champion, weapon, attack, power)", -- new
  120.         "onBrewPotion(self, potion, champion)", -- new
  121.         "onMultiplyHerbs(self, herbRates, champion)", -- new
  122.         "onLoadDefaultParty(self)",
  123.     },
  124. }
  125.  
  126. defineProxyClass{
  127.     class = "Champion",
  128.     description = "Champion's attributes, skills, traits, conditions and other statistics can be accessed through this class.",
  129.     methods = {
  130.         { "setEnabled", "boolean" },
  131.         { "setName", "string" },
  132.         { "setRace", "string" },
  133.         { "setSex", "string" },
  134.         { "setClass", "string" },
  135.         { "setHealth", "number" },
  136.         { "setEnergy", "number" },
  137.         { "getEnabled" },
  138.         { "getName" },
  139.         { "getRace" },
  140.         { "getSex" },
  141.         { "getClass" },
  142.         { "getDualClass" },
  143.         { "getLevel" },
  144.         { "getExp" },
  145.         { "getOrdinal" },
  146.         { "setPortrait", "string" },
  147.         { "isAlive" },
  148.         { "gainExp", "number" },
  149.         { "resetExp" },
  150.         { "levelUp" },
  151.         { "setSkillPoints", "number" },
  152.         { "getSkillPoints" },
  153.         { "addSkillPoints", "number" },
  154.         { "getSkillLevel", {"string", "number"} },
  155.         { "getSkillLevel", "string" },
  156.         { "trainSkill", {"string", "number"} },
  157.         { "addTrait", "string" },
  158.         { "removeTrait", "string" },
  159.         { "hasTrait", "string" },
  160.         { "setFood", "number" },
  161.         { "getFood" },
  162.         { "consumeFood", "number" },
  163.         { "modifyFood", "number" },
  164.         { "setCondition", "string" },
  165.         { "removeCondition", "string" },
  166.         { "hasCondition", "string" },
  167.         { "setConditionValue", {"string", "number"} },
  168.         { "getConditionValue", "string" },
  169.         { "isBodyPartWounded", "number" },
  170.         { "damage", {"number", "string"} },
  171.         { "playDamageSound" },
  172.         { "playSound", "string" },
  173.         { "showAttackResult", {"any", "string"} },
  174.         { "setBaseStat", {"string", "number"} },
  175.         { "modifyBaseStat", {"string", "number"} },
  176.         { "upgradeBaseStat", {"string", "number"} },
  177.         { "addStatModifier", {"string", "number"} },
  178.         { "getBaseStat", "string" },
  179.         { "getCurrentStat", "string" },
  180.         { "regainHealth", "number" },
  181.         { "regainEnergy", "number" },
  182.         { "getHealth" },
  183.         { "getMaxHealth" },
  184.         { "getEnergy" },
  185.         { "getMaxEnergy" },
  186.         { "getProtection" },
  187.         { "getEvasion" },
  188.         { "getResistance", "string" },
  189.         { "getLoad" },
  190.         { "getMaxLoad" },
  191.         { "getArmorSetPiecesEquipped", "string" },
  192.         { "isArmorSetEquipped", "string" },
  193.         { "isDualWielding" },
  194.         { "isReadyToAttack", "number" },
  195.         { "insertItem", {"number", "ItemComponent"} },
  196.         { "removeItem", "ItemComponent" },
  197.         { "removeItemFromSlot", "number" },
  198.         { "swapItems", {"number", "number"} },
  199.         { "swapWeaponSets" },
  200.         { "getItem", "number" },
  201.         { "getOtherHandItem", "number" },
  202.         { "playHealingIndicator" },
  203.         { "castSpell", "number" },
  204.         { "attack", {"number", "boolean"} },
  205.         { "getConditionStacks" }, -- new
  206.         { "getData" }, -- new
  207.         { "setData" }, -- new
  208.         { "addData" }, -- new
  209.     },
  210. }
  211.  
  212. defineProxyClass{
  213.     class = "MonsterComponent",
  214.     baseClass = "Component",
  215.     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.",
  216.     methods = {
  217.         { "setAIState", "string" },
  218.         { "setHealth", "number" },
  219.         { "setMaxHealth", "number" },
  220.         { "setLevel", "number" },
  221.         { "setCondition", "string" },
  222.         { "setCapsuleHeight", "number" },
  223.         { "setCapsuleRadius", "number" },
  224.         { "setCollisionRadius", "number" },
  225.         { "setDeathEffect", "string" },
  226.         { "setDieSound" },  -- accepts string or nil
  227.         { "setEvasion", "number" },
  228.         { "setExp", "number" },
  229.         { "setFlying", "boolean" },
  230.         { "setFootstepSound", "string" },
  231.         { "setHitEffect", "string" },
  232.         { "setHitSound", "string" },
  233.         { "setIdleAnimation", "string" },
  234.         { "setImmunities", "table" },
  235.         { "setLootDrop", "table" },
  236.         { "setMeshName", "string" },
  237.         { "setHeadRotation", "vec" },
  238.         { "setProtection", "number" },
  239.         { "setResistances", "table" },
  240.         { "setShape", "string" },
  241.         { "setSwarm", "boolean" },
  242.         { "setMonsterFlag", {"string", "boolean"} },
  243.         { "getHealth" },
  244.         { "getMaxHealth" },
  245.         { "getLevel" },
  246.         { "getCapsuleHeight" },
  247.         { "getCapsuleRadius" },
  248.         { "getCollisionRadius" },
  249.         { "getDeathEffect" },
  250.         { "getDieSound" },
  251.         { "getEvasion" },
  252.         { "getExp" },
  253.         { "getFlying" },
  254.         { "getFootstepSound" },
  255.         { "getGroupSize" },
  256.         { "getHitEffect" },
  257.         { "getHitSound" },
  258.         { "getIdleAnimation" },
  259.         { "getImmunities" },
  260.         { "getLootDrop" },
  261.         { "getMeshName" },
  262.         { "getHeadRotation" },
  263.         { "getProtection" },
  264.         { "getResistance" },
  265.         { "getShape" },
  266.         { "getSwarm" },
  267.         { "getCurrentAction" },
  268.         { "getMonsterFlag", "string" },
  269.         { "addItem", "ItemComponent" },
  270.         { "removeItem", "ItemComponent" },
  271.         { "dropAllItems" },
  272.         { "performAction", "string" },  -- TODO: optional parameters!
  273.         { "moveForward" },
  274.         { "moveBackward" },
  275.         { "turnLeft" },
  276.         { "turnRight" },
  277.         { "strafeLeft" },
  278.         { "strafeRight" },
  279.         { "attack" },
  280.         { "shootProjectile", {"string", "number", "number"} },
  281.         { "throwItem", {"string", "number", "number"} },
  282.         { "showDamageText", {"string"} },
  283.         { "die" },  -- optional arg: gainExp
  284.         { "isInBackRow" },
  285.         { "isIdle" },
  286.         { "isAlive" },
  287.         { "isChangingAltitude" },
  288.         { "isFalling" },
  289.         { "isGroupLeader" },
  290.         { "isImmuneTo", "string" },
  291.         { "isInvulnerable" },
  292.         { "isMoving" },
  293.         { "isPerformingAction", "string" },
  294.         { "isReadyToAct" },
  295.         { "knockback", "number" },
  296.         { "setTraits", "table" },
  297.         { "addTrait" , "string" },
  298.         { "removeTrait" , "string" },
  299.         { "hasTrait" , "string" },
  300.         -- contents() is defined at the end of this file
  301.     },
  302.     hooks = {
  303.         "onProjectileHit(self, item, champion, weapon, attack, damage, damageType, heading, crit)",
  304.         "onPerformAction(self, name)",
  305.         "onDamage(self, damage, damageType)",
  306.         "onSpellDamage(self, damage, damageType, c, spell, heading)", -- new
  307.         "onDie(self)",
  308.     },
  309. }
  310.  
  311. local oldDefineSkill = defineSkill
  312. function defineSkill(desc)
  313.     local f = "defineSkill"
  314.     checkNamedArgOpt("skillTraits", desc, f, "table") -- used to display traits in a more organized way
  315.     oldDefineSkill(desc)
  316. end
  317.  
  318. local oldDefineTrait = defineTrait
  319. function defineTrait(desc)
  320.     local f = "defineTrait"
  321.     checkNamedArgOpt("classEffect", desc, f, "table") -- used to display class hp/en growth separately
  322.     checkNamedArgOpt("classAbility", desc, f, "table")
  323.     oldDefineTrait(desc)
  324. end
  325.  
  326. local oldDefineCharClass = defineCharClass
  327. function defineCharClass(desc)
  328.     desc.order = desc.order or 99
  329.     local f = "defineCharClass"
  330.     checkNamedArgOpt("order", desc, f, "number") -- used to put classes in a specific order
  331.     oldDefineCharClass(desc)
  332. end
  333.  
  334.  
  335. local oldDefineCondition = defineCondition
  336. function defineCondition(desc)
  337.     local f = "defineCondition"
  338.     checkNamedArgOpt("onRecomputeFinalStats", desc, f, "function")
  339.     checkNamedArgOpt("maxStacks", desc, f, "number") --
  340.     oldDefineCondition(desc)
  341. end
  342.  
  343. defineProxyClass{
  344.     class = "Condition",
  345.     description = "Custom condition.",
  346.     methods = {
  347.         { "setDuration", "number" },
  348.         { "getDuration" },
  349.         -- { "getName" },
  350.         { "getStacks" }, -- new
  351.     },
  352. }
  353.  
  354. local oldDefineSpell = defineSpell
  355. function defineSpell(desc)
  356.     desc = table.copy(desc)
  357.     local f = "defineSpell"
  358.     checkNamedArgOpt("duration", desc, f, "number")
  359.     checkNamedArgOpt("durationScaling", desc, f, "number")
  360.     checkNamedArgOpt("power", desc, f, "number")
  361.     checkNamedArgOpt("powerScaling", desc, f, "number")
  362.    
  363.     oldDefineSpell(desc)
  364. end
  365.  
  366. local oldDungeonLoadInitFile = Dungeon.loadInitFile
  367. function Dungeon:loadInitFile()
  368.     oldDungeonLoadInitFile(self)
  369.  
  370.     AddToolTips()
  371.     AddStats()
  372.     AddStatNames()
  373. end
  374.  
  375. local oldNewGameMenuStartGame = NewGameMenu.startGame
  376. function NewGameMenu:startGame()
  377.     if not modSystem:getCurrentMod() then
  378.         oldDungeon = dungeon
  379.         dungeon:redefineTraits()
  380.         dungeon:redefineSkills()
  381.         dungeon:redefineSpells()
  382.         dungeon = oldDungeon
  383.     end
  384.  
  385.     oldNewGameMenuStartGame(self)
  386. end
  387.  
  388. function extendProxyClass(class, prop)
  389.     class.__class.synthesizeProperty(prop)
  390. end
  391.  
  392. function AddToolTips()
  393.     local toolTips = ToolTip.toolTips
  394.     toolTips["Critical Multiplier"] = "Multiplies damage dealt with criticals by the amount displayed."
  395.     table.insert(ToolTip.toolTips, toolTips)
  396. end
  397.  
  398. function AddStats()
  399.     table.insert(Stats, "critical_multiplier")
  400.     table.insert(Stats, "dual_wielding")
  401. end
  402.  
  403. function AddStatNames()
  404.     table.insert(ToolTip.toolTips, "Critical Damage")
  405.     table.insert(ToolTip.toolTips, "Dual Wielding")
  406. end
  407.  
  408. function GameMode:loadDefaultParty()
  409.     local defaultParty = {
  410.         {
  411.             name = "Shadow",
  412.             class = "fighter",
  413.             race = "human",
  414.             sex = "female",
  415.             portrait = "assets/textures/portraits/human_female_01.tga",
  416.             strength = 5,
  417.             dexterity = 3,
  418.             vitality = 2,
  419.             willpower = 0,
  420.             skills = { "light_weapons", 1, "armors", 1 },
  421.             traits = { "tough", "agile" },
  422.         },
  423.         {
  424.             name = "Mulrag",
  425.             class = "barbarian",
  426.             race = "minotaur",
  427.             sex = "male",
  428.             portrait = "assets/textures/portraits/minotaur_male_01.tga",
  429.             strength = 5,
  430.             dexterity = 1,
  431.             vitality = 4,
  432.             willpower = 0,
  433.             skills = { "athletics", 1, "dodge", 1 },
  434.             traits = { "aggressive", "head_hunter" },
  435.         },
  436.         {
  437.             name = "Fang",
  438.             class = "alchemist",
  439.             race = "ratling",
  440.             sex = "male",
  441.             portrait = "assets/textures/portraits/ratling_male_01.tga",
  442.             strength = 2,
  443.             dexterity = 4,
  444.             vitality = 2,
  445.             willpower = 2,
  446.             skills = { "alchemy", 1, "firearms", 1 },
  447.             traits = { "mutation", "tough" },
  448.         },
  449.         {
  450.             name = "Astaroth",
  451.             class = "wizard",
  452.             race = "human",
  453.             sex = "male",
  454.             portrait = "assets/textures/portraits/human_male_02.tga",
  455.             strength = 0,
  456.             dexterity = 2,
  457.             vitality = 2,
  458.             willpower = 6,
  459.             skills = { "concentration", 1, "fire_magic", 1 },
  460.             traits = { "aura", "strong_mind" },
  461.         },
  462.     }
  463.    
  464.     if party:isHookRegistered("onLoadDefaultParty") then
  465.         newDefaultParty = party:callHook("onLoadDefaultParty", objectToProxy(self), defaultParty)
  466.         if newDefaultParty then
  467.             assert(newDefaultParty[4], "default party must be a table with 4 entries")
  468.             defaultParty = newDefaultParty
  469.         end
  470.     end
  471.  
  472.     for i=1,4 do
  473.         local champion = Champion.create()
  474.         local def = defaultParty[i]
  475.         champion.ordinal = i
  476.         champion:setName(def.name)
  477.         champion:setClass(def.class)
  478.         champion:setRace(def.race)
  479.         champion:setSex(def.sex)
  480.         champion:setPortrait(def.portrait)
  481.         champion:setBaseStat("strength", def.strength + 10)
  482.         champion:setBaseStat("dexterity", def.dexterity + 10)
  483.         champion:setBaseStat("vitality", def.vitality + 10)
  484.         champion:setBaseStat("willpower", def.willpower + 10)
  485.         party:setChampion(i, champion)
  486.         champion:recomputeStats()
  487.         champion:setBaseStat("health", champion:getMaxHealth())
  488.         champion:setBaseStat("energy", champion:getMaxEnergy())
  489.  
  490.         for j=1,#def.skills,2 do
  491.             champion:setSkillLevel(def.skills[j], 0) -- avoid gaining +1 skill every time we preview the game
  492.             champion:trainSkill(def.skills[j], def.skills[j+1])
  493.         end
  494.  
  495.         for j=1,#def.traits do
  496.             champion:addTrait(def.traits[j])
  497.         end
  498.     end
  499. end
  500.  
  501.  
  502. -------------------------------------------------------------------------------------------------------
  503. -- Tooltip Functions                                                                                 --
  504. -------------------------------------------------------------------------------------------------------
  505.  
  506. -- Attack
  507.  
  508. function ToolTip.drawAttack(attack, tx, ty, width, height, powerAttack)
  509.     local actualWidth = width
  510.     local font = ToolTip.normalFont
  511.     local h = font:getLineHeight() --22
  512.  
  513.     -- charges
  514.     if attack.charges and attack.maxCharges then
  515.         local w = gui:drawText(string.format("Charges: ", attack.charges), tx, ty, font)
  516.         actualWidth = math.max(actualWidth, w)
  517.         local x,y = tx + w,ty - 13
  518.         gui:drawRect(x, y, attack.maxCharges * 10 + 2, 12 + 4, {40,40,40,255})
  519.         for i=1,attack.charges do
  520.             gui:fillRect(x + 2, y + 2, 8, 12, {200, 50, 20, 255}) --{99,114,105,255})
  521.             x = x + 8 + 2
  522.         end
  523.         actualWidth = math.max(actualWidth, attack.maxCharges * 10 + 65)
  524.         ty = ty + h
  525.     end
  526.  
  527.     -- energy cost
  528.     if attack.energyCost and attack.energyCost ~= 0 then
  529.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Energy Cost: %d", attack.energyCost), tx, ty, font))
  530.         ty = ty + h
  531.     end
  532.  
  533.     if not powerAttack then
  534.         if attack.attackPower and attack.attackPower ~= 0 then
  535.             local mod = { attack.minDamageMod, attack.maxDamageMod }
  536.             local min,max = getDamageRange(attack.attackPower, mod)
  537.            
  538.             local text
  539.             if attack.damageType and attack.damageType ~= "physical" then
  540.                 local damageType = attack.damageType
  541.                 if damageType == "dispel" then
  542.                     damageType = "Ethereal"
  543.                 else
  544.                     damageType = string.capitalize(damageType)
  545.                 end
  546.                 text = string.format("Damage: %s %d - %d", damageType, min, max)
  547.             else
  548.                 text = string.format("Damage: %d - %d", min, max)
  549.             end
  550.  
  551.             -- bonus
  552.             local baseStat = attack:getBaseDamageStat()
  553.             local baseMulti = attack:getBaseDamageMultiplier()
  554.             if baseStat then
  555.                 if baseMulti then
  556.                     text = string.format("%s + %s%% of user's %s", text, baseMulti*100, getStatName(baseStat))
  557.                 else
  558.                     text = string.format("%s + %s", text, getStatName(baseStat))
  559.                 end
  560.             end
  561.  
  562.             actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
  563.             ty = ty + h
  564.         end
  565.        
  566.         if attack.accuracy then
  567.             local text = "Accuracy:"
  568.             local accuracy = attack.accuracy or 0
  569.  
  570.             if accuracy ~= 0 then
  571.                 text = string.format("%s %+d", text, accuracy)
  572.             end
  573.                    
  574.             if accuracy ~= 0 then
  575.                 actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
  576.                 ty = ty + h
  577.             end
  578.         end
  579.  
  580.         if attack.critChance and attack.critChance ~= 0 then
  581.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Critical Chance: %+d%%", attack.critChance), tx, ty, font))
  582.             ty = ty + h
  583.         end
  584.  
  585.         if attack.critMultiplier and attack.critMultiplier ~= 0 then
  586.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Critical Damage: %+d%%", attack.critMultiplier * 100), tx, ty, font))
  587.             ty = ty + h
  588.         end
  589.  
  590.         if attack.range and attack.range ~= 0 then
  591.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Range: %d", attack.range), tx, ty, font))
  592.             ty = ty + h
  593.         end
  594.        
  595.         if attack.jamChance and attack.jamChance ~= 0 then
  596.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Chance to malfunction: %d%%", attack.jamChance), tx, ty, font))
  597.             ty = ty + h
  598.         end
  599.  
  600.         if attack.cooldown and attack.cooldown ~= 0 then
  601.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Cooldown: %.1f seconds", attack.cooldown), tx, ty, font))
  602.             ty = ty + h
  603.         end
  604.            
  605.         if attack.pierce and attack.pierce ~= 0 then
  606.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Ignores %d points of enemy's armor", attack.pierce), tx, ty, font))
  607.             ty = ty + h
  608.         end
  609.        
  610.         if attack.velocity and attack.velocity ~= 0 then
  611.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Projectiles are %d%% faster", (attack.velocity - 1) * 100), tx, ty, font))
  612.             ty = ty + h
  613.         end
  614.  
  615.         if attack and attack.reachWeapon then
  616.             actualWidth = math.max(actualWidth, gui:drawText("Reach Weapon", tx, ty, font))
  617.             ty = ty + h
  618.         end
  619.     end
  620.  
  621.     if attack.requirements then
  622.         local champion = charSheet:getActiveChampion()
  623.         local text = attack:getRequirementsText()
  624.         local color = iff(attack:checkRequirements(champion), Color.White, Color.Red)
  625.         actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font, color))
  626.         ty = ty + h
  627.     end
  628.  
  629.     if attack.gameEffect then
  630.         actualWidth = math.max(actualWidth, gui:drawText(attack.gameEffect, tx, ty, font))
  631.         ty = ty + h
  632.     end
  633.  
  634.     return ty,actualWidth
  635. end
  636.  
  637. -- EquipmentItem
  638. -- Shows some of the new stats, such as Min/Max Damage bonus and Crit Multiplier
  639. function ToolTip.drawEquipmentItem(item, tx, ty, width, height)
  640.     local actualWidth = width
  641.     local font = ToolTip.normalFont
  642.     local h = font:getLineHeight()
  643.    
  644.     if item.protection and item.protection ~= 0 then
  645.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Protection %+d", item.protection), tx, ty, font))
  646.         ty = ty + h
  647.     end
  648.    
  649.     if item.evasion then
  650.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Evasion %+d", item.evasion), tx, ty, font))
  651.         ty = ty + h
  652.     end
  653.    
  654.     if item.accuracy then
  655.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Accuracy %+d", item.accuracy), tx, ty, font))
  656.         ty = ty + h
  657.     end
  658.    
  659.     if item.critMultiplier then
  660.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Critical Damage %+d%%", item.critMultiplier * 100), tx, ty, font))
  661.         ty = ty + h
  662.     end
  663.    
  664.     if item.minDamageMod and not item.maxDamageMod then
  665.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Min Damage %+d", item.minDamageMod), tx, ty, font))
  666.         ty = ty + h
  667.     end
  668.    
  669.     if item.maxDamageMod and not item.minDamageMod then
  670.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Max Damage %+d", item.maxDamageMod), tx, ty, font))
  671.         ty = ty + h
  672.     end
  673.    
  674.     if item.maxDamageMod and item.minDamageMod then
  675.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Adds %d - %d damage to all attacks", item.minDamageMod, item.maxDamageMod), tx, ty, font))
  676.         ty = ty + h
  677.     end
  678.  
  679.     -- stat modifiers
  680.     for i=1,#Stats do
  681.         local stat = Stats[i]
  682.         local modifier = item[stat]
  683.         if modifier and stat ~= "protection" and stat ~= "evasion" then
  684.             if modifier > 0 then
  685.                 actualWidth = math.max(actualWidth, gui:drawText(string.format("%s +%d", StatNames[i], modifier), tx, ty, font))
  686.                 ty = ty + h    
  687.             else
  688.                 actualWidth = math.max(actualWidth, gui:drawText(string.format("%s %d", StatNames[i], modifier), tx, ty, font))
  689.                 ty = ty + h    
  690.             end
  691.         end
  692.     end
  693.  
  694.     -- resistances
  695.     if item.resistAll then
  696.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Resist All %+d", item.resistAll), tx, ty, font))
  697.         ty = ty + h
  698.     end
  699.  
  700.     for i=1,#Elements do
  701.         local resist = elementToResistance(Elements[i])
  702.         if item[resist] then
  703.             actualWidth = math.max(actualWidth, gui:drawText(string.format("Resist %s %+d", string.capitalize(Elements[i]), item[resist]), tx, ty, font))
  704.             ty = ty + h
  705.         end
  706.     end
  707.  
  708.     -- skill modifiers
  709.     if item.skillModifiers then
  710.         for name,value in pairs(item.skillModifiers) do
  711.             local skill = Skill.getSkill(name)
  712.             if skill then
  713.                 name = skill.uiName
  714.             else
  715.                 name = "???"
  716.             end
  717.             actualWidth = math.max(actualWidth, gui:drawText(string.format("%s %+d", name, value), tx, ty, font))
  718.             ty = ty + h            
  719.         end
  720.     end
  721.  
  722.     if item.go.item:hasTrait("light_armor") then
  723.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Evasion -5 without Light Armor proficiency"), tx, ty, font))
  724.         ty = ty + h
  725.     elseif item.go.item:hasTrait("heavy_armor") then
  726.         actualWidth = math.max(actualWidth, gui:drawText(string.format("Evasion -10 without Heavy Armor proficiency"), tx, ty, font))
  727.         ty = ty + h
  728.     end
  729.  
  730.     if item.cooldownRate and item.cooldownRate ~= 0 then
  731.         local text
  732.         if item.cooldownRate > 0 then
  733.             text = string.format("%d%% faster cooldown", item.cooldownRate)
  734.         else
  735.             text = string.format("%d%% slower cooldown", -item.cooldownRate)
  736.         end
  737.         actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
  738.         ty = ty + h    
  739.     end
  740.    
  741.     if item.healthRegenerationRate and item.healthRegenerationRate ~= 0 then
  742.         local text = string.format("Health Regeneration Rate %+d%%", item.healthRegenerationRate)
  743.         actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
  744.         ty = ty + h    
  745.     end
  746.  
  747.     if item.energyRegenerationRate and item.energyRegenerationRate ~= 0 then
  748.         local text = string.format("Energy Regeneration Rate %+d%%", item.energyRegenerationRate)
  749.         actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
  750.         ty = ty + h    
  751.     end
  752.  
  753.     if item.expRate and item.expRate ~= 0 then
  754.         local text = string.format( "Wearer gains experience points %d%% faster", item.expRate * 100)
  755.         actualWidth = math.max(actualWidth, gui:drawText(text, tx, ty, font))
  756.         ty = ty + h    
  757.     end
  758.  
  759.     return ty,actualWidth
  760. end
  761.  
  762. -- Conditions
  763. -- Draws number of stacks when appliable
  764. function ToolTip.drawCondition(cond, x, y, width, height)
  765.     local actualWidth = 0
  766.     local actualHeight = 0
  767.    
  768.     if ToolTip.style == "rounded_rect" then
  769.         -- add little extra to width to make it look nicer
  770.         gui:drawToolTipRect(x, y, width + 4, height)
  771.     end
  772.    
  773.     local tx,ty = x + 16, y + 16
  774.    
  775.     -- draw caption
  776.     if cond.iconIndex then
  777.         cond:drawIcon(tx, ty)
  778.         tx = tx + 36
  779.     end
  780.     local font = ToolTip.titleFont
  781.     ty = ty + font:getMaxBearingY()
  782.     local stacks = cond:getStacks()
  783.     local stacksText = stacks and ("(" .. stacks .. " stacks)") or ""
  784.     gui:drawText(cond.uiName .. stacksText, tx, ty + 3, font, Color.White)
  785.     ty = ty + 26
  786.    
  787.     local font = ToolTip.normalFont
  788.    
  789.     -- description
  790.     local desc = cond:getDescription()
  791.     if desc then
  792.         local tw,th = gui:drawTextParagraph(desc, tx, ty, 400, font)
  793.         actualWidth = math.max(actualWidth, tw / gui.guiScale)
  794.         ty = ty + th
  795.         ty = ty + 4
  796.     end
  797.    
  798.     -- debug info
  799.     if config.developer then
  800.         gui:drawText(string.format("value=%.1f timer=%.1f", cond.value, cond.timer), tx, ty, FontType.Default, Color.White)
  801.     end
  802.    
  803.     actualWidth = actualWidth + 32
  804.     if cond.iconIndex then
  805.         actualWidth = actualWidth + 36
  806.     end
  807.     actualHeight = ty - y
  808.    
  809.     return actualWidth, actualHeight
  810. end
  811.  
  812. -- Updated for aesthetic reasons
  813. -- When a trait has a description and a game effect, the desc shows in a blueish hue
  814. -- Skills have the option to show the traits separately from the gameEffect text
  815. function ToolTip.drawSkill(skill, x, y, width, height)
  816.     local points = ToolTip.hints["skill_level"] or 0
  817.     local compact = ToolTip.hints["compact"]
  818.  
  819.     local actualWidth = 0
  820.     local actualHeight = 0
  821.     local paragraphWidth = 450
  822.    
  823.     if ToolTip.style == "rounded_rect" then
  824.         gui:drawToolTipRect(x, y, width, height)
  825.     end
  826.    
  827.     local tx,ty = x + 16, y + 16
  828.    
  829.     gui:drawSkillIcon(skill, tx, ty, 75)
  830.     tx = tx + 90
  831.    
  832.     -- draw caption
  833.     local font = ToolTip.titleFont
  834.     local maxBearing = font:getMaxBearingY()
  835.     ty = ty + maxBearing
  836.     actualWidth = math.max(actualWidth, gui:drawText(skill.uiName, tx, ty, font, Color.White))
  837.     ty = ty + 24 + 5
  838.        
  839.     local font = ToolTip.normalFont
  840.     if compact then font = FontType.PalatinoTiny end
  841.    
  842.     -- description
  843.     local desc = skill:getDescriptionText()
  844.     local descColor = iff(not skill.gameEffect, Color.White, {162,183,206,255})
  845.     local tw,th = gui:drawTextParagraph(desc, tx, ty, paragraphWidth, font, descColor)
  846.     actualWidth = math.max(actualWidth, tw / gui.guiScale)
  847.     ty = ty + th / gui.guiScale
  848.  
  849.     local champion = charSheet:getActiveChampion()
  850.  
  851.     -- game effect
  852.     if skill.gameEffect then
  853.         ty = ty + 8
  854.         local tw,th = gui:drawTextParagraph(skill.gameEffect, tx, ty, paragraphWidth, font)
  855.         actualWidth = math.max(actualWidth, tw / gui.guiScale)
  856.         ty = ty + th / gui.guiScale
  857.     end
  858.  
  859.     -- skill traits
  860.     if skill.skillTraits then
  861.         ty = math.max(ty + 8, y + 16 + 75 + maxBearing )
  862.         tx = tx - 88
  863.         if skill.traits then
  864.             for traitLevel = 0, 5 do
  865.                 if skill.traits[traitLevel] then
  866.                     local trait = Skill.getTrait(skill.traits[traitLevel])
  867.                     if trait then
  868.                         local color = Color.White
  869.                         local tw,th = gui:drawTextParagraph(" Level " .. traitLevel .. ": " .. trait.uiName, tx, ty, paragraphWidth, font, {153,244,124,255})
  870.                         ty = ty + th / gui.guiScale
  871.  
  872.                         tw,th = gui:drawTextParagraph(skill.skillTraits[traitLevel], tx+12, ty, paragraphWidth+75, font, color)
  873.                         actualWidth = math.max(actualWidth, paragraphWidth)
  874.                         ty = ty + th / gui.guiScale
  875.                     end
  876.                 end
  877.             end
  878.  
  879.             if skill.skillTraits[9] then
  880.                 ty = ty + 4
  881.                 local tw,th = gui:drawTextParagraph("*" .. skill.skillTraits[9], tx, ty, paragraphWidth, FontType.ScrollScaled, {162,183,206,255})
  882.                 actualWidth = math.max(actualWidth, tw / gui.guiScale)
  883.                 ty = ty + th / gui.guiScale
  884.             end
  885.         end
  886.         tx = tx + 88
  887.     end
  888.  
  889.     -- levels
  890.     if skill.levels then
  891.         for i=1,#skill.levels do
  892.             local color = iff(champion:getSkillLevel(skill.name) >= i, Color.White, {110,110,110,255})
  893.             local tw,th = gui:drawTextParagraph(skill.levels[i], tx, ty, paragraphWidth, font, color)
  894.             actualWidth = math.max(actualWidth, tw / gui.guiScale)
  895.             ty = ty + th / gui.guiScale
  896.         end
  897.     end
  898.  
  899.     -- requirements
  900.     if skill.requirements then
  901.         local champion = charSheet:getActiveChampion()
  902.         local color
  903.         if champion and not skill:checkRequirements(champion) then color = Color.Red end
  904.        
  905.         local text = skill:getRequirementsText(champion)
  906.         local tw,th = gui:drawTextParagraph(text, tx, ty, 400, font, color)
  907.         actualWidth = math.max(actualWidth, tw / gui.guiScale)
  908.         ty = ty + th / gui.guiScale
  909.     end
  910.  
  911.     -- draw spell gesture
  912.     local spell = Spell.getSpell(skill.name)
  913.     if spell then
  914.         local width = actualWidth+90
  915.         local w = 48
  916.         local h = 40
  917.         do
  918.             local tx = tx + (width - 3*w)/2 - 90
  919.             local ty = ty - 25
  920.            
  921.             gui:drawGuiItem(GuiItem.SpellPanelNoButtons, tx+10, ty+20)
  922.            
  923.             local maxLen = math.min(math.floor(ToolTip.time*5), ToolTip.runePanel:getGestureLength(spell.gesture))
  924.             ToolTip.runePanel:drawGesture(spell.gesture, tx + w/2, ty + h/2, w, h, maxLen)
  925.         end
  926.        
  927.         ty = ty + 3 * h + 10
  928.     end
  929.  
  930.     ty = ty + 4
  931.  
  932.     actualWidth = actualWidth + 32 + 70 + 20
  933.     actualHeight = math.max(ty - y + 2, 105)
  934.    
  935.     return actualWidth, actualHeight
  936. end
  937.  
  938. local oldSkillInit = Skill.init
  939. function Skill:init(desc)
  940.     oldSkillInit(self, desc)
  941.     self.skillTraits = desc.skillTraits
  942.     self.classEffect = desc.classEffect
  943.     self.classAbility = desc.classAbility
  944.     self.onComputeCritMultiplier = desc.onComputeCritMultiplier
  945.     self.onComputeDamageModifier = desc.onComputeDamageModifier
  946.     self.onComputeDamageMultiplier = desc.onComputeDamageMultiplier
  947.     self.onComputeDualWieldingModifier = desc.onComputeDualWieldingModifier
  948.     self.onComputeChampionAttackDamage = desc.onComputeChampionAttackDamage
  949.     self.onComputeChampionSpellDamage = desc.onComputeChampionSpellDamage
  950.     self.onComputeMonsterDamage = desc.onComputeMonsterDamage
  951.     self.onCheckDualWielding = desc.onCheckDualWielding
  952.     self.onComputePierce = desc.onComputePierce
  953.     self.onCheckBackstab = desc.onCheckBackstab
  954.     self.onComputeItemWeight = desc.onComputeItemWeight
  955.     self.onComputeItemStats = desc.onComputeItemStats
  956.     self.onComputeToHit = desc.onComputeToHit
  957.     self.onRecomputeFinalStats = desc.onRecomputeFinalStats
  958.     self.onComputeSpellCost = desc.onComputeSpellCost
  959.     self.onComputeSpellCooldown = desc.onComputeSpellCooldown
  960.     self.onComputeSpellDamage = desc.onComputeSpellDamage
  961.     self.onComputeHerbMultiplicationRate = desc.onComputeHerbMultiplicationRate
  962.     self.onComputeBearFormDuration = desc.onComputeBearFormDuration
  963.     self.onComputeBombPower = desc.onComputeBombPower
  964.     self.onComputeDamageTaken = desc.onComputeDamageTaken
  965.     self.onComputeRange = desc.onComputeRange
  966. end
  967.  
  968. -------------------------------------------------------------------------------------------------------
  969. -- Party Functions                                                                                   --    
  970. -------------------------------------------------------------------------------------------------------
  971.  
  972. local oldPartyMoveEnter = PartyMove.enter
  973. function PartyMove:enter(direction, speed, forcedMovement)
  974.     oldPartyMoveEnter(self, direction, speed, forcedMovement)
  975.  
  976.     if not party:isUnderwater() then
  977.     -- update herbalism
  978.         for i=1,4 do
  979.             local champ = party.champions[i]
  980.             champ:updateHerbalismNew()
  981.         end
  982.     end
  983. end
  984.  
  985. -------------------------------------------------------------------------------------------------------
  986. -- Champion Functions                                                                                --    
  987. -------------------------------------------------------------------------------------------------------
  988.  
  989. local oldInit = Champion.init
  990. function Champion:init()
  991.     oldInit(self)
  992.     self:setBaseStat("critical_multiplier", 2.5)
  993.     self:setBaseStat("dual_wielding", 0.6)
  994.    
  995.     self.data = {} -- gonna be used to easily store new values into a champion
  996. end
  997.  
  998. function Champion:setData(name, value)
  999.     self.data[name] = value
  1000. end
  1001.  
  1002. function Champion:getData(name)
  1003.     return self.data[name]
  1004. end
  1005.  
  1006. function Champion:addData(name, value)
  1007.     self.data[name] = (self.data[name] or 0) + value
  1008. end
  1009.  
  1010. local oldChampionSaveState = Champion.saveState
  1011. function Champion:saveState(file)
  1012.     oldChampionSaveState(self,file)
  1013.     file:openChunk("CHAM")
  1014.     file:writeValue(self.data)
  1015.     file:closeChunk()
  1016. end
  1017.  
  1018. local oldChampionLoadState = Champion.loadState
  1019. function Champion:loadState(file, loadItems)
  1020.     oldChampionLoadState(self, file, loadItems)
  1021.     local chunkID = file:openChunk()
  1022.     assert(chunkID == "CHAM")
  1023.     self.data = file:readValue()
  1024.     file:closeChunk()
  1025. end
  1026.  
  1027. function Champion:setConditionValue(name, value, power, stacks)
  1028.     -- Now takes more optional parameters
  1029.     local curStacks = self:getConditionStacks(name)
  1030.     self:setCondition(name, value > 0)
  1031.  
  1032.     local cond = self.conditions[name]
  1033.     if cond then
  1034.         cond.value = value
  1035.         if power then cond.power = power end
  1036.         if stacks then cond.stacks = curStacks + 1 end
  1037.     end
  1038. end
  1039.  
  1040. function Champion:getAccuracyWithAttack(weapon, attack, target)
  1041.     -- Updated to having the target of the attack as an optional parameter
  1042.  
  1043.     -- check skill level requirement
  1044.     if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
  1045.         return nil
  1046.     end
  1047.  
  1048.     local accuracy = attack.accuracy or 0
  1049.  
  1050.     -- dexterity bonus
  1051.     accuracy = accuracy + self:getAccuracyFromDexterity()
  1052.  
  1053.     -- skill modifiers
  1054.     for name,skill in pairs(dungeon.skills) do
  1055.         if skill.onComputeAccuracy then
  1056.             local modifier = skill.onComputeAccuracy(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name), objectToProxy(target))
  1057.             accuracy = accuracy + (modifier or 0)
  1058.         end
  1059.     end
  1060.  
  1061.     -- trait modifiers
  1062.     for name,trait in pairs(dungeon.traits) do
  1063.         if trait.onComputeAccuracy then
  1064.             local modifier = trait.onComputeAccuracy(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0), objectToProxy(target))
  1065.             accuracy = accuracy + (modifier or 0)
  1066.         end
  1067.     end
  1068.  
  1069.     -- equipment modifiers (equipped items only)
  1070.     for i=1,ItemSlot.BackpackFirst-1 do
  1071.         local it = self:getItem(i)
  1072.         if it then
  1073.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1074.                 for i=1,it.go.components.length do
  1075.                     local comp = it.go.components[i]
  1076.                     if comp.onComputeAccuracy then
  1077.                         local modifier = comp:onComputeAccuracy(self, weapon, attack, attackType, target)
  1078.                         accuracy = accuracy + (modifier or 0)                  
  1079.                     end
  1080.                 end
  1081.             end
  1082.         end
  1083.     end
  1084.  
  1085.     -- conditions
  1086.     if self:hasCondition("blind") or self:hasCondition("head_wound") then accuracy = accuracy - 50 end
  1087.    
  1088.     return math.floor(accuracy)
  1089. end
  1090.  
  1091. function Champion:getToHitChanceWithAttack(weapon, attack, target, accuracy, damageType)
  1092.     if not accuracy then accuracy = champion:getAccuracyWithAttack(weapon, attack) end
  1093.     local tohit = 60 + accuracy - (target.evasion or 0)
  1094.     tohit = tohit + self.luck
  1095.     tohit = math.clamp(tohit, 5, 95)
  1096.  
  1097.     -- skill modifiers
  1098.     for name,skill in pairs(dungeon.skills) do
  1099.         if skill.onComputeToHit then
  1100.             local modifier = skill.onComputeToHit(objectToProxy(target), objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), damageType, tohit, self:getSkillLevel(name))
  1101.             tohit = modifier or tohit
  1102.         end
  1103.     end
  1104.  
  1105.     -- trait modifiers
  1106.     for name,trait in pairs(dungeon.traits) do
  1107.         if trait.onComputeToHit then
  1108.             local modifier = trait.onComputeToHit(objectToProxy(target), objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), damageType, tohit, iff(self:hasTrait(name), 1, 0))
  1109.             tohit = modifier or tohit
  1110.         end
  1111.     end
  1112.  
  1113.     return tohit
  1114. end
  1115.  
  1116. function Champion:getAccuracyFromDexterity()
  1117.     return (self:getCurrentStat("dexterity") - 10) * 2
  1118. end
  1119.  
  1120. function Champion:getCritChanceWithAttack(weapon, attack, target)
  1121.     -- Updated to having the target of the attack as an optional parameter
  1122.    
  1123.     -- check skill level requirement
  1124.     if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
  1125.         return nil
  1126.     end
  1127.  
  1128.     local critChance = 5 + (attack.critChance or 0)
  1129.  
  1130.     -- skill modifiers
  1131.     for name,skill in pairs(dungeon.skills) do
  1132.         if skill.onComputeCritChance then
  1133.             local modifier = skill.onComputeCritChance(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name), objectToProxy(target), self:getAccuracyWithAttack(weapon, attack))
  1134.             critChance = critChance + (modifier or 0)
  1135.         end
  1136.     end
  1137.    
  1138.     -- traits modifiers
  1139.     for name,trait in pairs(dungeon.traits) do
  1140.         if trait.onComputeCritChance then
  1141.             local modifier = trait.onComputeCritChance(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0), objectToProxy(target), self:getAccuracyWithAttack(weapon, attack))
  1142.             critChance = critChance + (modifier or 0)
  1143.         end
  1144.     end
  1145.  
  1146.     -- equipment modifiers (equipped items only)
  1147.     for i=1,ItemSlot.BackpackFirst-1 do
  1148.         local it = self:getItem(i)
  1149.         if it then
  1150.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1151.                 for i=1,it.go.components.length do
  1152.                     local comp = it.go.components[i]
  1153.                     if comp.onComputeCritChance then
  1154.                         local modifier = comp:onComputeCritChance(self, weapon, attack, target)
  1155.                         critChance = critChance + (modifier or 0)
  1156.                     end
  1157.                 end
  1158.             end
  1159.         end
  1160.     end
  1161.  
  1162.     critChance = math.clamp(critChance, 0, 100)
  1163.  
  1164.     return critChance
  1165. end
  1166.  
  1167. function Champion:getCritMultiplierWithAttack(weapon, attack, target)
  1168.     -- New function to deal with changing the crit damage multiplier
  1169.  
  1170.     -- check skill level requirement
  1171.     if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
  1172.         return nil
  1173.     end
  1174.  
  1175.     local critMulti = 2.5 + (attack.critMultiplier or 0)
  1176.  
  1177.     -- skill modifiers
  1178.     for name,skill in pairs(dungeon.skills) do
  1179.         if skill.onComputeCritMultiplier then
  1180.             local modifier = skill.onComputeCritMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name), target, self:getAccuracyWithAttack(weapon, attack))
  1181.             critMulti = critMulti + (modifier or 0)
  1182.         end
  1183.     end
  1184.    
  1185.     -- traits modifiers
  1186.     for name,trait in pairs(dungeon.traits) do
  1187.         if trait.onComputeCritMultiplier then
  1188.             local modifier = trait.onComputeCritMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0), target, self:getAccuracyWithAttack(weapon, attack))
  1189.             critMulti = critMulti + (modifier or 0)
  1190.         end
  1191.     end
  1192.  
  1193.     -- equipment modifiers (equipped items only)
  1194.     for i=1,ItemSlot.BackpackFirst-1 do
  1195.         local it = self:getItem(i)
  1196.         if it then
  1197.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1198.                 for i=1,it.go.components.length do
  1199.                     local comp = it.go.components[i]
  1200.                     if comp.onComputeCritMultiplier then
  1201.                         critMulti = critMulti + (comp:onComputeCritMultiplier(self, weapon, attack, target) or 0)
  1202.                     end
  1203.                 end
  1204.             end
  1205.         end
  1206.     end
  1207.  
  1208.     critMulti = math.clamp(critMulti, 0, 100)
  1209.  
  1210.     return critMulti
  1211. end
  1212.  
  1213. function Champion:getCritMultiplier(slot)
  1214.     assert(slot, "missing param 'slot'")
  1215.    
  1216.     local action = self:getPrimaryAction(slot)
  1217.     if action and action.getAttackType then
  1218.         local weapon = self:getItem(slot)
  1219.         if self:hasCondition("bear_form") then weapon = nil end
  1220.         return self:getCritMultiplierWithAttack(weapon, action)
  1221.     end
  1222. end
  1223.  
  1224. function Champion:getCritMultiplierText(slot)
  1225.     local crit = self:getCritMultiplier(slot)
  1226.     if crit then
  1227.         return crit.."x"
  1228.     else
  1229.         return "2.5x"
  1230.     end
  1231. end
  1232.  
  1233. function Champion:getTwoHanded(slot)
  1234.     local item = self:getItem(slot)
  1235.     return item and item:hasTrait("two_handed") and not self:hasTrait("two_handed_mastery")
  1236. end
  1237.  
  1238. function Champion:getDamageText(slot)
  1239.     -- when wielding a 2-handed weapon, the stats page should display same damage for the secondary hand
  1240.     local item = self:getItem(slot)
  1241.     if (slot == ItemSlot.Weapon or slot == ItemSlot.OffHand) and not item then
  1242.         local otherSlot = iff(slot == ItemSlot.Weapon, ItemSlot.OffHand, ItemSlot.Weapon)
  1243.         if otherItem and self:getTwoHanded(otherSlot) then
  1244.             slot = otherSlot
  1245.         end
  1246.     end
  1247.  
  1248.     local power,mod,var = self:getDamage(slot)
  1249.     if power then
  1250.         local min,max = getDamageRange(power, mod, var)
  1251.         return min.." - "..max
  1252.     else
  1253.         return "--"
  1254.     end
  1255. end
  1256.  
  1257. -- Returns attack power and damage modifier for an attack.
  1258. function Champion:getDamageWithAttack(weapon, attack)
  1259.     -- Some hard coded effects were turned into trait/skill hooks such as weapon skill bonus and dual wielding penalty
  1260.     -- Added the ability to alter damage via hooks and to affect that variation of damage calculated, instead of being always -50% and +50%
  1261.  
  1262.     -- check skill level requirement
  1263.     if attack.skill and attack.requiredLevel and self:getSkillLevel(attack.skill) < attack.requiredLevel then
  1264.         return nil
  1265.     end
  1266.  
  1267.     local power = attack:getAttackPower()
  1268.     local mod = {attack:getMinDamageMod() or 0, attack:getMaxDamageMod() or 0}
  1269.     local variation = attack:getAttackPowerVariation() or 0.5
  1270.    
  1271.     if party:isHookRegistered("onCalculateDamageWithAttack") then
  1272.         power = party:callHook("onCalculateDamageWithAttack", objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), power)
  1273.     end
  1274.  
  1275.     -- dual wield penalty
  1276.     if weapon and self:isDualWielding() then
  1277.         dualWieldingMulti = self:getCurrentStat("dual_wielding")
  1278.         for name,trait in pairs(dungeon.traits) do
  1279.             if trait.onComputeDualWieldingModifier then
  1280.                 local modifier = trait.onComputeDualWieldingModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
  1281.                 dualWieldingMulti = dualWieldingMulti + (modifier or 0)
  1282.             end
  1283.  
  1284.             if trait.onComputeDualWieldingModifier then
  1285.                 local modifier = trait.onComputeDualWieldingModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
  1286.                 dualWieldingMulti = dualWieldingMulti + (modifier or 0)
  1287.             end
  1288.         end
  1289.  
  1290.         power = power * dualWieldingMulti
  1291.     end
  1292.  
  1293.     -- str/dex stat modifier
  1294.     local baseStat = attack:getBaseDamageStat()
  1295.     local baseMulti = attack:getBaseDamageMultiplier() or 1
  1296.     if baseStat then
  1297.         mod[1] = mod[1] + math.floor((self:getCurrentStat(baseStat) - 10) * baseMulti)
  1298.         mod[2] = mod[2] + math.floor((self:getCurrentStat(baseStat) - 10) * baseMulti)
  1299.     end
  1300.        
  1301.     -- skill modifiers
  1302.     for name,skill in pairs(dungeon.skills) do
  1303.         if skill.onComputeDamageModifier then
  1304.             local modifier = skill.onComputeDamageModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name))
  1305.             if type(modifier) == "number" then
  1306.                 mod[1] = mod[1] + (modifier or 0)
  1307.                 mod[2] = mod[2] + (modifier or 0)
  1308.             elseif type(modifier) == "table" then
  1309.                 mod[1] = mod[1] + (modifier[1] or 0)
  1310.                 mod[2] = mod[2] + (modifier[2] or 0)
  1311.             end
  1312.         end
  1313.  
  1314.         if skill.onComputeDamageMultiplier then
  1315.             local modifier = skill.onComputeDamageMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), self:getSkillLevel(name))
  1316.             power = power * (modifier or 1)
  1317.         end
  1318.     end
  1319.                
  1320.     -- trait modifiers
  1321.     for name,trait in pairs(dungeon.traits) do
  1322.         if trait.onComputeDamageModifier then
  1323.             local modifier = trait.onComputeDamageModifier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
  1324.             if type(modifier) == "number" then
  1325.                 mod[1] = mod[1] + (modifier or 0)
  1326.                 mod[2] = mod[2] + (modifier or 0)
  1327.             elseif type(modifier) == "table" then
  1328.                 mod[1] = mod[1] + (modifier[1] or 0)
  1329.                 mod[2] = mod[2] + (modifier[2] or 0)
  1330.             end
  1331.         end
  1332.  
  1333.         if trait.onComputeDamageMultiplier then
  1334.             local modifier = trait.onComputeDamageMultiplier(objectToProxy(self), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), iff(self:hasTrait(name), 1, 0))
  1335.             power = power * (modifier or 1)
  1336.         end
  1337.     end
  1338.  
  1339.     -- equipment modifiers (equipped items only)
  1340.     for i=1,ItemSlot.BackpackFirst-1 do
  1341.         local it = self:getItem(i)
  1342.         if it then
  1343.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1344.                 for i=1,it.go.components.length do
  1345.                     local comp = it.go.components[i]
  1346.                     if comp.onComputeDamageModifier then
  1347.                         local modifier = comp:onComputeDamageModifier(self, weapon, attack)
  1348.                         mod[1] = mod[1] + (modifier[1] or 0)
  1349.                         mod[2] = mod[2] + (modifier[2] or 0)
  1350.                     end
  1351.                 end
  1352.             end
  1353.         end
  1354.     end
  1355.  
  1356.     -- ammo bonus for missile weapons
  1357.     if attack:getAttackType() == "missile" then
  1358.         local slot = iff(self:getItem(ItemSlot.Weapon) == weapon, ItemSlot.Weapon, ItemSlot.OffHand)
  1359.         local ammo = self:getOtherHandItem(slot)
  1360.         if ammo and attack:checkAmmo(self, slot) then
  1361.             local ammoItem = ammo.go.ammoitem
  1362.             if ammoItem then
  1363.                 --print("ammo bonus: ", ammoItem:getAttackPower() or 0)
  1364.                 mod[1] = mod[1] + (ammoItem:getAttackPower() or 0)
  1365.                 mod[2] = mod[2] + (ammoItem:getAttackPower() or 0)
  1366.             end
  1367.         end
  1368.     end
  1369.  
  1370.     -- conditions
  1371.     if self:hasCondition("starving") then power = power / 2 end
  1372.  
  1373.     power = math.max(math.floor(power), 0)
  1374.    
  1375.     return power,mod,variation
  1376. end
  1377.  
  1378. function Champion:updateHerbalism()
  1379.     -- We no longer use this function, since it's only called for the alchemist class
  1380.     return
  1381. end
  1382.  
  1383. function Champion:updateHerbalismNew()
  1384.     if not self:hasTrait("herb_multiplication") then return end -- herb multiplication no longer tied to alchemist class
  1385.     local multi = { 1, 1, 1, 1, 1, 1 }
  1386.     local herbRates = {
  1387.         ["blooddrop_cap"] = 850,
  1388.         ["etherweed"] = 930,
  1389.         ["mudwort"] = 1950,
  1390.         ["falconskyre"] = 2500,
  1391.         ["blackmoss"] = 3700,
  1392.         ["crystal_flower"] = 4500,
  1393.     }
  1394.  
  1395.     -- Herb multiplication can be canceled. For example, you could make them not multiply during the day, or increase the rate while under water
  1396.     local tilesMoved = party.go.statistics:getStat("tiles_moved")
  1397.     local returnVal = party:callHook("onMultiplyHerbs", herbRates, objectToProxy(champion))
  1398.     if returnVal then
  1399.         if returnVal[1] == false then return false end
  1400.         herbRates = returnVal[2] or herbRates
  1401.     end
  1402.  
  1403.     for name,skill in pairs(dungeon.skills) do
  1404.         if skill.onComputeHerbMultiplicationRate then
  1405.             local modifier = skill.onComputeHerbMultiplicationRate(objectToProxy(self), self:getSkillLevel(name))
  1406.             if modifier and modifier[1] then
  1407.                 for i = 1, 6 do
  1408.                     multi[i] = multi[i] * (modifier[i] or 1)
  1409.                 end
  1410.             end
  1411.         end
  1412.     end
  1413.  
  1414.     for name,trait in pairs(dungeon.traits) do
  1415.         if trait.onComputeHerbMultiplicationRate then
  1416.             local modifier = trait.onComputeHerbMultiplicationRate(objectToProxy(self), iff(self:hasTrait(name), 1, 0))
  1417.             if modifier and modifier[1] then
  1418.                 for i = 1, 6 do
  1419.                     multi[i] = multi[i] * (modifier[i] or 1)
  1420.                 end
  1421.             end
  1422.         end
  1423.     end
  1424.  
  1425.     local i = 1
  1426.     for herb,rate in pairs(herbRates) do
  1427.         -- check growth rate
  1428.         if (tilesMoved % math.floor(rate * multi[i])) == 0 then
  1429.             self:updateHerbalism2(herb)
  1430.         end
  1431.         i = i + 1
  1432.     end
  1433. end
  1434.  
  1435. function Champion:isDualWielding()
  1436.     -- Completely reworked to take this info from traits and equipment
  1437.  
  1438.     local isDualWielding = false
  1439.     local weapon1 = self:getItem(ItemSlot.Weapon)
  1440.     local weapon2 = self:getItem(ItemSlot.OffHand)
  1441.  
  1442.     -- skill modifiers
  1443.     for name,skill in pairs(dungeon.skills) do
  1444.         if skill.onCheckDualWielding then
  1445.             isDualWielding = isDualWielding or skill.onCheckDualWielding(objectToProxy(self), objectToProxy(weapon1), objectToProxy(weapon2),  self:getSkillLevel(name))
  1446.         end
  1447.     end
  1448.                
  1449.     -- trait modifiers
  1450.     for name,trait in pairs(dungeon.traits) do
  1451.         if trait.onCheckDualWielding then
  1452.             isDualWielding = isDualWielding or trait.onCheckDualWielding(objectToProxy(self), objectToProxy(weapon1), objectToProxy(weapon2), iff(self:hasTrait(name), 1, 0))
  1453.         end
  1454.     end
  1455.  
  1456.     -- equipment modifiers (equipped items only)
  1457.     for i=1,ItemSlot.BackpackFirst-1 do
  1458.         local it = self:getItem(i)
  1459.         if it then
  1460.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1461.                 for i=1,it.go.components.length do
  1462.                     local comp = it.go.components[i]
  1463.                     if comp.onCheckDualWielding then
  1464.                         isDualWielding = isDualWielding or comp:onCheckDualWielding(self, weapon1, weapon2)
  1465.                     end
  1466.                 end
  1467.             end
  1468.         end
  1469.     end
  1470.  
  1471.     return isDualWielding
  1472. end
  1473.  
  1474. function Champion:attack(slot, powerAttack)
  1475.     local item = self:getItem(slot)
  1476.     if self:hasCondition("bear_form") then item = nil end
  1477.    
  1478.     local action
  1479.     if not item then
  1480.         action = self:getUnarmedAttack()
  1481.     elseif powerAttack then
  1482.         action = self:getSecondaryAction(slot)
  1483.     else
  1484.         action = self:getPrimaryAction(slot)
  1485.     end
  1486.    
  1487.     if not action then return end
  1488.  
  1489.     -- disable unarmed attacks when holding a two-handed weapon
  1490.     if not item then
  1491.         local otherItem = self:getOtherHandItem(slot)
  1492.         if otherItem and otherItem:hasTrait("two_handed") and not self:hasTrait("two_handed_mastery") then
  1493.             return
  1494.         end
  1495.     end
  1496.    
  1497.     -- check weapon skill level requirement
  1498.     if item and not item:canBeUsedByChampion(self, slot) then
  1499.         self:showAttackResult("Can't use")
  1500.         return
  1501.     end
  1502.  
  1503.     -- can't attack with a broken hand
  1504.     if (slot == ItemSlot.Weapon and self:hasCondition("right_hand_wound")) or (slot == ItemSlot.OffHand and self:hasCondition("left_hand_wound")) then
  1505.         self:showAttackResult("Can't use")
  1506.         return
  1507.     end
  1508.  
  1509.     -- can't attack with 2-handed weapon if one of the hands is broken
  1510.     if item and item:hasTrait("two_handed") and not self:hasTrait("two_handed_mastery") then
  1511.         if self:hasCondition("left_hand_wound") or self:hasCondition("right_hand_wound") then
  1512.             self:showAttackResult("Can't use")
  1513.             return
  1514.         end
  1515.     end
  1516.  
  1517.     local repeatCount = action.repeatCount
  1518.     local repeatDelay = action.repeatDelay
  1519.     if party:isHookRegistered("onAttack") and action ~= self:getUnarmedAttack() then
  1520.         local returnValue = party:callHook("onAttack", objectToProxy(self), objectToProxy(action), slot, repeatCount, repeatDelay)
  1521.         if returnValue then
  1522.             if returnValue[1] == false then return end
  1523.             repeatCount = returnValue[2] or repeatCount
  1524.             repeatDelay = returnValue[3] or repeatDelay
  1525.         end
  1526.     end
  1527.    
  1528.     if action ~= self:getUnarmedAttack() and action:callHook("onAttack", objectToProxy(self), slot, 0) == false then
  1529.         return
  1530.     end
  1531.        
  1532.     if action.start then action:start(self, slot, 0) end
  1533.  
  1534.     if powerAttack then party.go.statistics:increaseStat("power_attacks", 1) end
  1535.  
  1536.     self:addData("attack_count", 1)
  1537.    
  1538.     -- chain action
  1539.     if action.chainAction then
  1540.         local next = action:getNextChainAction()
  1541.         if next then
  1542.             self.pendingAttack = {
  1543.                 action = next,
  1544.                 slot = slot,
  1545.                 time = action.chainActionDelay or 0.2,
  1546.                 chainIndex = 1,
  1547.             }
  1548.         end
  1549.     end
  1550.  
  1551.     -- repeat action
  1552.     if action.repeatCount and action.repeatCount > 1 then
  1553.         local delay = action.repeatDelay or 0.2
  1554.         self.pendingAttack = {
  1555.             action = action,
  1556.             slot = slot,
  1557.             time = delay,
  1558.             chainIndex = 1,
  1559.             repeatCount = action.repeatCount - 1,
  1560.             repeatDelay = delay,
  1561.         }
  1562.     end
  1563.  
  1564.     -- double throw
  1565.     if self:hasTrait("double_throw") and action.getAttackType and action:getAttackType() == "throw" and self.pendingAttack == nil then
  1566.         local otherItem = self:getOtherHandItem(slot)
  1567.         if otherItem and otherItem.go.throwattack then
  1568.             self.pendingAttack = {
  1569.                 action = otherItem.go.throwattack,
  1570.                 slot = iff(slot == ItemSlot.Weapon, ItemSlot.OffHand, ItemSlot.Weapon),
  1571.                 time = 0.2
  1572.             }          
  1573.         end
  1574.     end
  1575. end
  1576.  
  1577. function Champion:damage(dmg, damageType, hitContext)
  1578.     damageType = damageType or "physical"
  1579.     dmg = math.floor(dmg)
  1580.     local isSpell = hitContext and (hitContext.go.tiledamager or hitContext.go.cloudspell)
  1581.    
  1582.     if damageType == "dispel" then return end
  1583.    
  1584.     if self:isAlive() and not self:hasCondition("petrified") then
  1585.         -- apply damage resistance
  1586.         if damageType ~= "physical" and damageType ~= "drowning" and damageType ~= "pure" then
  1587.             local resist = self:getResistance(damageType)
  1588.             if resist == 100 then
  1589.                 dmg = 0
  1590.             elseif resist > 0 then
  1591.                 dmg = math.floor(dmg * (100 - resist) / 100 + 0.5)
  1592.             end
  1593.         end
  1594.         local onDamageReturn = party:callHook("onDamage", objectToProxy(self), dmg, damageType, objectToProxy(hitContext))
  1595.         if onDamageReturn then
  1596.             dmg = onDamageReturn[2]
  1597.             if onDamageReturn[1] == false then
  1598.                 return
  1599.             end
  1600.         end
  1601.  
  1602.         if dmg > 0 then
  1603.             -- skill modifiers
  1604.             for name,skill in pairs(dungeon.skills) do
  1605.                 if skill.onComputeDamageTaken then
  1606.                     dmg = skill.onComputeDamageTaken(objectToProxy(self), objectToProxy(hitContext), dmg, dmgType, isSpell, self:getSkillLevel(name)) or dmg
  1607.                 end
  1608.             end
  1609.            
  1610.             -- traits modifiers
  1611.             for name,trait in pairs(dungeon.traits) do
  1612.                 if trait.onComputeDamageTaken then
  1613.                     dmg = trait.onComputeDamageTaken(objectToProxy(self), objectToProxy(hitContext), dmg, dmgType, isSpell, iff(self:hasTrait(name), 1, 0)) or dmg
  1614.                 end
  1615.             end
  1616.  
  1617.             -- equipment modifiers (equipped items only)
  1618.             for i=1,ItemSlot.BackpackFirst-1 do
  1619.                 local it = self:getItem(i)
  1620.                 if it then
  1621.                     if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1622.                         for i=1,it.go.components.length do
  1623.                             local comp = it.go.components[i]
  1624.                             if comp.onComputeDamageTaken then
  1625.                                 dmg = comp:onComputeDamageTaken(self, hitContext, dmg, dmgType, isSpell) or dmg
  1626.                             end
  1627.                         end
  1628.                     end
  1629.                 end
  1630.             end
  1631.         end
  1632.  
  1633.         dmg = self:triggerOnDamage(dmg, dmgType, isSpell, hitContext)
  1634.  
  1635.         dmg = math.floor(dmg)
  1636.        
  1637.         if dmg > 0 then
  1638.             self:modifyBaseStat("health", -dmg)
  1639.             self:showDamageIndicator(dmg)
  1640.             messageSystem:sendMessageNEW("onChampionDamaged", self, dmg, damageType)
  1641.         end
  1642.        
  1643.         -- champion died?
  1644.         if not self:isAlive() then
  1645.             if party:callHook("onDie", objectToProxy(self)) == false then
  1646.                 if self:getBaseStat("health") < 1 then self:setBaseStat("health", 1) end
  1647.                 return
  1648.             end
  1649.  
  1650.             self:die()
  1651.         end
  1652.     end
  1653. end
  1654.  
  1655. function Champion:triggerOnDamage(dmg, dmgType, isSpell, hitContext)
  1656.     -- trigger minotaur rage?
  1657.     if self:hasTrait("rage") then
  1658.         local oldHealth = self:getHealth() / self:getMaxHealth()
  1659.         local newHealth = (self:getHealth() - dmg) / self:getMaxHealth()
  1660.         local threshold = 0.2
  1661.         if oldHealth > threshold and newHealth <= threshold then
  1662.             self:setConditionValue("rage", self:getConditionValue("rage") + 20)
  1663.         end
  1664.     end
  1665.  
  1666.     return dmg
  1667. end
  1668.  
  1669. local oldChampionRecomputeStats = Champion.recomputeStats
  1670. function Champion:recomputeStats()
  1671.     oldChampionRecomputeStats(self)
  1672.  
  1673.     -- Final stat modifiers
  1674.     -- apply item modifiers
  1675.     for i=1,ItemSlot.BackpackFirst-1 do
  1676.         local it = self:getItem(i)
  1677.         if it then
  1678.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1679.                 for i=1,it.go.components.length do
  1680.                     local comp = it.go.components[i]
  1681.                     if comp.onRecomputeFinalStats then
  1682.                         comp:onRecomputeFinalStats(self)
  1683.                     end
  1684.                 end
  1685.             end
  1686.         end
  1687.     end
  1688.  
  1689.     -- apply condition modifiers
  1690.     for _,cond in pairs(self.conditions) do
  1691.         if cond.recomputeFinalStats then cond:recomputeFinalStats(self) end
  1692.     end
  1693.  
  1694.     -- skill modifiers
  1695.     for name,skill in pairs(dungeon.skills) do
  1696.         if skill.onRecomputeFinalStats then
  1697.             local level = self:getSkillLevel(name)
  1698.             if level > 0 then
  1699.                 skill.onRecomputeFinalStats(objectToProxy(self), level)
  1700.             end
  1701.         end
  1702.     end
  1703.  
  1704.     -- trait modifiers
  1705.     for name,trait in pairs(dungeon.traits) do
  1706.         if trait.onRecomputeFinalStats and self:hasTrait(name) then
  1707.             trait.onRecomputeFinalStats(objectToProxy(self), 1)
  1708.         end
  1709.     end
  1710. end
  1711.  
  1712. function Champion:castSpell(gesture)
  1713.     -- Updated to allow traits and equipment to affect spell cost, cooldown and power
  1714.  
  1715.     -- can't cast spell with wounded head
  1716.     if self:hasCondition("head_wound") then
  1717.         self:showAttackResult("Fizzle", GuiItem.SpellFizzle)
  1718.         return false
  1719.     end
  1720.  
  1721.     -- find spell
  1722.     local spell = Spell.getSpellByGesture(gesture)
  1723.     if not spell then
  1724.         self:showAttackResult("Fizzle", GuiItem.SpellFizzle)
  1725.         soundSystem:playSound2D("spell_fizzle")
  1726.         self:spendEnergy(math.random(5,13))
  1727.         --self:clearRunes()
  1728.         return false
  1729.     end
  1730.    
  1731.     -- check skill level
  1732.     if not config.unlimitedSpells then
  1733.         -- check skill requirements
  1734.         if spell.requirements and not Skill.checkRequirements(self, spell.requirements) then
  1735.             self:showAttackResult("Fizzle", GuiItem.SpellFizzle)
  1736.             soundSystem:playSound2D("spell_fizzle")
  1737.             self:spendEnergy(math.random(5,13))
  1738.             self:clearRunes()
  1739.             return false
  1740.         end
  1741.     end
  1742.  
  1743.     -- spend energy
  1744.     if not config.unlimitedSpells then
  1745.         local cost = spell.manaCost
  1746.         -- console:print("cost before", cost)
  1747.  
  1748.         -- skill modifiers
  1749.         for name,skill in pairs(dungeon.skills) do
  1750.             if skill.onComputeSpellCost then
  1751.                 local modifier = skill.onComputeSpellCost(objectToProxy(self), spell.name, spell.manaCost, spell.skill, self:getSkillLevel(name))
  1752.                 cost = cost * (modifier or 1)
  1753.             end
  1754.         end
  1755.                    
  1756.         -- trait modifiers
  1757.         for name,trait in pairs(dungeon.traits) do
  1758.             if trait.onComputeSpellCost then
  1759.                 local modifier = trait.onComputeSpellCost(objectToProxy(self), spell.name, spell.manaCost, spell.skill, iff(self:hasTrait(name), 1, 0))
  1760.                 cost = cost * (modifier or 1)
  1761.             end
  1762.         end
  1763.  
  1764.         -- equipment modifiers (equipped items only)
  1765.         for i=1,ItemSlot.BackpackFirst-1 do
  1766.             local it = self:getItem(i)
  1767.             if it then
  1768.                 if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1769.                     for i=1,it.go.components.length do
  1770.                         local comp = it.go.components[i]
  1771.                         if comp.onComputeSpellCost then
  1772.                             local modifier = comp:onComputeSpellCost(self, spell.name, spell.manaCost, spell.skill)
  1773.                             cost = cost * (modifier or 1)
  1774.                         end
  1775.                     end
  1776.                 end
  1777.             end
  1778.         end
  1779.        
  1780.         if self:getEnergy() < cost then
  1781.             self:showAttackResult("Out of energy", GuiItem.SpellNoEnergy)
  1782.             soundSystem:playSound2D("spell_out_of_energy")
  1783.             return
  1784.         end    
  1785.         self:spendEnergy(cost)
  1786.     end
  1787.    
  1788.     if party:callHook("onCastSpell", objectToProxy(self), spell.name) == false then
  1789.         return false
  1790.     end
  1791.  
  1792.     messageSystem:sendMessageNEW("onChampionCastSpell", self, spell)
  1793.    
  1794.     self:clearRunes()
  1795.  
  1796.     local skill = 0
  1797.     if spell.skill then skill = self:getSkillLevel(spell.skill) end
  1798.     --if config.unlimitedSpells then skill = math.max(skill, 3) end
  1799.     local pos = party.go:getWorldPositionFast()
  1800.     local x,y = party.go.map:worldToMap(pos)
  1801.     local spl = Spell.castSpell(spell, self, x, y, party.go.facing, party.go.elevation, skill)
  1802.     local dmg = 0
  1803.     local spellObject = nil
  1804.     if spl then
  1805.     if spl.tiledamager then
  1806.         dmg = spl.tiledamager.attackPower
  1807.         spellObject = spl.tiledamager
  1808.     elseif spl.projectile then
  1809.         dmg = spl.projectile.attackPower
  1810.         spellObject = spl.projectile
  1811.     elseif spl.cloudspell then
  1812.         dmg = spl.cloudspell.attackPower
  1813.         spellObject = spl.cloudspell
  1814.     end
  1815.    
  1816.     -- Spell damage modifiers
  1817.     -- skill modifiers
  1818.     for name,skill in pairs(dungeon.skills) do
  1819.         if skill.onComputeSpellDamage then
  1820.             local returnVal = skill.onComputeSpellDamage(objectToProxy(self), objectToProxy(spellObject), spell.name, spell.manaCost, spell.skill, self:getSkillLevel(name))
  1821.             if returnVal then
  1822.                 if returnVal[1] == false then return end
  1823.                 if returnVal[2] then
  1824.                     local tempSpl = returnVal[2]
  1825.                     spl = tempSpl.go
  1826.                     if not (spl.tiledamager or spl.projectile or spl.cloudspell) then
  1827.                         console:print("invalid onComputeSpellDamage return value 2")
  1828.                     end
  1829.                 end
  1830.             end
  1831.         end
  1832.     end
  1833.                
  1834.     -- trait modifiers
  1835.     for name,trait in pairs(dungeon.traits) do
  1836.         if trait.onComputeSpellDamage then
  1837.             local returnVal = trait.onComputeSpellDamage(objectToProxy(self), objectToProxy(spellObject), spell.name, spell.manaCost, spell.skill, iff(self:hasTrait(name), 1, 0))
  1838.             if returnVal then
  1839.                 if returnVal[1] == false then return end
  1840.                 if returnVal[2] then
  1841.                     local tempSpl = returnVal[2]
  1842.                     spl = tempSpl.go
  1843.                     if not (spl.tiledamager or spl.projectile or spl.cloudspell) then
  1844.                         console:print("invalid onComputeSpellDamage return value 2")
  1845.                     end
  1846.                 end
  1847.             end
  1848.         end
  1849.     end
  1850.  
  1851.     -- equipment modifiers (equipped items only)
  1852.     for i=1,ItemSlot.BackpackFirst-1 do
  1853.         local it = self:getItem(i)
  1854.         if it then
  1855.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1856.                 for i=1,it.go.components.length do
  1857.                     local comp = it.go.components[i]
  1858.                     if comp.onComputeSpellDamage then
  1859.                         local returnVal = comp:onComputeSpellDamage(self, spellObject, spell.name, spell.manaCost, spell.skill)
  1860.                         if returnVal then
  1861.                             if returnVal[1] == false then return end
  1862.                             if returnVal[2] then
  1863.                                 local tempSpl = returnVal[2]
  1864.                                 spl = tempSpl.go
  1865.                                 if not (spl.tiledamager or spl.projectile or spl.cloudspell) then
  1866.                                     console:print("invalid onComputeSpellDamage return value 2")
  1867.                                 end
  1868.                             end
  1869.                         end
  1870.                     end
  1871.                 end
  1872.             end
  1873.         end
  1874.     end
  1875.     end
  1876.    
  1877.     -- cool down
  1878.     local cooldown = 5
  1879.     -- console:print("cooldown before", cooldown)
  1880.     -- skill modifiers
  1881.     for name,skill in pairs(dungeon.skills) do
  1882.         if skill.onComputeSpellCooldown then
  1883.             local modifier = skill.onComputeSpellCooldown(objectToProxy(self), spell.name, spell.manaCost, spell.skill, self:getSkillLevel(name))
  1884.             cooldown = cooldown * (modifier or 1)
  1885.         end
  1886.     end
  1887.                
  1888.     -- trait modifiers
  1889.     for name,trait in pairs(dungeon.traits) do
  1890.         if trait.onComputeSpellCooldown then
  1891.             local modifier = trait.onComputeSpellCooldown(objectToProxy(self), spell.name, spell.manaCost, spell.skill, iff(self:hasTrait(name), 1, 0))
  1892.             cooldown = cooldown * (modifier or 1)
  1893.         end
  1894.     end
  1895.  
  1896.     -- equipment modifiers (equipped items only)
  1897.     for i=1,ItemSlot.BackpackFirst-1 do
  1898.         local it = self:getItem(i)
  1899.         if it then
  1900.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  1901.                 for i=1,it.go.components.length do
  1902.                     local comp = it.go.components[i]
  1903.                     if comp.onComputeSpellCooldown then
  1904.                         local modifier = comp:onComputeSpellCooldown(self, spell.name, spell.manaCost, spell.skill)
  1905.                         cooldown = cooldown * (modifier or 1)
  1906.                     end
  1907.                 end
  1908.             end
  1909.         end
  1910.     end
  1911.  
  1912.     -- console:print("cooldown after", cooldown)
  1913.  
  1914.     self.cooldownTimer[1] = cooldown
  1915.     self.cooldownTimer[2] = cooldown
  1916.  
  1917.     -- strenous activity consumes food
  1918.     self:consumeFood(math.random(4,9))
  1919.    
  1920.     -- learn new spell?
  1921.     if not spell.hidden and not self:hasTrait(spell.name) then
  1922.         self:addTrait(spell.name)
  1923.         gui:hudPrint(self.name.." learned a new spell!")
  1924.         soundSystem:playSound2D("discover_spell")
  1925.     end
  1926.  
  1927.     party.go.statistics:increaseStat("spells_cast", 1)
  1928.    
  1929.     return true
  1930. end
  1931.  
  1932. function Champion:getLoad()
  1933.     -- Updated to enable affecting item weight with traits
  1934.  
  1935.     local load = 0
  1936.  
  1937.     local armorWeightReductionEquipped = 1
  1938.     local armorWeightReduction = 1
  1939.  
  1940.     for i=1,ItemSlot.MaxSlots do
  1941.         local it = self.items[i]
  1942.         if it then
  1943.             local equipped = false
  1944.             if i < ItemSlot.BackpackFirst and i ~= ItemSlot.Weapon and i ~= ItemSlot.OffHand and (it:hasTrait("light_armor") or it:hasTrait("heavy_armor")) then
  1945.                 equipped = true
  1946.             end
  1947.  
  1948.             for name,skill in pairs(dungeon.skills) do
  1949.                 if skill.onComputeItemWeight then
  1950.                     local modifier = skill.onComputeItemWeight(objectToProxy(self), equipped, self:getSkillLevel(name))
  1951.                     armorWeightReduction = armorWeightReduction * (modifier or 1)
  1952.                 end
  1953.             end
  1954.        
  1955.             for name,trait in pairs(dungeon.traits) do
  1956.                 if trait.onComputeItemWeight then
  1957.                     local modifier = trait.onComputeItemWeight(objectToProxy(self), equipped, iff(self:hasTrait(name), 1, 0))
  1958.                     armorWeightReduction = armorWeightReduction * (modifier or 1)
  1959.                 end
  1960.             end
  1961.  
  1962.             load = load + it:getTotalWeight() * armorWeightReduction
  1963.         end
  1964.     end
  1965.     return load
  1966. end
  1967.  
  1968. -------------------------------------------------------------------------------------------------------
  1969. -- Monster Functions                                                                                 --    
  1970. -------------------------------------------------------------------------------------------------------
  1971.  
  1972. function MonsterComponent:onAttackedByChampion(champion, weapon, attack, slot, dualWieldSide)
  1973.     if not self:isAlive() or self.go.elevation ~= party.go.elevation or self:getMonsterFlag(MonsterFlag.NonMaterial) then return end
  1974.    
  1975.     local target = self
  1976.  
  1977.     local damageType = attack.damageType or "physical"
  1978.     local gauntlets = champion:getItem(ItemSlot.Gloves)
  1979.     if gauntlets and gauntlets:hasTrait("fire_gauntlets") then damageType = "fire" end
  1980.        
  1981.     -- get target for monster groups
  1982.     if target.group then
  1983.         target = target:getNearestMonster(champion:getChampionPositionInWorld(0.5))
  1984.         if not target then return end
  1985.     end
  1986.  
  1987.     -- evasion
  1988.     if not self:hasCondition("sleep") and not self:hasCondition("frozen") and attack.name ~= "bonusAttack" then
  1989.         local accuracy = champion:getAccuracyWithAttack(weapon, attack)
  1990.         local tohit = champion:getToHitChanceWithAttack(weapon, attack, target, accuracy, damageType)
  1991.  
  1992.         if math.random() > tohit / 100 or target.evasion >= 1000 then
  1993.             target:showDamageText("miss", Color.Grey)
  1994.             champion.luck = math.min(champion.luck + 3, 15)
  1995.             champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
  1996.             return "miss"
  1997.         end
  1998.         champion.luck = 0
  1999.     end
  2000.    
  2001.     -- compute side
  2002.     local tside = (party.go.facing - target.go.facing + 6) % 4
  2003.     local side
  2004.     if tside == 0 then
  2005.         -- front left or right?
  2006.         local rightSide = (champion.championIndex == 1 or champion.championIndex == 3)
  2007.         side = iff(rightSide, "front_right", "front_left")
  2008.     elseif tside == 1 then
  2009.         side = "right"
  2010.     elseif tside == 2 then
  2011.         side = "back"
  2012.     else
  2013.         side = "left"
  2014.     end
  2015.  
  2016.     -- compute base damage
  2017.     local dmg = computeDamage(champion:getDamageWithAttack(weapon, attack))
  2018.  
  2019.     local damageFlags = DamageFlags.Impact
  2020.     -- crits & fumbles
  2021.     local crit = false
  2022.     local critChance = champion:getCritChanceWithAttack(weapon, attack, target) / 100
  2023.     if math.random() < critChance then
  2024.         if dmg > 0 then
  2025.             crit = true
  2026.         end
  2027.     elseif math.random() < 0.07 then
  2028.         dmg = dmg / 2
  2029.     end
  2030.  
  2031.     -- backstab
  2032.     local backstab = false
  2033.     local backStabMult = 0
  2034.     -- trait modifiers
  2035.     for name,trait in pairs(dungeon.traits) do
  2036.         if trait.onCheckBackstab then
  2037.             local modifier = trait.onCheckBackstab(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, iff(champion:hasTrait(name), 1, 0))
  2038.             backStabMult = backStabMult + (modifier or 0)
  2039.         end
  2040.     end
  2041.     -- skill modifiers
  2042.     for name,skill in pairs(dungeon.skills) do
  2043.         if skill.onCheckBackstab then
  2044.             local modifier = skill.onCheckBackstab(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, champion:getSkillLevel(name))
  2045.             backStabMult = backStabMult + (modifier or 0)
  2046.         end
  2047.     end
  2048.     -- equipment modifiers (equipped items only)
  2049.     for i=1,ItemSlot.BackpackFirst-1 do
  2050.         local it = champion:getItem(i)
  2051.         if it then
  2052.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  2053.                 for i=1,it.go.components.length do
  2054.                     local comp = it.go.components[i]
  2055.                     if comp.onComputeBackstabMultiplier then
  2056.                         local modifier = comp:onComputeBackstabMultiplier(champion, weapon, attack)
  2057.                         backStabMult = backStabMult + (modifier or 0)
  2058.                     end
  2059.                 end
  2060.             end
  2061.         end
  2062.     end
  2063.  
  2064.     if backStabMult > 0 and tside == 2 and not target:isImmuneTo("backstab") then
  2065.         local extraDamage = math.floor(dmg * (backStabMult-1) * (math.random() * 0.5 + 0.5))
  2066.         --print(string.format("backstab damage bonus +%d", extraDamage))
  2067.         dmg = dmg + extraDamage
  2068.         backstab = true
  2069.     end
  2070.  
  2071.     -- skill modifiers
  2072.     for name,skill in pairs(dungeon.skills) do
  2073.         if skill.onComputeChampionAttackDamage then
  2074.             local modifier = skill.onComputeChampionAttackDamage(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, backstab, champion:getSkillLevel(name))
  2075.             if modifier then
  2076.                 dmg = modifier[2] or dmg
  2077.                 heading = modifier[3] ~= nil and modifier[3] or heading
  2078.                 crit = modifier[4] ~= nil and modifier[4] or crit
  2079.                 backstab = modifier[5] or backstab
  2080.                 damageType = modifier[6] or damageType
  2081.             end
  2082.         end
  2083.     end
  2084.    
  2085.     -- trait modifiers
  2086.     for name,trait in pairs(dungeon.traits) do
  2087.         if trait.onComputeChampionAttackDamage then
  2088.             local modifier = trait.onComputeChampionAttackDamage(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, backstab, iff(champion:hasTrait(name), 1, 0))
  2089.             if modifier then
  2090.                 dmg = modifier[2] or dmg
  2091.                 heading = modifier[3] ~= nil and modifier[3] or heading
  2092.                 crit = modifier[4] ~= nil and modifier[4] or crit
  2093.                 backstab = modifier[5] or backstab
  2094.                 damageType = modifier[6] or damageType
  2095.             end
  2096.         end
  2097.     end
  2098.  
  2099.     -- equipment modifiers (equipped items only)
  2100.     for i=1,ItemSlot.BackpackFirst-1 do
  2101.         local it = champion:getItem(i)
  2102.         if it then
  2103.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  2104.                 for i=1,it.go.components.length do
  2105.                     local comp = it.go.components[i]
  2106.                     if comp.onComputeChampionAttackDamage then
  2107.                         local modifier = comp:onComputeChampionAttackDamage(self, champion, weapon, attack, dmg, damageType, crit, backstab, level)
  2108.                         if modifier then
  2109.                             dmg = modifier[2] or dmg
  2110.                             heading = modifier[3] ~= nil and modifier[3] or heading
  2111.                             crit = modifier[4] ~= nil and modifier[4] or crit
  2112.                             backstab = modifier[5] or backstab
  2113.                             damageType = modifier[6] or damageType
  2114.                         end
  2115.                     end
  2116.                 end
  2117.             end
  2118.         end
  2119.     end
  2120.  
  2121.     if crit then
  2122.         local critMult = champion:getCritMultiplierWithAttack(weapon, attack, target)
  2123.         if champion:getSecondaryAction(slot) == attack then critMult = 1.5 end
  2124.         if attack.getCritMultiplier then critMult = critMult + (attack:getCritMultiplier()-1) end
  2125.         dmg = dmg * critMult
  2126.         if dmg <= 0 then
  2127.             crit = false
  2128.         end
  2129.     end
  2130.  
  2131.     -- damage reduction
  2132.     local protection = target:getMonsterProtectionWithAttack(champion, weapon, attack, dmg, damageType, crit, backstab)
  2133.    
  2134.     if protection > 0 then dmg = computeDamageReduction(dmg, protection) end
  2135.  
  2136.     -- invulnerability
  2137.     if target:isInvulnerable() then
  2138.         dmg = 0
  2139.         damageFlags = DamageFlags.Impact
  2140.     end
  2141.  
  2142.     -- damage source
  2143.     damageFlags = damageFlags + bit.lshift(DamageFlags.Champion1, champion.ordinal-1)
  2144.        
  2145.     -- compute impact position
  2146.     local impactPos = target:findImpactPosition(champion:getChampionPositionInWorld(0.7))
  2147.    
  2148.     -- heading
  2149.     local heading
  2150.     if backstab then
  2151.         heading = "Backstab"
  2152.     elseif crit then
  2153.         heading = "Critical"
  2154.     end
  2155.  
  2156.     dmg = math.max(math.floor(dmg), 0)
  2157.    
  2158.     if self.go.goromorgshield and self.go.goromorgshield:getEnergy() == math.huge then dmg = 0 end
  2159.    
  2160.     -- call hook
  2161.     if attack.go and attack:callHook("onHitMonster", objectToProxy(target), tside, dmg, objectToProxy(champion), crit, backstab) == false then
  2162.         return true
  2163.     end
  2164.  
  2165.     self:hitTriggers(champion, weapon, attack)
  2166.  
  2167.     if attack.knockback then
  2168.         self:knockback(party.go.facing)
  2169.     end
  2170.  
  2171.     -- deal damage to target
  2172.     local oldHealth = target:getHealth()
  2173.     target:damage(dmg, side, damageFlags, damageType, impactPos, heading)
  2174.  
  2175.     -- HACK: show zero damage in attack panel if monster is invulnerable to damage
  2176.     if target:getHealth() == oldHealth then dmg = 0 end
  2177.  
  2178.     if backstab and not target:isAlive() then  
  2179.         steamContext:unlockAchievement("backstabber")
  2180.     end
  2181.  
  2182.     champion:showAttackResult(dmg, GuiItem.HitSplash, dualWieldSide)
  2183.  
  2184.     -- cause condition
  2185.     if attack.causeCondition then
  2186.         local chance = attack.conditionChance or 50
  2187.         if math.random(1,100) <= chance then
  2188.             target:setCondition(attack.causeCondition)
  2189.  
  2190.             -- mark condition so that exp is awarded if monster is killed by the condition
  2191.             local cond = target.go:getComponent(attack.causeCondition)
  2192.             if cond and cond.setCausedByChampion then
  2193.                 cond:setCausedByChampion(champion.ordinal)
  2194.             end
  2195.         end
  2196.     end
  2197.  
  2198.     return true
  2199. end
  2200.  
  2201. function MonsterComponent:getMonsterProtectionWithAttack(champion, weapon, attack, dmg, damageType, crit, backstab, projectile)
  2202.     local pierce = 0
  2203.     local protection = self:getProtection()
  2204.     if attack and attack.pierce then pierce = pierce + attack.pierce end
  2205.     if projectile and projectile.projectilePierce then pierce = pierce + projectile.projectilePierce end
  2206.     if not champion then return math.max(protection - pierce, 0) end
  2207.  
  2208.     -- traits modifiers
  2209.     for name,trait in pairs(dungeon.traits) do
  2210.         if trait.onComputePierce then
  2211.             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))
  2212.             pierce = pierce + (modifier or 0)
  2213.         end
  2214.     end
  2215.  
  2216.     -- skills modifiers
  2217.     for name,skill in pairs(dungeon.skills) do
  2218.         if skill.onComputePierce then
  2219.             local modifier = skill.onComputePierce(objectToProxy(self), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(projectile), dmg, damageType, attack:getAttackType(), crit, backstab, champion:getSkillLevel(name))
  2220.             pierce = pierce + (modifier or 0)
  2221.         end
  2222.     end
  2223.  
  2224.     -- equipment modifiers (equipped items only)
  2225.     for i=1,ItemSlot.BackpackFirst-1 do
  2226.         local it = champion:getItem(i)
  2227.         if it then
  2228.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  2229.                 for i=1,it.go.components.length do
  2230.                     local comp = it.go.components[i]
  2231.                     if comp.onComputePierce then
  2232.                         local modifier = comp:onComputePierce(self, champion, weapon, attack, projectile, dmg, damageType, attackType, crit, backstab)
  2233.                         pierce = pierce + (modifier or 0)
  2234.                     end
  2235.                 end
  2236.             end
  2237.         end
  2238.     end
  2239.  
  2240.     return math.max(protection - pierce, 0)
  2241. end
  2242.  
  2243. function MonsterComponent:hitTriggers(champion, weapon, attack)
  2244.     if not champion then return false end
  2245.     -- Triggers here
  2246. end
  2247.  
  2248. function MonsterComponent:damage(dmg, side, damageFlags, damageType, impactPos, heading, spell)
  2249.     damageFlags = damageFlags or 0
  2250.  
  2251.     if not self:isAlive() then return end
  2252.    
  2253.     -- tentacles hiding below ground ignore all damage
  2254.     if self.go.arch.name == "tentacles" and not self:getMonsterFlag(MonsterFlag.Collides) then return end
  2255.    
  2256.     if self:isInvulnerable() then dmg = 0 end
  2257.  
  2258.     -- resist
  2259.     local resist = self:getResistance(damageType)
  2260.     if resist then dmg = math.floor(dmg * getResistanceDamageMultiplier(resist)) end
  2261.  
  2262.     -- goromorg shield
  2263.     if self.go.goromorgshield and resist ~= "immune" and resist ~= "absorb" and self.go.goromorgshield:shieldHit(dmg) then return end
  2264.  
  2265.     -- dispel
  2266.     if damageType == "dispel" and not self:hasTrait("elemental") then return end
  2267.  
  2268.     local onDamageReturn = self:callHook("onDamage", dmg, damageType)
  2269.     if onDamageReturn then
  2270.         dmg = onDamageReturn[2]
  2271.         if onDamageReturn[1] == false then
  2272.             return
  2273.         end
  2274.     end
  2275.  
  2276.     -- remember who hit us
  2277.     local c = nil
  2278.     for i=0,3 do
  2279.         if bit.band(damageFlags, bit.lshift(DamageFlags.Champion1, i)) ~= 0 then
  2280.             c = i + 1
  2281.             self:setMonsterFlag(bit.lshift(MonsterFlag.DamagedByChampion1, i), true)
  2282.         end
  2283.     end
  2284.     local champion = party.champions[c]
  2285.  
  2286.     local onSpellDamageReturn = self:callHook("onSpellDamage", dmg, damageType, objectToProxy(champion), objectToProxy(spell), heading)
  2287.     if onSpellDamageReturn then
  2288.         if onSpellDamageReturn[1] == false then
  2289.             return
  2290.         end
  2291.         dmg = onSpellDamageReturn[2]
  2292.         heading = onSpellDamageReturn[3] or heading
  2293.     end
  2294.  
  2295.     -- trait modifiers
  2296.     if champion then
  2297.         for name,trait in pairs(dungeon.traits) do
  2298.             if trait.onComputeChampionSpellDamage then
  2299.                 local modifier = trait.onComputeChampionSpellDamage(objectToProxy(self), objectToProxy(champion), objectToProxy(spell), dmg, damageType, iff(champion:hasTrait(name), 1, 0))
  2300.                 if modifier then
  2301.                     if modifier[1] == false then return end
  2302.                     dmg = modifier[2]
  2303.                     heading = modifier[3] or heading
  2304.                 end
  2305.             end
  2306.         end
  2307.     end
  2308.  
  2309.     dmg = math.ceil(dmg)
  2310.  
  2311.     if dmg >= 0 then
  2312.         self.health = self.health - dmg
  2313.     else
  2314.         -- absorb
  2315.         self.health = math.min(self.health - dmg, self:getMaxHealth())
  2316.     end
  2317.  
  2318.     -- play get hit effects
  2319.     if bit.band(damageFlags, DamageFlags.Impact) ~= 0 then
  2320.         -- play get hit animation?
  2321.         if dmg > 0 and self.currentAction == nil then
  2322.             local anim
  2323.             if side == "front_left" then
  2324.                 anim = "getHitFrontLeft"
  2325.             elseif side == "front_right" then
  2326.                 anim = "getHitFrontRight"
  2327.             elseif side == "left" then
  2328.                 anim = "getHitLeft"
  2329.             elseif side == "right" then
  2330.                 anim = "getHitRight"
  2331.             elseif side == "back" then
  2332.                 anim = "getHitBack"
  2333.             else
  2334.                 error("invalid get hit side")
  2335.             end
  2336.            
  2337.             -- create get hit action
  2338.             if not self.go.damaged then
  2339.                 self.go:createComponent(MonsterActionComponent, "damaged")
  2340.             end
  2341.            
  2342.             self.go.damaged:setAnimation(anim)
  2343.             self:performAction("damaged")
  2344.         end
  2345.  
  2346.         if self:hasCondition("frozen") then
  2347.             self.go:playSound("ice_hit")
  2348.         else
  2349.             if self.hitSound then self.go:playSound(self.hitSound) end
  2350.         end
  2351.     end
  2352.    
  2353.     -- show damage in hud
  2354.     local color
  2355.     if heading then
  2356.         color = Color.White
  2357.     else
  2358.         color = Color.Grey
  2359.     end
  2360.  
  2361.     if resist == "weak" or resist == "vulnerable" then color = Color.Red end
  2362.  
  2363.     local showDamage = true
  2364.     if dmg == 0 and bit.band(damageFlags, DamageFlags.OngoingDamage) ~= 0 then showDamage = false end
  2365.     if showDamage then
  2366.         if dmg >= 0 and dmg < 9999 then
  2367.             self:showDamageText(dmg, color, heading)
  2368.         elseif dmg < 0 then
  2369.             self:showDamageText(-dmg, {0,200,0,255}, heading)
  2370.         else
  2371.             self:showDamageText("", Color.Red, heading)
  2372.         end
  2373.     end
  2374.  
  2375.     if self.health > 0 then
  2376.         if dmg > 0 then self.go:sendMessage("onMonsterDamaged", dmg, damageType) end
  2377.        
  2378.         -- play get hit particle effect
  2379.         if bit.band(damageFlags, DamageFlags.Impact) ~= 0 and bit.band(damageFlags, DamageFlags.NoLingeringEffects) == 0 then
  2380.             if dmg > 0 then
  2381.                 if damageType == "physical" then
  2382.                     self:playHitEffect(impactPos)
  2383.                 elseif damageType == "fire" then
  2384.                     self:playParticleEffect("damage_fire")
  2385.                 elseif damageType == "shock" then
  2386.                     self:playParticleEffect("damage_shock")
  2387.                 end
  2388.             end
  2389.         end
  2390.     else
  2391.         -- freeze monster if death from cold
  2392.         if not self:hasCondition("frozen") and damageType == "cold" then
  2393.             self:setCondition("frozen", 1)
  2394.         end
  2395.        
  2396.         local gainExp = false
  2397.         local anyChampion = DamageFlags.Champion1 + DamageFlags.Champion2 + DamageFlags.Champion3 + DamageFlags.Champion4
  2398.         local gainExp = bit.band(damageFlags, anyChampion) ~= 0
  2399.  
  2400.         if self:getHealth() - damage <= 0 then
  2401.             self:deathTriggers(champion, weapon, attack)
  2402.         end
  2403.        
  2404.         self:die(gainExp)
  2405.     end
  2406. end
  2407.  
  2408. function MonsterComponent:deathTriggers(champion, weapon, attack)
  2409.     if not champion then return false end
  2410.     -- Death triggers here
  2411. end
  2412.  
  2413. -------------------------------------------------------------------------------------------------------
  2414. -- Combat Functions                                                                                  --    
  2415. -------------------------------------------------------------------------------------------------------
  2416.  
  2417. function computeDamage(attackPower, modifier, variation)
  2418.     -- Updated in order to be able to affect the damage variation
  2419.     local min,max = getDamageRange(attackPower, modifier, variation)
  2420.     return math.max(math.random(min, max), 0)
  2421. end
  2422.  
  2423. -- Returns min and max damage.
  2424. function getDamageRange(attackPower, modifier, variation)
  2425.     -- Updated in order to be able to affect the damage variation and to directly affect the damage modifier min/max
  2426.     variation = variation or 0.5
  2427.     modifier = modifier or {0,0}
  2428.    
  2429.     if attackPower == 0 then return modifier[1],modifier[2] end
  2430.    
  2431.     if attackPower then
  2432.         local power = attackPower
  2433.         local min = math.max(math.floor( (power - power * variation) + (modifier[1] or 0) ), 1)
  2434.         local max = math.max(math.floor( (power + power * variation) + (modifier[2] or 0) ), 1)
  2435.        
  2436.         local minModded = math.clamp(min, 1, math.max(max - 1, 1))
  2437.         min = minModded
  2438.         local maxModded = math.clamp(max, min + 1, 999)
  2439.        
  2440.         return minModded,maxModded
  2441.     end
  2442. end
  2443.  
  2444. -- Damages all creatures in a tile
  2445. function damageTile(map, x, y, direction, elevation, damageFlags, damageType, power, screenEffect, hitCallback, hitContext)
  2446.     -- hit monster
  2447.     for _,monster in map:componentsAt(MonsterComponent, x, y) do
  2448.         local ent = monster.go
  2449.         if ent.elevation == elevation and not monster:getMonsterFlag(MonsterFlag.NonMaterial) and power and power > 0 then
  2450.             -- check temporal ice shards immunity
  2451.             local immune = bit.band(damageFlags, DamageFlags.DamageSourceIceShards) ~= 0 and monster.iceShardsImmunityTimer
  2452.             if not immune then
  2453.                 local side = computeSide(direction, ent.facing)
  2454.                 local dmg = computeDamage(power)
  2455.                 if prob(0.08) then dmg = dmg * 2 end
  2456.                
  2457.                 -- compute approximate impact position
  2458.                 local impactPos
  2459.                 if bit.band(damageFlags, DamageFlags.Impact) ~= 0 then
  2460.                     local dx,dy = getDxDy(direction)
  2461.                     local pos = map:mapToWorld(x + dx, y + dy)
  2462.                     impactPos = monster:findImpactPosition(pos)
  2463.                 end
  2464.  
  2465.                 local status
  2466.                 if hitCallback then status = hitCallback(hitContext, "monster", monster, dmg, damageType) end
  2467.                
  2468.                 if status ~= false then
  2469.                     monster:damage(dmg, side, damageFlags, damageType, impactPos, heading, hitContext)
  2470.                 end
  2471.             end
  2472.         end
  2473.     end
  2474.  
  2475.     -- hit obstacle
  2476.     -- avoid hitting just spawned obstacles in reaction to getting obstacle destroyed
  2477.     local obstacles
  2478.     for _,obstacle in map:componentsAt(ObstacleComponent, x, y) do
  2479.         if obstacle.go.elevation == elevation then
  2480.             if obstacles == nil then obstacles = {} end
  2481.             obstacles[#obstacles+1] = obstacle
  2482.         end
  2483.     end
  2484.  
  2485.     if obstacles then
  2486.         for i=1, #obstacles do
  2487.             local obstacle = obstacles[i]
  2488.             local dmg = computeDamage(power)
  2489.             local status
  2490.             if hitCallback then status = hitCallback(hitContext, "obstacle", obstacle, dmg, damageType) end
  2491.             if status ~= false and obstacle.go:sendMessage("onDamage", dmg, damageType) then
  2492.                 obstacle:playHitEffects()
  2493.             end
  2494.         end
  2495.     end
  2496.  
  2497.     -- hit party?
  2498.     for _,party in map:componentsAt(PartyComponent, x, y) do
  2499.         if party.go.elevation == elevation then
  2500.             -- check temporal ice shards immunity
  2501.             local immune = bit.band(damageFlags, DamageFlags.DamageSourceIceShards) ~= 0 and party.iceShardsImmunityTimer
  2502.             if not immune then
  2503.                 local partyHit = false
  2504.                
  2505.                 for i=1,4 do
  2506.                     local dmg = 0
  2507.                     if power and power > 0 then dmg = computeDamage(power) end
  2508.                    
  2509.                     -- halve damage for backrow
  2510.                     if damageFlags and bit.band(damageFlags, DamageFlags.HalveBackRowDamage) ~= 0 and checkPartyCover(i, direction) then
  2511.                         dmg = math.floor(dmg / 2)
  2512.                     end
  2513.                    
  2514.                     local status
  2515.                     if hitCallback then status = hitCallback(hitContext, "champion", party.champions[i], dmg, damageType) end
  2516.  
  2517.                     if status ~= false then
  2518.                         partyHit = true
  2519.                         if dmg > 0 then party.champions[i]:damage(dmg, damageType, hitContext) end
  2520.                     end
  2521.                 end
  2522.                
  2523.                 if partyHit then
  2524.                     party:wakeUp(true)
  2525.                    
  2526.                     if screenEffect then
  2527.                         party:playScreenEffect(screenEffect)
  2528.                     end
  2529.  
  2530.                     if bit.band(damageFlags, DamageFlags.CameraShake) ~= 0 then
  2531.                         party:shakeCamera(0.5, 0.3)
  2532.                     end
  2533.                 end
  2534.             end
  2535.         end
  2536.     end
  2537. end
  2538.  
  2539. -------------------------------------------------------------------------------------------------------
  2540. -- Item Functions                                                                                    --    
  2541. -------------------------------------------------------------------------------------------------------
  2542.  
  2543. function ItemComponent:dragItemToThrowZone(x, y)
  2544.     local champion = gameMode:getActiveChampion()
  2545.     if champion then   
  2546.         local vx,vy,vw,vh = gameMode:getViewport()
  2547.         local side = iff(x < vw/2, 0, 1)
  2548.         local origin = party.champions[side+1]:getChampionPositionInWorld(0.4)
  2549.        
  2550.         -- push forward so that item won't collide against a door behind us
  2551.         origin = origin + party:getWorldForward() * self:getBoundingRadius()
  2552.        
  2553.         local power = math.max(14 - self.weight, 10)
  2554.         local gravity = math.clamp(2 + self.weight*1.5, 4, 10)
  2555.         local velocityUp = 0
  2556.         local weapon = nil
  2557.         local attack = nil
  2558.  
  2559.         if self:hasTrait("throwing_weapon") then
  2560.             power = 14
  2561.             gravity = 1
  2562.             velocityUp = 0
  2563.             weapon = self
  2564.             attack = self.go.throwattack
  2565.         end
  2566.  
  2567.         self:throw(party, origin, party.go.facing, power, gravity, velocityUp)
  2568.         self.thrownByChampion = champion.ordinal
  2569.         -- store original weapon and attack data in projectile
  2570.         self.thrownByWeapon = weapon
  2571.         self.thrownByAttack = attack
  2572.         champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], 1)
  2573.         champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], 1)
  2574.         soundSystem:playSound2D("swipe")
  2575.         return true
  2576.     end
  2577. end
  2578.  
  2579. function ItemComponent:projectileHitEntity(target) 
  2580.     -- Updated for extra hook goodness
  2581.     -- compute damage
  2582.     local dmg = self.projectileDamage or math.random(1,3)
  2583.     local damageType = self.projectileDamageType or "physical"
  2584.     local pierce = self.projectilePierce or 0
  2585.     local accuracy = self.projectileAccuracy or 0
  2586.     local critChance = self.projectileCritChance or 5
  2587.     local weapon = self.thrownByWeapon
  2588.     local attack = self.thrownByAttack
  2589.  
  2590.     local thrownByChampion
  2591.     if self.thrownByChampion then
  2592.         thrownByChampion = party:getChampionByOrdinal(self.thrownByChampion)
  2593.     end
  2594.    
  2595.     -- crits & fumbles
  2596.     local crit = false
  2597.     if math.random() < critChance/100 then
  2598.         if dmg > 0 then
  2599.             crit = true
  2600.         end
  2601.     elseif math.random() < 0.1 then
  2602.         dmg = math.floor(dmg / 2)
  2603.     end
  2604.     local heading = nil
  2605.     if crit then heading = "Critical" end
  2606.        
  2607.     if target.monster then
  2608.         local target = target.monster
  2609.  
  2610.         -- evasion
  2611.         if not target:hasCondition("sleep") and not target:hasCondition("frozen") then
  2612.             local tohit = thrownByChampion:getToHitChanceWithAttack(weapon, attack, target, accuracy, damageType)
  2613.            
  2614.             if math.random() > tohit / 100 or target.evasion >= 1000 then
  2615.                 target:showDamageText("miss", Color.Grey)
  2616.                 if thrownByChampion then
  2617.                     thrownByChampion.luck = math.min(thrownByChampion.luck + 3, 15)
  2618.                 end
  2619.                 self.go:playSound("impact_blunt")
  2620.                 return "miss"
  2621.             end
  2622.  
  2623.             if thrownByChampion then thrownByChampion.luck = 0 end
  2624.         end
  2625.        
  2626.         if crit then
  2627.             local critMult = champion:getCritMultiplierWithAttack(weapon, attack, target)
  2628.             if champion:getSecondaryAction(slot) == attack then critMult = 1.5 end
  2629.             if attack.getCritMultiplier then critMult = critMult + (attack:getCritMultiplier()-1) end
  2630.             dmg = dmg * critMult
  2631.             if dmg <= 0 then
  2632.                 crit = false
  2633.             end
  2634.         end
  2635.  
  2636.         -- damage reduction
  2637.         local protection = target:getMonsterProtectionWithAttack(champion, weapon, attack, dmg, damageType, crit, nil, projectile)
  2638.  
  2639.         if protection > 0 then dmg = computeDamageReduction(dmg, protection) end
  2640.  
  2641.         -- hit monster
  2642.         local side = "front_left" -- TODO: compute side
  2643.         local impactPos = target:findImpactPosition(self.go:getWorldPosition())
  2644.         local damageFlags = DamageFlags.Impact
  2645.        
  2646.         if self.thrownByChampion then damageFlags = damageFlags + bit.lshift(DamageFlags.Champion1, self.thrownByChampion-1) end
  2647.        
  2648.         local returnValue = target:callHook("onProjectileHit", objectToProxy(self), objectToProxy(thrownByChampion), objectToProxy(self.thrownByWeapon), objectToProxy(self.thrownByAttack), dmg, damageType, heading, crit)
  2649.         if returnValue then
  2650.             if returnValue[1] == false then return end
  2651.             dmg = returnValue[2] or dmg
  2652.             damageType = returnValue[3] or damageType
  2653.             heading = returnValue[4] or heading
  2654.         end
  2655.        
  2656.         target:damage(dmg, side, damageFlags, damageType, impactPos, heading)
  2657.  
  2658.         self:callHook("onThrowAttackHitMonster", objectToProxy(target))
  2659.  
  2660.         target:hitTriggers(thrownByChampion, self.thrownByWeapon, self.thrownByAttack)
  2661.                
  2662.         -- stick projectile into monster
  2663.         if target:isAlive() then
  2664.             local sharp = self:getItemFlag(ItemFlag.SharpProjectile)
  2665.             local fragile = self:getItemFlag(ItemFlag.FragileProjectile)
  2666.            
  2667.             if self.projectileDamage and sharp and not fragile then
  2668.                 self.go.map:removeEntity(self.go)
  2669.                 if self.convertToItemOnImpact then self.convertItem = true end
  2670.                 target:addItem(self)
  2671.             end
  2672.         end
  2673.  
  2674.         if not target:isAlive() and self:hasTrait("leg_armor") then
  2675.             steamContext:unlockAchievement("full_monty")
  2676.         end
  2677.     elseif target.obstacle then
  2678.         -- hit obstacle
  2679.         target:sendMessage("onDamage", dmg)
  2680.         target.obstacle:playHitEffects()
  2681.     elseif target.party then
  2682.         -- hit party
  2683.         local target = party:getAttackTarget((self.go.facing+2) % 4, math.random(0,1))
  2684.         if target then
  2685.             if party:isHookRegistered("onProjectileHit") then
  2686.                 if party:callHook("onProjectileHit", objectToProxy(target), objectToProxy(self), dmg, damageType) == false then
  2687.                     return
  2688.                 end
  2689.             end
  2690.  
  2691.             soundSystem:playSound2D("projectile_hit_party")
  2692.             party:wakeUp(true)
  2693.             target:damage(dmg, damageType)
  2694.  
  2695.             -- HACK: hard code medusa arrow, 20% chance to petrify
  2696.             if self.go.arch.name == "petrifying_arrow" then
  2697.                 local petrified = math.random() < 0.2
  2698.                 if petrified and target:isAlive() and not target:hasCondition("petrified") then
  2699.                     target:setCondition("petrified")
  2700.                 end
  2701.             end
  2702.  
  2703.             if target:isAlive() then target:playDamageSound() end
  2704.         end
  2705.     end
  2706. end
  2707.  
  2708. -------------------------------------------------------------------------------------------------------
  2709. -- EquipmentItem Functions                                                                           --    
  2710. -------------------------------------------------------------------------------------------------------
  2711.  
  2712. defineProxyClass{
  2713.     class = "EquipmentItemComponent",
  2714.     baseClass = "Component",
  2715.     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.",
  2716.     methods = {
  2717.         { "setSlot", "number" },
  2718.         { "setStrength", "number" },
  2719.         { "setDexterity", "number" },
  2720.         { "setVitality", "number" },
  2721.         { "setWillpower", "number" },
  2722.         { "setProtection", "number" },
  2723.         { "setEvasion", "number" },
  2724.         { "setResistFire", "number" },
  2725.         { "setResistCold", "number" },
  2726.         { "setResistShock", "number" },
  2727.         { "setResistPoison", "number" },
  2728.         { "setHealth", "number" },
  2729.         { "setEnergy", "number" },
  2730.         { "setExpRate", "number" },
  2731.         { "setFoodRate", "number" },
  2732.         { "setHealthRegenerationRate", "number" },
  2733.         { "setEnergyRegenerationRate", "number" },
  2734.         { "setCooldownRate", "number" },
  2735.         { "setAccuracy", "number" },
  2736.         { "setDamage", "number" },
  2737.         { "setCriticalChance", "number" },
  2738.         { "setCriticalMultiplier", "number" },
  2739.         { "setDualWielding", "number" },
  2740.         { "setPierceOnCrit", "number" },
  2741.         { "getSlot" },
  2742.         { "getStrength" },
  2743.         { "getDexterity" },
  2744.         { "getVitality" },     
  2745.         { "getWillpower" },
  2746.         { "getProtection" },
  2747.         { "getEvasion" },
  2748.         { "getResistFire" },
  2749.         { "getResistCold" },
  2750.         { "getResistShock" },
  2751.         { "getResistPoison" },
  2752.         { "getHealth" },
  2753.         { "getEnergy" },
  2754.         { "getExpRate" },
  2755.         { "getFoodRate" },
  2756.         { "getHealthRegenerationRate" },
  2757.         { "getEnergyRegenerationRate" },
  2758.         { "getCooldownRate" },
  2759.         { "getAccuracy" },
  2760.         { "getDamage" },
  2761.         { "getCriticalChance" },
  2762.         { "getCriticalMultiplier" },
  2763.         { "getDualWielding" },
  2764.         { "getPierceOnCrit" },
  2765.         { "getMinDamageMod" },
  2766.         { "getMaxDamageMod" },
  2767.         { "setMinDamageMod", "number" },
  2768.         { "setMaxDamageMod", "number" },
  2769.         { "getBlockChance" },
  2770.         { "getBlockThreshhold" },
  2771.         { "setBlockChance", "number" },
  2772.         { "setBlockThreshhold", "number" },
  2773.     },
  2774.     hooks = {
  2775.         "onRecomputeStats(self, champion)",
  2776.         "onRecomputeFinalStats(self, champion)",
  2777.         "onComputeAccuracy(self, champion, weapon, attack, monster, attackType)",
  2778.         "onComputeCritChance(self, champion, weapon, attack, monster, attackType)",
  2779.         "onComputeDamageModifier(self, champion, weapon, attack, attackType)",
  2780.         "onCheckDualWielding(self, champion, weapon1, weapon2)",
  2781.         "onComputeBackstabMultiplier(self, champion, weapon, attack)",
  2782.         "onComputeCritMultiplier(self, champion, weapon, attack, monster)",
  2783.         "onComputeChampionAttackDamage(self, monster, champion, weapon, attack, dmg, damageType, crit, backstab)",
  2784.         "onComputePierce(self, monster, champion, weapon, attack, projectile, dmg, damageType, crit, backstab)",
  2785.         "onComputeSpellCost(self, champion, name, cost, skill)",
  2786.         "onComputeSpellCooldown(self, champion, name, cost, skill)",
  2787.         "onComputeSpellDamage(self, champion, spell, name, cost, skill)",
  2788.         "onComputeBombPower(self, bombItem, champion, power)",
  2789.         "onComputeBearFormDuration(self, champion)",
  2790.         "onComputeDamageTaken(self, champion, attack, dmg, dmgType, isSpell)",
  2791.         "onComputeMalfunctionChance(self, champion, weapon, attack, attackType)",
  2792.         "onComputeRange(self, champion, weapon, attack, attackType)",
  2793.     },
  2794. }
  2795.  
  2796. extendProxyClass(EquipmentItemComponent, "critMultiplier")
  2797. extendProxyClass(EquipmentItemComponent, "dualWielding")
  2798. extendProxyClass(EquipmentItemComponent, "minDamageMod")
  2799. extendProxyClass(EquipmentItemComponent, "maxDamageMod")
  2800.  
  2801. -- local oldRecomputeStats = EquipmentItemComponent.recomputeStats
  2802. function EquipmentItemComponent:recomputeStats(champion, slot)
  2803.     -- oldRecomputeStats(self,champion,slot)
  2804.  
  2805.     -- called at the beginning of each frame, updates champions stats
  2806.     if not self.enabled then return end
  2807.    
  2808.     if self:isEquipped(champion, slot) then
  2809.         local stats = champion.stats
  2810.         stats.strength.current = stats.strength.current + (self.strength or 0)
  2811.         stats.dexterity.current = stats.dexterity.current + (self.dexterity or 0)
  2812.         stats.vitality.current = stats.vitality.current + (self.vitality or 0)
  2813.         stats.willpower.current = stats.willpower.current + (self.willpower or 0)
  2814.         stats.protection.current = stats.protection.current + (self.protection or 0)
  2815.         stats.evasion.current = stats.evasion.current + (self.evasion or 0)
  2816.         stats.resist_fire.current = stats.resist_fire.current + (self.resistFire or 0) + (self.resistAll or 0)
  2817.         stats.resist_cold.current = stats.resist_cold.current + (self.resistCold or 0) + (self.resistAll or 0)
  2818.         stats.resist_shock.current = stats.resist_shock.current + (self.resistShock or 0) + (self.resistAll or 0)
  2819.         stats.resist_poison.current = stats.resist_poison.current + (self.resistPoison or 0) + (self.resistAll or 0)
  2820.         stats.max_health.current = stats.max_health.current + (self.health or 0)
  2821.         stats.max_energy.current = stats.max_energy.current + (self.energy or 0)
  2822.         stats.exp_rate.current = stats.exp_rate.current + (self.expRate or 0)
  2823.         stats.food_rate.current = stats.food_rate.current + (self.foodRate or 0)
  2824.         stats.health_regeneration_rate.current = stats.health_regeneration_rate.current + (self.healthRegenerationRate or 0)
  2825.         stats.energy_regeneration_rate.current = stats.energy_regeneration_rate.current + (self.energyRegenerationRate or 0)
  2826.         stats.cooldown_rate.current = stats.cooldown_rate.current + (self.cooldownRate or 0)
  2827.         stats.critical_multiplier.current = stats.critical_multiplier.current + (self.critMultiplier or 0)
  2828.         stats.dual_wielding.current = stats.dual_wielding.current + (self.dualWielding or 0)
  2829.  
  2830.         for name,skill in pairs(dungeon.skills) do
  2831.             if skill.onComputeItemStats then
  2832.                 skill.onComputeItemStats(objectToProxy(self), objectToProxy(champion), slot, champion:getSkillLevel(name))
  2833.             end
  2834.         end
  2835.    
  2836.         for name,trait in pairs(dungeon.traits) do
  2837.             if trait.onComputeItemStats then
  2838.                 trait.onComputeItemStats(objectToProxy(self), objectToProxy(champion), slot, iff(champion:hasTrait(name), 1, 0))
  2839.             end
  2840.         end
  2841.     end
  2842.  
  2843.     self:callHook("onRecomputeStats", objectToProxy(champion))
  2844. end
  2845.  
  2846. function EquipmentItemComponent:onComputeAccuracy(champion, weapon, attack, attackType, monster)
  2847.     if self.enabled then
  2848.         local modifier = self:callHook("onComputeAccuracy", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), objectToProxy(monster))
  2849.         modifier = modifier or 0
  2850.         if self.accuracy then modifier = modifier + self.accuracy end
  2851.         return modifier
  2852.     end
  2853. end
  2854.  
  2855. function EquipmentItemComponent:onComputeCritChance(champion, weapon, attack, monster)
  2856.     if self.enabled then
  2857.         local modifier = self:callHook("onComputeCritChance", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType(), objectToProxy(monster))
  2858.         modifier = modifier or 0
  2859.         if self.criticalChance then modifier = modifier + self.criticalChance end
  2860.         return modifier
  2861.     end
  2862. end
  2863.  
  2864. function EquipmentItemComponent:onComputeDamageModifier(champion, weapon, attack)
  2865.     if self.enabled then
  2866.         local modifier = self:callHook("onComputeDamageModifier", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType())
  2867.         modifier = (modifier and type(modifier) == "table") and modifier or modifier and { modifier/2, modifier*2 } or {0,0}
  2868.         if self.minDamageMod then modifier[1] = modifier[1] + self.minDamageMod end
  2869.         if self.maxDamageMod then modifier[2] = modifier[2] + self.maxDamageMod end
  2870.         return modifier
  2871.     end
  2872. end
  2873.  
  2874. function EquipmentItemComponent:onCheckDualWielding(champion, weapon1, weapon2)
  2875.     if self.enabled then
  2876.         local modifier = self:callHook("onCheckDualWielding", objectToProxy(champion), objectToProxy(weapon1), objectToProxy(weapon2))
  2877.         return modifier
  2878.     end
  2879. end
  2880.  
  2881. function EquipmentItemComponent:onComputeBackstabMultiplier(champion, weapon, attack)
  2882.     if self.enabled then
  2883.         local modifier = self:callHook("onComputeBackstabMultiplier", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attack:getAttackType())
  2884.         return modifier
  2885.     end
  2886. end
  2887.  
  2888. function EquipmentItemComponent:onComputeCritMultiplier(champion, weapon, attack, monster)
  2889.     if self.enabled then
  2890.         local modifier = self:callHook("onComputeCritMultiplier", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(monster))
  2891.         return modifier
  2892.     end
  2893. end
  2894.  
  2895. function EquipmentItemComponent:onComputeChampionAttackDamage(monster, champion, weapon, attack, dmg, damageType, crit, backstab)
  2896.     if self.enabled then
  2897.         local modifier = self:callHook("onComputeChampionAttackDamage", objectToProxy(monster), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), dmg, damageType, crit, backstab)
  2898.         return modifier
  2899.     end
  2900. end
  2901.  
  2902. function EquipmentItemComponent:onComputePierce(monster, champion, weapon, attack, projectile, dmg, damageType, attackType, crit, backstab)
  2903.     if self.enabled then
  2904.         local modifier = self:callHook("onComputePierce", objectToProxy(monster), objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), objectToProxy(projectile), dmg, damageType, attack:getAttackType(), crit, backstab)
  2905.         return modifier
  2906.     end
  2907. end
  2908.  
  2909. function EquipmentItemComponent:onRecomputeFinalStats(champion)
  2910.     if self.enabled then
  2911.         local modifier = self:callHook("onRecomputeFinalStats", objectToProxy(champion))
  2912.         return modifier
  2913.     end
  2914. end
  2915.  
  2916. function EquipmentItemComponent:onComputeSpellCost(champion, name, cost, skill)
  2917.     if self.enabled then
  2918.         local modifier = self:callHook("onComputeSpellCost", objectToProxy(champion), name, cost, skill)
  2919.         return modifier
  2920.     end
  2921. end
  2922.  
  2923. function EquipmentItemComponent:onComputeSpellCooldown(champion, name, cost, skill)
  2924.     if self.enabled then
  2925.         local modifier = self:callHook("onComputeSpellCooldown", objectToProxy(champion), name, cost, skill)
  2926.         return modifier
  2927.     end
  2928. end
  2929.  
  2930. function EquipmentItemComponent:onComputeSpellDamage(champion, spellObject, name, cost, skill)
  2931.     if self.enabled then
  2932.         local modifier = self:callHook("onComputeSpellDamage", objectToProxy(champion), objectToProxy(spellObject), name, cost, skill)
  2933.         return modifier
  2934.     end
  2935. end
  2936.  
  2937. function EquipmentItemComponent:onComputeBombPower(champion, power)
  2938.     if self.enabled then
  2939.         local modifier = self:callHook("onComputeBombPower", objectToProxy(bombItem), objectToProxy(champion), power)
  2940.         return modifier
  2941.     end
  2942. end
  2943.  
  2944. function EquipmentItemComponent:onComputeBearFormDuration(champion)
  2945.     if self.enabled then
  2946.         local modifier = self:callHook("onComputeBearFormDuration", objectToProxy(champion))
  2947.         return modifier
  2948.     end
  2949. end
  2950.  
  2951. -- Modifies damage taken by champion
  2952. function EquipmentItemComponent:onComputeDamageTaken(self, champion, attack, dmg, dmgType, isSpell)
  2953.     if self.enabled then
  2954.         local modifier = self:callHook("onComputeDamageTaken", objectToProxy(champion), objectToProxy(hitContext), dmg, dmgType, isSpell)
  2955.         return modifier
  2956.     end
  2957. end
  2958.  
  2959. function EquipmentItemComponent:onComputeMalfunctionChance(self, champion, weapon, attack, attackType)
  2960.     if self.enabled then
  2961.         local modifier = self:callHook("onComputeMalfunctionChance", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attackType)
  2962.         return modifier
  2963.     end
  2964. end
  2965.  
  2966. function EquipmentItemComponent:onComputeRange(self, champion, weapon, attack, attackType)
  2967.     if self.enabled then
  2968.         local modifier = self:callHook("onComputeRange", objectToProxy(champion), objectToProxy(weapon), objectToProxy(attack), attackType)
  2969.         return modifier
  2970.     end
  2971. end
  2972.  
  2973. -------------------------------------------------------------------------------------------------------
  2974. -- MeleeAttack Functions                                                                             --
  2975. -------------------------------------------------------------------------------------------------------
  2976.  
  2977. defineProxyClass{
  2978.     class = "MeleeAttackComponent",
  2979.     baseClass = "ItemActionComponent",
  2980.     description = "Implements melee attack action for items. Melee attacks can hit and damage a single target in front of the party.",
  2981.     methods = {
  2982.         { "setAttackPower", "number" },
  2983.         { "setAccuracy", "number" },
  2984.         { "setCooldown", "number" },
  2985.         { "setSwipe", "string" },
  2986.         { "setAttackSound", "string" },
  2987.         { "setDamageType", "string" },
  2988.         { "setReachWeapon", "boolean" },
  2989.         { "setSkill", "string" },
  2990.         { "setRequiredLevel", "number" },
  2991.         { "setBaseDamageStat", "string" },
  2992.         { "setCauseCondition", "string" },
  2993.         { "setConditionChance", "number" },
  2994.         { "setPierce", "number" },
  2995.         { "setUnarmedAttack", "boolean" },
  2996.         { "setCameraShake", "boolean" },
  2997.         { "getAttackPower" },
  2998.         { "getAccuracy" },
  2999.         { "getCooldown" },
  3000.         { "getSwipe" },
  3001.         { "getAttackSound" },
  3002.         { "getDamageType" },
  3003.         { "getReachWeapon" },
  3004.         { "getSkill" },
  3005.         { "getRequiredLevel" },
  3006.         { "getBaseDamageStat" },
  3007.         { "getCauseCondition" },
  3008.         { "getConditionChance" },
  3009.         { "getPierce" },
  3010.         { "getUnarmedAttack" },
  3011.         { "getCameraShake" },
  3012.         { "setBaseDamageMultiplier", "number" },
  3013.         { "getBaseDamageMultiplier" },
  3014.         { "getMinDamageMod" },
  3015.         { "getMaxDamageMod" },
  3016.         { "setMinDamageMod", "number" },
  3017.         { "setMaxDamageMod", "number" },
  3018.         { "setJamChance", "number" },
  3019.         { "getJamChance" },
  3020.         { "setJammed", "boolean" },
  3021.         { "getJammed" },
  3022.     },
  3023.     hooks = {
  3024.         "onPostAttack(self, champion, slot)",
  3025.         "onHitMonster(self, monster, tside, damage, champion, crit, backstab)",
  3026.     },
  3027. }
  3028.  
  3029. extendProxyClass(MeleeAttackComponent, "pierce")
  3030. extendProxyClass(MeleeAttackComponent, "attackPowerVariation")
  3031. extendProxyClass(MeleeAttackComponent, "baseDamageMultiplier")
  3032. extendProxyClass(MeleeAttackComponent, "minDamageMod")
  3033. extendProxyClass(MeleeAttackComponent, "maxDamageMod")
  3034. extendProxyClass(MeleeAttackComponent, "jamChance")
  3035. extendProxyClass(MeleeAttackComponent, "jamCount")
  3036. extendProxyClass(MeleeAttackComponent, "jamText")
  3037. extendProxyClass(MeleeAttackComponent, "velocity")
  3038.  
  3039. function MeleeAttackComponent:start(champion, slot, chainIndex)
  3040.     local weapon = champion:getItem(slot)
  3041.     if champion:hasCondition("bear_form") then weapon = nil end
  3042.    
  3043.     if not champion:getDamage(slot) then
  3044.         console:warn("no attack power")
  3045.         return
  3046.     end
  3047.  
  3048.     if self.unarmedAttack then
  3049.         party.go.statistics:increaseStat("unarmed_attacks", 1)
  3050.     else
  3051.         party.go.statistics:increaseStat("melee_attacks", 1)
  3052.     end
  3053.    
  3054.     -- Melee weapons could jam if you wanted to
  3055.     local malfunctionModifier = 1
  3056.     -- traits modifiers
  3057.     for name,trait in pairs(dungeon.traits) do
  3058.         if trait.onComputeMalfunctionChance then
  3059.             local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", iff(champion:hasTrait(name), 1, 0))
  3060.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3061.         end
  3062.     end
  3063.  
  3064.     -- skill modifiers
  3065.     for name,skill in pairs(dungeon.skills) do
  3066.         if skill.onComputeMalfunctionChance then
  3067.             local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", champion:getSkillLevel(name))
  3068.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3069.         end
  3070.     end
  3071.  
  3072.     -- equipment modifiers (equipped items only)
  3073.     for i=1,ItemSlot.BackpackFirst-1 do
  3074.         local it = champion:getItem(i)
  3075.         if it then
  3076.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  3077.                 for i=1,it.go.components.length do
  3078.                     local comp = it.go.components[i]
  3079.                     if comp.onComputeMalfunctionChance then
  3080.                         local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "melee")
  3081.                         malfunctionModifier = malfunctionModifier * (modifier or 1)
  3082.                     end
  3083.                 end
  3084.             end
  3085.         end
  3086.     end
  3087.  
  3088.     local dualWield = champion:isDualWielding()
  3089.  
  3090.     -- jam
  3091.     if self.jamChance then
  3092.         local chance = self.jamChance * malfunctionModifier
  3093.         if not weapon:getJammed() and math.random(1, 100) <= chance then
  3094.             weapon:setJammed(true)
  3095.             weapon.jamCount = math.random(2, 6)
  3096.         end
  3097.     end
  3098.    
  3099.     -- fix jammed weapon
  3100.     if weapon and weapon:getJammed() then
  3101.         weapon.jamCount = (weapon.jamCount or 3) - 1
  3102.         if weapon.jamCount < 0 then
  3103.             weapon:setJammed(false)
  3104.             weapon.jamCount = nil
  3105.         end
  3106.     end
  3107.  
  3108.     if weapon and weapon:getJammed() then
  3109.         soundSystem:playSound2D("firearm_jammed")
  3110.         champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
  3111.         return
  3112.     end
  3113.  
  3114.     -- consume food
  3115.     champion:consumeFood(0.5 + math.random() * 2)
  3116.  
  3117.     -- cooldown
  3118.     local cooldown = self.cooldown or 0
  3119.     for name,skill in pairs(dungeon.skills) do
  3120.         if skill.onComputeCooldown then
  3121.             local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", champion:getSkillLevel(name))
  3122.             cooldown = cooldown * (modifier or 1)
  3123.         end
  3124.     end
  3125.     for name,trait in pairs(dungeon.traits) do
  3126.         if trait.onComputeCooldown then
  3127.             local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "melee", iff(champion:hasTrait(name), 1, 0))
  3128.             cooldown = cooldown * (modifier or 1)
  3129.         end
  3130.     end
  3131.    
  3132.     if dualWield then
  3133.         if slot == ItemSlot.Weapon then
  3134.             champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3135.         else
  3136.             champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3137.         end
  3138.     else
  3139.         champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3140.         champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3141.     end
  3142.    
  3143.     party:endCondition("invisibility")
  3144.        
  3145.     -- play swipe effect
  3146.     if self.swipe then
  3147.         local swipe = self.swipe
  3148.         if swipe == "flurry" then
  3149.             swipe = iff(chainIndex == 1, "vertical", "horizontal")
  3150.         end
  3151.         local side = iff(champion.championIndex == 1 or champion.championIndex == 3, 0, 1) 
  3152.         party.swipes[swipe]:play(side)
  3153.     end
  3154.    
  3155.     -- play sound
  3156.     if self.attackSound then
  3157.         champion:playSound(self.attackSound)
  3158.     end
  3159.  
  3160.     if self.cameraShake then
  3161.         party:shakeCamera(0.5, 0.3)
  3162.     end
  3163.  
  3164.     -- get source and target tiles for attack
  3165.     local x,y = party.go.map:worldToMap(party.go:getWorldPosition())
  3166.     local dx,dy = getDxDy(party.go.facing)
  3167.     local tx = x + dx
  3168.     local ty = y + dy
  3169.    
  3170.     local map = party.go.map
  3171.  
  3172.     local dualWieldSide = iff(dualWield, slot, nil)
  3173.  
  3174.     -- check reach
  3175.     if champion.championIndex == 3 or champion.championIndex == 4 then
  3176.         if not self.reachWeapon and not champion:hasTrait("reach") then
  3177.             champion:showAttackResult("Can't reach")
  3178.             return
  3179.         end
  3180.     end
  3181.  
  3182.     -- hit wall
  3183.     if map:isWall(tx,ty) or map:getElevation(tx,ty) > party.go.elevation or map:isForceField(x, y, party.go.elevation) then
  3184.         if self.unarmedAttack then
  3185.             soundSystem:playSound2D("impact_punch")
  3186.         else
  3187.             if self.go.item.impactSound then soundSystem:playSound2D(self.go.item.impactSound) end
  3188.         end
  3189.         champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
  3190.         return
  3191.     end
  3192.  
  3193.     -- hit door
  3194.     local door = map:findDoor(x, y, party.go.facing, party.go.elevation)
  3195.     if door and not door:isPassable() then
  3196.         if door:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
  3197.             return
  3198.         end
  3199.     end
  3200.  
  3201.     -- hit force field
  3202.     for _,f in map:componentsAt(ForceFieldComponent, tx, ty) do
  3203.         if f.enabled then
  3204.             if f:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
  3205.                 return
  3206.             end
  3207.         end
  3208.     end
  3209.  
  3210.     -- hit obstacle first (so that monsters inside obstacles get cover)
  3211.     for _,obstacle in map:componentsAt(ObstacleComponent, tx, ty) do
  3212.         if obstacle.enabled then
  3213.             if obstacle:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
  3214.                 return
  3215.             end
  3216.         end
  3217.     end
  3218.  
  3219.     -- hit monster
  3220.     for _,monster in map:componentsAt(MonsterComponent, tx, ty) do
  3221.         local fullHealth = monster:getHealth() == monster:getMaxHealth()
  3222.         if monster:onAttackedByChampion(champion, weapon, self, slot, dualWieldSide) then
  3223.             -- unstoppable achievement: kill monster with a single blow
  3224.             if fullHealth and not monster:isAlive() then
  3225.                 steamContext:unlockAchievement("unstoppable")
  3226.             end
  3227.             if not monster:isAlive() and not weapon then
  3228.                 steamContext:unlockAchievement("fist_fighter")
  3229.             end
  3230.             return
  3231.         end
  3232.     end
  3233.  
  3234.     champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
  3235.  
  3236.     -- HACK: self.go is nil for bear claw attacks
  3237.     if self.go then
  3238.         self:callHook("onPostAttack", objectToProxy(champion), slot)
  3239.     end
  3240. end
  3241.  
  3242. -------------------------------------------------------------------------------------------------------
  3243. -- RangedAttack Functions                                                                            --    
  3244. -------------------------------------------------------------------------------------------------------
  3245.  
  3246. defineProxyClass{
  3247.     class = "RangedAttackComponent",
  3248.     baseClass = "ItemActionComponent",
  3249.     description = "Implements missile attack action for items. Missile attacks require ammo that must be held in champion's other hand.",
  3250.     methods = {
  3251.         { "setAttackPower", "number" },
  3252.         { "setAttackPowerVariation", "number" },
  3253.         { "setCooldown", "number" },
  3254.         { "setSwipe", "string" },
  3255.         { "setAttackSound", "string" },
  3256.         { "setSkill", "string" },
  3257.         { "setRequiredLevel", "number" },
  3258.         { "setAmmo", "string" },
  3259.         { "setBaseDamageStat", "string" },
  3260.         { "setDamageType", "string" },
  3261.         { "setProjectileItem", "string" },
  3262.         { "getAttackPower" },
  3263.         { "getAttackPowerVariation" },
  3264.         { "getCooldown" },
  3265.         { "getSwipe" },
  3266.         { "getAttackSound" },
  3267.         { "getSkill" },
  3268.         { "getRequiredLevel" },
  3269.         { "getAmmo" },
  3270.         { "getBaseDamageStat" },
  3271.         { "getDamageType" },
  3272.         { "getProjectileItem" },
  3273.         { "setBaseDamageMultiplier", "number" },
  3274.         { "getBaseDamageMultiplier" },
  3275.         { "getMinDamageMod" },
  3276.         { "getMaxDamageMod" },
  3277.         { "setMinDamageMod", "number" },
  3278.         { "setMaxDamageMod", "number" },
  3279.         { "setPierce", "number" },
  3280.         { "getPierce" },
  3281.         { "setJamChance", "number" },
  3282.         { "getJamChance" },
  3283.         { "setJammed", "boolean" },
  3284.         { "getJammed" },
  3285.         { "setCritMultiplier", "number" },
  3286.         { "getCritMultiplier" },
  3287.     },
  3288.     hooks = {
  3289.         "onPostAttack(self, champion, slot)",
  3290.     },
  3291. }
  3292.  
  3293. extendProxyClass(RangedAttackComponent, "pierce")
  3294. extendProxyClass(RangedAttackComponent, "critMultiplier")
  3295. extendProxyClass(RangedAttackComponent, "attackPowerVariation")
  3296. extendProxyClass(RangedAttackComponent, "baseDamageMultiplier")
  3297. extendProxyClass(RangedAttackComponent, "minDamageMod")
  3298. extendProxyClass(RangedAttackComponent, "maxDamageMod")
  3299. extendProxyClass(RangedAttackComponent, "jamChance")
  3300. extendProxyClass(RangedAttackComponent, "jamCount")
  3301. extendProxyClass(RangedAttackComponent, "jamText")
  3302. extendProxyClass(RangedAttackComponent, "velocity")
  3303.  
  3304. local oldRangedAttackComponentInit = RangedAttackComponent.init
  3305. function RangedAttackComponent:init(go)
  3306.     oldRangedAttackComponentInit(self, go)
  3307.     self.pierce = 0
  3308.     self.jamChance = 0
  3309. end
  3310.  
  3311. function RangedAttackComponent:start(champion, slot)
  3312.     local weapon = champion:getItem(slot)
  3313.     local ammoSlot = nil
  3314.     -- check ammo
  3315.     if not self:checkAmmo(champion, slot) then
  3316.         champion:showAttackResult("No ammo")
  3317.         return
  3318.     end
  3319.  
  3320.     -- cooldown
  3321.     local cooldown = self.cooldown or 0
  3322.     for name,skill in pairs(dungeon.skills) do
  3323.         if skill.onComputeCooldown then
  3324.             local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", champion:getSkillLevel(name))
  3325.             cooldown = cooldown * (modifier or 1)
  3326.         end
  3327.     end
  3328.     for name,trait in pairs(dungeon.traits) do
  3329.         if trait.onComputeCooldown then
  3330.             local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", iff(champion:hasTrait(name), 1, 0))
  3331.             cooldown = cooldown * (modifier or 1)
  3332.         end
  3333.     end
  3334.  
  3335.     -- check dual wielding
  3336.     local dualWield = champion:isDualWielding()
  3337.     if dualWield then
  3338.         if slot == ItemSlot.Weapon then
  3339.             champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3340.         else
  3341.             champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3342.         end
  3343.     else
  3344.         champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3345.         champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3346.     end
  3347.     local dualWieldSide = iff(dualWield, slot, nil)
  3348.  
  3349.     -- check ammo
  3350.     if not (item and item.go.ammoitem and self.ammo == item.go.ammoitem:getAmmoType()) then
  3351.         champion:showAttackResult("No ammo", nil, dualWieldSide)
  3352.         return
  3353.     end
  3354.    
  3355.     -- Missile weapons could jam if you wanted to
  3356.     local malfunctionModifier = 1
  3357.     -- traits modifiers
  3358.     for name,trait in pairs(dungeon.traits) do
  3359.         if trait.onComputeMalfunctionChance then
  3360.             local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", iff(champion:hasTrait(name), 1, 0))
  3361.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3362.         end
  3363.     end
  3364.  
  3365.     -- skill modifiers
  3366.     for name,skill in pairs(dungeon.skills) do
  3367.         if skill.onComputeMalfunctionChance then
  3368.             local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "missile", champion:getSkillLevel(name))
  3369.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3370.         end
  3371.     end
  3372.  
  3373.     -- equipment modifiers (equipped items only)
  3374.     for i=1,ItemSlot.BackpackFirst-1 do
  3375.         local it = champion:getItem(i)
  3376.         if it then
  3377.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  3378.                 for i=1,it.go.components.length do
  3379.                     local comp = it.go.components[i]
  3380.                     if comp.onComputeMalfunctionChance then
  3381.                         local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "missile")
  3382.                         malfunctionModifier = malfunctionModifier * (modifier or 1)
  3383.                     end
  3384.                 end
  3385.             end
  3386.         end
  3387.     end
  3388.  
  3389.     local dualWieldSide = iff(dualWield, slot, nil)
  3390.  
  3391.     -- jam
  3392.     if self.jamChance then
  3393.         local chance = self.jamChance * malfunctionModifier
  3394.         if not weapon:getJammed() and math.random(1, 100) <= chance then
  3395.             weapon:setJammed(true)
  3396.             weapon.jamCount = math.random(2, 6)
  3397.         end
  3398.     end
  3399.    
  3400.     -- fix jammed weapon
  3401.     if weapon:getJammed() then
  3402.         weapon.jamCount = (weapon.jamCount or 3) - 1
  3403.         if weapon.jamCount < 0 then
  3404.             weapon:setJammed(false)
  3405.             weapon.jamCount = nil
  3406.         end
  3407.     end
  3408.  
  3409.     if weapon:getJammed() then
  3410.         soundSystem:playSound2D("firearm_jammed")
  3411.         champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
  3412.         return
  3413.     end
  3414.  
  3415.     -- consume energy
  3416.     if math.random() < 0.8 then
  3417.         local cost = self.cooldown * (math.random() * 0.6 + 0.4)
  3418.         cost = math.floor(cost + 0.5)
  3419.         --print("consume energy: "..cost)
  3420.         champion:modifyBaseStat("energy", -cost)
  3421.     end
  3422.  
  3423.     -- consume food
  3424.     champion:consumeFood(0.5 + math.random() * 2)
  3425.  
  3426.     party:endCondition("invisibility")
  3427.  
  3428.     party.go.statistics:increaseStat("missile_attacks", 1)
  3429.    
  3430.     -- play sound
  3431.     if self.attackSound then
  3432.         champion:playSound(self.attackSound)
  3433.     end
  3434.  
  3435.     local count = 1
  3436.    
  3437.     for i=1,count do
  3438.         if not (item and self.ammo == item.go.ammoitem:getAmmoType()) then return end
  3439.         local ammo = champion:getItem(ammoSlot)
  3440.        
  3441.         -- determine damage
  3442.         -- note: this has to be done before removing the ammo from hand!
  3443.         local dmg = computeDamage(champion:getDamageWithAttack(self.go.item, self))
  3444.  
  3445.         -- split stack
  3446.         if ammo.stackable and ammo.count > 1 then
  3447.             ammo = ammo:splitStack(1)
  3448.         else
  3449.             -- shoot last
  3450.             champion:removeItemFromSlot(ammoSlot)
  3451.             if ammoSlot == ItemSlot.Weapon then
  3452.                 champion.autoEquipEmptyHand = ammo.go.arch.name
  3453.             else
  3454.                 champion.autoEquipOffHand = ammo.go.arch.name
  3455.             end
  3456.         end
  3457.  
  3458.         local side = iff(champion.championIndex == 1 or champion.championIndex == 3, 0, 1)
  3459.         local power = 14
  3460.         local gravity = 1
  3461.         local pos = champion:getChampionPositionInWorld(0.4)
  3462.        
  3463.         -- push forward so that item won't collide against a door behind us
  3464.         pos = pos + party:getWorldForward() * ammo:getBoundingRadius()
  3465.  
  3466.         -- separate missiles if shooting multiple missiles
  3467.         if i > 1 then
  3468.             pos = pos + party:getWorldForward() * (i-1) * 0.9
  3469.             if i == 2 then
  3470.                 pos.y = pos.y - 0.1
  3471.             elseif i == 3 then
  3472.                 pos.y = pos.y + 0.1
  3473.             end
  3474.         end
  3475.        
  3476.         -- convert projectile type (e.g. normal arrow to fire arrow)
  3477.         if self.projectileItem then
  3478.             ammo = create(self.projectileItem).item
  3479.         end
  3480.        
  3481.         ammo:throw(party, pos, party.go.facing, power, gravity, 0)
  3482.         ammo:setItemFlag(ItemFlag.AutoPickUp, true)
  3483.         ammo.projectileDamage = dmg
  3484.         ammo.projectileDamageType = self.damageType    
  3485.         ammo.projectileAccuracy = champion:getAccuracyWithAttack(weapon, self)
  3486.         ammo.projectileCritChance = champion:getCritChanceWithAttack(weapon, self)
  3487.         ammo.projectilePierce = self.pierce or 0
  3488.         ammo.go.projectile:setVelocity(ammo.go.projectile:getVelocity() * (self.velocity or 1))
  3489.         ammo.thrownByChampion = champion.ordinal
  3490.         -- store original weapon and attack data in projectile
  3491.         ammo.thrownByWeapon = weapon
  3492.         ammo.thrownByAttack = self
  3493.     end
  3494.    
  3495.     champion:showAttackResult("Shoot", nil, dualWieldSide)
  3496.     self:callHook("onPostAttack", objectToProxy(champion), slot)
  3497. end
  3498.  
  3499. -------------------------------------------------------------------------------------------------------
  3500. -- ThrowAttack Functions                                                                             --    
  3501. -------------------------------------------------------------------------------------------------------
  3502.  
  3503. defineProxyClass{
  3504.     class = "ThrowAttackComponent",
  3505.     baseClass = "ItemActionComponent",
  3506.     description = "Implements throw attack action for items. When thrown, a Projectile component is dynamically created and added to the thrown item.",
  3507.     methods = {
  3508.         { "setAttackPower", "number" },
  3509.         { "setAttackPowerVariation", "number" },
  3510.         { "setCooldown", "number" },
  3511.         { "setSwipe", "string" },
  3512.         { "setAttackSound", "string" },
  3513.         { "setSkill", "string" },
  3514.         { "setRequiredLevel", "number" },
  3515.         { "setBaseDamageStat", "string" },
  3516.         { "getAttackPower" },
  3517.         { "getAttackPowerVariation" },
  3518.         { "getCooldown" },
  3519.         { "getSwipe" },
  3520.         { "getAttackSound" },
  3521.         { "getSkill" },
  3522.         { "getRequiredLevel" },
  3523.         { "getBaseDamageStat" },
  3524.         { "setBaseDamageMultiplier", "number" },
  3525.         { "getBaseDamageMultiplier" },
  3526.         { "getMinDamageMod" },
  3527.         { "getMaxDamageMod" },
  3528.         { "setMinDamageMod", "number" },
  3529.         { "setMaxDamageMod", "number" },
  3530.         { "setJamChance", "number" },
  3531.         { "getJamChance" },
  3532.         { "setJammed", "boolean" },
  3533.         { "getJammed" },
  3534.         { "setCritMultiplier", "number" },
  3535.         { "getCritMultiplier" },
  3536.     },
  3537.     hooks = {
  3538.         "onPostAttack(self, champion, slot)",
  3539.     }
  3540. }
  3541.  
  3542. extendProxyClass(ThrowAttackComponent, "critMultiplier")
  3543. extendProxyClass(ThrowAttackComponent, "attackPowerVariation")
  3544. extendProxyClass(ThrowAttackComponent, "baseDamageMultiplier")
  3545. extendProxyClass(ThrowAttackComponent, "minDamageMod")
  3546. extendProxyClass(ThrowAttackComponent, "maxDamageMod")
  3547. extendProxyClass(ThrowAttackComponent, "jamChance")
  3548. extendProxyClass(ThrowAttackComponent, "jamCount")
  3549. extendProxyClass(ThrowAttackComponent, "jamText")
  3550.  
  3551. function ThrowAttackComponent:start(champion, slot)
  3552.     local weapon = champion:getItem(slot)
  3553.  
  3554.     party.go.statistics:increaseStat("throw_attacks", 1)
  3555.    
  3556.     -- Throw weapons could jam if you wanted to
  3557.     local malfunctionModifier = 1
  3558.     -- traits modifiers
  3559.     for name,trait in pairs(dungeon.traits) do
  3560.         if trait.onComputeMalfunctionChance then
  3561.             local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", iff(champion:hasTrait(name), 1, 0))
  3562.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3563.         end
  3564.     end
  3565.  
  3566.     -- skills modifiers
  3567.     for name,skill in pairs(dungeon.skills) do
  3568.         if skill.onComputeMalfunctionChance then
  3569.             local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", champion:getSkillLevel(name))
  3570.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3571.         end
  3572.     end
  3573.  
  3574.     -- equipment modifiers (equipped items only)
  3575.     for i=1,ItemSlot.BackpackFirst-1 do
  3576.         local it = champion:getItem(i)
  3577.         if it then
  3578.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  3579.                 for i=1,it.go.components.length do
  3580.                     local comp = it.go.components[i]
  3581.                     if comp.onComputeMalfunctionChance then
  3582.                         local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "throw")
  3583.                         malfunctionModifier = malfunctionModifier * (modifier or 1)
  3584.                     end
  3585.                 end
  3586.             end
  3587.         end
  3588.     end
  3589.  
  3590.     local dualWield = champion:isDualWielding()
  3591.  
  3592.     local dualWieldSide = iff(dualWield, slot, nil)
  3593.  
  3594.     -- jam
  3595.     if self.jamChance then
  3596.         local chance = self.jamChance * malfunctionModifier
  3597.         if not weapon:getJammed() and math.random(1, 100) <= chance then
  3598.             weapon:setJammed(true)
  3599.             weapon.jamCount = math.random(2, 6)
  3600.         end
  3601.     end
  3602.    
  3603.     -- fix jammed weapon
  3604.     if weapon:getJammed() then
  3605.         weapon.jamCount = (weapon.jamCount or 3) - 1
  3606.         if weapon.jamCount < 0 then
  3607.             weapon:setJammed(false)
  3608.             weapon.jamCount = nil
  3609.         end
  3610.     end
  3611.  
  3612.     if weapon:getJammed() then
  3613.         soundSystem:playSound2D("firearm_jammed")
  3614.         champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
  3615.         return
  3616.     end
  3617.        
  3618.     -- consume energy
  3619.     if math.random() < 0.8 then
  3620.         local cost = self.cooldown * (math.random() * 0.6 + 0.4)
  3621.         cost = math.floor(cost + 0.5)
  3622.         --print("consume energy: "..cost)
  3623.         champion:modifyBaseStat("energy", -cost)
  3624.     end
  3625.  
  3626.     -- consume food
  3627.     champion:consumeFood(0.5 + math.random() * 2)
  3628.  
  3629.     -- cooldown
  3630.     local cooldown = self.cooldown or 0
  3631.     for name,skill in pairs(dungeon.skills) do
  3632.         if skill.onComputeCooldown then
  3633.             local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", champion:getSkillLevel(name))
  3634.             cooldown = cooldown * (modifier or 1)
  3635.         end
  3636.     end
  3637.     for name,trait in pairs(dungeon.traits) do
  3638.         if trait.onComputeCooldown then
  3639.             local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "throw", iff(champion:hasTrait(name), 1, 0))
  3640.             cooldown = cooldown * (modifier or 1)
  3641.         end
  3642.     end
  3643.  
  3644.     champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3645.     champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3646.  
  3647.     party:endCondition("invisibility")
  3648.  
  3649.     -- play sound
  3650.     if self.attackSound then
  3651.         champion:playSound(self.attackSound)
  3652.     end
  3653.  
  3654.     local count = 1
  3655.     --if champion:getSkillLevel("double_throw") > 0 then count = 2 end
  3656.  
  3657.     for i=1,count do
  3658.         -- weapons left to throw?
  3659.         local item = champion:getItem(slot)
  3660.         if not item then return end
  3661.        
  3662.         -- determine damage
  3663.         -- note: this has to be done before removing the item from hand!
  3664.         local dmg = computeDamage(champion:getDamageWithAttack(weapon, self))
  3665.        
  3666.         -- split stack
  3667.         local projectile
  3668.         if item.stackable and item.count > 1 then
  3669.             projectile = item:splitStack(1)
  3670.         else
  3671.             -- throw last
  3672.             if champion:getItem(ItemSlot.Weapon) == item then
  3673.                 champion:removeItemFromSlot(ItemSlot.Weapon)
  3674.                 champion.autoEquipEmptyHand = item.go.arch.name
  3675.             else
  3676.                 champion:removeItemFromSlot(ItemSlot.OffHand)
  3677.                 champion.autoEquipOffHand = item.go.arch.name
  3678.             end
  3679.             projectile = item
  3680.         end
  3681.        
  3682.         local pos = champion:getChampionPositionInWorld(0.4)
  3683.        
  3684.         -- push forward so that item won't collide against a door behind us
  3685.         pos = pos + party:getWorldForward() * item:getBoundingRadius()
  3686.  
  3687.         -- separate projectiles if shooting multiple projectiles
  3688.         if self.spread then
  3689.             pos.x = pos.x + (math.random() - 0.5) * self.spread
  3690.             pos.y = pos.y + (math.random() - 0.5) * self.spread
  3691.             pos.z = pos.z + (math.random() - 0.5) * self.spread
  3692.         end
  3693.        
  3694.         local power = 14
  3695.         local gravity = 1
  3696.         local velocityUp = 0
  3697.  
  3698.         if projectile.go.arch.bombType then
  3699.             local weight = 0.8
  3700.             power = math.max(14 - weight, 10)
  3701.             gravity = math.clamp(2 + weight*1.5, 4, 10)
  3702.             velocityUp = 0
  3703.         end
  3704.        
  3705.         projectile:throw(party, pos, party.go.facing, power, gravity, velocityUp)
  3706.         projectile:setItemFlag(ItemFlag.AutoPickUp, true)
  3707.         projectile.go.projectile:setVelocity(projectile.go.projectile:getVelocity() * (self.velocity or 1))
  3708.         projectile.projectileDamage = dmg
  3709.         projectile.projectileAccuracy = champion:getAccuracyWithAttack(weapon, self)
  3710.         projectile.projectileCritChance = champion:getCritChanceWithAttack(weapon, self)
  3711.         projectile.projectilePierce = self.pierce
  3712.         projectile.thrownByChampion = champion.ordinal
  3713.         projectile.thrownByWeapon = weapon
  3714.         projectile.thrownByAttack = self
  3715.  
  3716.         messageSystem:sendMessageNEW("onPartyThrowItem", projectile)
  3717.     end
  3718.    
  3719.     champion:showAttackResult("Throw")
  3720.  
  3721.     self:callHook("onPostAttack", objectToProxy(champion), slot)
  3722. end
  3723.  
  3724. -------------------------------------------------------------------------------------------------------
  3725. -- FirearmAttack Functions                                                                           --    
  3726. -------------------------------------------------------------------------------------------------------
  3727.  
  3728. defineProxyClass{
  3729.     class = "FirearmAttackComponent",
  3730.     baseClass = "ItemActionComponent",
  3731.     description = "Implements firearm attacks. Firearm attacks need ammo.",
  3732.     methods = {
  3733.         { "setAttackPower", "number" },
  3734.         { "setAttackPowerVariation", "number" },
  3735.         { "setRange", "number" },
  3736.         { "setCooldown", "number" },
  3737.         { "setAttackSound", "string" },
  3738.         { "setSkill", "string" },
  3739.         { "setRequiredLevel", "number" },
  3740.         { "setAccuracy", "number" },
  3741.         { "setAmmo", "string" },
  3742.         { "setBaseDamageStat", "string" },
  3743.         { "setClipSize", "number" },
  3744.         { "setDamageType", "string" },
  3745.         { "setLoadedCount", "number" },
  3746.         { "setPierce", "number" },
  3747.         { "getAttackPower" },
  3748.         { "getAttackPowerVariation" },
  3749.         { "getRange" },
  3750.         { "getCooldown" },
  3751.         { "getAttackSound" },
  3752.         { "getSkill" },
  3753.         { "getRequiredLevel" },
  3754.         { "getAccuracy" },
  3755.         { "getAmmo" },
  3756.         { "getBaseDamageStat" },
  3757.         { "getClipSize" },
  3758.         { "getDamageType" },
  3759.         { "getLoadedCount" },
  3760.         { "getPierce" },
  3761.         { "setBaseDamageMultiplier", "number" },
  3762.         { "getBaseDamageMultiplier" },
  3763.         { "getMinDamageMod" },
  3764.         { "getMaxDamageMod" },
  3765.         { "setMinDamageMod", "number" },
  3766.         { "setMaxDamageMod", "number" },
  3767.         { "setJamChance", "number" },
  3768.         { "getJamChance" },
  3769.         { "setJammed", "boolean" },
  3770.         { "getJammed" },
  3771.         { "setCritMultiplier", "number" },
  3772.         { "getCritMultiplier" },
  3773.     },
  3774.     hooks = {
  3775.         "onBackfire(self, champion)",
  3776.         "onPostAttack(self, champion, slot)",
  3777.     },
  3778. }
  3779.  
  3780. extendProxyClass(FirearmAttackComponent, "critMultiplier")
  3781. extendProxyClass(FirearmAttackComponent, "attackPowerVariation")
  3782. extendProxyClass(FirearmAttackComponent, "baseDamageMultiplier")
  3783. extendProxyClass(FirearmAttackComponent, "minDamageMod")
  3784. extendProxyClass(FirearmAttackComponent, "maxDamageMod")
  3785. extendProxyClass(FirearmAttackComponent, "jamText")
  3786.  
  3787. function FirearmAttackComponent:start(champion, slot)
  3788.     local weapon = champion:getItem(slot)
  3789.     local ammoSlot = iff(slot == ItemSlot.Weapon, ItemSlot.OffHand, ItemSlot.Weapon)
  3790.  
  3791.     if not champion:getDamage(slot) then
  3792.         console:warn("no attack power")
  3793.         return
  3794.     end
  3795.  
  3796.     -- check ammo
  3797.     if self:checkAmmo(champion, ammoSlot) < 1 then
  3798.         if self.clipSize then
  3799.             champion:showAttackResult("Clip empty")
  3800.             --soundSystem:playSound2D("firearm_jammed")
  3801.         else
  3802.             champion:showAttackResult("No ammo")
  3803.         end
  3804.         return
  3805.     end
  3806.    
  3807.     local malfunctionModifier = 1
  3808.     -- traits modifiers
  3809.     for name,trait in pairs(dungeon.traits) do
  3810.         if trait.onComputeMalfunctionChance then
  3811.             local modifier = trait.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", iff(champion:hasTrait(name), 1, 0))
  3812.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3813.         end
  3814.     end
  3815.  
  3816.     -- skill modifiers
  3817.     for name,skill in pairs(dungeon.skills) do
  3818.         if skill.onComputeMalfunctionChance then
  3819.             local modifier = skill.onComputeMalfunctionChance(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", champion:getSkillLevel(name))
  3820.             malfunctionModifier = malfunctionModifier * (modifier or 1)
  3821.         end
  3822.     end
  3823.  
  3824.     -- equipment modifiers (equipped items only)
  3825.     for i=1,ItemSlot.BackpackFirst-1 do
  3826.         local it = champion:getItem(i)
  3827.         if it then
  3828.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  3829.                 for i=1,it.go.components.length do
  3830.                     local comp = it.go.components[i]
  3831.                     if comp.onComputeMalfunctionChance then
  3832.                         local modifier = comp:onComputeMalfunctionChance(champion, weapon, self, "firearm")
  3833.                         malfunctionModifier = malfunctionModifier * (modifier or 1)
  3834.                     end
  3835.                 end
  3836.             end
  3837.         end
  3838.     end
  3839.  
  3840.     local dualWield = champion:isDualWielding()
  3841.  
  3842.     -- jam
  3843.     if self.jamChance then
  3844.         local chance = self.jamChance * malfunctionModifier
  3845.         if not weapon:getJammed() and math.random(1, 100) <= chance then
  3846.             weapon:setJammed(true)
  3847.             weapon.jamCount = math.random(2, 6)
  3848.         end
  3849.     end
  3850.    
  3851.     -- fix jammed weapon
  3852.     if weapon:getJammed() then
  3853.         weapon.jamCount = (weapon.jamCount or 3) - 1
  3854.         if weapon.jamCount < 0 then
  3855.             weapon:setJammed(false)
  3856.             weapon.jamCount = nil
  3857.         end
  3858.     end
  3859.  
  3860.     if weapon:getJammed() then
  3861.         soundSystem:playSound2D("firearm_jammed")
  3862.         champion:showAttackResult(weapon.jamText or "Jammed!", nil, dualWieldSide)
  3863.         return
  3864.     end
  3865.  
  3866.     party.go.statistics:increaseStat("firearm_attacks", 1)
  3867.    
  3868.     -- consume ammo
  3869.     if self.clipSize then
  3870.         self.loadedCount = self.loadedCount - 1
  3871.     else
  3872.         self:consumeAmmo(champion, ammoSlot, 1)
  3873.     end
  3874.  
  3875.     champion:consumeFood(0.5 + math.random() * 2)
  3876.  
  3877.     -- cooldown
  3878.     local cooldown = self.cooldown or 0
  3879.     for name,skill in pairs(dungeon.skills) do
  3880.         if skill.onComputeCooldown then
  3881.             local modifier = skill.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", champion:getSkillLevel(name))
  3882.             cooldown = cooldown * (modifier or 1)
  3883.         end
  3884.     end
  3885.     for name,trait in pairs(dungeon.traits) do
  3886.         if trait.onComputeCooldown then
  3887.             local modifier = trait.onComputeCooldown(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", iff(champion:hasTrait(name), 1, 0))
  3888.             cooldown = cooldown * (modifier or 1)
  3889.         end
  3890.     end
  3891.  
  3892.     -- backfire
  3893.     if self.backfireChance then
  3894.         local chance = self.backfireChance * malfunctionModifier
  3895.         --print("backfire chance = ", chance)
  3896.         if math.random(1, 100) <= chance then
  3897.             if self:callHook("onBackfire", objectToProxy(champion)) ~= false then
  3898.                 party.go:spawn("fireburst")
  3899.                 party:shakeCamera(1, 0.3)
  3900.                 champion:showAttackResult("Backfire!")
  3901.                 champion:playDamageSound()
  3902.                 if self.attackSound then champion:playSound(self.attackSound) end
  3903.                 weapon:setJammed(true)
  3904.                 return
  3905.             end
  3906.         end
  3907.     end
  3908.    
  3909.     -- cooldown
  3910.     if dualWield then
  3911.         if slot == ItemSlot.Weapon then
  3912.             champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3913.         else
  3914.             champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3915.         end
  3916.     else
  3917.         champion.cooldownTimer[1] = math.max(champion.cooldownTimer[1], cooldown)
  3918.         champion.cooldownTimer[2] = math.max(champion.cooldownTimer[2], cooldown)
  3919.     end
  3920.    
  3921.     party:endCondition("invisibility")
  3922.        
  3923.     -- play sound
  3924.     if self.attackSound then
  3925.         champion:playSound(self.attackSound)
  3926.     end
  3927.  
  3928.     local dualWieldSide = iff(dualWield, slot, nil)
  3929.  
  3930.     -- trace ray
  3931.     local origin = party.go:getWorldPosition()
  3932.     origin.y = origin.y + 1.3
  3933.    
  3934.     local range = self.range
  3935.  
  3936.     -- traits modifiers
  3937.     for name,trait in pairs(dungeon.traits) do
  3938.         if trait.onComputeRange then
  3939.             local modifier = trait.onComputeRange(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", iff(champion:hasTrait(name), 1, 0))
  3940.             range = range + (modifier or 0)
  3941.         end
  3942.     end
  3943.  
  3944.     -- skill modifiers
  3945.     for name,skill in pairs(dungeon.skills) do
  3946.         if skill.onComputeRange then
  3947.             local modifier = skill.onComputeRange(objectToProxy(champion), objectToProxy(weapon), objectToProxy(self), "firearm", champion:getSkillLevel(name))
  3948.             range = range + (modifier or 0)
  3949.         end
  3950.     end
  3951.  
  3952.     -- equipment modifiers (equipped items only)
  3953.     for i=1,ItemSlot.BackpackFirst-1 do
  3954.         local it = champion:getItem(i)
  3955.         if it then
  3956.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  3957.                 for i=1,it.go.components.length do
  3958.                     local comp = it.go.components[i]
  3959.                     if comp.onComputeRange then
  3960.                         local modifier = comp:onComputeRange(champion, weapon, self, "firearm")
  3961.                         range = range + (modifier or 0)
  3962.                     end
  3963.                 end
  3964.             end
  3965.         end
  3966.     end
  3967.    
  3968.     local hitWhat, hitEntity, hitPos = self.raycast(origin, party.go.facing, range, party)
  3969.  
  3970.     local hitStatus
  3971.     if hitEntity then
  3972.         --print(hitWhat, hitEntity.arch.name)
  3973.         hitStatus = hitEntity:sendMessage("onAttackedByChampion", champion, weapon, self, slot, dualWieldSide)
  3974.     end
  3975.  
  3976.     -- spawn particle effect
  3977.     if hitWhat and hitStatus ~= "miss" then
  3978.         local map = party.go.map
  3979.         local x,y = map:worldToMap(hitPos)
  3980.         local fx = spawn(map, "particle_system", x, y, 0, party.go.elevation)
  3981.         fx.particle:setParticleSystem("hit_firearm")
  3982.  
  3983.         -- random spread
  3984.         local spread = 0.5
  3985.         local dx,dy = getDxDy(party.go.facing)
  3986.         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)
  3987.  
  3988.         fx:setWorldPosition(pos)
  3989.     end
  3990.  
  3991.     if not hitStatus then
  3992.         champion:showAttackResult("Miss", GuiItem.HitSplash, dualWieldSide)
  3993.     end
  3994.  
  3995.     self:callHook("onPostAttack", objectToProxy(champion), slot)
  3996. end
  3997.  
  3998. -------------------------------------------------------------------------------------------------------
  3999. -- BombItem Functions                                                                                --
  4000. -------------------------------------------------------------------------------------------------------
  4001.  
  4002. function BombItemComponent:explode(map, x, y, facing)
  4003.     if self:callHook("onExplode", map.level, x, y, facing, self.go.elevation) == false then
  4004.         return
  4005.     end
  4006.  
  4007.     local power = self.bombPower or 0
  4008.  
  4009.     -- damage multiplier from multiple bombs
  4010.     local item = self.go.item
  4011.     if item and item.count and item.count > 1 then
  4012.         power = math.floor(power * (1 + (item.count-1) * 0.2))
  4013.     end
  4014.  
  4015.     local thrownByChampion = item.thrownByChampion
  4016.  
  4017.     -- bombs boost
  4018.     local champion = party:getChampionByOrdinal(thrownByChampion)
  4019.     if champion and champion:getClass() == "alchemist" then
  4020.        
  4021.     end
  4022.    
  4023.     -- traits modifiers
  4024.     for name,skill in pairs(dungeon.skills) do
  4025.         if skill.onComputeBombPower then
  4026.             power = skill.onComputeBombPower(objectToProxy(self), objectToProxy(champion), power, champion:getSkillLevel(name)) or power
  4027.         end
  4028.     end
  4029.    
  4030.     -- traits modifiers
  4031.     for name,trait in pairs(dungeon.traits) do
  4032.         if trait.onComputeBombPower then
  4033.             power = trait.onComputeBombPower(objectToProxy(self), objectToProxy(champion), power, iff(self:hasTrait(name), 1, 0)) or power
  4034.         end
  4035.     end
  4036.  
  4037.     -- equipment modifiers (equipped items only)
  4038.     for i=1,ItemSlot.BackpackFirst-1 do
  4039.         local it = champion:getItem(i)
  4040.         if it then
  4041.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(champion, i) then
  4042.                 for i=1,it.go.components.length do
  4043.                     local comp = it.go.components[i]
  4044.                     if comp.onComputeBombPower then
  4045.                         power = comp:onComputeBombPower(self, champion, power) or power
  4046.                     end
  4047.                 end
  4048.             end
  4049.         end
  4050.     end
  4051.    
  4052.     local elevation = self.go.elevation
  4053.  
  4054.     if self.bombType == "shock" then
  4055.         local ent = spawn(map, "shockburst", x, y, facing, elevation)
  4056.         ent.tiledamager:setAttackPower(power)
  4057.         ent.tiledamager:setCastByChampion(thrownByChampion)
  4058.     elseif self.bombType == "fire" then
  4059.         local ent = spawn(map, "fireburst", x, y, facing, elevation)
  4060.         ent.tiledamager:setAttackPower(power)
  4061.         ent.tiledamager:setCastByChampion(thrownByChampion)
  4062.  
  4063.         -- spawn wall of fire but only if there's ground or a platform underneath
  4064.         if elevation == map:getElevation(x, y) or PlatformComponent.getPlatformAt(map, x, y, elevation) then
  4065.             local ent = spawn(map, "wall_fire", x, y, facing, elevation)
  4066.             ent.tiledamager:setAttackPower(5)
  4067.             ent.tiledamager:setCastByChampion(thrownByChampion)
  4068.         end
  4069.     elseif self.bombType == "frost" then
  4070.         local ent = spawn(map, "frostburst", x, y, facing, elevation)
  4071.         ent.tiledamager:setAttackPower(power)
  4072.         ent.tiledamager:setCastByChampion(thrownByChampion)
  4073.     elseif self.bombType == "poison" then
  4074.         local ent = spawn(map, "poison_cloud_medium", x, y, 0, elevation)
  4075.         ent.cloudspell:setAttackPower(power)
  4076.         ent.cloudspell:setCastByChampion(thrownByChampion)
  4077.     else
  4078.         console:warn("unknown bomb type: "..tostring(self.bombType))
  4079.     end
  4080. end
  4081.  
  4082. -------------------------------------------------------------------------------------------------------
  4083. -- CastSpell Functions                                                                               --
  4084. -------------------------------------------------------------------------------------------------------
  4085.  
  4086. function CastSpellComponent:start(champion, slot)
  4087.     local name = self.spell
  4088.     if not name then console:warn("unknown wand spell"); return end
  4089.    
  4090.     -- find spell
  4091.     local spell = Spell.getSpell(name)
  4092.     if not spell then
  4093.         console:warn("Unknown spell: "..name)
  4094.         return
  4095.     end
  4096.    
  4097.     if self:checkCharges(champion) == false then return end
  4098.  
  4099.     -- use wand's power as spell skill
  4100.     --if not self.power then console:warn("wand power not set for "..weapon.go.arch.name) end
  4101.     local skill = (self.power or 0)
  4102.     local pos = party.go:getWorldPositionFast()
  4103.     local x,y = party.go.map:worldToMap(pos)   
  4104.     Spell.castSpell(spell, champion, x, y, party.go.facing, party.go.elevation, skill, spell)
  4105.  
  4106.     champion.cooldownTimer[1] = champion.cooldownTimer[1] + (self.cooldown or 0)
  4107.     champion.cooldownTimer[2] = champion.cooldownTimer[2] + (self.cooldown or 0)
  4108.  
  4109.     -- consume charges
  4110.     self:consumeCharges(champion)
  4111.    
  4112.     -- strenous activity consumes food
  4113.     champion:consumeFood(math.random(1,5))
  4114. end
  4115.  
  4116. function CastSpellComponent:checkCharges(champion)
  4117.     if self.charges == 0 then return false end
  4118. end
  4119.  
  4120. function CastSpellComponent:consumeCharges(champion)
  4121.     if self.charges then
  4122.         self.charges = self.charges - 1
  4123.         if self.charges < 1 then
  4124.             self:deplete()
  4125.         end
  4126.     end
  4127. end
  4128.  
  4129. -------------------------------------------------------------------------------------------------------
  4130. -- CraftPotion Functions                                                                             --    
  4131. -------------------------------------------------------------------------------------------------------
  4132.  
  4133. function CraftPotionComponent:brewPotion(champion)
  4134.     if self.recipe == 0 then return end
  4135.  
  4136.     local alchemy = champion:getSkillLevel("alchemy")
  4137.  
  4138.     -- verify that champion has enough herbs
  4139.     local herbs = CraftPotionComponent.Herbs
  4140.     for i=1,#herbs do
  4141.         if herbs[i].count < herbs[i].reserved then
  4142.             gui:hudPrint(champion.name.." does not have enough herbs to craft this potion.")
  4143.             return
  4144.         end
  4145.     end
  4146.  
  4147.     -- get recipe
  4148.     local recipe = self:getPotionRecipe(self.recipe)
  4149.     if not recipe then
  4150.         gui:hudPrint(champion.name.." failed to brew a potion.")
  4151.         self.recipe = 0
  4152.         champion:showAttackPanel(nil)
  4153.         return
  4154.     end
  4155.  
  4156.     -- check alchemy skill
  4157.     if alchemy < (recipe.level or 0) then
  4158.         gui:hudPrint(champion.name.." is not skilled enough in Alchemy to brew this potion.")
  4159.         return
  4160.     end
  4161.  
  4162.     -- consume herbs
  4163.     local r = self.recipe
  4164.     for i=5,0,-1 do
  4165.         -- extract herb from recipe
  4166.         local h = math.floor(r / 10^i)
  4167.         r = r - h * 10^i
  4168.  
  4169.         if h ~= 0 then
  4170.             self:consumeHerb(champion, CraftPotionComponent.Herbs[h%10].name)
  4171.         end
  4172.     end
  4173.  
  4174.     local potion = recipe.potion
  4175.    
  4176.     if champion:hasTrait("improved_alchemy") then
  4177.         if potion == "potion_healing" then potion = "potion_greater_healing" end
  4178.         if potion == "potion_energy" then potion = "potion_greater_energy" end
  4179.     end
  4180.  
  4181.     local count = self:countPotions(recipe, champion)
  4182.     local returnVal = party:callHook("onBrewPotion", count, potion, objectToProxy(champion))
  4183.     if returnVal then
  4184.         if returnVal[1] == false then return false end
  4185.         count = returnVal[2] or count
  4186.         potion = returnVal[3] or potion
  4187.     end
  4188.  
  4189.     self:onBrewPotion(count, recipe, champion)
  4190.  
  4191.     local mouseItem = gui:getMouseItem()
  4192.     if mouseItem == nil then
  4193.         -- create new potion to mouse hand
  4194.         local item = create(potion).item
  4195.         item:setStackSize(count)
  4196.         gui:setMouseItem(item)
  4197.     elseif mouseItem.go.arch.name == potion then
  4198.         -- merge new potion to stack in hand
  4199.         mouseItem.count = mouseItem.count + count
  4200.     else
  4201.         -- create new potion on the ground
  4202.         local item = spawn(party.go.map, potion, party.go.x, party.go.y, party.go.facing, party.go.elevation).item
  4203.         item:setStackSize(count)
  4204.     end
  4205.  
  4206.     soundSystem:playSound2D("brew_potion")
  4207.  
  4208.     party.go.statistics:increaseStat("potions_mixed", 1)
  4209.  
  4210.     self.recipe = 0
  4211.     champion:showAttackPanel(nil)
  4212. end
  4213.  
  4214. function CraftPotionComponent:countPotions(recipe, champion)
  4215.     local potion = recipe.potion
  4216.     local count = 1
  4217.  
  4218.     if champion:hasTrait("bomb_expert") then
  4219.         if potion == "fire_bomb" or potion == "shock_bomb" or potion == "poison_bomb" or potion == "frost_bomb" then
  4220.             count = 3
  4221.         end
  4222.     end
  4223.  
  4224.     return count
  4225. end
  4226.  
  4227. function CraftPotionComponent:onBrewPotion(recipe, champion)
  4228.     -- Triggers on potion crafted
  4229. end
  4230.  
  4231. -------------------------------------------------------------------------------------------------------
  4232. -- Condition Functions                                                                               --
  4233. -------------------------------------------------------------------------------------------------------
  4234.  
  4235. local oldConditionInit = Condition.init
  4236. function Condition:init(uiName, description, iconIndex, ...)
  4237.     oldConditionInit(self, uiName, description, iconIndex, ...)
  4238.     self.power = 0
  4239.     self.stacks = 1
  4240. end
  4241.  
  4242. function Condition:setPower(value)
  4243.     self.power = power
  4244. end
  4245.  
  4246. function Condition:getPower()
  4247.     return self.power
  4248. end
  4249.  
  4250. local oldConditionLoadState = Condition.loadState
  4251. function Condition:loadState(file, ...)
  4252.     oldConditionLoadState(self, file, ...)
  4253.     self.power = file:readValue()
  4254.     self.stacks = file:readValue()
  4255. end
  4256.  
  4257. local oldConditionSaveState = Condition.saveState
  4258. function Condition:saveState(file, ...)
  4259.     oldConditionSaveState(self, file, ...)
  4260.     file:writeValue(self.power)
  4261.     file:writeValue(self.stacks)
  4262. end
  4263.  
  4264. function Condition:update(champion)
  4265.     self.timer = self.timer - Time.deltaTime
  4266.     if self.timer <= 0 then
  4267.         self:tick(champion)
  4268.     end
  4269.    
  4270.     if self.value then
  4271.         self.value = self.value - Time.deltaTime
  4272.         if self.value <= 0 then
  4273.             if self.stacks > 1 then
  4274.                 self.stacks = self.stacks - 1
  4275.                 self.value = self.stackTimer
  4276.             elseif self.stacks == 1 then
  4277.                 self.stacks = self.stacks - 1
  4278.             else
  4279.                 return false
  4280.             end
  4281.         end
  4282.     end
  4283. end
  4284.  
  4285. local oldCustomConditionInit = CustomCondition.init
  4286. function CustomCondition:init(name)
  4287.     oldCustomConditionInit(self, name)
  4288.     local desc = dungeon.conditions[name]
  4289.     self.maxStacks = desc.maxStacks
  4290.     self.stackTimer = desc.stackTimer
  4291.     self.name = desc.name
  4292. end
  4293.  
  4294. function CustomCondition:getStacks()
  4295.     return self.stacks
  4296. end
  4297.  
  4298. function CustomCondition:start(champion)
  4299.     if self.onStart then self.onStart(objectToProxy(self), objectToProxy(champion), true, self.power, self.stacks) end
  4300. end
  4301.  
  4302. function CustomCondition:restart(champion)
  4303.     if self.onStart then self.onStart(objectToProxy(self), objectToProxy(champion), false, self.power, self.stacks) end
  4304. end
  4305.  
  4306. function CustomCondition:stop(champion)
  4307.     if self.onStop then self.onStop(objectToProxy(self), objectToProxy(champion), self.power, self.stacks) end
  4308. end
  4309.  
  4310. function CustomCondition:tick(champion)
  4311.     if self.onTick then self.onTick(objectToProxy(self), objectToProxy(champion), self.power, self.stacks) end
  4312.     self.timer = self.tickInterval or 1
  4313. end
  4314.  
  4315. function CustomCondition:recomputeStats(champion)
  4316.     if self.onRecomputeStats then self.onRecomputeStats(objectToProxy(self), objectToProxy(champion), self.power, self.stacks) end
  4317. end
  4318.  
  4319. function ProtectiveShieldCondition:recomputeStats(champion)
  4320.     local stats = champion.stats
  4321.     stats.protection.current = stats.protection.current + champion.conditions.protective_shield.power
  4322. end
  4323.  
  4324. function FireShieldCondition:recomputeStats(champion)
  4325.     local stats = champion.stats
  4326.     stats.resist_fire.current = stats.resist_fire.current + champion.conditions.fire_shield.power
  4327. end
  4328.  
  4329. function FrostShieldCondition:recomputeStats(champion)
  4330.     local stats = champion.stats
  4331.     stats.resist_cold.current = stats.resist_cold.current + champion.conditions.frost_shield.power
  4332. end
  4333.  
  4334. function ShockShieldCondition:recomputeStats(champion)
  4335.     local stats = champion.stats
  4336.     stats.resist_shock.current = stats.resist_shock.current + champion.conditions.shock_shield.power
  4337. end
  4338.  
  4339. function PoisonShieldCondition:recomputeStats(champion)
  4340.     local stats = champion.stats
  4341.     stats.resist_poison.current = stats.resist_poison.current + champion.conditions.poison_shield.power
  4342. end
  4343.  
  4344. function BearFormCondition:update(champion)
  4345.     local multi = 1
  4346.     -- skill modifiers
  4347.     for name,skill in pairs(dungeon.skill) do
  4348.         if skill.onComputeBearFormDuration then
  4349.             local modifier = skill.onComputeBearFormDuration(objectToProxy(self), self:getSkillLevel(name))
  4350.             multi = multi * (modifier or 1)
  4351.         end
  4352.     end
  4353.  
  4354.     -- traits modifiers
  4355.     for name,trait in pairs(dungeon.traits) do
  4356.         if trait.onComputeBearFormDuration then
  4357.             local modifier = trait.onComputeBearFormDuration(objectToProxy(self), iff(self:hasTrait(name), 1, 0))
  4358.             multi = multi * (modifier or 1)
  4359.         end
  4360.     end
  4361.  
  4362.     -- equipment modifiers (equipped items only)
  4363.     for i=1,ItemSlot.BackpackFirst-1 do
  4364.         local it = self:getItem(i)
  4365.         if it then
  4366.             if it.go.equipmentitem and it.go.equipmentitem:isEquipped(self, i) then
  4367.                 for i=1,it.go.components.length do
  4368.                     local comp = it.go.components[i]
  4369.                     if comp.onComputeBearFormDuration then
  4370.                         multi = multi * (comp:onComputeBearFormDuration(self, champion) or 1)
  4371.                     end
  4372.                 end
  4373.             end
  4374.         end
  4375.     end
  4376.  
  4377.     return champion:spendEnergy(Time.deltaTime*multi)
  4378. end
  4379.  
  4380. -------------------------------------------------------------------------------------------------------
  4381. -- Spell Functions                                                                                   --
  4382. -------------------------------------------------------------------------------------------------------
  4383.  
  4384. function Spell.castSpell(spell, caster, x, y, direction, elevation, skill)
  4385.     -- caster = champion casting the spell
  4386.     -- skill = caster's skill in spell's school of magic (or wand's strength for wand spells)
  4387.        
  4388.     if type(spell.onCast) == "string" then
  4389.         -- built-in spell
  4390.         local spellFunc = BuiltInSpell[spell.onCast]
  4391.         if spellFunc then
  4392.             spellFunc(caster, x, y, direction, elevation, skill, spell) -- adds spell data to call
  4393.         else
  4394.             console:warn("unknown built-in spell: "..spell.onCast)
  4395.         end
  4396.     else
  4397.         -- custom spell
  4398.         spell.onCast(objectToProxy(caster), x, y, direction, elevation, skill, spell) -- adds spell data to call
  4399.     end
  4400. end
  4401.  
  4402. BuiltInSpell = {}
  4403.  
  4404. -- We redefine built-in spells so it can take the spell definition data instead of using hard-coded values
  4405.  
  4406. function BuiltInSpell.shield(caster, x, y, direction, elevation, skill, spl)
  4407.     soundSystem:playSound2D("generic_spell")
  4408.     caster:setCondition("protective_shield")
  4409.     local duration = spl.duration + skill * (spl.durationScaling or 0)
  4410.     local power = spl.power + skill * (spl.powerScaling or 0)
  4411.     caster.conditions.protective_shield:setDuration(duration)
  4412.     caster.conditions.protective_shield:setPower(power)
  4413.     -- gui:hudPrint(caster.name.." conjures a magical shield.")
  4414. end
  4415.  
  4416. function BuiltInSpell.light(caster, x, y, direction, elevation, skill, spl)
  4417.     soundSystem:playSound2D("light")
  4418.     if party.lightSpell < 0 then
  4419.         -- light cancels darkness
  4420.         party.lightSpell = 0
  4421.     else
  4422.         party.lightSpell = spl.duration + level * spl.durationScaling
  4423.     end
  4424.     -- gui:hudPrint(caster.name.." conjures magical light.")
  4425. end
  4426.        
  4427. function BuiltInSpell.darkness(caster, x, y, direction, elevation, skill, spl)
  4428.     soundSystem:playSound2D("generic_spell")
  4429.     if party.lightSpell > 0 then
  4430.         -- darkness cancels light
  4431.         party.lightSpell = 0
  4432.     else
  4433.         party.lightSpell = (spl.duration + level * spl.durationScaling) * -1
  4434.     end
  4435. end
  4436.  
  4437. function BuiltInSpell.darkbolt(caster, x, y, direction, elevation, skill, spl)
  4438.     --soundSystem:playSound2D("generic_spell")
  4439.     local spell = spawn(party.go.map, "dark_bolt", x, y, direction, elevation)
  4440.     spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
  4441.     local power = spl.power + skill * (spl.powerScaling or 0)
  4442.     spell.projectile:setAttackPower(power)
  4443.     spell.projectile:setIgnoreEntity(party.go)
  4444.     spell.projectile:setCastByChampion(caster.ordinal)
  4445.     party:endCondition("invisibility")
  4446. end
  4447.  
  4448. function BuiltInSpell.forceField(caster, x, y, direction, elevation, skill, spl)
  4449.     x,y = Spell.getBurstTargetTile(x, y, direction)
  4450.  
  4451.     local duration = math.max(spl.duration + skill * (spl.durationScaing or 0), 5)
  4452.     soundSystem:playSound2D("force_field_cast")
  4453.  
  4454.     for _,f in party.go.map:componentsAt(ForceFieldComponent, x, y) do
  4455.         if f.enabled then
  4456.             if not f.duration then
  4457.                 -- square already has a permanent force field
  4458.                 return
  4459.             else
  4460.                 -- square already has a temporary force field -> reset duration
  4461.                 f.duration = math.max(f.duration, duration)
  4462.                 return
  4463.             end
  4464.         end
  4465.     end
  4466.  
  4467.     local spell = spawn(party.go.map, "force_field", x, y, direction, elevation)
  4468.     spell.forcefield:setDuration(duration)
  4469. end
  4470.  
  4471. function BuiltInSpell.fireburst(caster, x, y, direction, elevation, skill, spl)
  4472.     local map = party.go.map
  4473.     x,y = Spell.getBurstTargetTile(x, y, direction)
  4474.     local spell = spawn(map, "fireburst", x, y, direction, elevation)
  4475.     local power = spl.power + skill * (spl.powerScaling or 0)
  4476.     spell.tiledamager:setAttackPower(power)
  4477.     spell.tiledamager:setCastByChampion(caster.ordinal)
  4478.  
  4479.     if skill == 1 then spell.tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects) end
  4480.  
  4481.     -- cause burning condition
  4482.     local chance = spl.duration * (1 + skill * (spl.durationScaling or 0))
  4483.     if skill >= 3 and math.random() < chance then
  4484.         for _,monster in map:componentsAt(MonsterComponent, x, y) do
  4485.             if monster.go.elevation == elevation then
  4486.                 monster:setCondition("burning")
  4487.  
  4488.                 -- mark condition so that exp is awarded if monster is killed by the condition
  4489.                 local burningCondition = monster.go.burning
  4490.                 if burningCondition then
  4491.                     burningCondition:setCausedByChampion(caster.ordinal)
  4492.                 end
  4493.             end
  4494.         end
  4495.     end
  4496.  
  4497.     party:endCondition("invisibility")
  4498.     return spell
  4499. end
  4500.  
  4501. function BuiltInSpell.fireball(caster, x, y, direction, elevation, skill, spl)
  4502.     local spell
  4503.     if skill < 2 then
  4504.         spell = spawn(party.go.map, "fireball_small", x, y, direction, elevation)
  4505.     elseif skill == 2 then
  4506.         spell = spawn(party.go.map, "fireball_medium", x, y, direction, elevation)
  4507.     else
  4508.         spell = spawn(party.go.map, "fireball_large", x, y, direction, elevation)
  4509.     end
  4510.  
  4511.     local power = spl.power + skill * (spl.powerScaling or 0)
  4512.  
  4513.     spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
  4514.     spell.projectile:setAttackPower(power)
  4515.     spell.projectile:setIgnoreEntity(party.go)
  4516.     spell.projectile:setCastByChampion(caster.ordinal)
  4517.     party:endCondition("invisibility")
  4518.     return spell
  4519. end
  4520.  
  4521. function Spell_meteorStorm(casterOrdinal, spreadX, spreadY, spl)
  4522.     if party:isUnderwater() then return end
  4523.    
  4524.     local meteorCount = 5
  4525.  
  4526.     local spell = spawn(party.go.map, "fireball_medium", party.go.x, party.go.y, party.go.facing, party.go.elevation)
  4527.     local caster = party:getChampionByOrdinal(casterOrdinal)
  4528.     local pos = Spell.getCasterPositionInWorld(caster)
  4529.     local skill = caster:getSkillLevel("fire_magic")
  4530.  
  4531.     -- offset position
  4532.     local rdx,rdy = getDxDy((party.go.facing+1)%4)
  4533.     pos.x = pos.x - rdx * spreadX
  4534.     pos.y = pos.y + spreadY
  4535.     pos.z = pos.z + rdy * spreadX
  4536.  
  4537.     spell:setWorldPosition(pos)
  4538.     local power = spl.power + skill * (spl.powerScaling or 0)
  4539.     spell.projectile:setAttackPower(power)
  4540.     spell.projectile:setIgnoreEntity(party.go)
  4541.     spell.projectile:setCastByChampion(casterOrdinal)
  4542. end
  4543.  
  4544. function BuiltInSpell.meteorStorm(caster, x, y, direction, elevation, skill, spl)
  4545.     local meteorCount = 5
  4546.  
  4547.     for i=1,meteorCount do
  4548.         local spreadX = math.random() * 0.5 * iff((i % 2) == 0, 1, -1)
  4549.         local spreadY = -(i / meteorCount - 0.5)
  4550.         messageSystem:delayedFunctionCall("Spell_meteorStorm", (i-1) * 0.15, caster.ordinal, spreadX, spreadY, spl)
  4551.     end
  4552.  
  4553.     party:endCondition("invisibility")
  4554. end
  4555.  
  4556. function BuiltInSpell.fireShield(caster, x, y, direction, elevation, skill, spl)
  4557.     local duration = spl.duration + skill * (spl.durationScaling or 0)
  4558.     local power = spl.power + skill * (spl.powerScaling or 0)
  4559.     Spell.elementalShield("fire_shield", duration, power)
  4560. end
  4561.  
  4562. function BuiltInSpell.iceShards(caster, x, y, direction, elevation, skill, spl)
  4563.     local map = party.go.map
  4564.     x,y = Spell.getBurstTargetTile(x, y, direction)
  4565.     local spell = spawn(party.go.map, "ice_shards", x, y, direction, elevation)
  4566.     local power = spl.power + skill * (spl.powerScaling or 0)
  4567.     spell.tiledamager:setAttackPower(power)
  4568.     local range = spl.duration + skill * (spl.durationScaling or 0)
  4569.     spell.iceshards:setRange(range)
  4570.     spell.tiledamager:setCastByChampion(caster.ordinal)
  4571.     party:endCondition("invisibility")
  4572.  
  4573.     -- cast on invalid space (e.g. empty air)?
  4574.     if not spell.tiledamager:isEnabled() then
  4575.         soundSystem:playSound2D("spell_fizzle")
  4576.     end
  4577. end
  4578.        
  4579. function BuiltInSpell.frostbolt(caster, x, y, direction, elevation, skill, spl)
  4580.     local name = "frostbolt_"..math.clamp(skill, 1, 5)
  4581.     local spell = spawn(party.go.map, name, x, y, direction, elevation)
  4582.     spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
  4583.     local power = spl.power + skill * (spl.powerScaling or 0)
  4584.     spell.projectile:setAttackPower(power)
  4585.     spell.projectile:setIgnoreEntity(party.go)
  4586.     spell.projectile:setCastByChampion(caster.ordinal)
  4587.     party:endCondition("invisibility")
  4588. end
  4589.        
  4590. function BuiltInSpell.frostShield(caster, x, y, direction, elevation, skill, spl)
  4591.     local duration = spl.duration + skill * (spl.durationScaling or 0)
  4592.     local power = spl.power + skill * (spl.powerScaling or 0)
  4593.     Spell.elementalShield("frost_shield", duration, power)
  4594. end
  4595.  
  4596. function BuiltInSpell.shock(caster, x, y, direction, elevation, skill, spl)
  4597.     local map = party.go.map
  4598.     x,y = Spell.getBurstTargetTile(x, y, direction)
  4599.     local spell = spawn(party.go.map, "shockburst", x, y, direction, elevation)
  4600.     local power = spl.power + skill * (spl.powerScaling or 0)
  4601.     spell.tiledamager:setAttackPower(power)
  4602.     spell.tiledamager:setCastByChampion(caster.ordinal)
  4603.  
  4604.     if skill == 1 then spell.tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects) end
  4605.  
  4606.     party:endCondition("invisibility")
  4607. end
  4608.  
  4609. function BuiltInSpell.invisibility(caster, x, y, direction, elevation, skill, spl)
  4610.     local duration = spl.duration + skill * (spl.durationScaling or 0)
  4611.     for i=1,4 do
  4612.         party.champions[i]:setConditionValue("invisibility", duration)
  4613.     end
  4614.     soundSystem:playSound2D("generic_spell")
  4615. end
  4616.  
  4617. function BuiltInSpell.lightningBolt(caster, x, y, direction, elevation, skill, spl)
  4618.     local spell
  4619.     if skill > 1 then
  4620.         spell = spawn(party.go.map, "lightning_bolt_greater", x, y, direction, elevation)
  4621.     else
  4622.         spell = spawn(party.go.map, "lightning_bolt", x, y, direction, elevation)
  4623.     end
  4624.     spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
  4625.     local power = spl.power + skill * (spl.powerScaling or 0)
  4626.     spell.projectile:setAttackPower(power)
  4627.     spell.projectile:setIgnoreEntity(party.go)
  4628.     spell.projectile:setCastByChampion(caster.ordinal)
  4629.     party:endCondition("invisibility")
  4630. end
  4631.  
  4632. function BuiltInSpell.shockShield(caster, x, y, direction, elevation, skill, spl)
  4633.     local duration = spl.duration + skill * (spl.durationScaling or 0)
  4634.     local power = spl.power + skill * (spl.powerScaling or 0)
  4635.     Spell.elementalShield("shock_shield", duration, power)
  4636. end
  4637.  
  4638. function BuiltInSpell.poisonCloud(caster, x, y, direction, elevation, skill, spl)
  4639.     if caster:isEquipped("shaman_staff") then skill = skill + 1 end
  4640.     local map = party.go.map
  4641.     x,y = Spell.getBurstTargetTile(x, y, direction, true)
  4642.     local spell
  4643.     if skill >= 5 then
  4644.         spell = spawn(party.go.map, "poison_cloud_large", x, y, 0, elevation)
  4645.     elseif skill >= 3 then
  4646.         spell = spawn(party.go.map, "poison_cloud_medium", x, y, 0, elevation)
  4647.     else
  4648.         spell = spawn(party.go.map, "poison_cloud_small", x, y, 0, elevation)
  4649.     end
  4650.     local power = spl.power + skill * (spl.powerScaling or 0)
  4651.     spell.cloudspell:setAttackPower(power)
  4652.     spell.cloudspell:setCastByChampion(caster.ordinal)
  4653.     spell.cloudspell:setDamageInterval(math.max(0.8 - skill * 0.4 / 5, 0.2))    -- damage doubles with 5 skill levels
  4654.     spell.cloudspell:combineClouds()
  4655.     party:endCondition("invisibility")
  4656. end
  4657.        
  4658. function BuiltInSpell.poisonBolt(caster, x, y, direction, elevation, skill, spl)
  4659.     if caster:isEquipped("shaman_staff") then skill = skill + 1 end
  4660.     local spell
  4661.     if skill >= 3 then
  4662.         spell = spawn(party.go.map, "poison_bolt_greater", x, y, direction, elevation)
  4663.     else
  4664.         spell = spawn(party.go.map, "poison_bolt", x, y, direction, elevation)
  4665.     end
  4666.     spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
  4667.     local power = spl.power + skill * (spl.powerScaling or 0)
  4668.     spell.projectile:setAttackPower(power)
  4669.     spell.projectile:setIgnoreEntity(party.go)
  4670.     spell.projectile:setCastByChampion(caster.ordinal)
  4671.     party:endCondition("invisibility")
  4672. end
  4673.  
  4674. function BuiltInSpell.poisonShield(caster, x, y, direction, elevation, skill, spl)
  4675.     local duration = spl.duration + skill * (spl.durationScaling or 0)
  4676.     local power = spl.power + skill * (spl.powerScaling or 0)
  4677.     Spell.elementalShield("poison_shield", duration, power)
  4678. end
  4679.  
  4680. function BuiltInSpell.dispel(caster, x, y, direction, elevation, skill, spl)
  4681.     local spell = spawn(party.go.map, "dispel_projectile", x, y, direction, elevation)
  4682.     spell:setWorldPosition(Spell.getCasterPositionInWorld(caster))
  4683.     local power = spl.power + skill * (spl.powerScaling or 0)
  4684.     spell.projectile:setAttackPower(power)
  4685.     spell.projectile:setIgnoreEntity(party.go)
  4686.     spell.projectile:setCastByChampion(caster.ordinal)
  4687. end
  4688.  
  4689. function BuiltInSpell.causeFear(caster, x, y, direction, elevation, skill, spl)
  4690.     local map = party.go.map
  4691.     x,y = Spell.getBurstTargetTile(x, y, direction, true)
  4692.     local power = spl.power + skill * (spl.powerScaling or 0)
  4693.  
  4694.     for _,monster in party.go.map:componentsAt(MonsterComponent, x, y) do
  4695.         if monster:isAlive() and not monster.fleeing then
  4696.             local brain = monster.go.brain
  4697.             if brain then
  4698.                 if math.random(power, 100) > brain.morale then
  4699.                     brain:startFleeing()
  4700.                 else
  4701.                     monster:showDamageText("Resists", Color.White)
  4702.                 end
  4703.             end
  4704.         end
  4705.     end
  4706.  
  4707.     -- hit self?
  4708.     if x == party.go.x and y == party.go.y then
  4709.         for i=1,4 do
  4710.             local ch = party.champions[i]
  4711.             if ch and ch:isAlive() and not ch:hasCondition("paralyzed") and math.random() < 0.5 then
  4712.                 ch:setCondition("paralyzed")
  4713.             end
  4714.         end
  4715.     end
  4716.  
  4717.     local fx = spawn(party.go.map, "particle_system", x, y, 0, elevation)
  4718.     fx.particle:setParticleSystem("fear_cloud")
  4719.     fx.particle:setOffset(vec(0, 1.5, 0))
  4720.  
  4721.     soundSystem:playSound2D("wand_fear")
  4722.  
  4723.     party:endCondition("invisibility")
  4724. end
  4725.  
  4726. function BuiltInSpell.heal(caster, x, y, direction, elevation, skill, spl)
  4727.     local power = spl.power + skill * (spl.powerScaling or 0)
  4728.     for i=1,4 do
  4729.         local champion = party:getChampion(i)
  4730.         if champion:isAlive() then
  4731.             champion:regainHealth(power)
  4732.             champion:playHealingIndicator()
  4733.             soundSystem:playSound2D("heal_party")
  4734.         end
  4735.     end
  4736. end
  4737.  
  4738. function Spell.elementalShield(condition, duration, power)
  4739.     -- cancel existing shields
  4740.     for i=1,4 do
  4741.         party.champions[i]:removeCondition("fire_shield")
  4742.         party.champions[i]:removeCondition("frost_shield")
  4743.         party.champions[i]:removeCondition("poison_shield")
  4744.         party.champions[i]:removeCondition("shock_shield")
  4745.     end
  4746.    
  4747.     for i=1,4 do
  4748.         party.champions[i]:setConditionValue(condition, duration, power)
  4749.     end
  4750.  
  4751.     soundSystem:playSound2D("generic_spell")
  4752. end
  4753.  
  4754. function Dungeon:redefineTraits()
  4755. -------------------------------------------------------------------------------------------------------
  4756. -- Redefining vanilla classes                                                                        --
  4757. -------------------------------------------------------------------------------------------------------
  4758.  
  4759.     defineCharClass{
  4760.         name = "alchemist",
  4761.         uiName = "Alchemist",
  4762.         traits = { "herb_multiplication", "firearm_expert" },
  4763.         optionalTraits = 2,
  4764.     }
  4765.    
  4766.     defineCharClass{
  4767.         name = "barbarian",
  4768.         uiName = "Barbarian",
  4769.         optionalTraits = 2,
  4770.     }
  4771.    
  4772.     defineCharClass{
  4773.         name = "battle_mage",  
  4774.         uiName = "Battle Mage",
  4775.         traits = { "hand_caster", "armor_expert", "staff_defence" },
  4776.         optionalTraits = 2,
  4777.     }
  4778.    
  4779.     defineCharClass{
  4780.         name = "farmer",
  4781.         uiName = "Farmer",
  4782.         skillPoints = 0,
  4783.         optionalTraits = 2,
  4784.     }
  4785.    
  4786.     defineCharClass{
  4787.         name = "fighter",
  4788.         uiName = "Fighter",
  4789.         traits = { "melee_specialist" },
  4790.         optionalTraits = 2,
  4791.     }
  4792.    
  4793.     defineCharClass{
  4794.         name = "knight",
  4795.         uiName = "Knight",
  4796.         traits = { "armor_expert", "shield_expert" },
  4797.         optionalTraits = 2,
  4798.     }
  4799.    
  4800.     defineCharClass{
  4801.         name = "rogue",
  4802.         uiName = "Rogue",
  4803.         traits = { "rogue_dual_wield" },
  4804.         optionalTraits = 2,
  4805.     }
  4806.    
  4807.     defineCharClass{
  4808.         name = "wizard",
  4809.         uiName = "Wizard",
  4810.         traits = { "hand_caster" },
  4811.         optionalTraits = 2,
  4812.     }
  4813.  
  4814. -------------------------------------------------------------------------------------------------------
  4815. -- Redefining vanilla traits                                                                         --    
  4816. -------------------------------------------------------------------------------------------------------
  4817.  
  4818. -- Class traits
  4819.  
  4820. defineTrait{
  4821.     name = "fighter",
  4822.     uiName = "Fighter",
  4823.     icon = 96,
  4824.     description = "As a fighter you are a master of close combat. You are trained to use a wide variety of weapons.",
  4825.     gameEffect = [[
  4826.     - Health 60 (+7 per level), Energy 30 (+3 per level)
  4827.     - Special attacks with melee weapons take half the time to build up and cost 25% less energy.]],
  4828.     onRecomputeStats = function(champion, level)
  4829.         if level > 0 then
  4830.             level = champion:getLevel()
  4831.             champion:addStatModifier("max_health", 60 + (level-1) * 7)
  4832.             champion:addStatModifier("max_energy", 30 + (level-1) * 3)
  4833.         end
  4834.     end,
  4835. }
  4836.  
  4837. defineTrait{
  4838.     name = "barbarian",
  4839.     uiName = "Barbarian",
  4840.     icon = 94,
  4841.     description = "As a barbarian you do not care about finesse in combat. Instead you rely on raw power and speed.",
  4842.     gameEffect = [[
  4843.     - Health 80 (+10 per level), Energy 30 (+3 per level)
  4844.     - Strength +1 per level.]],
  4845.     onRecomputeStats = function(champion, level)
  4846.         if level > 0 then
  4847.             level = champion:getLevel()
  4848.             champion:addStatModifier("strength", level)
  4849.             champion:addStatModifier("max_health", 80 + (level-1) * 10)
  4850.             champion:addStatModifier("max_energy", 30 + (level-1) * 3)
  4851.         end
  4852.     end,
  4853. }
  4854.  
  4855. defineTrait{
  4856.     name = "knight",
  4857.     uiName = "Knight",
  4858.     icon = 97,
  4859.     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.",
  4860.     gameEffect = [[
  4861.     - Health 60 (+7 per level), Energy 30 (+3 per level)
  4862.     - Protection +1 per level.
  4863.     - Weight of equipped armor is reduced by 50%.
  4864.     - Evasion bonus of equipped shields is increased by 50%.]],
  4865.     onRecomputeStats = function(champion, level)
  4866.         if level > 0 then
  4867.             level = champion:getLevel()
  4868.             champion:addStatModifier("max_health", 60 + (level-1) * 7)
  4869.             champion:addStatModifier("max_energy", 30 + (level-1) * 3)
  4870.             champion:addStatModifier("protection", level)
  4871.         end
  4872.     end,
  4873. }
  4874.  
  4875. defineTrait{
  4876.     name = "rogue",
  4877.     uiName = "Rogue",
  4878.     icon = 95,
  4879.     description = "As a rogue you are a stealthy warrior who prefers to use ranged weapons or light melee weapons.",
  4880.     gameEffect = [[
  4881.     - Health 45 (+5 per level), Energy 40 (+5 per level)
  4882.     - When dual wielding you suffer only 25% penalty to weapon damage (normally 40%).
  4883.     - +1% chance per level to score a critical hit with missile or throwing weapons.]],
  4884.     onRecomputeStats = function(champion, level)
  4885.         if level > 0 then
  4886.             level = champion:getLevel()
  4887.             champion:addStatModifier("max_health", 45 + (level-1) * 5)
  4888.             champion:addStatModifier("max_energy", 40 + (level-1) * 5)
  4889.         end
  4890.     end,
  4891.     onComputeCritChance = function(champion, weapon, attack, attackType, level, monster, accuracy)
  4892.         if level > 0 and (attackType == "throw" or attackType == "missile") then return champion:getLevel() end
  4893.     end,
  4894. }
  4895.  
  4896. defineTrait{
  4897.     name = "wizard",
  4898.     uiName = "Wizard",
  4899.     icon = 33,
  4900.     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.",
  4901.     gameEffect = [[
  4902.     - Health 35 (+3 per level), Energy 50 (+7 per level)
  4903.     - Willpower +2
  4904.     - You can cast spells with bare hands.]],
  4905.     onRecomputeStats = function(champion, level)
  4906.         if level > 0 then
  4907.             level = champion:getLevel()
  4908.             champion:addStatModifier("willpower", 2)
  4909.             champion:addStatModifier("max_health", 35 + (level-1) * 3)
  4910.             champion:addStatModifier("max_energy", 50 + (level-1) * 7)
  4911.         end
  4912.     end,
  4913. }
  4914.  
  4915. defineTrait{
  4916.     name = "battle_mage",
  4917.     uiName = "Battle Mage",
  4918.     icon = 98,
  4919.     description = "As a battle mage you are comfortable with fighting in the front row as well as blasting with spells from the back row.",
  4920.     gameEffect = [[
  4921.     - Health 50 (+5 per level), Energy 50 (+5 per level)
  4922.     - Weight of equipped armor is reduced by 50%.
  4923.     - You can cast spells with bare hands.
  4924.     - You gain Protection +10 and Resist All +10 when equipped with a magical staff or an orb.]],
  4925.     onRecomputeStats = function(champion, level)
  4926.         if level > 0 then
  4927.             level = champion:getLevel()
  4928.             champion:addStatModifier("max_health", 50 + (level-1) * 5)
  4929.             champion:addStatModifier("max_energy", 50 + (level-1) * 5)
  4930.         end
  4931.     end,
  4932. }
  4933.  
  4934. defineTrait{
  4935.     name = "alchemist",
  4936.     uiName = "Alchemist",
  4937.     icon = 92,
  4938.     description = "As an alchemist you brew potions and defend yourself in combat by wielding firearms.",
  4939.     gameEffect = [[
  4940.     - Health 50 (+6 per level), Energy 50 (+4 per level)
  4941.     - Herbs in your inventory multiply. The growth rate is determined by the number of steps taken.
  4942.     - Firearms have 50% less chance to malfunction.]],
  4943.     onRecomputeStats = function(champion, level)
  4944.         if level > 0 then
  4945.             level = champion:getLevel()
  4946.             champion:addStatModifier("max_health", 50 + (level-1) * 6)
  4947.             champion:addStatModifier("max_energy", 50 + (level-1) * 4)
  4948.         end
  4949.     end,
  4950. }
  4951.  
  4952. defineTrait{
  4953.     name = "farmer",
  4954.     uiName = "Farmer",
  4955.     icon = 93,
  4956.     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.",
  4957.     gameEffect = [[
  4958.     - Health 30 (+5 per level), Energy 30 (+5 per level)
  4959.     - You receive no skillpoints at first level.
  4960.     - Instead of slaying monsters you gain experience points by eating food.]],
  4961.     onRecomputeStats = function(champion, level)
  4962.         if level > 0 then
  4963.             level = champion:getLevel()
  4964.             champion:addStatModifier("max_health", 30 + (level-1) * 5)
  4965.             champion:addStatModifier("max_energy", 30 + (level-1) * 5)
  4966.         end
  4967.     end,
  4968. }
  4969.  
  4970.     -- Race traits
  4971.  
  4972.     defineTrait{
  4973.         name = "human",
  4974.         uiName = "Human",
  4975.         icon = 37,
  4976.         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.",
  4977.         gameEffect = "- You gain experience points 10% faster.",
  4978.         onRecomputeStats = function(champion, level)
  4979.             if level > 0 then
  4980.                 champion:addStatModifier("exp_rate", 10)
  4981.             end
  4982.         end,   
  4983.     }
  4984.  
  4985.     defineTrait{
  4986.         name = "minotaur",
  4987.         uiName = "Minotaur",
  4988.         icon = 38,
  4989.         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.",
  4990.         gameEffect = "- Strength +5, Dexterity -4, Vitality +4, Willpower -3.\n- Your food consumption rate is 25% higher than normal.",
  4991.         onRecomputeStats =