PaintProjectile.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
public sealed class PaintProjectile : Component
{
[Property, Category( "Paint" )] public GameObject DecalPrefab { get; set; }
[Property, Category( "Paint" )] public float LifeTime { get; set; } = 4f;
[Property, Category( "Spawn Variety" )] public float MinSpawnScale { get; set; } = 0.8f;
[Property, Category( "Spawn Variety" )] public float MaxSpawnScale { get; set; } = 1.5f;
[Property, Category( "Merge Settings" )] public float BaseMergeRadius { get; set; } = 40f;
[Property, Category( "Merge Settings" )] public float ScaleMergeMultiplier { get; set; } = 35f;
[Property, Category( "Merge Settings" )] public float MaxMergeRadius { get; set; } = 200f;
[Property, Category( "Grid Optimization" )] public float CellSize { get; set; } = 50f;
public Vector3 Velocity { get; set; }
public Color PaintColor { get; set; } = Color.White;
public int TeamId { get; set; } = 0;
public GameObject Shooter { get; set; }
public SoundEvent HitSound { get; set; }
private TimeSince timeSinceCreated;
public static readonly Dictionary<(int X, int Y, int Z), GameObject> PaintGrid = new();
protected override void OnStart()
{
timeSinceCreated = 0;
}
protected override void OnUpdate()
{
if ( timeSinceCreated >= LifeTime )
{
GameObject.Destroy();
return;
}
Velocity += Vector3.Down * 800f * Time.Delta;
// ОНОВЛЕНО: Використовуємо WorldPosition замість Transform.Position
var startPos = WorldPosition;
var endPos = startPos + Velocity * Time.Delta;
var tr = Scene.Trace.Ray( startPos, endPos )
.Radius( 8f )
.IgnoreGameObjectHierarchy( GameObject )
.IgnoreGameObjectHierarchy( Shooter )
.WithoutTags( "paint" )
.Run();
if ( tr.Hit )
{
if ( HitSound != null )
{
Sound.Play( HitSound, tr.HitPosition );
}
// Перевіряємо, чи влучили в гравця (шукаємо PlayerTeam в ієрархії об'єкта)
var hitPlayerTeam = tr.GameObject.Components.Get<PlayerTeam>() ?? tr.GameObject.Components.GetInAncestors<PlayerTeam>();
if ( !hitPlayerTeam.IsValid() )
{
// Якщо не знайшли безпосередньо, шукаємо через PlayerController
var playerController = tr.GameObject.Components.Get<PlayerController>() ?? tr.GameObject.Components.GetInAncestors<PlayerController>();
if ( playerController.IsValid() )
{
hitPlayerTeam = playerController.GameObject.Components.Get<PlayerTeam>() ?? playerController.GameObject.Components.GetInDescendants<PlayerTeam>();
}
}
if ( hitPlayerTeam.IsValid() )
{
if ( hitPlayerTeam.TeamId != TeamId )
{
// Шукаємо компонент PaintSwimming, щоб перевірити, чи гравець у формі слизу
var pc = hitPlayerTeam.GameObject.Components.Get<PlayerController>() ?? hitPlayerTeam.GameObject.Components.GetInAncestors<PlayerController>();
var searchObj = pc.IsValid() ? pc.GameObject : hitPlayerTeam.GameObject;
var swimming = searchObj.Components.Get<PaintSwimming>() ?? searchObj.Components.GetInDescendants<PaintSwimming>();
if ( swimming.IsValid() && swimming.IsDiving )
{
// Якщо гравець у формі слизу — моментальна смерть
hitPlayerTeam.TakeDamage( 100f );
}
else
{
// Звичайне попадання (5 пострілів = смерть)
hitPlayerTeam.TakeDamage( 20f );
}
// Знищуємо снаряд
GameObject.Destroy();
return;
}
else
{
// Якщо влучили у тіммейта — пролітаємо крізь нього далі!
WorldPosition = endPos;
return;
}
}
int gX = (int)MathF.Round( tr.HitPosition.x / CellSize );
int gY = (int)MathF.Round( tr.HitPosition.y / CellSize );
int gZ = (int)MathF.Round( tr.HitPosition.z / CellSize );
var gridKey = (gX, gY, gZ);
// Шукаємо найближчий блоб з урахуванням його розміру
// Більший блоб = більший радіус злиття
GameObject nearestPaint = null;
float nearestDist = float.MaxValue;
int searchRange = Convert.ToInt32(MathF.Max( 3, (int)MathF.Ceiling( MaxMergeRadius / CellSize )));
for ( int dx = -searchRange; dx <= searchRange; dx++ )
for ( int dy = -searchRange; dy <= searchRange; dy++ )
for ( int dz = -searchRange; dz <= searchRange; dz++ )
{
var neighborKey = (gX + dx, gY + dy, gZ + dz);
if ( PaintGrid.TryGetValue( neighborKey, out var paint ) && paint.IsValid() )
{
float dist = paint.WorldPosition.Distance( tr.HitPosition );
// Радіус злиття залежить від поточного масштабу блобу
float blobScale = paint.LocalScale.x;
float mergeRadius = BaseMergeRadius + ( blobScale * ScaleMergeMultiplier );
if ( dist < mergeRadius && dist < nearestDist )
{
nearestDist = dist;
nearestPaint = paint;
}
}
}
if ( nearestPaint != null )
{
var blob = nearestPaint.Components.Get<PaintBlob>();
if ( blob.IsValid() )
{
if ( blob.TeamId == TeamId )
{
// СВОЯ КОМАНДА: злиття та ріст плям
var currentScale = nearestPaint.LocalScale;
float growthStep = 0.15f;
float maxScale = 3.5f;
if ( currentScale.x < maxScale )
{
// Зсуваємо позицію тільки поки блоб ще росте
float lerpFactor = 0.15f * ( 1f - ( currentScale.x / maxScale ) );
nearestPaint.WorldPosition = Vector3.Lerp( nearestPaint.WorldPosition, tr.HitPosition, lerpFactor );
nearestPaint.LocalScale = new Vector3(
currentScale.x + growthStep,
currentScale.y + growthStep,
currentScale.z
);
}
}
else
{
// ВОРОЖА КОМАНДА: ворожий слайм зникає, а на його місці з'являється наш слайм!
// 1. Видаляємо ворожу пляму з сітки та знищуємо її GameObject
foreach ( var kvp in PaintGrid.Where( x => x.Value == nearestPaint ).ToList() )
{
PaintGrid.Remove( kvp.Key );
}
nearestPaint.Destroy();
// 2. Спавнимо нову пляму нашої команди на її місці
if ( DecalPrefab.IsValid() )
{
var decal = DecalPrefab.Clone();
decal.WorldPosition = tr.HitPosition;
decal.WorldRotation = Rotation.LookAt( tr.Normal ) * Rotation.FromPitch( 90 ) * Rotation.FromYaw( Random.Shared.Float( 0f, 360f ) );
float randomScale = Random.Shared.Float( MinSpawnScale, MaxSpawnScale );
decal.LocalScale = new Vector3( randomScale, randomScale, randomScale );
var renderer = decal.Components.Get<ModelRenderer>();
if ( renderer.IsValid() )
{
renderer.Tint = PaintColor;
}
var blobInfo = decal.Components.Create<PaintBlob>();
blobInfo.TeamId = TeamId;
PaintGrid[gridKey] = decal;
}
}
}
}
else
{
if ( DecalPrefab.IsValid() )
{
var decal = DecalPrefab.Clone();
decal.WorldPosition = tr.HitPosition;
decal.WorldRotation = Rotation.LookAt( tr.Normal ) * Rotation.FromPitch( 90 ) * Rotation.FromYaw( Random.Shared.Float( 0f, 360f ) );
float randomScale = Random.Shared.Float( MinSpawnScale, MaxSpawnScale );
decal.LocalScale = new Vector3( randomScale, randomScale, randomScale );
// Фарбуємо слиз-декаль у колір команди
var renderer = decal.Components.Get<ModelRenderer>();
if ( renderer.IsValid() )
{
renderer.Tint = PaintColor;
}
// Додаємо інформацію про команду
var blob = decal.Components.Create<PaintBlob>();
blob.TeamId = TeamId;
PaintGrid[gridKey] = decal;
}
}
GameObject.Destroy();
}
else
{
// ОНОВЛЕНО: Снаряд летить далі через WorldPosition
WorldPosition = endPos;
}
}
}