Page 1 of 1

Add costum spells

Posted: Mon Apr 26, 2021 7:41 pm
by kcmc
Hi there

I took some time reading various topics about spells and came across the spells.dfn and the js scripts, but most of the topics were pretty old. As far as I read, the idea of the js scripts is to outsource the spells from the hardcoded c++ source?

Now I'm wondering if there's a way to add new costum spells to the game rather than modify/outsource already existing ones? Sure thing the animations, sounds and so on are static but looking at the js scripts, I guess there are still some options to play around with.

Which course of action would you recommend for trying to add a costum spell?

Re: Add costum spells

Posted: Tue Apr 27, 2021 4:32 am
by Xuri
Creating custom spells should be possible. You might need to come up with some new way of casting them though (your own custom created spellbook?) - not sure if adding them to a normal spellbook would require a client change... hm hm.

In either case, you have a couple of different ways to go about when setting up your custom spells:

Option A)
Create your new spell (and potentially entire magic/spellcasting system) entirely in JS. Set up JS events that detect spoken words of power and trigger a spell that way, or do it via objects, or a custom spellbook gump, or custom commands. When spell is triggered, check that player meets requirements to cast, then handle all the details of spellcasting like consuming reagents (if any), mana (if any), etc, and add spell FX and sound FX using JS methods like DoMovingEffect() and SoundEffect(). A setup like this doesn't have to conform to the existing spell system, you can customize entirely how it will work, what rules apply, etc.

Option B)
Piggyback on the existing spell system, add new spell definitions to spells.dfn, use some JS event (item usage, detect spoken words of power, custom spellbook gump) to trigger the spell (and do all your checks to ensure player meets requirements to cast etc) and then use the JS function pUser.CastSpell( spellID ) to actually cast the spell. This way, all the targeting rules, reagent usage, mana check, spell FXs etc are handled automatically by the default magic system, based on the details you put in spells.dfn (EDIT: That was a lie. You still need to handle these things in your custom spell JS script, but at least the spell data itself can still be setup in spells.dfn, if you want). The downside of this approach is that there's currently a hard limit on the amount of spells supported this way in the UOX3 source. That limit is currently at 68, while there are already 64 spells in spells.dfn, giving you room only for 4 new spells. This is a limit we can probably lift in the source though, and in fact I'll change it right now so it will go in with the next update I push to the develop branch on github.

