BLSE0.6

Python mods

Companions

On this page 11 sections

An NPC that goes with the player through level changes and fights beside them. BLSE's core does the part a mod cannot: it makes the game carry the NPC to the next map, with its whole state. The behaviour (following, fighting, regrouping) is a mod's: the mod folder examples/mods/companion (its script is blse/scripts/companion.py) is a complete example built only on the functions below and on NPC AI.

(b0.5.48)

Come here when your mod gives the player a follower: a ghoul bodyguard, a hired gun, a character who travels along for a quest. This page covers taking the NPC along to other maps and the tools a mod uses to make it fight beside the player.

A first example: look at an NPC, type recruit in the console, and it follows you from now on, also through doors, taxis and after loading a save.

Python
import blse

def recruit(name, args):
    target = blse.look_target()          # (classname, name, handle) or None
    if target and blse.set_companion(target[2], 1, 'follow'):
        blse.set_relationship(target[2], 1, 'like', 99)   # never hostile to the player
        blse.console_print('%s joins you.\n' % (target[1] or target[0]))

blse.add_command('recruit', recruit, 'recruit: the NPC you look at becomes a companion')

Read this first

  • Only while a level runs (after blse.level_init, before blse.level_shutdown), from Python's normal code (the main thread). Elsewhere set_companion returns 0 and companions / companion_options return None. block_companion_map works at any time.
  • Being a companion is saved. A save keeps who is a companion, on which map and with which options. Blocked maps, damage multipliers and relationships set by your mod are not saved by BLSE: set them again when your script runs and after loads (see the notes in each entry).
  • Turning one of the game's own NPCs into a companion takes it out of its map for good: quests that need it there break. Prefer NPCs your mod spawns (blse.spawn_entity).
  • Use names of your own for companions ('mymod_companion_1'): the game's map scripts look NPCs up by name and may find yours instead of their own.
  • Only living NPCs can become companions; a companion that dies or is removed stops being one (the 'lost' event).

Words used on this page#

  • Entity, index, handle: see the world page. npc is an NPC's index or handle (look_target()[2], or from blse.spawn_entity / blse.find_entities). 1 is the player.
  • NPC: a character the game's AI controls (pedestrians, guards, vampires), not the player.
  • Level, map: a level is one map of the game loaded and running (sm_pawnshop_1). A level change loads another map (through a door or area exit, a taxi, a script). Interiors are usually separate maps.
  • Landmark: an invisible point both maps of a level change share. The game places the player (and everything it carries over) relative to it.
  • Transition data: the list of things the game takes from one map to the next at a level change (normally the player, their items and Discipline effects).
  • Targetname: the name a map or a mod gives an entity ('mymod_companion_1'); many entities have none.
  • Classname: the entity's kind ('npc_VHumanCombatant').
  • Options: a text of words separated by spaces, commas or tabs ('follow range=800').
  • Units: the game's distance unit, as in positions (blse.entity_position).
  • Payload: the text an event hands to your handler: several fields separated by NUL characters (character 0), read with data.split('\0').

Going along through level changes#

Function
blse.set_companion(npc, on[, options]) on 1: the NPC becomes a companion; 0: it is one no longer (it stays where it is). Kept in the save. 1 or 0; ValueError for an unknown option
blse.companions([all_maps]) [(handle, map, class, name, options), ...] for this level's companions; with all_maps 1 also those left on other maps (handle None)
blse.companion_options(npc) Its options ('' for none), or None if it is not a companion
blse.block_companion_map(map[, block]) No companion goes to that map (cutscene maps, scripted scenes); block 0 ends it. Not saved: set it when the script runs
blse.level_change (event) map, landmark = data.split('\0'): a level change was asked for and happens on a later frame: block the map now if no companion may go there (events)
blse.companion (event) what, npc, cls, name, map = data.split('\0'): 'arrived' (it came along from map), 'stayed' (it stayed on map: stay, a blocked map, out of range, dead), 'lost' (it died or was removed and is no companion now). npc is empty when it is not on this level

