Keeps the repo root clean - only README.md visible on landing page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
416 lines
12 KiB
C#
416 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
|
|
public struct Point : IEquatable<Point>
|
|
{
|
|
public int X { get; }
|
|
public int Y { get; }
|
|
public Point(int x, int y) { X = x; Y = y; }
|
|
public static implicit operator Point((int x, int y) tuple) => new Point(tuple.x, tuple.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})";
|
|
}
|
|
|
|
Log("=== Hope Habubu Avoider + Auto Gate ===");
|
|
|
|
// ==================== KONFIGURATION ====================
|
|
const string DANGER_FURNI_NAME = "Hope Habubu";
|
|
const string GATE_FURNI_NAME = "One Way Gate";
|
|
const int DANGER_RADIUS = 2;
|
|
const int REACTION_DISTANCE = 4;
|
|
const int LOOP_DELAY_MS = 25;
|
|
// =======================================================
|
|
|
|
Regex mvRegex = new Regex(@"/mv (\d+),(\d+),([\d\.]+)/");
|
|
HashSet<Point> walkableTiles = new HashSet<Point>();
|
|
bool floorPlanReady = false;
|
|
DateTime lastFloorPlanParse = DateTime.MinValue;
|
|
Dictionary<long, Point> trackedThreats = new Dictionary<long, Point>();
|
|
Point myTargetTile = (-1, -1);
|
|
DateTime lastMoveTime = DateTime.MinValue;
|
|
|
|
bool IsInvalidPoint(Point p)
|
|
{
|
|
return p.X == -1 && p.Y == -1;
|
|
}
|
|
|
|
Point GetMyPosition()
|
|
{
|
|
if (Self == null || Self.Location == null) return (-1, -1);
|
|
if (!IsInvalidPoint(myTargetTile) && (DateTime.UtcNow - lastMoveTime).TotalMilliseconds < 300)
|
|
return myTargetTile;
|
|
return (Self.Location.X, Self.Location.Y);
|
|
}
|
|
|
|
void DoMove(int x, int y)
|
|
{
|
|
Move(x, y);
|
|
myTargetTile = (x, y);
|
|
lastMoveTime = DateTime.UtcNow;
|
|
}
|
|
|
|
double GetDistance(Point a, Point b)
|
|
{
|
|
return Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2));
|
|
}
|
|
|
|
void ParseFloorPlan()
|
|
{
|
|
if ((DateTime.UtcNow - lastFloorPlanParse).TotalSeconds < 5 && floorPlanReady) return;
|
|
lastFloorPlanParse = DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
dynamic fp = FloorPlan;
|
|
if (fp == null) { floorPlanReady = false; return; }
|
|
|
|
int width = fp.Width;
|
|
int length = fp.Length;
|
|
if (width <= 0 || length <= 0) { floorPlanReady = false; return; }
|
|
|
|
var tiles = new HashSet<Point>();
|
|
IReadOnlyList<int> tilesData = null;
|
|
string heightmap = null;
|
|
|
|
try { tilesData = fp.Tiles; } catch { }
|
|
try { heightmap = fp.Heightmap?.Replace("\r", "").Replace("\n", ""); } catch { }
|
|
|
|
if (tilesData != null)
|
|
{
|
|
for (int y = 0; y < length; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int idx = y * width + x;
|
|
if (idx < tilesData.Count && tilesData[idx] >= 0 && tilesData[idx] < 250)
|
|
tiles.Add((x, y));
|
|
}
|
|
}
|
|
}
|
|
else if (heightmap != null && heightmap.Length == width * length)
|
|
{
|
|
for (int y = 0; y < length; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
if (heightmap[y * width + x] != 'x')
|
|
tiles.Add((x, y));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tiles.Count > 0)
|
|
{
|
|
walkableTiles = tiles;
|
|
floorPlanReady = true;
|
|
Log($"FloorPlan: {tiles.Count} tiles");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"FloorPlan error: {ex.Message}");
|
|
floorPlanReady = false;
|
|
}
|
|
}
|
|
|
|
bool IsWalkable(int x, int y)
|
|
{
|
|
if (!floorPlanReady || walkableTiles == null) return true;
|
|
return walkableTiles.Contains((x, y));
|
|
}
|
|
|
|
// ==================== GATE LOGIC ====================
|
|
void CheckAndEnterGate(int userX, int userY)
|
|
{
|
|
if (FloorItems == null) return;
|
|
|
|
foreach (var item in FloorItems)
|
|
{
|
|
if (item == null || item.Location == null) continue;
|
|
|
|
string itemName = null;
|
|
try { itemName = item.GetName(); } catch { continue; }
|
|
|
|
if (itemName != null && itemName.Contains(GATE_FURNI_NAME))
|
|
{
|
|
int gateX = item.Location.X;
|
|
int gateY = item.Location.Y;
|
|
int gateDir = item.Direction;
|
|
long gateId = item.Id;
|
|
bool shouldEnter = false;
|
|
|
|
if (gateDir == 0) { if (userX == gateX && userY == gateY - 1) shouldEnter = true; }
|
|
else if (gateDir == 2) { if (userX == gateX + 1 && userY == gateY) shouldEnter = true; }
|
|
else if (gateDir == 4) { if (userX == gateX && userY == gateY + 1) shouldEnter = true; }
|
|
else if (gateDir == 6) { if (userX == gateX - 1 && userY == gateY) shouldEnter = true; }
|
|
|
|
if (shouldEnter)
|
|
{
|
|
Log($"Gate! ID:{gateId} at ({gateX},{gateY}) Dir:{gateDir}");
|
|
Send(Out.EnterOneWayDoor, gateId);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CheckGateOnObjectUpdate(int furniId, int itemX, int itemY, int newDir)
|
|
{
|
|
if (Self == null || Self.Location == null) return;
|
|
|
|
var item = FloorItems?.FirstOrDefault(f => f != null && f.Id == furniId);
|
|
if (item == null) return;
|
|
|
|
string itemName = null;
|
|
try { itemName = item.GetName(); } catch { return; }
|
|
|
|
if (itemName != null && itemName.Contains(GATE_FURNI_NAME))
|
|
{
|
|
int userX = Self.Location.X;
|
|
int userY = Self.Location.Y;
|
|
bool shouldEnter = false;
|
|
|
|
if (newDir == 0) { if (userX == itemX && userY == itemY - 1) shouldEnter = true; }
|
|
else if (newDir == 2) { if (userX == itemX + 1 && userY == itemY) shouldEnter = true; }
|
|
else if (newDir == 4) { if (userX == itemX && userY == itemY + 1) shouldEnter = true; }
|
|
else if (newDir == 6) { if (userX == itemX - 1 && userY == itemY) shouldEnter = true; }
|
|
|
|
if (shouldEnter)
|
|
{
|
|
Log($"Gate rotated! ID:{furniId}");
|
|
Send(Out.EnterOneWayDoor, furniId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== THREAT LOGIC ====================
|
|
void ScanThreats()
|
|
{
|
|
if (FloorItems == null) return;
|
|
trackedThreats.Clear();
|
|
|
|
foreach (var item in FloorItems)
|
|
{
|
|
if (item == null || item.Location == null) continue;
|
|
|
|
string name = null;
|
|
try { name = item.GetName(); } catch { continue; }
|
|
|
|
if (name != null && name.Contains(DANGER_FURNI_NAME))
|
|
{
|
|
trackedThreats[item.Id] = (item.Location.X, item.Location.Y);
|
|
}
|
|
}
|
|
}
|
|
|
|
Point FindBestEscapeStep(Point myPos)
|
|
{
|
|
Point bestStep = myPos;
|
|
double bestScore = double.MinValue;
|
|
|
|
for (int dx = -1; dx <= 1; dx++)
|
|
{
|
|
for (int dy = -1; dy <= 1; dy++)
|
|
{
|
|
Point candidate = (myPos.X + dx, myPos.Y + dy);
|
|
if (!IsWalkable(candidate.X, candidate.Y)) continue;
|
|
|
|
double minDangerDist = double.MaxValue;
|
|
foreach (var threat in trackedThreats.Values)
|
|
{
|
|
double d = GetDistance(candidate, threat);
|
|
if (d < minDangerDist) minDangerDist = d;
|
|
}
|
|
|
|
double score = minDangerDist * 10;
|
|
if (dx != 0 || dy != 0) score += 1;
|
|
|
|
if (score > bestScore)
|
|
{
|
|
bestScore = score;
|
|
bestStep = candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestStep;
|
|
}
|
|
|
|
// ==================== EVENT HANDLERS ====================
|
|
void HandleUserUpdate(dynamic e)
|
|
{
|
|
if (Self == null) return;
|
|
var packet = e.Packet;
|
|
int numUpdates = packet.ReadInt();
|
|
|
|
for (int i = 0; i < numUpdates; i++)
|
|
{
|
|
int entityIndex = packet.ReadInt();
|
|
int currentX = packet.ReadInt();
|
|
int currentY = packet.ReadInt();
|
|
packet.ReadString();
|
|
packet.ReadInt();
|
|
packet.ReadInt();
|
|
string action = packet.ReadString();
|
|
|
|
if (entityIndex == Self.Index)
|
|
{
|
|
Point tileForGateCheck;
|
|
Match match = mvRegex.Match(action);
|
|
if (match.Success)
|
|
{
|
|
try
|
|
{
|
|
int targetX = int.Parse(match.Groups[1].Value);
|
|
int targetY = int.Parse(match.Groups[2].Value);
|
|
myTargetTile = (targetX, targetY);
|
|
lastMoveTime = DateTime.UtcNow;
|
|
tileForGateCheck = myTargetTile;
|
|
}
|
|
catch { tileForGateCheck = (currentX, currentY); }
|
|
}
|
|
else
|
|
{
|
|
if (action.EndsWith("//") && !action.Contains("/mv"))
|
|
myTargetTile = (-1, -1);
|
|
tileForGateCheck = (currentX, currentY);
|
|
}
|
|
CheckAndEnterGate(tileForGateCheck.X, tileForGateCheck.Y);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HandleObjectUpdate(dynamic e)
|
|
{
|
|
if (Self == null || Self.Location == null) return;
|
|
|
|
var packet = e.Packet;
|
|
int furniId = packet.ReadInt();
|
|
packet.ReadInt();
|
|
int itemX = packet.ReadInt();
|
|
int itemY = packet.ReadInt();
|
|
int newDir = packet.ReadInt();
|
|
|
|
// Update threat position
|
|
if (trackedThreats.ContainsKey(furniId))
|
|
{
|
|
trackedThreats[furniId] = (itemX, itemY);
|
|
}
|
|
|
|
// Check gate
|
|
CheckGateOnObjectUpdate(furniId, itemX, itemY, newDir);
|
|
}
|
|
|
|
void HandleWiredMovements(dynamic e)
|
|
{
|
|
var packet = e.Packet;
|
|
int count = packet.ReadInt();
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
packet.ReadInt();
|
|
packet.ReadInt();
|
|
packet.ReadInt();
|
|
int toX = packet.ReadInt();
|
|
int toY = packet.ReadInt();
|
|
packet.ReadString();
|
|
packet.ReadString();
|
|
int id = packet.ReadInt();
|
|
packet.ReadInt();
|
|
packet.ReadInt();
|
|
|
|
if (trackedThreats.ContainsKey(id))
|
|
{
|
|
trackedThreats[id] = (toX, toY);
|
|
}
|
|
}
|
|
}
|
|
|
|
void HandleRoomEnter(dynamic e)
|
|
{
|
|
Log("Room entered - scanning...");
|
|
trackedThreats.Clear();
|
|
myTargetTile = (-1, -1);
|
|
ParseFloorPlan();
|
|
ScanThreats();
|
|
Log($"Found: {trackedThreats.Count} '{DANGER_FURNI_NAME}' objects");
|
|
|
|
if (Self != null && Self.Location != null)
|
|
CheckAndEnterGate(Self.Location.X, Self.Location.Y);
|
|
}
|
|
|
|
// ==================== REGISTER EVENTS ====================
|
|
OnIntercept(In["UserUpdate"], e => HandleUserUpdate(e));
|
|
OnIntercept(In["ObjectUpdate"], e => HandleObjectUpdate(e));
|
|
OnIntercept(In["WiredMovements"], e => HandleWiredMovements(e));
|
|
OnEnteredRoom(e => HandleRoomEnter(e));
|
|
|
|
// Initial setup
|
|
if (Self != null && Self.Location != null)
|
|
{
|
|
ParseFloorPlan();
|
|
ScanThreats();
|
|
Log($"Initial: {trackedThreats.Count} threats found");
|
|
CheckAndEnterGate(Self.Location.X, Self.Location.Y);
|
|
}
|
|
|
|
// ==================== MAIN LOOP ====================
|
|
int tick = 0;
|
|
while (Run)
|
|
{
|
|
try
|
|
{
|
|
tick++;
|
|
if (!floorPlanReady) ParseFloorPlan();
|
|
if (tick % 50 == 0) ScanThreats();
|
|
|
|
Point myPos = GetMyPosition();
|
|
if (IsInvalidPoint(myPos)) { Delay(LOOP_DELAY_MS); continue; }
|
|
if (trackedThreats.Count == 0) { Delay(LOOP_DELAY_MS); continue; }
|
|
|
|
double closestDanger = double.MaxValue;
|
|
Point closestThreat = (-1, -1);
|
|
|
|
foreach (var threat in trackedThreats.Values)
|
|
{
|
|
double d = GetDistance(myPos, threat);
|
|
if (d < closestDanger)
|
|
{
|
|
closestDanger = d;
|
|
closestThreat = threat;
|
|
}
|
|
}
|
|
|
|
if (closestDanger <= REACTION_DISTANCE)
|
|
{
|
|
Point nextStep = FindBestEscapeStep(myPos);
|
|
|
|
if (!nextStep.Equals(myPos))
|
|
{
|
|
if (tick % 10 == 0)
|
|
Log($"DODGE! Threat at {closestThreat} dist:{closestDanger:F1} -> {nextStep}");
|
|
DoMove(nextStep.X, nextStep.Y);
|
|
}
|
|
}
|
|
else if (tick % 100 == 0 && trackedThreats.Count > 0)
|
|
{
|
|
Log($"Monitoring {trackedThreats.Count} threats... nearest: {closestDanger:F1}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"Error: {ex.Message}");
|
|
}
|
|
|
|
if (!Run) break;
|
|
Delay(LOOP_DELAY_MS);
|
|
}
|
|
|
|
Log("=== Stopped ===");
|