|
|
|
using System.Linq;
|
|
|
|
using Global;
|
|
|
|
using UnityEngine;
|
|
|
|
using UnityEngine.UIElements;
|
|
|
|
|
|
|
|
namespace Game {
|
|
|
|
|
|
|
|
public class AIPlayer : Player {
|
|
|
|
private const float SmoothTime = 0.1f;
|
|
|
|
|
|
|
|
private float currentSmoothV;
|
|
|
|
|
|
|
|
private bool isApproaching;
|
|
|
|
private float lastDirection;
|
|
|
|
private bool lastGoingLeft, lastGoingRight;
|
|
|
|
private Label leftButton, rightButton;
|
|
|
|
|
|
|
|
public Difficulty Difficulty { get; set; }
|
|
|
|
|
|
|
|
private float FutureSeconds => GameManager.BalanceValues.aiFutureSecondsBase + (float) Difficulty * GameManager.BalanceValues.aiFutureSecondsPerDifficulty;
|
|
|
|
|
|
|
|
private float IdlePosition => Difficulty >= GameManager.BalanceValues.aiSmartIdleMinDifficulty ? 0 : X;
|
|
|
|
|
|
|
|
private float DistortAmount => 1 - 1 / ((float) Difficulty + 1);
|
|
|
|
|
|
|
|
protected new void Start() {
|
|
|
|
base.Start();
|
|
|
|
leftButton = GameUI.Singleton.GetGoButton(Side, "left");
|
|
|
|
rightButton = GameUI.Singleton.GetGoButton(Side, "right");
|
|
|
|
}
|
|
|
|
|
|
|
|
private void FixedUpdate() {
|
|
|
|
var dt = Time.fixedDeltaTime;
|
|
|
|
var target = GetTargetPosition();
|
|
|
|
var h = Mathf.Max(Speed * dt, Width / 2);
|
|
|
|
goingLeftLinear = target < X - h;
|
|
|
|
goingRightLinear = target > X + h;
|
|
|
|
if (goingLeftLinear || goingRightLinear) {
|
|
|
|
isApproaching = false;
|
|
|
|
lastDirection = goingLeftLinear ? -1 : 1;
|
|
|
|
TryLinearMove(dt);
|
|
|
|
} else {
|
|
|
|
if (!isApproaching) {
|
|
|
|
isApproaching = true;
|
|
|
|
currentSmoothV = Speed * lastDirection;
|
|
|
|
}
|
|
|
|
ApproachPosition(target);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool goingLeft = isApproaching ? currentSmoothV < 0 : goingLeftLinear;
|
|
|
|
bool goingRight = isApproaching ? currentSmoothV > 0 : goingRightLinear;
|
|
|
|
switch (lastGoingLeft) {
|
|
|
|
case false when goingLeft:
|
|
|
|
|
|
|
|
break;
|
|
|
|
case true when !goingLeft:
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
switch (lastGoingRight) {
|
|
|
|
case false when goingRight:
|
|
|
|
|
|
|
|
break;
|
|
|
|
case true when !goingRight:
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
lastGoingLeft = goingLeft;
|
|
|
|
lastGoingRight = goingRight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// True if ball y velocity points towards player
|
|
|
|
private bool BallApproaches(Ball ball) {
|
|
|
|
var ballVy = ball.Rb.velocity.y;
|
|
|
|
return ballVy * transform.up.y < 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not euclid, but only y distance
|
|
|
|
private float YDistanceToBall(Ball ball) {
|
|
|
|
return Mathf.Abs(ball.Rb.position.y - Y);
|
|
|
|
}
|
|
|
|
|
|
|
|
// o: Origin, e: End
|
|
|
|
public static bool Intersect(Vector2 o1, Vector2 e1, Vector2 o2, Vector2 e2, out Vector2 point, out Vector2 rs) {
|
|
|
|
point = Vector2.zero;
|
|
|
|
rs = Vector2.zero;
|
|
|
|
|
|
|
|
var m1 = e1 - o1;
|
|
|
|
var m2 = e2 - o2;
|
|
|
|
|
|
|
|
/*
|
|
|
|
*
|
|
|
|
* o1 + r * m1 = 02 + s * m2
|
|
|
|
*
|
|
|
|
* o1x + r * m1x = o2x + s * m2x
|
|
|
|
* o1y + r * m1y = o2y + s * m2y
|
|
|
|
*
|
|
|
|
* Solve for r and s:
|
|
|
|
* Formulas below achieved by reordering
|
|
|
|
* Order is irrelevant, but if m1x == 0 then first approach results in division by zero ->
|
|
|
|
* Invert order then, and if division by zero is still a problem, both lines are parallel anyway
|
|
|
|
*/
|
|
|
|
|
|
|
|
float r, s;
|
|
|
|
if (m1.x != 0) {
|
|
|
|
s = (o1.y + o2.x * m1.y / m1.x - o1.x * m1.y / m1.x - o2.y) / (m2.y - m2.x * m1.y / m1.x);
|
|
|
|
r = (o2.x + s * m2.x - o1.x) / m1.x;
|
|
|
|
} else if (m2.x != 0) {
|
|
|
|
r = (o2.y + o1.x * m2.y / m2.x - o2.x * m2.y / m2.x - o1.y) / (m1.y - m1.x * m2.y / m2.x);
|
|
|
|
s = (o1.x + r * m1.x - o2.x) / m2.x;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (s is > 0 and < 1 && r is > 0 and < 1) {
|
|
|
|
point = o1 + r * m1;
|
|
|
|
rs = new Vector2(r, s);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO Also must include fact that players have a height, maybe check the incoming angle
|
|
|
|
// angle: 0 -> perpendicular, from -90 to 90
|
|
|
|
private bool IsPositionReachableInTime(float futurePosition, float seconds, float radius, float angle) {
|
|
|
|
var requiredDistance = Mathf.Abs(futurePosition - X) - Width / 2 - radius;
|
|
|
|
if (requiredDistance < 0)
|
|
|
|
return true;
|
|
|
|
// TODO this is too strict, reachable balls are given up
|
|
|
|
if (Mathf.Abs(futurePosition) > Border)
|
|
|
|
return false;
|
|
|
|
return Speed * seconds > requiredDistance;
|
|
|
|
}
|
|
|
|
|
|
|
|
private float Simulate(Vector2 origin, Vector2 velocity, float radius, float secondsLeft, float secondsUsed, out bool ignore) {
|
|
|
|
ignore = false;
|
|
|
|
var r = new Vector2(radius, radius);
|
|
|
|
var validAreaOrigin = -Dimensions.Singleton.PlaySizeBoards / 2 + r;
|
|
|
|
var validAreaSize = Dimensions.Singleton.PlaySizeBoards - r * 2;
|
|
|
|
var area = new Rect(validAreaOrigin, validAreaSize);
|
|
|
|
|
|
|
|
// Try to follow this line from origin -> end
|
|
|
|
var end = origin + velocity * secondsLeft;
|
|
|
|
|
|
|
|
// Line ends in playground
|
|
|
|
if (area.Contains(end))
|
|
|
|
return end.x;
|
|
|
|
|
|
|
|
var playerY = Side == Side.Bottom ? area.yMin : area.yMax;
|
|
|
|
var playerLeft = new Vector2(area.xMin, playerY);
|
|
|
|
var playerRight = new Vector2(area.xMax, playerY);
|
|
|
|
|
|
|
|
// Horizontal line (player line) -> stop simulating
|
|
|
|
if (Intersect(origin, end, playerLeft, playerRight, out var point, out var rs)) {
|
|
|
|
secondsUsed += secondsLeft * rs.x;
|
|
|
|
if (!IsPositionReachableInTime(point.x, secondsUsed, radius, Vector2.Angle(velocity, transform.up)))
|
|
|
|
ignore = true;
|
|
|
|
return point.x;
|
|
|
|
}
|
|
|
|
|
|
|
|
var borderHit = false;
|
|
|
|
var borderHitPoint = Vector2.zero;
|
|
|
|
var borderRs = Vector2.zero;
|
|
|
|
|
|
|
|
// Left vertical border
|
|
|
|
if (Intersect(origin, end, new Vector2(area.xMin, area.yMin), new Vector2(area.xMin, area.yMax), out point, out rs)) {
|
|
|
|
borderHit = true;
|
|
|
|
borderHitPoint = point;
|
|
|
|
borderRs = rs;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Right vertical border
|
|
|
|
if (Intersect(origin, end, new Vector2(area.xMax, area.yMin), new Vector2(area.xMax, area.yMax), out point, out rs)) {
|
|
|
|
borderHit = true;
|
|
|
|
borderHitPoint = point;
|
|
|
|
borderRs = rs;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Any border -> invert x velocity and simulate again from there
|
|
|
|
if (borderHit) {
|
|
|
|
var secondsUsedHere = borderRs.x * secondsLeft;
|
|
|
|
secondsLeft -= secondsUsedHere;
|
|
|
|
secondsUsed += secondsUsedHere;
|
|
|
|
velocity = new Vector2(-velocity.x, velocity.y);
|
|
|
|
origin = borderHitPoint;
|
|
|
|
return Simulate(origin, velocity, radius, secondsLeft, secondsUsed, out ignore);
|
|
|
|
}
|
|
|
|
|
|
|
|
// No intersection -> Ball outside of field -> ignore
|
|
|
|
ignore = true;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
private float Distort(float target) {
|
|
|
|
var max = Width / 3;
|
|
|
|
var distortionOffset = DistortAmount * max;
|
|
|
|
distortionOffset *= target > X ? -1 : 1;
|
|
|
|
return target + distortionOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
private float GetTargetPosition() {
|
|
|
|
var approaching = GameManager.Singleton.Balls.Where(BallApproaches).ToList();
|
|
|
|
|
|
|
|
while (approaching.Count > 0) {
|
|
|
|
|
|
|
|
// TODO nearest by impact time
|
|
|
|
// Nearest by Y-Distance
|
|
|
|
var ball = approaching.Aggregate(approaching[0], (prev, current) => YDistanceToBall(current) < YDistanceToBall(prev) ? current : prev);
|
|
|
|
|
|
|
|
var target = Simulate(ball.Rb.position, ball.Rb.velocity, ball.Radius, FutureSeconds, 0, out var ignore);
|
|
|
|
if (!ignore)
|
|
|
|
return Distort(target);
|
|
|
|
|
|
|
|
approaching.Remove(ball);
|
|
|
|
// This ball was marked "ignore" by simulation for unknown reasons, try next one
|
|
|
|
}
|
|
|
|
|
|
|
|
return IdlePosition;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void ApproachPosition(float pos) {
|
|
|
|
var result = Mathf.SmoothDamp(X, pos, ref currentSmoothV, SmoothTime, Speed);
|
|
|
|
transform.position = new Vector2(result, Y);
|
|
|
|
ClampInsideBorders();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|