This commit is contained in:
Boki 2026-03-03 12:54:30 -05:00
parent a8341e8232
commit a8c43ba7e2
43 changed files with 2618 additions and 48 deletions

View file

@ -2,7 +2,7 @@ using System.Numerics;
namespace Roboto.Core;
public enum ClickType { Left, Right }
public enum ClickType { Left, Right, Middle }
public enum KeyActionType { Press, Down, Up }
public abstract record BotAction(int Priority);

View file

@ -14,10 +14,6 @@ public class BotConfig
// Navigation
public float WorldToGrid { get; set; } = 23f / 250f;
// Combat
public float CriticalHpPercent { get; set; } = 30f;
public float LowHpPercent { get; set; } = 50f;
// Loot
public float LootPickupRange { get; set; } = 600f;
@ -30,11 +26,4 @@ public class BotConfig
// Anti-detection
public float PollIntervalJitter { get; set; } = 0.2f;
// Flasks
public float LifeFlaskThreshold { get; set; } = 50f;
public float ManaFlaskThreshold { get; set; } = 50f;
public int FlaskCooldownMs { get; set; } = 4000;
public ushort LifeFlaskScanCode { get; set; } = 0x02; // Key1
public ushort ManaFlaskScanCode { get; set; } = 0x03; // Key2
}

View file

@ -0,0 +1,24 @@
namespace Roboto.Core;
public class CharacterProfile
{
public string Name { get; set; } = "Default";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastModified { get; set; } = DateTime.UtcNow;
public FlaskSettings Flasks { get; set; } = new();
public CombatSettings Combat { get; set; } = new();
public List<SkillProfile> Skills { get; set; } = DefaultSkills();
public static List<SkillProfile> DefaultSkills() =>
[
new() { SlotIndex = 0, Label = "LMB", InputType = SkillInputType.LeftClick, Priority = 0 },
new() { SlotIndex = 1, Label = "RMB", InputType = SkillInputType.RightClick, Priority = 1 },
new() { SlotIndex = 2, Label = "MMB", InputType = SkillInputType.MiddleClick, Priority = 2 },
new() { SlotIndex = 3, Label = "Q", InputType = SkillInputType.KeyPress, ScanCode = 0x10, Priority = 3 },
new() { SlotIndex = 4, Label = "E", InputType = SkillInputType.KeyPress, ScanCode = 0x12, Priority = 4 },
new() { SlotIndex = 5, Label = "R", InputType = SkillInputType.KeyPress, ScanCode = 0x13, Priority = 5 },
new() { SlotIndex = 6, Label = "T", InputType = SkillInputType.KeyPress, ScanCode = 0x14, Priority = 6 },
new() { SlotIndex = 7, Label = "F", InputType = SkillInputType.KeyPress, ScanCode = 0x21, Priority = 7 },
];
}

View file

@ -0,0 +1,11 @@
namespace Roboto.Core;
public class CombatSettings
{
public int GlobalCooldownMs { get; set; } = 500;
public float AttackRange { get; set; } = 600f;
public float SafeRange { get; set; } = 400f;
public bool KiteEnabled { get; set; }
public float KiteRange { get; set; } = 300f;
public int KiteDelayMs { get; set; } = 200;
}

View file

@ -58,6 +58,7 @@ public record EntitySnapshot
public MonsterRarity Rarity { get; init; }
public List<string>? ModNames { get; init; }
public string? TransitionName { get; init; }
public int TransitionState { get; init; } = -1;
public string? Metadata { get; init; }
// Action state (from Actor component)

View file

@ -0,0 +1,10 @@
namespace Roboto.Core;
public class FlaskSettings
{
public float LifeFlaskThreshold { get; set; } = 50f;
public float ManaFlaskThreshold { get; set; } = 50f;
public int FlaskCooldownMs { get; set; } = 4000;
public ushort LifeFlaskScanCode { get; set; } = 0x02; // Key1
public ushort ManaFlaskScanCode { get; set; } = 0x03; // Key2
}

