Code snippet: clean skill point refund

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
Post Reply
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Code snippet: clean skill point refund

Post by minmay »

I've played quite a few Grimrock dungeons that have characters changing classes mid-game, and more recently some that allow you to "respec" characters by refunding their skill points.
Unfortunately, most of these just naively called Champion:setClass() without doing much else. This leaves the champion with all the extra bonuses they earned from their old skills - health, energy, strength, dexterity, vitality, willpower, and traits like quick strike and combat caster.
All of this is easily avoidable! The only thing that isn't trivial to fix is the champion's collected experience, since as far as I can tell the script interface doesn't have access to that - only their level. You could track it via hooks on every monster (and any other source of experience), but I won't address that here.

This script allows you to cleanly change the class (or reset the skill points) of a champion. Just stick this code in a script entity in your dungeon, and whenever you get the urge to call Champion:setClass(), call the script's cleanSetClass() function instead.

Code: Select all

-- Clean setClass
-- Version 1.0
-- Written by Andrew Minton (minmay)
-- I place this script in the public domain to be used however you wish.
-- If you don't believe code can be in the public domain, feel free to
-- interpret under any version of the GNU GPL or the WTFPL.
-- You can even remove this comment if you want!
--
-- Usage: When you want to change a champion's class, call
-- cleanSetClass(champion,class,refundPoints)
-- For example, "cleanSetClass(party:getChampion(1),"Mage",true)
--
-- champion must be a champion.
-- class must be "Ranger", "Fighter", "Rogue", or "Mage".
-- If refundPoints is present, the champion will get any spent skill points
-- back. If the refundPoints parameter is false or absent, spent skill points
-- will be lost (but unspent skill points are still kept).
-- This function can also be used to allow champions to "respec" by refunding
-- their skill points and keeping their original class. To allow a champion
-- to respec, use a line like this:
-- cleanSetClass(party:getChampion(1),party:getChampion(1):getClass(),true)
--
-- Like Champion:setClass(), cleanSetClass() leaves the champion with all
-- skills at 0. Unlike Champion:setClass(), it will also remove all stats
-- and traits gained from skills, and retain the champion's experience level
-- (although they will lose any experience gained between their current level
-- and the next).
-- Note that in order to retain the champion's experience level, the champion
-- has to be leveled up a number of times equal to their current level minus one.
-- If your mod has onLevelUp hooks, make sure that you account for this.

