BLSE0.6

Python mods

Combat, damage, stats and Disciplines

On this page 13 sections

This page is about fighting and character values: changing how much damage hits do, knowing every hit, kill and stat change (blood, health, Humanity, Disciplines), blocking Disciplines or changing their blood cost, hurting and pushing entities, and running console commands. Come here for a difficulty mod, a survival mod, a mod that rewards kills, or new rules for Disciplines.

A minimal working example: the player takes half damage, and every kill by the player is announced.

Python
import blse

blse.set_damage_multiplier('player', 0.5)

def on_killed(topic, data):
    f = data.split('\0')
    if f[3] == 'player':
        blse.show_text('You killed %s' % (f[1] or f[0]), 'top')

blse.subscribe('blse.entity_killed', on_killed)

Important, before you start:

  • Almost everything here needs a recognized game build (blse.build_id() is not None). On other builds the rule functions raise RuntimeError and the events are never sent.
  • Rules are not saved with the game and are cleared on an engine restart: set them when your script runs (at the top of the file is fine; they need no level).
  • Events reach Python at the end of the frame, after the game applied the change. To change a hit before it lands, use a rule (set_damage_multiplier); native plugins can also use filters.
  • Do not change a stat from a blse.stat_change handler without a guard: your change is reported again and can loop.

Words used on this page#

