s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=wurstsoftware.koth&take=20
Showing code results for query:
*
(198 total matches found)
Game
game
global using Sandbox; global using Sandbox.UI; global using System; global using System.Collections.Generic; global using System.Linq;
Game
game
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Events;
using Sandbox.Utility;
using System.ComponentModel.DataAnnotations;
using static Sandbox.PhysicsContact;
namespace KOTH;
// TODO : rename health manager
public sealed class DamageManager : SingletonComponent<DamageManager>,
IGameEventHandler<DamageRequestEvent>,
IGameEventHandler<HealingRequestEvent>
{
[Property] public SoundEvent HitSound { get; set; }
// special bool zone
[Property] public bool KnockbackOnly { get; private set; } = false;
[Property] public bool IgnoreTeams { get; private set; } = false;
const float SelfDamageMultiplyer = 0.25f;
/////////////////////////////////////////////////////////////////////////////////////////////
public void OnGameEvent(HealingRequestEvent EventArgs)
{
var HealingRequest = EventArgs.HealingRequest;
if (!HealingRequest.IsValid())
{
Log.Warning($"tried to parse HealingRequestEvent that is not valid {EventArgs} {this}");
return;
}
ServerInflictHealing(HealingRequest);
}
public void OnGameEvent(DamageRequestEvent EventArgs)
{
var DamageRequest = EventArgs.DamageRequest;
if (!DamageRequest.IsValid())
{
Log.Warning($"tried to parse DamageRequestEvent that is not valid {EventArgs} {this}");
return;
}
// tell server to do damage from request
ServerInflictDamageToPlayer(DamageRequest);
}
/////////////////////////////////////////////////////////////////////////////////////////////
// host only broadcasts
/////////////////////////////////////////////////////////////////////////////////////////////
[Rpc.Host]
private void ServerInflictDamageToPlayer(FDamageRequest DamageRequest)
{
Assert.True(Networking.IsHost);
var DamageOrigin = DamageRequest.DamageOrigin;
var TargetDamageComponent = DamageRequest.TargetDamageComponent;
var AttackerPlayerPawn = DamageRequest.AttackerPlayerPawn;
var Damage = DamageRequest.BaseDamage;
if (!TargetDamageComponent.IsValid() || TargetDamageComponent.Health <= 0)
{
return;
}
var TargetPlayerPawn = DamageRequest.TargetDamageComponent.GetComponent<PlayerPawn>();
// we've taken damage without an attacker pawn, apply & return early ///////////////////
if (!AttackerPlayerPawn.IsValid())
{
FDamageTaken EnvDamageTaken = new()
{
VictimGameObject = TargetDamageComponent.GameObject,
AssumedVictimPlayerPawn = TargetPlayerPawn,
VictimPlayerState = GameUtils.GetPlayerState(TargetPlayerPawn?.Id),
Damage = Damage,
DamageLocation = DamageOrigin,
DamageType = DamageRequest.DamageType,
IsDummyDamage = TargetPlayerPawn.IsValid() && TargetPlayerPawn.IsDummy,
};
TargetDamageComponent.TakeDamage(EnvDamageTaken);
return; // NOTE : early return
}
// team check //////////////////////////////////////////////////////////////////////////////////////
if (!IgnoreTeams)
{
if (TargetDamageComponent.Team == AttackerPlayerPawn.Team && TargetPlayerPawn != AttackerPlayerPawn)
{
return; // NOTE : early return
}
}
var TargetCenter = DamageRequest.TargetOrigin;
// calculate damage ///////////////////////////////////////////
if (DamageRequest.DamageFalloffType != EDamageFalloffType.None)
{
var TargetToImpactDistance = TargetCenter.Distance(DamageOrigin);
var TargetToAttackerDistance = TargetCenter.Distance(AttackerPlayerPawn.CenterPosition);
float MaxDamage = Damage;
if (DamageRequest.DamageType == EDamageType.Projectile && !DamageRequest.DirectImpact)
{
var DamageDistanceLerp = TargetToImpactDistance / DamageRequest.MaxDamageImpactDistance;
MaxDamage = MathX.Lerp(MaxDamage, MaxDamage * 0.5f, DamageDistanceLerp);
}
float MinDamage = MaxDamage * 0.33f;
float DamageLerp = TargetToAttackerDistance / 1600f;
if (DamageRequest.DamageFalloffType == EDamageFalloffType.Falloff)
{
Damage = MathX.Lerp(MaxDamage, MinDamage, DamageLerp).FloorToInt();
}
else
{
Damage = MathX.Lerp(MinDamage, MaxDamage, DamageLerp).FloorToInt();
}
}
// knockback ////////////////
var Knockback = Vector3.Zero;
if (TargetPlayerPawn.IsValid())
{
var DirectionVec = (TargetCenter - DamageOrigin).Normal;
var CrouchFactor = TargetPlayerPawn.IsCrouching ? 62 : 82;
var KBStrength = DamageRequest.BaseKnockbackStrength;
if (!TargetPlayerPawn.IsGrounded && TargetPlayerPawn == AttackerPlayerPawn)
{
KBStrength *= 1.33f;
}
var KnockbackFactor = Damage * (KBStrength / CrouchFactor);
Knockback = DirectionVec * KnockbackFactor * TargetPlayerPawn.WeightFactor;
TargetPlayerPawn.DoKnockback(Knockback);
}
if (KnockbackOnly)
{
return; // !
}
// deal the damage /////////////////////////////////////////
bool WasSelfDamage = TargetPlayerPawn == AttackerPlayerPawn;
if (WasSelfDamage && DamageRequest.DoesLessSelfDamage)
{
Damage = (Damage * SelfDamageMultiplyer).FloorToInt();
}
FDamageTaken DamageTaken = new()
{
VictimGameObject = TargetDamageComponent.GameObject,
AssumedAttackerPlayerPawn = AttackerPlayerPawn,
AssumedVictimPlayerPawn = TargetPlayerPawn,
AttackerPlayerState = GameUtils.GetPlayerState(AttackerPlayerPawn?.Id),
VictimPlayerState = GameUtils.GetPlayerState(TargetPlayerPawn?.Id),
Damage = Damage,
DamageLocation = DamageOrigin,
DamageType = DamageRequest.DamageType,
IsDummyDamage = TargetPlayerPawn.IsValid() && TargetPlayerPawn.IsDummy,
};
TargetDamageComponent.TakeDamage(DamageTaken);
Log.Info($"{Damage}:{Knockback.Length:0.0} damage:kb has been taken {AttackerPlayerPawn.DisplayName}:{AttackerPlayerPawn.Health}" +
$" -> {TargetPlayerPawn?.DisplayName}:{TargetDamageComponent.Health}");
}
[Rpc.Host]
private void ServerInflictHealing(FHealingRequest HealingRequest)
{
Assert.True(Networking.IsHost);
if (!HealingRequest.TargetDamageComponent.IsValid())
{
return;
}
// we do it the other way round to the damage here :)
HealingRequest.TargetDamageComponent.Heal(HealingRequest);
}
[Rpc.Broadcast(NetFlags.HostOnly)]
private void ClientDidDamage(PlayerState PlayerState, PlayerPawn PlayerPawn, float Damage, bool WasSelfDamage)
{
if (!WasSelfDamage)
{
var HitSound = Sound.Play(this.HitSound, PlayerPawn.WorldPosition);
if (!HitSound.IsValid()) return;
HitSound.Volume = 0.5f;
HitSound.Pitch = MathX.Lerp(1.2f, 0.7f, Damage * .02f);
HitSound.ListenLocal = true;
}
}
}
Game
game
@import "/UI/Styles/Theme.scss";
StickyOverlay {
* {
position: absolute;
}
.charge {
position: absolute;
top: 80%;
left: 50%;
width: 200px;
height: 8px;
background-color: rgba(0, 0, 0, 0.6);
.charge-fill {
height: 100%;
background-color: whitesmoke;
}
transform: translateX(-48px);
}
.stickies {
position: absolute;
top: 75%;
left: 70%;
color: white;
font-size: 36px;
}
}
Game
game
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Sandbox.Events;
/// <summary>
/// Interface for event payloads that can be listened for by <see cref="IGameEventHandler{T}"/>s.
/// </summary>
public interface IGameEvent { }
/// <summary>
/// Interface for components that handle game events with a payload of type <see cref="T"/>.
/// </summary>
/// <typeparam name="T">Event payload type.</typeparam>
public interface IGameEventHandler<in T>
where T : IGameEvent
{
/// <summary>
/// Called when an event with payload of type <see cref="T"/> is dispatched on a <see cref="GameObject"/>
/// that contains this component, including on a descendant.
/// </summary>
/// <param name="eventArgs">Event payload.</param>
void OnGameEvent( T eventArgs );
}
/// <summary>
/// Helper for dispatching game events in a scene.
/// </summary>
public static class GameEvent
{
private static Dictionary<Type, IReadOnlyDictionary<Type, int>> HandlerOrderingCache { get; } = new();
/// <summary>
/// Notifies all <see cref="IGameEventHandler{T}"/> components that are within <paramref name="root"/>,
/// with a payload of type <typeparamref name="T"/>.
/// </summary>
public static void Dispatch<T>( this GameObject root, T eventArgs )
where T : IGameEvent
{
var handlers = (root is Scene scene
? scene.GetAllComponents<IGameEventHandler<T>>() // I think this is more efficient?
: root.Components.GetAll<IGameEventHandler<T>>())
.ToArray();
if ( !HandlerOrderingCache.TryGetValue( typeof(T), out var ordering ) || handlers.Any( x => !ordering.ContainsKey( x.GetType() ) ) )
{
ordering = HandlerOrderingCache[typeof(T)] = GetHandlerOrdering<T>();
}
List<Exception>? exceptions = null;
foreach ( var handler in handlers.OrderBy( x => ordering[x.GetType()] ) )
{
try
{
handler.OnGameEvent( eventArgs );
}
catch ( Exception e )
{
exceptions ??= new();
exceptions.Add( e );
}
}
switch ( exceptions?.Count )
{
case 1:
Log.Error( exceptions[0] );
break;
case > 1:
Log.Error( new AggregateException( exceptions ) );
break;
}
}
private static bool IsImplementingMethodName( string methodName )
{
if ( methodName == nameof(IGameEventHandler<IGameEvent>.OnGameEvent) )
{
return true;
}
return methodName.StartsWith( "Sandbox.Events.IGameEventHandler<" ) && methodName.EndsWith( ">.OnGameEvent" );
}
private static MethodDescription? GetImplementation<T>( TypeDescription type )
{
foreach ( var method in type.Methods )
{
if ( method.IsStatic ) continue;
if ( method.Parameters.Length != 1 ) continue;
if ( method.Parameters[0].ParameterType != typeof( T ) ) continue;
if ( !IsImplementingMethodName( method.Name ) ) continue;
return method;
}
return null;
}
private static IReadOnlyDictionary<Type, int> GetHandlerOrdering<T>()
where T : IGameEvent
{
var types = TypeLibrary.GetTypes<IGameEventHandler<T>>().ToArray();
var helper = new SortingHelper( types.Length );
for ( var i = 0; i < types.Length; ++i )
{
var type = types[i];
var method = GetImplementation<T>( type );
if ( method is null )
{
Log.Warning( $"Can't find {nameof( IGameEventHandler<T> )}<{typeof( T ).Name}> implementation in {type.Name}!" );
continue;
}
foreach ( var attrib in method.Attributes )
{
switch ( attrib )
{
case EarlyAttribute:
helper.AddFirst( i );
break;
case LateAttribute:
helper.AddLast( i );
break;
case IBeforeAttribute before:
for ( var j = 0; j < types.Length; ++j )
{
if ( i == j ) continue;
var other = types[j];
if ( before.Type.IsAssignableFrom( other.TargetType ) )
{
helper.AddConstraint( i, j );
}
}
break;
case IAfterAttribute after:
for ( var j = 0; j < types.Length; ++j )
{
if ( i == j ) continue;
var other = types[j];
if ( after.Type.IsAssignableFrom( other.TargetType ) )
{
helper.AddConstraint( j, i );
}
}
break;
}
}
}
var ordering = new List<int>();
if ( !helper.Sort( ordering, out var invalid ) )
{
Log.Error( $"Invalid event ordering constraint between {types[invalid.EarlierIndex].Name} and {types[invalid.LaterIndex].Name}!" );
return ImmutableDictionary<Type, int>.Empty;
}
return Enumerable.Range( 0, ordering.Count )
.ToImmutableDictionary( i => types[ordering[i]].TargetType, i => i );
}
}
public delegate void GameEventAction<in T>( T eventArgs )
where T : IGameEvent;
/// <summary>
/// Base class for components that expose game events to Action Graph.
/// </summary>
public abstract class GameEventComponent<T> : Component, IGameEventHandler<T>
where T : IGameEvent
{
/// <summary>
/// Action invoked when the <typeparamref name="T"/> event is dispatched.
/// </summary>
[Property]
public GameEventAction<T>? OnEvent { get; set; }
/// <summary>
/// If this component is within a state machine, optional state to transition
/// to when this event is dispatched.
/// </summary>
[Property]
public StateComponent? NextState { get; set; }
void IGameEventHandler<T>.OnGameEvent( T eventArgs )
{
OnEvent?.Invoke( eventArgs );
if ( NextState is not null )
{
Components.GetInAncestorsOrSelf<StateMachineComponent>()?.Transition( NextState );
}
}
}
Game
game
using System.Threading.Tasks;
namespace KOTH;
/// <summary>
/// Manages sound playback within the game, including playing sounds globally or for specific teams,
/// updating sound states, and handling active sound instances.
/// </summary>
/// <remarks>
/// This component is responsible for managing the lifecycle of sounds,
/// including their initiation, positional updates, and cleanup. It provides
/// methods for playing sounds either globally or targeted to specific teams
/// and maintains references to active sound handles.
/// </remarks>
public sealed class AudioComponent : SingletonComponent<AudioComponent>
{
/// <summary>
/// A collection of sound events that have been stopped during the audio handling process.
/// This list is managed internally and is primarily used to track and clean up finished or stopped sounds,
/// ensuring efficient audio management within the system.
/// </summary>
[Property]
private List<FSound> StoppedSounds { get; set; }
/// <summary>
/// A mapping between sound events and their corresponding sound handles used during audio processing.
/// This dictionary is utilized to associate each active sound event with its control and state information,
/// enabling management of sound playback, updates, and cleanup within the system.
/// </summary>
[Property]
private Dictionary<FSound, SoundHandle> SoundHandles { get; set; }
protected override void OnAwake()
{
base.OnAwake();
SoundHandles = new Dictionary<FSound, SoundHandle>();
StoppedSounds = new List<FSound>();
}
/// <summary>
/// Plays a specified sound for all players on the given team.
/// </summary>
/// <param name="soundEvent">The sound event to be played.</param>
/// <param name="team">The team for which the sound will be played.</param>
public void PlaySoundForTeam(FSound soundEvent, Team team)
{
using (Rpc.FilterInclude(GameUtils.GetPlayers(team).Select(x => x.Connection)))
{
PlaySound(soundEvent);
}
}
/// <summary>
/// Plays a specified sound globally for all players in the game.
/// </summary>
/// <param name="soundEvent">The sound event to be played.</param>
[Rpc.Broadcast]
public void PlaySound(FSound soundEvent)
{
var alreadyPlaying = Instance.SoundHandles.Any(x => x.Key.Owner.Id == soundEvent.Owner.Id || x.Key.SoundId == soundEvent.SoundId);
if (alreadyPlaying) return;
Log.Info("Playing sound " + soundEvent.SoundId);
var handle = Sound.Play(soundEvent.SoundEvent, soundEvent.Position);
Instance.SoundHandles.Add(soundEvent, handle);
}
/// <summary>
/// Stops the playback of a specified sound in the game.
/// </summary>
/// <param name="soundEvent">The sound event to be stopped.</param>
[Rpc.Broadcast]
public void StopSound(FSound soundEvent)
{
var soundHandle = Instance.SoundHandles.FirstOrDefault(x => x.Key.SoundId == soundEvent.SoundId);
var alreadyRemoving = Instance.StoppedSounds.Any(x => x.SoundId == soundEvent.SoundId);
if(soundHandle.Value == null || alreadyRemoving) return;
soundHandle.Value.Stop();
Instance.StoppedSounds.Add(soundHandle.Key);
}
/// <summary>
/// Determines whether the specified sound event is currently playing.
/// </summary>
/// <param name="soundEvent">The sound event to check.</param>
/// <returns>True if the sound event is currently playing; otherwise, false.</returns>
public Boolean IsPlayingSound(FSound soundEvent)
{
return Instance.SoundHandles.Any(x => x.Key.SoundId == soundEvent.SoundId);
}
/// <summary>
/// Updates the positions of active sounds and removes references to completed or invalid ones.
/// </summary>
/// <remarks>
/// This method iterates through all active sound handles. If a sound should update its position,
/// its position is recalculated. Stopped, finished, or invalid sound handles are marked for cleanup
/// and later removed.
/// </remarks>
public void SoundTick()
{
foreach (var soundHandle in Instance.SoundHandles)
{
if (!soundHandle.Key.Owner.IsValid())
{
soundHandle.Value.Stop();
Instance.StoppedSounds.Add(soundHandle.Key);
continue;
}
if (soundHandle is { Value: not null, Value.Finished: false, Key.UpdatePosition: true, }
&& soundHandle.Value.Position != soundHandle.Key.Owner.WorldPosition
)
{
soundHandle.Value.Position = soundHandle.Key.Owner.WorldPosition;
}
if (!soundHandle.Value.IsValid() || soundHandle.Value.Finished || soundHandle.Value.IsStopped)
{
Instance.StoppedSounds.Add(soundHandle.Key);
}
}
CleanUpStoppedSounds();
}
/// <summary>
/// Cleans up the references to sounds that have been stopped, finished, or are no longer valid.
/// </summary>
/// <remarks>
/// This method iterates through the list of stopped sounds, attempting to remove their corresponding entries
/// from the active sound handles dictionary. Logs errors or successful removals as appropriate, then clears
/// the list of stopped sounds.
/// </remarks>
private void CleanUpStoppedSounds()
{
foreach (var soundHandle in Instance.StoppedSounds)
{
Instance.SoundHandles.Remove(soundHandle);
}
Instance.StoppedSounds.Clear();
}
}
/// <summary>
/// Represents a sound event within the game.
/// This struct is used to encapsulate data necessary to play a sound, including its location,
/// the associated sound event, and the owning component triggering the sound.
/// </summary>
public struct FSound : IEquatable<FSound>
{
/// <summary>
/// Represents the underlying data or identity of a playable sound within the audio system.
/// </summary>
public SoundEvent SoundEvent { get; init; }
/// <summary>
/// Represents the 3D position in the game world where the sound event should occur.
/// </summary>
public Vector3 Position { get; init; }
/// <summary>
/// A unique identifier for a specific sound event within the game.
/// This identifier is automatically generated upon the creation of a sound event and is used
/// to distinguish and manage individual sound instances effectively.
/// </summary>
public Guid SoundId { get; init; }
/// <summary>
/// The component responsible for triggering the sound event in the game.
/// </summary>
public Component Owner { get; init; }
/// <summary>
/// Indicates whether the position of the sound should be updated dynamically.
/// </summary>
/// <remarks>
/// If set to true, the sound's position is recalculated during the sound management process
/// to match the world position of its owning component. This is particularly useful for
/// sounds tied to moving objects or entities within the game world. If false, the sound
/// remains static at its initial position.
/// </remarks>
public bool UpdatePosition { get; init; } = false;
public FSound(SoundEvent soundEvent, Vector3 position, Component owner, bool updatePosition = false)
{
SoundEvent = soundEvent;
Position = position;
Owner = owner;
UpdatePosition = updatePosition;
SoundId = Guid.NewGuid();
}
public bool Equals(FSound other)
{
return other.SoundId == SoundId;
}
public override bool Equals(object obj)
{
return obj is FSound other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(SoundEvent, UpdatePosition, SoundId);
}
}
Game
game
using Sandbox;
namespace KOTH;
public sealed class ClassList : Component
{
[Property] public List<CharacterDefinition> ClassDefinitions { get; private set; }
}
Game
game
using Sandbox;
using Sandbox.Events;
namespace KOTH;
public sealed class HillController : Component, IGameEventHandler<EnterStateEvent>
{
[Property] public bool HillEnabled { get; set; } = false;
[Sync(SyncFlags.FromHost)] public Hill ActiveHill { get; private set; }
void IGameEventHandler<EnterStateEvent>.OnGameEvent(EnterStateEvent eventArgs)
{
GetActiveHill()?.SetHillActive(HillEnabled);
}
public Hill GetActiveHill()
{
if (ActiveHill.IsValid())
{
return ActiveHill;
}
foreach (var GameObject in Scene.GetAllObjects(true))
{
var Hill = GameObject.Components.Get<Hill>();
if (Hill.IsValid())
{
ActiveHill = Hill;
break;
}
}
return ActiveHill;
}
}
Game
game
namespace KOTH;
// tony: Not a huge fan of this, can't we be reacting to trigger events instead of tracing against spheres for every player every update?
// or just do this on the local player, and tell the host about what zones they're in?
// travis : I AGREE
partial class PlayerPawn
{
private readonly List<Zone> _zones = new();
/// <summary>
/// Which <see cref="Zone"/>s is the player currently standing in.
/// </summary>
public IEnumerable<Zone> Zones => _zones;
/// <summary>
/// Update which <see cref="Zone"/>s the player is standing in.
/// </summary>
private void UpdateZones()
{
_zones.Clear();
_zones.AddRange(Zone.GetAt(WorldPosition));
}
public T GetZone<T>()
{
return Zones.Select(x => x.Components.Get<T>()).FirstOrDefault(x => x is not null);
}
}
Game
game
using KOTH.UI;
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Events;
using System.Collections.Generic;
namespace KOTH;
public partial class PlayerState
{
// TODO : create connection -> player state map?
public static PlayerState Local { get; private set; }
public static bool IsPaused { get; set; }
protected override void OnUpdate()
{
base.OnUpdate();
CameraUtils.LocalTick();
if (Input.EscapePressed)
{
Input.EscapePressed = false;
IsPaused = !IsPaused;
}
}
}
Game
game
using KOTH.UI;
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Events;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
namespace KOTH;
public struct FPlayerPawnDefinition
{
public FPlayerPawnDefinition()
{
}
public CharacterDefinition CharacterDefinition { get; init; }
// public PlayerState OwnerPlayerState { get; init; }
public string Name { get; init; } = "UNINITALIZED";
public Team Team { get; init; } = Team.Unassigned;
public bool IsDummy { get; init; } = false;
public readonly bool IsValid()
{
return CharacterDefinition.IsValid();
}
}
//////////////////////////////////////////////////////////////////////////////////
/*
* State of the player pawn
* menu when displaying a menu (true when first joining a lobby)
* waitingforspawn when in a game & requestedcharacterdefinition is not null
* alive when playing in game
*/
public enum EPlayerStateSpawningState
{
MainMenu,
WaitingForSpawn,
InstantSpawn,
Alive,
Spectating,
}
//////////////////////////////////////////////////////////////////////////////////
public partial class PlayerState
{
[Sync(SyncFlags.FromHost)/*, Change(nameof(OnPlayerStateSpawningStateChanged))*/] public EPlayerStateSpawningState PlayerStateSpawningState { get; private set; } = EPlayerStateSpawningState.MainMenu;
//////////////////////////////////////////////////////////////////////////////////
[Sync(SyncFlags.FromHost)] public float TimeTilAttemptedSpawn { get; private set; } = -1;
public void SetTimeTilAttemptedSpawn(float TimeTilSpawn)
{
Assert.True(Networking.IsHost);
TimeTilAttemptedSpawn = TimeTilSpawn;
}
//////////////////////////////////////////////////////////////////////////////////
/*
* NOTE : Sync will only care about the vaule given by an object's host, so bc this player state is owned
* by the server the requested character def will never sync! this is the workaround for now..
*/
public CharacterDefinition RequestedCharacterDefinition { get; private set; } = null;
public void RequestCharacterDefinition(CharacterDefinition CharacterDefintionIn)
{
Log.Info($"Requested Character {CharacterDefintionIn}");
RequestedCharacterDefinition = CharacterDefintionIn;
// TODO : actaully send a request
HostSetCharacterDefinition(CharacterDefintionIn);
}
[Rpc.Host]
private void HostSetCharacterDefinition(CharacterDefinition CharacterDefinition)
{
if (!CharacterDefinition.IsValid())
{
return;
}
RequestedCharacterDefinition = CharacterDefinition;
// if we come from the main menu we want to spawn instantly
if (PlayerStateSpawningState == EPlayerStateSpawningState.MainMenu)
{
PlayerStateSpawningState = EPlayerStateSpawningState.InstantSpawn;
return;
}
PlayerStateSpawningState = EPlayerStateSpawningState.WaitingForSpawn;
}
//////////////////////////////////////////////////////////////////////////////////
[Sync(SyncFlags.FromHost)] public Team Team { get; set; }
//////////////////////////////////////////////////////////////////////////////////
public void SpawnPlayerPawn(TeamSpawnPoint SpawnPoint)
{
Assert.True(Networking.IsHost);
if (PlayerPawn.IsValid())
{
PlayerPawn.GameObject.Root.Destroy();
PlayerPawn = null;
LocalOnPlayerDeath();
GameMode.Instance?.DestroySpawnZoneBlockers();
}
if (!SpawnPoint.IsValid())
{
Log.Warning($"trying to spawn player {this} with invalid spawn point");
return;
}
SpawnPlayerPawn(Connection, SteamName, RequestedCharacterDefinition, SpawnPoint);
}
[Property] public GameObject DefaultPlayerPawnPrefab { get; private set; }
private void OnPlayerPawnSpawn()
{
Assert.True(Networking.IsHost);
Assert.True(PlayerPawn.IsValid());
PlayerPawn.OnDeath += OnPlayerPawnDeath;
PlayerStateSpawningState = EPlayerStateSpawningState.Alive;
BroadcastPlayerSpawn(PlayerPawn);
using (Rpc.FilterInclude(Connection))
{
LocalPlayerSpawn(PlayerPawn);
}
}
[Rpc.Host]
private void SpawnPlayerPawn(Connection OwningConnection, string Name, CharacterDefinition CharacterDefinition, TeamSpawnPoint SpawnPoint)
{
Assert.True(Networking.IsHost);
Assert.IsValid(SpawnPoint);
Log.Info($"attempting to spawn player {RequestedCharacterDefinition} for {OwningConnection}");
var SpawnPlayerPawnPrefab = DefaultPlayerPawnPrefab.Clone(SpawnPoint.GameObject.WorldTransform, null, true);
SpawnPlayerPawnPrefab.Network.SetOrphanedMode(NetworkOrphaned.Destroy);
var SpawnPlayerPawnComponent = SpawnPlayerPawnPrefab.Components.Get<PlayerPawn>();
Assert.NotNull(SpawnPlayerPawnComponent);
var TeamIn = Team; // :P
FPlayerPawnDefinition PlayerPawnDefinition = new()
{
CharacterDefinition = RequestedCharacterDefinition,
Name = Name,
Team = TeamIn,
};
SpawnPlayerPawnComponent.SetPlayerPawnDefinition(PlayerPawnDefinition);
if (!SpawnPlayerPawnPrefab.NetworkSpawn(OwningConnection))
{
SpawnPlayerPawnPrefab.Destroy();
return;
}
PlayerPawn = SpawnPlayerPawnComponent;
OnPlayerPawnSpawn();
}
void OnPlayerPawnDeath()
{
Assert.True(Networking.IsHost);
PlayerStateSpawningState = EPlayerStateSpawningState.WaitingForSpawn;
using (Rpc.FilterInclude(Connection))
{
LocalOnPlayerDeath();
}
}
//////////////////////////////////////////////////////////////////////////////////
[Rpc.Broadcast]
private void BroadcastPlayerSpawn(PlayerPawn PlayerPawn)
{
Scene.Dispatch(new PlayerSpawnedEvent(PlayerPawn));
}
[Rpc.Broadcast] // broadcast filter
private void LocalPlayerSpawn(PlayerPawn PlayerPawn)
{
Scene.Dispatch(new LocalPlayerSpawnedEvent(PlayerPawn));
}
[Rpc.Broadcast] // broadcast filter
public void LocalOnPlayerDeath()
{
Scene.Dispatch(new LocalPlayerDiedEvent());
}
}
Game
game
@namespace KOTH.UI
@using KOTH.Notification
@using Sandbox.Events
@inherits PanelComponent
@if (!DeathCameraData.IsValid())
{
return;
}
<root>
<div class="dead">
killed by:
</div>
<div class="name">
@DeathCameraData.KillerName
</div>
<div class="hp">
@DeathCameraData.KillerHealth HP
</div>
</root>
@code
{
public static PlayerDeathHUD Instance { get; set; }
public PlayerDeathHUD()
{
Instance = this;
}
public FDeathCameraData DeathCameraData { get; set; }
public void SetData(FDeathCameraData Data)
{
DeathCameraData = Data;
}
protected override void OnEnabled()
{
base.OnEnabled();
}
protected override void OnUpdate()
{
base.OnUpdate();
}
protected override int BuildHash()
{
return HashCode.Combine(Time.Now);
}
}
Game
game
@* @namespace KOTH.UI
@inherits Panel
@attribute [StyleSheet]
@if (!Player.IsValid() || !EngiePlayer.IsValid() || EngiePlayer.ActiveTurretComponent == null)
{
return;
}
<root>
<label class="building">@(EngiePlayer.ActiveTurretComponent.DamageComponent.Health)/@(EngiePlayer.ActiveTurretComponent.DamageComponent.MaxBaseHealth)</label>
</root>
@code
{
public PlayerPawn Player => PlayerState.Local.PlayerPawn;
public EngiePlayer EngiePlayer => Player.GameObject.GetComponent<EngiePlayer>();
protected override int BuildHash()
{
if (!Player.IsValid())
return 0;
return HashCode.Combine(Time.Now);
}
}
*@
Game
game
@namespace KOTH.UI
@attribute [StyleSheet]
@inherits Panel
<root class="">
@* @foreach (var voice in VoiceList.Where(x => CanDisplay(x)))
{
<div class="row">
<div class="item-row with-background">
@{
var player = GetPlayer(voice);
}
<Icon class="square rounded shrink-0" File="/ui/chat/radio.png" size="24" style="transform: scale( @GetAmplitudeScale( voice ) )" />
<img class="avatar" src="avatar:@player.SteamId" />
<PlayerPanel PlayerState=@player />
</div>
</div>
} *@
</root>
@code
{
public IEnumerable<PlayerVoiceComponent> VoiceList => Scene.GetAllComponents<PlayerVoiceComponent>().Where(x => x.PlayerState.IsConnected);
private PlayerState GetPlayer(Voice voice)
{
return voice.Components.Get<PlayerState>();
}
private bool CanDisplay(Voice voice)
{
return voice.LastPlayed < 0.25f;
}
private string GetAmplitudeScale(Voice voice)
{
var scale = 0.5f + (voice.Amplitude * 2.365f);
return (1f + scale).ToString("0.#");
}
protected override int BuildHash()
{
return HashCode.Combine(Time.Now);
}
}
Game
game
@namespace KOTH.UI
@inherits Panel
@attribute [StyleSheet]
@if ( !Player.IsValid() )
return;
<root>
<div class="circle"></div>
</root>
@code
{
/// The player
public PlayerPawn Player => PlayerState.Local.PlayerPawn;
protected override int BuildHash()
{
if ( !Player.IsValid() || !Player.CurrentEquipment.IsValid() )
return 0;
return HashCode.Combine(Player, Player.CurrentEquipment);
}
}
Game
game
using KOTH.UI;
using Sandbox;
using Sandbox.Events;
using System.Net.Http;
using System;
using System.Text;
using Sandbox.Diagnostics;
using System.Data.Common;
namespace KOTH;
// TODO : this piece of shit needs a proper refactor
[Title("Projectile Shooter"), Group("Weapon Components")]
public class ProjectileWeaponComponent : InputWeaponComponent
{
[Property, Group("Projectile")] public GameObject ProjectilePrefab { get; set; }
[Property, Group("Projectile")] public float ProjectileHorizontalSpeed { get; set; } = 600.0f;
[Property, Group("Projectile")] public float ProjectileVerticalSpeed { get; set; } = 0f;
[Property, Group("VFX")] public SoundEvent ShootSound { get; set; }
////////////////////////////////////////////////////////////////////////
// TODO : sticky only, look into doing a better way
public bool IsStickyLauncher { get => StickyTrackerComponent.IsValid(); }
public int ActiveStickies { get => StickyTrackerComponent.IsValid() ? StickyTrackerComponent.StickyCount : 0; }
protected StickyTrackerComponent StickyTrackerComponent { get; set; }
////////////////////////////////////////////////////////////////////////
protected override void OnAwake()
{
base.OnAwake();
// TODO : revisit, sticky only
StickyTrackerComponent = Components.Get<StickyTrackerComponent>();
}
protected override void OnInputUpdate()
{
if (!Equipment.Owner.IsValid())
{
return;
}
if (!Equipment.Owner.IsLocallyControlled)
{
return;
}
bool IsShooting = IsDown() && CanShoot();
if (IsShooting)
{
if (IsReloading && Ammo > 0)
{
CancelReload();
}
Equipment.Owner.VFXOnShoot();
Shoot();
}
}
protected virtual GameObject Shoot() // TODO : revisit the return
{
var PlayerPawn = Equipment.Owner;
if (!PlayerPawn.IsValid())
{
return null;
}
TimeSinceShot = 0;
Ammo--;
WorldShotVFX();
var AimForward = PlayerPawn.AimRay.Forward;
// create projectile object from prefab
var ProjectilePosition = PlayerPawn.AimRay.Position + (Vector3.Down * 3.33f); // magic
var ProjectileRotation = Rotation.LookAt(AimForward);
var Projectile = ProjectilePrefab.Clone(ProjectilePosition, ProjectileRotation);
// give it the player
var ProjectileComponent = Projectile.Components.Get<Projectile>();
ProjectileComponent.OwnerPlayerPawn = PlayerPawn;
if (IsStickyLauncher)
{
StickyTrackerComponent.AddSticky(Projectile.Components.Get<Sticky>());
}
var Rigidbody = Projectile.Components.Get<Rigidbody>();
SetProjectileVelocity(Rigidbody, AimForward);
Projectile.Root.Tags.Add("self");
Projectile.NetworkSpawn();
return Projectile;
}
[Rpc.Broadcast]
public void WorldShotVFX()
{
if (ShootSound.IsValid())
{
if (Sound.Play(ShootSound, Equipment.WorldPosition) is { } SoundHandel)
{
}
}
}
protected virtual void SetProjectileVelocity(Rigidbody ProjectileRigidbody, Vector3 AimForward)
{
ProjectileRigidbody.Velocity = AimForward * ProjectileHorizontalSpeed;
ProjectileRigidbody.Velocity += Vector3.Up * ProjectileVerticalSpeed;
}
protected TimeSince TimeSinceShot = new();
protected virtual bool CanShoot()
{
// these 2 should be ensures?
if (!Equipment.IsValid()) return false;
if (!Equipment.Owner.IsValid()) return false;
if (Equipment.Owner.IsFrozen)
return false;
if (Equipment.Tags.Has("equipping"))
return false;
if (TimeSinceShot < FireRate)
return false;
if (!HasAmmo)
return false;
return true;
}
}
////////////////////////////////////////////////////////////////////////
// NOTE : i think this should now just be a 'mode' within the base projectile comp
// TODO : please god fix this
[Title("Sticky Shooter"), Group("Weapon Components")]
public class StickyWeaponComponent : ProjectileWeaponComponent
{
[Property, Category("Sticky")] public float MaxChargeTime { get; private set; } = 1.6f;
// TODO : THIS IS QUICKLY BECOMING A MESS!
public TimeSince TimeSinceInputFirstDown { get; private set; } = new();
public bool WasInputDownLastTick { get; private set; } = false;
protected override void OnInputUpdate() // TODO : OnInput()?
{
if (!Equipment.Owner.IsValid())
{
return;
}
if (!Equipment.Owner.IsLocallyControlled)
{
return;
}
bool KeyDown = IsDown();
if (WasInputDownLastTick)
{
if (!KeyDown && CanShoot())
{
if (IsReloading && Ammo > 0)
{
CancelReload();
}
GameObject Projectile = Shoot();
if (Projectile.Components.Get<Sticky>() is Sticky Sticky)
{
Equipment.Owner.OnDeath += Sticky.Destroy;
}
}
}
else
{
if (KeyDown)
{
TimeSinceInputFirstDown = 0;
}
}
WasInputDownLastTick = KeyDown;
}
protected override void SetProjectileVelocity(Rigidbody ProjectileRigidbody, Vector3 AimForward)
{
const float Factor = .66f;
var SpeedFactor = 1 + (Math.Min(TimeSinceInputFirstDown, MaxChargeTime) * Factor);
ProjectileRigidbody.Velocity = AimForward * ProjectileHorizontalSpeed * SpeedFactor;
ProjectileRigidbody.Velocity += Vector3.Up * ProjectileVerticalSpeed;
}
protected override void OnUpdate()
{
base.OnUpdate();
bool IsAltFire = Input.Down("attack2");
if (IsAltFire && StickyTrackerComponent.IsValid())
{
StickyTrackerComponent.Detonate();
}
}
}
Game
game
using Sandbox.Events;
namespace KOTH;
public record HealingRequestEvent(FHealingRequest HealingRequest) : IGameEvent;
public record HealingBroadcastEvent(FHealingReceived HealingRequest) : IGameEvent;
public record FHealingRequest
{
public DamageComponent TargetDamageComponent { get; init; }
public PlayerPawn TargetPlayerPawn { get; init; }
public PlayerPawn HealerPlayerPawn { get; init; }
public int BaseHealing { get; init; } = 0;
public bool AllowOverheal { get; init; } = false;
public EHealingType HealingType { get; init; } = EHealingType.Continuous;
public Vector3 HealingOrigin { get; init; } = Vector3.Zero;
public RealTimeSince TimeSinceEvent { get; init; } = 0;
public bool IsValid()
{
return TargetPlayerPawn.IsValid();
}
}
public record FHealingReceived
{
public PlayerPawn TargetPlayerPawn { get; init; }
public PlayerPawn HealerPlayerPawn { get; init; }
public PlayerState TargetPlayerState { get; init; }
public PlayerState HealerPlayerState { get; init; }
public int Heals { get; init; } = 0;
public EHealingType HealingType { get; init; } = EHealingType.Continuous;
}
public enum EHealingType
{
Continuous = 0,
OneOff,
}
Game
game
namespace KOTH;
[GameResource("koth/Map Definition", "map", "A map definition")]
public partial class MapDefinition : GameResource
{
[Property] public string Title { get; set; }
[Property] public string Description { get; set; }
[Property] public SceneFile SceneFile { get; set; }
[Property, ImageAssetPath] public string Background { get; set; }
}
Game
game
namespace KOTH.Notification;
/// <summary>
/// Manages the queue and display of notifications in the game.
/// NOTE - only one Notification will show at a time, even if its in different zones
/// </summary>
public class NotificationManager: SingletonComponent<NotificationManager>
{
// Queue of notifications
public Queue<FNotification> NotificationQueue { get; set; } = new();
// Notification - Time Display Ends
public KeyValuePair<FNotification, DateTime>? CurrentNotifications { get; set; } = null;
/// <summary>
/// Adds a new notification to the notification queue.
/// </summary>
/// <param name="Notification">The notification to be added to the queue.</param>
public void AddNotification(FNotification Notification)
{
NotificationQueue.Enqueue(Notification);
}
/// <summary>
/// Retrieves the currently active notification if one exists.
/// </summary>
/// <returns>
/// The currently active notification or null if no notification is active.
/// </returns>
public FNotification? GetNotification()
{
if (CurrentNotifications == null) return null;
return CurrentNotifications.Value.Key;
}
/// <summary>
/// Updates the state of the notification manager, including processing the notification queue
/// and managing the display timing of the current notification.
/// </summary>
protected override void OnUpdate()
{
if (CurrentNotifications != null && CurrentNotifications.Value.Value <= DateTime.Now)
{
CurrentNotifications = null;
}
if (CurrentNotifications == null && NotificationQueue.Count > 0)
{
var notification = NotificationQueue.Dequeue();
CurrentNotifications = new KeyValuePair<FNotification, DateTime>(notification, DateTime.Now.AddSeconds(notification.Duration));
}
}
}
Game
game
using Sandbox.Events;
namespace KOTH;
public record HillCapturedEvent(List<PlayerPawn> PlayerPawns, Team Team, Hill Hill) : IGameEvent;
public record HillWinEvent(Team Team, Hill Hill) : IGameEvent;
public record HillCappingEvent(Team Team, float CaptureDelta, Hill Hill) : IGameEvent;
public record HillDecayCapEvent(float CaptureDelta, Hill Hill) : IGameEvent;
[Title("Hill Captured Event")]
public class HillCapturedEventComponent : GameEventComponent<HillCapturedEvent> { }
[Title("Hill Won Event")]
public class HillWinEventComponent : GameEventComponent<HillWinEvent> { }
[Title("Hill Capping Event")]
public class HillCappingEventComponent : GameEventComponent<HillCappingEvent> { }
[Title("Hill Decay Event")]
public class HillDecayCapEventComponent : GameEventComponent<HillDecayCapEvent> { }
Game
game
using Sandbox;
using Sandbox.Events;
using Sandbox.Utility;
namespace KOTH;
public sealed class KothWinCondition : Component,
IGameEventHandler<HillWinEvent>
{
[Property, Sync(SyncFlags.FromHost)]
public int TargetScore { get; set; } = 3;
[Sync(SyncFlags.FromHost)]
public NetDictionary<Team, int> Scores { get; private set; } = new();
[Property]
public StateComponent RoundWinState { get; set; }
[Property]
public StateComponent TGameWinState { get; set; }
[Property]
public StateComponent CTGameWinState { get; set; }
///////////////////////////////////////////////////////////////////////////
protected override void OnAwake()
{
base.OnAwake();
if (Networking.IsHost)
{
//if (Scores != null)
//{
Scores = new()
{
// TODO : will need to add any extra teams here as well! not pragmatic!
{ Team.CounterTerrorist, 0 }, { Team.Terrorist, 0 }, { Team.Unassigned, 0 },
};
//}
}
}
public void OnGameEvent(HillWinEvent EventArgs)
{
Log.Info($"all : scores {Scores[Team.CounterTerrorist]} : {Scores[Team.Terrorist]}");
if (!Networking.IsHost)
return;
var Hill = EventArgs.Hill;
if (!Hill.IsValid())
{
Log.Error("Invalid hill captured!");
return;
}
Team WinningTeam = EventArgs.Team;
var TeamScore = ++Scores[WinningTeam];
if (TeamScore >= TargetScore)
{
var NextState = WinningTeam == Team.CounterTerrorist ? CTGameWinState : TGameWinState;
GameMode.Instance.StateMachine.Transition(NextState);
}
else
{
GameMode.Instance.StateMachine.Transition(RoundWinState);
}
Log.Info($"host : scores {Scores[Team.CounterTerrorist]} : {Scores[Team.Terrorist]}");
}
}
Debug: View Raw JSON Response
{
"TotalCount": 198,
"Files": [
{
"Ident": "wurstsoftware.koth",
"Path": "Assembly.cs",
"FileName": "Assembly.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "global using Sandbox;\r\nglobal using Sandbox.UI;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "Damage/DamageManager.cs",
"FileName": "DamageManager.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using Sandbox;\r\nusing Sandbox.Diagnostics;\r\nusing Sandbox.Events;\r\nusing Sandbox.Utility;\r\nusing System.ComponentModel.DataAnnotations;\r\nusing static Sandbox.PhysicsContact;\r\n\r\nnamespace KOTH;\r\n\r\n// TODO : rename health manager\r\npublic sealed class DamageManager : SingletonComponent<DamageManager>,\r\n\tIGameEventHandler<DamageRequestEvent>,\r\n\tIGameEventHandler<HealingRequestEvent>\r\n{\r\n\t[Property] public SoundEvent HitSound { get; set; }\r\n\r\n\t// special bool zone\r\n\t[Property] public bool KnockbackOnly { get; private set; } = false;\r\n\t[Property] public bool IgnoreTeams { get; private set; } = false;\r\n\r\n\tconst float SelfDamageMultiplyer = 0.25f;\r\n\r\n\t/////////////////////////////////////////////////////////////////////////////////////////////\r\n\r\n\tpublic void OnGameEvent(HealingRequestEvent EventArgs)\r\n\t{\r\n\t\tvar HealingRequest = EventArgs.HealingRequest;\r\n\t\tif (!HealingRequest.IsValid())\r\n\t\t{\r\n\t\t\tLog.Warning($\"tried to parse HealingRequestEvent that is not valid {EventArgs} {this}\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tServerInflictHealing(HealingRequest);\r\n\t}\r\n\r\n\tpublic void OnGameEvent(DamageRequestEvent EventArgs)\r\n\t{\r\n\t\tvar DamageRequest = EventArgs.DamageRequest;\r\n\t\tif (!DamageRequest.IsValid())\r\n\t\t{\r\n\t\t\tLog.Warning($\"tried to parse DamageRequestEvent that is not valid {EventArgs} {this}\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// tell server to do damage from request\r\n\t\tServerInflictDamageToPlayer(DamageRequest);\r\n\t}\r\n\r\n\t/////////////////////////////////////////////////////////////////////////////////////////////\r\n\t// host only broadcasts\r\n\t/////////////////////////////////////////////////////////////////////////////////////////////\r\n\r\n\t[Rpc.Host]\r\n\tprivate void ServerInflictDamageToPlayer(FDamageRequest DamageRequest)\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\r\n\t\tvar DamageOrigin = DamageRequest.DamageOrigin;\r\n\t\tvar TargetDamageComponent = DamageRequest.TargetDamageComponent;\r\n\t\tvar AttackerPlayerPawn = DamageRequest.AttackerPlayerPawn;\r\n\t\tvar Damage = DamageRequest.BaseDamage;\r\n\r\n\t\tif (!TargetDamageComponent.IsValid() || TargetDamageComponent.Health <= 0)\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar TargetPlayerPawn = DamageRequest.TargetDamageComponent.GetComponent<PlayerPawn>();\r\n\r\n\t\t// we've taken damage without an attacker pawn, apply & return early ///////////////////\r\n\t\tif (!AttackerPlayerPawn.IsValid())\r\n\t\t{\r\n\t\t\tFDamageTaken EnvDamageTaken = new()\r\n\t\t\t{\r\n\t\t\t\tVictimGameObject = TargetDamageComponent.GameObject,\r\n\t\t\t\tAssumedVictimPlayerPawn = TargetPlayerPawn,\r\n\t\t\t\tVictimPlayerState = GameUtils.GetPlayerState(TargetPlayerPawn?.Id),\r\n\r\n\t\t\t\tDamage = Damage,\r\n\t\t\t\tDamageLocation = DamageOrigin,\r\n\t\t\t\tDamageType = DamageRequest.DamageType,\r\n\t\t\t\tIsDummyDamage = TargetPlayerPawn.IsValid() && TargetPlayerPawn.IsDummy,\r\n\t\t\t};\r\n\r\n\t\t\tTargetDamageComponent.TakeDamage(EnvDamageTaken);\r\n\t\t\treturn; // NOTE : early return\r\n\t\t}\r\n\r\n\t\t// team check //////////////////////////////////////////////////////////////////////////////////////\r\n\t\tif (!IgnoreTeams)\r\n\t\t{\r\n\t\t\tif (TargetDamageComponent.Team == AttackerPlayerPawn.Team && TargetPlayerPawn != AttackerPlayerPawn)\r\n\t\t\t{\r\n\t\t\t\treturn; // NOTE : early return\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar TargetCenter = DamageRequest.TargetOrigin;\r\n\r\n\t\t// calculate damage ///////////////////////////////////////////\r\n\t\tif (DamageRequest.DamageFalloffType != EDamageFalloffType.None)\r\n\t\t{\r\n\t\t\tvar TargetToImpactDistance = TargetCenter.Distance(DamageOrigin);\r\n\t\t\tvar TargetToAttackerDistance = TargetCenter.Distance(AttackerPlayerPawn.CenterPosition);\r\n\r\n\t\t\tfloat MaxDamage = Damage;\r\n\t\t\tif (DamageRequest.DamageType == EDamageType.Projectile && !DamageRequest.DirectImpact)\r\n\t\t\t{\r\n\t\t\t\tvar DamageDistanceLerp = TargetToImpactDistance / DamageRequest.MaxDamageImpactDistance;\r\n\t\t\t\tMaxDamage = MathX.Lerp(MaxDamage, MaxDamage * 0.5f, DamageDistanceLerp);\r\n\t\t\t}\r\n\r\n\t\t\tfloat MinDamage = MaxDamage * 0.33f;\r\n\t\t\tfloat DamageLerp = TargetToAttackerDistance / 1600f;\r\n\r\n\t\t\tif (DamageRequest.DamageFalloffType == EDamageFalloffType.Falloff)\r\n\t\t\t{\r\n\t\t\t\tDamage = MathX.Lerp(MaxDamage, MinDamage, DamageLerp).FloorToInt();\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tDamage = MathX.Lerp(MinDamage, MaxDamage, DamageLerp).FloorToInt();\r\n\t\t\t}\r\n\t\t}\r\n\r\n\r\n\t\t// knockback ////////////////\r\n\t\tvar Knockback = Vector3.Zero; \r\n\t\tif (TargetPlayerPawn.IsValid())\r\n\t\t{\r\n\t\t\tvar DirectionVec = (TargetCenter - DamageOrigin).Normal;\r\n\t\t\tvar CrouchFactor = TargetPlayerPawn.IsCrouching ? 62 : 82;\r\n\t\t\tvar KBStrength = DamageRequest.BaseKnockbackStrength;\r\n\t\t\tif (!TargetPlayerPawn.IsGrounded && TargetPlayerPawn == AttackerPlayerPawn)\r\n\t\t\t{\r\n\t\t\t\tKBStrength *= 1.33f;\r\n\t\t\t}\r\n\r\n\t\t\tvar KnockbackFactor = Damage * (KBStrength / CrouchFactor);\r\n\r\n\t\t\tKnockback = DirectionVec * KnockbackFactor * TargetPlayerPawn.WeightFactor;\r\n\t\t\tTargetPlayerPawn.DoKnockback(Knockback);\r\n\t\t}\r\n\r\n\t\tif (KnockbackOnly)\r\n\t\t{\r\n\t\t\treturn; // !\r\n\t\t}\r\n\r\n\t\t// deal the damage /////////////////////////////////////////\r\n\t\tbool WasSelfDamage = TargetPlayerPawn == AttackerPlayerPawn;\r\n\t\tif (WasSelfDamage && DamageRequest.DoesLessSelfDamage)\r\n\t\t{\r\n\t\t\tDamage = (Damage * SelfDamageMultiplyer).FloorToInt();\r\n\t\t}\r\n\r\n\t\tFDamageTaken DamageTaken = new()\r\n\t\t{\r\n\t\t\tVictimGameObject = TargetDamageComponent.GameObject,\r\n\r\n\t\t\tAssumedAttackerPlayerPawn = AttackerPlayerPawn,\r\n\t\t\tAssumedVictimPlayerPawn = TargetPlayerPawn,\r\n\t\t\tAttackerPlayerState = GameUtils.GetPlayerState(AttackerPlayerPawn?.Id),\r\n\t\t\tVictimPlayerState = GameUtils.GetPlayerState(TargetPlayerPawn?.Id),\r\n\r\n\t\t\tDamage = Damage,\r\n\t\t\tDamageLocation = DamageOrigin,\r\n\t\t\tDamageType = DamageRequest.DamageType,\r\n\t\t\tIsDummyDamage = TargetPlayerPawn.IsValid() && TargetPlayerPawn.IsDummy,\r\n\t\t};\r\n\r\n\t\tTargetDamageComponent.TakeDamage(DamageTaken);\r\n\r\n\t\tLog.Info($\"{Damage}:{Knockback.Length:0.0} damage:kb has been taken {AttackerPlayerPawn.DisplayName}:{AttackerPlayerPawn.Health}\" +\r\n\t\t\t$\" -> {TargetPlayerPawn?.DisplayName}:{TargetDamageComponent.Health}\");\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tprivate void ServerInflictHealing(FHealingRequest HealingRequest)\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\r\n\t\tif (!HealingRequest.TargetDamageComponent.IsValid())\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// we do it the other way round to the damage here :)\r\n\t\tHealingRequest.TargetDamageComponent.Heal(HealingRequest);\r\n\t}\r\n\r\n\t[Rpc.Broadcast(NetFlags.HostOnly)]\r\n\tprivate void ClientDidDamage(PlayerState PlayerState, PlayerPawn PlayerPawn, float Damage, bool WasSelfDamage)\r\n\t{\r\n\t\tif (!WasSelfDamage)\r\n\t\t{\r\n\t\t\tvar HitSound = Sound.Play(this.HitSound, PlayerPawn.WorldPosition);\r\n\t\t\tif (!HitSound.IsValid()) return;\r\n\r\n\t\t\tHitSound.Volume = 0.5f;\r\n\t\t\tHitSound.Pitch = MathX.Lerp(1.2f, 0.7f, Damage * .02f);\r\n\t\t\tHitSound.ListenLocal = true;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "ui/playerhud/components/stickyoverlay.razor.scss",
"FileName": "stickyoverlay.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "@import \"/UI/Styles/Theme.scss\";\r\n\r\nStickyOverlay {\r\n * {\r\n position: absolute;\r\n }\r\n\r\n .charge {\r\n position: absolute;\r\n top: 80%;\r\n left: 50%;\r\n width: 200px;\r\n height: 8px;\r\n background-color: rgba(0, 0, 0, 0.6);\r\n\r\n .charge-fill {\r\n height: 100%;\r\n background-color: whitesmoke;\r\n }\r\n\r\n transform: translateX(-48px);\r\n }\r\n\r\n .stickies {\r\n position: absolute;\r\n top: 75%;\r\n left: 70%;\r\n color: white;\r\n font-size: 36px;\r\n }\r\n}"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameEvent.cs",
"FileName": "GameEvent.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Collections.Immutable;\r\nusing System.Linq;\r\n\r\nnamespace Sandbox.Events;\r\n\r\n/// <summary>\r\n/// Interface for event payloads that can be listened for by <see cref=\"IGameEventHandler{T}\"/>s.\r\n/// </summary>\r\npublic interface IGameEvent { }\r\n\r\n/// <summary>\r\n/// Interface for components that handle game events with a payload of type <see cref=\"T\"/>.\r\n/// </summary>\r\n/// <typeparam name=\"T\">Event payload type.</typeparam>\r\npublic interface IGameEventHandler<in T>\r\n\twhere T : IGameEvent\r\n{\r\n\t/// <summary>\r\n\t/// Called when an event with payload of type <see cref=\"T\"/> is dispatched on a <see cref=\"GameObject\"/>\r\n\t/// that contains this component, including on a descendant.\r\n\t/// </summary>\r\n\t/// <param name=\"eventArgs\">Event payload.</param>\r\n\tvoid OnGameEvent( T eventArgs );\r\n}\r\n\r\n/// <summary>\r\n/// Helper for dispatching game events in a scene.\r\n/// </summary>\r\npublic static class GameEvent\r\n{\r\n\tprivate static Dictionary<Type, IReadOnlyDictionary<Type, int>> HandlerOrderingCache { get; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Notifies all <see cref=\"IGameEventHandler{T}\"/> components that are within <paramref name=\"root\"/>,\r\n\t/// with a payload of type <typeparamref name=\"T\"/>.\r\n\t/// </summary>\r\n\tpublic static void Dispatch<T>( this GameObject root, T eventArgs )\r\n\t\twhere T : IGameEvent\r\n\t{\r\n\t\tvar handlers = (root is Scene scene\r\n\t\t\t? scene.GetAllComponents<IGameEventHandler<T>>() // I think this is more efficient?\r\n\t\t\t: root.Components.GetAll<IGameEventHandler<T>>())\r\n\t\t\t.ToArray();\r\n\r\n\t\tif ( !HandlerOrderingCache.TryGetValue( typeof(T), out var ordering ) || handlers.Any( x => !ordering.ContainsKey( x.GetType() ) ) )\r\n\t\t{\r\n\t\t\tordering = HandlerOrderingCache[typeof(T)] = GetHandlerOrdering<T>();\r\n\t\t}\r\n\r\n\t\tList<Exception>? exceptions = null;\r\n\r\n\t\tforeach ( var handler in handlers.OrderBy( x => ordering[x.GetType()] ) )\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\thandler.OnGameEvent( eventArgs );\r\n\t\t\t}\r\n\t\t\tcatch ( Exception e )\r\n\t\t\t{\r\n\t\t\t\texceptions ??= new();\r\n\t\t\t\texceptions.Add( e );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tswitch ( exceptions?.Count )\r\n\t\t{\r\n\t\t\tcase 1:\r\n\t\t\t\tLog.Error( exceptions[0] );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase > 1:\r\n\t\t\t\tLog.Error( new AggregateException( exceptions ) );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate static bool IsImplementingMethodName( string methodName )\r\n\t{\r\n\t\tif ( methodName == nameof(IGameEventHandler<IGameEvent>.OnGameEvent) )\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn methodName.StartsWith( \"Sandbox.Events.IGameEventHandler<\" ) && methodName.EndsWith( \">.OnGameEvent\" );\r\n\t}\r\n\r\n\tprivate static MethodDescription? GetImplementation<T>( TypeDescription type )\r\n\t{\r\n\t\tforeach ( var method in type.Methods )\r\n\t\t{\r\n\t\t\tif ( method.IsStatic ) continue;\r\n\t\t\tif ( method.Parameters.Length != 1 ) continue;\r\n\t\t\tif ( method.Parameters[0].ParameterType != typeof( T ) ) continue;\r\n\r\n\t\t\tif ( !IsImplementingMethodName( method.Name ) ) continue;\r\n\r\n\t\t\treturn method;\r\n\t\t}\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\tprivate static IReadOnlyDictionary<Type, int> GetHandlerOrdering<T>()\r\n\t\twhere T : IGameEvent\r\n\t{\r\n\t\tvar types = TypeLibrary.GetTypes<IGameEventHandler<T>>().ToArray();\r\n\t\tvar helper = new SortingHelper( types.Length );\r\n\r\n\t\tfor ( var i = 0; i < types.Length; ++i )\r\n\t\t{\r\n\t\t\tvar type = types[i];\r\n\t\t\tvar method = GetImplementation<T>( type );\r\n\r\n\t\t\tif ( method is null )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\"Can't find {nameof( IGameEventHandler<T> )}<{typeof( T ).Name}> implementation in {type.Name}!\" );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var attrib in method.Attributes )\r\n\t\t\t{\r\n\t\t\t\tswitch ( attrib )\r\n\t\t\t\t{\r\n\t\t\t\t\tcase EarlyAttribute:\r\n\t\t\t\t\t\thelper.AddFirst( i );\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase LateAttribute:\r\n\t\t\t\t\t\thelper.AddLast( i );\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase IBeforeAttribute before:\r\n\t\t\t\t\t\tfor ( var j = 0; j < types.Length; ++j )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif ( i == j ) continue;\r\n\r\n\t\t\t\t\t\t\tvar other = types[j];\r\n\r\n\t\t\t\t\t\t\tif ( before.Type.IsAssignableFrom( other.TargetType ) )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\thelper.AddConstraint( i, j );\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase IAfterAttribute after:\r\n\t\t\t\t\t\tfor ( var j = 0; j < types.Length; ++j )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tif ( i == j ) continue;\r\n\r\n\t\t\t\t\t\t\tvar other = types[j];\r\n\r\n\t\t\t\t\t\t\tif ( after.Type.IsAssignableFrom( other.TargetType ) )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\thelper.AddConstraint( j, i );\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar ordering = new List<int>();\r\n\r\n\t\tif ( !helper.Sort( ordering, out var invalid ) )\r\n\t\t{\r\n\t\t\tLog.Error( $\"Invalid event ordering constraint between {types[invalid.EarlierIndex].Name} and {types[invalid.LaterIndex].Name}!\" );\r\n\t\t\treturn ImmutableDictionary<Type, int>.Empty;\r\n\t\t}\r\n\r\n\t\treturn Enumerable.Range( 0, ordering.Count )\r\n\t\t\t.ToImmutableDictionary( i => types[ordering[i]].TargetType, i => i );\r\n\t}\r\n}\r\n\r\npublic delegate void GameEventAction<in T>( T eventArgs )\r\n\twhere T : IGameEvent;\r\n\r\n/// <summary>\r\n/// Base class for components that expose game events to Action Graph.\r\n/// </summary>\r\npublic abstract class GameEventComponent<T> : Component, IGameEventHandler<T>\r\n\twhere T : IGameEvent\r\n{\r\n\t/// <summary>\r\n\t/// Action invoked when the <typeparamref name=\"T\"/> event is dispatched.\r\n\t/// </summary>\r\n\t[Property]\r\n\tpublic GameEventAction<T>? OnEvent { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// If this component is within a state machine, optional state to transition\r\n\t/// to when this event is dispatched.\r\n\t/// </summary>\r\n\t[Property]\r\n\tpublic StateComponent? NextState { get; set; }\r\n\r\n\tvoid IGameEventHandler<T>.OnGameEvent( T eventArgs )\r\n\t{\r\n\t\tOnEvent?.Invoke( eventArgs );\r\n\r\n\t\tif ( NextState is not null )\r\n\t\t{\r\n\t\t\tComponents.GetInAncestorsOrSelf<StateMachineComponent>()?.Transition( NextState );\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/AudioComponent.cs",
"FileName": "AudioComponent.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using System.Threading.Tasks;\r\n\r\nnamespace KOTH;\r\n\r\n/// <summary>\r\n/// Manages sound playback within the game, including playing sounds globally or for specific teams,\r\n/// updating sound states, and handling active sound instances.\r\n/// </summary>\r\n/// <remarks>\r\n/// This component is responsible for managing the lifecycle of sounds,\r\n/// including their initiation, positional updates, and cleanup. It provides\r\n/// methods for playing sounds either globally or targeted to specific teams\r\n/// and maintains references to active sound handles.\r\n/// </remarks>\r\npublic sealed class AudioComponent : SingletonComponent<AudioComponent>\r\n{\r\n\t/// <summary>\r\n\t/// A collection of sound events that have been stopped during the audio handling process.\r\n\t/// This list is managed internally and is primarily used to track and clean up finished or stopped sounds,\r\n\t/// ensuring efficient audio management within the system.\r\n\t/// </summary>\r\n\t[Property]\r\n\tprivate List<FSound> StoppedSounds { get; set; }\r\n\r\n\t/// <summary>\r\n\t/// A mapping between sound events and their corresponding sound handles used during audio processing.\r\n\t/// This dictionary is utilized to associate each active sound event with its control and state information,\r\n\t/// enabling management of sound playback, updates, and cleanup within the system.\r\n\t/// </summary>\r\n\t[Property]\r\n\tprivate Dictionary<FSound, SoundHandle> SoundHandles { get; set; }\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\t\tSoundHandles = new Dictionary<FSound, SoundHandle>();\r\n\t\tStoppedSounds = new List<FSound>();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Plays a specified sound for all players on the given team.\r\n\t/// </summary>\r\n\t/// <param name=\"soundEvent\">The sound event to be played.</param>\r\n\t/// <param name=\"team\">The team for which the sound will be played.</param>\r\n\tpublic void PlaySoundForTeam(FSound soundEvent, Team team)\r\n\t{\r\n\t\tusing (Rpc.FilterInclude(GameUtils.GetPlayers(team).Select(x => x.Connection)))\r\n\t\t{\r\n\t\t\tPlaySound(soundEvent);\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Plays a specified sound globally for all players in the game.\r\n\t/// </summary>\r\n\t/// <param name=\"soundEvent\">The sound event to be played.</param>\r\n\t[Rpc.Broadcast]\r\n\tpublic void PlaySound(FSound soundEvent)\r\n\t{\r\n\t\tvar alreadyPlaying = Instance.SoundHandles.Any(x => x.Key.Owner.Id == soundEvent.Owner.Id || x.Key.SoundId == soundEvent.SoundId);\r\n\t\tif (alreadyPlaying) return;\r\n\t\tLog.Info(\"Playing sound \" + soundEvent.SoundId);\r\n\t\tvar handle = Sound.Play(soundEvent.SoundEvent, soundEvent.Position);\r\n\t\tInstance.SoundHandles.Add(soundEvent, handle);\r\n\t}\r\n\r\n\r\n\t/// <summary>\r\n\t/// Stops the playback of a specified sound in the game.\r\n\t/// </summary>\r\n\t/// <param name=\"soundEvent\">The sound event to be stopped.</param>\r\n\t[Rpc.Broadcast]\r\n\tpublic void StopSound(FSound soundEvent)\r\n\t{\r\n\t\tvar soundHandle = Instance.SoundHandles.FirstOrDefault(x => x.Key.SoundId == soundEvent.SoundId);\r\n\t\tvar alreadyRemoving = Instance.StoppedSounds.Any(x => x.SoundId == soundEvent.SoundId);\r\n\t\tif(soundHandle.Value == null || alreadyRemoving) return;\r\n\t\tsoundHandle.Value.Stop();\r\n\t\tInstance.StoppedSounds.Add(soundHandle.Key);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Determines whether the specified sound event is currently playing.\r\n\t/// </summary>\r\n\t/// <param name=\"soundEvent\">The sound event to check.</param>\r\n\t/// <returns>True if the sound event is currently playing; otherwise, false.</returns>\r\n\tpublic Boolean IsPlayingSound(FSound soundEvent)\r\n\t{\r\n\t\treturn Instance.SoundHandles.Any(x => x.Key.SoundId == soundEvent.SoundId);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Updates the positions of active sounds and removes references to completed or invalid ones.\r\n\t/// </summary>\r\n\t/// <remarks>\r\n\t/// This method iterates through all active sound handles. If a sound should update its position,\r\n\t/// its position is recalculated. Stopped, finished, or invalid sound handles are marked for cleanup\r\n\t/// and later removed.\r\n\t/// </remarks>\r\n\tpublic void SoundTick()\r\n\t{\r\n\t\tforeach (var soundHandle in Instance.SoundHandles)\r\n\t\t{\r\n\t\t\tif (!soundHandle.Key.Owner.IsValid())\r\n\t\t\t{\r\n\t\t\t\tsoundHandle.Value.Stop();\r\n\t\t\t\tInstance.StoppedSounds.Add(soundHandle.Key);\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\t\t\tif (soundHandle is { Value: not null, Value.Finished: false, Key.UpdatePosition: true, }\r\n\t\t\t\t&& soundHandle.Value.Position != soundHandle.Key.Owner.WorldPosition\r\n\t\t\t\t)\r\n\t\t\t{\r\n\t\t\t\tsoundHandle.Value.Position = soundHandle.Key.Owner.WorldPosition;\r\n\t\t\t}\r\n\r\n\t\t\tif (!soundHandle.Value.IsValid() || soundHandle.Value.Finished || soundHandle.Value.IsStopped)\r\n\t\t\t{\r\n\t\t\t\tInstance.StoppedSounds.Add(soundHandle.Key);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tCleanUpStoppedSounds();\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Cleans up the references to sounds that have been stopped, finished, or are no longer valid.\r\n\t/// </summary>\r\n\t/// <remarks>\r\n\t/// This method iterates through the list of stopped sounds, attempting to remove their corresponding entries\r\n\t/// from the active sound handles dictionary. Logs errors or successful removals as appropriate, then clears\r\n\t/// the list of stopped sounds.\r\n\t/// </remarks>\r\n\tprivate void CleanUpStoppedSounds()\r\n\t{\r\n\t\tforeach (var soundHandle in Instance.StoppedSounds)\r\n\t\t{\r\n\t\t\tInstance.SoundHandles.Remove(soundHandle);\r\n\t\t}\r\n\r\n\t\tInstance.StoppedSounds.Clear();\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Represents a sound event within the game.\r\n/// This struct is used to encapsulate data necessary to play a sound, including its location,\r\n/// the associated sound event, and the owning component triggering the sound.\r\n/// </summary>\r\npublic struct FSound : IEquatable<FSound>\r\n{\r\n\t/// <summary>\r\n\t/// Represents the underlying data or identity of a playable sound within the audio system.\r\n\t/// </summary>\r\n\tpublic SoundEvent SoundEvent { get; init; }\r\n\r\n\t/// <summary>\r\n\t/// Represents the 3D position in the game world where the sound event should occur.\r\n\t/// </summary>\r\n\tpublic Vector3 Position { get; init; }\r\n\r\n\t/// <summary>\r\n\t/// A unique identifier for a specific sound event within the game.\r\n\t/// This identifier is automatically generated upon the creation of a sound event and is used\r\n\t/// to distinguish and manage individual sound instances effectively.\r\n\t/// </summary>\r\n\tpublic Guid SoundId { get; init; }\r\n\r\n\t/// <summary>\r\n\t/// The component responsible for triggering the sound event in the game.\r\n\t/// </summary>\r\n\tpublic Component Owner { get; init; }\r\n\r\n\t/// <summary>\r\n\t/// Indicates whether the position of the sound should be updated dynamically.\r\n\t/// </summary>\r\n\t/// <remarks>\r\n\t/// If set to true, the sound's position is recalculated during the sound management process\r\n\t/// to match the world position of its owning component. This is particularly useful for\r\n\t/// sounds tied to moving objects or entities within the game world. If false, the sound\r\n\t/// remains static at its initial position.\r\n\t/// </remarks>\r\n\tpublic bool UpdatePosition { get; init; } = false;\r\n\r\n\tpublic FSound(SoundEvent soundEvent, Vector3 position, Component owner, bool updatePosition = false)\r\n\t{\r\n\t\tSoundEvent = soundEvent;\r\n\t\tPosition = position;\r\n\t\tOwner = owner;\r\n\t\tUpdatePosition = updatePosition;\r\n\t\tSoundId = Guid.NewGuid();\r\n\t}\r\n\r\n\tpublic bool Equals(FSound other)\r\n\t{\r\n\t\treturn other.SoundId == SoundId;\r\n\t}\r\n\r\n\tpublic override bool Equals(object obj)\r\n\t{\r\n\t\treturn obj is FSound other && Equals(other);\r\n\t}\r\n\r\n\tpublic override int GetHashCode()\r\n\t{\r\n\t\treturn HashCode.Combine(SoundEvent, UpdatePosition, SoundId);\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/ClassList.cs",
"FileName": "ClassList.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using Sandbox;\r\n\r\nnamespace KOTH;\r\n\r\npublic sealed class ClassList : Component\r\n{\r\n\t[Property] public List<CharacterDefinition> ClassDefinitions { get; private set; }\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/Rules/Koth/HillController.cs",
"FileName": "HillController.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using Sandbox;\r\nusing Sandbox.Events;\r\n\r\nnamespace KOTH;\r\n\r\npublic sealed class HillController : Component, IGameEventHandler<EnterStateEvent>\r\n{\r\n\t[Property] public bool HillEnabled { get; set; } = false;\r\n\t[Sync(SyncFlags.FromHost)] public Hill ActiveHill { get; private set; }\r\n\r\n\tvoid IGameEventHandler<EnterStateEvent>.OnGameEvent(EnterStateEvent eventArgs)\r\n\t{\r\n\t\tGetActiveHill()?.SetHillActive(HillEnabled);\r\n\t}\r\n\r\n\tpublic Hill GetActiveHill()\r\n\t{\r\n\t\tif (ActiveHill.IsValid())\r\n\t\t{\r\n\t\t\treturn ActiveHill;\r\n\t\t}\r\n\r\n\t\tforeach (var GameObject in Scene.GetAllObjects(true))\r\n\t\t{\r\n\t\t\tvar Hill = GameObject.Components.Get<Hill>();\r\n\t\t\tif (Hill.IsValid())\r\n\t\t\t{\r\n\t\t\t\tActiveHill = Hill;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn ActiveHill;\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "Player/PlayerPawn.Zones.cs",
"FileName": "PlayerPawn.Zones.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "\r\nnamespace KOTH;\r\n\r\n// tony: Not a huge fan of this, can't we be reacting to trigger events instead of tracing against spheres for every player every update?\r\n// or just do this on the local player, and tell the host about what zones they're in?\r\n// travis : I AGREE\r\npartial class PlayerPawn\r\n{\r\n\tprivate readonly List<Zone> _zones = new();\r\n\r\n\t/// <summary>\r\n\t/// Which <see cref=\"Zone\"/>s is the player currently standing in.\r\n\t/// </summary>\r\n\tpublic IEnumerable<Zone> Zones => _zones;\r\n\r\n\t/// <summary>\r\n\t/// Update which <see cref=\"Zone\"/>s the player is standing in.\r\n\t/// </summary>\r\n\tprivate void UpdateZones()\r\n\t{\r\n\t\t_zones.Clear();\r\n\t\t_zones.AddRange(Zone.GetAt(WorldPosition));\r\n\t}\r\n\r\n\tpublic T GetZone<T>()\r\n\t{\r\n\t\treturn Zones.Select(x => x.Components.Get<T>()).FirstOrDefault(x => x is not null);\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "Player/State/PlayerState.Helpers.cs",
"FileName": "PlayerState.Helpers.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using KOTH.UI;\r\nusing Sandbox;\r\nusing Sandbox.Diagnostics;\r\nusing Sandbox.Events;\r\nusing System.Collections.Generic;\r\n\r\nnamespace KOTH;\r\n\r\npublic partial class PlayerState\r\n{\r\n\t// TODO : create connection -> player state map?\r\n\tpublic static PlayerState Local { get; private set; }\r\n\tpublic static bool IsPaused { get; set; }\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\r\n\t\tCameraUtils.LocalTick();\r\n\r\n\t\tif (Input.EscapePressed)\r\n\t\t{\r\n\t\t\tInput.EscapePressed = false;\r\n\t\t\tIsPaused = !IsPaused;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "Player/State/PlayerState.Spawning.cs",
"FileName": "PlayerState.Spawning.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using KOTH.UI;\r\nusing Sandbox;\r\nusing Sandbox.Diagnostics;\r\nusing Sandbox.Events;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Xml.Linq;\r\n\r\nnamespace KOTH;\r\n\r\npublic struct FPlayerPawnDefinition\r\n{\r\n\tpublic FPlayerPawnDefinition()\r\n\t{\r\n\t}\r\n\r\n\tpublic CharacterDefinition CharacterDefinition { get; init; }\r\n\t// public PlayerState OwnerPlayerState { get; init; }\r\n\r\n\tpublic string Name { get; init; } = \"UNINITALIZED\";\r\n\tpublic Team Team { get; init; } = Team.Unassigned;\r\n\tpublic bool IsDummy { get; init; } = false;\r\n\r\n\tpublic readonly bool IsValid()\r\n\t{\r\n\t\treturn CharacterDefinition.IsValid();\r\n\t}\r\n}\r\n\r\n//////////////////////////////////////////////////////////////////////////////////\r\n\r\n/*\r\n * State of the player pawn\r\n * menu when displaying a menu (true when first joining a lobby)\r\n * waitingforspawn when in a game & requestedcharacterdefinition is not null\r\n * alive when playing in game\r\n */\r\npublic enum EPlayerStateSpawningState\r\n{\r\n\tMainMenu,\r\n\tWaitingForSpawn,\r\n\tInstantSpawn,\r\n\tAlive,\r\n\tSpectating,\r\n}\r\n\r\n//////////////////////////////////////////////////////////////////////////////////\r\n\r\npublic partial class PlayerState\r\n{\r\n\t[Sync(SyncFlags.FromHost)/*, Change(nameof(OnPlayerStateSpawningStateChanged))*/] public EPlayerStateSpawningState PlayerStateSpawningState { get; private set; } = EPlayerStateSpawningState.MainMenu;\r\n\r\n\t//////////////////////////////////////////////////////////////////////////////////\r\n\r\n\t[Sync(SyncFlags.FromHost)] public float TimeTilAttemptedSpawn { get; private set; } = -1;\r\n\r\n\tpublic void SetTimeTilAttemptedSpawn(float TimeTilSpawn)\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\r\n\t\tTimeTilAttemptedSpawn = TimeTilSpawn;\r\n\t}\r\n\r\n\t//////////////////////////////////////////////////////////////////////////////////\r\n\r\n\t/*\r\n\t * NOTE : Sync will only care about the vaule given by an object's host, so bc this player state is owned \r\n\t * by the server the requested character def will never sync! this is the workaround for now..\r\n\t */\r\n\tpublic CharacterDefinition RequestedCharacterDefinition { get; private set; } = null;\r\n\r\n\tpublic void RequestCharacterDefinition(CharacterDefinition CharacterDefintionIn)\r\n\t{\r\n\t\tLog.Info($\"Requested Character {CharacterDefintionIn}\");\r\n\r\n\t\tRequestedCharacterDefinition = CharacterDefintionIn;\r\n\r\n\t\t// TODO : actaully send a request\r\n\t\tHostSetCharacterDefinition(CharacterDefintionIn);\r\n\t}\r\n\r\n\t[Rpc.Host]\r\n\tprivate void HostSetCharacterDefinition(CharacterDefinition CharacterDefinition)\r\n\t{\r\n\t\tif (!CharacterDefinition.IsValid())\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tRequestedCharacterDefinition = CharacterDefinition;\r\n\r\n\t\t// if we come from the main menu we want to spawn instantly\r\n\t\tif (PlayerStateSpawningState == EPlayerStateSpawningState.MainMenu)\r\n\t\t{\r\n\t\t\tPlayerStateSpawningState = EPlayerStateSpawningState.InstantSpawn;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tPlayerStateSpawningState = EPlayerStateSpawningState.WaitingForSpawn;\r\n\t}\r\n\r\n\t//////////////////////////////////////////////////////////////////////////////////\r\n\r\n\t[Sync(SyncFlags.FromHost)] public Team Team { get; set; }\r\n\r\n\t//////////////////////////////////////////////////////////////////////////////////\r\n\r\n\tpublic void SpawnPlayerPawn(TeamSpawnPoint SpawnPoint)\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\r\n\t\tif (PlayerPawn.IsValid())\r\n\t\t{\r\n\t\t\tPlayerPawn.GameObject.Root.Destroy();\r\n\t\t\tPlayerPawn = null;\r\n\t\t\tLocalOnPlayerDeath();\r\n\r\n\t\t\tGameMode.Instance?.DestroySpawnZoneBlockers();\r\n\t\t}\r\n\r\n\t\tif (!SpawnPoint.IsValid())\r\n\t\t{\r\n\t\t\tLog.Warning($\"trying to spawn player {this} with invalid spawn point\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tSpawnPlayerPawn(Connection, SteamName, RequestedCharacterDefinition, SpawnPoint);\r\n\t}\r\n\r\n\r\n\t[Property] public GameObject DefaultPlayerPawnPrefab { get; private set; }\r\n\r\n\tprivate void OnPlayerPawnSpawn()\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\t\tAssert.True(PlayerPawn.IsValid());\r\n\r\n\t\tPlayerPawn.OnDeath += OnPlayerPawnDeath;\r\n\t\tPlayerStateSpawningState = EPlayerStateSpawningState.Alive;\r\n\r\n\t\tBroadcastPlayerSpawn(PlayerPawn);\r\n\t\t\r\n\t\tusing (Rpc.FilterInclude(Connection))\r\n\t\t{\r\n\t\t\tLocalPlayerSpawn(PlayerPawn);\r\n\t\t}\r\n\t}\r\n\t\r\n\t[Rpc.Host]\r\n\tprivate void SpawnPlayerPawn(Connection OwningConnection, string Name, CharacterDefinition CharacterDefinition, TeamSpawnPoint SpawnPoint)\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\t\tAssert.IsValid(SpawnPoint);\r\n\r\n\t\tLog.Info($\"attempting to spawn player {RequestedCharacterDefinition} for {OwningConnection}\");\r\n\r\n\t\tvar SpawnPlayerPawnPrefab = DefaultPlayerPawnPrefab.Clone(SpawnPoint.GameObject.WorldTransform, null, true);\r\n\t\tSpawnPlayerPawnPrefab.Network.SetOrphanedMode(NetworkOrphaned.Destroy);\r\n\r\n\t\tvar SpawnPlayerPawnComponent = SpawnPlayerPawnPrefab.Components.Get<PlayerPawn>();\r\n\t\tAssert.NotNull(SpawnPlayerPawnComponent);\r\n\r\n\t\tvar TeamIn = Team; // :P\r\n\t\tFPlayerPawnDefinition PlayerPawnDefinition = new()\r\n\t\t{\r\n\t\t\tCharacterDefinition = RequestedCharacterDefinition,\r\n\t\t\tName = Name,\r\n\t\t\tTeam = TeamIn,\r\n\t\t};\r\n\r\n\t\tSpawnPlayerPawnComponent.SetPlayerPawnDefinition(PlayerPawnDefinition);\r\n\r\n\t\tif (!SpawnPlayerPawnPrefab.NetworkSpawn(OwningConnection))\r\n\t\t{\r\n\t\t\tSpawnPlayerPawnPrefab.Destroy();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tPlayerPawn = SpawnPlayerPawnComponent;\r\n\t\tOnPlayerPawnSpawn();\r\n\t}\r\n\r\n\tvoid OnPlayerPawnDeath()\r\n\t{\r\n\t\tAssert.True(Networking.IsHost);\r\n\r\n\t\tPlayerStateSpawningState = EPlayerStateSpawningState.WaitingForSpawn;\r\n\r\n\t\tusing (Rpc.FilterInclude(Connection))\r\n\t\t{\r\n\t\t\tLocalOnPlayerDeath();\r\n\t\t}\r\n\t}\r\n\r\n\t//////////////////////////////////////////////////////////////////////////////////\r\n\r\n\t[Rpc.Broadcast]\r\n\tprivate void BroadcastPlayerSpawn(PlayerPawn PlayerPawn)\r\n\t{\r\n\t\tScene.Dispatch(new PlayerSpawnedEvent(PlayerPawn));\r\n\t}\r\n\r\n\t[Rpc.Broadcast] // broadcast filter\r\n\tprivate void LocalPlayerSpawn(PlayerPawn PlayerPawn)\r\n\t{\r\n\t\tScene.Dispatch(new LocalPlayerSpawnedEvent(PlayerPawn));\r\n\t}\r\n\r\n\t[Rpc.Broadcast] // broadcast filter\r\n\tpublic void LocalOnPlayerDeath()\r\n\t{\r\n\t\tScene.Dispatch(new LocalPlayerDiedEvent());\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "UI/PlayerDeath/PlayerDeathHUD.razor",
"FileName": "PlayerDeathHUD.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "@namespace KOTH.UI\r\n@using KOTH.Notification\r\n@using Sandbox.Events\r\n@inherits PanelComponent\r\n\r\n@if (!DeathCameraData.IsValid())\r\n{\r\n\treturn;\r\n}\r\n\r\n<root>\r\n\t<div class=\"dead\">\r\n\t\tkilled by:\r\n\t</div>\r\n\t<div class=\"name\">\r\n\t\[email protected]\r\n\t</div>\r\n\r\n\t<div class=\"hp\">\r\n\t\[email protected] HP\r\n\t</div>\r\n</root>\r\n\r\n@code\r\n{\r\n\tpublic static PlayerDeathHUD Instance { get; set; }\r\n\r\n\tpublic PlayerDeathHUD()\r\n\t{\r\n\t\tInstance = this;\r\n\t}\r\n\r\n\tpublic FDeathCameraData DeathCameraData { get; set; }\r\n\r\n\tpublic void SetData(FDeathCameraData Data)\r\n\t{\r\n\t\tDeathCameraData = Data;\r\n\t}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\tbase.OnEnabled();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\t}\r\n\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\treturn HashCode.Combine(Time.Now);\r\n\t}\r\n}"
},
{
"Ident": "wurstsoftware.koth",
"Path": "UI/PlayerHud/Components/EngieOverlay.razor",
"FileName": "EngieOverlay.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "@* @namespace KOTH.UI\r\n@inherits Panel\r\n@attribute [StyleSheet]\r\n\r\n@if (!Player.IsValid() || !EngiePlayer.IsValid() || EngiePlayer.ActiveTurretComponent == null)\r\n{\r\n return;\r\n}\r\n\r\n<root>\r\n <label class=\"building\">@(EngiePlayer.ActiveTurretComponent.DamageComponent.Health)/@(EngiePlayer.ActiveTurretComponent.DamageComponent.MaxBaseHealth)</label>\r\n</root>\r\n\r\n@code\r\n{\r\n public PlayerPawn Player => PlayerState.Local.PlayerPawn;\r\n public EngiePlayer EngiePlayer => Player.GameObject.GetComponent<EngiePlayer>();\r\n\r\n protected override int BuildHash()\r\n {\r\n if (!Player.IsValid())\r\n return 0;\r\n\r\n return HashCode.Combine(Time.Now);\r\n }\r\n}\r\n *@"
},
{
"Ident": "wurstsoftware.koth",
"Path": "UI/PlayerHud/Components/Voices.razor",
"FileName": "Voices.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "@namespace KOTH.UI\r\n@attribute [StyleSheet]\r\n@inherits Panel\r\n\r\n<root class=\"\">\r\n\t@* @foreach (var voice in VoiceList.Where(x => CanDisplay(x)))\r\n\t{\r\n\t\t<div class=\"row\">\r\n\t\t\t<div class=\"item-row with-background\">\r\n\t\t\t\t@{\r\n\t\t\t\t\tvar player = GetPlayer(voice);\r\n\t\t\t\t}\r\n\r\n\t\t\t\t<Icon class=\"square rounded shrink-0\" File=\"/ui/chat/radio.png\" size=\"24\" style=\"transform: scale( @GetAmplitudeScale( voice ) )\" />\r\n\t\t\t\t<img class=\"avatar\" src=\"avatar:@player.SteamId\" />\r\n\t\t\t\t<PlayerPanel PlayerState=@player />\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t} *@\r\n\r\n</root>\r\n\r\n@code\r\n{\r\n\tpublic IEnumerable<PlayerVoiceComponent> VoiceList => Scene.GetAllComponents<PlayerVoiceComponent>().Where(x => x.PlayerState.IsConnected);\r\n\r\n\tprivate PlayerState GetPlayer(Voice voice)\r\n\t{\r\n\t\treturn voice.Components.Get<PlayerState>();\r\n\t}\r\n\r\n\tprivate bool CanDisplay(Voice voice)\r\n\t{\r\n\t\treturn voice.LastPlayed < 0.25f;\r\n\t}\r\n\r\n\tprivate string GetAmplitudeScale(Voice voice)\r\n\t{\r\n\t\tvar scale = 0.5f + (voice.Amplitude * 2.365f);\r\n\t\treturn (1f + scale).ToString(\"0.#\");\r\n\t}\r\n\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\treturn HashCode.Combine(Time.Now);\r\n\t}\r\n}"
},
{
"Ident": "wurstsoftware.koth",
"Path": "UI/PlayerHud/Components/XHair.razor",
"FileName": "XHair.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "@namespace KOTH.UI\r\n@inherits Panel\r\n@attribute [StyleSheet]\r\n\r\n@if ( !Player.IsValid() )\r\n return;\r\n\r\n<root>\r\n <div class=\"circle\"></div>\r\n</root>\r\n\r\n@code\r\n{\r\n /// The player\r\n public PlayerPawn Player => PlayerState.Local.PlayerPawn;\r\n\r\n protected override int BuildHash()\r\n {\r\n if ( !Player.IsValid() || !Player.CurrentEquipment.IsValid() )\r\n return 0;\r\n\r\n return HashCode.Combine(Player, Player.CurrentEquipment);\r\n }\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "Weapons/Components/ProjectileWeaponComponent.cs",
"FileName": "ProjectileWeaponComponent.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using KOTH.UI;\r\nusing Sandbox;\r\nusing Sandbox.Events;\r\nusing System.Net.Http;\r\nusing System;\r\nusing System.Text;\r\nusing Sandbox.Diagnostics;\r\nusing System.Data.Common;\r\n\r\nnamespace KOTH;\r\n\r\n// TODO : this piece of shit needs a proper refactor\r\n\r\n[Title(\"Projectile Shooter\"), Group(\"Weapon Components\")]\r\npublic class ProjectileWeaponComponent : InputWeaponComponent\r\n{\r\n\t[Property, Group(\"Projectile\")] public GameObject ProjectilePrefab { get; set; }\r\n\t[Property, Group(\"Projectile\")] public float ProjectileHorizontalSpeed { get; set; } = 600.0f;\r\n\t[Property, Group(\"Projectile\")] public float ProjectileVerticalSpeed { get; set; } = 0f;\r\n\r\n\t[Property, Group(\"VFX\")] public SoundEvent ShootSound { get; set; }\r\n\r\n\t////////////////////////////////////////////////////////////////////////\r\n\r\n\t// TODO : sticky only, look into doing a better way\r\n\tpublic bool IsStickyLauncher { get => StickyTrackerComponent.IsValid(); }\r\n\tpublic int ActiveStickies { get => StickyTrackerComponent.IsValid() ? StickyTrackerComponent.StickyCount : 0; }\r\n\tprotected StickyTrackerComponent StickyTrackerComponent { get; set; }\r\n\r\n\t////////////////////////////////////////////////////////////////////////\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\r\n\t\t// TODO : revisit, sticky only\r\n\t\tStickyTrackerComponent = Components.Get<StickyTrackerComponent>();\r\n\t}\r\n\r\n\tprotected override void OnInputUpdate()\r\n\t{\r\n\t\tif (!Equipment.Owner.IsValid())\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (!Equipment.Owner.IsLocallyControlled)\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbool IsShooting = IsDown() && CanShoot();\r\n\t\tif (IsShooting)\r\n\t\t{\r\n\t\t\tif (IsReloading && Ammo > 0)\r\n\t\t\t{\r\n\t\t\t\tCancelReload();\r\n\t\t\t}\r\n\t\t\tEquipment.Owner.VFXOnShoot();\r\n\t\t\tShoot();\r\n\t\t}\r\n\t}\r\n\r\n\tprotected virtual GameObject Shoot() // TODO : revisit the return\r\n\t{\r\n\t\tvar PlayerPawn = Equipment.Owner;\r\n\t\tif (!PlayerPawn.IsValid())\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tTimeSinceShot = 0;\r\n\t\tAmmo--;\r\n\t\tWorldShotVFX();\r\n\r\n\t\tvar AimForward = PlayerPawn.AimRay.Forward;\r\n\r\n\t\t// create projectile object from prefab\r\n\t\tvar ProjectilePosition = PlayerPawn.AimRay.Position + (Vector3.Down * 3.33f); // magic\r\n\t\tvar ProjectileRotation = Rotation.LookAt(AimForward);\r\n\t\tvar Projectile = ProjectilePrefab.Clone(ProjectilePosition, ProjectileRotation);\r\n\r\n\t\t// give it the player\r\n\t\tvar ProjectileComponent = Projectile.Components.Get<Projectile>();\r\n\t\tProjectileComponent.OwnerPlayerPawn = PlayerPawn;\r\n\r\n\t\tif (IsStickyLauncher)\r\n\t\t{\r\n\t\t\tStickyTrackerComponent.AddSticky(Projectile.Components.Get<Sticky>());\r\n\t\t}\r\n\r\n\t\tvar Rigidbody = Projectile.Components.Get<Rigidbody>();\r\n\t\tSetProjectileVelocity(Rigidbody, AimForward);\r\n\r\n\t\tProjectile.Root.Tags.Add(\"self\");\r\n\t\tProjectile.NetworkSpawn();\r\n\t\treturn Projectile;\r\n\t}\r\n\r\n\t[Rpc.Broadcast]\r\n\tpublic void WorldShotVFX()\r\n\t{\r\n\t\tif (ShootSound.IsValid())\r\n\t\t{\r\n\t\t\tif (Sound.Play(ShootSound, Equipment.WorldPosition) is { } SoundHandel)\r\n\t\t\t{\r\n\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected virtual void SetProjectileVelocity(Rigidbody ProjectileRigidbody, Vector3 AimForward)\r\n\t{\r\n\t\tProjectileRigidbody.Velocity = AimForward * ProjectileHorizontalSpeed;\r\n\t\tProjectileRigidbody.Velocity += Vector3.Up * ProjectileVerticalSpeed;\r\n\t}\r\n\r\n\tprotected TimeSince TimeSinceShot = new();\r\n\tprotected virtual bool CanShoot()\r\n\t{\r\n\t\t// these 2 should be ensures?\r\n\t\tif (!Equipment.IsValid()) return false;\r\n\t\tif (!Equipment.Owner.IsValid()) return false;\r\n\r\n\t\tif (Equipment.Owner.IsFrozen)\r\n\t\t\treturn false;\r\n\r\n\t\tif (Equipment.Tags.Has(\"equipping\"))\r\n\t\t\treturn false;\r\n\r\n\t\tif (TimeSinceShot < FireRate)\r\n\t\t\treturn false;\r\n\r\n\t\tif (!HasAmmo)\r\n\t\t\treturn false;\r\n\r\n\t\treturn true;\r\n\t}\r\n}\r\n\r\n////////////////////////////////////////////////////////////////////////\r\n\r\n// NOTE : i think this should now just be a 'mode' within the base projectile comp\r\n// TODO : please god fix this\r\n\r\n[Title(\"Sticky Shooter\"), Group(\"Weapon Components\")]\r\npublic class StickyWeaponComponent : ProjectileWeaponComponent\r\n{\r\n\t[Property, Category(\"Sticky\")] public float MaxChargeTime { get; private set; } = 1.6f;\r\n\r\n\t// TODO : THIS IS QUICKLY BECOMING A MESS!\r\n\r\n\tpublic TimeSince TimeSinceInputFirstDown { get; private set; } = new();\r\n\tpublic bool WasInputDownLastTick { get; private set; } = false;\r\n\tprotected override void OnInputUpdate() // TODO : OnInput()?\r\n\t{\r\n\t\tif (!Equipment.Owner.IsValid())\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (!Equipment.Owner.IsLocallyControlled)\r\n\t\t{\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbool KeyDown = IsDown();\r\n\r\n\t\tif (WasInputDownLastTick)\r\n\t\t{\r\n\t\t\tif (!KeyDown && CanShoot())\r\n\t\t\t{\r\n\t\t\t\tif (IsReloading && Ammo > 0)\r\n\t\t\t\t{\r\n\t\t\t\t\tCancelReload();\r\n\t\t\t\t}\r\n\t\t\t\tGameObject Projectile = Shoot();\r\n\t\t\t\tif (Projectile.Components.Get<Sticky>() is Sticky Sticky)\r\n\t\t\t\t{\r\n\t\t\t\t\tEquipment.Owner.OnDeath += Sticky.Destroy;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tif (KeyDown)\r\n\t\t\t{\r\n\t\t\t\tTimeSinceInputFirstDown = 0;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tWasInputDownLastTick = KeyDown;\r\n\t}\r\n\r\n\tprotected override void SetProjectileVelocity(Rigidbody ProjectileRigidbody, Vector3 AimForward)\r\n\t{\r\n\t\tconst float Factor = .66f;\r\n\t\tvar SpeedFactor = 1 + (Math.Min(TimeSinceInputFirstDown, MaxChargeTime) * Factor);\r\n\t\tProjectileRigidbody.Velocity = AimForward * ProjectileHorizontalSpeed * SpeedFactor;\r\n\t\tProjectileRigidbody.Velocity += Vector3.Up * ProjectileVerticalSpeed;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\r\n\t\tbool IsAltFire = Input.Down(\"attack2\");\r\n\t\tif (IsAltFire && StickyTrackerComponent.IsValid())\r\n\t\t{\r\n\t\t\tStickyTrackerComponent.Detonate();\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "Damage/HealingEvent.cs",
"FileName": "HealingEvent.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using Sandbox.Events;\r\n\r\nnamespace KOTH;\r\n\r\npublic record HealingRequestEvent(FHealingRequest HealingRequest) : IGameEvent;\r\npublic record HealingBroadcastEvent(FHealingReceived HealingRequest) : IGameEvent;\r\n\r\npublic record FHealingRequest\r\n{\r\n\tpublic DamageComponent TargetDamageComponent { get; init; }\r\n\tpublic PlayerPawn TargetPlayerPawn { get; init; }\r\n\tpublic PlayerPawn HealerPlayerPawn { get; init; }\r\n\tpublic int BaseHealing { get; init; } = 0;\r\n\tpublic bool AllowOverheal { get; init; } = false;\r\n\r\n\tpublic EHealingType HealingType { get; init; } = EHealingType.Continuous;\r\n\tpublic Vector3 HealingOrigin { get; init; } = Vector3.Zero;\r\n\r\n\tpublic RealTimeSince TimeSinceEvent { get; init; } = 0;\r\n\r\n\tpublic bool IsValid()\r\n\t{\r\n\t\treturn TargetPlayerPawn.IsValid();\r\n\t}\r\n}\r\n\r\npublic record FHealingReceived\r\n{\r\n\tpublic PlayerPawn TargetPlayerPawn { get; init; }\r\n\tpublic PlayerPawn HealerPlayerPawn { get; init; }\r\n\tpublic PlayerState TargetPlayerState { get; init; }\r\n\tpublic PlayerState HealerPlayerState { get; init; }\r\n\tpublic int Heals { get; init; } = 0;\r\n\tpublic EHealingType HealingType { get; init; } = EHealingType.Continuous;\r\n}\r\n\r\npublic enum EHealingType\r\n{\r\n\tContinuous = 0,\r\n\tOneOff,\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/MapDefinition.cs",
"FileName": "MapDefinition.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "namespace KOTH;\r\n\r\n[GameResource(\"koth/Map Definition\", \"map\", \"A map definition\")]\r\npublic partial class MapDefinition : GameResource\r\n{\r\n\t[Property] public string Title { get; set; }\r\n\t[Property] public string Description { get; set; }\r\n\t[Property] public SceneFile SceneFile { get; set; }\r\n\t[Property, ImageAssetPath] public string Background { get; set; }\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/Notification/NotificationManager.cs",
"FileName": "NotificationManager.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "namespace KOTH.Notification;\r\n\r\n/// <summary>\r\n/// Manages the queue and display of notifications in the game.\r\n/// NOTE - only one Notification will show at a time, even if its in different zones\r\n/// </summary>\r\npublic class NotificationManager: SingletonComponent<NotificationManager>\r\n{\r\n\t// Queue of notifications\r\n\tpublic Queue<FNotification> NotificationQueue { get; set; } = new();\r\n\r\n\t// Notification - Time Display Ends\r\n\tpublic KeyValuePair<FNotification, DateTime>? CurrentNotifications { get; set; } = null;\r\n\r\n\t/// <summary>\r\n\t/// Adds a new notification to the notification queue.\r\n\t/// </summary>\r\n\t/// <param name=\"Notification\">The notification to be added to the queue.</param>\r\n\tpublic void AddNotification(FNotification Notification)\r\n\t{\r\n\t\tNotificationQueue.Enqueue(Notification);\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Retrieves the currently active notification if one exists.\r\n\t/// </summary>\r\n\t/// <returns>\r\n\t/// The currently active notification or null if no notification is active.\r\n\t/// </returns>\r\n\tpublic FNotification? GetNotification()\r\n\t{\r\n\t\tif (CurrentNotifications == null) return null;\r\n\r\n\t\treturn CurrentNotifications.Value.Key;\r\n\t}\r\n\r\n\r\n\t/// <summary>\r\n\t/// Updates the state of the notification manager, including processing the notification queue\r\n\t/// and managing the display timing of the current notification.\r\n\t/// </summary>\r\n\tprotected override void OnUpdate()\r\n\t{\r\n if (CurrentNotifications != null && CurrentNotifications.Value.Value <= DateTime.Now)\r\n\t\t{\r\n\t\t\tCurrentNotifications = null;\r\n\t\t}\r\n\r\n\t\tif (CurrentNotifications == null && NotificationQueue.Count > 0)\r\n\t\t{\r\n\t\t\tvar notification = NotificationQueue.Dequeue();\r\n\t\t\tCurrentNotifications = new KeyValuePair<FNotification, DateTime>(notification, DateTime.Now.AddSeconds(notification.Duration));\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/Rules/Koth/Events.cs",
"FileName": "Events.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using Sandbox.Events;\r\n\r\nnamespace KOTH;\r\n\r\npublic record HillCapturedEvent(List<PlayerPawn> PlayerPawns, Team Team, Hill Hill) : IGameEvent;\r\npublic record HillWinEvent(Team Team, Hill Hill) : IGameEvent;\r\npublic record HillCappingEvent(Team Team, float CaptureDelta, Hill Hill) : IGameEvent;\r\npublic record HillDecayCapEvent(float CaptureDelta, Hill Hill) : IGameEvent;\r\n\r\n[Title(\"Hill Captured Event\")]\r\npublic class HillCapturedEventComponent : GameEventComponent<HillCapturedEvent> { }\r\n\r\n[Title(\"Hill Won Event\")]\r\npublic class HillWinEventComponent : GameEventComponent<HillWinEvent> { }\r\n\r\n[Title(\"Hill Capping Event\")]\r\npublic class HillCappingEventComponent : GameEventComponent<HillCappingEvent> { }\r\n\r\n[Title(\"Hill Decay Event\")]\r\npublic class HillDecayCapEventComponent : GameEventComponent<HillDecayCapEvent> { }\r\n"
},
{
"Ident": "wurstsoftware.koth",
"Path": "GameMode/Rules/Koth/KothWinCondition.cs",
"FileName": "KothWinCondition.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 393654,
"Code": "using Sandbox;\r\nusing Sandbox.Events;\r\nusing Sandbox.Utility;\r\n\r\nnamespace KOTH;\r\n\r\npublic sealed class KothWinCondition : Component,\r\n\tIGameEventHandler<HillWinEvent>\r\n{\r\n\t[Property, Sync(SyncFlags.FromHost)]\r\n\tpublic int TargetScore { get; set; } = 3;\r\n\r\n\t[Sync(SyncFlags.FromHost)]\r\n\tpublic NetDictionary<Team, int> Scores { get; private set; } = new();\r\n\r\n\t[Property]\r\n\tpublic StateComponent RoundWinState { get; set; }\r\n\r\n\t[Property]\r\n\tpublic StateComponent TGameWinState { get; set; }\r\n\r\n\t[Property]\r\n\tpublic StateComponent CTGameWinState { get; set; }\r\n\r\n\t///////////////////////////////////////////////////////////////////////////\r\n\r\n\tprotected override void OnAwake()\r\n\t{\r\n\t\tbase.OnAwake();\r\n\r\n\t\tif (Networking.IsHost)\r\n\t\t{\r\n\t\t\t//if (Scores != null)\r\n\t\t\t//{\r\n\t\t\tScores = new()\r\n\t\t\t\t{\r\n\t\t\t\t// TODO : will need to add any extra teams here as well! not pragmatic!\r\n\t\t\t\t{ Team.CounterTerrorist, 0 }, { Team.Terrorist, 0 }, { Team.Unassigned, 0 },\r\n\t\t\t\t};\r\n\t\t\t//}\r\n\t\t}\r\n\r\n\t}\r\n\r\n\tpublic void OnGameEvent(HillWinEvent EventArgs)\r\n\t{\r\n\t\tLog.Info($\"all : scores {Scores[Team.CounterTerrorist]} : {Scores[Team.Terrorist]}\");\r\n\r\n\t\tif (!Networking.IsHost)\r\n\t\t\treturn;\r\n\r\n\t\tvar Hill = EventArgs.Hill;\r\n\t\tif (!Hill.IsValid())\r\n\t\t{\r\n\t\t\tLog.Error(\"Invalid hill captured!\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tTeam WinningTeam = EventArgs.Team;\r\n\t\tvar TeamScore = ++Scores[WinningTeam];\r\n\r\n\t\tif (TeamScore >= TargetScore)\r\n\t\t{\r\n\t\t\tvar NextState = WinningTeam == Team.CounterTerrorist ? CTGameWinState : TGameWinState;\r\n\t\t\tGameMode.Instance.StateMachine.Transition(NextState);\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tGameMode.Instance.StateMachine.Transition(RoundWinState);\r\n\t\t}\r\n\r\n\t\tLog.Info($\"host : scores {Scores[Team.CounterTerrorist]} : {Scores[Team.Terrorist]}\");\r\n\t}\r\n}\r\n"
}
]
}