Setting all this up and have it work properly can be a challenge, something I can attest to myself after several attempts to port the magic system from code to JS (still a project that's work in progress), but it's definitely doable, and if there's some part of it that's not as doable as I thought, give a shout and we can see if it's possible to fix/improve on that somehow.

Oh, and check out js/magic/clumsy.js and js/magic/level1targ.js scripts for some insights into how spells can be setup in JS. Should also give you some idea of what kind of checks and requirements are actually in place to cast spells!

Re: Add costum spells

Posted: Tue Apr 27, 2021 6:13 am
by Xuri
Cooked up a quick example of adding another spell via option B.

First, I added this to the end of spells.dfn (reused words of power from another spell):
// Summon Hero (Dupre The Hero)
[SPELL 65]
{
NAME=Summon Hero
ENABLE=1
CIRCLE=8
MANA=4
LOSKILL=11
HISKILL=401
SCLO=561
SCHI=1001
MANTRA=Uus Jux
ACTION=16
DELAY=20
ASH=0
DRAKE=1
GARLIC=0
GINSENG=0
MOSS=1
PEARL=0
SHADE=0
SILK=1
FLAGS=0x0005
SOUNDFX=0x0217
}
Then, I added a new js file - js/magic/customspell.js, and registered it in jse_fileassociations.scp under the [MAGIC_SCRIPTS} section as 700=magic/customspell.js. Then I took an existing script (clumsy.js) and modified it to remove all but the most essential checks, and changed the end result of the spell to summon an NPC (Dupre the Hero), who after being summoned behaves like any other summoned creature:
function SpellRegistration()
{
    RegisterSpell( 65, true );
}

function onSpellCast( mSock, mChar, directCast, spellNum )
{
    // Are we already casting?
    if( mChar.GetTimer( 6 ) != 0 )
    {
        if( mChar.isCasting )
        {
            // You are already casting a spell.
            mSock.SysMessage( GetDictionaryEntry( 762, mSock.language ) );
            return true;
        }
        else if( mChar.GetTimer( 6 ) > GetCurrentClock() )
        {
            // You must wait a little while before casting
            mSock.SysMessage( GetDictionaryEntry( 1638, mSock.language ) );
            return true;
        }
    }

    var mSpell  = Spells[spellNum];
    var spellType   = mSock.currentSpellType;

    // Set the spell as current spell being cast
    mChar.spellCast = spellNum;

    // Turn character visible if not already visible
    if( mChar.visible == 1 || mChar.visible == 2 )
        mChar.visible = 0;

    // Break caster's concentration (meditation skill)
    mChar.BreakConcentration( mSock );

    if( mChar.commandlevel < 2  )
    {
        //Check for enough reagents
        // type == 0 -> SpellBook
        if( spellType == 0 && !checkReagents( mChar, mSpell ) )
        {
            mChar.SetTimer( 6, 0 );
            mChar.isCasting = false;
            mChar.spellCast = -1;
            return true;
        }
    }

    // Check if caster fails magery skill check, delete reagents, etc
    if( ( mChar.commandlevel < 2 ) && ( !mChar.CheckSkill( 25, lowSkill, highSkill ) ) )
    {
        mChar.TextMessage( mSpell.mantra );
        if( spellType == 0 )
        {
            deleteReagents( mChar, mSpell );
            mChar.SpellFail();
            mChar.SetTimer( 6, 0 );
            mChar.isCasting = false;
            mChar.spellCast = -1;
            return true;
        }
    }

    mChar.nextAct = 75;     // why 75?

    // Start the spellcast cooldown timer, which determines next time they can cast a spell
    var delay = mSpell.delay * 100;
    if( spellType == 0 && mChar.commandlevel < 2 ) // if they are a gm they don't have a delay :-)
    {
        mChar.SetTimer( 6, delay );
        mChar.frozen = true;
    }
    else
        mChar.SetTimer( 6, 0 );

    // Play casting anim, if caster is not on a horse!
    if( !mChar.isonhorse )
    {
        var actionID = mSpell.action;
        if( mChar.isHuman || actionID != 0x22 )
            mChar.DoAction( actionID );
    }

    // Have caster speak the words of power for this spell
    var tempString = mSpell.mantra;
    mChar.TextMessage( tempString );
    mChar.isCasting = true;

    // Start the cast timer, after which a target cursor will be provided
    mChar.StartTimer( delay, spellNum, true );
    return true;
}

function checkReagents( mChar, mSpell )
{
    // Check if caster has enough reagents to cast spell
    var failedCheck = 0;
    if( mSpell.ash > 0 && mChar.ResourceCount( 0x0F8C ) < mSpell.ash )
        failedCheck = 1;
    if( mSpell.drake > 0 && mChar.ResourceCount( 0x0F86 ) < mSpell.drake )
        failedCheck = 1;
    if( mSpell.garlic > 0 && mChar.ResourceCount( 0x0F84 ) < mSpell.garlic )
        failedCheck = 1;
    if( mSpell.ginseng > 0 && mChar.ResourceCount( 0x0F85 ) < mSpell.ginseng )
        failedCheck = 1;
    if( mSpell.moss > 0 && mChar.ResourceCount( 0x0F7B ) < mSpell.moss )
        failedCheck = 1;
    if( mSpell.pearl > 0 && mChar.ResourceCount( 0x0F7A ) < mSpell.pearl )
        failedCheck = 1;
    if( mSpell.shade > 0 && mChar.ResourceCount( 0x0F88 ) < mSpell.shade )
        failedCheck = 1;
    if( mSpell.silk > 0 && mChar.ResourceCount( 0x0F8D ) < mSpell.silk )
        failedCheck = 1;
    if( failedCheck == 1 )
    {
        mChar.SysMessage( "You do not have enough reagents to cast that spell." );
        return false;
    }
    else
        return true;
}

function deleteReagents( mChar, mSpell )
{
    // Use appropriate amount of reagents for spell, as registered in spells.dfn
    mChar.UseResource( mSpell.pearl, 0x0F7A );
    mChar.UseResource( mSpell.moss, 0x0F7B );
    mChar.UseResource( mSpell.garlic, 0x0F84 );
    mChar.UseResource( mSpell.ginseng, 0x0F85 );
    mChar.UseResource( mSpell.drake, 0x0F86 );
    mChar.UseResource( mSpell.shade, 0x0F88 );
    mChar.UseResource( mSpell.ash, 0x0F8C );
    mChar.UseResource( mSpell.silk, 0x0F8D );
}

function onTimer( mChar, timerID )
{
    // Free caster from spellcasting action
    mChar.isCasting = false;
    mChar.frozen    = false;

    // Proceed to give caster a targeting cursor
    var mSock = mChar.socket;
    if( mSock )
        mSock.CustomTarget( 0, Spells[timerID].strToSay );
}

function onCallback0( mSock, ourTarg )
{
    // If GetWord( 1 ) is true, target is a location, not an object
    if( mSock.GetWord( 1 ))
    {
        // Fetch coordinates of where caster clicked, and store it in socket properties
        mSock.clickX = mSock.GetWord( 11 );
        mSock.clickY = mSock.GetWord( 13 );
        mSock.clickZ = mSock.GetSByte( 16 ) + GetTileHeight( mSock.GetWord( 17 ));

        // Proceed to finalize spellcast
        var mChar = mSock.currentChar;
        if( mChar )
            onSpellSuccess( mSock, mChar, null );
    }
}

function onSpellSuccess( mSock, mChar, ourTarg )
{
    if( mChar.isCasting )
        return;

    var spellNum    = mChar.spellCast;
    var mSpell      = Spells[spellNum];

    // Set spellcast cooldown for caster
    mChar.SetTimer( 6, 0 );
    mChar.spellCast = -1;

    // Delete reagents used by spell
    deleteReagents( mChar, mSpell );

    // Play SFX associated with spell
    mChar.SoundEffect( spellNum, true );

    // Summon the NPC "dupre-summon" at target location
    var summon = SpawnNPC( "dupre-summon", mSock.clickX, mSock.clickY, mSock.clickZ, mChar.worldnumber, mChar.instanceID );
    if( summon )
    {
        // set owner to the summoner
        summon.owner = mChar;
        // make summon follow owner by default
        summon.Follow( mChar );
        summon.wandertype = 1;
    }
}
Finally, to enable actually casting the spell, I wrote the following small script, which I added to jse_fileassociations.scp with a unique script ID (7000 in my case) under the [SCRIPT_LIST] section (7000=custom/mySpellObject.js), and attached the script to an object in-game using 'setscptrig 7000. Now, whenever I double-click the object in question (a death robe), my character summons Dupre the Hero!
function onUseChecked( pUser, iUsed )
{
    pUser.CastSpell( 65 );
    return false;
}
Image

Re: Add costum spells

Posted: Tue Apr 27, 2021 7:26 am
by kcmc
Hey Xuri

Wow, thanks a lot for the detailed insight!

I think I now have a pretty good idea about how things are and where to start from.
Guess, I'll start with baby steps and follow your option b before trying to walk on the moon with your option a. :wink: