Temple+ Modding Question

Discussion in 'General Modification' started by _doug_, Feb 21, 2018.

Remove all ads!
  1. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    About movement AOOs, movement is a special case. I think you need a hook for EK_QUE_AOOIncurs. See here:
    https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/d20.cpp#L991

    Target list: that's right, currently not possible. I could add such a method, but I'm not 100% that's the best way to handle this. Will have to get back to you on that (remind me on the weekend).
     
  2. Sagenlicht

    Sagenlicht Member

    Joined:
    Apr 14, 2004
    Messages:
    78
    Likes Received:
    9
    Thanks Sitra, AOOIncurs did it :), about the target list, sure :)

    About Masters Touch, this is more complex, as I have a working code but I think this is not the way to do it as I did not find a way to parse certain values and added them as lists. There must be a better way:
    Code:
    def    OnSpellEffect(spell):
        print "Masters Touch OnSpellEffect"
        spell.duration = 10 * spell.caster_level # 1 Min/cl
        spellTarget = spell.target_list[0]
        spellEnum = (str(spell)[str(spell).index('(')+len('('):str(spell).index(')')])
       
        mastersTouchPossiblyTargetSlots = []
        mastersTouchPossiblyTargetSlots.append(spell.caster.item_worn_at(3)) #mainhand
        mastersTouchPossiblyTargetSlots.append(spell.caster.item_worn_at(4)) #offhand
        mastersTouchPossiblyTargetSlots.append(spell.caster.item_worn_at(11)) #shield
       
        casterHasProficiency = False
        casterBaseProficiencies = []
        weaponProficiencyBard = [wt_sap, wt_short_sword, wt_longsword, wt_rapier, wt_shortbow, wt_composite_shortbow, wt_whip]
        weaponProficiencyDruid = [wt_dagger, wt_sickle, wt_club, wt_shortspear, wt_quarterstaff, wt_spear, wt_dart, wt_sling, wt_scimitar, wt_longspear]
        weaponProficiencyMonk = [wt_dagger, wt_club, wt_quarterstaff, wt_light_crossbow, wt_sling, wt_heavy_crossbow, wt_javelin, wt_handaxe, wt_kama, wt_nunchaku, wt_siangham, wt_shuriken]
        weaponProficiencyRogue = [wt_hand_crossbow, wt_rapier, wt_short_sword, wt_sap, wt_shortbow, wt_composite_shortbow]
        weaponProficiencyWizard = [wt_dagger, wt_club, wt_quarterstaff, wt_light_crossbow, wt_heavy_crossbow]
        weaponProficiencyElf = [wt_longsword, wt_rapier, wt_shortbow, wt_composite_shortbow, wt_longbow, wt_composite_longbow]
        if spell.caster.has_feat(feat_simple_weapon_proficiency_bard):
            casterBaseProficiencies.extend(weaponProficiencyBard)
            hasFullSimpleProficiency = True
        if spell.caster.has_feat(feat_simple_weapon_proficiency_druid):
            casterBaseProficiencies.extend(weaponProficiencyDruid)
        if spell.caster.has_feat(feat_simple_weapon_proficiency_monk):
            casterBaseProficiencies.extend(weaponProficiencyMonk)
        if spell.caster.has_feat(feat_simple_weapon_proficiency_rogue):
            casterBaseProficiencies.extend(weaponProficiencyRogue)
            hasFullSimpleProficiency = True
        if spell.caster.has_feat(feat_simple_weapon_proficiency_wizard):
            casterBaseProficiencies.extend(weaponProficiencyWizard)
        if spell.caster.has_feat(feat_simple_weapon_proficiency_elf):
            casterBaseProficiencies.extend(weaponProficiencyElf)
       
        if spellTarget.obj == OBJ_HANDLE_NULL:
            spell.caster.float_text_line("No item equipped", tf_red)
            game.particles( 'Fizzle', spell.caster )
            spell.target_list.remove_target( spellTarget.obj)
        else:
            try: #Masters Gift only works on equipped items in main or offhand
                itemSlot = mastersTouchPossiblyTargetSlots.index(spellTarget.obj)
            except ValueError:
                spell.caster.float_text_line("Item must be an equipped weapon or shield")
                game.particles('Fizzle', spell.caster)
                spell.target_list.remove_target(spellTarget.obj)
            else:
                if itemSlot == 0:
                    wornItemType = spell.caster.item_worn_at(3).obj_get_int(197) #Get weapon type mainhand
                elif itemSlot == 1:
                    wornItemType = spell.caster.item_worn_at(4).obj_get_int(197) #Get weapon type offhand
                else:
                    wornItemType = 0
                #check for Proficiency
                if wornItemType == 0:
                    if spell.caster.has_feat(278): #check if caster is already proficient with shields; Tower Shields don't seem to differenciate???
                        casterHasProficiency = True
                else:
                    featNeeded = game.get_feat_for_weapon_type(wornItemType)
                    if spell.caster.has_feat(featNeeded):
                        casterHasProficiency = True
                    if spell.caster.has_feat(feat_martial_weapon_proficiency_all):
                        if featNeeded in range(228, 259): #range inlcudes lower and excludes upper value
                            casterHasProficiency = True
                    if featNeeded == 281: #every simple weapon returns 281 when querying game.get_feat_for_weapon_type(wornItemType)
                        if hasFullSimpleProficiency:
                            casterHasProficiency = True
                    if wornItemType in casterBaseProficiencies:
                        casterHasProficiency = True
               
                if not casterHasProficiency:
                    spell.caster.condition_add_with_args('sp-Masters Touch', spell.id, spell.duration, int(spellEnum), wornItemType)
                    spellTarget.partsys_id = game.particles('sp-Detect Magic 2 Med', spell.caster)
                else:
                    spell.caster.float_text_line("Already proficient")
                    game.particles('Fizzle', spell.caster)
                    spell.target_list.remove_target(spellTarget.obj)
    
        spell.spell_end( spell.id)

    Here is my problem: game.get_feat_for_weapon_type(wornItemType) returns only (e.g. feat_martial_weapon_proficiency_battleaxe) but not any class based feats that also grants the proficiency for it. For example Longsword has not only feat_martial_weapon_proficiency_longsword (which is returned) but also martial_all, simple_bard and simple_elf which all are not returned. I assume that all of these were not part of the original game (more of this later). I am aware of the weapon.cpp (https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/weapon.cpp) from which I took the data for my lists in the code, but it would be much better if I actually could access the data.

    It's getting even worse with simple weapons as game.get_feat_for_weapon_type(wornItemType) always returns 281 (simple weapon prof) for them. All those return values are taken from rules\feat_enum.mes (at least I think so) and obviously the game originally granted all classes simple weapon prof, so there was no need to return something else.

    Long story short, the information I would like to have is in weapon.cpp, but how do I access it? As mentioned, at the moment I simply added the lists to the spell, but this feels bad.
     
  3. Sagenlicht

    Sagenlicht Member

    Joined:
    Apr 14, 2004
    Messages:
    78
    Likes Received:
    9
    How do I unset
    Code:
    attachee.ai_flee_add(tpdp.SpellPacket(spell_id).caster)
    I did take a look at https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/python/python_object.cpp and https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/ai.cpp but could not find an answer.
    Code:
    attachee.obj_get_int(obj_f_critter_fleeing_from)
    still returns a 0 after I do set ai_flee_add but the critter runs away.

    When I tried to do simply
    Code:
    attachee.critter_flag_set(OCF_FLEEING)
    the mob did not act as he was in flee mode but did not move, I guess because I would have to set obj_f_critter_fleeing_from but the arg only takes an integer, so I cannot simply pass the tpdp.SpellPacket(spell_id).caster arg.
     
  4. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    I don't understand why you go through all this work to determine whether the caster is already proficient. Just apply the condition - worst case, it's a pointless cast, which will have already been wasted regardless of whether it fizzles or not. Also I don't think it's too much to expect from the player to use the spell judiciously.

    Nevertheless for future reference, if you ever need to check whether a character is proficient with a certain weapon, this is handled here in the C++ side:
    https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/feat.cpp#L1461
    https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/feat.cpp#L1559
    it is easy to expose it to python, and is much more preferable than duplicating all the code in python.

    Some further general notes:

    1. Please do not use the explicit values of enumerations - such as 197 instead of obj_f_weapon_type, or the shield prof. feat. If you've seen it used elsewhere this way - that is also bad practice.
    2. spellEnum - again, there is no reason to store it in the condition args. This is a static quantity known in advance.
    3. Also, parsing the spell enum from the spell string representation is very hacky. If you really have need it for whatever reason, it's easy to add a "spell_enum" property to the PySpell to directly obtain it.
     
  5. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    BTW. It's kinda hard to keep track of outstanding issues in this format. Could you make a public google doc where I can reply inline and cross off addressed issues?
     
  6. Sagenlicht

    Sagenlicht Member

    Joined:
    Apr 14, 2004
    Messages:
    78
    Likes Received:
    9
    I don't want to apply the condition I just want to negate the penalty because I want to avoid that the game belives you are proficient with the weapon when you are not. Else you could simply cast the spell before leveling up and get a weapon focus for something you actually would not qualify for. At least this is what I am afraid of.

    I fully agree, I would prefer not to have duplicates, this is really bad.

    1. Will fix it.
    2. + 3. I fully agree that it's hacky. I am even unsure if I need it, I just added it due to the sanctuary thing. If these codelines are not needed in every spell:
    Code:
    def warCrySpellHasSpellActive(attachee, args, evt_obj):
        if evt_obj.data1 == args.get_arg(2):
            evt_obj.return_val = 1
        return 0
    
    warCrySpell.AddHook(ET_OnD20Query, EK_Q_Critter_Has_Spell_Active, warCrySpellHasSpellActive, ())
    I would simply drop it altogether. I just don't know if they are needed. I took dougs spell Moment of Presciency as a template (which I hope does not sound negative at all, I am pretty thankful I had a good starting point!).
    I am aware that you don't like passing the spell enum as it's a static property, if you really dislike it, I can change that but I personally like to gather all information that I know I need in the beginning. But as I said if you really dislike it, I can change it, no problem.

    Sure I can do a google docs document, no problem :)
     
  7. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    Oh, I see now. But in this case there's a simpler solution - make the condition apply a bonus capper to bonus type 37 (which is what the non-proficiency penalty is). You'll want this to occur on the OnGetToHitBonus2 event.
    Although, perhaps that is not accurate either. For example, what if a character is wearing bracers of archery? In that case, shouldn't the bracers apply the bonus that proficient characters get?
    To address that, perhaps it's better to modify the proficiency check with a flag indicating permanent / non-permanent status effects. I've done sthg similar for stat boosters like Fox Cunning vs. Headband of Intellect. Unfortunately it's a bit hairier in this case so I'm not sure that's the best solution.
    Also, I think I've seen that it is ok to get the weapon focus feat - ideally the game should check if you have the proficiency, and enable/disable its effects accordingly.
    All those things considered - I think it's best to simply grant the proficiency. Exploiters gonna exploit, enjoy it while you can :)

    There's a simple solution actually.
    You can retrieve the SpellPacket inside the HasSpellActive() callback, and then get its spell_enum, like so:
    Code:
    spell_id = args.get_arg(0)
    spell_packet = tpdp.SpellPacket(spell_id)
    spell_enum = spell_packet.spell_enum
    
    Cool :)
     
  8. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    P.S. here's the latest decompiled C code. I think this will be a handy reference for things not covered in the Temple+ code base (e.g. check out GlobalWeaponProficiencyToHitPenalty).

    https://fil.email/LwNXiYVK
     
    anatoliy likes this.
  9. Sagenlicht

    Sagenlicht Member

    Joined:
    Apr 14, 2004
    Messages:
    78
    Likes Received:
    9
  10. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    Yes, you have to add sp-Concentrating when the condition is added, using the ET_OnAdd event. It should have the same spell id in its args naturally.
    Your own condition should have a hook for S_Concentration_Broken.
     
  11. Sagenlicht

    Sagenlicht Member

    Joined:
    Apr 14, 2004
    Messages:
    78
    Likes Received:
    9
    Thanks, works like a charm with the exception that the sp-concentration does not expire at the end of the spells duration.
    Code:
    spellPacket.caster.condition_add_with_args('sp-Concentrating', args.get_arg(0),  args.get_arg(1))
    seems to ignore the second argument (which would be the duration). No big thing I guess, I just noticed it, when I mouseover over the caster and can still see the concentration tooltip when the spell expired naturally.
     
  12. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    Right, looks like this is usually handled inside remove_spell. Basically it should also send a S_REMOVE_CONCENTRATION at the end.

    Funny thing is that it looks like some other spells have this bug too! Meld Into Stone for one. But I guess usually you take some action that breaks concentration anyway.
     
    anatoliy likes this.
  13. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    About python debugging:

    I found a way to partially debug py scripts, using ptvsd + VS Code.
    By injecting some lines of code into the script you want to debug, this allows you to:
    • Set a breakpoint and wait until VSCode attaches.
    • View the local variables.
    • Step the code.
    • Execute commands in the debug console.
    The only problem is that it doesn't load the source python code for some reason. Haven't been able to figure that one out yet. But you can open it in the editor for reference so you know what to expect when stepping the code.

    upload_2021-3-5_1-13-52.png

    To set this up:
    1. Install VSCode + the python extension.
      Set up a "Python remote attach" debug configuration:
      {
      "name": "Python: Remote Attach",
      "type": "python",
      "request": "attach",
      "connect": {
      "host": "localhost",
      "port": 5678
      },
      "pathMappings": [
      {
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "."
      }
      ]
      }

    2. Download ptvsd:
      https://pypi.org/project/ptvsd/#files
      I chose ptvsd-4.3.2-cp27-cp27mu-manylinux1_x86_64.whl , not sure if it matters.
    3. Create a folder in your ToEE installation 'python-lib'.
    4. Open the .whl file using 7zip.
      Extract the ptvsd folder (found inside ptvsd-4.3.2.data/purelib) into the python-lib folder.
    5. Inside the script you want to debug, add the following piece of code:
      Code:
      import ptvsd
      ptvsd.enable_attach()
      ptvsd.wait_for_attach()
      ptvsd.break_into_debugger()
      
    6. Once ToEE reaches this code, it'll wait for the debugger to attach.
      To attach, click on the run button, with the Python: Remote attach config selected in the drop down box:
      upload_2021-3-5_1-18-9.png
    7. It should then enter debug in the point where you injected the code in step 5.

    I suppose it might work in vanilla ToEE too, haven't tested...
    I also tried using debugpy but got some errors so I went for good ol' ptvsd instead.
     
    anatoliy, Sagenlicht and _doug_ like this.
  14. Sagenlicht

    Sagenlicht Member

    Joined:
    Apr 14, 2004
    Messages:
    78
    Likes Received:
    9
    Thanks for the debug help Sitra, I will test it :)

    Attached is my current working state of the overrides.zip and a save which includes all relevant spells already learned.

    Atm I have a few problems:
    1. How do I reapply an existing condition or reset its duration?

    For example in the spell Fugue, at the start of every round, it rolls on a table and applies an effect depending on the result. The condition lasts one round. Now on the next round the Fugue rolls first (I've added the roll to the OnBeginRound) before the old condition expires, which is also handled in OnBeginRound, but obviously does not have priority. If Fugue now ends up in trying to apply the same condition it fails as it is still applied and directly afterwards the condition ends resulting in no condition for that round.

    I actually had a similar problem with Cloud of Bewilderment, but did a workaround because I know in CoB what condition will be applied, but now I can't do that. I would see three possibly solutions:
    a) Overwrite the exisiting condition. I actually thought that this would be default behaviour , but it's not.
    b) Reset the duration of the condition by accessing it's duration argument
    c) Prioritize downticks over other onBeginRound effects

    2. Speaking of persistent AoE effects. I did not find an option to get the full list of targets in a spellPacket.
    Code:
    py::class_<SpellPacketBody>(m, "SpellPacket")
                .def(py::init<uint32_t>(), py::arg("spell_id"))
                .def_readwrite("spell_enum", &SpellPacketBody::spellEnum)
                .def_readwrite("spell_known_slot_level", &SpellPacketBody::spellKnownSlotLevel)
                .def_readwrite("inventory_idx", &SpellPacketBody::invIdx)
                .def_readwrite("picker_result", &SpellPacketBody::pickerResult)
                .def_readwrite("spell_class", &SpellPacketBody::spellClass)
                .def_readwrite("spell_id", &SpellPacketBody::spellId)
                .def_readwrite("caster_level", &SpellPacketBody::casterLevel)
                .def_readwrite("loc", &SpellPacketBody::aoeCenter)
                .def_readwrite("caster", &SpellPacketBody::caster)
                .def("get_spell_casting_class", [](SpellPacketBody&pkt) {
                    return static_cast<int>(spellSys.GetCastingClass(pkt.spellClass));
                    })
                .def("get_metamagic_data", [](SpellPacketBody&pkt) {
                    return pkt.metaMagicData;
                })
                .def("get_target",[](SpellPacketBody &pkt, int idx)->objHndl
                {
                    if (idx < (int)pkt.targetCount)
                        return pkt.targetListHandles[idx];
                    return objHndl::null;
                } )
                .def("set_projectile", [](SpellPacketBody &pkt, int idx, objHndl projectile){
                    if (idx >=0 && idx < 5){
                        spellSys.GetSpellPacketBody(pkt.spellId, &pkt);
                        pkt.projectiles[idx] = projectile;
                        if (pkt.projectileCount <= (uint32_t) idx)
                            pkt.projectileCount = idx+1;
    
                        // update the spell repositories
                        spellSys.UpdateSpellPacket(pkt);
                        pySpellIntegration.UpdateSpell(pkt.spellId);
                    }
                })
                .def("is_divine_spell", &SpellPacketBody::IsDivine)
                .def("debit_spell", &SpellPacketBody::Debit)
                .def("update_registry", [](SpellPacketBody &pkt){
                    spellSys.UpdateSpellPacket(pkt);
                    pySpellIntegration.UpdateSpell(pkt.spellId);
                }, "Updates the changes made in this local copy in the active spell registry.")
                .def("set_spell_object", [](SpellPacketBody&pkt, int idx,  objHndl spellObj, int partsysId){
                    pkt.spellObjs[idx].obj = spellObj;
                    pkt.spellObjs[idx].partySysId = partsysId;
                })
                .def("add_spell_object", [](SpellPacketBody&pkt, objHndl spellObj, int partsysId) {
                    auto idx = pkt.numSpellObjs;
                    if (idx >= 128)
                        return;
                    pkt.spellObjs[idx].obj = spellObj;
                    pkt.spellObjs[idx].partySysId = partsysId;
                    pkt.numSpellObjs++;
                })
                .def("add_target",[](SpellPacketBody&pkt, objHndl handle, int partsysId)->bool{
                    return pkt.AddTarget(handle, partsysId, false);
                })
                .def("end_target_particles", [](SpellPacketBody&pkt, objHndl handle){
                    pkt.EndPartsysForTgtObj(handle);
                })
                .def("remove_target", [](SpellPacketBody&pkt, objHndl handle){
                    pkt.RemoveObjFromTargetList(handle);
                })
                .def("check_spell_resistance", [](SpellPacketBody&pkt, objHndl tgt){
                    return pkt.CheckSpellResistance(tgt);
                })
                .def("check_spell_resistance_force", [](SpellPacketBody& pkt, objHndl tgt) {
                    return pkt.CheckSpellResistance(tgt, true);  //Force the check even if the spell wouldn't normally allow it
                })
                .def("trigger_aoe_hit", [](SpellPacketBody&pkt) {
                    if (!pkt.spellEnum)
                        return;
                    pySpellIntegration.SpellTrigger(pkt.spellId, SpellEvent::AreaOfEffectHit);
                })
                .def("float_spell_line", [](SpellPacketBody& pkt, objHndl handle, int lineId, int color){
                    auto color_ = (FloatLineColor)color;
                    floatSys.FloatSpellLine(handle, lineId, color_);
                })
                ;
    I did a workaround in CoB because on the start of CoB turn it checks for all targets in its AoE and tries to nauseat them.
    Code:
    targetsInAoe = game.obj_list_cone(attachee, OLC_CRITTERS, 10, 0, 360)
    but something like
    Code:
    targetsInAoe = spellPacket.target_list
    and simply use the known target list would be better I think?
    I know this is only relevant for persistent AoE spells and effects.

    3. Do I have somehow access in python to this information? (code is from TemplePlus/ai.cpp):
    Code:
    if (combatSys.CanMeleeTarget(enemies[i], performer))
                {
                    isThreatened = true;
                    break;
    at least I assume, this checks if attachee is targeted not vice versa.
    At the moment I've done
    Code:
    threateningTargets = game.obj_list_cone(attachee, OLC_PC, 5, 0, 360)
    But this check fails, if a target actually has a reach weapon, is enlarged or even worse is enlarged while wealding a reach weapon. (The spell that uses this is Phantom Foe).

    4. I fumbled while trying to do Dolorous Blow, which automatically confirms critical threats.
    I thought, it's easy and did:
    Code:
    def dolorousBlowSpellBonusToConfirmCrit(attachee, args, evt_obj):
        evt_obj.attack_packet.set_flags(D20CAF_ALWAYS_HIT) #Dolorous Blow automaticaly confirms critical hits
        return 0
    But yeah it's not:
    Screenshot 2021-03-05 121815.jpg
    which actually leads to a miss (it should be a hit) and no damage dealt. And I actually thought about skipping the spell before I started... The line commented out atm.

    Maybe this is related to why Power Critical feat (not done by me) and Critical Strike (a new spell done by me) show in the Roll Window the +4 bonus on both rolls but only add the bonus to confirm (which is correct).
     

    Attached Files:

  15. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    3,039
    Likes Received:
    220
    The default behavior, as mentioned here, is to prevent duplicates. It's the default because that's how many of the conditions are in ToEE (usually for things like racial modifiers and class ability/feats).

    You can disable this default by spawning the PyModifier with the third arg as 0, and add your own ET_OnConditionAddPre handler.
    There you can:
    * Refresh duration (e.g. used by Inspire Courage to reset the countdown), but still prevent the new condition being applied.
    * Add up some variable (e.g. condition Damaged which gets applied when you take damage this round)
    * Prevent duplicates only if same arg (e.g. Weapon feats, Skill Focus)
    * Remove a dual existing effect / spell modifier (e.g. used by Bless↔Bane, Slow↔Haste etc to remove each other)
    etc.

    The central condition "sp-Cloud of Bewilderment" does not need to manage the target list, it should just apply the 'Cloud of Bew Effect' condition when targets enter the AoE - and this is handled by the game's AoE engine for you. The effect condition should then handle everything on its own, including the OnConditionAdd and OnBeginRound effects, not just ticking down the duration and the tooltip.

    For a python example, see how Wall of Fire does it (minus the wall handling obviously). You can also see examples in the decompiled code, e.g. Cloudkill, Stinking Cloud etc.

    Not yet, but it's easy to add as a PyObjHndl method (put it on the google doc).

    The relevant code is here:
    https://github.com/GrognardsFromHell/TemplePlus/blob/master/TemplePlus/combat.cpp#L1815
    So yeah, the ALWAYS_HIT doesn't affect the critical hit roll, just the normal to-hit roll.
    So instead of that, you could just add a very large bonus to guarantee a hit.
    I think you can also set D20CAF_CRITICAL directly, because that's what it does in the code :)

    Also speaking of Power Critical - looks like it was done the wrong way, by spawning a different modifier for each weapon type... instead as I said earlier, it should instead allows duplicates so long as the arg (weapon type) is different.
    The MapToFeat method has an optonal arg:
    def MapToFeat(self, feat_enum, feat_list_max = -1, feat_cond_arg2 = 0)
    The third arg should specify weapon type in such cases.
    @_doug_ FYI. (but I guess it's too late to change it since it might ruin savegames... so for future cases keep this in mind)
     
Our Host!