If I made a CombatLib

(finally got around to changing the title)

A few times, I've looked at CombatLib when trying to help someone else on here; and it always seems a little awkward to me. I'm wondering if, because of the complexity combat can entail, it might be useful to build a CombatLib that's more tightly structured. Not to insult the original, it's a big project. But in places it feels like it was built for a specific game, and then extended in ways that don't quite fit.

For example, rather than giving every monster two very similar scripts doattack for when the player attacks it, and attackplayer for when it attacks either the player or another monster (which is a little confusing in itself), it might make more sense that a monster/player/spell/weapon/armour can have scripts beginattack and receiveattack. The former calculates damage (allowing for spells like the FF series "demi" which have special damage calculation methods) and the latter applies damage to the target (allowing for things such as elemental resistance and similar).

I'm thinking that each attack would be calculated as a 'pipeline'. A temporary object is created to represent the attack, with properties including attack power, attack elements, attack accuracy, calculated damage, attack success (boolean), and success/failure/critical strings. This object would then be passed as a parameter to all the scripts in its path; including room/game "beforeattack" and "afterattack" scripts, and room/monster specific "beforedeath", "makedead" and "afterdeath" scripts.

An attack object would be useful, because any of the scripts on its path could modify it, making it easier for a game to deal with even relatively odd stuff (for example, I vaguely recall there being a 2nd ed D&D monster that can be harmed normally by other damage types, but will happily run on negative HP without dying until it's hit by a fire attack. The tri-stat Dark Side Of The Sun RPG has a ).

CombatLib's method of dealing with attackdesc and similar is pretty neat. But I'd probably make it look more flexible. Rather than using a %, maybe replace variable names like "{attacker}", "{target}", "{weapon}" etc…

(and that's another thought ... rather than ProcessText understanding a variable game.text_processor_this, why not have game.text_processor_params be a dictionary, like the one passed to do or invoke? Then combatlib could just pass the attackdesc to ProcessText, and have the attacker/target/damage/etc filled in automatically)


I definitely lack the technical wherewithal to contribute, but I'm very interested in seeing how this develops and happy to help with testing. Just seeing how these things emerge is great for learning off.


@ ScriptingIshard:

if you want to just study some of the basics of combat coding/scripting, you can take a look at this old combat code I did back when I was learning this stuff for my own first time:

(I used pertex' combat code as the structure for my combat coding, so the credit goes to him)

(Pixie's combat library is much more extensive combat and handling, but if you want a more simple and study of some of the basics of combat coding, this combat code of mine is one good starting point if you're new to combat coding)

https://textadventures.co.uk/forum/quest/topic/3348/noobie-hks-help-me-thread#22485 (pertex' cleaned-up/fixed version link/download of it)

https://textadventures.co.uk/forum/quest/topic/3348/noobie-hks-help-me-thread#22483 (if you don't want to download pertex' fixed/cleaned-up version of my combat code file, here's my combat code, but it's got some errors and is very bad code with lots of unnecessary stuff and/or redundant stuff in it, so be warned, it'll be tough to follow due to its mistakes/errors and lots of unnecessary/redundant code in it)

https://textadventures.co.uk/forum/quest/topic/3348/noobie-hks-help-me-thread#22486 (key/legend for it... I've learned to never use abrevs ever again/since, lol. Sorry about all of the abrevs in this old combat code of mine)


My thought for structure is that there would be a standard set of scripts, run through in order.
These scripts may be attributes of several objects:

  • The game
  • The room containing the combat (or its parent rooms)
  • The attacker
  • The weapon/attack/spell
  • The target
  • The target's armour
  • Any effects/spells acting on the attacker or target

For the target, the target's armour, and any spells on the target, there would be an "-ed" suffix. So we'd run the attacker's "beforeattack" script, but the target's "beforeattacked".
Any of these scripts can modify the attack object, altering damage, elements, or success/critical flags. Scripts, in order:

  1. beforeattack(ed)
  • Attacker's scripts:
    1. beforemakeattack - a 'confuse' spell that can change your target might go here
    2. makeattack - There would be a default script for this one, whose purpose is to calculate damage and set attackdesc and similar variables to sane values. This is where we do the dice rolls.
    3. aftermakeattack - a poison which makes you inflict half damage might be put here, to modify the already-calculated damage values
  1. onattack(ed)
  2. if the attack has multiple targets, it's cloned and the following are run once for each target. So magic armour that automatically counters a spell attacking you would have an "onattacked" script, while a magic shield that prevents you being hit by fireballs would have a "beforeresolveattack" - only the former protects your allies too.
  • Target's scripts (Note that changing the attack's target at this point will start over):
    1. beforeresolveattack - a spell like "protection from fire"might have a script here
    2. resolveattack - Default script applies damage to the target, and apply effects if they're still alive
    3. afterresolveattack
  1. afterattack(ed)success / afterattack(ed)failure / afterattack(ed)crit / afterattack(ed)kill
  2. print the attack's message, as relevant
  3. afterattack(ed)

I know it seems a bit overcomplex, but I think it would be useful to have in a library. Because it would rarely be necessary to make a new script re-implementing the damage calculation script, you can just add scripts to be called at various points to add all the customisation you need. It's a system designed to be as extensible as it needs to be, where almost every spell, status effect, and magical item I can think of could be added quite easily.


Writing functions instead of scripts could be more clear and "clean", but you must pay attention of every possibility in which it could be called. So keeping this in mind is important to prevent bugs. I just did what you are saying with the code last summer, trying to "customize" the combat library with the Warhammer Fantasy Roleplay basic rules. So I changed the scripts to functions, callable from every pc or npc object. The result was non so bad, it was a big coding (for my little "cat" brain) ... :-) but the result was near to what you are suggesting!
So, have a nice coding and good work! If you want any help (if you need) let me know, I will be happy to contribute!


Also, I applied the Effective Initiative during an attack (see WFRP rules). This means that a "creature with Initiative of 40 and 2 Attacks could not strike both before a creature with Initiative 35 and 5 Attacks. Otherwise, this seems implausible. "
See what I am referring here (WFRP, Multiple Attacks) : http://wfrp1e.wikia.com/wiki/Combat


I said scripts for a reason. Functions have global scope.

I'm looking at a function which contains only the core mechanics. Object scripts allow the code to be encapsulated, making it more organised, more stable, and easier to maintain.

A weapon that follows different damage allocation rules? A shield spell that has a percentage chance of reflecting arrows back at the attacker? A magic ring that increases your weapon damage when fighting drunk? A cursed cathedral where non-blessed weapons do half damage? A monster that only dies if hit by fire damage?

Anything like that needs to be accounted for as a special case in the combat code. If you put it in a function, then you either have an incredibly complex function with an awful lot of 'if' clauses checking dozens of attributes; or you give each spell/weapon/monster a script attribute containing its special-case code.

Encapsulation is good; especially if the code might be reused in a future game. Any code applying to a particular weapon/spell/monster/location belongs in a script attribute. That is what they're for.

(yes, I could use delegates instead. But in this case there is no benefit to doing so, and I've looked into the Quest code enough that I don't trust them not to introduce bizarre scope bugs in unintuitive edge cases)


Looking at this again, I thought I might try to make some actual code.
I'm making a game now, just so I've got something to focus my efforts on in between writing prose. But figure I'll post the code here as well, so if you can see any problems with it, please let me know.

(edited for silly error. Still needs a lot of work)

Old version of the code, preserved for the curious
<function name="DoAttack" parameters="attacker, weapon, spell, target">
  object_name = "current_attack"
  while (not GetObject(object_name) = null) {
    object_name = object_name + "_"
  }
  create (object_name)
  attack = GetObject (object_name)

  if (attacker = null) attacker = game
  if (weapon = null) weapon = attacker
  if (spell = null) spell = weapon
  attack.attacker = attacker
  attack.weapon = weapon
  attack.spell = spell
  attack.valid = true
  if (target = null) {
    target = GetValidTargets (attack)
    if (not GetBoolean (spell, "attack_multiple")) target = PickOneObject (target)
  }
  attack.target = target
  attack.filters_attacker = NewObjectList()
  if (not (spell = weapon or spell = attacker)) {
    attack.filters_attacker = GetEffects (spell)
  }
  if (not weapon = attacker) {
    attack.filters_attacker = ListCombine (attack.filters_attacker, GetEffects (weapon))
  }
  attack.filters_attacker = ListCombine (attack.filters_attacker, GetEffects (attacker))
  foreach (room, ListParents (attacker)) {
    attack.filters_attacker = ListCombine (attack.filters_attacker, GetEffects (room))
  }
  list add (attack.filters_attacker, game)
  list add (attack.filters_attacker, attack)
  attack.filters_target = GetEffects (target)
  foreach (room, ListParents (target)) {
    attack.filters_target = ListCombine (attack.filters_target, GetEffects (room))
  }
  list add (attack.filters_target, game)
  list add (attack.filters_target, attack)
  params = QuickParams ("attack", attack)
  foreach (obj, attack.filters_attacker) {
    if (HasScript (obj, "beforeattack")) {
      do (obj, "beforeattack", params)
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_target) {
      if (HasScript (obj, "beforeattacked")) {
        do (obj, "beforeattacked", params)
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_attacker) {
      if (HasScript (obj, "beforemakeattack")) {
        do (obj, "beforemakeattack", params)
      }
    }
  }
  if (attack.valid) {
    done = false
    foreach (obj, attack.filters_attacker) {
      if (HasScript (obj, "makeattack")) {
        do (obj, "makeattack", params)
        done = true
      }
    }
    if (not done) {
      attack.attackroll = GetRandomInt(1, 12)
      attack.message = "{=CapFirst(GetDisplayName(attack.attacker))} {=Conjugate(attack.attacker, \"attack\")} {=GetDisplayName(attack.target)} {either attack.hit:for {attack.damage} damage:but {=WriteVerb(attack.attacker, \"attack\")}."
      if (HasString (spell, "damageroll")) {
        attack.damage = eval (spell.damageroll, params)
      }
      if (HasInt (spell, "damage")) {
        attack.damage = GetRandomInt (spell.damage, spell.damage * 2)
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_attacker) {
      if (HasScript (obj, "aftermakeattack")) {
        do (obj, "aftermakeattack", params)
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_attacker) {
      if (HasScript (obj, "onattack")) {
        do (obj, "onattack", params)
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_target) {
      if (HasScript (obj, "onattacked")) {
        do (obj, "onattacked", params)
      }
    }
  }
  // code for splitting target lists should go here.
  if (attack.valid) {
    foreach (obj, attack.filters_target) {
      if (HasScript (obj, "beforeresolveattack")) {
        do (obj, "beforeresolveattack", params)
      }
    }
  }
  if (attack.valid) {
    done = false
    foreach (obj, attack.filters_target) {
      if (HasScript (obj, "resolveattack")) {
        do (obj, "resolveattack", params)
        done = true
      }
    }
    if (not done) {
      attack.dodgeroll = GetRandomInt (1, 12)
      attack.hit = (attack.attackroll + GetSkill(attacker, "combat") > attack.dodgeroll + GetInt (target, "agility"))
      if (not HasAttribute (attack, "status")) attack.status = NewStringList()
      if (attack.hit and attack.damage > 0) {
        target.health = target.health - attack.damage
        list add (attack.status, "hit")
        if (target.health < 0) {
          list add (attack.status, "kill")
        }
      }
      else {
        attack.damage = 0
        list add (attack.status, "miss")
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_target) {
      if (HasScript (obj, "afterresolveattack")) {
        do (obj, "afterresolveattack", params)
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_attacker) {
      foreach (status, attack.status) {
        if (HasScript (obj, "afterattack"+status)) {
          do (obj, "afterattack"+status, params)
        }
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_target) {
      foreach (status, attack.status) {
        if (HasScript (obj, "afterattacked"+status)) {
          do (obj, "afterattacked"+status, params)
        }
      }
    }
  }
  if (attack.valid and HasString (attack, "message")) {
    if (ListContains (ScopeVisible(), attacker) or ListContains (ScopeVisible(), target) or target = game.pov or attacker = game.pov) {
      message = attack.message
      game.text_processor_variables = GetAttackParams (attack)
      while (not message = null) {
        newmessage = ProcessText (message)
        if (message = newmessage) {
          msg (message)
          message = null
        }
        else {
          message = newmessage
        }
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_attacker) {
      if (HasScript (obj, "afterattack")) {
        do (obj, "afterattack", params)
      }
    }
  }
  if (attack.valid) {
    foreach (obj, attack.filters_target) {
      if (HasScript (obj, "afterattacked")) {
        do (obj, "afterattacked", params)
      }
    }
  }
  destroy (attack.name)
</function>

<function name="MakeAttackParams" type="dictionary" parameters="attack">
  result = NewDictionary()
  foreach (attr, GetAttributeNames (attack, true)) {
    dictionary add (result, attr, GetAttribute (attack, attr))
  }
  if (not DictionaryContains (result, "attack")) {
    dictionary add (result, "attack", attack)
  }
  return (result)
</function>
Just a first version off the top of my head

(I'm thinking that the game I'm building with this will be mostly a procedural dungeon crawl, spawning random monsters. I just want to play with the systems, rather than coming up with a story that ends up being excessively convoluted. So I'm going to give the player a couple of classes to choose from, each with their own set of abilities, and see how many floors you can get down. As currently planned, the classes will include: Pimp, Philatelist, Edgelord, Janitor/Janitrix, Goth, and Barista)

(At some point in the future, there might be plot involved. But it would be in the form of static dungeon segments, whether rooms or whole areas, which can be spliced into the randomly-generated part in any convenient place.)


There seems to be an awful lot of copy-pasted code in the part above. I can do this better.

EDIT: I realised there's some things I'm missing.

Old version of the code, preserved for the curious
<function name="DoAttack" parameters="attacker, weapon, spell, target">
  if (Equal (attacker, "RESUME")) {
    attacks = weapon
  }
  else {
    object_name = "current_attack"
    while (not GetObject(object_name) = null) {
      object_name = object_name + "_"
    }
    create (object_name)
    attack = GetObject (object_name)
    attack.is_attack = true

    if (attacker = null) attacker = game
    if (weapon = null) {
      if (HasObject (attacker, "weapon")) {
        weapon = attacker.weapon
      }
      else {
        weapon = attacker
      }
    }
    if (spell = null) spell = weapon
    attack.attacker = attacker
    attack.weapon = weapon
    attack.spell = spell
    if (target = null) {
      target = GetValidTargets (attack)
      if (not GetBoolean (spell, "attack_multiple")) target = PickOneObject (target)
    }
    attack.target = target
    attack.filters_attacker = NewObjectList()
    if (not (spell = weapon or spell = attacker)) {
      attack.filters_attacker = GetEffects (spell)
    }
    if (not weapon = attacker) {
      attack.filters_attacker = ListCombine (attack.filters_attacker, GetEffects (weapon))
    }
    attack.filters_attacker = ListCombine (attack.filters_attacker, GetEffects (attacker))
    foreach (room, ListParents (attacker)) {
      attack.filters_attacker = ListCombine (attack.filters_attacker, GetEffects (room))
    }
    list add (attack.filters_attacker, game)
    list add (attack.filters_attacker, attack)
    attack.filters_target = GetEffects (target)
    foreach (room, ListParents (target)) {
      attack.filters_target = ListCombine (attack.filters_target, GetEffects (room))
    }
    list add (attack.filters_target, game)
    list add (attack.filters_target, attack)
    attacks = NewObjectList()
    list add (attacks, attack)
  }

  DoAttackPhase (false, attacks, true, true, "beforeattack")
  DoAttackPhase (false, attacks, true, false, "beforemakeattack")
  DoAttackPhase (false, attacks, true, false, "makeattack") {
    attack.attackroll = GetRandomInt(1, 12)
    attack.message = "{=CapFirst(GetDisplayName(attack.attacker))} {=Conjugate(attack.attacker, \"attack\")} {=GetDisplayName(attack.target)} {either attack.hit:for {attack.damage} damage:but {=WriteVerb(attack.attacker, \"attack\")}."
    if (HasString (spell, "damageroll")) {
      attack.damage = eval (spell.damageroll, params)
    }
    if (HasInt (spell, "damage")) {
      attack.damage = GetRandomInt (spell.damage, spell.damage * 2)
    }
  }
  DoAttackPhase (false, attacks, true, false, "aftermakeattack")
  DoAttackPhase (false, attacks, true, true, "onattack")
  DoAttackPhase (false, attacks, true, true, "splitattack") {
    i = 0
    while (i < ListCount (attacks)) {
      attack = ListItem (attacks, i)
      if (not HasAttribute (attack, "target")) {
        list remove (attacks, attack)
      }
      else if (EndsWith (TypeOf (attack, "target"), "list")) {
        foreach (target, attack.target) {
          newattack = Clone (attack)
          newattack.target = target
          list add (attacks, newattack)
        }
        list remove (attacks, attack)
      }
      else {
        i = i + 1
      }
    }
  }
  DoAttackPhase (false, attacks, false, true, "beforeresolveattack")
  DoAttackPhase (false, attacks, false, true, "resolveattack") {
    attack.dodgeroll = GetRandomInt (1, 12)
    attack.hit = (attack.attackroll + GetSkill(attacker, "combat") > attack.dodgeroll + GetInt (target, "agility"))
    if (not HasAttribute (attack, "status")) attack.status = NewStringList()
      if (attack.hit and attack.damage > 0) {
        target.health = target.health - attack.damage
        list add (attack.status, "hit")
        if (target.health < 0) {
          list add (attack.status, "kill")
        }
      }
      else {
        attack.damage = 0
        list add (attack.status, "miss")
      }
    }
  }
  DoAttackPhase (false, attacks, false, true, "afterresolveattack")
  bystatus = NewDictionary()
  foreach (attack, attacks) {
    if (HasAttribute (attack, "status")) {
      foreach (status, attack.status) {
        if (not DictionaryContains (bystatus, status)) {
          dictionary add (bystatus, status, NewObjectList())
        }
        list add (DictionaryItem (bystatus, status), attack)
      }
    }
  }
  foreach (status, bystatus) {
    DoAttackPhase (false, DictionaryItem (bystatus, status), true, false, "afterattack"+status)
    DoAttackPhase (false, DictionaryItem (bystatus, status), false, true, "afterattacked"+status)
  }
  if (ListContains (ScopeVisible(), attacker) or ListContains (ScopeVisible(), target) or target = game.pov or attacker = game.pov) {
    DoAttackPhase (true, attacks, true, true, "showattackmessage") {
      if (HasString (attack, "message")) {
        message = attack.message
        game.text_processor_variables = GetAttackParams (attack)
        while (not message = null) {
          newmessage = ProcessText (message)
          if (message = newmessage) {
            msg (message)
            message = null
          }
          else {
            message = newmessage
          }
        }
      }
    }
  }
  DoAttackPhase (false, attacks, true, true, "afterattack")
  foreach (att, attacks) {
    destroy (att.name)
  }
</function>

<turnscript name="deferred_attacks">
  <enabled />
  <script><![CDATA[
  resume = NewObjectList()
  foreach (attack, FilterByAttribute(AllObjects(), "is_attack", true)) {
    switch (TypeOf (attack, "deferred")) {
      case ("int") {
        attack.deferred = attack.deferred - 1
        if (attack.deferred < 0) {
          attack.deferred = null
        }
      }
      case ("string") {
        if (Equal (true, eval (attack.deferred, MakeAttackParams(attack)))) {
          attack.deferred = null
        }
      }
      case ("script") {
        do (attack, "deferred")
      }
    }
    if (not HasAttribute (attack, "deferred")) {
      list add (resume, attack)
    }
  }
  if (ListCount (resume) > 0) {
    DoAttack ("RESUME", resume)
  }
  ]]></script>
</turnscript>

<function name="MakeAttackParams" type="dictionary" parameters="attack">
  result = NewDictionary()
  foreach (attr, GetAttributeNames (attack, true)) {
    dictionary add (result, attr, GetAttribute (attack, attr))
  }
  if (not DictionaryContains (result, "attack")) {
    dictionary add (result, "attack", attack)
  }
  return (result)
</function>

<function name="DoAttackPhase" parameters="singleton, attacks, for_attacker, for_target, phase, default">
  newattacks = NewObjectList()
  foreach (attack, attacks) {
    if (not HasAttribute (attack, "phases_complete")) attack.phases_complete = NewStringList()
    done = false
    if (ListContains (attack.phases_complete, phase)) {
      done = true
    }
    else {
      list add (attack.phases_complete, phase)
      if (for_attacker) {
        foreach (obj, attack.filters_attacker) {
          if (HasScript (obj, phase) and (not done or not singleton)) {
            do (obj, phase, MakeAttackParams (attack))
            done = true
          }
        }
      }
      if (for_attacker and for_target) {
        phase = phase + "ed"
      }
      if (for_target) {
        foreach (obj, attack.filters_target) {
          if (HasScript (obj, phase) and (not done or not singleton)) {
            do (obj, phase, MakeAttackParams (attack))
            done = true
          }
        }
      }
      if (IsDefined ("default") and not done) {
        if (TypeOf (default) = "script") {
          invoke (default, MakeAttackParams (attack)
        }
        done = true
      }
    }
    if (HasObject (attack, "replacement")) {
      list add (newattacks, attack.replacement)
    } else if (HasAttribute (attack, "replacement")) {
      newattacks = ListCombine (newattacks, attack.replacement)
    } else if (not GetBoolean (attack, "negated") and not HasInt (attack, "deferred")) {
      list add (newattacks, attack)
    }
  }
  while (ListCount (attacks) > 0) {
    list remove (attacks, ListItem (attacks, 0))
  }
  foreach (attack, newattacks) {
    list add (attacks, newattacks)
  }
</function>

<function name="GetEffects" type="objectlist" parameters="items">
  result = NewObjectList()
  if (not EndsWith (TypeOf (items), "list")) {
    item = items
    items = NewObjectList()
    list add (items, item)
  }
  foreach (item, items) {
    list add (result, item)
    foreach (effect, FilterByAttribute (GetDirectChildren(item), "status_effect", true)) {
      list add (result, effect)
    }
    foreach (garment, GetWornFor (item)) {
      foreach (effect, FilterByAttribute (GetDirectChildren (garment), "status_effect", true)) {
        list add (result, effect)
      }
    }
  }
  return (result)
</function>

i couldnt even get combatkib to work anyone have an idea to tell me how to get it to work it says it loads and i followed the instructions from the site but i cant see the 3 new tabs it describes


@LeftUnscarred
Not sure this is the best post to reply to. I started out thinking about the way CombatLib is implemented, but then changed to working out how I'd make a more versatile CombatLib.

(My version shouldn't require modification - the functions are horrendously complex, but it allows you to do everything I've ever seen in an RPG battle system without changing them, so the user doesn't need to understand how they work internally)

I might change the name to RPGLib or something like that. But changing the thread title would mess with the script I'm using to measure how much time I spend on various projects.

As far as your problems with CombatLib go, I'm afraid you'd have to ask someone who's using the desktop version of Quest.


OK… there's some horrible bits in here.

The whole section from object_name = "current_attack" down to list add (attacks, attack) needs a rewrite.

Working on the code

Here's a thought:

    object_name = "current_attack"
    while (not GetObject(object_name) = null) {
      object_name = object_name + "_"
    }
    create (object_name)
    attack = GetObject (object_name)
    attack.is_attack = true

    attack.generate_filters => {
      att = NewObjectList()
      list add (att, this)
      if (HasAttribute (this, "spell") and not (Equal(this.spell, this.weapon) or Equal(this.spell, this.attacker))) {
        att = ListCombine (att, GetEffects (this.spell))
      }
      if (HasAttribute (this, "weapon") and not Equal(this.weapon, this.attacker)) {
        att = ListCombine (att, GetEffects (this.weapon))
      }
      if (HasAttribute (this, "attacker")) {
        att = ListCombine (att, GetEffects (this.attacker))
        foreach (room, ListAllParents (this.attacker)) {
          att = ListCombine (att, GetEffects (room))
        }
      }
      list add (att, game)
      this.filters_attacker = att
  
      def = NewObjectList()
      list add (def, this)
      if (HasAttribute (this, "target")) {
        def = ListCombine (def, GetEffects (this.target))
        foreach (room, ListAllParents (this.target)) {
          def = ListCombine (def, GetEffects (room))
        }
      }
      list add (def, game)
      attack.filters_target = def
    }
    if (IsDefined ("attacker")) {
      attack.attacker = attacker
    }
    if (IsDefined ("weapon")) {
      attack.attacker = attacker
    }
    if (IsDefined ("spell")) {
      attack.attacker = attacker
    }
    if (IsDefined ("target")) {
      attack.attacker = attacker
    }
    attacks = NewObjectList()
    list add (attacks, attack)

Then we have the function to generate the status-effect lists all in one place. So we can do that script attribute any time the spell's target changes, or something like that.

To the DoAttackPhase function, immediately after list add (attack.phases_complete, phase) we add:

      do (attack, "generate_filters")

In this script I've also used a function ListAllParents which should work as:

<function name="ListAllParents" type="objectlist" parameters="list"><![CDATA[
  if (TypeOf (list) = "object") {
    return (ListParents (object))
  }
  result = NewObjectList()
  i = 0
  while (i < ListCount (list)) {
    item = ListItem (list, i)
    if (HasObject (item, "parent")) {
      if (not ListContains (result, item.parent)) {
        list add (list, item.parent)
        list add (result, item.parent)
      }
    }
    i = i + 1
  }
  return (result)
]]></function>

However, I'm no longer checking that the attacker/weapon/target are valid.

That's fine. Before the line DoAttackPhase (false, attacks, true, true, "beforeattack") we can add:

  // This could be useful in the case of AI scripts
  //    you could have a turnscript: `DoAttack()`
  //    and add this script on the game element to determine which enemies need to do stuff
  DoAttackPhase (true, attacks, true, false, "findvalidattackers") {
    attack.valid_attackers = FilterByAttribute (AllObjects(), "dead", false)
  }
  DoAttackPhase (false, attacks, true, false, "afterfindvalidattackers")
  DoAttackPhase (true, attacks, true, false, "setattacker") {
    if (not HasAttribute (attack, "attacker")) {
      if (HasAttribute (attack, "valid_attackers")) {
        attack.attacker = attack.valid_attackers
      }
      else {
        attack.negated = true
      }
    }
    if (HasObject (attack, "attacker")) {
      if (HasAttribute (attack, "valid_attackers")) {
        if (not ListContains (attack.valid_attackers, attack.attacker)) {
          attack.negated = true
        }
      }
    else if (HasAttribute (attack, "attacker")) {
      // it's a list
      processed = NewObjectList()
      foreach (trying, attack.attacker) {
        if (not ListContains (processed, trying)) {
          if (not HasAttribute (attack, "valid_attackers")) {
            list add (processed, trying)
          }
          else if (ListContains (attack.valid_attackers)) {
            list add (processed, trying)
          }
        }
      }
      if (ListCount (processed) = 0) {
        attack.negated = true
      }
      else if (ListCount (processed) = 1) {
        attack.attacker = ListItem (processed, 0)
      }
      else {
        attack.replacement = NewObjectList()
        foreach (attacker, processed) {
          newclone = CloneObject (attack)
          newclone.attacker = attacker
          list add (attack.replacement, newclone)
        }
      }
    }
  }

... and similar for weapon, spell, and target

So that the enemy AI system can use those attack phases to decide which weapon/spell/target an enemy chooses, and for the player character, presumably pick their equipped weapon if there is one or weapon=attacker otherwise, and the same for the spell/technique.

Noting that I'm having weapon and spell separate, because a spell could either be an actual spell (weapon could be a staff that buffs it, or could be the caster), or a weapon technique (usable with a whole class of weapons), or the weapon (for that weapon's usual attack), or a martial arts technique (weapon = attacker) or the attacker (for their basic unarmed attack)


I can't get this out of my head. So here's the proto-RPGlib as it stands.
Edit: Just realised I missed out a script :p We now allow weapons to have an "attack_attributes" attribute; a list of attributes which should be copied from the spell/weapon/attacker to the attack itself. Useful for things like elemental affinities.

Note that in the case of attacker not being specified, it attempts to start an attack for all valid attackers.
In the case of weapon or spell not being set, the setspell script will be run first. So a spell/technique object can have an afterfindvalidweapons script which filters valid_weapons based on the weapon type that spell or technique can be used with.

This huge function is really horrible. I think a lot of the scripts could be placed on the game element, making it easier to manage (but harder to copy into the web editor)

Still haven't tested all this. I kind of want something that works in my head first.

So, the battle phases are currently:

  • AI hook phases
    • findvalidattackers
    • afterfindvalidattackers
    • setattacker
    • findvalidspells
    • afterfindvalidspells
    • setspell
    • findvalidweapons
    • afterfindvalidweapons
    • setweapon
    • findvalidtargets
    • afterfindvalidtargets
    • settarget
  • beforeattack
  • Attack roll phases:
    • beforemakeattack
    • makeattack
    • aftermakeattack
  • onattack
  • splitattack
  • Damage resolution phases:
    • beforeresolveattack
    • resolveattack
    • afterresolveattack
  • afterattack[hit / miss / crit / kill / etc]
  • showattackmessage
  • afterattack
  • destroyattack
<function name="SetupDefaultCombatScripts"><![CDATA[
  DefaultCombatScript ("findvalidattackers"){
    attack.valid_attackers = FilterByAttribute (AllObjects(), "dead", false)
  }
  DefaultCombatScript ("setattacker") {
    if (not HasAttribute (attack, "attacker")) {
      if (HasAttribute (attack, "valid_attackers")) {
        attack.attacker = attack.valid_attackers
      }
      else {
        attack.negated = true
      }
    }
    if (HasObject (attack, "attacker")) {
      if (HasAttribute (attack, "valid_attackers")) {
        if (not ListContains (attack.valid_attackers, attack.attacker)) {
          attack.negated = true
        }
      }
      else if (HasAttribute (attack, "attacker")) {
        // it's a list
        processed = NewObjectList()
        foreach (trying, attack.attacker) {
          if (not ListContains (processed, trying)) {
            if (not HasAttribute (attack, "valid_attackers")) {
              list add (processed, trying)
            }
            else if (ListContains (attack.valid_attackers)) {
              list add (processed, trying)
            }
          }
        }
        if (ListCount (processed) = 0) {
          attack.negated = true
        }
        else if (ListCount (processed) = 1) {
          attack.attacker = ListItem (processed, 0)
        }
        else {
          attack.replacement = NewObjectList()
          foreach (attacker, processed) {
            newclone = CloneObject (attack)
            newclone.attacker = attacker
            list add (attack.replacement, newclone)
          }
        }
      }
    }
  }
  DefaultCombatScript ("findvalidspells") {
    attack.valid_spells = NewObjectList()
    if (HasAttribute (attack, "weapon")) {
      if (HasAttribute (attack.weapon, "available_spells")) {
        attack.valid_spells = attack.weapon.available_spells
      }
    }
    if (HasAttribute (attack, "attacker")) {
      if (HasAttribute (attack.attacker, "available_spells")) {
        attack.valid_spells = ListCombine (attack.valid_spells, attack.attacker.available_spells)
      }
    }
  }
  DefaultCombatScript ("setspell") {
    if (not HasObject (attack, "spell") and not attacker = game.pov) {
      attack.spell = PickOneObject (attack.valid_spells)
    }
    if (not HasObject (attack, "spell")) {
      if (attack.attacker = game.pov) {
        if (HasAttribute (attack, "weapon")) {
          if (HasAttribute (weapon, "default_spell")) {
            attack.spell = weapon.default_spell
          }
          else {
            attack.spell = weapon
          }
        }
        else if (HasAttribute (attacker, "default_spell")) {
          attack.spell = attacker.default_spell
        }
        else {
          attack.spell = attacker
        }
      }
    }
  }
  DefaultCombatScript ("findvalidweapons") {
    attack.valid_weapons = FilterByAttribute (GetAllChildObjects (attack.attacker), "is_weapon", true)
  }
  DefaultCombatScript ("setweapon") {
    if (not HasObject (attack, "weapon")) {
      if (EndsWith (TypeOf (attack, "weapon"), "list")) {
        attack.weapon = PickOneObject (attack.weapon)
      }
    }
    if (not HasObject (attack, "weapon")) {
      if (EndsWith (TypeOf (attack, "valid_weapons"), "list")) {
        attack.weapon = PickOneObject (attack.weapon)
      }
    }
    if (not HasObject (attack, "weapon")) {
      attack.weapon = attack.attacker
    }
  }
  DefaultCombatScript ("findvalidtargets") {
    if (attacker = game.pov) {
      attack.valid_targets = ScopeReachable()
    }
    else {
      attack.valid_targets = FilterByAttribute (ListExclude (GetDirectChildren (attacker.parent), attacker), "dead", false)
    }
  }
  DefaultCombatScript ("settarget") {
    if (not HasAttribute (attack, "target") and not attacker = game.pov) {
      attack.target = PickOneObject (attack.valid_targets)
    }
  }
  DefaultCombatScript ("beforeattack") {
    foreach (type, Split ("spell;weapon;attacker")) {
      if (HasObject (attack, type)) {
        obj = GetAttribute (attack, type)
        if (HasAttribute (obj, "attack_attributes")) {
          foreach (attr, obj.attack_attributes) {
            if (not HasAttribute (attack, attr)) {
              if (EndsWith (TypeOf (obj, "attack_attributes"), "dictionary")) {
                set (attack, attr, DictionaryItem (obj.attack_attributes, attr))
              }
            }
            if (HasAttribute (obj, attr) and not HasAttribute (attack, attr)) {
              set (attack, attr, GetAttribute (obj, attr))
            }
          }
        }
      }
    }
  }
  DefaultCombatScript ("makeattack") {
    attack.attackroll = GetRandomInt(1, 12)
    attack.message = "{=CapFirst(GetDisplayName(attack.attacker))} {=Conjugate(attack.attacker, \"attack\")} {=GetDisplayName(attack.target)} {either attack.hit:for {attack.damage} damage:but {=WriteVerb(attack.attacker, \"attack\")}."
    if (HasString (spell, "damageroll")) {
      attack.damage = eval (spell.damageroll, params)
    }
    if (HasInt (spell, "damage")) {
      attack.damage = GetRandomInt (spell.damage, spell.damage * 2)
    }
  }
  DefaultCombatScript ("splitattack") {
    i = 0
    while (i < ListCount (attacks)) {
      attack = ListItem (attacks, i)
      if (not HasAttribute (attack, "target")) {
        list remove (attacks, attack)
      }
      else if (EndsWith (TypeOf (attack, "target"), "list")) {
        foreach (target, attack.target) {
          newattack = Clone (attack)
          newattack.target = target
          list add (attacks, newattack)
        }
        list remove (attacks, attack)
      }
      else {
        i = i + 1
      }
    }
  }
  DefaultCombatScript ("resolveattack") {
    attack.dodgeroll = GetRandomInt (1, 12)
    attack.hit = (attack.attackroll + GetSkill(attacker, "combat") > attack.dodgeroll + GetInt (target, "agility"))
    if (not HasAttribute (attack, "status")) attack.status = NewStringList()
    if (attack.hit and attack.damage > 0) {
      target.health = target.health - attack.damage
      list add (attack.status, "hit")
      if (target.health < 0) {
        list add (attack.status, "kill")
      }
    }
    else {
      attack.damage = 0
      list add (attack.status, "miss")
    }
  }
  DefaultCombatScript ("showattackmessage") {
    if (HasString (attack, "message")) {
      message = attack.message
      game.text_processor_variables = GetAttackParams (attack)
      while (not message = null) {
        newmessage = ProcessText (message)
        if (message = newmessage) {
          msg (message)
          message = null
        }
        else {
          message = newmessage
        }
      }
    }
  }
]]></function>

<function name="DefaultCombatScript" parameters="name, script">
  if (not HasScript (game, name)) {
    set (game, name, script)
  }
</function>

<function name="DoAttack" parameters="attacker, weapon, spell, target">
  if (Equal (attacker, "RESUME")) {
    attacks = weapon
  }
  else {
    object_name = "current_attack"
    while (not GetObject(object_name) = null) {
      object_name = object_name + "_"
    }
    create (object_name)
    attack = GetObject (object_name)
    attack.is_attack = true

    attack.generate_filters => {
      att = NewObjectList()
      list add (att, this)
      if (HasAttribute (this, "spell") and not (Equal(this.spell, this.weapon) or Equal(this.spell, this.attacker))) {
        att = ListCombine (att, GetEffects (this.spell))
      }
      if (HasAttribute (this, "weapon") and not Equal(this.weapon, this.attacker)) {
        att = ListCombine (att, GetEffects (this.weapon))
      }
      if (HasAttribute (this, "attacker")) {
        att = ListCombine (att, GetEffects (this.attacker))
        foreach (room, ListAllParents (this.attacker)) {
          att = ListCombine (att, GetEffects (room))
        }
      }
      list add (att, game)
      this.filters_attacker = att
  
      def = NewObjectList()
      list add (def, this)
      if (HasAttribute (this, "target")) {
        def = ListCombine (def, GetEffects (this.target))
        foreach (room, ListAllParents (this.target)) {
          def = ListCombine (def, GetEffects (room))
        }
      }
      list add (def, game)
      attack.filters_target = def
    }
    if (IsDefined ("attacker")) {
      attack.attacker = attacker
    }
    if (IsDefined ("weapon")) {
      attack.attacker = attacker
    }
    if (IsDefined ("spell")) {
      attack.attacker = attacker
    }
    if (IsDefined ("target")) {
      attack.attacker = attacker
    }
    attacks = NewObjectList()
    list add (attacks, attack)
  }

  // This could be useful in the case of AI scripts
  //    you could have a turnscript: `DoAttack()`
  //    and add this script on the game element to determine which enemies need to do stuff
  DoAttackPhase (true, attacks, true, false, "findvalidattackers", null)
  DoAttackPhase (false, attacks, true, false, "afterfindvalidattackers", null)
  DoAttackPhase (true, attacks, true, false, "setattacker", null)
  DoAttackPhase (true, attacks, true, false, "findvalidspells", null)
  DoAttackPhase (false, attacks, true, false, "afterfindvalidspells", null)
  DoAttackPhase (true, attacks, true, false, "setspell", null)
  DoAttackPhase (true, attacks, true, false, "findvalidweapons", null)
  DoAttackPhase (false, attacks, true, false, "afterfindvalidweapons", null)
  DoAttackPhase (true, attacks, true, false, "setweapon", null)
  DoAttackPhase (true, attacks, true, false, "findvalidtargets", null)
  DoAttackPhase (false, attacks, true, false, "afterfindvalidtargets", null)
  DoAttackPhase (true, attacks, true, false, "settarget", null)
  DoAttackPhase (false, attacks, true, true, "beforeattack", null)
  DoAttackPhase (false, attacks, true, false, "beforemakeattack", null)
  DoAttackPhase (false, attacks, true, false, "makeattack", null)
  DoAttackPhase (false, attacks, true, false, "aftermakeattack", null)
  DoAttackPhase (false, attacks, true, true, "onattack", null)
  DoAttackPhase (false, attacks, true, true, "splitattack", null)
  DoAttackPhase (false, attacks, false, true, "beforeresolveattack", null)
  DoAttackPhase (false, attacks, false, true, "resolveattack", null)
  DoAttackPhase (false, attacks, false, true, "afterresolveattack", null)
  bystatus = NewDictionary()
  foreach (attack, attacks) {
    if (HasAttribute (attack, "status")) {
      foreach (status, attack.status) {
        if (not DictionaryContains (bystatus, status)) {
          dictionary add (bystatus, status, NewObjectList())
        }
        list add (DictionaryItem (bystatus, status), attack)
      }
    }
  }
  foreach (status, bystatus) {
    DoAttackPhase (false, DictionaryItem (bystatus, status), true, false, "afterattack"+status, null)
    DoAttackPhase (false, DictionaryItem (bystatus, status), false, true, "afterattacked"+status, null)
  }
  if (target = game.pov or attacker = game.pov or ListContains (ScopeVisible(), attacker) or ListContains (ScopeVisible(), target)) {
    DoAttackPhase (true, attacks, true, true, "showattackmessage", null)
  }
  else {
    // A script the user can implement if they want the player to see a message about an attack hitting
    //   when the player isn't in the same room
    //       ("You hear the distinctive {b:BOOM} of a fireball from a distant room!")
    DoAttackPhase (true, attacks, true, true, "showremoteattackmessage", null)
  }
  DoAttackPhase (false, attacks, true, true, "afterattack", null)
  DoAttackPhase (false, attacks, true, true, "destroyattack") {
    attack.destroy = true
  }
</function>

<turnscript name="deferred_attacks">
  <enabled />
  <script><![CDATA[
  resume = NewObjectList()
  foreach (attack, FilterByAttribute(AllObjects(), "is_attack", true)) {
    switch (TypeOf (attack, "deferred")) {
      case ("int") {
        attack.deferred = attack.deferred - 1
        if (attack.deferred < 0) {
          attack.deferred = null
        }
      }
      case ("string") {
        if (Equal (true, eval (attack.deferred, MakeAttackParams(attack)))) {
          attack.deferred = null
        }
      }
      case ("script") {
        do (attack, "deferred")
      }
    }
    if (not HasAttribute (attack, "deferred")) {
      list add (resume, attack)
    }
  }
  if (ListCount (resume) > 0) {
    DoAttack ("RESUME", resume)
  }
  ]]></script>
</turnscript>

<function name="MakeAttackParams" type="dictionary" parameters="attack">
  result = NewDictionary()
  foreach (attr, GetAttributeNames (attack, true)) {
    dictionary add (result, attr, GetAttribute (attack, attr))
  }
  if (not DictionaryContains (result, "attack")) {
    dictionary add (result, "attack", attack)
  }
  return (result)
</function>

<function name="DoAttackPhase" parameters="singleton, attacks, for_attacker, for_target, phase, default">
  original_phase = phase
  newattacks = NewObjectList()
  foreach (attack, attacks) {
    if (GetBoolean (game, "combat_playthrough_menu") or not HasScript (game, "menucallback")) {
      if (not HasAttribute (attack, "phases_complete")) attack.phases_complete = NewStringList()
      done = false
      if (ListContains (attack.phases_complete, phase)) {
        done = true
      }
      else {
        do (attack, "generate_filters")
        if (for_attacker) {
          foreach (obj, attack.filters_attacker) {
            if (HasScript (obj, phase) and (not done or not singleton)) {
              do (obj, phase, MakeAttackParams (attack))
              done = true
            }
          }
        }
        if (for_attacker and for_target) {
          phase = phase + "ed"
        }
        if (for_target) {
          foreach (obj, attack.filters_target) {
            if (HasScript (obj, phase) and (not done or not singleton)) {
              do (obj, phase, MakeAttackParams (attack))
              done = true
            }
          }
        }
        if (IsDefined ("default") and not done) {
          if (TypeOf (default) = "script") {
            invoke (default, MakeAttackParams (attack)
          }
          done = true
        }
        list add (attack.phases_complete, phase)
      }
      if (HasObject (attack, "replacement")) {
        replist = NewObjectList()
        list add (replist, attack.replacement)
        attack.replacement = replist
      }
      if (HasAttribute (attack, "replacement")) {
        if (not singleton) {
          DoAttackPhase(singleton, attack.replacement, for_attacker, for_target, original_phase, default)
        }
        newattacks = ListCombine (newattacks, attack.replacement)
        if (not ListContains (attack.replacement, attack)) {
          attack.destroy = true
        }
      }
      if (not GetBoolean (attack, "negated") and not GetBoolean (attack, "destroy") and not HasAttribute (attack, "deferred")) {
        list add (newattacks, attack)
      }
    }
    else {
      newattacks = NewObjectList()
      if (not HasAttribute (attack, "deferred")) {
        attack.deferred = "not HasScript (game, \"menucallback\")"
      }
    }
  }
  while (ListCount (attacks) > 0) {
    list remove (attacks, ListItem (attacks, 0))
  }
  foreach (attack, FilterByAttribute (newattacks, "destroy", true)) {
    destroy (attack.name)
  }
  foreach (attack, newattacks) {
    list add (attacks, newattacks)
  }
</function>

<function name="GetEffects" type="objectlist" parameters="items">
  result = NewObjectList()
  if (not EndsWith (TypeOf (items), "list")) {
    item = items
    items = NewObjectList()
    list add (items, item)
  }
  foreach (item, items) {
    list add (result, item)
    foreach (effect, FilterByAttribute (GetDirectChildren(item), "status_effect", true)) {
      list add (result, effect)
    }
    foreach (garment, GetWornFor (item)) {
      foreach (effect, FilterByAttribute (GetDirectChildren (garment), "status_effect", true)) {
        list add (result, effect)
      }
    }
  }
  return (result)
</function>

<function name="ListAllParents" type="objectlist" parameters="list"><![CDATA[
  if (TypeOf (list) = "object") {
    return (ListParents (object))
  }
  result = NewObjectList()
  i = 0
  while (i < ListCount (list)) {
    item = ListItem (list, i)
    if (HasObject (item, "parent")) {
      if (not ListContains (result, item.parent)) {
        list add (list, item.parent)
        list add (result, item.parent)
      }
    }
    i = i + 1
  }
  return (result)
]]></function>

OK ... question for people who aren't so interested in reading through huge chunks of code.

The point of this was to make some core functions so that any type of attack you want to create can be done simply, with minimal code. And you can make your own weapons, spells and monsters without needing to understand how DoAttack() works, just like you can create your own commands and verbs without needing to understand how the parser functions work.

So… I think this should be able to handle any kind of attack you want to make.
What kind of attack would you like to see?

Here's one that comes to mind; a grenade:

<object name="grenades">
  <attr name="is_weapon" type="boolean">true</attr>
  <number type="int">7</number>
  <look type="string">A bunch of {this.number} grenades</look>
  <damage type="int">24</damage>
  <elements type="simplestringlist">fire;percussion</elements>
  <attr name="attack_attributes" type="simplestringlist">elements</attr>

  <beforeattack type="script"><![CDATA[
    // at the start of throwing the grenade, deduct 1 from the number the attacker is carrying
    this.number = this.number - 1
    if (this.number <= 0) {
      if (Has (this)) {
        msg ("That was my last grenade.")
      }
      RemoveObject (this)
    }
  ]]></beforeattack>

  <aftermakeattack type="script">
    // after the grenade is thrown, make it visible, put it in the room with the target
    // and set it to go off after 3 turns
    attack.visible = true
    attack.alias = "grenade (LIVE!)"
    attack.look = "The grenade looks like it's about to go off!"
    attack.deferred = 3
    attack.take = true
    attack.drop = true
    attack.displayverbs = Split ("look;take")
    attack.inventoryverbs = Split ("look;drop")
    attack.parent = attack.target.parent
    attack.message = "{either Contains(target,attack):{=GetDisplayAlias(target)} was carrying a grenade. Bad idea:Exploding fragments do {attack.damage} damage to {=GetDisplayAlias(target)}}."
  </aftermakeattack>

  <onattack type="script">
    // The grenade is going off now, so work out who's within range
    //    First, find the room containing the grenade, even if someone picked it up
    room = attack.parent
    while (not room = null and not GetBoolean (room, "isroom")) {
      room = room.parent
    }
    if (room = null) {
      attack.destroy = true
    }
    else {
      // Set the grenade's targets to everyone in the room
      msg ("A grenade goes off in the " + GetDisplayAlias (room) ".")
      attack.target = FilterByAttribute (GetDirectChildren (room), "dead", false)
      // And make sure that anyone carrying the grenade suffers
      attack.beforeresolveattack => {
        if (Contains (target, attack)) {
          attack.damage = attack.damage * GetRandomInt (5, 9)
        }
      }
    }
  </onattack>
</object>

And here's a simple status effect that makes you immune to fire:

<object name="fireproof">
  <attr name="status_effect" type="boolean">true</attr>
  <visible>false</visible>
  <beforeresolveattack type="script">
    if (HasAttribute (attack, "elements")) {
      if (ListContains (elements, "fire")) {
        attack.destroy = true
        if (not IsDefined ("alias")) {
          alias = GetDisplayAlias (spell)
        }
        msg (CapFirst(GetDisplayAlias(this.parent)) + " is protected from the " + alias + " by their Fireproof Aura.")
      }
    }
  </beforeresolveattack>
</object>

Yes, I'd like to see a new combat code, and I'd like to see how the attacking works for that code.


Looking forward to seeing how this turns out. I sent you a PM by the way (I have found the system flags them as read even when not sometimes, so you might not realise).


Awesome work mrangel (along with all of the other stuff you've done too), been lurking on your posts/threads of quest code, and plan to study all of them, eventually. Thanks for the awesome code work you've done on everything, this post/thread, and others!


This one's still got issues, but I think I can see the shape of it.

The functions probably need renaming, because it's not just for combat. It's a simple way to handle spellcasting as well, and it feels weird using a function called DoAttack() to apply a healing spell on someone.

Off the top of my head, here's a couple of commands that could be useful:

<command name="attack">
  <pattern>attack #object#</pattern>
  <script>
    if (GetBoolean (object, "dead")) {
      msg (CapFirst (WriteVerb(object, "be")) + " already dead.")
    }
    else if (HasBoolean (object, "dead")) {
      DoAttack (game.pov, null, null, object)
    }
    else if (HasScript(object, "attack")) {
      do (object, "attack")
    }
    else if (HasString(object, "attack")) {
      game.text_processor_this = object
      msg (object.attack)
    }
    else {
      msg ("That isn't something you can attack.")
    }
  </script>
<command>

<command name="spellcast">
  <pattern>cast #object_spell# on #object_target#;cast #object_spell#</pattern>
  <script>
    if (not IsDefined ("object_target")) {
      object_target = null
    }
    DoAttack (game.pov, null, object_spell, object_target)
  </script>
</command>

Next step is making it work nicely with menus. I figure that a sword could have a script like:

<setspell type="script">
  if (not IsDefined ("spell")) {
    // Assume this function returns a list of objects like "stab", "slash", "Infinite Bladestorm", etc
    spells = GetWeaponAttackTypes (attacker, this)
    if (attacker = game.pov) {
      list add (spells, "Cancel")
      ShowMenu ("How do you want to attack?", spells, false) {
        if (result = "Cancel") {
          attack.destroy = true
        }
        else {
          attack.spell = GetObject (result)
        }
      }
    }
    else {
      // It's an NPC, so pick randomly
      attack.spell = PickOneObject (spells)
    }
  }
</setspell>

I think I can see a place in DoAttackPhase where it could check for the existence of game.menucallback and hold off running any of the remaining phases until next turn; as well as ensuring that all our variables are accessible within the menu callback.

Do you think that would be useful? It's adding some fairly inscrutable code, but created with the intent that there will be no need for the user to modify it.


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums