BLSE0.6

Python mods

NPC AI

On this page 17 sections

Relationships, enemies, schedules, moving and following, perception, and events when an NPC's schedule or state changes. Everything goes through the game's own code: the functions its maps and its aiscripted_schedule entity use. npc is an NPC's index or handle; other entities likewise, -1 for none. While a level runs; functions return 1 or 0 unless noted.

This page is for mods that tell NPCs what to do: make a bodyguard who follows the player and fights for them, turn a group of thugs hostile, make a pedestrian walk to a spot, make guards see further, or react when an NPC starts fleeing.

A first example: type bodyguard while looking at an NPC, and that NPC follows you and attacks whoever you look at next with sic.

Python
import blse

guard = [None]    # the bodyguard's handle (a list, so the functions below can change it)

def bodyguard(name, args):
    target = blse.look_target()           # (classname, name, handle) or None
    if target:
        guard[0] = target[2]
        blse.follow_npc(guard[0], 1)      # follow the player
        blse.console_print('Following you.\n')

def sic(name, args):
    target = blse.look_target()
    if guard[0] and target:
        blse.set_relationship(guard[0], target[2], 'hate', 99)
        blse.set_enemy(guard[0], target[2])

blse.add_command('bodyguard', bodyguard, 'bodyguard: the NPC you look at follows you')
blse.add_command('sic', sic, 'sic: your bodyguard attacks what you look at')