Options, words in one string:

Word
follow BLSE's follow_npc(npc, player) again on every level and after loads (follow_npc alone is not kept)
stay Stays a companion but does not go along (waiting somewhere). The player finds it there when coming back
range=<units> Goes along only when it is within that distance of the player at the level change. Without it, it comes from anywhere on the map

blse.set_companion#

Python
blse.set_companion(npc, on[, options])   # -> 1 or 0

Makes an NPC a companion (it goes along to the next map at every level change) or ends that. Use it when the player recruits someone, and with on 0 when they part.

  • npc: index or handle of a living NPC (not the player).
  • on (int): 1 makes it a companion, 0 ends it (it stays where it stands, and if it had follow, it stops following).
  • options (str, optional): words separated by spaces, commas or tabs, case ignored, at most 200 characters. None or '' means none.
    • follow: it follows the player, also on every new level and after loads;
    • stay: it stays a companion but does not go along (it waits where it is);
    • range=<units>: it goes along only if it is within that distance of the player when the level changes; a number from 0 to 65536; range=0 is the same as leaving it out (from anywhere on the map).
    • Any other word, or a bad range, raises ValueError (only checked when on is 1).
  • Returns 1 on success, 0 otherwise: no level runs, the entity does not exist, is not an NPC or (with on 1) is dead, or the game version is not recognized. Raises RuntimeError if memory runs out.

Calling it again for a companion replaces its options: to switch a waiting companion back to following, call set_companion(npc, 1, 'follow'). The options are stored in a fixed form (lower case, in the order follow, stay, range=...).

Python
blse.set_companion(npc, 1, 'follow range=1500')   # follows; comes along when near
blse.set_companion(npc, 1, 'stay')                # waits here
blse.set_companion(npc, 0)                        # parts ways

blse.companions#

Python
blse.companions([all_maps])   # -> [(handle, map, class, name, options), ...] or None

Lists the companions. Use it to give orders to all of them (attack this enemy, regroup) or to show them in a menu.

  • all_maps (int, optional): 0 (default) lists only the companions on the current level; 1 adds those left on other maps.
  • Returns a list of tuples:
    • handle (int or None): the companion's handle on this level; None for one on another map;
    • map (str): the map it is on;
    • class (str): its classname;
    • name (str): its targetname ('' if none);
    • options (str): its options ('' for none).
  • Returns None when no level runs. Raises RuntimeError if memory runs out.
Python
for handle, map, cls, name, options in blse.companions(1):
    if handle is None:
        blse.log('%s waits on %s' % (name, map))

blse.companion_options#

Python
blse.companion_options(npc)   # -> str or None

Tells whether an NPC is a companion, and with which options.

  • npc: index or handle.
  • Returns its options as text ('' if it has none), or None if it is not a companion (or no level runs). Raises RuntimeError if memory runs out.
Python
if blse.companion_options(npc) is not None:
    blse.console_print('Already with you.\n')

blse.block_companion_map#

Python
blse.block_companion_map(map[, block])   # -> 1 or 0

Keeps all companions out of a map: at a level change to it, they stay behind (event 'stayed') and the player finds them again on the way back. Use it for cutscene maps and maps with scripted scenes that would break with an extra character.

  • map (str): the map's name, case ignored, with or without .bsp ('sm_pawnshop_1'), at most 63 characters.
  • block (int, optional): 1 (default) blocks, 0 unblocks.
  • Returns 1, or 0 for an empty or too long name.

Not saved and not tied to a level: set it at the top of your script (it runs at every start), or in a blse.level_change handler just before the change.

Python
blse.block_companion_map('sm_pawnshop_1')   # the scene there has no room for a companion

blse.level_change#

Fires when a level change was asked for (a door or exit, a taxi, a script). The change itself happens on a later frame, so this arrives before blse.level_shutdown: the last moment to decide who goes along. Full description on the events page.

