Skip to content

Templates for tpModifier Spell Conditions

Sagenlicht edited this page Mar 23, 2021 · 3 revisions

Templates for tpModifier Spell Conditions

Script Structure

Usually you import

from templeplus.pymod import PythonModifier
from toee import *
import tpdp
from utilities import *

To get a feedback in the log and console add:

print "Registering sp-Spell Name"

Next are the functions

def myNewFunction(attachee, args, evt_obj): #arguments should always be the same
    *some code*
    return 0

The next part defines when the function should be called. This is done by defining a PythonModifier:

spellNameModifier = PythonModifier("sp-Spell Name", 2) # spell_id, duration

("sp-Spell Name") is the name of the condition. If you want to call from a different script, usually the Spellxxxx - Spell Name.py you do:

spellTarget.obj.condition_add_with_args('sp-Spell Name', spell.id, spell.duration)

the number behind the name is the number of arguments the modifier takes. If you need more than two you have to increase the number. Please be aware that if you want to read an argument, the first argument is actually not 1 but 0 (args.get_arg(0)). I personally add the arguments as a comment here, but you can also do this in the beginning of the script (after the import stuff). Something like this is also fine:

# Arg0 = spell.id
# Arg 1= duration

Just be sure to add it somewhere.

The following lines are the event hooks, defining when the above functions are actually called

spellNameModifier.AddHook(Event_Trigger, Event_Key, myNewFunction, ())

All triggers and keys can be found here

Details can be found here

Templates for functions and hooks

I will list the function first and afterwards its correspondending hook.

Close to always used

Description

Mouseover description:

def focusingChantSpellTooltip(attachee, args, evt_obj):
    if args.get_arg(1) == 1:
        evt_obj.append("Focusing Chant ({} round)".format(args.get_arg(1)))
    else:
        evt_obj.append("Focusing Chant ({} rounds)".format(args.get_arg(1)))
    return 0
focusingChantSpell.AddHook(ET_OnGetTooltip, EK_NONE, focusingChantSpellTooltip, ())

Buff Symbol:

def focusingChantSpellEffectTooltip(attachee, args, evt_obj):
    if args.get_arg(1) == 1:
        evt_obj.append(tpdp.hash("FOCUSING_CHANT"), -2, " ({} round)".format(args.get_arg(1)))
    else:
        evt_obj.append(tpdp.hash("FOCUSING_CHANT"), -2, " ({} rounds)".format(args.get_arg(1)))
    return 0
focusingChantSpell.AddHook(ET_OnGetEffectTooltip, EK_NONE, focusingChantSpellEffectTooltip, ())

Spell active

