NPC factions?

Sorry if this has been asked before - a quick search didn't turn it up. Like my name says, scripting isn't my strong point though I'm slowly getting the hang of it (and shamelessly exploiting pix's libraries) for a lot of basic fundamentals and I can do most of what I need as is.

The current exception is a tricky one, though. I want to have NPCs dynamically fight each other, based on factional allegiance - or monster type, e.g. vampires not otherwise flagged will always fight werewolves not otherwise flagged - to set up things like the PC being able to lure enemies into an ambush. I'm using CombatLib as the basis for the RPG system I have in place and I understand spawning random mobs etc. The issue I'm running into is in creating a simple set of dictionaries or lists that will interact with each other. I did briefly manage to set one up but it was extremely rudimentary and resulted in every NPC attacking every other NPC in the enemy list!

Any suggestions?


My first thought was that you could give each player a list of factions, and have a dictionary on the game object for which of those attack each other.

Probably not explaining it too well.

Here's some example code, a function to check if enemy X is willing to attack enemy Y. Not sure how to integrate this into CombatLib, as I've not used that. But it should be relatively simple.

<function name="willXattackY" type="boolean" parameters="enemyX, enemyY">
  if (HasAttribute (enemyX, "has_been_attacked_by")) {
    if (ListContains (enemyX.has_been_attacked_by, enemyY)) {
      return (true)
    }
  }
  if (HasAttribute (enemyX, "factions") and HasAttribute (enemyY, "factions")) {
    foreach (factionX, enemyX.factions) {
      foreach (factionY, enemyY.factions + "*") {
        if (DictionaryContains (game.faction_will_attack, factionX+factionY)) {
          result = Eval (ProcessText(DictionaryItem(game.faction_will_attack, factionX+factionY)))
          if (IsBoolean (result)) {
            return (result)
          }
        }
      }
    }
  }
  return (false)
</function>

So then you can have the stringdictionary game.faction_will_attack, with keys like "vampirewerewolf" or similar, and the values "true" or "false". You could also allow more complex rivalries, such as the key "werewolf*" and value "{either IsFullMoon():true:null}" to indicate that werewolves will attack everyone during a full moon.
The value can be "true" (attack), false (don't attack), or null (check other values, so that each enemy can be a member of more than one faction).

I also allowed for an objectlist attribute has_been_attacked_by, because I think in most circumstances an enemy will strike back at someone who's previously attacked them regardless of their normal preferences.


(filler for getting my edited post, updated/posted)
(again, filler for getting my edited post, updated/posted)


Pixie's (opposing spells / spell damage types, handling) spell library can be used for factions as well (though it's basic form is limited for faction usage):

mrangel can probably help/explain/code this for you faster than I can, lol

(just have mrangel look at this post, and I think he'll understand what I'm talking about)

(mrangel also crafted code that is scripting that will find the opposite value that you can use instead of manually writing/adding it to a string dictionary)

Opposing Faction String Dictionary:

item1:

key (input): vampire
value (output): werewolf

item2:

key (input): werewolf
value (output): vampire

item3:

key (input): elf
value (output): dwarf

item4:

key (input): dwarf
value (output): elf

// ------------------------------------------------

// example:

faction_stringlist_variable ("vampire;werewolf;elf;dwarf", ";")

npc1.faction_string_attribute = StringListItem (faction_stringlist_variable, GetRandomInt (0, ListCount (faction_stringlist_variable) - 1))

npc2.faction_string_attribute = StringListItem (faction_stringlist_variable, GetRandomInt (0, ListCount (faction_stringlist_variable) - 1))

if (npc1.faction_string_attribute = npc2.faction_string_attribute) {
  msg ("Being members of the same faction, they're cordial to each other")
} else if (npc1.faction_string_attribute = StringDictionaryItem (OPPOSING_FACTION_STRINGDICTIONARY, npc2.faction_string_attribute)) {
  msg ("Being members of their opposing factions, they immediately attack each other!")
} else {
  msg ("being members of neither the same faction nor their opposing factions, they maintain a cautious neutral state with each other")
}

as can see though, as this is in its basic form, you can only have these 3 states (same, opposite, neither same nor opposite ), but I'm sure it can be modified to handle more complexity, without too much work (or mrangel's code in his post above does this already and especially if more/most efficiently, and thus you can just use his code, in that case, lol)


further mechanics if curious/interested, is have a table of relationships amongst the factions, and use those as modifiers multiplied/divided/added/subtracted/etc-arithmetic-operations-equations-formulas to your 'personality' Integer Attribute, to give you a base 'disposition' integer attribute for that specific faction member and do whatever other stuff that can be built upon this...

