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