Payload, in order: map (the map being loaded), landmark (the landmark's name; may be '').

Python
def on_level_change(topic, data):
    map, landmark = data.split('\0')
    if map.lower() == 'sm_pawnshop_1':
        blse.block_companion_map(map)

blse.subscribe('blse.level_change', on_level_change)

blse.companion#

Fires when a companion arrives on a new level, stays behind on the old one, or is lost. It arrives at the next safe point after the level is ready, so a handler can give orders to the companion right away.

Payload, in order:

  • what: 'arrived', 'stayed' or 'lost';
    • 'arrived': it came along to this level;
    • 'stayed': at the level change it stayed on the old map (its stay option, a blocked map, out of range, or dead);
    • 'lost': it died or was removed, and is no companion now;
  • npc: its handle on this level (decimal text; use int(npc)), or '' when it is not on this level (always for 'stayed'; for 'lost' when it was found missing as the level started);
  • cls, name: its classname and targetname;
  • map: for 'arrived' the map it came from; for 'stayed' the map it stayed on; for 'lost' the current map.
Python
def on_companion(topic, data):
    what, npc, cls, name, map = data.split('\0')
    if what == 'arrived':
        # Not saved, and a new level gives the companion a new handle: set them again.
        blse.set_damage_multiplier('#%s' % npc, 0, 'player')
    elif what == 'lost':
        blse.console_print('%s is gone.\n' % name)

blse.subscribe('blse.companion', on_companion)

Note: a dead or removed companion is noticed within half a second.

How it works#

How it works: the game itself carries the player's items and Discipline effects to the next map, by marking them in the level's transition data. BLSE marks companions (and every item in their inventories) the same way, so everything the game saves about the NPC comes along: class, model, its keyvalues (name, dialogue file, ...), health, stats, inventory and the weapon in its hands, relationships it saved, its skin and body groups. It arrives where it stood relative to the level change's landmark. Bloodlines' interiors are separate maps that do not line up with the street at the landmark, so that place can be far from the player, or outside the rooms (about 1250 units away in the Santa Monica clinic). The mod puts it near the player: not right next to the player (that spot may be inside a desk), but on a spot the player stood on a moment ago, which is known to be free floor. The example's companion.py keeps a short trail of the player's positions for this. Its copy on the old map is removed by the game. BLSE moves its own data with it: entity values (entity_set), kept looks and scales (keep).

Every level change the game makes uses a landmark (doors and area exits, the taxi, the sewer map and the map screen), so all of them take companions. A load takes nothing anywhere: companions are where the save has them, and found again.

Things to know:

  • Names. A companion keeps its targetname on the new map. Map and dialogue scripts that look NPCs up by name (FindEntityByName) find it, and may find it instead of their own NPC of that name. Give companions names of your own ('mymod_companion_1'); one of the game's NPCs can be renamed with blse.set_keyvalue(npc, 'targetname', 'mymod_companion_1').
  • Making one of the game's own NPCs a companion takes it out of its map for good: quests that need it there break. Prefer NPCs a mod spawns (blse.spawn_entity).
  • Scripted scenes on the new map do not know the companion. Block maps where it must not appear with block_companion_map; it then stays behind and the player finds it again on the way back.
  • An arriving companion is calmed (idle, the schedule it ran on the old map ended) and, with follow, follows again: its old map's route and goal mean nothing on the new one.
  • A dead companion is not taken. One in a scripted sequence of its old map is taken; the game ends the sequence on arrival ("Scripted Sequence stripped on level transition").
  • Handles change. A companion gets a new handle on every new level and after every load. Do not keep handles across levels: read them again from blse.companions() or the 'arrived' event.

Fighting beside the player#

What the game and BLSE give a companion mod:

Need How
A class that fights and talks npc_VHumanCombatant (gangsters, guards, hunters: 558 in the game's maps, 53 with a dialogue file, e.g. Zygaena) or npc_VVampire (59 of 157 with dialogue, e.g. Barabus). Their weapon is the additionalequipment keyvalue ('item_w_glock_17c', 'item_w_mac_10', 'item_w_katana'). Pedestrians (npc_VPedestrian, npc_VDialogPedestrian) have no attack: they take cover. Spawn a fighter with a stattemplate ('ClinicGuard'; see vdata/system/npctemplate*.txt): without one it never sees its enemies
Who hurt the player, whom the player hit blse.hit (below), with handles
Who starts a fight with the player blse.npc_combat ('start', target the player)
Attack someone set_relationship(npc, target, 'hate', 99), then set_enemy(npc, target); then start_schedule(npc, 'SCHED_TROIKA_FORCED_RANGE_ATTACK1'): the game does not fire while the player is in the line of fire, and a companion follows behind the player (it draws its gun and never fires)
Why it does not shoot npc_conditions(npc) (79 can fire, 99 a friend in the way, 64 no ammunition, ...) and npc_weapon(npc) (NPC AI)
Keep fighting The game drops an enemy it lost sight of (blse.npc_combat 'stop'); send it again while the enemy still fights (combat_npcs)
Not hostile to the player set_relationship(npc, 1, 'like', 99)
No friendly fire set_damage_multiplier('#%d' % npc, 0, 'player') and set_damage_multiplier('player', 0, '#%d' % npc): '#' and the handle name this one NPC, also an unnamed one. Not saved, and a load gives entities new handles: set them again after blse.after_load and when a companion arrives
Regroup set_enemy(npc, -1) calms it; follow_npc (or the follow option) carries on
Enemies fighting the companion back set_relationship(enemy, npc, 'hate', 50)

A spawned fighter, ready to recruit (keyvalues as in spawn_entity):

Python
import blse

def hire(name, args):
    x, y, z = blse.entity_position(1)        # where the player stands
    npc = blse.spawn_entity('npc_VHumanCombatant', x + 64, y, z + 8, 0,
                            {'targetname': 'mymod_bodyguard',
                             'model': 'models/character/npc/unique/giovanni_mansion/bodyguard/bodyguard.mdl',
                             'additionalequipment': 'item_w_glock_17c',
                             'stattemplate': 'ClinicGuard'})
    if npc:
        blse.set_relationship(npc, 1, 'like', 99)
        blse.set_companion(npc, 1, 'follow')

blse.add_command('hire', hire, 'hire: a bodyguard joins you')
Event
blse.hit victim, vcls, vname, attacker, acls, aname, damage, kind = data.split('\0'): every hit the game applies, as blse.damage, but with the handles of the victim and the attacker (decimal; empty for none). Only built while anyone listens

blse.hit#

Fires after each hit the game applies to any entity with health (the player, NPCs, breakable objects), while anyone listens. Unlike blse.damage, it gives the handles of the victim and the attacker, so a companion mod can turn on whoever hurt the player. Delivered to Python at the next safe point.

Payload, in order:

  • victim, vcls, vname: the victim's handle (decimal text), classname and targetname;
  • attacker, acls, aname: the same for the attacker; all three '' when the hit has no attacker (a fall, fire from the world);
  • damage: the amount applied, as text ('12.5'), after BLSE's damage rules and before the game's own difficulty and resistance scaling;
  • kind: the damage type bits as decimal text (see damage_entity).
Python
def on_hit(topic, data):
    victim, vcls, vname, attacker, acls, aname, damage, kind = data.split('\0')
    if vcls == 'player' and attacker:
        for handle, map, cls, name, options in blse.companions():
            blse.set_relationship(handle, int(attacker), 'hate', 99)
            blse.set_enemy(handle, int(attacker))
blse.subscribe('blse.hit', on_hit)

Note: the attacker of a gunshot is the shooter, not the gun. Every hit sends one event; listen only while your mod needs it.

Kills by a companion count as its own: the game's attacker is the companion, not the player.