aka, see TES:Morrowind mechanics:

http://en.uesp.net/wiki/Morrowind:Factions (scroll down to near bottom to see the table)

http://en.uesp.net/wiki/Morrowind:Personality

http://en.uesp.net/wiki/Morrowind:Disposition

http://en.uesp.net/wiki/Morrowind:Speechcraft

http://en.uesp.net/wiki/Morrowind:Commerce

http://en.uesp.net/wiki/Morrowind:Mercantile


P.S.

@ ScriptingIsHard:

here's some links (if you're still new to coding/scripting) that can help you learn to code/script better with quest and learn quest a bit more too:

http://textadventures.co.uk/forum/general/topic/ljjm32av4e2t9ot49k478g/help#710be61e-eae1-4af1-8363-520cc718ba1c

(especially see the 'attributes and if script usage guide' link and the 'list and dictionary attribute guide' link)


Looking at CombatLib now, you'd probably want to modify the monster_attack type, changing its selecttarget script to something like:

    <selecttarget type="script">
      // Is the current target dead? If so reset
      if (not this.target = null) {
        if (this.target.dead) {
          this.target = null
        }
      }
      // Is no target set? If so, find one.
      if (this.target = null) {
        allmonsters = ListCombine (game.friends, game.attackers)
        targets = NewObjectList()
        foreach (monster, allmonsters) {
          if (monster = this) {
            monster = game.pov
          }
          if (Contains (this.parent, monster)) {
            if (willXattackY (this, monster)) {
              list add (targets, monster)
            }
          }
        }
        this.target = PickOneObject (targets)
      }
    </selecttarget>

Actually, I'm wondering if it might be better to have a kind of 'anger score' for each pair of enemies. So a vampire might attack a werewolf if there's one in the room, but dwarves really hate elves, and will attack one as soon as it walks into the room, even ignoring the guy they were previously fighting. Rather than just true or false, you'd have a numeric value. Factions who hate each other more would have a greater chance of attacking each other (but not guaranteed)/


I believe you need to give every object an attribute, something like:

orc.alliance = "Enemy"
Todd.alliance = "NPC"
or
orc.faction = "Enemy"
Todd.faction = "NPC"

I can't remember exactly though.


Wow, you guys work fast! I'll sit down and try and wrap my head around the underlying logic of the code before I get to implementing it (it's why my reliance on Pix's libraries is a mixed bag - on the one hand, it's great not to have to understand it and reap the rewards, but on the other, taking the time to learn to do it myself would be much more useful in the long run) and then tweak it as needed. The idea about variable hate scores is super neat, especially since I'm working on a game with a complex orcish clan system where it'd be a neat way to implement tensions. If I can figure out a way to make it refer to a clan object variable - I think I can just about wrangle that as is - then I can have it be fairly dynamic without needing to call and update every clan orc every time the PC does something to change the balance of tension.


You can give NPCs a script called "selecttarget" (or modify the existing one) to set the "target" attribute of the NPC. For a werewolf it could check if "target" is already set, and if not, set it to a randomly chosen vampire.

ETA: I see MrAngel has already said that.


Off the top of my head, this is how I'd do factions with some kind of probability balance:

<function name="GetAttackChance" type="int" parameters="attacker, target">
  score = 0
  if (HasAttribute (attacker, "attackedby")) {
    foreach (att, attacker.attackedby) {
      if (att = target) {
        score = score + 10
      }
    }
  }
  if (HasAttribute (attacker, "factions") and HasAttribute (target, "factions")) {
    foreach (i, attacker.factions) {
      foreach (j, target.factions + "*") {
        if (DictionaryContains (game.faction_aggression, i+"/"+j)) {
          result = Eval (ProcessText(DictionaryItem(game.faction_aggression, i+"/"+j)))
          if (TypeOf (result) = "int") {
            score = score + result
          }
        }
      }
    }
  }
  return (score)
</function>

and the selecttarget script:

<selecttarget type="script">
  allmonsters = ListCombine (game.friends, game.attackers)
  targets = NewObjectList()
  foreach (monster, allmonsters) {
    if (Contains (this.parent, monster) and not monster = this) {
      score = GetAttackChance (this, monster)
      if (monster = this.target) {
        // increase chance the monster will keep attacking the same target
        score = GetRandomInt (score, score * 2)
      }
      if (score > 0) {
        // if the score is greater than zero, add to the list of targets to choose from
        // higher score gets added to the list multiple times, so has a higher chance of getting chosen.
        for (i, 0, score) {
          list add (targets, monster)
        }
      }
    }
  }
  this.target = PickOneObject (targets)
  // add ourselves to the target's "attackedby" list, making them more likely to attack back on their turn
  if (not this.target = null) {
    if (not HasAttribute (this.target, "attackedby")) {
      this.target.attackedby = NewObjectList()
    }
    list add (this.target.attackedby, this)
  }