View file

@ -16,10 +16,12 @@ public class GameState
public uint AreaHash { get; set; }
public int AreaLevel { get; set; }
public string? CurrentAreaName { get; set; }
public bool IsLoading { get; set; }
public bool IsEscapeOpen { get; set; }
public DangerLevel Danger { get; set; }
public Matrix4x4? CameraMatrix { get; set; }
public IReadOnlyList<QuestProgress> ActiveQuests { get; set; } = [];
// Derived (computed once per tick by GameStateEnricher)
public ThreatMap Threats { get; set; } = new();

View file

@ -7,9 +7,11 @@ public interface IInputController
void KeyUp(ushort scanCode);
void KeyPress(ushort scanCode, int holdMs = 50);
void MouseMoveTo(int x, int y);
void SmoothMoveTo(int x, int y);
void MouseMoveBy(int dx, int dy);
void LeftClick(int x, int y);
void RightClick(int x, int y);
void MiddleClick(int x, int y);
void LeftDown();
void LeftUp();
void RightDown();

View file

@ -4,6 +4,7 @@ namespace Roboto.Core;
public record PlayerState
{
public string? CharacterName { get; init; }
public Vector2 Position { get; init; }
public float Z { get; init; }
public bool HasPosition { get; init; }
@ -19,6 +20,9 @@ public record PlayerState
public float ManaPercent => ManaTotal > 0 ? (float)ManaCurrent / ManaTotal * 100f : 0f;
public float EsPercent => EsTotal > 0 ? (float)EsCurrent / EsTotal * 100f : 0f;
// Action state (from Actor component)
public short ActionId { get; init; }
// Flask state (populated by memory when available)
public IReadOnlyList<FlaskState> Flasks { get; init; } = [];

View file

@ -0,0 +1,174 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Roboto.Core;
public sealed class ProfileManager
{
private static readonly JsonSerializerOptions JsonOpts = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
private readonly string _profilesDir;
private readonly string _assignmentsFile;
private readonly object _lock = new();
private Dictionary<string, string> _assignments = new(); // charName → profileName
public ProfileManager(string profilesDir = "profiles")
{
_profilesDir = Path.GetFullPath(profilesDir);
_assignmentsFile = Path.Combine(_profilesDir, "_assignments.json");
EnsureDirectory();
LoadAssignments();
}
public CharacterProfile LoadForCharacter(string charName)
{
lock (_lock)
{
if (_assignments.TryGetValue(charName, out var profileName))
{
var profile = LoadProfile(profileName);
if (profile is not null)
return profile;
}
// Create default profile for this character
var defaultName = $"{charName}_Default";
var defaultProfile = new CharacterProfile { Name = defaultName };
SaveProfile(defaultProfile);
_assignments[charName] = defaultName;
SaveAssignments();
return defaultProfile;
}
}
public void Save(CharacterProfile profile)
{
lock (_lock)
{
profile.LastModified = DateTime.UtcNow;
SaveProfile(profile);
}
}
public CharacterProfile? Duplicate(string profileName, string newName)
{
lock (_lock)
{
var source = LoadProfile(profileName);
if (source is null) return null;
source.Name = newName;
source.CreatedAt = DateTime.UtcNow;
source.LastModified = DateTime.UtcNow;
SaveProfile(source);
return source;
}
}
public void AssignToCharacter(string charName, string profileName)
{
lock (_lock)
{
_assignments[charName] = profileName;
SaveAssignments();
}
}
public void Delete(string profileName)
{
lock (_lock)
{
var path = ProfilePath(profileName);
if (File.Exists(path))
File.Delete(path);
// Remove any assignments pointing to this profile
var toRemove = _assignments
.Where(kv => kv.Value == profileName)
.Select(kv => kv.Key)
.ToList();
foreach (var key in toRemove)
_assignments.Remove(key);
if (toRemove.Count > 0)
SaveAssignments();
}
}
public List<string> ListProfiles()
{
lock (_lock)
{
EnsureDirectory();
return Directory.GetFiles(_profilesDir, "*.json")
.Select(Path.GetFileNameWithoutExtension)
.Where(n => n is not null && !n.StartsWith("_"))
.Cast<string>()
.OrderBy(n => n)
.ToList();
}
}
public string? GetAssignment(string charName)
{
lock (_lock)
{
return _assignments.GetValueOrDefault(charName);
}
}
private CharacterProfile? LoadProfile(string name)
{
var path = ProfilePath(name);
if (!File.Exists(path)) return null;
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<CharacterProfile>(json, JsonOpts);
}
catch
{
return null;
}
}
private void SaveProfile(CharacterProfile profile)
{
EnsureDirectory();
var json = JsonSerializer.Serialize(profile, JsonOpts);
File.WriteAllText(ProfilePath(profile.Name), json);
}
private void LoadAssignments()
{
if (!File.Exists(_assignmentsFile)) return;
try
{
var json = File.ReadAllText(_assignmentsFile);
_assignments = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOpts) ?? new();
}
catch
{
_assignments = new();
}
}
private void SaveAssignments()
{
EnsureDirectory();
var json = JsonSerializer.Serialize(_assignments, JsonOpts);
File.WriteAllText(_assignmentsFile, json);
}
private void EnsureDirectory()
{
if (!Directory.Exists(_profilesDir))
Directory.CreateDirectory(_profilesDir);
}
private string ProfilePath(string name) => Path.Combine(_profilesDir, $"{name}.json");
}

