# ../silent_hill/silent_hill.py (npc edition)
# Python
import random
# Source.Python
from colors import Color
from commands.server import ServerCommand
from core import PLATFORM, echo_console
from engines.precache import Model
from engines.server import server
from engines.sound import Sound
from engines.trace import ContentMasks, engine_trace, GameTrace, Ray
from entities.constants import (EntityEffects, RenderMode, RenderEffects,
MoveType, WORLD_ENTITY_INDEX)
from entities.entity import Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from filters.entities import EntityIter
from listeners import (OnEntityCreated, OnEntityDeleted, OnLevelInit,
OnLevelEnd, OnClientActive, OnClientDisconnect)
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector, QAngle
from memory import Convention, DataType, NULL, find_binary
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables
# NOTE: This plugin was designed to work with the default settings for
# 'SIREN_SOUND' and 'SIREN_TIME'. Modifying these values might degrade your
# experience.
# Sound that plays during the siren phase - before the fog sets in.
# Default: 'ambient/alarms/citadel_alert_loop2.wav'
SIREN_SOUND = Sound('ambient/alarms/citadel_alert_loop2.wav')
# How long should the siren phase last? (in seconds)
# Default: 26
SIREN_TIME = 26
# How long until the apocalypse starts again? (in seconds)
INTERVALS = (30, 45, 60, 90)
# Seconds until the thick Silent Hill-like fog starts fading away.
FOG_FADE_DELAY = 60
# How long should the fading of the fog take? (in seconds)
FOG_FADE_TIME = 60
# Should NPCs spawn during the thick fog phase? (True/False)
NPCS_ENABLED = True
# Maximum number of NPCs at any given time.
NPCS_MAX = 10
# NPCs that can spawn. Be sure to properly configure the needed convars. These
# should be located in your server.cfg file. (e.g. sk_manhack_health,
# sk_manhack_damage, sk_zombie_health, sk_zombie_dmg_one_slash, and so on)
NPCS = (
'npc_zombie',
'npc_manhack',
'npc_headcrab',
'npc_antlion'
)
# If you wish to change models for certain NPCs, add them here.
NPC_MODELS = {
'npc_zombie': (Model('models/zombie/fast.mdl'),),
'npc_headcrab': (Model('models/headcrab.mdl'),)
}
# For how many seconds should 'npc_manhack' entities glow after spawning?
# (setting this to 0 disables the glow)
MANHACK_GLOW_DURATION = 2
FOG_COLOR = Color(185, 185, 185)
FLASH_COLOR = Color(255, 0, 0, 150)
FLASH_COLOR_END = Color(255, 0, 0, 255)
# Sprite used for tinting the player's screen.
SCREEN_SPRITE = Model('sprites/white.vmt')
SCREEN_SPRITE_OFFSET = Vector(10, 0, 0)
# Offset for NPC spawn positions.
NPC_ORIGIN_OFFSET = Vector(0, 0, 32)
NPC_ORIGIN_MINS = Vector(-64, -64, 0)
NPC_ORIGIN_MAXS = Vector(64, 64, 64)
# Dictionary used to keep track of 'env_sprite' entities we'll be using.
_black_screens = {}
# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
# Are there any players on the server?
if server.num_players > 0:
dark_times.initialize()
def unload():
"""Called when the plugin gets unloaded."""
dark_times.stop(pause_init=False)
dark_times.remove_all_npcs()
# Remove any leftover player entities.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).remove_black_screen()
@OnLevelEnd
def on_level_end():
"""Called when the map starts changing."""
dark_times.stop()
# Remove old data (spawn points, npc indexes).
dark_times.clean_up_data()
@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
dark_times.stop()
dark_times.initialize()
@OnLevelInit
def on_level_init(map_name):
"""Called when the new map is done loading."""
dark_times.initialize()
@OnClientActive
def on_client_active(index):
"""Called when a player fully connects to the server."""
# Is this the first player to join the server? (server was empty)
if server.num_players == 1:
dark_times.initialize()
@OnClientDisconnect
def on_client_disconnect(index):
"""Called when a player leaves the server."""
# Delay the call by a single frame - otherwise we might get incorrect info.
Delay(0, _on_client_disconnect)
def _on_client_disconnect():
# Did the last player just leave the server? (server is now empty)
if server.num_players == 0:
dark_times.stop()
@OnEntityCreated
def on_entity_created(base_entity):
"""Called when an entity gets created/spawned."""
try:
index = base_entity.index
except ValueError:
# Not a networked entity.
return
# Did a headcrab just spawn?
if 'npc_headcrab' in base_entity.classname:
npc = NPC(index)
npc.delay(0, npc.check_headcrab)
@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity gets deleted."""
try:
index = base_entity.index
except ValueError:
return
try:
# Was this one of our 'env_sprite' entities?
player_index = _black_screens.pop(index)
# Remove the instance reference from the player.
PlayerSH(player_index).black_screen = None
except (KeyError, ValueError):
pass
try:
# Was this an NPC we spawned?
dark_times.npc_indexes.remove(index)
except KeyError:
pass
@Event('entity_killed')
def npc_killed(event):
"""Called when an entity gets killed."""
index = event['entindex_killed']
# Is this one of our NPCs?
if index in dark_times.npc_indexes:
NPC(index).dissolve()
# =============================================================================
# >> PLAYER STUFF
# =============================================================================
class PlayerSH(Player):
"""Modified Player class."""
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.black_screen = None
self.target_name = f'player_{self.userid}'
@property
def viewmodel(self):
"""Returns the Entity instance of the player's viewmodel."""
return Entity.from_inthandle(self.get_property_int('m_hViewModel'))
def darken_view(self, amount):
"""Lowers the brightness of the player's screen."""
if self.black_screen is None:
self.black_screen = create_sprite(
origin=NULL_VECTOR, scale=30.0, model=SCREEN_SPRITE)
self.black_screen.set_parent(self.viewmodel, -1)
self.black_screen.teleport(SCREEN_SPRITE_OFFSET)
# Create a sprite:player reference for later use.
_black_screens[self.black_screen.index] = self.index
# Change the alpha/transparency of the 'env_sprite'.
self.black_screen.set_network_property_int('m_nBrightness', amount)
def remove_black_screen(self):
"""Removes the 'env_sprite' used for tinting the player's screen."""
try:
self.black_screen.remove()
except AttributeError:
return
self.black_screen = None
# =============================================================================
# >> DARK TIMES
# =============================================================================
class DarkTimes:
"""Class used to start and stop the apocalypse.
Attributes:
current_darkness (int): Level of darkness used for darkening players'
screens.
in_progress (bool): Is the apocalypse currently happening?
npc_indexes (set of int): Contains indexes of NPCs spawned during the
thick fog phase.
flash_think (Repeat): Instance of Repeat() used for looping the
`_flash_think()` function.
darken_think (Repeat): Instance of Repeat() used for looping the
`_darken_think()` function.
gather_data_think (Repeat): Instance of Repeat() used for looping the
`_gather_data_think()` function.
old_fog_values (tuple): Tuple that holds values of the previous fog.
_fog (Entity): Entity instance of the 'env_fog_controller' we'll be
using.
_push (Entity): Entity instance of the 'point_push' entity.
_delays (dict of Delay): Dictionary that holds any Delay() instances
we might be using.
_saved_time (float): Remaining time from the previous initialization.
_valid_npc_origins (list of Vector): List containing spawn positions
for NPCs.
"""
def __init__(self):
"""Initializes the object."""
self.current_darkness = 0
self.in_progress = False
self.npc_indexes = set()
self.flash_think = Repeat(self._flash_think)
self.darken_think = Repeat(self._darken_think)
self.gather_data_think = Repeat(self._gather_data_think)
self.spawn_npcs_think = Repeat(self._spawn_npcs_think)
self.old_fog_values = None
self._fog = None
self._push = None
self._delays = {}
self._saved_time = None
self._valid_npc_origins = []
def initialize(self, instant=False):
"""Starts the apocalypse after a randomly chosen delay."""
# Don't go further if the apocalypse is already happening.
if self.in_progress:
return
try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass
# Are we trying to instantly start the apocalypse?
if instant:
self.begin()
else:
self._delays['init'] = Delay(
# Resume the time to start from the previous initialization if
# there is one, otherwise pick a random time.
self._saved_time if self._saved_time else random.choice(
INTERVALS), self.begin)
def begin(self):
"""Starts the apocalypse."""
self.in_progress = True
self.current_darkness = 0
self._saved_time = None
try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass
SIREN_SOUND.play()
# Sync the red flashes with the siren sound - every time the players
# hear the siren, their screen will be flashed red.
# NOTE: This was synced with the default 'SIREN_SOUND'.
self.flash_think.start(
interval=6.5, limit=SIREN_TIME / 6.5, execute_on_start=True)
# Start lowering the brightness.
self.darken_think.start(interval=0.5, limit=40, execute_on_start=True)
# Look for valid spawn positions for NPCs during the siren phase.
self.gather_origin_data()
# Change to a thick fog similar to the one from Silent Hill.
# (credit: killer89 - https://gamebanana.com/prefabs/1308 )
self._delays['final_flash'] = Delay(
SIREN_TIME, self.change_fog, (FOG_COLOR, FOG_COLOR, 0, 620))
# Start changing the fog back to normal.
self._delays['restoration'] = Delay(
SIREN_TIME + FOG_FADE_DELAY, self.restore_fog_smooth, (
FOG_FADE_TIME,))
def stop(self, pause_init=True):
"""Stops the apocalypse and restores everything back to normal."""
SIREN_SOUND.stop()
# Stop the looping functions.
self.flash_think.stop()
self.darken_think.stop()
self.gather_data_think.stop()
self.spawn_npcs_think.stop()
if pause_init:
try:
# If we're stopping the apocalypse before it had a chance to
# begin, save the remaining time - so we can resume it later.
self._saved_time = self._delays['init'].time_remaining
except KeyError:
pass
# Cancel all delays.
for delay in self._delays.values():
try:
delay.cancel()
except ValueError:
continue
# Set the brightness back to normal levels.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)
self.restore_fog()
self.in_progress = False
self._fog = None
try:
self._push.remove()
except AttributeError:
pass
self._push = None
def clean_up_data(self):
"""Removes data that's no longer needed/valid."""
self._valid_npc_origins.clear()
self.npc_indexes.clear()
def remove_all_npcs(self):
"""Removes all currently active NPCs."""
for index in self.npc_indexes.copy():
NPC(index).remove()
def on_completed(self):
"""Called when the fog finally settles back to the default values."""
self.in_progress = False
# Prepare for the next apocalypse.
self.initialize()
def push_away_from(self, origin, radius, magnitude):
"""Pushes all movable entities at the specified origin.
Args:
origin (Vector): Point from which to push away entities from.
radius (float): Maximum distance an entity can be from the `origin`
and still be pushed away.
magnitude (float): Power/strength of the push.
"""
if self._push is None:
push = Entity.create('point_push')
# Set a couple of spawn flags.
# 8: Push players
# 16: Push physics
push.spawn_flags = 8 + 16
push.spawn()
self._push = push
self._push.origin = origin
self._push.set_property_float('m_flRadius', radius)
self._push.set_property_float('m_flMagnitude', magnitude)
self._push.call_input('Enable')
self._push.delay(0.15, self._push.call_input, ('Disable',))
def gather_origin_data(self):
"""Starts gathering valid spawn positions for NPCs."""
# Have we gathered a decent amount of spawn positions?
if len(self._valid_npc_origins) >= 32:
return
self.gather_data_think.start(
interval=2, limit=SIREN_TIME / 2, execute_on_start=True)
def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))
# Is this player dead?
if player.dead:
continue
new_origin = player.origin + NPC_ORIGIN_OFFSET
# Let's see if there's enough room for NPCs to spawn here.
enough_space = NPC.check_space(
origin=new_origin,
mins=NPC_ORIGIN_MINS,
maxs=NPC_ORIGIN_MAXS
)
if not enough_space:
continue
# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)
def _flash_think(self):
UTIL_ScreenFadeAll(FLASH_COLOR, 0.5, 0.25, 1)
def _darken_think(self):
# Increase the darkness.
self.current_darkness += 5
# Reduce the brightness for each player on the server.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(
self.current_darkness)
def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return
try:
origin = random.choice(self._valid_npc_origins)
except IndexError:
# Missing data for NPC spawn points.
return
# Push away any entities (props, players, npcs) from the spawn point.
self.push_away_from(origin, 200, 500)
# Spawn the NPC after a short delay.
self._delays['npc_spawn'] = Delay(0.15, self._spawn_npc, (origin,))
def _spawn_npc(self, origin):
# Pick an NPC to spawn.
npc = NPC.create(npc_name=random.choice(NPCS), origin=origin)
try:
# Try to add the NPC's index to the set.
self.npc_indexes.add(npc.index)
except AttributeError:
# The NPC wasn't properly created - skipping this wave.
return
# Move the NPC slightly in a random direction after spawning.
npc.base_velocity = get_random_direction() * 180
def get_fog_instance(self):
"""Returns an Entity instance of an 'env_fog_controller'."""
if self._fog is not None:
return self._fog
old_fog = Entity.find('env_fog_controller')
# Does an 'env_fog_controller' already exist on this map?
if old_fog:
# Store the old values for later use.
self.old_fog_values = (
old_fog.get_property_color('m_fog.colorPrimary'),
old_fog.get_property_color('m_fog.colorSecondary'),
old_fog.fog_start,
old_fog.fog_end
)
# We'll use that one for our fog shenanigans.
self._fog = old_fog
return self._fog
# Guess we need to make a new one.
new_fog = Entity.create('env_fog_controller')
new_fog.fog_enable = True
new_fog.fog_blend = True
new_fog.fog_max_density = 1.0
new_fog.spawn_flags = 1
new_fog.spawn()
new_fog.target_name = 'silent_hill_fog'
# Fix for maps without fog.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).call_input(
'SetFogController', new_fog.target_name)
self._fog = new_fog
return self._fog
def remove_fog(self):
"""Removes the stored 'env_fog_controller' entity.
Note:
This is only used if we're making our own 'env_fog_controller'.
"""
try:
self._fog.remove()
except AttributeError:
return
self._fog = None
def change_fog(self, color1, color2, start, end, final_flash=True):
"""Changes the fog visuals.
Args:
color1 (Color): Primary color of the fog.
color2 (Color): Secondary color of the fog.
start (float): Distance at which the fog begins.
end (float): Distance at which the fog is at its maximum.
final_flash (bool): Is this the final red flash?
"""
fog = self.get_fog_instance()
fog.set_color(color1)
fog.set_color_secondary(color2)
fog.set_start_dist(start)
fog.set_end_dist(end)
# Is this the final flash?
if final_flash:
# Add a stronger flash to mask the changes in fog and screen
# brightness.
UTIL_ScreenFadeAll(FLASH_COLOR_END, 1, 0.5, 1)
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)
# Should we start spawning NPCs?
if NPCS_ENABLED:
# Spawn them only during the thick fog phase.
self.spawn_npcs_think.start(
interval=3, limit=FOG_FADE_DELAY / 3)
def restore_fog(self):
"""Restores the fog back to normal."""
if self._fog is None:
return
# Was the map without fog?
if self.old_fog_values is None:
self.remove_fog()
return
self.change_fog(*self.old_fog_values, False)
def restore_fog_smooth(self, duration):
"""Smoothly restores the fog back to normal over the given duration."""
if self._fog is None:
return
should_remove = False
old_values = self.old_fog_values
# Was the map missing an 'env_fog_controller' entity?
if old_values is None:
should_remove = True
# Just increase the 'start' and 'end' values for the fog before
# removing it - making the transition semi-smooth.
old_values = (FOG_COLOR, FOG_COLOR, 512, 14000)
self._fog.set_color_lerp_to(old_values[0])
self._fog.set_color_secondary_lerp_to(old_values[1])
self._fog.set_start_dist_lerp_to(old_values[2])
self._fog.set_end_dist_lerp_to(old_values[3])
# Add 0.001 to the transition duration to avoid flashes caused by the
# fog bouncing back (engine quirk) before being set in place.
self._fog.fog_lerp_time = duration + 0.001
self._fog.start_fog_transition()
# If we created our own fog, we need to remove it.
if should_remove:
self._delays['ending'] = self._fog.delay(duration, self.remove_fog)
else:
# Make sure the fog values stay put after the transition.
self._delays['ending'] = self._fog.delay(
duration, self.change_fog, (*self.old_fog_values, False))
# The cycle is complete - let's do that again!
self._delays['restart'] = Delay(duration, self.on_completed)
dark_times = DarkTimes()
# =============================================================================
# >> UTIL_SCREENFADEALL - https://git.io/JJXoe
# =============================================================================
server_binary = find_binary('server')
if PLATFORM == 'windows':
identifier_screen = b'\x55\x8B\xEC\xD9\x45\x10\x8D\x45\xF4'
else:
identifier_screen = '_Z18UTIL_ScreenFadeAllRK9color32_sffi'
UTIL_ScreenFadeAll = server_binary[identifier_screen].make_function(
Convention.CDECL,
(DataType.POINTER, DataType.FLOAT, DataType.FLOAT, DataType.INT),
DataType.VOID
)
# =============================================================================
# >> UTIL_GETLOCALPLAYER FIX - (thank you Ayuto)
# viewtopic.php?f=20&t=1907#p12247
# =============================================================================
if PLATFORM == 'windows':
identifier_local = \
b'\xA1\x2A\x2A\x2A\x2A\x8B\x2A\x2A\x83\x2A\x01\x7E\x03\x33\xC0\xC3'
else:
identifier_local = '_Z19UTIL_GetLocalPlayerv'
UTIL_GetLocalPlayer = server_binary[identifier_local].make_function(
Convention.CDECL,
[],
DataType.POINTER
)
@PreHook(UTIL_GetLocalPlayer)
def get_local_player_pre(stack_data):
"""Called when the engine tries to get the local Player object.
This function was designed for single-player and should NOT be called in
multi-player games unless you want the server to crash.
"""
for edict in PlayerGenerator():
try:
return pointer_from_edict(edict)
except ValueError:
pass
return NULL
# =============================================================================
# >> CNPC_BaseZombie::ReleaseHeadcrab() - https://git.io/JUou2
# =============================================================================
if PLATFORM == 'windows':
identifier_release = \
b'\x55\x8B\xEC\x83\xEC\x18\x53\x56\x57\x8B\xF9\x8B\x4D\x08\x80\xBF\x60'
else:
identifier_release = \
'_ZN15CNPC_BaseZombie15ReleaseHeadcrabERK6VectorS2_bbb'
ReleaseHeadcrab = server_binary[identifier_release].make_function(
Convention.THISCALL,
(
DataType.POINTER, DataType.POINTER, DataType.POINTER, DataType.BOOL,
DataType.BOOL, DataType.BOOL),
DataType.VOID
)
@PreHook(ReleaseHeadcrab)
def release_headcrab_pre(stack_data):
"""Called when a zombie is about to release a headcrab."""
# Is the zombie trying to release a headcrab ragdoll?
if stack_data[5]:
# Block the release - stop the ragdoll from spawning.
return False
# =============================================================================
# >> ENV_SPRITE
# =============================================================================
def create_sprite(origin, scale, model):
"""Creates an 'env_sprite' entity.
Args:
origin (Vector): Spawn position of the 'env_sprite'.
scale (float): Size of the sprite (max size: 64.0).
model (Model): Appearance of the sprite.
"""
sprite = Entity.create('env_sprite')
sprite.model = model
sprite.origin = origin
sprite.set_key_value_float('scale', scale)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 0)
sprite.render_amt = 1
sprite.render_mode = RenderMode.TRANS_COLOR
sprite.set_key_value_string('rendercolor', '0 0 0')
sprite.render_fx = RenderEffects.NONE
sprite.spawn_flags = 1
sprite.spawn()
return sprite
# =============================================================================
# >> NPC
# =============================================================================
class NPC(Entity):
"""Class used to create and interact with NPC entities."""
effect_name = 'vortigaunt_hand_glow'
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
# Give the NPC a name.
self.target_name = f'sh_npc_{self.inthandle}'
@staticmethod
def check_space(origin, mins, maxs):
"""Checks if there is enough space at the given origin."""
trace = GameTrace()
engine_trace.clip_ray_to_entity(
Ray(origin, origin, mins, maxs),
ContentMasks.ALL,
Entity(WORLD_ENTITY_INDEX),
trace
)
return not trace.did_hit()
@classmethod
def create(cls, npc_name, origin, seek_after_spawn=True):
"""Creates the NPC entity.
Args:
npc_name (str): Name of the NPC to spawn. (e.g. 'npc_zombie')
origin (Vector): Spawn position of the NPC.
seek_after_spawn (bool): Should the NPC start looking for players
after spawning?
"""
try:
npc = super().create(npc_name)
except ValueError:
# Invalid entity class name.
echo_console(f'ERROR! Attempted to create invalid NPC: {npc_name}')
return
npc.origin = origin
# Make the NPC fully transparent.
npc.render_amt = 1
# Slowly fade it into existence.
npc.set_key_value_int('renderfx', 7)
# Set a couple of spawn flags.
# 2: Gag (No IDLE sounds until angry)
# 256: Long Visibility/Shoot
npc.spawn_flags = 2 + 256
npc.spawn()
npc.become_hostile()
try:
# Are there any alternative models for this NPC?
npc.model = random.choice(NPC_MODELS[npc_name])
except KeyError:
# Nope, moving on..
pass
if seek_after_spawn and npc_name != 'npc_antlion':
# Start going towards a random player after a short delay.
# We need to do this so the NPC doesn't just stand still once it
# spawns. This way it won't block the spawn point.
npc.delay(0.2, npc.seek_random_player)
# Is this an 'npc_manhack' and are glows enabled?
if 'manhack' in npc_name and MANHACK_GLOW_DURATION > 0:
npc.create_spawn_particle(MANHACK_GLOW_DURATION)
return npc
def create_spawn_particle(self, life_time):
"""Creates and parents an 'info_particle_system' to the NPC."""
particle = Entity.create('info_particle_system')
particle.origin = self.origin
particle.effect_name = NPC.effect_name
particle.effect_index = string_tables.ParticleEffectNames.add_string(
NPC.effect_name)
particle.start()
# Parent the particle to the NPC.
particle.set_parent(self)
# Stop the effect before removing it. This will prevent the effect
# from stopping (detaching from the NPC) in place and looking weird.
particle.delay(life_time, particle.stop)
# Remove the particle after a bit of a delay.
particle.delay(life_time + 2, particle.remove)
def become_hostile(self):
"""Makes the NPC hate the players."""
self.call_input('SetRelationship', 'player D_HT 99')
def seek_random_player(self):
"""Starts heading towards a randomly selected player."""
targets = []
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))
# Is this player dead?
if player.dead:
# Skip them.
continue
targets.append(player.target_name)
# Is the list empty?
if not targets:
return
# We need an 'aiscripted_schedule' entity to make the NPC do things.
schedule = Entity.create('aiscripted_schedule')
# Set which entity we'd like to command.
schedule.set_key_value_string('m_iszEntity', self.target_name)
# Set their current state to 'Idle'.
# (0: None, 1: Idle, 2: Alert, 3: Combat)
schedule.set_key_value_int('forcestate', 1)
# Make the NPC set the target as their enemy and start going towards
# them. Here are all the options for this keyvalue:
# 0: <None>
# 1: Walk to Goal Entity
# 2: Run to Goal Entity
# 3: Set enemy to Goal Entity
# 4: Walk Goal Path
# 5: Run Goal Path
# 6: Set enemy to Goal Entity AND Run to Goal Entity
schedule.set_key_value_int('schedule', 6)
# In case the NPC takes damage, stop the schedule.
# (0: General, 1: Damage or death, 2: Death)
schedule.set_key_value_int('interruptability', 1)
# Pick a random player to be the enemy.
schedule.set_key_value_string('goalent', random.choice(targets))
schedule.spawn()
schedule.call_input('StartSchedule')
schedule.delay(5, schedule.remove)
def dissolve(self):
"""Removes the ragdoll of the NPC."""
dissolver = Entity.find_or_create('env_entity_dissolver')
dissolver.dissolve_type = 0
dissolver.dissolve(self.target_name)
def check_headcrab(self):
"""Checks if the headcrab was previously attached to a zombie."""
# Does this headcrab lack an owner? (invalid inthandle)
if self.owner_handle == -1:
# No need to go further.
return
self.become_hostile()
self.delay(0.1, self.seek_random_player)
dark_times.npc_indexes.add(self.index)
# =============================================================================
# >> HELPERS
# =============================================================================
def get_random_direction():
"""Returns a random directional vector without the Z axis (up/down)."""
direction = Vector()
QAngle(0, random.randint(0, 360), 0).get_angle_vectors(forward=direction)
return direction.normalized()
# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)