# ../lights/lights.py
# Python
import random
from colorsys import hsv_to_rgb
# Source.Python
from colors import Color
from effects.base import TempEntity
from effects.hooks import TempEntityPreHook
from entities.constants import EntityEffects, RenderMode, RenderEffects
from entities.dictionary import EntityDictionary
from entities.entity import Entity, BaseEntity
from entities.helpers import index_from_pointer, index_from_inthandle
from entities.hooks import EntityPreHook, EntityCondition
from filters.entities import BaseEntityIter
from filters.weapons import WeaponClassIter
from listeners import OnEntityOutput, OnEntitySpawned, OnEntityDeleted
from listeners.tick import Delay
from mathlib import QAngle
from stringtables import string_tables
class ColorEx(Color):
"""Extended Color class."""
def __init__(self, r, g, b, a=255):
super().__init__(r, g, b, a)
self.raw = r + (g << 8) + (b << 16) + (a << 24)
if self.raw >= 2**31: self.raw -= 2**32
# Colors when the 'prop_combine_ball' bounces and explodes.
COLOR_BALL_BOUNCE = Color(0, 191, 255)
COLOR_BALL_EXPLODE = Color(170, 0, 255)
# Color when the 'crossbow_bolt' bounces.
COLOR_BOLT_BOUNCE = Color(255, 88, 0)
# Colors when the 'npc_grenade_frag' pulses and explodes.
COLOR_FRAG_PULSE = ColorEx(0, 255, 88)
COLOR_FRAG_EXPLODE = ColorEx(0, 255, 88)
# Should the grenade color be random? (0 - no, 1 - yes)
COLOR_FRAG_RANDOM = 1
# Default color for weapons.
COLOR_WEAPON_DEFAULT = Color(255, 25, 0)
# How wide should the 'point_spotlight' beam be?
SPOTLIGHT_DEFAULT_WIDTH = 20
# How tall should the 'point_spotlight' beam be?
SPOTLIGHT_DEFAULT_LENGTH = 300
item_colors = {
'item_battery': Color(255, 255, 25),
'item_healthkit': Color(25, 255, 55),
'item_healthvial': Color(25, 255, 55),
'weapon_default': COLOR_WEAPON_DEFAULT
}
spotlight_pairs = {}
# Angle to make the 'point_spotlight' point upwards.
SPOTLIGHT_ANGLE = QAngle(-90, 0, 0)
# Globalized instances of temporary entities.
light_bolt_bounce = TempEntity('Dynamic Light', radius=200, decay=175,
life_time=3, exponent=8, color=COLOR_BOLT_BOUNCE)
light_ball_bounce = TempEntity('Dynamic Light', radius=200, decay=150,
life_time=3, exponent=8, color=COLOR_BALL_BOUNCE)
light_ball_explode = TempEntity('Dynamic Light', radius=600, decay=600,
life_time=1, exponent=8, color=COLOR_BALL_EXPLODE)
light_frag_pulse = TempEntity('Dynamic Light', radius=200, decay=280,
life_time=0.6, exponent=8, color=COLOR_FRAG_PULSE)
light_frag_explode = TempEntity('Dynamic Light', radius=400, decay=560,
life_time=0.6, exponent=8, color=COLOR_FRAG_EXPLODE)
class Grenade(Entity):
"""Extended Entity class."""
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.glow = BaseEntity(index_from_inthandle(self.main_glow))
self.trail = BaseEntity(index_from_inthandle(self.glow_trail))
self.change_trail_color(COLOR_FRAG_PULSE)
def start_pulsing(self):
"""Starts looping the `self.pulse()` function."""
self.repeat(self.pulse).start(interval=0.75)
def pulse(self):
"""Creates a dynamic light effect at the current origin."""
# Should the color be random?
if COLOR_FRAG_RANDOM:
color = random.choice(grenade_colors)
# Change the color of the glow and trail.
self.change_trail_color(color)
# Change the color of the dynamic light effect.
light_frag_pulse.color = color
# Adjust the origin of the dynamic light effect and create it.
light_frag_pulse.origin = self.origin
light_frag_pulse.create()
def boom(self):
"""Creates a dynamic light effect upon detonation at the current
origin."""
if COLOR_FRAG_RANDOM:
light_frag_explode.color = random.choice(grenade_colors)
light_frag_explode.origin = self.origin
light_frag_explode.create()
def change_trail_color(self, color):
"""Changes the color of the parented sprite and spritetrail."""
self.glow.set_network_property_int('m_clrRender', color.raw)
self.trail.set_network_property_int('m_clrRender', color.raw)
def get_pretty_colors(amount):
"""Returns a list of vibrant colors.
Args:
amount (int): How many colors should be generated?
Returns:
list of ColorEx: A list containing ColorEx instances.
"""
colors = []
step = 1 / amount
for hue in range(0, amount):
colors.append(
ColorEx(*(int(255 * x) for x in hsv_to_rgb(step * hue, 1, 1))))
return colors
# List of vibrant colors.
grenade_colors = get_pretty_colors(amount=25)
# Dictionary that stores 'npc_grenade_frag' instances.
grenade_instances = EntityDictionary(Grenade)
def load():
"""Called when the plugin is loaded."""
# In case of a late plugin load, find the entities that need spotlights.
for base_entity in BaseEntityIter(
{weapon.name for weapon in WeaponClassIter()}.union(
item_colors.keys())):
# Avoid weapons equipped by players.
if base_entity.owner_handle != -1:
continue
# Avoid disabled weapons.
if base_entity.effects & EntityEffects.NODRAW:
continue
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)
def unload():
"""Called when the plugin is unloaded."""
# Go through all the 'point_spotlight' entities and remove them.
for spotlight in spotlight_pairs.values():
spotlight.remove()
# =============================================================================
# >> DYNAMIC_LIGHT: crossbow_bolt, prop_combine_ball, npc_grenade_frag
# =============================================================================
@EntityPreHook(
EntityCondition.equals_entity_classname('npc_grenade_frag'), 'detonate')
def detonate_pre(stack_data):
"""Called when an 'npc_grenade_frag' is about to explode."""
index = index_from_pointer(stack_data[0])
if index in grenade_instances:
grenade_instances[index].boom()
@TempEntityPreHook('EffectDispatch')
def effect_dispatch_pre(temp_entity, recipient_filter):
# Get the name of the effect.
effect_name = string_tables.EffectDispatch[temp_entity.effect_name_index]
# Did the 'prop_combine_ball' bounce?
if 'cball_bounce' in effect_name:
light_ball_bounce.origin = temp_entity.origin
# Delay the effect by a single frame, otherwise the server will crash!
temp_entity.entity.delay(0, light_ball_bounce.create)
# Or did it explode?
if 'cball_explode' in effect_name:
light_ball_explode.origin = temp_entity.origin
temp_entity.entity.delay(0, light_ball_explode.create)
@EntityPreHook(
EntityCondition.equals_entity_classname('crossbow_bolt'), 'start_touch')
def bolt_touch_pre(stack_data):
entity = Entity._obj(stack_data[0])
# Is this a bolt?
if 'crossbow_bolt' in entity.classname:
light_bolt_bounce.origin = entity.origin
light_bolt_bounce.create()
# Try not to freeze the server. (thank you L'In20Cible)
parent = entity.parent
if parent is not None:
parent.start_touch.skip_hooks(stack_data[1])
return False
# =============================================================================
# >> POINT_SPOTLIGHT: items and weapons
# =============================================================================
class Spotlight(Entity):
"""Modified Entity class for properly removing a 'point_spotlight'."""
caching = True
def remove(self):
"""Turns off the 'point_spotlight' before removing it."""
self.call_input('LightOff')
super().remove()
def create_spotlight(origin, color, **kwargs):
"""Creates a 'point_spotlight' at the specified origin.
Args:
origin (Vector): Spawn position of the 'point_spotlight'.
color (Color): Color of the light.
**kwargs: Additional keywords arguments.
"""
spotlight = Spotlight.create('point_spotlight')
spotlight.origin = origin
spotlight.angles = kwargs.get('angle', SPOTLIGHT_ANGLE)
spotlight.color = color
spotlight.render_mode = RenderMode.NORMAL
spotlight.render_amt = 0
spotlight.render_fx = RenderEffects.NONE
spotlight.set_key_value_bool('disablereceiveshadows', False)
spotlight.set_key_value_float('HDRColorScale', 1)
spotlight.set_key_value_int(
'spotlightwidth', kwargs.get('width', SPOTLIGHT_DEFAULT_WIDTH))
spotlight.set_key_value_int(
'spotlightlength', kwargs.get('length', SPOTLIGHT_DEFAULT_LENGTH))
# Make sure the 'point_spotlight' spawns turned on.
spotlight.spawn_flags = 1
spotlight.spawn()
return spotlight
def create_item_spotlight(classname, index, origin):
# Is this a weapon without a set color?
if classname not in item_colors and 'weapon' in classname:
classname = 'weapon_default'
# Is there a color set for this item/weapon?
if classname in item_colors:
# Check if there's a 'point_spotlight' tied to this index already.
try:
# If there is, remove it.
spotlight_pairs.pop(index).remove()
except KeyError:
pass
# Store the 'point_spotlight' instance in a dictionary, but tie it to
# the index of the specified item/weapon.
spotlight_pairs[index] = create_spotlight(
origin, item_colors[classname])
@OnEntityOutput
def on_entity_output(output, activator, caller, value, delay):
# Items (item_battery, item_healthkit) send out 'OnPlayerTouch' whenever
# they are applied to a player, while weapons send out 'OnPlayerPickup'.
if output in ('OnPlayerTouch', 'OnPlayerPickup'):
# Is there a 'point_spotlight' for this item/weapon?
try:
spotlight = spotlight_pairs.pop(caller.index)
except KeyError:
return
# Delay the removal by a single frame to avoid crashing the server.
spotlight.delay(0, spotlight.remove)
@EntityPreHook(
EntityCondition.equals_entity_classname('weapon_frag'), 'materialize')
@EntityPreHook(
EntityCondition.equals_entity_classname('item_battery'), 'materialize')
def materialize_pre(stack_data):
"""Called when an item or weapon becomes enabled."""
base_entity = BaseEntity._obj(stack_data[0])
create_item_spotlight(
base_entity.classname, base_entity.index, base_entity.origin)
@OnEntitySpawned
def on_entity_spawned(base_entity):
"""Called when an entity is spawned."""
try:
index = base_entity.index
except ValueError:
return
# Delay our checks for a single frame. Without this, checking for
# EntityEffects.NODRAW doesn't seem to work.
Delay(0, on_actual_entity_spawned, (index,))
def on_actual_entity_spawned(index):
try:
base_entity = BaseEntity(index)
except ValueError:
return
# Is this a grenade?
if 'npc_grenade_frag' in base_entity.classname:
grenade_instances[index].start_pulsing()
return
# Is this a weapon?
if 'weapon' in base_entity.classname:
# Does it have an owner? (player equipped it)
if base_entity.owner_handle != -1:
return
# When a player picks up a map placed weapon, another one gets spawned
# right away, but it's disabled. This line helps avoid spawning
# another 'point_spotlight' for the disabled weapon.
# More information: https://git.io/JfIFH
if base_entity.effects & EntityEffects.NODRAW:
return
create_item_spotlight(
base_entity.classname, index, base_entity.origin)
@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity is removed."""
try:
index = base_entity.index
except ValueError:
return
# Does this entity have a 'point_spotlight' tied to it?
try:
spotlight_pairs.pop(index).remove()
except KeyError:
pass