Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
389 lines
9.8 KiB
C#
389 lines
9.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
public struct Point : IEquatable<Point>
|
|
{
|
|
public int X { get; }
|
|
public int Y { get; }
|
|
public Point(int x, int y) { X = x; Y = y; }
|
|
public bool Equals(Point other) => X == other.X && Y == other.Y;
|
|
public override bool Equals(object obj) => obj is Point other && Equals(other);
|
|
public override int GetHashCode() => HashCode.Combine(X, Y);
|
|
public static bool operator ==(Point left, Point right) => left.Equals(right);
|
|
public static bool operator !=(Point left, Point right) => !(left == right);
|
|
public override string ToString() => $"({X},{Y})";
|
|
}
|
|
|
|
int ballHandItemId = 2147418815;
|
|
string[] targetTileNameContains = { "ice skating patch", "snow-covered rocks" };
|
|
string passMessage = ":pass";
|
|
int passIntervalMs = 300;
|
|
int loopDelayMs = 80;
|
|
int moveIntervalMs = 200;
|
|
int rescanTilesMs = 3000;
|
|
int approachDistance = 1;
|
|
int escapeDurationMs = 15000;
|
|
Point escapeTileOverride = new Point(-1, -1);
|
|
|
|
bool enabled = true;
|
|
bool hasBall = false;
|
|
bool escapeMode = false;
|
|
DateTime escapeStart = DateTime.MinValue;
|
|
Point escapeTarget = new Point(-1, -1);
|
|
|
|
DateTime lastPass = DateTime.MinValue;
|
|
DateTime lastMove = DateTime.MinValue;
|
|
DateTime lastScan = DateTime.MinValue;
|
|
|
|
HashSet<Point> targetTiles = new HashSet<Point>();
|
|
HashSet<Point> walkableTiles = new HashSet<Point>();
|
|
Dictionary<Point, List<Point>> adj = new Dictionary<Point, List<Point>>();
|
|
Point[] dirs = { new Point(0, 1), new Point(0, -1), new Point(1, 0), new Point(-1, 0), new Point(1, 1), new Point(1, -1), new Point(-1, 1), new Point(-1, -1) };
|
|
|
|
bool floorPlanParsed = false;
|
|
|
|
void ParseFloorPlan()
|
|
{
|
|
if (floorPlanParsed) return;
|
|
try
|
|
{
|
|
dynamic floorPlan = FloorPlan;
|
|
if (floorPlan == null) return;
|
|
|
|
int width = floorPlan.Width;
|
|
int length = floorPlan.Length;
|
|
|
|
walkableTiles.Clear();
|
|
|
|
IReadOnlyList<int> tilesData = null;
|
|
string heightmapString = null;
|
|
|
|
try { tilesData = floorPlan.Tiles; } catch { }
|
|
try { heightmapString = floorPlan.Heightmap; } catch { }
|
|
|
|
if (heightmapString != null)
|
|
{
|
|
heightmapString = heightmapString.Replace("\r", "").Replace("\n", "");
|
|
for (int y = 0; y < length; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
if (heightmapString[y * width + x] != 'x')
|
|
walkableTiles.Add(new Point(x, y));
|
|
}
|
|
}
|
|
}
|
|
else if (tilesData != null)
|
|
{
|
|
for (int y = 0; y < length; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int tileIndex = y * width + x;
|
|
if (tileIndex < tilesData.Count && tilesData[tileIndex] >= 0 && tilesData[tileIndex] < 250)
|
|
walkableTiles.Add(new Point(x, y));
|
|
}
|
|
}
|
|
}
|
|
|
|
BuildAdjacencyMap();
|
|
floorPlanParsed = true;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
void BuildAdjacencyMap()
|
|
{
|
|
adj.Clear();
|
|
foreach (var t in walkableTiles)
|
|
{
|
|
var n = new List<Point>();
|
|
foreach (var d in dirs)
|
|
{
|
|
var p = new Point(t.X + d.X, t.Y + d.Y);
|
|
if (walkableTiles.Contains(p)) n.Add(p);
|
|
}
|
|
adj[t] = n;
|
|
}
|
|
}
|
|
|
|
void ScanTargetTiles()
|
|
{
|
|
targetTiles.Clear();
|
|
if (FloorItems == null) return;
|
|
|
|
foreach (var item in FloorItems)
|
|
{
|
|
if (item == null || item.Location == null) continue;
|
|
string name = null;
|
|
try { name = item.GetName(); } catch { continue; }
|
|
if (string.IsNullOrEmpty(name)) continue;
|
|
|
|
string lower = name.ToLowerInvariant();
|
|
if (targetTileNameContains.Any(n => lower.Contains(n)))
|
|
targetTiles.Add(new Point(item.Location.X, item.Location.Y));
|
|
}
|
|
|
|
lastScan = DateTime.UtcNow;
|
|
}
|
|
|
|
Point GetMyPosition()
|
|
{
|
|
if (Self == null || Self.Location == null) return new Point(-1, -1);
|
|
return new Point(Self.Location.X, Self.Location.Y);
|
|
}
|
|
|
|
int Dist(Point a, Point b) => Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
|
|
|
|
IEntity FindNearestTargetUser(Point me)
|
|
{
|
|
IEntity best = null;
|
|
int bestDist = int.MaxValue;
|
|
|
|
foreach (var user in Users)
|
|
{
|
|
if (user == null || user.Id == Self.Id || user.Location == null) continue;
|
|
var p = new Point(user.Location.X, user.Location.Y);
|
|
if (!targetTiles.Contains(p)) continue;
|
|
|
|
int d = Dist(me, p);
|
|
if (d < bestDist)
|
|
{
|
|
bestDist = d;
|
|
best = user;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
Point FindClosestWalkable(Point target)
|
|
{
|
|
if (walkableTiles.Contains(target)) return target;
|
|
|
|
Point best = new Point(-1, -1);
|
|
int bestDist = int.MaxValue;
|
|
|
|
foreach (var t in walkableTiles)
|
|
{
|
|
int d = Dist(t, target);
|
|
if (d < bestDist)
|
|
{
|
|
bestDist = d;
|
|
best = t;
|
|
}
|
|
}
|
|
|
|
return bestDist == int.MaxValue ? target : best;
|
|
}
|
|
|
|
List<Point> FindPath(Point start, Point goal)
|
|
{
|
|
if (!walkableTiles.Contains(start) || !walkableTiles.Contains(goal))
|
|
return new List<Point>();
|
|
|
|
var queue = new Queue<Point>();
|
|
var visited = new HashSet<Point>();
|
|
var cameFrom = new Dictionary<Point, Point>();
|
|
|
|
queue.Enqueue(start);
|
|
visited.Add(start);
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var cur = queue.Dequeue();
|
|
if (cur == goal) break;
|
|
|
|
if (!adj.TryGetValue(cur, out var neighbors)) continue;
|
|
foreach (var n in neighbors)
|
|
{
|
|
if (visited.Contains(n)) continue;
|
|
visited.Add(n);
|
|
cameFrom[n] = cur;
|
|
queue.Enqueue(n);
|
|
}
|
|
}
|
|
|
|
if (!visited.Contains(goal)) return new List<Point>();
|
|
|
|
var path = new List<Point>();
|
|
var node = goal;
|
|
while (node != start)
|
|
{
|
|
path.Add(node);
|
|
if (!cameFrom.TryGetValue(node, out var prev)) break;
|
|
node = prev;
|
|
}
|
|
|
|
path.Add(start);
|
|
path.Reverse();
|
|
return path;
|
|
}
|
|
|
|
void StepToward(Point goal)
|
|
{
|
|
if ((DateTime.UtcNow - lastMove).TotalMilliseconds < moveIntervalMs) return;
|
|
|
|
var me = GetMyPosition();
|
|
if (me.X < 0) return;
|
|
|
|
var realGoal = FindClosestWalkable(goal);
|
|
var path = FindPath(me, realGoal);
|
|
if (path.Count >= 2)
|
|
{
|
|
var next = path[1];
|
|
Move(next.X, next.Y);
|
|
lastMove = DateTime.UtcNow;
|
|
}
|
|
else if (walkableTiles.Contains(realGoal))
|
|
{
|
|
Move(realGoal.X, realGoal.Y);
|
|
lastMove = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
Point FindEscapeTile(Point me)
|
|
{
|
|
if (escapeTileOverride.X >= 0 && escapeTileOverride.Y >= 0 && walkableTiles.Contains(escapeTileOverride))
|
|
return escapeTileOverride;
|
|
|
|
var others = Users.Where(u => u != null && u.Id != Self.Id && u.Location != null)
|
|
.Select(u => new Point(u.Location.X, u.Location.Y))
|
|
.ToList();
|
|
|
|
if (others.Count == 0) return me;
|
|
|
|
Point best = me;
|
|
int bestScore = int.MinValue;
|
|
|
|
foreach (var t in walkableTiles)
|
|
{
|
|
int minDist = others.Min(o => Dist(t, o));
|
|
if (minDist > bestScore)
|
|
{
|
|
bestScore = minDist;
|
|
best = t;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
void TryPass(IEntity target)
|
|
{
|
|
if (target == null) return;
|
|
if ((DateTime.UtcNow - lastPass).TotalMilliseconds < passIntervalMs) return;
|
|
|
|
var msg = passMessage.Replace("{name}", target.Name);
|
|
Talk(msg);
|
|
lastPass = DateTime.UtcNow;
|
|
}
|
|
|
|
void SetBallState(bool active)
|
|
{
|
|
if (active == hasBall) return;
|
|
|
|
hasBall = active;
|
|
if (hasBall)
|
|
{
|
|
escapeMode = false;
|
|
escapeTarget = new Point(-1, -1);
|
|
}
|
|
else
|
|
{
|
|
escapeMode = true;
|
|
escapeStart = DateTime.UtcNow;
|
|
escapeTarget = FindEscapeTile(GetMyPosition());
|
|
}
|
|
}
|
|
|
|
OnIntercept(In["CarryObject"], e =>
|
|
{
|
|
int userIndex = e.Packet.ReadInt();
|
|
int carrying = e.Packet.ReadInt();
|
|
|
|
if (Self != null && userIndex == Self.Index)
|
|
{
|
|
SetBallState(carrying == ballHandItemId);
|
|
}
|
|
});
|
|
|
|
OnEnteredRoom(e =>
|
|
{
|
|
floorPlanParsed = false;
|
|
targetTiles.Clear();
|
|
ScanTargetTiles();
|
|
});
|
|
|
|
OnIntercept(Out.Chat, e =>
|
|
{
|
|
string message = e.Packet.ReadString();
|
|
if (message.Equals(".icebot on", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
enabled = true;
|
|
e.Block();
|
|
Log("Ice bot enabled");
|
|
}
|
|
else if (message.Equals(".icebot off", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
enabled = false;
|
|
e.Block();
|
|
Log("Ice bot disabled");
|
|
}
|
|
else if (message.Equals(".icebot scan", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
e.Block();
|
|
ScanTargetTiles();
|
|
Log($"Target tiles: {targetTiles.Count}");
|
|
}
|
|
});
|
|
|
|
while (Run)
|
|
{
|
|
try
|
|
{
|
|
if (!enabled) { Delay(200); continue; }
|
|
if (Self == null || Self.Location == null) { Delay(200); continue; }
|
|
|
|
if (!floorPlanParsed) ParseFloorPlan();
|
|
if (!floorPlanParsed) { Delay(100); continue; }
|
|
|
|
if ((DateTime.UtcNow - lastScan).TotalMilliseconds > rescanTilesMs)
|
|
ScanTargetTiles();
|
|
|
|
var me = GetMyPosition();
|
|
if (me.X < 0) { Delay(loopDelayMs); continue; }
|
|
|
|
if (hasBall)
|
|
{
|
|
var target = FindNearestTargetUser(me);
|
|
if (target != null && target.Location != null)
|
|
{
|
|
var tp = new Point(target.Location.X, target.Location.Y);
|
|
if (Dist(me, tp) <= approachDistance)
|
|
TryPass(target);
|
|
else
|
|
StepToward(tp);
|
|
}
|
|
}
|
|
else if (escapeMode)
|
|
{
|
|
if ((DateTime.UtcNow - escapeStart).TotalMilliseconds > escapeDurationMs)
|
|
{
|
|
escapeMode = false;
|
|
}
|
|
else if (escapeTarget.X >= 0)
|
|
{
|
|
if (me != escapeTarget)
|
|
StepToward(escapeTarget);
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
Delay(loopDelayMs);
|
|
}
|
|
|
|
Wait();
|