prev | toc | next
 

3.7 Programming Argo Spells and Psionic Abilities

The coded effects of spells and psionic abilities are added much like those of skills: modify the skeleton program for spells or psiabs, asys-xspells or asys-xpsiabs.

The following example creates a spell called Energy, which gives a boost of 1dX to the target player's Fatigue, where X is equal to the caster's Mage skill plus Energy skill. For the example, let's assume that the spell has already been defined, and requires a wizard's staff as a component and a pinch of anise as a material. The example will concentrate on topics specific to spells and generic topics not covered in the last two examples. (Psionic abilites are added in exactly the same way, except psiabs don't use components or materials.)

First, a quick review of the functions provided in asys-xspells. DoInstall and DoUninstall are functional equivalents of the corresponding functions in the other skeleton programs. Replace all instances of `ppp' with the name of the program and all instances of `xxx' with the name of the spell. If multiple spells are to handled, duplicated the appropriate `xxx' lines from both functions, once per spell to be handled.

DoTargetLoop starts an event loop for the target player if needed, with the action set to `wait'. Ensuring that the target has an event loop running is often important for spells because the spells wear off on a turn-by-turn basis. If the target does not have an event loop running, the spell will never wear off (Argo does its best to close the resulting loophole, by which a player could do +stop to keep the effects of a beneficial spell indefinitely... when you execute a +stop, beneficial spells expire immediately and harmful ones stay).

As in asys-xskills, DoChecks does some event verification checks immediately before running a spell, in case the situation has changed over the course of the turn... makes sure the target is valid, make sure a valid spell is selected, makes sure the mage has needed components and materials, etc.

DoBreakSomething is provided as a way of adding harmful side effects for critical failures. In general, critical failures should result in a negative effect related to the intended effect of the spell: a spell that is suppose to increase the target's Strength, for example, would decrease either the target's or caster's Strength on a critical failure. For spells that don't really lend themselves to this, you can include DoBreakSomething in the section of code handling critical failures... DoBreakSomething searches the caster's inventory for components used by the current spell, and if it finds one, breaks it (sets it's @a/broken property to yes). A broken object cannot be used until it is repaired.

As discussed in the Player Guide, mages must make a Presence roll to maintain concentration if they are attacked on the turn they are casting a spell, and another if the attack successfully deals damage. This is handled by the DoPresenceChecks function, which should be called right before success rolls are made for any spells. The function works by checking the mage's @a/eloop/attacked and @a/eloop/injured properties. Standard Argo combat programs and martial magic and psionic programs set these to true values when a player is attacked and/or injured; new programs should do the same. The props are cleared by the event manager at the end of the player's turn.

DoCastRoll is the functional equivalent of DoUseRoll in asys-skills... it is used to determine if the mage's spell succeeds. DoDefenceRoll is used to determine if the target player's defence succeeds (very few skills invoke defense rolls, but almost all spells do). Both functions return "normsucc", "normfail", "critsucc", or "critfail". DoCastRoll stores the amount the caster made her roll by in local variable ourAttackVal. DoDefenceRoll stores the amount the defender made his roll by in local variable ourDefenceVal. These values will be negative if the roll failed; comparing the two allows you to determine which player made the roll by the greater amount in order to determine the final result if both player's rolls result in a normal success. (For psionics, this function is called DoFocusRoll

Using the standard naming conventions, the primary function for our spell would be DoEnergy. Verifying that the spell is being legitimately cast, ensuring that the mage has needed components and materials, ensuring that the target is valid, making Presence rolls if required, determining whether the spell goes off successfully... all these tasks — which are required with almost any Argo spell — can be handled with a few relatively short lines of code:

  : DoEnergy  (  --  )    (* make checks and rolls to cast Energy spell *)
  
    VerifyEvent          
  
    DoChecks not if TellWait exit then 
  
    DoPresenceChecks not if exit then
  
    DoCastRoll
  ;

At this point, all necessary checks will have been made, and the roll will have been made to see if the mage got the spell off. The top value on the stack will be either "normsucc", "normfail", "critsucc", or "critfail". Notice that we do not have to include code to that specifies components and materials. DoChecks checks the realm database to find out what components and materials are required; DoPresenceChecks and/or DoCastRoll will use up the materials as needed. The type and number of components (or tools) and materials needed are set when the spell is defined. If the spell is redefined with new components and materials, the code here will not need to change.

So, we know if the spell `went off' or not, but we do not yet know for sure if the results should be applied. If the top value on the stack is "normsucc", and the spell allows a defense roll, then we need to determine what happens with the defender's roll. (The approach used for the standard Argo spells is that even positive, helpful spells invoke a defence roll... it's harder to improve something that's already very high. Newly added spells do not necessarily have to follow this approach.) If we are using defence rolls, we need to roll for the defender, and then compare the results of the two rolls. There are a number of ways this can be done; spells in the standard set use an approach like the following:

: DoEnergy  (  --  )    (* make checks and rolls to cast Energy spell *)
  
  VerifyEvent          
  
  DoChecks not if TellWait exit then 
  
  DoPresenceChecks not if exit then
  
  DoCastRoll
    
  dup "critsucc" smatch if
    DoEnergyCritSucc 
  else
    dup "critfail" smatch if
      DoEnergyCritFail
    else
      "normsucc" smatch if
        DoDefenceRoll
        dup "critsucc" smatch if
          DoEnergyNormFail
        else
          "critfail" smatch if
            DoEnergyNormSucc
          else        
            ourAttackVal @ ourDefenceVal @ >= if
              DoEnergyNormSucc
            else
              DoEnergyNormFail
            then
          then
        then
      else
        DoEnergyNormFail
      then
    then
  then
  
  SetWait NukeStack
;

The nested IF-ELSE-THEN statements here do the following: If the caster's roll results in a normal failure, critical success, or critical failure, the results are immediately applied. If the caster's roll results in a normal success, a roll is made for the defender. If the defender gets a critical success, the spell automatically fails. If the defender gets a critical failure, the spell automatically succeeds. If the defender gets a normal success, the amounts that each player made the roll by are compared: if the caster made her roll by and amount equal to or greater than the defender, the spell succeeds; if the defender made his roll by more, the spell fails. If the defender gets a normal failure, the `made by' amounts are compared in the same way, but the caster will always win. So, these nested statements will route to one of four functions for any possible combination of rolls (you can copy and paste this block of code into your own spells, replacing Energy with the name of your own spell).

So, now we need the four functions that handle the four possible outcomes of the spell. Generally it's easiest to start with normal success, and make critical success and critical failure variations upon this (normal failure will just be a notice to the room that the spell failed).

For the Energy spell, normal success should boost the player's energy... or, more accurately, lower the amount of fatigue the player has spent. Fatigue is stored in @a/stats/fat. A completely rested player will have a zero in this prop, or will not have the prop at all. A tired player will have a positive number (stored as a string) in the prop... the more tired he is, the higher the number will be. So, DoEnergyNormSucc should make a roll of 1dX, where X is equal to the caster's Mage skill plus Energy skill, and decrease the target's fatigue by that amount. Whether or not a player should be allowed to have `negative fatigue', and be `more rested than rested' is a rather interesting question, but it is also for the most part moot: the event manager doesn't allow negative fatigue... if the target gets a `negative fatigue', it will be reset to zero on his next turn. So, we can either allow the very temporary negative fatigue, or we can include some code to check, and make sure that the setting for @a/stats/fat doesn't go below zero. Let's keep things simple, and let asys-eventmgr take care of it... If the target player gets a 10 second or so period to enjoy a magical energy high, fine.

  : DoEnergyNormSucc  (  --  )      (* apply success for energy spell *)
  
    ourTarget @ "@a/stats/fat" over over
    getpropstr atoi
    me @ "@a/skills/mage"   GetModAbility
    me @ "@a/spells/energy" GetModAbility +
    1 swap 0 Dice -
    intostr setprop
    
    ">>  $me casts $spell $target."
    SpellTell DoTargetLoop
  ;

The GetModAbility library call returns the specified player's ability level for the specified ability, as an integer, modified for factors such as objects that influence abilities. So, we can get the value of X in 1dX by adding the results of GetModAbility for the Mage skill and the Energy spell. A little stack handling puts this in the proper order to call Dice. The result is subtracted from the target's current fatigue and the result of this subtraction is applied as the new fatigue. (The ourTarget variable is initialized to the target player in DoChecks, which makes sure the target is valid and stores its debref in ourTarget if so.)

SpellTell handles notifications of spell results to the room. The substring $me is replaced at runtime with the caster's name, $spell with the name of the spell, and $target with the name of the target. If the target is the caster herself, the $target portion of the string is omitted. A couple examples:

  +cast energy on antar
  >>  Nim casts Energy on Antar.
  
  +cast energy on me
  >>  Nim casts Energy.

For critical failure, we could use DoBreakSomething to break the caster's wizard's staff, but this spell does lend itself to `backfire' type critical failures... we could have it increase the fatigue of either the target or the caster. Since the caster is expecting to spend her own fatigue by casting the spell, and evidently really wants to reduce the fatigue of the target (or else she wouldn't be casting the spell), applyng the backfire to the target seems a more severe penalty, so let's use that:

: DoEnergyCritFail ( -- ) (* apply crit fail for energy spell *) ourTarget @ "@a/stats/fat" over over getpropstr atoi me @ "@a/skills/mage" GetModAbility me @ "@a/spells/energy" GetModAbility + 1 swap 0 Dice + intostr setprop ">> $me's attempt to cast $spell $targetfails. [CRITICAL FAILURE]" SpellTell DoTargetLoop ;

This is the same as the results for a normal success, except that the result of the die roll is added to the amount of fatigue the target has spent rather than subtracted. And the output of the notify is modified, telling the room that the spell failed critically. In this case, there's no harm in telling the truth about the critical failure; for information-gathering spells, a critical failure should often instead lie about the results.

A critical success is the same as a normal success, except the amount of the roll is doubled:

  : DoEnergyCritSucc  (  --  ) (* apply crit success for energy spell *)
  
    ourTarget @ "@a/stats/fat" over over
    getpropstr atoi
    me @ "@a/skills/mage"   GetModAbility
    me @ "@a/spells/energy" GetModAbility +
    1 swap 0 Dice 2 * -
    intostr setprop
    
    ">>  $me casts $spell $target. [CRITICAL SUCCESS]"
    SpellTell DoTargetLoop
  ;

Notice that there is no code in the critical success funtion to handle automatic rolls for experience points. This is handled in DoCastRoll. If you don't want automatic experience points at all, use +tune to set the auto_xp sysparm to no. If you want automatic experience points in general, but not for this spell, delete or comment out the following line from DoCastRoll:

    dup  4 <= if me @ RollXPs then

After the header comment is created and the an entry for the spell is created in the online manual, we're done. Here is the final product.

prev | toc | top | next