PaintSwimming.cs
using Sandbox;
using System;
/// <summary>
/// Paint Swimming (Dive) mechanic — Splatoon-style.
/// When the player stands on/near paint objects (tagged "paint") and holds Shift (Run):
/// - Player model hides
/// - A paint blob visual appears and follows the player
/// - Movement speed increases
/// Releasing Shift or leaving paint area = emerge back to normal.
///
/// Setup in S&Box Inspector:
/// 1. Add this component to the "Player Controller" GameObject
/// 2. Set "Body" = the "Body" child object (with SkinnedModelRenderer)
/// 3. Set "Paint Visual Prefab" = syzygium.prefab (or any paint blob prefab)
/// </summary>
public sealed class PaintSwimming : Component
{
/// <summary>
/// The player's 3D body object (child with SkinnedModelRenderer). Will be hidden when diving.
/// </summary>
[Property, Category( "References" )] public GameObject Body { get; set; }
/// <summary>
/// Prefab to clone as the "paint blob" visual when diving.
/// Example: syzygium.prefab
/// </summary>
[Property, Category( "References" )] public GameObject PaintVisualPrefab { get; set; }
/// <summary>
/// Movement speed while swimming in paint.
/// </summary>
[Property, Category( "Settings" )] public float SwimSpeed { get; set; } = 450f;
/// <summary>
/// Normal walk speed (restored on emerge).
/// </summary>
[Property, Category( "Settings" )] public float NormalWalkSpeed { get; set; } = 110f;
/// <summary>
/// Normal run speed (restored on emerge).
/// </summary>
[Property, Category( "Settings" )] public float NormalRunSpeed { get; set; } = 320f;
/// <summary>
/// Radius around the player to search for paint objects.
/// </summary>
[Property, Category( "Settings" )] public float DetectRadius { get; set; } = 60f;
/// <summary>
/// How fast the swim speed boost decays back to normal after emerging.
/// Higher values = faster decay. Lower values = smoother, longer slide.
/// </summary>
[Property, Category( "Settings" )] public float SpeedDecayRate { get; set; } = 4f;
[RequireComponent] PlayerController Player { get; set; }
bool _isDiving;
public bool IsDiving => _isDiving;
public TimeSince TimeSinceStoppedDiving { get; private set; } = 10f; // Дефолтне значення, щоб не блокувати стрільбу зі старту
GameObject _activePaintVisual;
protected override void OnUpdate()
{
if ( IsProxy ) return;
bool onPaint = CheckIfOnPaint();
bool wantToDive = Input.Down( "Dive" ); // Окрема кнопка, щоб не конфліктувало з присіданням
if ( onPaint && wantToDive )
{
if ( !_isDiving )
StartDiving();
UpdateDiveVisual();
}
else
{
if ( _isDiving )
StopDiving();
}
// Плавно зменшуємо (тушимо) швидкість розгону після виходу з дайву
if ( !_isDiving )
{
if ( Player.WalkSpeed > NormalWalkSpeed )
{
Player.WalkSpeed = Player.WalkSpeed + (NormalWalkSpeed - Player.WalkSpeed) * MathF.Min( 1f, Time.Delta * SpeedDecayRate );
if ( Player.WalkSpeed - NormalWalkSpeed < 0.5f )
Player.WalkSpeed = NormalWalkSpeed;
}
else if ( Player.WalkSpeed < NormalWalkSpeed )
{
Player.WalkSpeed = NormalWalkSpeed;
}
if ( Player.RunSpeed > NormalRunSpeed )
{
Player.RunSpeed = Player.RunSpeed + (NormalRunSpeed - Player.RunSpeed) * MathF.Min( 1f, Time.Delta * SpeedDecayRate );
if ( Player.RunSpeed - NormalRunSpeed < 0.5f )
Player.RunSpeed = NormalRunSpeed;
}
else if ( Player.RunSpeed < NormalRunSpeed )
{
Player.RunSpeed = NormalRunSpeed;
}
}
}
/// <summary>
/// Detect paint by finding nearby GameObjects with tag "paint".
/// Uses a sphere trace that INCLUDES triggers (syzygium has IsTrigger=true).
/// </summary>
bool CheckIfOnPaint()
{
// Використовуємо фізичне трасування сферою навколо гравця,
// щоб перевірити перетин з будь-яким колайдером/тригером з тегом "paint" (наприклад, PlaneCollider на нашому префабі слизу).
var tr = Scene.Trace.Sphere( DetectRadius, WorldPosition, WorldPosition )
.WithTag( "paint" )
.HitTriggers()
.Run();
if ( tr.Hit && tr.GameObject.IsValid() )
{
// Пропускаємо наш власний візуальний ефект дайву
if ( _activePaintVisual.IsValid() && tr.GameObject == _activePaintVisual )
return false;
return true;
}
return false;
}
/// <summary>
/// Dive into paint — hide body, show paint blob, boost speed.
/// </summary>
void StartDiving()
{
_isDiving = true;
// Hide the player's 3D model
if ( Body.IsValid() )
Body.Enabled = false;
// Spawn the paint blob visual from prefab
if ( PaintVisualPrefab.IsValid() && !_activePaintVisual.IsValid() )
{
_activePaintVisual = PaintVisualPrefab.Clone();
_activePaintVisual.WorldPosition = WorldPosition;
_activePaintVisual.Enabled = true;
// Remove "paint" tag from our visual so it doesn't detect itself
_activePaintVisual.Tags.Remove( "paint" );
_activePaintVisual.Tags.Add( "dive_visual" );
// Фарбуємо візуал дайву в колір команди
var playerTeam = GameObject.Components.Get<PlayerTeam>() ?? GameObject.Components.GetInAncestors<PlayerTeam>() ?? GameObject.Components.GetInDescendants<PlayerTeam>();
if ( playerTeam.IsValid() )
{
var color = playerTeam.TeamColor;
var renderers = _activePaintVisual.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants );
foreach ( var r in renderers ) r.Tint = color;
var props = _activePaintVisual.Components.GetAll<Prop>( FindMode.EverythingInSelfAndDescendants );
foreach ( var p in props ) p.Tint = color;
var skinnedRenderers = _activePaintVisual.Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
foreach ( var sr in skinnedRenderers ) sr.Tint = color;
}
}
// Boost speed
Player.WalkSpeed = SwimSpeed;
Player.RunSpeed = SwimSpeed;
}
/// <summary>
/// Emerge from paint — show body, destroy paint blob, restore speed.
/// </summary>
void StopDiving()
{
_isDiving = false;
TimeSinceStoppedDiving = 0f;
// Show the player's 3D model
if ( Body.IsValid() )
Body.Enabled = true;
// Remove the paint blob visual
if ( _activePaintVisual.IsValid() )
{
_activePaintVisual.Destroy();
_activePaintVisual = null;
}
// Швидкість більше не скидається миттєво тут!
// Вона плавно тухне в OnUpdate().
}
/// <summary>
/// Keep the paint blob following the player while diving.
/// </summary>
void UpdateDiveVisual()
{
if ( !_activePaintVisual.IsValid() ) return;
_activePaintVisual.WorldPosition = WorldPosition;
_activePaintVisual.WorldRotation = WorldRotation;
}
/// <summary>
/// Safety: if this component is disabled or destroyed while diving, restore the body and speed.
/// </summary>
protected override void OnDisabled()
{
if ( _isDiving )
StopDiving();
if ( Player.IsValid() )
{
Player.WalkSpeed = NormalWalkSpeed;
Player.RunSpeed = NormalRunSpeed;
}
}
protected override void OnDestroy()
{
if ( _isDiving )
StopDiving();
}
}