1# -*-python-*- 2# GemRB - Infinity Engine Emulator 3# Copyright (C) 2009 The GemRB Project 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18# 19# 20# LUCommon.py - common functions related to leveling up 21 22import GemRB 23import GameCheck 24import GUICommon 25import CommonTables 26from GUIDefines import * 27from ie_stats import * 28from ie_feats import * 29 30def GetNextLevelExp (Level, Class): 31 """Returns the amount of XP required to gain the next level.""" 32 Row = CommonTables.NextLevel.GetRowIndex (Class) 33 if Level < CommonTables.NextLevel.GetColumnCount (Row): 34 return CommonTables.NextLevel.GetValue (Row, Level, GTV_INT) 35 36 # we could display the current level's max, but likely nobody cares 37 # if you change it, check that all callers can handle it 38 return 0 39 40def GetNextLevels (actor, Classes): 41 """Returns the next level the PC will attain based on current XP""" 42 43 Level = [0]*3 44 45 # Used only for TNO, all other games divide XP/Number of classes 46 XPStat = [IE_XP, IE_XP_MAGE, IE_XP_THIEF] 47 48 # Dual class character only care about the first class 49 # However the inactive class is needed to handle reactivation 50 if GUICommon.IsDualClassed (actor, False)[0]: 51 NumClasses = 1 52 if GUICommon.IsDualSwap(actor): 53 Level[1] = GemRB.GetPlayerStat (actor, IE_LEVEL) 54 else: 55 Level[1] = GemRB.GetPlayerStat (actor, IE_LEVEL2) 56 else: 57 if GUICommon.IsNamelessOne (actor): 58 # no guaranteed class-getting order 59 NumClasses = 3 60 else: 61 NumClasses = len([x for x in Classes if x > 0]) 62 63 for i in range(NumClasses): 64 # Get the next level we will use to look up new stats from the 2da tables 65 if GUICommon.IsNamelessOne(actor): 66 #TNO is the only character in all games that tracks individual XP stats 67 Level[i] = GetNextLevelFromExp (GemRB.GetPlayerStat (actor, XPStat[i]), Classes[i]) 68 else: 69 # In all other cases, XP cut exactly by number of classes 70 Level[i] = GetNextLevelFromExp (GemRB.GetPlayerStat (actor, IE_XP) // NumClasses, Classes[i]) 71 72 return Level 73 74def GetLevelDiff (actor, Level): 75 """Calculate the number of new levels based on current levels""" 76 77 LevelDiff = [0]*3 78 LevelStat = [IE_LEVEL, IE_LEVEL2, IE_LEVEL3] 79 80 # Dual Swap: since dual class characters are based on mc combinations 81 # the level stat used by the 'new' class must be the one that 82 # matches the multiclass combo. EG a Fighter that duals to a Mage 83 # will now advance the IE_LEVEL2 stat since he will be FIGHTER_MAGE 84 85 if (GUICommon.IsDualSwap(actor)): 86 LevelStat = [IE_LEVEL2, IE_LEVEL, IE_LEVEL3] 87 88 for i in range(len(Level)): 89 LevelDiff[i] = max(0, Level[i] - GemRB.GetPlayerStat (actor, LevelStat[i])) 90 91 return LevelDiff 92 93def CanLevelUp(actor): 94 """Returns true if the actor can level up.""" 95 96 # get our class and placements for Multi'd and Dual'd characters 97 Class = GUICommon.GetClassRowName (actor) 98 Multi = GUICommon.IsMultiClassed (actor, 1) 99 Dual = GUICommon.IsDualClassed (actor, 1) 100 101 # get all the levels and overall xp here 102 xp = GemRB.GetPlayerStat (actor, IE_XP) 103 Levels = [GemRB.GetPlayerStat (actor, IE_LEVEL), GemRB.GetPlayerStat (actor, IE_LEVEL2),\ 104 GemRB.GetPlayerStat (actor, IE_LEVEL3)] 105 106 if GemRB.GetPlayerStat(actor, IE_LEVELDRAIN)>0: 107 return 0 108 109 if GameCheck.IsIWD2(): 110 import GUIREC 111 levelsum = GemRB.GetPlayerStat (actor, IE_CLASSLEVELSUM) 112 next = GUIREC.GetNextLevelExp(levelsum, GUIREC.GetECL(actor)) 113 return next <= xp 114 115 # hardcoded special case to handle TNO, who is usually a single class 116 # but with three separate Levels/XP values and the ability to switch between them 117 # it returns the active class if true 118 SwitcherClass = GUICommon.NamelessOneClass(actor) 119 if SwitcherClass: 120 xp = { "FIGHTER" : GemRB.GetPlayerStat (actor, IE_XP), "MAGE" : GemRB.GetPlayerStat (actor, IE_XP_MAGE), "THIEF" : GemRB.GetPlayerStat (actor, IE_XP_THIEF) } 121 lvls = { "FIGHTER" : Levels[0] , "MAGE": Levels[1], "THIEF": Levels [2] } 122 123 tmpNext = GetNextLevelExp (lvls[SwitcherClass], SwitcherClass) 124 if (tmpNext != 0 or lvls[SwitcherClass] == 0) and tmpNext <= xp[SwitcherClass]: 125 return 1 126 #ignore the rest of this function, to avoid false positives 127 #other classes can only be achieved by hacking the game somehow 128 return 0 129 130 if Multi[0] > 1: # multiclassed 131 xp = xp // Multi[0] # divide the xp evenly between the classes 132 for i in range (Multi[0]): 133 # if any class can level, return 1 134 TmpClassName = GUICommon.GetClassRowName (Multi[i+1], "class") 135 tmpNext = GetNextLevelExp (Levels[i], TmpClassName) 136 if (tmpNext != 0 or Levels[i] == 0) and tmpNext <= xp: 137 return 1 138 139 # didn't find a class that could level 140 return 0 141 elif Dual[0] > 0: # dual classed 142 # get the class we can level 143 if Dual[0] == 3: 144 ClassID = CommonTables.KitList.GetValue (Dual[2], 7) 145 Class = GUICommon.GetClassRowName (ClassID, "class") 146 else: 147 Class = GUICommon.GetClassRowName(Dual[2], "index") 148 if GUICommon.IsDualSwap(actor): 149 Levels = [Levels[1], Levels[0], Levels[2]] 150 151 # check the class that can be level (single or dual) 152 tmpNext = GetNextLevelExp (Levels[0], Class) 153 return ((tmpNext != 0 or Levels[0] == 0) and tmpNext <= xp) 154 155# expects a list of character levels of all classes 156# returns sparse list of class ids (of same length) 157def GetAllClasses (Levels): 158 Class = [0]*len(Levels) 159 for c in range(len(Levels)): 160 if Levels[c] > 0: 161 # level stats are implicitly keyed by class id 162 Class[c] = c + 1 163 return Class 164 165# internal shared function for the various Setup* stat updaters 166def _SetupLevels (pc, Level, offset=0, noclass=0): 167 #storing levels as an array makes them easier to deal with 168 if not Level: 169 Levels = [IE_LEVEL, IE_LEVEL2, IE_LEVEL3] 170 if GameCheck.IsIWD2(): 171 import IDLUCommon 172 Levels = IDLUCommon.Levels 173 Levels = [ GemRB.GetPlayerStat (pc, l)+offset for l in Levels ] 174 else: 175 Levels = [] 176 for level in Level: 177 Levels.append (level+offset) 178 179 if noclass: 180 return Levels, -1, -1 181 182 # adjust the class for multi/dual chars 183 Class = [GemRB.GetPlayerStat (pc, IE_CLASS)] 184 Multi = GUICommon.IsMultiClassed (pc, 1) 185 Dual = GUICommon.IsDualClassed (pc, 1) 186 NumClasses = 1 187 if Multi[0]>1: #get each of the multi-classes 188 NumClasses = Multi[0] 189 Class = [Multi[1], Multi[2], Multi[3]] 190 elif Dual[0]: #only worry about the newer class 191 if Dual[0] == 3: 192 Class = [CommonTables.KitList.GetValue (Dual[2], 7)] 193 else: 194 ClassRow = GUICommon.GetClassRowName(Dual[2], "index") 195 Class = [CommonTables.Classes.GetValue (ClassRow, "ID")] 196 #assume Level is correct if passed 197 if GUICommon.IsDualSwap(pc) and not Level: 198 Levels = [Levels[1], Levels[0], Levels[2]] 199 elif GameCheck.IsIWD2(): 200 Class = GetAllClasses (Levels) 201 202 return Levels, NumClasses, Class 203 204def SetupSavingThrows (pc, Level=None): 205 """Updates an actors saving throws based upon level. 206 207 Level should contain the actors current level. 208 If Level is None, it is filled with the actors current level.""" 209 210 Levels, NumClasses, Class = _SetupLevels (pc, Level, -1) 211 if NumClasses > len(Levels): 212 return 213 214 #get some basic values 215 Race = GemRB.GetPlayerStat (pc, IE_RACE) 216 217 #see if we can add racial bonuses to saves 218 Race = CommonTables.Races.GetRowName (CommonTables.Races.FindValue (3, Race) ) 219 RaceSaveTableName = CommonTables.Races.GetValue (Race, "SAVE", GTV_STR) 220 RaceSaveTable = None 221 if RaceSaveTableName != "-1" and RaceSaveTableName != "*": 222 Con = GemRB.GetPlayerStat (pc, IE_CON, 1)-1 223 RaceSaveTable = GemRB.LoadTable (RaceSaveTableName) 224 if Con >= RaceSaveTable.GetColumnCount (): 225 Con = RaceSaveTable.GetColumnCount () - 1 226 227 #preload our tables to limit multi-classed lookups 228 SaveTables = [] 229 ClassBonus = 0 230 for i in range (NumClasses): 231 RowName = GUICommon.GetClassRowName (Class[i], "class") 232 SaveName = CommonTables.Classes.GetValue (RowName, "SAVE", GTV_STR) 233 SaveTables.append (GemRB.LoadTable (SaveName) ) 234 #use numeric value 235 ClassBonus += CommonTables.ClassSkills.GetValue (RowName, "SAVEBONUS", GTV_INT) 236 237 if not len (SaveTables): 238 return 239 240 #make sure to limit the levels to the table allowable 241 MaxLevel = SaveTables[0].GetColumnCount ()-1 242 for i in range (len(Levels)): 243 if Levels[i] > MaxLevel: 244 Levels[i] = MaxLevel 245 246 #save the saves 247 for row in range (5): 248 CurrentSave = GemRB.GetPlayerStat(pc, IE_SAVEVSDEATH+i, 1) 249 for i in range (NumClasses): 250 #loop through each class and update the save value if we have 251 #a better save 252 TmpSave = SaveTables[i].GetValue (row, Levels[i]) 253 if TmpSave and (TmpSave < CurrentSave or i == 0): 254 CurrentSave = TmpSave 255 256 #add racial bonuses if applicable (small pc's) 257 if RaceSaveTable: 258 CurrentSave -= RaceSaveTable.GetValue (row, Con) 259 260 #add class bonuses if applicable (paladin) 261 CurrentSave -= ClassBonus 262 GemRB.SetPlayerStat (pc, IE_SAVEVSDEATH+row, CurrentSave) 263 return 264 265def GetNextLevelFromExp (XP, Class): 266 """Gets the next level based on current experience.""" 267 268 ClassName = GUICommon.GetClassRowName (Class, "class") 269 Row = CommonTables.NextLevel.GetRowIndex (ClassName) 270 NLNumCols = CommonTables.NextLevel.GetColumnCount() 271 for i in range(1, NLNumCols): 272 if XP < CommonTables.NextLevel.GetValue (Row, i): 273 return i 274 # fix hacked characters that have more xp than the xp cap 275 return NLNumCols 276 277def SetupThaco (pc, Level=None): 278 """Updates an actors THAC0 based upon level. 279 280 Level should contain the actors current level. 281 If Level is None it is filled with the actors current level.""" 282 283 Levels, NumClasses, Class = _SetupLevels (pc, Level, -1) 284 if NumClasses > len(Levels): 285 return 286 287 #get some basic values 288 ThacoTable = GemRB.LoadTable ("THAC0") 289 290 #make sure to limit the levels to the table allowable 291 MaxLevel = ThacoTable.GetColumnCount ()-1 292 for i in range (len(Levels)): 293 if Levels[i] > MaxLevel: 294 Levels[i] = MaxLevel 295 296 CurrentThaco = GemRB.GetPlayerStat (pc, IE_TOHIT, 1) 297 NewThaco = 0 298 for i in range (NumClasses): 299 #loop through each class and update the save value if we have 300 #a better thac0 301 ClassName = GUICommon.GetClassRowName (Class[i], "class") 302 TmpThaco = ThacoTable.GetValue (ClassName, str(Levels[i]+1)) 303 if TmpThaco < CurrentThaco: 304 NewThaco = 1 305 CurrentThaco = TmpThaco 306 307 #only update if we have a better thac0 308 if NewThaco: 309 GemRB.SetPlayerStat (pc, IE_TOHIT, CurrentThaco) 310 return 311 312def SetupLore (pc, LevelDiff=None): 313 """Updates an actors lore based upon level. 314 315 Level should contain the actors current level. 316 LevelDiff should contain the change in levels. 317 Level and LevelDiff must be of the same length. 318 If either are None, they are filled with the actors current level.""" 319 320 LevelDiffs, NumClasses, Class = _SetupLevels (pc, LevelDiff) 321 if NumClasses > len(LevelDiffs): 322 return 323 324 #get some basic values 325 LoreTable = GemRB.LoadTable ("lore") 326 327 #loop through each class and update the lore value if we have 328 CurrentLore = GemRB.GetPlayerStat (pc, IE_LORE, 1) 329 for i in range (NumClasses): 330 #correct unlisted progressions 331 ClassName = GUICommon.GetClassRowName (Class[i], "class") 332 if ClassName == "SORCERER": 333 ClassName = "MAGE" 334 elif ClassName == "MONK": #monks have a rate of 1, so this is arbitrary 335 ClassName = "CLERIC" 336 337 #add the lore from this class to the total lore 338 TmpLore = LevelDiffs[i] * LoreTable.GetValue (ClassName, "RATE", GTV_INT) 339 if TmpLore: 340 CurrentLore += TmpLore 341 342 #update our lore value 343 GemRB.SetPlayerStat (pc, IE_LORE, CurrentLore) 344 return 345 346def SetupHP (pc, Level=None, LevelDiff=None): 347 """Updates an actors hp based upon level. 348 349 Level should contain the actors current level. 350 LevelDiff should contain the change in levels. 351 Level and LevelDiff must be of the same length. 352 If either are None, they are filled with the actors current level.""" 353 354 Levels, NumClasses, Class = _SetupLevels (pc, Level, noclass=1) 355 LevelDiffs, NumClasses, Class = _SetupLevels (pc, LevelDiff, noclass=1) 356 if len (Levels) != len (LevelDiffs): 357 return 358 359 #adjust the class for multi/dual chars 360 Class = [GemRB.GetPlayerStat (pc, IE_CLASS)] 361 Multi = GUICommon.IsMultiClassed (pc, 1) 362 Dual = GUICommon.IsDualClassed (pc, 1) 363 NumClasses = 1 364 if Multi[0]>1: #get each of the multi-classes 365 NumClasses = Multi[0] 366 Class = [Multi[1], Multi[2], Multi[3]] 367 elif Dual[0]: #only worry about the newer class 368 #we only get the hp bonus if the old class is reactivated 369 if (Levels[0]<=Levels[1]): 370 return 371 if Dual[0] == 3: 372 Class = [CommonTables.KitList.GetValue (Dual[2], 7)] 373 else: 374 ClassRow = GUICommon.GetClassRowName(Dual[2], "index") 375 Class = [CommonTables.Classes.GetValue (ClassRow, "ID")] 376 #if Level and LevelDiff are passed, we assume it is correct 377 if GUICommon.IsDualSwap(pc) and not Level and not LevelDiff: 378 LevelDiffs = [LevelDiffs[1], LevelDiffs[0], LevelDiffs[2]] 379 elif GameCheck.IsIWD2(): 380 Class = GetAllClasses (Levels) 381 if NumClasses>len(Levels): 382 return 383 384 #get the correct hp for barbarians 385 Kit = GUICommon.GetKitIndex (pc) 386 ClassName = None 387 if Kit and not Dual[0] and Multi[0]<2: 388 KitName = CommonTables.KitList.GetValue (Kit, 0, GTV_STR) 389 if CommonTables.Classes.GetRowIndex (KitName) >= 0: 390 ClassName = KitName 391 392 # determine the minimum hp roll 393 ConBonTable = GemRB.LoadTable ("hpconbon") 394 MinRoll = ConBonTable.GetValue (GemRB.GetPlayerStat (pc, IE_CON)-1, 2) # MIN_ROLL column 395 396 #loop through each class and update the hp 397 OldHP = GemRB.GetPlayerStat (pc, IE_MAXHITPOINTS, 1) 398 CurrentHP = 0 399 Divisor = float (NumClasses) 400 if GameCheck.IsIWD2(): 401 # hack around so we can reuse more of the main loop 402 NumClasses = len(Levels) 403 Divisor = 1.0 404 for i in range (NumClasses): 405 if GameCheck.IsIWD2() and not Class[i]: 406 continue 407 408 #check this classes hp table for any gain 409 if not ClassName or NumClasses > 1: 410 ClassName = GUICommon.GetClassRowName (Class[i], "class") 411 HPTable = CommonTables.Classes.GetValue (ClassName, "HP") 412 HPTable = GemRB.LoadTable (HPTable) 413 414 #make sure we are within table ranges 415 MaxLevel = HPTable.GetRowCount()-1 416 LowLevel = Levels[i]-LevelDiffs[i] 417 HiLevel = Levels[i] 418 if LowLevel >= HiLevel: 419 continue 420 if LowLevel < 0: 421 LowLevel = 0 422 elif LowLevel > MaxLevel: 423 LowLevel = MaxLevel 424 if HiLevel < 0: 425 HiLevel = 0 426 elif HiLevel > MaxLevel: 427 HiLevel = MaxLevel 428 429 #add all the hp for the given level 430 #we use ceil to ensure each class gets hp 431 for level in range(LowLevel, HiLevel): 432 sides = HPTable.GetValue (level, 0) 433 rolls = HPTable.GetValue (level, 1) 434 bonus = HPTable.GetValue (level, 2) 435 436 # we only do a roll on core difficulty or higher 437 # and if maximum HP rolls (bg2 and later) are disabled 438 # and/or if it is bg1 chargen (I guess too many testers got annoyed) 439 # BUT when we do roll, constitution gives a kind of a luck bonus to the roll 440 if rolls: 441 if GemRB.GetVar ("Difficulty Level") >= 3 and not GemRB.GetVar ("Maximum HP") \ 442 and not (GameCheck.IsBG1() and LowLevel == 0) and MinRoll < sides: 443 if MinRoll > 1: 444 roll = GemRB.Roll (rolls, sides, bonus) 445 if roll-bonus < MinRoll: 446 roll = MinRoll + bonus 447 AddedHP = int (roll / Divisor + 0.5) 448 else: 449 AddedHP = int (GemRB.Roll (rolls, sides, bonus) / Divisor + 0.5) 450 else: 451 AddedHP = int ((rolls * sides + bonus) / Divisor + 0.5) 452 else: 453 AddedHP = int (bonus / Divisor + 0.5) 454 # ensure atleast 1hp is given 455 # this is safe for inactive dualclass levels too (handled above) 456 if AddedHP == 0: 457 AddedHP = 1 458 CurrentHP += AddedHP 459 460 #update our hp values 461 GemRB.SetPlayerStat (pc, IE_MAXHITPOINTS, CurrentHP+OldHP) 462 # HACK: account also for the new constitution bonus for the current hitpoints 463 GemRB.SetPlayerStat (pc, IE_HITPOINTS, GemRB.GetPlayerStat (pc, IE_HITPOINTS, 1)+CurrentHP+5) 464 return 465 466def ApplyFeats(MyChar): 467 468 #don't mess with feats outside of IWD2 469 if not GameCheck.IsIWD2(): 470 return 471 472 #feats giving a single innate ability 473 SetSpell(MyChar, "SPIN111", FEAT_WILDSHAPE_BOAR) 474 SetSpell(MyChar, "SPIN197", FEAT_MAXIMIZED_ATTACKS) 475 SetSpell(MyChar, "SPIN231", FEAT_ENVENOM_WEAPON) 476 SetSpell(MyChar, "SPIN245", FEAT_WILDSHAPE_PANTHER) 477 SetSpell(MyChar, "SPIN246", FEAT_WILDSHAPE_SHAMBLER) 478 SetSpell(MyChar, "SPIN275", FEAT_POWER_ATTACK) 479 SetSpell(MyChar, "SPIN276", FEAT_EXPERTISE) 480 SetSpell(MyChar, "SPIN277", FEAT_ARTERIAL_STRIKE) 481 SetSpell(MyChar, "SPIN278", FEAT_HAMSTRING) 482 SetSpell(MyChar, "SPIN279", FEAT_RAPID_SHOT) 483 484 #extra rage 485 level = GemRB.GetPlayerStat(MyChar, IE_LEVELBARBARIAN) 486 if level>0: 487 if level>=15: 488 GemRB.RemoveSpell(MyChar, "SPIN236") 489 Spell = "SPIN260" 490 else: 491 GemRB.RemoveSpell(MyChar, "SPIN260") 492 Spell = "SPIN236" 493 cnt = GemRB.GetPlayerStat (MyChar, IE_FEAT_EXTRA_RAGE) + (level + 3) // 4 494 GUICommon.MakeSpellCount(MyChar, Spell, cnt) 495 else: 496 GemRB.RemoveSpell(MyChar, "SPIN236") 497 GemRB.RemoveSpell(MyChar, "SPIN260") 498 499 #extra smiting 500 level = GemRB.GetPlayerStat(MyChar, IE_LEVELPALADIN) 501 if level>1: 502 cnt = GemRB.GetPlayerStat (MyChar, IE_FEAT_EXTRA_SMITING) + 1 503 GUICommon.MakeSpellCount(MyChar, "SPIN152", cnt) 504 else: 505 GemRB.RemoveSpell(MyChar, "SPIN152") 506 507 #extra turning 508 level = GemRB.GetPlayerStat(MyChar, IE_TURNUNDEADLEVEL) 509 if level>0: 510 cnt = GUICommon.GetAbilityBonus(MyChar, IE_CHR) + 3 511 if cnt<1: cnt = 1 512 cnt += GemRB.GetPlayerStat (MyChar, IE_FEAT_EXTRA_TURNING) 513 GUICommon.MakeSpellCount(MyChar, "SPIN970", cnt) 514 else: 515 GemRB.RemoveSpell(MyChar, "SPIN970") 516 517 #stunning fist 518 if GemRB.HasFeat (MyChar, FEAT_STUNNING_FIST): 519 cnt = GemRB.GetPlayerStat(MyChar, IE_CLASSLEVELSUM) // 4 520 GUICommon.MakeSpellCount(MyChar, "SPIN232", cnt) 521 else: 522 GemRB.RemoveSpell(MyChar, "SPIN232") 523 524 #remove any previous SPLFOCUS 525 #GemRB.ApplyEffect(MyChar, "RemoveEffects",0,0,"SPLFOCUS") 526 #spell focus stats 527 SPLFocusTable = GemRB.LoadTable ("splfocus") 528 for i in range(SPLFocusTable.GetRowCount()): 529 Row = SPLFocusTable.GetRowName(i) 530 Stat = SPLFocusTable.GetValue(Row, "STAT", GTV_STAT) 531 if Stat: 532 Column = GemRB.GetPlayerStat(MyChar, Stat) 533 if Column: 534 Value = SPLFocusTable.GetValue(i, Column) 535 if Value: 536 #add the effect, value could be 2 or 4, timing mode is 8 - so it is not saved 537 GemRB.ApplyEffect(MyChar, "SpellFocus", Value, i,"","","","SPLFOCUS", 8) 538 return 539 540def SetSpell(pc, SpellName, Feat): 541 if GemRB.HasFeat (pc, Feat): 542 GUICommon.MakeSpellCount(pc, SpellName, 1) 543 else: 544 GemRB.RemoveSpell(pc, SpellName) 545 return 546 547