547 lines
18 KiB
C#
547 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace PeePeeMod
|
|
{
|
|
static class PeeVisual
|
|
{
|
|
const int Steps = 20;
|
|
const int Droplets = 10;
|
|
const float StepTime = 0.04f;
|
|
const float Speed = 7.5f;
|
|
const float SplatterLife = 0.25f;
|
|
static readonly Color PeeColor = new Color(1f, 0.92f, 0.05f, 1f);
|
|
|
|
static readonly Dictionary<int, Stream> _streams = new Dictionary<int, Stream>();
|
|
static readonly Dictionary<int, ushort> _seq = new Dictionary<int, ushort>();
|
|
static readonly List<Puddle> _puddles = new List<Puddle>();
|
|
static readonly List<int> _dropIds = new List<int>();
|
|
static readonly Vector3[] _points = new Vector3[Steps];
|
|
|
|
static Mesh _sphere;
|
|
static Mesh _cube;
|
|
static Material _material;
|
|
static MaterialPropertyBlock _block;
|
|
|
|
sealed class Stream
|
|
{
|
|
public int Id;
|
|
public SkopjePlayer Player;
|
|
public bool Local;
|
|
public Vector3 Aim;
|
|
public float Heard;
|
|
public Transform Hips;
|
|
public bool SearchedHips;
|
|
public GameObject Root;
|
|
public LineRenderer Line;
|
|
public Transform[] Drops;
|
|
public float[] Seeds;
|
|
}
|
|
|
|
sealed class Puddle
|
|
{
|
|
public GameObject Object;
|
|
public float Born;
|
|
public Vector3 Scale;
|
|
}
|
|
|
|
public static Vector3 AimOf(SkopjePlayer player)
|
|
{
|
|
if (player != null && player.IsOwner && player.SkopjeFPSCamera != null && player.SkopjeFPSCamera.mainCamera != null)
|
|
return player.SkopjeFPSCamera.mainCamera.transform.forward;
|
|
if (player != null && player.IsOwner && Camera.main != null)
|
|
return Camera.main.transform.forward;
|
|
|
|
Transform body = Body(player);
|
|
if (body == null)
|
|
return Vector3.forward;
|
|
return (body.forward + Vector3.down * 0.45f).normalized;
|
|
}
|
|
|
|
public static void ShowLocal(SkopjePlayer player, Vector3 aim)
|
|
{
|
|
Show(player, aim, true);
|
|
}
|
|
|
|
public static void ShowRemote(SkopjePlayer player, Vector3 aim)
|
|
{
|
|
Show(player, aim, false);
|
|
}
|
|
|
|
public static bool Accept(int objectId, ushort seq)
|
|
{
|
|
ushort last;
|
|
if (!_seq.TryGetValue(objectId, out last))
|
|
{
|
|
_seq[objectId] = seq;
|
|
return true;
|
|
}
|
|
|
|
ushort delta = (ushort)(seq - last);
|
|
if (delta == 0 || delta >= 32768)
|
|
return false;
|
|
|
|
_seq[objectId] = seq;
|
|
return true;
|
|
}
|
|
|
|
public static void Forget(int objectId)
|
|
{
|
|
_seq.Remove(objectId);
|
|
}
|
|
|
|
public static void Hide(int objectId)
|
|
{
|
|
Stream stream;
|
|
if (!_streams.TryGetValue(objectId, out stream))
|
|
return;
|
|
_streams.Remove(objectId);
|
|
DestroyStream(stream);
|
|
}
|
|
|
|
public static void Clear()
|
|
{
|
|
foreach (Stream stream in _streams.Values)
|
|
DestroyStream(stream);
|
|
_streams.Clear();
|
|
_seq.Clear();
|
|
|
|
for (int i = 0; i < _puddles.Count; i++)
|
|
{
|
|
if (_puddles[i].Object != null)
|
|
UnityEngine.Object.Destroy(_puddles[i].Object);
|
|
}
|
|
|
|
_puddles.Clear();
|
|
}
|
|
|
|
public static void Tick()
|
|
{
|
|
float now = Time.unscaledTime;
|
|
_dropIds.Clear();
|
|
foreach (KeyValuePair<int, Stream> pair in _streams)
|
|
{
|
|
Stream stream = pair.Value;
|
|
if (stream.Player == null)
|
|
{
|
|
_dropIds.Add(pair.Key);
|
|
continue;
|
|
}
|
|
|
|
if (!stream.Local && now - stream.Heard > 0.5f)
|
|
{
|
|
_dropIds.Add(pair.Key);
|
|
continue;
|
|
}
|
|
|
|
Draw(stream);
|
|
}
|
|
|
|
for (int i = 0; i < _dropIds.Count; i++)
|
|
Hide(_dropIds[i]);
|
|
|
|
for (int i = _puddles.Count - 1; i >= 0; i--)
|
|
{
|
|
Puddle puddle = _puddles[i];
|
|
if (puddle.Object == null)
|
|
{
|
|
_puddles.RemoveAt(i);
|
|
continue;
|
|
}
|
|
|
|
float age = now - puddle.Born;
|
|
if (age >= SplatterLife)
|
|
{
|
|
UnityEngine.Object.Destroy(puddle.Object);
|
|
_puddles.RemoveAt(i);
|
|
continue;
|
|
}
|
|
|
|
float shrink = 1f - (age / SplatterLife);
|
|
puddle.Object.transform.localScale = puddle.Scale * Mathf.Max(0.2f, shrink);
|
|
}
|
|
}
|
|
|
|
static void Show(SkopjePlayer player, Vector3 aim, bool local)
|
|
{
|
|
if (player == null || player.NetworkObject == null || !player.NetworkObject.IsSpawned)
|
|
return;
|
|
if (aim.sqrMagnitude < 0.0001f)
|
|
aim = Vector3.forward;
|
|
else
|
|
aim.Normalize();
|
|
|
|
Stream stream;
|
|
if (!_streams.TryGetValue(player.ObjectId, out stream) || stream == null)
|
|
{
|
|
stream = Create(player);
|
|
_streams[player.ObjectId] = stream;
|
|
}
|
|
|
|
stream.Player = player;
|
|
stream.Local = local;
|
|
stream.Aim = aim;
|
|
stream.Heard = Time.unscaledTime;
|
|
}
|
|
|
|
static Stream Create(SkopjePlayer player)
|
|
{
|
|
EnsureArt();
|
|
var stream = new Stream();
|
|
stream.Id = player.ObjectId;
|
|
stream.Player = player;
|
|
stream.Root = new GameObject("PeeStream");
|
|
if (PeePeePlugin.Host != null)
|
|
stream.Root.transform.SetParent(PeePeePlugin.Host.transform, false);
|
|
|
|
var lineObject = new GameObject("Stream");
|
|
lineObject.transform.SetParent(stream.Root.transform, false);
|
|
stream.Line = lineObject.AddComponent<LineRenderer>();
|
|
stream.Line.positionCount = 0;
|
|
stream.Line.startWidth = 0.035f;
|
|
stream.Line.endWidth = 0.018f;
|
|
stream.Line.numCapVertices = 4;
|
|
stream.Line.numCornerVertices = 3;
|
|
stream.Line.useWorldSpace = true;
|
|
stream.Line.shadowCastingMode = ShadowCastingMode.Off;
|
|
stream.Line.receiveShadows = false;
|
|
stream.Line.lightProbeUsage = LightProbeUsage.Off;
|
|
stream.Line.reflectionProbeUsage = ReflectionProbeUsage.Off;
|
|
if (_material != null)
|
|
{
|
|
stream.Line.material = _material;
|
|
Paint(stream.Line.material);
|
|
}
|
|
stream.Line.startColor = PeeColor;
|
|
stream.Line.endColor = PeeColor;
|
|
Paint(stream.Line);
|
|
|
|
stream.Drops = new Transform[Droplets];
|
|
stream.Seeds = new float[Droplets];
|
|
for (int i = 0; i < Droplets; i++)
|
|
{
|
|
stream.Drops[i] = MakeDrop(stream.Root.transform, 0.04f);
|
|
stream.Seeds[i] = i / (float)Droplets;
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
static void Draw(Stream stream)
|
|
{
|
|
if (stream.Line == null)
|
|
return;
|
|
|
|
Vector3 aim = stream.Aim.sqrMagnitude > 0.0001f ? stream.Aim.normalized : Vector3.forward;
|
|
Vector3 origin = Origin(stream, aim);
|
|
Vector3 velocity = aim * Speed;
|
|
Vector3 pos = origin;
|
|
int count = 0;
|
|
bool landed = false;
|
|
RaycastHit landing = default;
|
|
|
|
for (int i = 0; i < Steps; i++)
|
|
{
|
|
Vector3 next = pos + velocity * StepTime + 0.5f * Physics.gravity * (StepTime * StepTime);
|
|
velocity += Physics.gravity * StepTime;
|
|
RaycastHit hit;
|
|
if (HitWorld(pos, next, stream.Player.transform, out hit))
|
|
{
|
|
_points[i] = hit.point;
|
|
count = i + 1;
|
|
landed = true;
|
|
landing = hit;
|
|
break;
|
|
}
|
|
|
|
_points[i] = next;
|
|
pos = next;
|
|
count = i + 1;
|
|
}
|
|
|
|
if (count < 2)
|
|
return;
|
|
|
|
stream.Line.positionCount = count;
|
|
for (int i = 0; i < count; i++)
|
|
stream.Line.SetPosition(i, _points[i]);
|
|
|
|
float now = Time.time;
|
|
for (int i = 0; i < stream.Drops.Length; i++)
|
|
{
|
|
if (stream.Drops[i] == null)
|
|
continue;
|
|
float travel = Mathf.Repeat(now * 1.7f + stream.Seeds[i], 1f);
|
|
Vector3 at = Along(_points, count, travel);
|
|
Vector3 side = Vector3.Cross(aim, Vector3.up);
|
|
if (side.sqrMagnitude < 0.0001f)
|
|
side = Vector3.right;
|
|
side.Normalize();
|
|
at += side * Mathf.Sin((now + stream.Seeds[i]) * 16f) * 0.015f;
|
|
stream.Drops[i].position = at;
|
|
float size = Mathf.Lerp(0.025f, 0.048f, travel);
|
|
stream.Drops[i].localScale = new Vector3(size, size, size);
|
|
}
|
|
|
|
if (landed)
|
|
SpawnPuddle(landing);
|
|
}
|
|
|
|
static Vector3 Origin(Stream stream, Vector3 aim)
|
|
{
|
|
SkopjePlayer player = stream.Player;
|
|
if (player != null && player.IsOwner && !BodyVisible(player))
|
|
{
|
|
Camera cam = null;
|
|
if (player.SkopjeFPSCamera != null)
|
|
cam = player.SkopjeFPSCamera.mainCamera;
|
|
if (cam == null)
|
|
cam = Camera.main;
|
|
if (cam != null)
|
|
return cam.transform.position + cam.transform.forward * 0.55f - cam.transform.up * 0.28f;
|
|
}
|
|
|
|
Transform hips = Hips(stream);
|
|
Vector3 at = hips != null ? hips.position : (player != null ? player.transform.position + Vector3.up * 0.85f : aim);
|
|
return at + aim * 0.16f + Vector3.down * 0.06f;
|
|
}
|
|
|
|
static Transform Hips(Stream stream)
|
|
{
|
|
if (stream.SearchedHips)
|
|
return stream.Hips;
|
|
|
|
stream.SearchedHips = true;
|
|
Transform body = Body(stream.Player);
|
|
if (body == null)
|
|
return null;
|
|
|
|
Transform[] all = body.GetComponentsInChildren<Transform>(true);
|
|
if (all == null)
|
|
return null;
|
|
|
|
for (int i = 0; i < all.Length; i++)
|
|
{
|
|
string name = all[i].name;
|
|
if (name.IndexOf("hip", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("pelvis", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
stream.Hips = all[i];
|
|
return stream.Hips;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static Transform Body(SkopjePlayer player)
|
|
{
|
|
if (player == null)
|
|
return null;
|
|
if (player.ThirdPerson != null)
|
|
return player.ThirdPerson.transform;
|
|
return player.transform;
|
|
}
|
|
|
|
static bool BodyVisible(SkopjePlayer player)
|
|
{
|
|
Renderer renderer = player != null ? player.ThirdPersonRenderer : null;
|
|
return renderer != null && renderer.enabled && renderer.gameObject.activeInHierarchy;
|
|
}
|
|
|
|
static bool HitWorld(Vector3 from, Vector3 to, Transform ignore, out RaycastHit closest)
|
|
{
|
|
closest = default;
|
|
Vector3 delta = to - from;
|
|
float dist = delta.magnitude;
|
|
if (dist < 0.001f)
|
|
return false;
|
|
|
|
RaycastHit[] hits = Physics.RaycastAll(from, delta / dist, dist, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore);
|
|
if (hits == null || hits.Length == 0)
|
|
return false;
|
|
|
|
float best = float.MaxValue;
|
|
bool found = false;
|
|
for (int i = 0; i < hits.Length; i++)
|
|
{
|
|
Collider collider = hits[i].collider;
|
|
if (collider == null)
|
|
continue;
|
|
Transform hit = collider.transform;
|
|
if (ignore != null && (hit == ignore || hit.IsChildOf(ignore)))
|
|
continue;
|
|
if (hits[i].distance < best)
|
|
{
|
|
best = hits[i].distance;
|
|
closest = hits[i];
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
static Vector3 Along(Vector3[] points, int count, float t)
|
|
{
|
|
if (count <= 1)
|
|
return points[0];
|
|
|
|
float total = 0f;
|
|
for (int i = 1; i < count; i++)
|
|
total += Vector3.Distance(points[i - 1], points[i]);
|
|
|
|
float want = total * Mathf.Clamp01(t);
|
|
float walked = 0f;
|
|
for (int i = 1; i < count; i++)
|
|
{
|
|
float seg = Vector3.Distance(points[i - 1], points[i]);
|
|
if (walked + seg >= want)
|
|
{
|
|
float u = seg < 0.0001f ? 0f : (want - walked) / seg;
|
|
return Vector3.Lerp(points[i - 1], points[i], u);
|
|
}
|
|
|
|
walked += seg;
|
|
}
|
|
|
|
return points[count - 1];
|
|
}
|
|
|
|
static void SpawnPuddle(RaycastHit hit)
|
|
{
|
|
EnsureArt();
|
|
if (_cube == null || _material == null)
|
|
return;
|
|
|
|
var puddle = new GameObject("PeePuddle");
|
|
var filter = puddle.AddComponent<MeshFilter>();
|
|
filter.sharedMesh = _cube;
|
|
var renderer = puddle.AddComponent<MeshRenderer>();
|
|
renderer.sharedMaterial = _material;
|
|
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
|
renderer.receiveShadows = false;
|
|
Paint(renderer);
|
|
|
|
Vector3 normal = hit.normal.sqrMagnitude > 0.001f ? hit.normal : Vector3.up;
|
|
puddle.transform.position = hit.point + normal * 0.02f;
|
|
puddle.transform.rotation = Quaternion.FromToRotation(Vector3.up, normal) * Quaternion.Euler(0f, UnityEngine.Random.Range(0f, 360f), 0f);
|
|
float width = UnityEngine.Random.Range(0.16f, 0.3f);
|
|
puddle.transform.localScale = new Vector3(width, 0.012f, width * UnityEngine.Random.Range(0.75f, 1.15f));
|
|
|
|
_puddles.Add(new Puddle
|
|
{
|
|
Object = puddle,
|
|
Born = Time.unscaledTime,
|
|
Scale = puddle.transform.localScale,
|
|
});
|
|
}
|
|
|
|
static Transform MakeDrop(Transform parent, float size)
|
|
{
|
|
var drop = new GameObject("Drop");
|
|
drop.transform.SetParent(parent, false);
|
|
drop.transform.localScale = Vector3.one * size;
|
|
var filter = drop.AddComponent<MeshFilter>();
|
|
filter.sharedMesh = _sphere;
|
|
var renderer = drop.AddComponent<MeshRenderer>();
|
|
renderer.sharedMaterial = _material;
|
|
renderer.shadowCastingMode = ShadowCastingMode.Off;
|
|
renderer.receiveShadows = false;
|
|
Paint(renderer);
|
|
return drop.transform;
|
|
}
|
|
|
|
static void Paint(Renderer renderer)
|
|
{
|
|
if (renderer == null || _material == null)
|
|
return;
|
|
if (_block == null)
|
|
_block = new MaterialPropertyBlock();
|
|
_block.SetColor("_BaseColor", PeeColor);
|
|
_block.SetColor("_Color", PeeColor);
|
|
_block.SetColor("_EmissionColor", PeeColor);
|
|
renderer.SetPropertyBlock(_block);
|
|
}
|
|
|
|
static void Paint(Material material)
|
|
{
|
|
if (material == null)
|
|
return;
|
|
if (material.HasProperty("_BaseColor"))
|
|
material.SetColor("_BaseColor", PeeColor);
|
|
if (material.HasProperty("_Color"))
|
|
material.SetColor("_Color", PeeColor);
|
|
material.color = PeeColor;
|
|
if (material.HasProperty("_EmissionColor"))
|
|
{
|
|
material.EnableKeyword("_EMISSION");
|
|
material.SetColor("_EmissionColor", PeeColor * 1.4f);
|
|
}
|
|
}
|
|
|
|
static Shader YellowShader()
|
|
{
|
|
Shader shader = Shader.Find("Universal Render Pipeline/Unlit");
|
|
if (shader == null)
|
|
shader = Shader.Find("Universal Render Pipeline/Particles/Unlit");
|
|
if (shader == null)
|
|
shader = Shader.Find("Unlit/Color");
|
|
if (shader == null)
|
|
shader = Shader.Find("Sprites/Default");
|
|
return shader;
|
|
}
|
|
|
|
static void EnsureArt()
|
|
{
|
|
if (_material != null && _sphere != null && _cube != null)
|
|
return;
|
|
|
|
if (_material == null)
|
|
{
|
|
Shader shader = YellowShader();
|
|
if (shader != null)
|
|
{
|
|
_material = new Material(shader);
|
|
Paint(_material);
|
|
}
|
|
}
|
|
|
|
if (_sphere == null)
|
|
{
|
|
var temp = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
|
var filter = temp.GetComponent<MeshFilter>();
|
|
if (filter != null)
|
|
_sphere = filter.sharedMesh;
|
|
if (_material == null)
|
|
{
|
|
var renderer = temp.GetComponent<MeshRenderer>();
|
|
if (renderer != null && renderer.sharedMaterial != null)
|
|
{
|
|
_material = new Material(renderer.sharedMaterial);
|
|
Paint(_material);
|
|
}
|
|
}
|
|
|
|
UnityEngine.Object.Destroy(temp);
|
|
}
|
|
|
|
if (_cube == null)
|
|
{
|
|
var temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
|
var filter = temp.GetComponent<MeshFilter>();
|
|
if (filter != null)
|
|
_cube = filter.sharedMesh;
|
|
UnityEngine.Object.Destroy(temp);
|
|
}
|
|
}
|
|
|
|
static void DestroyStream(Stream stream)
|
|
{
|
|
if (stream != null && stream.Root != null)
|
|
UnityEngine.Object.Destroy(stream.Root);
|
|
}
|
|
}
|
|
}
|