SKILL_BONUSES = {
	air_magic = {
		[5]={resist_shock=10},
		[11]={dexterity=1},
		[17]={resist_shock=10},
		[25]={dexterity=1},
		[27]="greater_lightning_bolt",
		[30]={resist_shock=10},
		[32]="air_circle",
		[35]={dexterity=1},
		[39]={resist_shock=10},
		[44]={dexterity=1},
		[50]={resist_shock=100},
	},
	armors = {
		[2]={protection=1},
		[5]={health=10},
		[8]="light_armor",
		[12]={health=10},
		[16]="heavy_armor",
		[19]={health=15},
		[22]={protection=2},
		[25]="shield_expert",
		[28]={health=15},
		[33]={evasion=5},
		[35]={health=25},
		[38]={protection=2},
		[44]={health=25},
		[50]={protection=25}
	},
	assassination = {
		[4]={strength=1},
		[8]="backstab",
		[12]="reach_attack",
		[16]={strength=2},
		[20]="improved_backstab",
		[23]="quick_strike",
		[27]={strength=2},
		[31]="improved_critical",
		[35]="piercing_attack",
		[40]={strength=2},
		[45]="improved_quick_strike",
		[50]="assassin_master"
	},
	athletics = {
		[2]={strength=1},
		[5]={vitality=2},
		[8]={health=10},
		[10]="endurance",
		[12]={dexterity=2},
		[16]={strength=2},
		[20]="porter",
		[22]={vitality=2},
		[24]={dexterity=2},
		[28]={health=10},
		[30]={resist_fire=10,resist_cold=10},
		[33]={strength=2},
		[38]={vitality=2},
		[40]={resist_poison=10,resist_shock=10},
		[45]={health=10},
		[50]={health=100}
	},
	axes = {
		[4]={strength=1},
		[7]={health=5},
		[10]="chop",
		[14]={health=5},
		[18]={strength=1},
		[22]="cleave",
		[26]={health=5},
		[29]={strength=1},
		[33]="rampage",
		[38]={health=5},
		[42]={strength=1},
		[46]={health=5},
		[50]="axe_master",
	},
	daggers = {
		[4]={dexterity=1},
		[7]={energy=5},
		[10]="stab",
		[14]={energy=5},
		[18]={dexterity=1},
		[22]="piercing_strike",
		[26]={energy=5},
		[29]={dexterity=1},
		[33]="flurry_slashes",
		[38]={energy=5},
		[42]={dexterity=1},
		[46]={energy=5},
		[50]="dagger_master"
	},
	dodge = {
		[2]={evasion=5},
		[5]={resist_fire=5,resist_shock=5},
		[8]={health=15},
		[11]="stealth",
		[14]={evasion=5},
		[17]="light_armor",
		[20]={health=15},
		[24]="improved_stealth",
		[28]={resist_poison=20},
		[32]={evasion=5},
		[34]={resist_fire=25,resist_shock=25},
		[38]={health=15},
		[42]={evasion=5},
		[46]={health=15},
		[50]={evasion=50}
	},
	earth_magic = {
		[9]={resist_poison=10},
		[17]={vitality=1},
		[19]={resist_poison=10},
		[23]="improved_poison_bolt",
		[26]={vitality=1},
		[29]={resist_poison=10},
		[32]="earth_circle",
		[35]={vitality=1},
		[39]={resist_poison=10},
		[44]={vitality=1},
		[50]={resist_poison=100}
	},
	fire_magic = {
		[6]={resist_fire=10},
		[10]={strength=1},
		[19]={resist_fire=10},
		[21]={strength=1},
		[24]="greater_fireball",
		[28]={resist_fire=10},
		[32]="fire_circle",
		[35]={strength=1},
		[39]={resist_fire=10},
		[44]={strength=1},
		[50]={resist_fire=100}	
	},
	ice_magic = {
		[5]={resist_cold=10},
		[10]={willpower=1},
		[16]={resist_cold=10},
		[21]={willpower=1},
		[24]="improved_frostbolt",
		[28]={resist_cold=10},
		[32]="ice_circle",
		[35]={willpower=1},
		[39]={resist_cold=10},
		[44]={willpower=1},
		[50]={resist_cold=100},
	},
	maces = {
		[4]={vitality=1},
		[7]={health=5},
		[10]="bash",
		[13]={vitality=1},
		[17]={health=5},
		[20]="crushing_blow",
		[24]={vitality=1},
		[28]={health=5},
		[33]="devastating_blow",
		[38]={vitality=1},
		[42]={health=5},
		[46]={vitality=1},
		[50]="mace_master"
	},
	missile_weapons = {
		[4]={dexterity=1},
		[8]={energy=5},
		[12]="quick_shot",
		[16]={dexterity=1},
		[20]={energy=5},
		[24]="improved_quick_shot",
		[28]={energy=5},
		[32]="volley",
		[36]={dexterity=1},
		[40]={energy=5},
		[45]={dexterity=1},
		[50]="master_archer"
	},
	spellcraft = {
		[2]={willpower=1},
		[6]={energy=10},
		[8]={willpower=1},
		[10]="combat_caster",
		[12]={energy=10},
		[15]={willpower=1},
		[18]="improved_combat_caster",
		[22]={energy=10},
		[26]={willpower=1},
		[30]={energy=10},
		[34]={willpower=1},
		[38]={energy=10},
		[42]={willpower=1},
		[46]={energy=10},
		[50]="archmage"
	},
	staves = {
		[2]={protection=1},
		[5]={evasion=5},
		[8]={health=10},
		[11]={protection=2},
		[14]="light_armor",
		[18]={evasion=5},
		[22]={health=10},
		[27]={evasion=5},
		[32]={health=10},
		[37]={protection=2},
		[42]={evasion=5},
		[50]={protection=10,evasion=30}
	},
	swords = {
		[4]={strength=1},
		[7]={health=5},
		[10]="slash",
		[13]={energy=5},
		[16]="parry",
		[19]={dexterity=1},
		[23]="thrust",
		[26]={health=5},
		[29]={strength=1},
		[33]="flurry_slashes",
		[37]={energy=5},
		[42]={health=5},
		[46]={dexterity=1},
		[50]="sword_master"
	},
	throwing_weapons = {
		[4]={strength=1},
		[8]={health=5},
		[12]="quick_throw",
		[16]={strength=1},
		[20]={health=5},
		[24]="improved_quick_throw",
		[28]={strength=1},
		[32]="double_throw",
		[36]={strength=1},
		[40]={health=5},
		[45]={strength=1},
		[50]="throwing_master"
	},
	unarmed_combat = {
		[4]={dexterity=1},
		[7]={health=10},
		[11]="jab",
		[15]={dexterity=2},
		[19]={health=10},
		[23]="kick",
		[27]={health=10},
		[30]={evasion=20},
		[34]={strength=2},
		[38]={dexterity=2},
		[42]={strength=5,dexterity=5},
		[45]={health=10},
		[50]="three_point_technique"
	}
}