Read this first

  • Only while a level runs (after blse.level_init, before blse.level_shutdown), and only on the game version BLSE recognizes. Otherwise the functions return 0 or None.
  • Only NPCs. npc must be a non-player character (an entity of the game's NPC kind). The player, props or items give 0 / None.
  • Busy NPCs are left alone. An NPC that is dead, in a scripted scene (a cut scene, an animation a map plays) or in a conversation cannot be given a schedule or sent walking: those functions return 0.
  • What is saved: relationships and senses you set are kept in saves. Following is not (see follow_npc). Enemies and schedules are the NPC's current state and change on their own.
  • Who fights. Hate makes an NPC fight only if its kind can: civilians (pedestrians, bluebloods, prostitutes, patients) have no attack and take cover from an enemy instead. Thugs, gangsters, guards and cops attack.

Words used on this page#

  • NPC: a non-player character: anyone in the world except the player.
  • Entity, index, handle: see Words used on the world page. Here: npc and the other entity arguments take an index or a handle (1 is the player); -1 means "none" where a function allows it. Functions that return an entity return its handle.
  • Relationship / disposition: how one NPC feels about one other entity: 'hate' (attacks it, or takes cover if it cannot fight), 'fear' (runs from it), 'like' (an ally) or 'neutral' (ignores it). The game also has relationships by class (all cops hate criminals, for example); yours are added on top, for one entity.
  • Priority: when an NPC hates several entities, it picks the one with the highest priority as its enemy.
  • Enemy: the one entity an NPC is fighting (or fleeing from) right now.
  • Schedule: a small plan an NPC carries out: "idle and patrol", "flee", "take cover", "attack from range". The game's AI picks one after another. Names look like SCHED_TROIKA_FLEE; each kind of NPC knows its own set.
  • State: the NPC's overall mood: 'idle', 'alert', 'combat', 'dead', ... (list below). Schedules are chosen to fit the state.
  • Condition: one fact the AI has noticed this moment, as a number: "can see the enemy", "no ammunition", "too far to shoot". The game decides what to do from the conditions.
  • Input: a command sent to an entity, as maps do it with their I/O ("input/output") system: Kill, Open, ChangeSchedule.
  • Units: distances in the game's units (a unit is roughly an inch).

Functions#

Function
blse.fire_input(entity, input[, value[, activator]]) Sends an input to any entity, also an unnamed one, as the map's I/O does: fire_input(npc, 'Kill'), fire_input(door, 'Open'). Input rules, filters and blse.entity_input see it. activator: an entity, or -1 (the default) for the player. 1 if the entity handled it
blse.set_relationship(npc, target, disposition[, priority]) How the NPC feels about one entity: 'hate', 'fear', 'like' or 'neutral'; priority (default 0) decides between enemies, higher first. Saved with the game
blse.relationship(npc, target) (disposition, priority), e.g. ('neutral', 0); also the game's own (by class), or None
blse.set_enemy(npc, target) Makes target (a character: the player or an NPC) the NPC's enemy and tells it where the target is. -1 clears the enemy and calms the NPC: back to idle, its current schedule (taking cover, chasing) ended, so it talks again
blse.npc_enemy(npc) The handle of its enemy, or None
blse.start_schedule(npc, schedule) Runs one of the NPC's schedules once ('SCHED_TROIKA_FLEE' or 'TROIKA_FLEE'), then its own AI carries on. It starts when the NPC's running schedule ends, as the map's ChangeSchedule input does: a walking NPC first finishes its step of the path (up to about 1.5 s). 0 if its kind has no such schedule, or it is dead, in a scripted scene or a conversation
blse.npc_schedule(npc) The schedule it runs now ('SCHED_TROIKA_IDLE_PATROL'; '' if none), or None
blse.npc_state(npc) 'idle', 'alert', 'combat', 'fleeing', 'cowering', 'hunting', 'dialog', 'script', 'dead', ... (the game's own names), or None
blse.move_npc(npc, goal[, run]) Sends the NPC to where an entity is now, walking or running (run 1)
blse.move_npc_to(npc, x, y, z[, run]) Sends the NPC to a point
blse.follow_npc(npc, leader) The NPC follows the leader (the player, another NPC, any entity): when it is more than 160 units away it is sent after it, running beyond 420. -1 stops. It also stops when the NPC dies, the level ends, or it is moved with move_npc; fights (also while it looks for an enemy it still hates), scripted scenes and conversations pause it. Not kept in saves: a mod that wants a lasting follower stores that (entity_set) and calls it again after blse.after_load and level_init
blse.npc_leader(npc) The handle of the entity it follows, or None
blse.npc_conditions(npc) (b0.5.48) The AI conditions set on the NPC now, as numbers (below): why it does or does not shoot. None if not an NPC
blse.npc_weapon(npc) (b0.5.48) (classname, clip): the weapon in its hands and the rounds in its clip, or None
blse.npc_senses(npc) {'sight': 3072.0, 'fov': 156.9, 'vision': 440.0, 'hearing': 1.0, 'vision_set': 0, 'hearing_set': 0, 'inspection': 3}, or None
blse.set_npc_sense(npc, sense, value) 'sight': how far it sees, in units. 'fov': its field of view in degrees (1-360). 'vision', 'hearing': the multipliers of the game's stealth system (how quickly it notices a sneaking player); a negative value gives the game's own back (from the NPC's stealth level; a value the map set is not restored: read it with npc_senses first). Kept in saves
blse.npc_schedule (event) npc, cls, name, old, new = data.split('\0'): an NPC started a schedule (every change, also the game's own; frequent, so only listen while needed)
blse.npc_state (event) npc, cls, name, old, new = data.split('\0'): an NPC's state changed (checked ten times a second while anyone listens)

Where do you get an NPC's handle? From blse.find_entities (the NPCs near a point), blse.look_target() (the one under the crosshair), blse.spawn_entity (one you created), the blse.entity_spawned event, or the events on this page.

blse.fire_input#

Python
blse.fire_input(entity, input[, value[, activator]])   # -> 1 or 0

Sends an input (a command) to any entity, exactly as a map's I/O system does, also to an entity without a name (maps can only address named ones).

When to use it: kill an NPC ('Kill'), open a door ('Open'), start a map's relay ('Trigger'), send an NPC another schedule the map's way ('ChangeSchedule').

  • entity: an index or handle. Not 0 (the world).
  • input (text): the input's name, 1 to 127 characters, e.g. 'Kill', 'Open', 'SetHealth'.
  • value (text, optional): the input's parameter, up to 1023 characters, e.g. '50' for SetHealth. Default '' (none).
  • activator (optional): the entity that "caused" it (maps and scripts can check it). -1, the default, means the player.

Returns 1 if the entity handled the input, 0 if not (it does not know that input, no level, unknown entity or activator). Too long input or value raises ValueError.

Input rules, filters and the blse.entity_input event see inputs sent this way like any other.

Python
blse.fire_input(npc, 'Kill')
blse.fire_input(door, 'Open')

Limit: entities may keep the text you send, so BLSE keeps every different input and value text for the rest of the game session, at most 20000. After that, new texts are refused (0, logged once). Sending the same few texts again and again costs nothing.

blse.set_relationship and blse.relationship#

Python
blse.set_relationship(npc, target, disposition[, priority])   # -> 1 or 0
blse.relationship(npc, target)                                # -> (disposition, priority), or None

Set or read how an NPC feels about one other entity.

When to use them: turn a gang hostile to the player after a quest choice; make a companion like the player; make cops ignore one particular NPC.

  • npc: the NPC whose feelings change.
  • target: the entity it feels this way about (the player, another NPC, any entity). Not the NPC itself.
  • disposition (text): 'hate', 'fear', 'like' or 'neutral' (the game's names 'D_HT', 'D_FR', 'D_LI', 'D_NU' work too; any case). Another word raises ValueError.
  • priority (whole number, optional, default 0): -1000 to 1000 (outside raises ValueError). Among hated entities, the NPC fights the one with the highest priority first.

set_relationship returns 1 if set, 0 if not (not an NPC, unknown target, no level). The relationship is saved with the game.

relationship returns what the NPC feels now, as (disposition, priority), e.g. ('neutral', 0). This includes the game's own relationships by class, not only ones you set. None if the NPC or target does not exist (or no level).

Python
blse.set_relationship(thug, 1, 'hate', 50)        # this thug now hates the player
blse.log(str(blse.relationship(thug, 1)))     # logs ('hate', 50)

Enemies stay while hated. The game drops an enemy the NPC does not hate; to start a fight, set_relationship(npc, target, 'hate', 99) first, then set_enemy. To end one, make it neutral again and set_enemy(npc, -1).

blse.set_enemy and blse.npc_enemy#

Python
blse.set_enemy(npc, target)   # -> 1 or 0
blse.npc_enemy(npc)           # -> handle, or None

set_enemy makes target the NPC's current enemy and tells it where the target is, so it reacts at once instead of waiting to notice it.

  • target: a character (the player or an NPC), not the NPC itself. Props and items are refused.
  • target = -1 clears the enemy and calms the NPC: no enemy, its state goes back to idle (from combat or alert), and its current schedule (taking cover, chasing) is ended so it picks a peaceful one. Until then an NPC stays "in combat" and will not talk.

Returns 1 if done, 0 if not (not an NPC, a dead NPC, a target that is not a character, no level).

npc_enemy returns the handle of the NPC's current enemy, or None if it has none (or it is not an NPC).

Python
# Start a fight between two NPCs:
blse.set_relationship(a, b, 'hate', 99)
blse.set_enemy(a, b)

# End it:
blse.set_relationship(a, b, 'neutral')
blse.set_enemy(a, -1)

blse.start_schedule#

Python
blse.start_schedule(npc, schedule)   # -> 1 or 0

Makes an NPC carry out one schedule once; afterwards its own AI carries on as usual.

When to use it: make a pedestrian flee ('SCHED_TROIKA_FLEE'), cower ('SCHED_TROIKA_COWER'), or make a gunman shoot at its enemy regardless of what it thinks ('SCHED_TROIKA_FORCED_RANGE_ATTACK1').

  • schedule (text): the schedule's name, with or without SCHED_ ('SCHED_TROIKA_FLEE' or 'TROIKA_FLEE'), 1 to 120 characters (an empty or longer name raises ValueError).

When it starts: like the map's ChangeSchedule input, it starts when the NPC's running schedule ends. A walking NPC first finishes its step of the path (up to about 1.5 seconds).

Returns 1 if the schedule was given, 0 if the NPC's kind has no such schedule, or the NPC is dead, in a scripted scene or a conversation (or not an NPC, no level).

Schedule names are the game's (SCHED_TROIKA_COWER, SCHED_TROIKA_FLEE, SCHED_VCOP_WANDER_PATROL, ...). Each kind of NPC knows its own set; the general schedules every NPC has (SCHED_WAIT_FOR_SCRIPT, for example) work too (b0.5.49). npc_schedule and the blse.npc_schedule event show which ones an NPC uses.

Python
if not blse.start_schedule(npc, 'SCHED_TROIKA_FLEE'):
    blse.log('this NPC cannot flee right now')

blse.npc_schedule#

Python
blse.npc_schedule(npc)   # -> name, or None

The name of the schedule the NPC runs now, e.g. 'SCHED_TROIKA_IDLE_PATROL'; '' if it runs none. None if it is not an NPC (or no level).

When to use it: find out which schedule names an NPC kind uses, before you call start_schedule; check whether your schedule is running.

blse.npc_state#

Python
blse.npc_state(npc)   # -> state name, or None

The NPC's current state, as the game names it: 'none', 'idle', 'alert', 'combat', 'script' (in a scripted scene), 'playdead', 'prone', 'dead', 'fleeing', 'retreating', 'cowering', 'hunting' (looking for an enemy it lost), 'dialog' (in a conversation), 'oblivious', 'criminal_suspicion', or 'unknown' for any other value. None if it is not an NPC (or no level).

Python
if blse.npc_state(npc) == 'dead':
    blse.log('too late')

blse.move_npc and blse.move_npc_to#

Python
blse.move_npc(npc, goal[, run])           # -> 1 or 0
blse.move_npc_to(npc, x, y, z[, run])     # -> 1 or 0

Send an NPC walking (or running) somewhere: to where an entity is now (move_npc), or to a point (move_npc_to). The NPC finds its own path, as with the game's aiscripted_schedule.

When to use them: a contact walks to the bar; a guard runs to the alarm; a quest NPC meets the player at a door.

  • goal: an entity (index or handle), not the NPC itself. The NPC walks to the spot where the goal is at the moment of the call; it does not chase it if it moves (use follow_npc for that).
  • x, y, z: the point, each between -65536 and 65536.
  • run (optional, default 0): 1 runs, 0 walks. Some kinds of NPC crawl instead, whatever you choose (as with aiscripted_schedule).

Returns 1 if the NPC started moving, 0 if not: not an NPC, dead, in a scripted scene or a conversation, the game could not start the walk (no path, for example), or no level. Both also end follow_npc for this NPC.

Python
blse.move_npc_to(npc, 512, -128, 0, 1)    # run there

How it works: the NPC walks to a marker BLSE places (an info_target named blse_goal, reused, never removed), because the game keeps the goal as a plain pointer and reads it again while walking: a goal entity removed meanwhile would crash it. Do not remove these markers.

blse.follow_npc and blse.npc_leader#

Python
blse.follow_npc(npc, leader)   # -> 1 or 0
blse.npc_leader(npc)           # -> handle, or None

follow_npc makes an NPC follow a leader: the player, another NPC, or any entity. Twice a second BLSE checks the distance: when the NPC is more than 160 units away it is sent after the leader, running when more than 420 units away; it is sent again when the leader has moved about 96 units from where the NPC was sent.

When to use it: a bodyguard, a companion of your own, a prisoner being escorted.

  • leader: an index or handle, not the NPC itself. -1 stops following.

Returns 1 if set (or stopped), 0 if not (not an NPC, a dead NPC, unknown leader, no level).

Following stops when the NPC dies, the leader no longer exists, the level ends, or the NPC is moved with move_npc / move_npc_to. Fights (also while it looks for an enemy it still hates), scripted scenes and conversations pause it; it resumes afterwards.

Not kept in saves: a mod that wants a lasting follower stores that (for example with entity_set) and calls follow_npc again after blse.after_load and level_init.

npc_leader returns the handle of the entity the NPC follows, or None.

The game's own follower system is not used: for NPCs in a squad (cops) it ends the game ("Followers can not be in squads"), and pedestrians ignore it.

Python
blse.follow_npc(npc, 1)       # follow the player
blse.follow_npc(npc, -1)      # stop

blse.npc_conditions#

Python
blse.npc_conditions(npc)   # -> list of numbers, or None

(b0.5.48) The AI conditions the NPC has right now, as a list of numbers (ids 0 to 255 are checked). The main use: finding out why an NPC does or does not shoot. None if it is not an NPC (or no level).

Why an NPC does not shoot (b0.5.48, from the game's code): it fires only while it has condition 79 (can fire). Others it can have instead: 64 no ammunition in the clip, 8 or 95 too close (under 100 units, or under the weapon's minimum), 96 too far, 97 not facing the enemy, 99 a character it does not hate is in the line of fire (the player, for a companion behind the player; 46 when that lasts over 2.5 s), 102 sight blocked, 71 it lost its enemy. start_schedule(npc, 'SCHED_TROIKA_FORCED_RANGE_ATTACK1') makes it face its enemy and fire regardless, until the enemy dies. Other attack schedules: SCHED_TROIKA_RANGE_ATTACK1, SCHED_TROIKA_TAKE_COVER_PEEK_OUT_FIRE, SCHED_TROIKA_STEP_BACK_RANGE_ATTACK1, SCHED_TROIKA_COMBAT_DODGE.

Python
c = blse.npc_conditions(npc)
if c is not None and 79 not in c:
    if 99 in c:
        blse.log('someone is in its line of fire')

blse.npc_weapon#

Python
blse.npc_weapon(npc)   # -> (classname, clip), or None

(b0.5.48) The weapon in the NPC's hands and how many rounds are in its clip, e.g. ('item_w_colt_anaconda', 6). None if it holds nothing (or it is not an NPC, no level). A clip of 0 or less means "no ammunition" to the game.

blse.npc_senses and blse.set_npc_sense#

Python
blse.npc_senses(npc)                  # -> dict, or None
blse.set_npc_sense(npc, sense, value) # -> 1 or 0

Read or change how well an NPC perceives the world.

When to use them: guards that see further at night; a blind NPC; a stealth mod that makes some NPCs harder to sneak past.

npc_senses returns a dict such as {'sight': 3072.0, 'fov': 156.9, 'vision': 440.0, 'hearing': 1.0, 'vision_set': 0, 'hearing_set': 0, 'inspection': 3}, or None if it is not an NPC (or no level):

  • 'sight': how far it can see, in units.
  • 'fov': its field of view, in degrees (how wide it looks).
  • 'vision', 'hearing': the multipliers of the game's stealth system, how quickly it notices a sneaking player, as in effect now.
  • 'vision_set', 'hearing_set': 1 if that multiplier was set (by the map or a mod), 0 if it comes from the NPC's stealth level.
  • 'inspection': the NPC's stealth level, as the game stores it.

set_npc_sense changes one of them:

  • sense: 'sight', 'fov', 'vision' or 'hearing' (any case; another word raises ValueError).
  • value:
    • 'sight': 0 to 100000 units.
    • 'fov': 1 to 360 degrees.
    • 'vision', 'hearing': a multiplier up to 100; a negative value gives the game's own back (from the NPC's stealth level; a value the map set is not restored: read it with npc_senses first).

Returns 1 if set, 0 if not (a value out of range, not an NPC, no level). Kept in saves.

Python
blse.set_npc_sense(guard, 'sight', 4096)
blse.set_npc_sense(guard, 'fov', 200)

Events#

Two events tell you when NPCs change what they do. Both have the same five parts:

  1. npc: the NPC's handle, as text (int(npc) gives the number);
  2. cls: its classname;
  3. name: its targetname ('' if none);
  4. old: the schedule or state before;
  5. new: the schedule or state now.

The blse.npc_schedule event#

Sent every time any NPC starts a schedule: the game's own choices too, not only yours. This is frequent (many per second in a busy level), so subscribe only while you need it and keep the handler short. Delivered to Python a moment later in the same frame (the NPC is in the middle of thinking when it happens).

Python
def on_schedule(topic, data):
    npc, cls, name, old, new = data.split('\0')
    if new == 'SCHED_TROIKA_FLEE':
        blse.log('%s (%s) runs away' % (name, cls))

token = blse.subscribe('blse.npc_schedule', on_schedule)
# ... when done:
blse.unsubscribe(token)

The blse.npc_state event#

Sent when an NPC's state changes (old and new are state names as npc_state gives them). States are compared ten times a second while anyone listens, so a change is reported up to a tenth of a second late. The first comparison after you subscribe only records the states; NPCs that appear later are recorded without an event.

Python
def on_state(topic, data):
    npc, cls, name, old, new = data.split('\0')
    if new == 'combat' and int(npc) == guard[0]:    # guard: from the first example
        blse.show_text('Your bodyguard is fighting!')

blse.subscribe('blse.npc_state', on_state)

Notes#

  • Who fights. Hate makes an NPC fight only if its kind can: civilians (pedestrians, bluebloods, prostitutes, patients) have no attack and take cover from an enemy instead. Thugs, gangsters, guards and cops attack.
  • Why an NPC does not shoot: see npc_conditions.
  • Enemies stay while hated: see set_relationship.
  • Schedule names: see start_schedule.
  • Moves go to markers named blse_goal: see move_npc.
  • The game's own follower system is not used: see follow_npc.
  • fire_input keeps every different input text for the rest of the game (an entity may keep it), at most 20000.
Python
def make_bodyguard(npc):
    blse.follow_npc(npc, 1)
    blse.set_npc_sense(npc, 'sight', 4096)

def on_damage_to_player(attacker, npc):
    blse.set_relationship(npc, attacker, 'hate', 99)
    blse.set_enemy(npc, attacker)