Modding Help How exactly do the lua AI files work with each other?

Discussion in 'Starbound Modding' started by Rumble Bot, Mar 13, 2014.

  1. Rumble Bot

    Rumble Bot Void-Bound Voyager

    What functions are currently well... functional?

    What determines what rulesets each NPC type follows?

    For instance we have NPCs with "type" and "baseType"

    Then what confuses me are the scripts and how they're called, especially main.lua

    I've noticed this function available

    Code:
    function shouldAttackOnSight(targetId)
      for _, attackOnSightId in pairs(storage.attackOnSightIds) do
        if targetId == attackOnSightId then return true end
      end
    
      if entity.isValidTarget(targetId) then
        -- Always attack aggressive monsters
        if world.isMonster(targetId, true) then
          return true
        end
    
        -- Attack other npcs on different damage teams (if they are a valid
        -- target, then they have a different damage team)
        if world.isNpc(targetId) then
          return true
        end
      end
    
      return false
    end
    
    I've also noticed things like "friendlyguard" and "cultist" have almost similar values, the different being cultist has
    Code:
    "damageTeam" : 2,
    
    "scriptDelta" : 5,
    whereas friendlyguard doesn't have scriptDelta, and damageTeam is set to 1.

    From what I can see all damageTeam does is change who and what they can shoot.

    Not sure what scriptDelta does.

    All the scripts point to the same lua files.

    Because of this I'm inclined to believe that the main AI for the NPCs is hardcoded elsewhere as there doesn't seem to be anything in these files that point towards Cultists being aggressive NPCs that attack you on sight, as opposed to friendly guards that only attack you if you shoot them.

    Neither of them seem to shoot aggressive monsters even when they're locked onto them, and only retaliate if they're shot leading me to believe that a lot of the functions in the main.lua file don't even function yet. I want to be able to mess with the AI a bit, which is possible in some cases, but some variables seem unreachable at the moment.
     
  2. Healthire

    Healthire Can't Most Program the Least

    The magic is in: entity.isValidTarget(targetId)

    It uses damageTeam to determine what is and isn't a valid target. If you want specifics I can't help you, because I don't know the specifics.
     
  3. Its really WAY too much to type to describe the entire state machine to you, but check statemachine.lua in the scripts folder and that should give you the run down on how most of it works.

    The AI goes off of "States" in which a state is switched based on events such as taking damage, exiting a state and auto-entering a new one, interacting with monsters/npcs, etc. In your code, every time you see "statemachine.pickState()" then you are trying to change to a new state. The monsters have various states attached to them based on the type of mosnters they are. For example, in the smallbiped.monstertype file we see the following:
    Code:
        "scripts" : [
          "/monsters/capturepod.lua",
          "/monsters/ground/groundMonster.lua",
          "/scripts/stateMachine.lua",
          "/scripts/util.lua",
          "/scripts/vec2.lua",
    
          "/monsters/ground/aggressState.lua",
          "/monsters/ground/captiveState.lua",
          "/monsters/ground/knockoutState.lua",
          "/monsters/ground/socializeState.lua",
          "/monsters/ground/wanderState.lua"
        ],
    Therefore we have the following states attached to this type of moster: AggressState (Agressive / Attack on sight), captiveState (capture pod behaviour), knockoutState (Dying IIRC), socializeState ( Gathering and "talking" to other monsters ) and wanderState ( wandering around aimlessly)

    Each of these scripts are .lua files in the /npc/ folder. None of the AI is hard coded so feel free to take a look at what they did and how they work. The magic happens in the state machine, and what controls the various behaviour.
     
    Rumble Bot, lornlynx and The | Suit like this.
  4. lornlynx

    lornlynx Cosmic Narwhal

    good to see some short explanation of the state machine, I had only merely a clue of what it did and how it worked, but this makes me a lot more interested in influencing AI behaviour
     
  5. Rumble Bot

    Rumble Bot Void-Bound Voyager

    Yeah
    This is the stuff that is interesting me; I mean non-monster NPCs attack other non-monster NPCs on sight, and Monsters attack NPCs in general on sight.

    But non-monster NPCs don't react to aggressive monsters unless they are hit by them, even when targeted by them, and I want to figure out what simple value I have to overwrite or change to fix that.

    For instance it'd be far more useful if normal guards attacked aggressive monsters on sight rather than they attack a villager or fellow guard. And for friendly guards, it'd be more logical to come to the owner's aid, as soon as they see that a monster has "aggro'd" itself onto the player, or onto themselves, or any other fellow friendly guard.

    Now what I want to do is take the pet captivestate.lua and take some of the variables from that, maybe create a new function from it, apply it to a friendly guard (create a new kindof guard spawner like "bodyguard" or something) so that it follows its owning player around. I've already made use of one of the existing mods on the forum that overwrites the "attack player on friendly fire" mechanism, to where they just complain bitterly if shot.

    A combination of shooting aggressive monsters on sight, whilst maintaining a solid distance (not too far not too close) to the player, and focusing on monsters aggro'd on the player or the "bodyguard" first and what I just mentioned would probably be a great addition to the game.

    That and it'd be nice for people who have no friends to play the game online with, are feeling a tad lonely and would like an AI who talks to them and looks after them. Eh heh heh.
     
  6. Well.. Lets go through it then. (Be sure to actually READ the code as I have added a few comments to each to better explain whats going on)

    So first off we have npcs\main.lua's main function:
    Code:
    function main()
      noticePlayers()
    
      local dt = entity.dt()
    
      self.state.update(dt)
      self.timers.tick(dt)
    end
    
    Each tick we call noticePlayers(), then update the state machine. By default, if the state machine does not have a state, it will automatically try and enter a new one. This will cause the state machine to loop through its attached states and it will enter the first state it finds that returns a non-nil when the [state].enter() or [state].enterWith(args) is called. This is how we can selectively choose how to enter a state, and when.

    So. For sake of explanation, lets say we try and enter the "meleeattackstate". Its enterWith() function is below: (I added a few comments)
    Code:
    function meleeAttackState.enterWith(args)
      if args.attackTargetId == nil then return nil end -- If we dont have an attack target in the table, return NIL
      if not self.hasMeleeWeapon and not self.hasSheathedMeleeWeapon then return nil, 100 end -- If we dont have a weapon, return NIL
    
      local targetPosition = world.entityPosition(args.attackTargetId)
      if targetPosition == nil then return nil end -- if we cant get the entity's position.... take a guess.
    
      -- Only switch from a ranged weapon to a sheathed melee weapon if in melee range
      if self.hasRangedWeapon and self.hasSheathedMeleeWeapon and not meleeAttackState.inRange(targetPosition) then
        return nil
      end
    
      local attackerLimit = entity.configParameter("attackerLimit", nil)
      if attackerLimit ~= nil then
        if nearbyAttackerCount(args.attackTargetId) >= attackerLimit then
          return nil, entity.configParameter("attackerLimitCooldown", nil)
        end
      end
    
      return {
        targetId = args.attackTargetId,
        targetPosition = targetPosition,
        searchTimer = 0,
        swingTimer = 0,
        swingCooldownTimer = 0,
        backoffDistance = entity.randomizeParameterRange("meleeAttack.backoffDistanceRange")
      }
    end
    So we see that if the arguments table does not have a key called "attackTargetId", then we return nil, and do NOT enter the state. Each of the cases I outlined where we return nil is a requirement to be satisfied to enter the melee attack state: Target, Position, Weapon, Melee weapon. There is also an attack limit where the NPC wont attack monsters when they are outnumbered. If ALLLL of these prerequisites are met, then we return a table to the state machine that holds its current state data:
    Code:
      return {
        targetId = args.attackTargetId,
        targetPosition = targetPosition,
        searchTimer = 0,
        swingTimer = 0,
        swingCooldownTimer = 0,
        backoffDistance = entity.randomizeParameterRange("meleeAttack.backoffDistanceRange")
      }
    This is pretty much the only way of saving data in a state machine as this table is re-used over the course of each call. If you wish to save the state data to the npc itself, you can of course, but I don't recommend it.

    So, if we take a further look at the states for the default NPC, we can see that the only way we can enter the attack state is via the "Guard" state, or when the NPC takes damage:
    Code:
    function damage(args)
      local dead = entity.health() <= 0
      if not dead and not isAttacking() then
        attack(args.sourceId, entity.id())
      else
        sendNotification("attack", { targetId = args.sourceId, sourceId = entity.id(), sourceDamageTeam = entity.damageTeam(), dead = dead })
      end
    end
    Code:
    function attack(targetId, sourceId, allowReAttack)
      if targetId == self.attackTargetId and not allowReAttack then
        return true
      end
    
      if not entity.isValidTarget(targetId) then
        return false
      end
    
      if self.state.pickState({ attackTargetId = targetId, attackSourceId = sourceId }) then -- THIS IS THE MAGIC HAPPENING HERE!
    -- Notice we are passing in the attack target and the source to satisfy the pre reqs of the conditions to enter the state
        self.attackTargetId = targetId
    
        -- Only re-broadcast attacks if this entity was the originator
        if sourceId == entity.id() then
          sendNotification("attack", { targetId = targetId, sourceId = sourceId, sourceDamageTeam = entity.damageTeam() })
        end
    
        -- TODO: add a timer to this table so they don't re-attack on sight forever
        table.insert(storage.attackOnSightIds, targetId)
    
        return true
      else
        return false
      end
    end

    So, to anwser your question: "How to make it so they attack on sight?" Make a state that takes very few pre-reqs and finds its own targets. You can then pretty much copy paste the melee/ranged attack states from there. the only thing you need to change is the conditions that it enters with.

    Hope that helps.

    EDIT: I also missed your question of DeltaScript. All that does is tell it how often to run the script. It is not a perfect timer though so dont use it to make precisely timed scripts. There are 60 ticks in a second, and deltaScript specifies how many ticks there are between calls. a DS of 1 will run 60 times a second (IF possible. The script might take longer to run than 1/60th of a second, which is why I said dont use it as a timer), and a DS of 60 will run once a second.
     
    Last edited: Mar 14, 2014
    lornlynx and Rumble Bot like this.
  7. Rumble Bot

    Rumble Bot Void-Bound Voyager

    Woooow, thanks. Given I'm a tad new to programming but understood everything you said, I'm gonna try it out once I get the time to.

    I wasn't expecting someone to be experienced and nice enough to actually hold me by the hand all the way into understanding it all.
     
    Last edited: Mar 14, 2014
  8. Kawa

    Kawa Tiy's Beard

    * ScriptDelta, Mr. President.
     
  9. Technicalities.... :p
     
    Kawa likes this.

Share This Page