提交下
This commit is contained in:
@@ -2,150 +2,113 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
public class GearFollower : MonoBehaviour
|
||||
[RequireComponent(typeof(LineRenderer))]
|
||||
public class GearFollowerWithVisualCut : MonoBehaviour
|
||||
{
|
||||
[Header("参数")]
|
||||
public SpriteRenderer targetSpriteRenderer; // 目标图片
|
||||
public float snapDistance = 0.3f; // 距离边缘多少时吸附
|
||||
public float detachDistance = 1.0f; // 超过多少距离脱离牵引
|
||||
public float tractionSpeed = 5f; // 牵引时沿边缘移动速度
|
||||
public float rotationSpeed = 360f; // 齿轮旋转速度(度/秒)
|
||||
public float smoothTime = 0.1f; // 平滑过渡时间
|
||||
public float edgeSmoothTime = 0.02f; // 边缘移动平滑时间
|
||||
public float edgeScaleFactor = 1.1f; // 边缘点采样放大倍数
|
||||
public int interpolationPoints = 0; // 每两个采样点之间插入的点数
|
||||
public float cutTimeThreshold = 0.5f; // 切割时间阈值
|
||||
public Color normalColor = Color.white; // 正常状态颜色
|
||||
public Color tractionColor = Color.yellow; // 牵引状态颜色
|
||||
public Color pathColor = new Color(1f, 1f, 1f, 0.3f); // 路径颜色
|
||||
public Color cutPathColor = new Color(0f, 1f, 0f, 0.5f); // 已切割路径颜色
|
||||
public float pathWidth = 0.05f; // 路径宽度
|
||||
public SpriteRenderer targetSpriteRenderer;
|
||||
public float snapDistance = 0.3f;
|
||||
public float detachDistance = 1.0f;
|
||||
public float tractionSpeed = 2f;
|
||||
public float rotationSpeed = 360f;
|
||||
public float edgeScaleFactor = 1.1f;
|
||||
|
||||
[Header("反馈效果")]
|
||||
public float speedMultiplier = 1f; // 切割时速度倍率
|
||||
|
||||
private List<Vector2> edgePoints; // 边缘路径点
|
||||
private int currentEdgeIndex = 0; // 当前吸附边缘点索引
|
||||
private SpriteRenderer gearSpriteRenderer; // 齿轮的SpriteRenderer
|
||||
private Vector2 currentVelocity; // 用于平滑移动
|
||||
private float currentRotationVelocity; // 用于平滑旋转
|
||||
private LineRenderer pathRenderer; // 路径渲染器
|
||||
private float currentPathProgress; // 当前路径进度
|
||||
private List<bool> cutStates; // 记录每个点是否被切割
|
||||
private List<float> cutTimes; // 记录每个点的切割时间
|
||||
private float currentCutTime; // 当前点的切割时间
|
||||
|
||||
private enum State { Follow, Traction }
|
||||
private State currentState = State.Follow;
|
||||
public Color normalEdgeColor = Color.white;
|
||||
public Color cutEdgeColor = Color.yellow;
|
||||
|
||||
private List<Vector2> edgePoints;
|
||||
private int currentEdgeIndex = 0;
|
||||
private SpriteRenderer gearSpriteRenderer;
|
||||
private Camera mainCamera;
|
||||
|
||||
private Vector2 targetEdgePosition; // 目标边缘位置
|
||||
private float currentEdgeProgress; // 当前边缘移动进度
|
||||
private Vector2 imageCenter; // 图片中心点
|
||||
private float imageRadius; // 图片半径
|
||||
private Vector2 lastMousePosition; // 上一帧的鼠标位置
|
||||
private enum State { Follow, Attract, Traction }
|
||||
private State currentState = State.Follow;
|
||||
|
||||
private float currentSpeed; // 当前速度
|
||||
private float targetSpeed; // 目标速度
|
||||
private float screenShakeTimer; // 屏幕震动计时器
|
||||
private ParticleSystem cutParticleSystem; // 切割粒子系统
|
||||
private AudioSource cutAudioSource; // 切割音效
|
||||
private bool[] cutSegments;
|
||||
|
||||
private LineRenderer edgeLineRenderer; // 绘制全部边缘
|
||||
private LineRenderer cutLineRenderer; // 绘制切割线段
|
||||
|
||||
void Start()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
gearSpriteRenderer = GetComponent<SpriteRenderer>();
|
||||
if (gearSpriteRenderer == null)
|
||||
{
|
||||
Debug.LogError("齿轮对象缺少SpriteRenderer组件");
|
||||
}
|
||||
|
||||
// 计算图片中心点和半径
|
||||
if (targetSpriteRenderer != null)
|
||||
{
|
||||
imageCenter = targetSpriteRenderer.bounds.center;
|
||||
// 使用bounds的size计算半径(取较大的值)
|
||||
imageRadius = Mathf.Max(targetSpriteRenderer.bounds.size.x, targetSpriteRenderer.bounds.size.y) * 0.5f;
|
||||
}
|
||||
|
||||
// 添加并设置LineRenderer
|
||||
pathRenderer = gameObject.AddComponent<LineRenderer>();
|
||||
pathRenderer.startWidth = pathWidth;
|
||||
pathRenderer.endWidth = pathWidth;
|
||||
pathRenderer.startColor = pathColor;
|
||||
pathRenderer.endColor = pathColor;
|
||||
pathRenderer.material = new Material(Shader.Find("Sprites/Default"));
|
||||
pathRenderer.sortingOrder = 0;
|
||||
pathRenderer.enabled = false;
|
||||
|
||||
LoadEdgePointsFromSprite();
|
||||
|
||||
cutSegments = new bool[edgePoints.Count - 1];
|
||||
|
||||
// 设置主边缘线条渲染器
|
||||
edgeLineRenderer = GetComponent<LineRenderer>();
|
||||
edgeLineRenderer.positionCount = edgePoints.Count;
|
||||
edgeLineRenderer.loop = false;
|
||||
edgeLineRenderer.startWidth = 0.05f;
|
||||
edgeLineRenderer.endWidth = 0.05f;
|
||||
edgeLineRenderer.material = new Material(Shader.Find("Sprites/Default"));
|
||||
edgeLineRenderer.startColor = normalEdgeColor;
|
||||
edgeLineRenderer.endColor = normalEdgeColor;
|
||||
edgeLineRenderer.sortingOrder = 3;
|
||||
|
||||
for (int i = 0; i < edgePoints.Count; i++)
|
||||
{
|
||||
edgeLineRenderer.SetPosition(i, edgePoints[i]);
|
||||
}
|
||||
|
||||
// 创建用于切割线段的LineRenderer
|
||||
GameObject cutLineObj = new GameObject("CutLineRenderer");
|
||||
cutLineObj.transform.parent = this.transform;
|
||||
cutLineRenderer = cutLineObj.AddComponent<LineRenderer>();
|
||||
cutLineRenderer.positionCount = 0;
|
||||
cutLineRenderer.loop = false;
|
||||
cutLineRenderer.startWidth = 0.07f;
|
||||
cutLineRenderer.endWidth = 0.07f;
|
||||
cutLineRenderer.material = new Material(Shader.Find("Sprites/Default"));
|
||||
cutLineRenderer.startColor = cutEdgeColor;
|
||||
cutLineRenderer.endColor = cutEdgeColor;
|
||||
cutLineRenderer.sortingOrder = 4;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
Vector2 mouseWorldPos = mainCamera.ScreenToWorldPoint(Input.mousePosition);
|
||||
Vector2 mouseDelta = mouseWorldPos - lastMousePosition;
|
||||
lastMousePosition = mouseWorldPos;
|
||||
|
||||
switch (currentState)
|
||||
{
|
||||
case State.Follow:
|
||||
UpdateFollowState(mouseWorldPos);
|
||||
break;
|
||||
case State.Attract:
|
||||
UpdateAttractState(mouseWorldPos);
|
||||
break;
|
||||
case State.Traction:
|
||||
UpdateTractionState(mouseWorldPos);
|
||||
break;
|
||||
}
|
||||
// 更新齿轮旋转
|
||||
UpdateGearRotation();
|
||||
// 更新视觉反馈
|
||||
UpdateVisualFeedback();
|
||||
// 更新路径显示
|
||||
UpdatePathDisplay();
|
||||
|
||||
UpdateGearRotation();
|
||||
UpdateVisualFeedback();
|
||||
UpdateCutLineRenderer();
|
||||
CheckAllSegmentsCut();
|
||||
}
|
||||
|
||||
// 更新齿轮旋转
|
||||
private void UpdateGearRotation()
|
||||
{
|
||||
if (currentState == State.Traction && Input.GetMouseButton(0))
|
||||
{
|
||||
// 在牵引状态下按住左键时旋转
|
||||
transform.Rotate(0, 0, rotationSpeed * Time.deltaTime);
|
||||
|
||||
// 更新当前点的切割时间
|
||||
currentCutTime += Time.deltaTime;
|
||||
if (currentCutTime >= cutTimeThreshold)
|
||||
{
|
||||
if (!cutStates[currentEdgeIndex])
|
||||
{
|
||||
// 触发切割效果
|
||||
//TriggerCutEffects();
|
||||
}
|
||||
cutStates[currentEdgeIndex] = true;
|
||||
cutTimes[currentEdgeIndex] = currentCutTime;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
currentCutTime = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新视觉反馈
|
||||
private void UpdateVisualFeedback()
|
||||
{
|
||||
if (gearSpriteRenderer != null)
|
||||
{
|
||||
gearSpriteRenderer.color = currentState == State.Traction ? tractionColor : normalColor;
|
||||
gearSpriteRenderer.color = (currentState == State.Traction || currentState == State.Attract) ? cutEdgeColor : normalEdgeColor;
|
||||
}
|
||||
}
|
||||
|
||||
// 从Sprite的PhysicsShape获取边缘点,构成闭环路径
|
||||
private void LoadEdgePointsFromSprite()
|
||||
{
|
||||
List<Vector2> originalPoints = new List<Vector2>();
|
||||
edgePoints = new List<Vector2>();
|
||||
if (targetSpriteRenderer == null)
|
||||
{
|
||||
Debug.LogError("targetSpriteRenderer 为空");
|
||||
@@ -161,7 +124,6 @@ public class GearFollower : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算形状的中心点
|
||||
Vector2 center = Vector2.zero;
|
||||
foreach (Vector2 point in shapePoints)
|
||||
{
|
||||
@@ -169,191 +131,129 @@ public class GearFollower : MonoBehaviour
|
||||
}
|
||||
center /= shapePoints.Count;
|
||||
|
||||
// 转换为世界坐标并应用放大
|
||||
originalPoints = shapePoints
|
||||
.Select(p => {
|
||||
edgePoints = shapePoints
|
||||
.Select(p =>
|
||||
{
|
||||
Vector2 dirFromCenter = p - center;
|
||||
Vector2 scaledPoint = center + dirFromCenter * edgeScaleFactor;
|
||||
return (Vector2)targetSpriteRenderer.transform.TransformPoint(scaledPoint);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// 确保首尾连通
|
||||
if (originalPoints[0] != originalPoints[originalPoints.Count - 1])
|
||||
if (edgePoints[0] != edgePoints[edgePoints.Count - 1])
|
||||
{
|
||||
originalPoints.Add(originalPoints[0]);
|
||||
}
|
||||
|
||||
// 生成插值点
|
||||
edgePoints = new List<Vector2>();
|
||||
for (int i = 0; i < originalPoints.Count - 1; i++)
|
||||
{
|
||||
Vector2 start = originalPoints[i];
|
||||
Vector2 end = originalPoints[i + 1];
|
||||
|
||||
// 添加起始点
|
||||
edgePoints.Add(start);
|
||||
|
||||
// 在两点之间插入插值点
|
||||
for (int j = 1; j <= interpolationPoints; j++)
|
||||
{
|
||||
float t = j / (float)(interpolationPoints + 1);
|
||||
Vector2 interpolatedPoint = Vector2.Lerp(start, end, t);
|
||||
edgePoints.Add(interpolatedPoint);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最后一个点
|
||||
edgePoints.Add(originalPoints[originalPoints.Count - 1]);
|
||||
|
||||
// 初始化切割状态数组
|
||||
cutStates = new List<bool>(new bool[edgePoints.Count]);
|
||||
cutTimes = new List<float>(new float[edgePoints.Count]);
|
||||
|
||||
// 更新路径渲染器
|
||||
UpdatePathRenderer();
|
||||
}
|
||||
|
||||
private void UpdatePathRenderer()
|
||||
{
|
||||
if (pathRenderer != null)
|
||||
{
|
||||
pathRenderer.positionCount = edgePoints.Count;
|
||||
for (int i = 0; i < edgePoints.Count; i++)
|
||||
{
|
||||
pathRenderer.SetPosition(i, new Vector3(edgePoints[i].x, edgePoints[i].y, 0));
|
||||
}
|
||||
edgePoints.Add(edgePoints[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// 跟随模式逻辑
|
||||
private void UpdateFollowState(Vector2 mousePos)
|
||||
{
|
||||
// 使用平滑移动
|
||||
transform.position = Vector2.SmoothDamp(transform.position, mousePos, ref currentVelocity, smoothTime);
|
||||
transform.position = mousePos;
|
||||
|
||||
// 检测距离最近边缘点是否在snapDistance内,进入牵引
|
||||
int nearestIndex = FindNearestEdgePointIndex(mousePos, out float dist);
|
||||
|
||||
if (dist < snapDistance)
|
||||
{
|
||||
currentEdgeIndex = nearestIndex;
|
||||
currentState = State.Traction;
|
||||
currentPathProgress = 0f;
|
||||
|
||||
// 立即吸附到最近的边缘点
|
||||
transform.position = edgePoints[currentEdgeIndex];
|
||||
|
||||
currentState = State.Attract;
|
||||
if (gearSpriteRenderer != null)
|
||||
{
|
||||
gearSpriteRenderer.sortingOrder = 1;
|
||||
}
|
||||
pathRenderer.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 牵引模式逻辑
|
||||
private void UpdateTractionState(Vector2 mousePos)
|
||||
private void UpdateAttractState(Vector2 mousePos)
|
||||
{
|
||||
Vector2 gearPos = (Vector2)transform.position;
|
||||
Vector2 closestPoint = Vector2.zero;
|
||||
float closestDist = float.MaxValue;
|
||||
int closestSegmentStartIndex = 0;
|
||||
|
||||
// 计算鼠标到齿轮的距离
|
||||
float distToGear = Vector2.Distance(mousePos, gearPos);
|
||||
|
||||
// 计算鼠标到图片中心的距离
|
||||
float distToCenter = Vector2.Distance(mousePos, imageCenter);
|
||||
|
||||
// 如果鼠标在图片内部(距离中心小于半径),或者距离齿轮太近,则不脱离吸附
|
||||
if (distToGear > detachDistance && distToCenter > imageRadius)
|
||||
for (int i = 0; i < edgePoints.Count - 1; i++)
|
||||
{
|
||||
Vector2 a = edgePoints[i];
|
||||
Vector2 b = edgePoints[i + 1];
|
||||
Vector2 projected = ProjectPointOnSegment(mousePos, a, b);
|
||||
float dist = Vector2.Distance(mousePos, projected);
|
||||
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
closestPoint = projected;
|
||||
closestSegmentStartIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
transform.position = closestPoint;
|
||||
currentEdgeIndex = closestSegmentStartIndex;
|
||||
|
||||
if (Vector2.Distance(mousePos, closestPoint) > detachDistance)
|
||||
{
|
||||
currentState = State.Follow;
|
||||
if (gearSpriteRenderer != null)
|
||||
{
|
||||
gearSpriteRenderer.sortingOrder = 5;
|
||||
}
|
||||
pathRenderer.enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是否按住左键
|
||||
if (!Input.GetMouseButton(0))
|
||||
if (Input.GetMouseButton(0))
|
||||
{
|
||||
// 未按键,齿轮固定在当前位置
|
||||
return;
|
||||
currentState = State.Traction;
|
||||
}
|
||||
|
||||
// 沿路径移动
|
||||
MoveAlongPath(mousePos);
|
||||
}
|
||||
|
||||
// 沿路径移动的逻辑
|
||||
private void MoveAlongPath(Vector2 mousePos)
|
||||
private void UpdateTractionState(Vector2 mousePos)
|
||||
{
|
||||
// 计算鼠标到当前齿轮位置的距离
|
||||
float distToGear = Vector2.Distance(mousePos, transform.position);
|
||||
|
||||
// 获取当前段落的起点和终点
|
||||
Vector2 startPoint = edgePoints[currentEdgeIndex];
|
||||
Vector2 endPoint = edgePoints[(currentEdgeIndex + 1) % edgePoints.Count];
|
||||
|
||||
// 计算鼠标到当前段落两端点的距离
|
||||
float distToStart = Vector2.Distance(mousePos, startPoint);
|
||||
float distToEnd = Vector2.Distance(mousePos, endPoint);
|
||||
|
||||
// 计算移动方向
|
||||
float moveDirection = 0f;
|
||||
|
||||
// 获取鼠标相对于当前段落的向量
|
||||
Vector2 mouseToStart = mousePos - startPoint;
|
||||
Vector2 segmentVector = endPoint - startPoint;
|
||||
|
||||
// 计算叉积来判断方向
|
||||
float crossProduct = mouseToStart.x * segmentVector.y - mouseToStart.y * segmentVector.x;
|
||||
|
||||
// 根据叉积的符号决定移动方向
|
||||
moveDirection = crossProduct > 0 ? -1f : 1f;
|
||||
|
||||
// 计算移动速度
|
||||
float moveSpeed = tractionSpeed * (cutStates[currentEdgeIndex] ? speedMultiplier : 1f);
|
||||
|
||||
// 使用固定速度更新移动
|
||||
float maxProgressChange = moveSpeed * Time.deltaTime;
|
||||
float targetProgress = currentPathProgress + moveDirection * maxProgressChange;
|
||||
|
||||
// 处理段落切换
|
||||
if (targetProgress >= 1f)
|
||||
if (Vector2.Distance(mousePos, transform.position) > detachDistance)
|
||||
{
|
||||
currentEdgeIndex = (currentEdgeIndex + 1) % edgePoints.Count;
|
||||
targetProgress = 0f;
|
||||
currentCutTime = 0f; // 重置切割时间
|
||||
currentState = State.Follow;
|
||||
if (gearSpriteRenderer != null)
|
||||
gearSpriteRenderer.sortingOrder = 5;
|
||||
return;
|
||||
}
|
||||
else if (targetProgress <= 0f)
|
||||
|
||||
if (Input.GetMouseButton(0))
|
||||
{
|
||||
// 修改逆时针移动的处理逻辑
|
||||
currentEdgeIndex = (currentEdgeIndex - 1 + edgePoints.Count) % edgePoints.Count;
|
||||
targetProgress = 1f;
|
||||
currentCutTime = 0f; // 重置切割时间
|
||||
|
||||
// 确保不会出现负值
|
||||
if (currentEdgeIndex < 0)
|
||||
MoveAlongEdge(mousePos);
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveAlongEdge(Vector2 mousePos)
|
||||
{
|
||||
float closestDist = float.MaxValue;
|
||||
Vector2 closestPoint = Vector2.zero;
|
||||
int closestSegmentStartIndex = 0;
|
||||
|
||||
for (int i = 0; i < edgePoints.Count - 1; i++)
|
||||
{
|
||||
Vector2 a = edgePoints[i];
|
||||
Vector2 b = edgePoints[i + 1];
|
||||
|
||||
Vector2 projected = ProjectPointOnSegment(mousePos, a, b);
|
||||
float dist = Vector2.Distance(mousePos, projected);
|
||||
|
||||
if (dist < closestDist)
|
||||
{
|
||||
currentEdgeIndex = edgePoints.Count - 1;
|
||||
closestDist = dist;
|
||||
closestPoint = projected;
|
||||
closestSegmentStartIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
currentPathProgress = targetProgress;
|
||||
float stopThreshold = 0.05f;
|
||||
if (Vector2.Distance(transform.position, closestPoint) < stopThreshold)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算目标位置
|
||||
startPoint = edgePoints[currentEdgeIndex];
|
||||
endPoint = edgePoints[(currentEdgeIndex + 1) % edgePoints.Count];
|
||||
Vector2 targetPos = Vector2.Lerp(startPoint, endPoint, currentPathProgress);
|
||||
transform.position = Vector2.MoveTowards(transform.position, closestPoint, tractionSpeed * Time.deltaTime);
|
||||
currentEdgeIndex = closestSegmentStartIndex;
|
||||
|
||||
// 使用平滑移动
|
||||
transform.position = Vector2.SmoothDamp(transform.position, targetPos, ref currentVelocity, edgeSmoothTime);
|
||||
// 标记当前线段已被切割
|
||||
if (!cutSegments[currentEdgeIndex])
|
||||
{
|
||||
cutSegments[currentEdgeIndex] = true;
|
||||
Debug.Log($"切割了线段 {currentEdgeIndex} ({edgePoints[currentEdgeIndex]}->{edgePoints[currentEdgeIndex + 1]})");
|
||||
}
|
||||
}
|
||||
|
||||
// 找到距离pos最近的边缘点索引和距离
|
||||
private int FindNearestEdgePointIndex(Vector2 pos, out float minDist)
|
||||
{
|
||||
int nearestIndex = 0;
|
||||
@@ -371,19 +271,50 @@ public class GearFollower : MonoBehaviour
|
||||
return nearestIndex;
|
||||
}
|
||||
|
||||
// 更新路径显示
|
||||
private void UpdatePathDisplay()
|
||||
private Vector2 ProjectPointOnSegment(Vector2 point, Vector2 a, Vector2 b)
|
||||
{
|
||||
if (pathRenderer != null && pathRenderer.enabled)
|
||||
Vector2 ab = b - a;
|
||||
float abLengthSquared = ab.sqrMagnitude;
|
||||
if (abLengthSquared == 0f) return a;
|
||||
|
||||
float t = Vector2.Dot(point - a, ab) / abLengthSquared;
|
||||
t = Mathf.Clamp01(t);
|
||||
return a + t * ab;
|
||||
}
|
||||
|
||||
private void UpdateCutLineRenderer()
|
||||
{
|
||||
// 统计所有已切割线段点数 = 2*切割线段数(start和end)
|
||||
int count = cutSegments.Count(cut => cut);
|
||||
if (count == 0)
|
||||
{
|
||||
for (int i = 0; i < edgePoints.Count; i++)
|
||||
cutLineRenderer.positionCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 每个切割线段2个点
|
||||
cutLineRenderer.positionCount = count * 2;
|
||||
|
||||
int index = 0;
|
||||
for (int i = 0; i < cutSegments.Length; i++)
|
||||
{
|
||||
if (cutSegments[i])
|
||||
{
|
||||
Color segmentColor = cutStates[i] ? cutPathColor : pathColor;
|
||||
pathRenderer.startColor = segmentColor;
|
||||
pathRenderer.endColor = segmentColor;
|
||||
cutLineRenderer.SetPosition(index, edgePoints[i]);
|
||||
cutLineRenderer.SetPosition(index + 1, edgePoints[i + 1]);
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckAllSegmentsCut()
|
||||
{
|
||||
if (cutSegments == null) return;
|
||||
|
||||
if (System.Array.TrueForAll(cutSegments, cut => cut))
|
||||
{
|
||||
Debug.Log("所有边缘线段都已经切割完成!");
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user