def focusingChantSpellHasSpellActive(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    if evt_obj.data1 == spellPacket.spell_enum:
        evt_obj.return_val = 1
    return 0
focusingChantSpell.AddHook(ET_OnD20Query, EK_Q_Critter_Has_Spell_Active, focusingChantSpellHasSpellActive, ())

Spell Killed

def focusingChantSpellKilled(attachee, args, evt_obj):
    args.remove_spell()
    args.remove_spell_mod()
    return 0
focusingChantSpell.AddHook(ET_OnD20Signal, EK_S_Killed, focusingChantSpellKilled, ())

Spell End

def focusingChantSpellSpellEnd(attachee, args, evt_obj):
    print "Focusing Chant SpellEnd"
    return 0
focusingChantSpell.AddHook(ET_OnD20Signal, EK_S_Spell_End, focusingChantSpellSpellEnd, ())

Standard Hooks

focusingChantSpell.AddSpellDispelCheckStandard()
focusingChantSpell.AddSpellTeleportPrepareStandard()
focusingChantSpell.AddSpellTeleportReconnectStandard()
focusingChantSpell.AddSpellCountdownStandardHook()

Spell independet functions

Dismiss

needs to be added

Concentration

Add concentration to a spell condition:

def harmonicChorusSpellAddConcentration(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    spellPacket.caster.condition_add_with_args('sp-Concentrating', args.get_arg(0))
    return 0
def harmonicChorusSpellConcentrationBroken(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    if spellPacket.spell_enum == 0:
        return 0
    args.remove_spell()
    args.remove_spell_mod()
    return 0
harmonicChorusSpell.AddHook(ET_OnConditionAdd, EK_NONE, harmonicChorusSpellAddConcentration,())
harmonicChorusSpell.AddHook(ET_OnD20Signal, EK_S_Concentration_Broken, harmonicChorusSpellConcentrationBroken, ())

Add Concentration to an AoE spell:

def fugueSpellConcentrationBroken(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    if spellPacket.spell_enum == 0:
        return 0
    attachee.d20_send_signal(S_Spell_End, args.get_arg(0))
    return 0
fugueSpell.AddHook(ET_OnD20Signal, EK_S_Concentration_Broken, fugueSpellConcentrationBroken, ())

If the spell adds another condition you need to add there as well:

fugueCondition.AddHook(ET_OnD20Signal, EK_S_Concentration_Broken, fugueConditionEnd, ())

Key a condition to a worn item

Some spells enchant a specific item but in my experience you want to add the condition to caster and tie simply check if the caster is actually wearing the item and then apply the effect. I originially thought I simply can add the target to the arguments, but noticed I can't as they are limited to integers. I found a workaround by adding the item as target to the spell registry:

def sonicWeaponSpellChainToWeapon(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    mainhandWeapon = attachee.item_worn_at(item_wear_weapon_primary)

    mainhandWeapon.d20_status_init()
    spellPacket.add_target(mainhandWeapon, 0)
    spellPacket.update_registry()
    return 0
sonicWeaponSpell.AddHook(ET_OnConditionAdd, EK_NONE, sonicWeaponSpellChainToWeapon,())

then simply add a check in the function that requires the worn item:

if spellPacket.get_target(1) == evt_obj.attack_packet.get_weapon_used():

When the spell ends, you have to manually remove the item from the spell registry, or the spell will not end properly

def sonicWeaponSpellConditionRemove(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    removeWeaponFromSpellRegistry = spellPacket.get_target(0)
    spellPacket.remove_target(removeWeaponFromSpellRegistry)
    spellPacket.update_registry()
    args.remove_spell()
    return 0
sonicWeaponSpell.AddHook(ET_OnConditionRemove, EK_NONE, sonicWeaponSpellConditionRemove, ())

Please note, that the target number changes as the original spell target is already removed, which means that our added target has moved up a slot. I have used this method in a few spells, including an AoE effect that is only turned on, when the item is equipped and it works without troubles. The AoE spell in question would be Chechmate's Light.

Radial Menu Entry

Top level menu

    #Add the top level menu
    radialParent = tpdp.RadialMenuEntryParent("Improvisation ({})".format(args.get_arg(3)))
    improvisationRadialId = radialParent.add_child_to_standard(attachee, tpdp.RadialMenuStandardNode.Class)

Child Menu Toggle Box

    #Add checkboxes to activate or deactivate the bonus for different options
    checkboxAbilityBonus = tpdp.RadialMenuEntryToggle("+{} to next Ability Check".format(args.get_arg(2)), "TAG_SPELLS_IMPROVISATION")
    checkboxAbilityBonus.link_to_args(args, 4)
    checkboxAbilityBonus.add_as_child(attachee, improvisationRadialId)
    return 0

To be done: other radial menu options

Spell specific functions

Freeform versus mesfile

My policy is: if I can use an existing mesfile entry, use the mesfile entry, if I would need to create an entry, use freeform if possible. If I use a mesfile entry I always add a comment directly behind it, so I can track it easily in the future, if I can replace it with a freefrom then.

Help file link

In general, you can link in every string to the help by using the internal help file call:

~Focusing Chant~[TAG_SPELLS_FOCUSING_CHANT]

The string between the ~~ is the displayed string and the call to the help file is the help file tag in the [] bracets. Please leave no space between the two parts or the link will fail.

BonusList

bonus_list is the most common function you will define. It adds are bonus or penalty. ** Please be sure to use the correct bonus type or you will create stacking issues.** Bonus types can be found here

def focusingChantSpellBonus(attachee, args, evt_obj):
    evt_obj.bonus_list.add(1,21,"~Focusing Chant~[TAG_SPELLS_FOCUSING_CHANT] ~Circumstance~[TAG_MODIFIER_CIRCUMSTANCE] Bonus") #Focusing Chant adds a +1 Circumstance Bonus to Attack Rolls, Skill and Ability Checks
    return 0

if you want to add it to the damage packet, you need to call if slightly different

evt_obj.damage_packet.bonus_list.add(4, 13, "~War Cry~[TAG_SPELLS_WAR_CRY] ~Morale~[TAG_MODIFIER_MORALE] Bonus")

The list can be called by a lot of Event Triggers. A few examples:

focusingChantSpell.AddHook(ET_OnToHitBonus2, EK_NONE, focusingChantSpellBonus,())
focusingChantSpell.AddHook(ET_OnGetSkillLevel, EK_NONE, focusingChantSpellBonus,())
focusingChantSpell.AddHook(ET_OnGetAbilityCheckModifier, EK_NONE, focusingChantSpellBonus,())
sirinesGraceSpell.AddHook(ET_OnGetSkillLevel, EK_SKILL_PERFORM, sirinesGraceSpellPerformBonus,())
sirinesGraceSpell.AddHook(ET_OnAbilityScoreLevel, EK_STAT_CHARISMA, sirinesGraceSpellAbilityBonus,())
sirinesGraceSpell.AddHook(ET_OnAbilityScoreLevel, EK_STAT_DEXTERITY, sirinesGraceSpellAbilityBonus,())

In the second example, the event will only trigger if the event key is matching. This means the bonus is only added to the Perform Skill and to the Charisma and Dex stats. Also note as the bonus differs, I call two seperate functions.

Add damage dice

def sonicWeaponSpellBonusToDamage(attachee, args, evt_obj):
    spellPacket = tpdp.SpellPacket(args.get_arg(0))
    if spellPacket.get_target(1) == evt_obj.attack_packet.get_weapon_used():
        bonusDice = dice_new('1d6') #Sonic Weapon Bonus Damage
        evt_obj.damage_packet.add_dice(bonusDice, D20DT_SONIC, 3001) #ID3001 added in damage.mes 
    return 0
sonicWeaponSpell.AddHook(ET_OnDealingDamage, EK_NONE, sonicWeaponSpellBonusToDamage,())

Bonus to caster level

def harmonicChorusSpellBonusToCasterLevel(attachee, args, evt_obj):
    evt_obj.return_val += 2
    return 0
harmonicChorusSpell.AddHook(ET_OnGetCasterLevelMod, EK_NONE, harmonicChorusSpellBonusToCasterLevel,())

Defender Concealment

Add concealment (like blur)

def veilOfShadowSpellConcealment(attachee, args, evt_obj):
    evt_obj.bonus_list.add(20,0,"~Veil of Shadow~[TAG_SPELLS_VEIL_OF_SHADOW] Concealment Bonus") #Veil of Shadow grants 20% Concealment
    return 0
veilOfShadowSpell.AddHook(ET_OnGetDefenderConcealmentMissChance, EK_NONE, veilOfShadowSpellConcealment,())

Concealment Penalty for Attacker (e.g. blinded)

def phantomFoeSpellAttackMissChance(attachee, args, evt_obj):
    evt_obj.bonus_list.add(50,0,"~Phantom Foe~[TAG_SPELLS_PHANTOM_FOE] Concealment Penalty") #Attacked Opponent gets 50% concealment due to Phantom Foe
    return 0
phantomFoeSpell.AddHook(ET_OnToHitBonusFromDefenderCondition, EK_NONE, phantomFoeSpellSetFlankedCondition,())

Add Damage Reduction

def angelskinSpellEvilDr(attachee, args, evt_obj): 
    evt_obj.damage_packet.add_physical_damage_res(5, D20DAP_UNHOLY, 126) #Angelskin grants DR 5/evil; ID126 in damage.mes is DR
    return 0
angelskinSpell.AddHook(ET_OnTakingDamage , EK_NONE, angelskinSpellEvilDr,())

Add a save bonus to a specific subtype of saves

This is not doable by using a flag I tried using both D20STD_F_POISON and D20STF_POISON and it did not work.

def irongutsSpellBonusToPoisonSaves(attachee, args, evt_obj):
    if evt_obj.flags & 0x8: 
        evt_obj.bonus_list.add(5, 151,"~Ironguts~[TAG_SPELLS_IRONGUTS] ~Alchemical~[TAG_MODIFIER_ALCHEMICAL] Bonus") #151 = Alchemical; Ironguts adds a +5 Alchemical Bonus to Fortitude Saves vs. poison
    return 0
irongutsSpell.AddHook(ET_OnSaveThrowLevel, EK_SAVE_FORTITUDE, irongutsSpellBonusToPoisonSaves,())

This is the table you have to use in place of the descriptor flag:

D20STF_NONE = 0,
D20STF_REROLL = 0x1, 
D20STF_CHARM = 0x2, 
D20STF_TRAP = 0x4,  
D20STF_POISON = 0x8, 
D20STF_SPELL_LIKE_EFFECT = 0x10,
D20STF_SPELL_SCHOOL_ABJURATION = 0x20,
D20STF_SPELL_SCHOOL_CONJURATION = 0x40,
D20STF_SPELL_SCHOOL_DIVINATION = 0x80,
D20STF_SPELL_SCHOOL_ENCHANTMENT =  0x100,
D20STF_SPELL_SCHOOL_EVOCATION = 0x200,
D20STF_SPELL_SCHOOL_ILLUSION = 0x400,
D20STF_SPELL_SCHOOL_NECROMANCY =0x800,
D20STF_SPELL_SCHOOL_TRANSMUTATION = 0x1000,
D20STF_SPELL_DESCRIPTOR_ACID = 0x2000,
D20STF_SPELL_DESCRIPTOR_CHAOTIC = 0x4000,
D20STF_SPELL_DESCRIPTOR_COLD = 0x8000,
D20STF_SPELL_DESCRIPTOR_DARKNESS = 0x10000,
D20STF_SPELL_DESCRIPTOR_DEATH = 0x20000,
D20STF_SPELL_DESCRIPTOR_ELECTRICITY = 0x40000,
D20STF_SPELL_DESCRIPTOR_EVIL =  0x80000,
D20STF_SPELL_DESCRIPTOR_FEAR = 0x100000,
D20STF_SPELL_DESCRIPTOR_FIRE = 0x200000,
D20STF_SPELL_DESCRIPTOR_FORCE =0x400000,
D20STF_SPELL_DESCRIPTOR_GOOD = 0x800000,
D20STF_SPELL_DESCRIPTOR_LANGUAGE_DEPENDENT = 0x1000000,
D20STF_SPELL_DESCRIPTOR_LAWFUL = 0x2000000,
D20STF_SPELL_DESCRIPTOR_LIGHT = 0x4000000,
D20STF_SPELL_DESCRIPTOR_MIND_AFFECTING = 0x8000000,
D20STF_SPELL_DESCRIPTOR_SONIC = 0x10000000, // might be an offset here
D20STF_SPELL_DESCRIPTOR_TELEPORTATION = 0x20000000,
D20STF_SPELL_DESCRIPTOR_AIR = 0x40000000,
D20STF_SPELL_DESCRIPTOR_EARTH = 0x80000000,
D20STF_SPELL_DESCRIPTOR_WATER = 33, // <- This one might not even work anymore...
D20STF_DISABLE_SLIPPERY_MIND = 34

Damage over time

DoT effects are handled at the beginning of the round of the affected target.

def bonefiddleSpellBeginRound(attachee, args, evt_obj):
    if args.get_arg(1) >= 0:
        spellDamageDice = dice_new('3d6')
        spellPacket = tpdp.SpellPacket(args.get_arg(0))
        game.create_history_freeform(attachee.description + " saves versus ~Bonefiddle~[TAG_SPELLS_BONEFIDDLE]\n\n")
        if attachee.saving_throw_spell(args.get_arg(2), D20_Save_Fortitude, D20STD_F_NONE, spellPacket.caster, args.get_arg(0)): #save for no damage and to end spell immediately
            attachee.float_text_line("Bonefiddle saved")
            args.set_arg(1, -1)
        else:
            attachee.float_text_line("Bonefiddle damage", tf_red)
            attachee.spell_damage(spellPacket.caster, D20DT_SONIC, spellDamageDice, D20DAP_MAGIC, D20A_CAST_SPELL, args.get_arg(0)) #is there a way to change unknown to spell name in the history window?
    return 0

The args.set_arg(1, -1) line sets the duration to -1 and thus triggers the normal spell end trigger as the duration is below 0. I found this works better than ending the spell manually.

bonefiddleSpell.AddHook(ET_OnBeginRound, EK_NONE, bonefiddleSpellBeginRound, ())

Modify crit range of a wepaon

This one was a tricky one, crit range increasing effects do not stack anymore in D&D 3.5 (which differs from 3.0) I found a solution that does not stack with any other effect.

def criticalStrikeSpellModifyThreatRange(attachee, args, evt_obj):
    if evt_obj.attack_packet.get_weapon_used().obj_get_int(obj_f_type) == obj_t_weapon: #Keen requires weapon
        getWeaponKeenRange = evt_obj.attack_packet.get_weapon_used().obj_get_int(obj_f_weapon_crit_range)
    else:
        return 0

    appliedKeenRange =  evt_obj.bonus_list.get_sum()

    if appliedKeenRange == getWeaponKeenRange:
        evt_obj.bonus_list.add(getWeaponKeenRange, 0 , "~Critical Strike~[TAG_SPELLS_CRITICAL_STRIKE] Bonus")
    return 0
criticalStrikeSpell.AddHook(ET_OnGetCriticalHitRange, EK_NONE, criticalStrikeSpellModifyThreatRange,())

Temporary ability damage

attachee.condition_add_with_args('Temp_Ability_Loss', stat_strength, 2)
attachee.condition_add_with_args('Temp_Ability_Loss', stat_dexterity, 2)
game.create_history_freeform("{} Abilities damaged\n\n".format(attachee.description))

Add spell failure chance

    failDice = dice_new('1d100')
    distortDiceResult = failDice.roll()
    if distortDiceResult < 51: #Distort Speech is a 50% Chance to fail spells and activate items
        evt_obj.return_val = 100
        attachee.float_text_line("Distort Speech Failure", tf_red)
        game.create_history_freeform("~Distort Speech~[TAG_SPELLS_DISTORT_SPEECH] check: {} rolls a {}. Failure!\n\n".format(attachee.description, distortDiceResult))
        game.particles('Fizzle', attachee)
distortSpeechSpell.AddHook(ET_OnD20Query, EK_Q_SpellInterrupted, distortSpeechSpellDistortCheck,())

Cancel incoming Attack of Opportunity(AoO)

def lightfootSpellCancelAoO(attachee, args, evt_obj):
    attachee.float_text_line("Lightfooted")
    evt_obj.return_val = 0
    return 0
lightfootSpell.AddHook(ET_OnD20Query, EK_Q_AOOIncurs, lightfootSpellCancelAoO,())

Cancel outgoing AoO

def nauseatedConditionAoOPossible(attachee, args, evt_obj): #Can't AoO under Nauseated condition
    evt_obj.return_val = 0
    return 0
nauseatedCondition.AddHook(ET_OnD20Query, EK_Q_AOOPossible, nauseatedConditionAoOPossible, ())

Set a D20CAF_FLAG

def phantomFoeSpellSetFlankedCondition(attachee, args, evt_obj):
    if evt_obj.attack_packet.get_flags() & D20CAF_FLANKED:
        return 0

    flags = evt_obj.attack_packet.get_flags()
    flags |= D20CAF_FLANKED
    evt_obj.attack_packet.set_flags(flags)

Limit actions

Reduce targets actions options

def rayOfDizzinessSpellTurnBasedStatusInit(attachee, args, evt_obj):
    if evt_obj.tb_status.hourglass_state > 2:
        attachee.float_text_line("Dizzy", tf_red)
        evt_obj.tb_status.hourglass_state = 2 # Limited to a Standard or Move Action only
    return 0

2 reduces to either move or standard action, 1 reduces to only move action

rayOfDizzinessSpell.AddHook(ET_OnTurnBasedStatusInit, EK_NONE, rayOfDizzinessSpellTurnBasedStatusInit, ())

To be continued

Clone this wiki locally