-- Pass a champion to remove all bonuses gained from skills.
-- Does not remove the skill levels themselves.
-- Returns the total number of skill levels possessed by the champion.
function stripSkillBonuses(champ)
	local totalPoints = 0
	for skill,bonuses in pairs(SKILL_BONUSES) do
		local sklev = champ:getSkillLevel(skill)
		totalPoints = totalPoints+sklev
		for i = 1, sklev do
			local bonus = bonuses[i]
			if bonus then
				if type(bonus) == "table" then -- stats
					for stat,amount in pairs(bonus) do
						champ:modifyStat(stat,-amount)
						champ:modifyStatCapacity(stat,-amount)
					end
				else -- trait
					champ:removeTrait(bonus)
				end
			end
		end
	end
	return totalPoints
end

-- Pass a champion to set their class while retaining their level
-- and removing all skill bonuses from the old class.
-- Like the native setClass, this sets all of the champion's skills to 0.
-- (You can set a champion to their own class for a "respec" feature.)
-- However, it retains their level and eliminates all bonuses and traits
-- gained from those skills.
-- If refundPoints parameter is left out (or false), skill points
-- spent on skills will be lost. Unspent skill points are still kept. 
-- NOTE: The champion will lose any experience gained between their
-- current level and the next.
-- NOTE: This will trigger any onLevelUp hook a number of times equal
-- to the champion's level.
function cleanSetClass(champ,class,refundPoints)
	local spentSkillPoints = stripSkillBonuses(champ)
	local unspentSkillPoints = champ:getSkillPoints()
	local level = champ:getLevel()
	local health = champ:getStatMax("health")
	local energy = champ:getStatMax("energy")
	champ:setClass(class)
	while champ:getLevel() < level do champ:levelUp() end
	-- Eliminates skill point gains from leveling up. In vanilla Grimrock
	-- this can obviously be done without storing unspent skill points by subtracting
	-- level*4, but I've seen numerous mods where champions don't always
	-- gain 4 points per level, so I might as well account for that case.
	champ:addSkillPoints((refundPoints and spentSkillPoints or 0)+unspentSkillPoints-champ:getSkillPoints())
	-- Eliminate health and energy gains from leveling up.
	champ:setStatMax("health",health)
	champ:setStatMax("energy",energy)
end
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
User avatar
Dr.Disaster
Posts: 2876
Joined: Wed Aug 15, 2012 11:48 am

Re: Code snippet: clean skill point refund

Post by Dr.Disaster »

When i read this correct then skill-granted abilities like "Light Armor Proficiency" are in fact traits. They can't be given with a call to champ:addTrait() as the modding section states but they can be revoked by champ:removeTrait(). Tricky find!
minmay
Posts: 2780
Joined: Mon Sep 23, 2013 2:24 am

Re: Code snippet: clean skill point refund

Post by minmay »

Dr.Disaster wrote:When i read this correct then skill-granted abilities like "Light Armor Proficiency" are in fact traits. They can't be given with a call to champ:addTrait()
Actually, they can.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
Post Reply