View file

@ -0,0 +1,9 @@
namespace Roboto.Core;
public record QuestProgress
{
public string? QuestName { get; init; }
public byte StateId { get; init; }
public string? StateText { get; init; }
public string? ProgressText { get; init; }
}

View file

@ -0,0 +1,25 @@
namespace Roboto.Core;
public enum SkillInputType { KeyPress, LeftClick, RightClick, MiddleClick }
public class SkillProfile
{
public int SlotIndex { get; set; }
public string Label { get; set; } = "";
public string? SkillName { get; set; }
public SkillInputType InputType { get; set; }
public ushort ScanCode { get; set; }
public int Priority { get; set; }
public bool IsEnabled { get; set; } = true;
public int CooldownMs { get; set; } = 300;
public float RangeMin { get; set; }
public float RangeMax { get; set; } = 600f;
public TargetSelection TargetSelection { get; set; } = TargetSelection.Nearest;
public bool RequiresTarget { get; set; } = true;
public bool IsAura { get; set; }
public bool IsMovementSkill { get; set; }
public int MinMonstersInRange { get; set; } = 1;
public bool MaintainPressed { get; set; }
}

View file

@ -0,0 +1,11 @@
namespace Roboto.Core;
public enum TargetSelection
{
Nearest,
All,
Rarest,
MagicPlus,
RarePlus,
UniqueOnly,
}

View file

@ -0,0 +1,33 @@
using System.Numerics;
namespace Roboto.Core;
public static class WorldToScreen
{
private const float HalfW = 1280f; // 2560 / 2
private const float HalfH = 720f; // 1440 / 2
/// <summary>
/// Projects a world position to screen coordinates using the camera's view-projection matrix.
/// Returns null if the point is behind the camera or off screen.
/// </summary>
public static Vector2? Project(Vector2 worldPos, float z, Matrix4x4 cameraMatrix)
{
var world = new Vector4(worldPos.X, worldPos.Y, z, 1f);
var clip = Vector4.Transform(world, cameraMatrix);
if (clip.W is 0f or float.NaN)
return null;
clip = Vector4.Divide(clip, clip.W);
var sx = (clip.X + 1f) * HalfW;
var sy = (1f - clip.Y) * HalfH;
// Off-screen check
if (sx < 0 || sx > 2560 || sy < 0 || sy > 1440)
return null;
return new Vector2(sx, sy);
}
}