</selecttarget>

Then you configure that by giving each enemy type a set of factions. This is a stringlist.
eg: <factions type="simplestringlist">red team;zombie;undead</factions>

And the game object has a string dictionary.
For example: <attr name="faction_aggression" type="simplestringdictionary">vampire/werewolf=4;red team/blue team=9;blue team/red team=9;priest/vampire=5;berserker/*=99;pacifist/*=-16</attr>

(The berserker has +99 change to attack everybody, which means he pretty much picks a target at random each turn; and the 'pacifist' faction means that a monster probably won't attack at all unless somebody attacks him first)


I've implemented MrAngel's suggestions, and they work great with the exception that every NPC seems to decide that suicide is the way to go when combat starts. I suspect I've managed to execute it wrongly somehow, since my test is two generic enemies with opposing factions. When combat starts, they start attacking themselves, but when one dies the other stops trying to commit suicide for lack of a target. I'm not sure where the issue's stemming from, but I think it should be reproducible by anyone running CombatLib and this code.


Hmm... I can't see how it's managing that.
Could you share the game so I can take a look and see where it's not going as I expected?


Sure thing - I'm using an extremely basic testing game setup (quite literally just combatlib and your scripts and functions) for this so I can actually pop the entire code (except the combatlib .xmls) in here, if that'd work for you, or upload it somewhere if that's a better option. I've also noticed that they don't die unless I've struck the killing blow, so something in the mix isn't communicating damage correctly either.


If anyone wants to take a shot, I'm throwing it on in here just as rawcode. The only thing missing is pix's combatlib xmls. As you can see, it's about as basic a testing setup as they come so it shouldn't be interference from any other variables.

<asl version="550">
  <include ref="English.aslx" />
  <include ref="Core.aslx" />
  <include ref="CombatLib.aslx" />
  <game name="factionalcombattestbed">
    <gameid>60ec50a9-86ea-44fa-b2b8-74f02d56c7d1</gameid>
    <version>1.0</version>
    <firstpublished>2018</firstpublished>
    <faction_aggression type="stringdictionary">
      <item>
        <key>vampire/werewolf</key>
        <value>4</value>
      </item>
      <item>
        <key>red team/blue team</key>
        <value>9</value>
      </item>
      <item>
        <key>blue team/red team</key>
        <value>9</value>
      </item>
      <item>
        <key>priest/vampire</key>
        <value>5</value>
      </item>
      <item>
        <key>berserker/*</key>
        <value>99</value>
      </item>
      <item>
        <key>pacifist/*</key>
        <value>-16</value>
      </item>
    </faction_aggression>
    <start type="script">
      CombatInitialise
    </start>
  </game>
  <object name="room">
    <inherit name="editor_room" />
    <object name="player">
      <inherit name="editor_object" />
      <inherit name="editor_player" />
    </object>
    <object name="mob1">
      <inherit name="editor_object" />
      <inherit name="monster" />
      <factions type="stringlist">
        <value>red team</value>
      </factions>
      <monstertype>Goblinoid</monstertype>
    </object>
    <object name="mob2">
      <inherit name="editor_object" />
      <inherit name="monster" />
      <monstertype>Human</monstertype>
      <factions type="stringlist">
        <value>blue team</value>
      </factions>
    </object>
  </object>
  <function name="willXattackY" parameters="enemyX, enemyY" type="boolean">
    if (HasAttribute (enemyX, "has_been_attacked_by")) {
      if (ListContains (enemyX.has_been_attacked_by, enemyY)) {
        return (true)
      }
    }
    if (HasAttribute (enemyX, "factions") and HasAttribute (enemyY, "factions")) {
      foreach (factionX, enemyX.factions) {
        foreach (factionY, enemyY.factions + "*") {
          if (DictionaryContains (game.faction_will_attack, factionX+factionY)) {
            result = Eval (ProcessText(DictionaryItem(game.faction_will_attack, factionX+factionY)))
            if (IsBoolean (result)) {
              return (result)
            }
          }
        }
      }
    }
    return (false)
  </function>
  <function name="GetAttackChance" parameters="attacker, target" type="int">
    score = 0
    if (HasAttribute (attacker, "attackedby")) {
      foreach (att, attacker.attackedby) {
        if (att = target) {
          score = score + 10
        }
      }
    }
    if (HasAttribute (attacker, "factions") and HasAttribute (target, "factions")) {
      foreach (i, attacker.factions) {
        foreach (j, target.factions + "*") {
          if (DictionaryContains (game.faction_aggression, i+"/"+j)) {
            result = Eval (ProcessText(DictionaryItem(game.faction_aggression, i+"/"+j)))
            if (TypeOf (result) = "int") {
              score = score + result
            }
          }
        }
      }
    }
    return (score)
  </function>
</asl>

Hmm ... I wonder if I messed up using "monster" as a variable name when it's also a type name. Not sure if that could be a problem.

I'm just looking at the code here; I can't test it out because I don't have a Windows machine.
I'd say the next step in debugging would be to find out whether the problem is in GetAttackChance or selecttarget.
Before the line return (score), try adding a debug statement: msg ("Chance of "+attacker.name+" attacking "+target.name+": "+score).

That should come up with:

Chance of mob1 attacking player: 0
Chance of mob1 attacking mob2: 9
Chance of mob2 attacking player: 0
Chance of mob2 attacking mob1: 9

If it returns something else, then the results should give a clue where the problem lies.
Hoping Pixie/KV/someone can chip in to point out what I've missed, because I've looked over the code several times and I can't spot it.


Yep, we're getting what we're after there once I initiate combat by taking a swing at both mobs -
Chance of mob1 attacking player: 0
Chance of mob1 attacking mob2: 9


Hmm ... the code to add to the attackedby list should probably be moved to the actual attack function; because if you've just hit a monster, it should be more likely to go for you than its traditional rival. But that's a secondary concern if they're misbehaving.

My next thought would be to check that the selecttarget script is doing the right thing.
So where we have the line this.target = PickOneObject (targets), change that to:

  msg (this.name+" picking a target. Previous target was: "+this.target)
  msg ("Possible targets: "+FormatList(targets, ", ", ", ", "none"))
  this.target = PickOneObject (targets)
  msg ("Chose: "+this.target)

foreach (factionY, enemyY.factions + "*") {

I don't understand what this bit is doing: + "*"


Oh, I think I see the problem.

I think my code is fine.
The attackplayer script in CombatLib has a bug.

It generates a string like "Mob1 swings at you and hits, doing 9 damage."

Then if the target isn't the player, it does a string search-and-replace to change "you" to the name of the attacking monster. So it's actually doing damage to mob2, it's just the message that seems to be wrong.

In Monsters.xml, look for the line:

            s = Replace(s, " you", " " + GetDisplayAlias (this))

I think that should be:

            s = Replace(s, " you", " " + GetDisplayAlias (this.target))

@KV

I don't understand what this bit is doing: + "*"

Adds the wildcard faction "*" to the list. So that you can have faction animosity like "pacifist/*" or "berserker/*"; gives a faction an increase/decrease to their chance to attack against all targets.
I think that works, but it's off the top of my head so maybe I got the syntax wrong.

If a red team;vampire is in a room with a blue team;werewolf, its chance of attacking that target will be the sum of the values for red team/blue team, red team/werewolf, red team/*, vampire/blue team, vampire/werewolf, and vampire/*.


(note that this script will not work for the monsterarcher type, which selects a target and then attacks the player regardless of which target it picked)


I've also noticed that they don't die unless I've struck the killing blow, so something in the mix isn't communicating damage correctly either.

Yeah; it only checks if a monster is dead when the player attacks it.

In the attackplayer script for the monster_attack type, you could add at the end:

  if (this.target.hitpoints <= 0) {
    do (this.target, "makedead")
  }

I think it would make more sense for this to be done in the monster's changedhitpoints script; but as the monster's hit points are changed before generating the attack message, that would result in the messages appearing out of order.


I'll take a look in myself once I implement those fixes and try to work out where in the scripting the mobs aren't dying until struck a deathblow by the player, too, and let people know what I find. Debugging stuff like this is a good way to wrap my head around the surrounding code. They're definitely getting damaged, but the death script isn't triggering, so I'm going to guess it's checking independently for either a reference to the player or the limited ally set-up already in combatlib.

EDIT:
Damn you you helpful monster!


The good news is the attack names are fixed. The bad news is that I can't get that piece of code for triggering death scripts in without it refusing to run due to nested XML, which I'm pretty sure is down to my inexperience. This is the case regardless of where in the script I try and insert it.


Ah, because it thinks the <= sign is an XML element.

You could change it to if (not this.target.hitpoints > 0), or if (0 >= this.target.hitpoints). Or put <![CDATA[ at the start of the script and ]]> after, which tells it to ignore any other < within that block.


That's sorted it, so I think I can proceed now and dig myself into all kinds of traps of boolean states and string lists. Thanks, MrAngel!


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

Support

Forums