Word Meaning
Victim, attacker, inflictor Who is hit; who caused it; what delivered it (the weapon or projectile)
Damage type bits A number whose bits say what kind of damage a hit is: 0 generic, 2 bullet, 128 club, ... (the Source engine's DMG_* values)
Stat A character value the game keeps in its stat lists (vdata/system/stats.txt): blood pool, health (damage taken), Humanity, attributes, abilities, Disciplines
Base value A stat's own value, before temporary effects
Discipline A vampire power (Potence, Celerity, Auspex, ...). Activating one costs blood
Rule A setting made with a set_..._rule or set_..._multiplier function; the game applies it itself, with no Python running at that moment
Velocity Speed and direction, in units a second, as (x, y, z)

Damage#

On recognized game builds, BLSE sees every hit the game applies through CBaseEntity::TakeDamage: weapons, fists, scripted damage, damage to props.

Function / topic
blse.set_damage_multiplier(victim, factor[, attacker]) Multiplies damage to victim from attacker (default '*'). Each is '*', a classname ('player', 'npc_VVampire'), a targetname, or (b0.5.48) '#' and an entity's handle for that one entity ('#%d' % npc: a companion with no name of its own; set it again after a load, which gives entities new handles). Factor 0 means no damage; 1 removes the rule. Matching rules multiply. Returns 1; raises RuntimeError on unknown builds
blse.clear_damage_multipliers() Removes every rule set from Python
blse.damage Report after each hit: victim_class, victim_name, attacker_class, attacker_name, inflictor_class, inflictor_name, damage, original, type = data.split('\0'). damage is what was applied after the rules (before the game's own difficulty/resistance scaling), original what the game asked for, type the damage-type bits. Delivered at the end of the frame
Python
import blse
blse.set_damage_multiplier('player', 0.5)          # the player takes half damage
blse.set_damage_multiplier('*', 2.0, 'player')      # and deals double

The rules are applied natively inside the game's damage code, so no Python runs there. They are not saved with the game; set them again when your script runs (they are cleared on an engine restart, when scripts run again anyway).

blse.set_damage_multiplier, blse.clear_damage_multipliers#

Python
blse.set_damage_multiplier(victim, factor[, attacker])   # -> 1
blse.clear_damage_multipliers()

(b0.5.3) When to use it: difficulty settings, armour that protects against one enemy, a companion that cannot be hurt by the player.

  • victim, attacker: '*' (anyone), a classname ('player', 'npc_VVampire'), a targetname, or '#' and a handle ('#%d' % npc, b0.5.48) for one entity. Names are 1-63 characters. attacker defaults to '*'.
  • factor: 0 or more; 0 no damage, 0.5 half, 2 double, 1 removes the rule. When several rules match a hit, their factors multiply.
  • Returns 1. Errors: ValueError for a bad name or a negative factor; RuntimeError on unknown builds.
  • clear_damage_multipliers() removes every rule set from Python; returns None.
  • A handle rule must be set again after a load (entities get new handles).

blse.damage#

Sent after each hit the game applied (b0.5.3), at the end of the frame. Fields, in order: victim classname, victim name, attacker classname, attacker name, inflictor classname, inflictor name, damage (what was applied after the rules, before the game's own difficulty and resistance scaling), original (what the game asked for), type (the damage type bits). Numbers are text: float(damage), int(type). For the entities' handles, use blse.hit.

Python
def on_damage(topic, data):
    vcls, vname, acls, aname, icls, iname, damage, original, kind = data.split('\0')
    if vcls == 'player':
        blse.log('player hit by %s for %s' % (acls, damage))
blse.subscribe('blse.damage', on_damage)

Stat changes#

On recognized game builds, blse.stat_change (b0.5.5) reports every change to a character stat's base value, for the player and for NPCs: blood, damage taken, humanity, attributes, abilities, Disciplines and which of them are active.

Python
import blse

def on_stat(topic, data):
    owner, index, old, new, method, cause, entity, name = data.split('\0')
    if owner == '1' and name == 'bloodpool':
        blse.log('blood %s -> %s (%s)' % (old, new, cause.split(' < ')[0]))

blse.subscribe('blse.stat_change', on_stat)
Field
owner Entity index of the character. The player is 1
index, name The stat, as defined in vdata/system/stats.txt, by its internal (lower-case) name: bloodpool, health, humanity, strength, brawl, potence, active_potence, ... The index is its position within its group (Attributes, Abilities, Disciplines, ActiveDisciplines), so compare names, not indices
old, new Integer values before and after
method The game method that made the change: IncBase, DecBase, AddBase, SubBase, SetBase
cause The game functions that led to it, innermost first, separated by ' < ' (from the game's own crash-report trace). Only engine frames (RunListenServer < CEngineAPI::Run ...) when the change came from player input or a game script, such as activating a Discipline
entity The entity name the innermost cause reported, if any (for kills: the victim)

Delivered at the end of the frame. Filling a new character's stats with their defaults is not reported.

When to use it: a survival mod that watches blood, a mod that rewards Humanity changes, a HUD that shows a stat. owner, index, old and new are text: compare owner == '1' for the player and use int(new). Experience, Masquerade violations and Humanity also arrive here (names experience, masquerade, humanity).

  • health counts damage taken, not health left: 0 is unhurt, it rises when hit (OnTakeDamage_Alive) and falls when healing (HealthHeal < UpdateVampHeal_HOT).
  • Feeding: the player's bloodpool rises by 1 per gulp (IncBloodPool < Feed), the victim's falls (DecBloodPool < Feed) and its health rises by 3.
  • Potence: bloodpool falls by 1 (CVStatAction_t::Apply), active_potence goes 0 -> 2, and back to 0 when it runs out (GameFrame).
  • Killing an innocent: humanity falls by 1, cause Event_Killed, entity names the victim.

Disciplines#

On recognized game builds (b0.5.14):

Topic data
blse.discipline_activated owner_class, owner_name, owner_index, discipline, level = data.split('\0'): a character activated a Discipline. discipline is its stats.txt name in lower case (potence, celerity, presence, blood_healing, auspex, obfuscate, ...); the player is owner_index == '1'. owner_class and owner_name are empty if BLSE cannot confirm the owner
blse.discipline_ended Same fields: the Discipline's effect ended (ran out or was cancelled)

They are derived from the active_<discipline> stats, so the same changes also appear in blse.stat_change. Delivered at the end of the frame. To block a Discipline or change its cost, see the next section (b0.5.20).

level is the Discipline's level as text (int(level)); owner_index is the entity index as text ('1' the player).

Blocking Disciplines and changing their cost#

blse.set_discipline_rule(discipline[, who[, allow[, cost]]]) (b0.5.20) controls activations before the game applies them. discipline is a stats.txt name ('potence') or '*'; who is '*' (default), 'player', a classname or a targetname. allow=0 blocks the activation: no effect and no blood cost. cost multiplies the blood cost (0 free, 2 double; rounded to whole blood points, halves away from zero). Matching rules all must allow, and their costs multiply; allow=1, cost=1 removes a rule. blse.clear_discipline_rules() removes Python's rules (they are also cleared on an engine restart).

Python
import blse
blse.set_discipline_rule('potence', 'player', 0)          # the player cannot use Potence
blse.set_discipline_rule('*', 'player', 1, 0.5)            # every Discipline at half cost
blse.set_discipline_rule('celerity', 'npc_VCop', 0)       # cops cannot use Celerity

blse.set_discipline_rule, blse.clear_discipline_rules#

Python
blse.set_discipline_rule(discipline[, who[, allow[, cost]]])   # -> 1
blse.clear_discipline_rules()
  • discipline: a stats.txt name in lower case ('potence', 'celerity') or '*' for all.
  • who: '*' (default), 'player', a classname or a targetname; 1-63 characters.
  • allow: 1 (default) allows, 0 blocks.
  • cost: multiplies the blood cost, 0 or more (default 1).
  • Returns 1. Errors: ValueError for a bad name or a negative cost; RuntimeError on unknown builds.
  • clear_discipline_rules() removes Python's rules; returns None.
  • Native plugins can decide per activation with a Discipline filter; Python rules are applied first.

blse.discipline_blocked reports each blocked activation (same fields as discipline_activated). The game would show its "Need More Blood" notice for a blocked activation (a failed activation is all it knows); since b0.5.21 BLSE hides that notice, and a mod can show its own explanation with blse.show_text.

Kills, damage, pushes and console commands#

blse.entity_killed (event) An entity with health (a character, a breakable object) was killed by a hit. Data: victim class, name, handle; attacker class, name, handle; inflictor (the weapon or projectile) class, name, handle; damage; damage type bits, separated by '\0' (empty fields where there is none)
blse.damage_entity(entity, amount[, attacker[, type]]) Hurts an entity through the game's own damage: the victim reacts to the attacker (flees, fights back), and a kill counts as the attacker's (the player loses Humanity, the police come). attacker: an entity, or -1 (the default) for none; type: damage type bits (0 generic, 128 club, 2 bullet...). Damage rules, filters and blse.damage apply. Returns 1 or 0
blse.entity_velocity(entity) (x, y, z) in units a second, or None
blse.push_entity(entity, x, y, z[, add]) Adds the velocity (add 1, the default) or sets it (0). A push upwards takes the entity off the ground first. The player, NPCs (b0.5.44) and physics props (b0.5.44). Returns 1 or 0
blse.console_command(line) Runs a console command line as if typed (queued: it runs before the next frame). 1-1023 characters, no line breaks

push_entity moves the player by its velocity. NPCs set their own velocity every time they think, so since b0.5.44 a push on an NPC uses the game's own flying knockback (what a strong blow does) with the given velocity: the NPC flies (the animation chosen from the direction), lands and gets up. It returns 0 when the game would not knock that NPC back now (in a conversation, dead) or the NPC has no flying animation. Physics props (prop_physics: cans, bottles, boxes) are moved by the physics engine, so a push on them sets their physics object's velocity (b0.5.44). entity_velocity of a prop is not its physics velocity.

Python
def on_killed(topic, data):
    f = data.split('\0')
    victim, killer, weapon = f[1] or f[0], f[3], f[6]
    if killer == 'player':
        blse.console_print('You killed %s with %s' % (victim, weapon))
blse.subscribe('blse.entity_killed', on_killed)

blse.entity_killed#

(b0.5.41) Fields, in order (11): victim classname, name, handle; attacker classname, name, handle; inflictor classname, name, handle; the damage; the damage type bits. Empty fields where there is none. Delivered at the end of the frame (native plugins right after the hit). Handles are text: int(f[2]).

blse.damage_entity#

Python
blse.damage_entity(entity, amount[, attacker[, type]])   # -> 1 or 0

(b0.5.41) When to use it: a trap, poison, a Discipline of your own, a scripted execution.

  • entity: an index or handle. amount: 0 to 1000000 (else ValueError).
  • attacker: an entity, or -1 (default) for none. With an attacker the victim reacts to it and a kill counts as the attacker's.
  • type: damage type bits (default 0, generic).
  • Returns 1, or 0 if the entity (or the attacker given) does not exist or the game refused the damage.
Python
blse.damage_entity(npc, 25, 1, 128)   # the player clubs the NPC for 25

blse.entity_velocity, blse.push_entity#

Python
blse.entity_velocity(entity)              # -> (x, y, z) or None
blse.push_entity(entity, x, y, z[, add])  # -> 1 or 0

(b0.5.41; NPCs and props b0.5.44) entity_velocity returns the entity's speed and direction in units a second, or None. push_entity adds (add 1, the default) or sets (0) a velocity; each component -10000 to 10000 (else ValueError). Returns 1, or 0 if the entity cannot be pushed now. When to use it: a knockback blow, a jump boost, throwing a prop.

Python
blse.push_entity(1, 0, 0, 400)   # the player jumps

blse.console_command#

Python
blse.console_command(line)   # -> 1 or 0

(b0.5.41) Runs a console command line as if typed. It is queued and runs before the next frame, so its effect is not visible right after the call. line: 1-1023 characters, no line breaks (else ValueError). Returns 1 if queued, 0 if the console is not available.

Python
blse.console_command('fov 90')

The game's own Python already reaches an entity's appearance: fields by name (ent.renderamt = 128, ent.skin = 1) and inputs as methods (ent.Kill()).

The game's Python errors#

Python's error output (the game's tracebacks) is always copied to blse.log (b0.5.40). When game code run as a string fails (a map's output naming Python, a dialogue condition), the log also names that code and where in vampire.dll it was run from (b0.5.44), since the traceback only says <string>. Not logged: map outputs calling ccmd.<name>, which never worked and fail harmlessly (every map's leftover cubemaps_builder entity calls ccmd.wc_create 1.5 s after it loads; the game still prints its SystemError).

BLSE fixes none of the game's own bugs (b0.5.49): game fixes belong to a separate fixes mod. blse.fixes() and the [Fixes] section of config/blse.ini (b0.5.41-b0.5.48) are gone; BLSE removes the old section's entries. The old long_saved_lines fix is part of BLSE's save system since b0.5.49, always on (see Saving data).