# ../throwables/throwables.py
# Python
from random import choice
from time import time
# Source.Python
from core import PLATFORM
from effects.base import TempEntity
from engines.precache import Model
from engines.server import global_vars
from engines.sound import Sound
from engines.trace import (ContentMasks, engine_trace, GameTrace, Ray,
SurfaceFlags)
from entities.constants import RenderMode, RenderEffects, WORLD_ENTITY_INDEX
from entities.datamaps import InputData
from entities.entity import Entity
from entities.helpers import index_from_pointer
from entities.hooks import EntityPreHook, EntityCondition
from events import Event
from filters.recipients import RecipientFilter
from listeners import (ButtonStatus, OnButtonStateChanged,
get_button_combination_status)
from mathlib import Vector, QAngle, NULL_VECTOR
from memory import Convention, DataType
from memory.manager import CustomType, TypeManager, Type
from players.constants import PlayerButtons
from players.entity import Player
# How much melee ammo should the player spawn with?
INITIAL_AMMO = 2
# Maximum number of melee ammo the player can carry.
MAX_AMMO = 5
# Should the player be allowed to throw their own melee weapon, leaving them
# without one? (True/False)
LAST_DITCH_EFFORT = True
# Time (in seconds) until the thrown melee despawns/gets removed. (0 - never)
LIFE_TIME = 60
# Material/sprite used for the trail.
TRAIL_MODEL = Model('materials/sprites/laser.vmt')
# Sound that plays when the player is out of melee ammo and tries to throw
# their melee weapon (and LAST_DITCH_EFFORT is set to False).
SOUND_DENIED = Sound('player/suit_denydevice.wav', pitch=90)
# Dictionary used to store relevant information about melee weapons.
# weapon_name: (weapon world model, angle offset, angular impulse)
melee_weapons = {
'weapon_crowbar': (
Model('models/weapons/w_crowbar.mdl'),
QAngle(90, 0, -34),
Vector(0, 520, 360)
),
'weapon_stunstick': (
Model('models/weapons/w_stunbaton.mdl'),
QAngle(90, 0, 0),
Vector(0, 520, 0)
)
}
is_prop_physics_override = EntityCondition.equals_entity_classname(
'prop_physics_override')
manager = TypeManager()
VPHYSICS_COLLISION_OFFSET = 161 if PLATFORM == 'windows' else 162
# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
@OnButtonStateChanged
def on_button_state_changed(player, old_buttons, new_buttons):
"""Called when the button state of a player changed."""
status = get_button_combination_status(
old_buttons, new_buttons, PlayerButtons.ATTACK2)
# Did the player just press right click (+attack2)?
if status == ButtonStatus.PRESSED:
PlayerT(player.index).start_charging_throw()
# Or did they release it?
elif status == ButtonStatus.RELEASED:
PlayerT(player.index).throw()
@Event('player_spawn')
def player_spawn(event):
PlayerT.from_userid(event['userid']).on_spawned()
# =============================================================================
# >> VPHYSICSCOLLISION - (thank you L'In20Cible, Jezza, and Ayuto)
# viewtopic.php?f=20&t=2402
# =============================================================================
class GameVCollisionEvent(CustomType, metaclass=manager):
"""Reconstructed 'gamevcollisionevent_t' struct from the engine."""
pre_velocity = manager.static_instance_array('Vector', 32, 2)
post_velocity = manager.static_instance_array('Vector', 56, 2)
pre_angular_velocity = manager.static_instance_array('Vector', 80, 2)
entity_pointers = manager.static_instance_array(Type.POINTER, 104, 2)
@EntityPreHook(
EntityCondition.equals_entity_classname('worldspawn'),
lambda entity: entity.pointer.make_virtual_function(
VPHYSICS_COLLISION_OFFSET,
Convention.THISCALL,
(DataType.POINTER, DataType.INT, DataType.POINTER),
DataType.VOID
)
)
def vphysics_collision_pre(stack_data):
"""Called when two physical objects (VPhysics) collide."""
event = GameVCollisionEvent._obj(stack_data[2])
index = stack_data[1] ^ 1
try:
throwable = Throwable.cache[index_from_pointer(
event.entity_pointers[index])]
except (KeyError, IndexError, RuntimeError):
# KeyError: Not a Throwable instance.
# IndexError: Missing entity pointer - invalid vphysics collision.
# RuntimeError: Not sure why this happens yet. Pops up when the player
# dies, and a couple of other places.
return
velocity = event.pre_velocity[index]
# Is the throwable going fast enough?
if velocity.length > 950:
start = throwable.origin
end = start + velocity.normalized() * 50
trace = GameTrace()
# Fire a GameTrace() to see where the throwable hit the world.
engine_trace.clip_ray_to_entity(
Ray(start, end),
ContentMasks.ALL,
Entity(WORLD_ENTITY_INDEX),
trace
)
# Did the trace hit the world?
if trace.did_hit():
# Was it the skybox?
if trace.surface.flags & SurfaceFlags.SKY:
# We don't want to stick to the skybox, don't go further.
return
# Convert the throwable's current angles into a directional vector.
forward = Vector()
throwable.angles.get_angle_vectors(forward)
# Let's check how similar the forward and normal vectors are in
# order to determine which part of the throwable hit the world.
# 0: The vectors are perpendicular.
# 1: The vectors are pointing in the same direction.
# -1: The vectors are pointing in the opposite direction.
dot = trace.plane.normal.dot(forward)
if throwable.should_stick(dot):
throwable.stick_to_world(trace.end_position)
# =============================================================================
# >> USE
# =============================================================================
@EntityPreHook(is_prop_physics_override, 'use')
def use_pre(stack_data):
"""Called when a player presses +USE (E by default) on a 'prop_physics'
entity."""
index = index_from_pointer(stack_data[0])
try:
throwable = Throwable.cache[index]
except KeyError:
return
# Is the throwable moving somewhat fast?
if throwable.physics_object.velocity[0].length > 250:
return
if throwable.already_picked_up:
return
# Get the InputData object so we can see who pressed +USE on the throwable.
input_data = InputData._obj(stack_data[1])
player = PlayerT(input_data.activator.index)
# Does the player have full melee ammo?
if player.melee_ammo >= MAX_AMMO:
return
# Is the player missing their melee weapon?
if player.melee_ammo == -1:
player.give_melee_weapon(throwable.weapon_name)
# Increase the player's melee ammo by 1.
player.melee_ammo += 1
# Notify the throwable that it has been picked up.
throwable.on_picked_up()
# =============================================================================
# >> WEAPON SWITCH
# =============================================================================
@EntityPreHook(EntityCondition.is_player, 'weapon_switch')
def weapon_switch_pre(stack_data):
"""Called when a player changes their weapon."""
PlayerT(index_from_pointer(stack_data[0])).stop_charging_throw()
# =============================================================================
# >> THROWABLE
# =============================================================================
class Throwable(Entity):
"""Class used to represent thrown/throwable melee weapons."""
impact_sound = {
'weapon_crowbar': (
'physics/metal/sawblade_stick1.wav',
'physics/metal/sawblade_stick2.wav',
'physics/metal/sawblade_stick3.wav'
),
'weapon_stunstick': (
'weapons/stunstick/stunstick_impact1.wav',
'weapons/stunstick/stunstick_impact2.wav'
)
}
sparks = TempEntity(
temp_entity='Sparks',
magnitude=1,
trail_length=1,
direction=Vector(0, 0, -1)
)
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.already_picked_up = False
self.weapon_name = None
@classmethod
def create(cls, origin, angles, velocity, model, owner_handle, **kwargs):
throwable = super().create('prop_physics_override')
throwable.origin = origin
throwable.angles = angles
throwable.model = model
throwable.owner_handle = owner_handle
# Invalidate the owner handle after a short delay. Without this, the
# player who threw the object won't be able to pick it back up unless
# it got stuck in the world.
throwable.delay(0.3, setattr, (throwable, 'owner_handle', -1))
# Store the 'weapon_name' so we can determine which weapon to give to
# the player in case they are without a melee weapon.
throwable.weapon_name = kwargs.get('weapon_name', None)
# How much damage should entities take from this entity when hit by it?
throwable.set_key_value_float('physdamagescale', 0.45)
# Multiplier for the object's mass.
throwable.set_key_value_float('massscale', 8)
throwable.set_key_value_float('inertiascale', 0.5)
# Make the 'prop_physics_override' sharp.
throwable.set_key_value_int('Damagetype', 1)
# Set certain spawn flags for the entity.
# 64: Enable motion when grabbed by gravity gun.
# 256: Generate output on +USE.
throwable.spawn_flags = 64 + 256
throwable.spawn()
# Setup all of the throw/physics properties so the player counts as the
# attacker when dealing damage with the Throwable.
throwable.set_property_bool('m_bThrownByPlayer', True)
throwable.set_property_bool('m_bFirstCollisionAfterLaunch', True)
throwable.set_property_int('m_hPhysicsAttacker', owner_handle)
throwable.set_property_float(
'm_flLastPhysicsInfluenceTime', global_vars.current_time)
# Add a visual effect.
trail = create_sprite_trail(
origin, TRAIL_MODEL.path, 5, 1, '255 255 255', 0.4
)
trail.set_parent(throwable)
# Set the entity's velocity after a single frame delay.
throwable.delay(0, throwable.apply_velocity, (
velocity, kwargs.get('angular_impulse', NULL_VECTOR)))
# Should we remove the entity after a while?
if LIFE_TIME > 0:
throwable.delay(LIFE_TIME, throwable.remove)
return throwable
def apply_velocity(self, velocity, angular_impulse, impulse_multiplier=1):
"""Sets the Throwable's velocity and angular impulse."""
self.physics_object.set_velocity(
velocity, angular_impulse * impulse_multiplier)
def create_impact_effect(self, origin):
"""Creates sparks at the given origin."""
Throwable.sparks.origin = origin
Throwable.sparks.create(RecipientFilter())
def should_stick(self, dot):
"""Checks whether or not the Throwable should stick to the world."""
# Is this a crowbar?
if 'crowbar' in self.weapon_name:
# Did either the top or bottom of the crowbar hit the world?
if dot > 0.78 or dot < -0.85:
return True
# Checking for everything else (stunstick)..
else:
# Did the top part of the object hit the world?
if dot > 0.78:
return True
# Nothing sharp hit, move along.
return False
def stick_to_world(self, end_position):
"""Freezes and puts the Throwable inside world geometry/brushes."""
# Freeze the entity.
self.call_input('DisableMotion')
# Invalidate the owner handle.
self.owner_handle = -1
direction = end_position - self.origin
# After a short delay, move the Throwable a bit towards where the
# collision took place (stick it inside of world geometry).
self.delay(
0.05, self.teleport, (self.origin + direction.normalized() * 10,))
self.create_impact_effect(end_position)
try:
# Try to get what sound the impact should make.
sample = choice(Throwable.impact_sound[self.weapon_name])
except KeyError:
return
self.emit_sound(
sample=sample,
attenuation=0.5
)
def on_picked_up(self):
"""Called when a player picks up the Throwable with +USE."""
self.already_picked_up = True
self.delay(0, self.remove)
# =============================================================================
# >> PLAYER
# =============================================================================
class PlayerT(Player):
"""Extended Player class.
Args:
index (int): A valid Player index.
caching (bool): Check for a cached instance?
Attributes:
is_charging (bool): Is the player currently preparing to throw their
melee weapon?
melee_ammo (int): Current amount of melee weapons the player can throw.
_pressed_time (float): Time (seconds since the epoch) when the player
pressed +attack2 (right click).
"""
throw_sound = {
'default': 'weapons/slam/throw.wav',
'weapon_crowbar': ('weapons/iceaxe/iceaxe_swing1.wav',),
'weapon_stunstick': (
'weapons/stunstick/stunstick_swing1.wav',
'weapons/stunstick/stunstick_swing2.wav'
)
}
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.is_charging = False
self.melee_ammo = INITIAL_AMMO
self._pressed_time = 0
@property
def charging_time(self):
"""Returns how long the player held their +attack2 for."""
charging_time = time() - self._pressed_time
# Return the clamped value.
return 2 if charging_time > 2 else charging_time
def has_melee(self):
"""Checks if the player has a melee weapon."""
for weapon in self.weapons(is_filters='melee'):
# As soon as we find at least one melee weapon, return True.
return True
else:
# No melee weapons found - return False.
return False
def give_melee_weapon(self, weapon_name):
"""Gives the player the specified melee weapon, if they don't already
have one."""
# Did the player get a melee weapon in the meantime?
if self.has_melee():
# Just increase their ammo by 1.
self.melee_ammo += 1
return
# Give them a melee weapon.
self.give_named_item(weapon_name)
def change_fov(self, target_fov, rate=0.3, starting_fov=None):
"""Changes a player's field of view (FOV) smoothly.
Args:
index (int): A valid player index.
target_fov (int): The new FOV value.
rate (float): Duration of the FOV transition (in seconds).
starting_fov (int): FOV value from which to start the transition.
Raises:
OverflowError: If 'target_fov' is less than 0 or greater than 255.
"""
# Get the player's current FOV.
old_fov = self.fov if starting_fov is None else starting_fov
if old_fov == 0:
# When changing a player's FOV for the first time, the value of
# 'fov' will be zero. So we get the 'default_fov' instead.
old_fov = self.default_fov
# Time when the FOV starts changing.
self.fov_time = global_vars.current_time
# Duration of the transition (in seconds).
self.fov_rate = rate
# The FOV will transition from 'fov_start' to 'fov'.
self.fov_start = old_fov
self.fov = target_fov
def start_charging_throw(self):
"""Begins the throwing process."""
try:
# Try to get the name of the weapon the player is holding.
weapon_name = self.active_weapon.classname
except AttributeError:
# Player might be dead or missing their weapons.
return
# Don't go further if the player isn't holding a melee weapon.
if weapon_name not in melee_weapons:
return
# Is the player out of ammo? (and LAST_DITCH_EFFORT is set to False)
if self.melee_ammo < 1 and not LAST_DITCH_EFFORT:
SOUND_DENIED.play(self.index)
return
# Don't allow the player to use +attack (left click) while charging.
self.active_weapon.set_network_property_float(
'LocalActiveWeaponData.m_flNextPrimaryAttack',
global_vars.current_time + 600
)
self._pressed_time = time()
self.is_charging = True
# Transition the player's field of view to 102 over the next 2 seconds.
self.change_fov(102, 2)
def stop_charging_throw(self):
"""Stops the player from charging the throw."""
if not self.is_charging:
return
self.is_charging = False
self.change_fov(90, 0.1, 90 + int(6 * self.charging_time))
def throw(self):
"""Throws the player's melee weapon."""
if not self.is_charging:
return
self.is_charging = False
charging_time = self.charging_time
weapon = self.active_weapon
# Has the player held +attack2 (right click) for at least 200ms and
# are they holding an actual weapon?
if charging_time > 0.2 and weapon is not None:
# Restore the player's ability to use +attack (left click).
weapon.set_network_property_float(
'LocalActiveWeaponData.m_flNextPrimaryAttack',
global_vars.current_time + 0.05
)
view_vector = self.view_vector
weapon_name = weapon.classname
try:
model, angles, angular_impulse = melee_weapons[weapon_name]
except KeyError:
return
Throwable.create(
origin=self.eye_location + view_vector * 10,
angles=self.view_angle + angles,
velocity=view_vector * (600 + 900 * charging_time),
model=model,
owner_handle=self.inthandle,
angular_impulse=angular_impulse,
weapon_name=weapon_name
)
try:
# Try to get a weapon specific throw sound.
sample = choice(PlayerT.throw_sound[weapon_name])
except KeyError:
# No specific sound found, use the default one.
sample = PlayerT.throw_sound['default']
self.emit_sound(
sample=sample,
attenuation=0.8,
# Adjust the pitch depending on how long the player held their
# +attack2 (right click) for.
pitch=int(90 + 15 * charging_time)
)
# Is the player completely out of ammo?
if self.melee_ammo <= 0:
# Force them to drop their melee weapon.
self.drop_weapon(weapon.pointer, NULL_VECTOR, NULL_VECTOR)
# And remove it.
weapon.remove()
# Does the player not have any other melee weapons?
if not self.has_melee():
# Set their ammo to -1.
self.melee_ammo = -1
else:
# Reduce the ammo by 1.
self.melee_ammo -= 1
# Restore the player's field of view back to normal.
self.change_fov(90, 0.1, 90 + int(6 * charging_time))
def on_spawned(self):
"""Called when the player spawns."""
# Reset the player's ammo.
self.melee_ammo = INITIAL_AMMO
# =============================================================================
# >> ENV_SPRITETRAIL
# =============================================================================
def create_sprite_trail(
origin, sprite_path, start_width, end_width, color_str, life_time):
"""Creates an 'env_spritetrail' entity.
Args:
origin (Vector): Spawn position.
sprite_path (string): Path to the sprite material.
start_width (float): Starting width of the trail.
end_width (float): Ending width of the trail.
color_str (str): String containing the RGB values of the color.
(e.g. '255 255 255' for white)
life_time (float): How long does the trail last before it starts to
fade (in seconds)?
Returns:
Entity: The entity instance of the created 'env_spritetrail'.
"""
trail = Entity.create('env_spritetrail')
trail.sprite_name = sprite_path
trail.origin = origin
trail.life_time = life_time
trail.start_width = start_width
trail.end_width = end_width
trail.render_mode = RenderMode.TRANS_ADD
trail.render_amt = 120
trail.render_fx = RenderEffects.NONE
trail.set_key_value_string('rendercolor', color_str)
trail.spawn()
# Texture resolution of the trail.
trail.texture_res = 0.05
return trail