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.
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, beforeblse.level_shutdown), from Python's normal code (the main thread). Elsewhereset_companionreturns0andcompanions/companion_optionsreturnNone.block_companion_mapworks 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.
npcis an NPC's index or handle (look_target()[2], or fromblse.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#
blse.set_companion(npc, on[, options]) # -> 1 or 0Makes 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):1makes it a companion,0ends it (it stays where it stands, and if it hadfollow, it stops following).options(str, optional): words separated by spaces, commas or tabs, case ignored, at most 200 characters.Noneor''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=0is the same as leaving it out (from anywhere on the map).- Any other word, or a bad range, raises
ValueError(only checked whenonis 1).
- Returns
1on success,0otherwise: no level runs, the entity does not exist, is not an NPC or (withon1) is dead, or the game version is not recognized. RaisesRuntimeErrorif 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=...).
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 waysblse.companions#
blse.companions([all_maps]) # -> [(handle, map, class, name, options), ...] or NoneLists 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;1adds those left on other maps.- Returns a list of tuples:
handle(int orNone): the companion's handle on this level;Nonefor 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
Nonewhen no level runs. RaisesRuntimeErrorif memory runs out.
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#
blse.companion_options(npc) # -> str or NoneTells whether an NPC is a companion, and with which options.
npc: index or handle.- Returns its options as text (
''if it has none), orNoneif it is not a companion (or no level runs). RaisesRuntimeErrorif memory runs out.
if blse.companion_options(npc) is not None:
blse.console_print('Already with you.\n')blse.block_companion_map#
blse.block_companion_map(map[, block]) # -> 1 or 0Keeps 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,0unblocks.- Returns
1, or0for 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.
blse.block_companion_map('sm_pawnshop_1') # the scene there has no room for a companionblse.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 '').
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 (itsstayoption, a blocked map, out ofrange, or dead);'lost': it died or was removed, and is no companion now;
npc: its handle on this level (decimal text; useint(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.
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 withblse.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):
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 (seedamage_entity).
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.