AFK Macro

Idle Mafia Macro/Source/src/Events.cs

Events.cs

Game events and their boosts · 735 lines · 38 KB

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
namespace IdleMafiaBot
{
enum GameEvent { Cash, EnergyRegen, StaminaRegen, HealthRegen, OpsSpeed, BossDamage, TakedownDamage, Mastery, CrateLuck, RecruitLuck, ShopSale, Exp, Other }
sealed partial class Game
{
public sealed class EventBadge
{
public Rectangle Box;
public Color Colour;
public override string ToString() { return Box.X + "," + Box.Y + " " + Box.Width + "x" + Box.Height + " (" + Colour.R + "," + Colour.G + "," + Colour.B + ")"; }
}
public sealed class ActiveEvent
{
public GameEvent Kind = GameEvent.Other;
public string Title = "", Text = "";
public int SecondsLeft = -1;
public override string ToString() { return (Title.Length > 0 ? Title : Kind.ToString()) + (SecondsLeft >= 0 ? " (ends in " + Parse.Short(SecondsLeft) + ")" : ""); }
}
static bool Saturated(Color c)
{
int max = Math.Max(c.R, Math.Max(c.G, c.B)), min = Math.Min(c.R, Math.Min(c.G, c.B));
return max >= 110 && max - min >= 50;
}
static bool SameColour(Color a, Color b, int tol) { return Math.Abs(a.R - b.R) <= tol && Math.Abs(a.G - b.G) <= tol && Math.Abs(a.B - b.B) <= tol; }
public static List<EventBadge> FindBadges(Frame f)
{
var found = new List<EventBadge>();
int x0 = Math.Max(Math.Max(f.Width * 12 / 100, 180), Math.Min(f.Width * 17 / 100, f.Height * 32 / 100)), x1 = f.Width * 55 / 100, y1 = Math.Min(f.Height, f.Height * 6 / 100 + 4);
int minRun = Math.Max(8, f.Width / 110), maxRun = f.Width * 5 / 100 + 10, minH = Math.Max(8, f.Height * 12 / 1000), maxH = f.Height * 45 / 1000 + 2;
var runs = new List<Tuple<int, int, int, Color>>();
for (int y = 2; y < y1; y++)
for (int x = x0; x < x1; )
{
var c = f.Pixel(x, y);
if (!Saturated(c)) { x++; continue; }
int s = x;
while (x < x1 && Saturated(f.Pixel(x, y)) && SameColour(f.Pixel(x, y), c, 16)) x++;
if (x - s >= minRun && x - s <= maxRun) runs.Add(Tuple.Create(y, s, x - 1, c));
}
foreach (var top in runs)
{
if (found.Any(b => Math.Abs(b.Box.X - top.Item2) <= 2 && top.Item1 >= b.Box.Y && top.Item1 <= b.Box.Bottom)) continue;
var bottom = runs.Where(r => r.Item1 >= top.Item1 + minH && r.Item1 <= top.Item1 + maxH && Math.Abs(r.Item2 - top.Item2) <= 2
&& Math.Abs(r.Item3 - top.Item3) <= 2 && SameColour(r.Item4, top.Item4, 24))
.OrderByDescending(r => r.Item1).FirstOrDefault();
if (bottom == null) continue;
int side = 0, n = 0;
for (int y = top.Item1; y <= bottom.Item1; y++)
{
n++;
for (int dx = 0; dx >= -2; dx--)
if (SameColour(f.Pixel(Math.Max(0, top.Item2 + dx), y), top.Item4, 30)) { side++; break; }
}
if (side * 10 < n * 7) continue;
found.Add(new EventBadge { Box = Rectangle.FromLTRB(top.Item2, top.Item1, top.Item3 + 1, bottom.Item1 + 1), Colour = top.Item4 });
}
return found.OrderBy(b => b.Box.X).ToList();
}
public static bool LuckBadge(List<EventBadge> badges)
{
return badges.Any(b => SameColour(b.Colour, Color.FromArgb(110, 155, 220), 16));
}
public static string BadgeSignature(List<EventBadge> badges)
{
return string.Join(";", badges.Select(b => (b.Colour.R / 32) + "." + (b.Colour.G / 32) + "." + (b.Colour.B / 32) + "@" + b.Box.Width / 4));
}
static readonly Regex EndsRx = new Regex(@"ENDS\s*[I1l]N\s*(.+)$", RegexOptions.IgnoreCase);
public static ActiveEvent ReadTooltip(Frame f, EventBadge b)
{
var frame = TooltipFrame(f, b);
if (!frame.IsEmpty)
{
var inside = Rectangle.FromLTRB(frame.Left + 3, frame.Top + 3, frame.Right - 3, frame.Bottom - 3);
var all = Ocr.Read(f, inside, f.Height < 800 ? 3 : 2, Prep.None).OrderBy(l => l.Box.Y).ThenBy(l => l.Box.X).ToList();
if (all.Count > 0)
{
var t = new ActiveEvent { Title = all[0].Text.Trim(), Text = string.Join(" ", all.Select(l => l.Text.Trim())) };
var endsLine = all.FirstOrDefault(l => EndsRx.IsMatch(l.Text)) ?? all.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("ENDSIN"));
if (endsLine != null && EndsRx.IsMatch(endsLine.Text)) t.SecondsLeft = Parse.Countdown(EndsRx.Match(endsLine.Text).Groups[1].Value);
if (t.SecondsLeft < 0) t.SecondsLeft = TooltipEnds(f, inside, endsLine);
t.Kind = EventKind(t.Text);
return t;
}
}
var area = Rectangle.Intersect(new Rectangle(b.Box.X - f.Width / 100, b.Box.Bottom + 1, f.Width * 24 / 100, f.Height * 15 / 100), new Rectangle(0, 0, f.Width, f.Height));
if (area.Width < 20 || area.Height < 20) return null;
int scale = f.Height < 800 ? 3 : 2;
var lines = Ocr.Read(f, area, scale, Prep.None).OrderBy(l => l.Box.Y).ThenBy(l => l.Box.X).ToList();
var ends = lines.FirstOrDefault(l => EndsRx.IsMatch(l.Text));
var mine = lines.Where(l => ends == null || l.Box.Y <= ends.Box.Y + 2).Where(l => l.Box.X >= b.Box.X - f.Width / 100 - 2).ToList();
if (mine.Count == 0) return null;
var e = new ActiveEvent { Title = mine[0].Text.Trim(), Text = string.Join(" ", mine.Select(l => l.Text.Trim())) };
if (ends != null) e.SecondsLeft = Parse.Countdown(EndsRx.Match(ends.Text).Groups[1].Value);
e.Kind = EventKind(e.Text);
if (ends == null && e.Kind == GameEvent.Other) return null;
return e;
}
static Rectangle TooltipFrame(Frame f, EventBadge b)
{
int x = b.Box.X + 2, maxY = Math.Min(f.Height - 2, b.Box.Bottom + 2 * b.Box.Height);
for (int y = b.Box.Bottom + 1; y < maxY; y++)
{
if (!SameColour(f.Pixel(x, y), b.Colour, 30)) continue;
int right = x;
for (int xx = x, miss = 0; xx < f.Width && miss < 3; xx++)
if (SameColour(f.Pixel(xx, y), b.Colour, 30)) { right = xx; miss = 0; } else miss++;
if (right - x < 3 * b.Box.Width) continue;
int left = x, bottom = y;
while (left > 0 && SameColour(f.Pixel(left - 1, y), b.Colour, 30)) left--;
while (bottom < f.Height - 1 && SameColour(f.Pixel(left, bottom + 1), b.Colour, 30)) bottom++;
if (bottom - y < 2 * b.Box.Height) continue;
return Rectangle.FromLTRB(left, y, right + 1, bottom + 1);
}
return Rectangle.Empty;
}
static int TooltipEnds(Frame f, Rectangle inside, OcrLine endsLine)
{
Rectangle row;
if (endsLine != null)
{
int h = Math.Max(6, endsLine.Box.Height);
row = Rectangle.Intersect(Rectangle.FromLTRB(inside.Left, endsLine.Box.Y - h / 2, inside.Right, endsLine.Box.Bottom + h / 2), inside);
}
else row = Rectangle.FromLTRB(inside.Left, inside.Top + inside.Height * 2 / 3, inside.Right, inside.Bottom);
var tries = new[] { Tuple.Create(2, Prep.None), Tuple.Create(3, Prep.None), Tuple.Create(4, Prep.None), Tuple.Create(3, Prep.Contrast), Tuple.Create(2, Prep.GreyText), Tuple.Create(2, Prep.Contrast) };
var words = Ocr.ReadWords(f, row, 3, Prep.None).OrderBy(w => w.Box.X).ToList();
int ends = words.FindIndex(w => Parse.Key(w.Text).StartsWith("ENDS"));
var inWord = ends >= 0 ? words.Skip(ends + 1).FirstOrDefault(w => Regex.IsMatch(w.Text, @"^[I1l]N$", RegexOptions.IgnoreCase)) : null;
if (inWord != null)
{
var time = Rectangle.FromLTRB(inWord.Box.Right + 2, row.Top, row.Right, row.Bottom);
foreach (var t in tries)
{
int s = Parse.Countdown(Ocr.ReadText(f, time, t.Item1, t.Item2).Trim());
if (s >= 0) return s;
}
}
foreach (var t in tries)
{
var m = EndsRx.Match(Ocr.ReadText(f, row, t.Item1, t.Item2));
int s = m.Success ? Parse.Countdown(m.Groups[1].Value) : -1;
if (s < 0 && m.Success) s = LostColon(m.Groups[1].Value);
if (s >= 0) return s;
}
return -1;
}
internal static int LostColon(string s)
{
var m = Regex.Match(s ?? "", @"^\s*(\d{1,2})\s*[:.]\s*([0-5]\d)([0-5]\d)\s*$");
return m.Success ? int.Parse(m.Groups[1].Value) * 3600 + int.Parse(m.Groups[2].Value) * 60 + int.Parse(m.Groups[3].Value) : -1;
}
internal static GameEvent EventKind(string text)
{
string k = Parse.Key(text);
if (k.Contains("RECRUIT") || k.Contains("RARERHENCHMEN")) return GameEvent.RecruitLuck;
if (k.Contains("CRATE") && (k.Contains("IUCK") || k.Contains("RARER"))) return GameEvent.CrateLuck;
if (k.Contains("TAKEDOWN") && k.Contains("DAMAGE")) return GameEvent.TakedownDamage;
if (k.Contains("BOSS") && k.Contains("DAMAGE")) return GameEvent.BossDamage;
if (k.Contains("OPERATION") && (k.Contains("SPEED") || k.Contains("FAST") || k.Contains("TIMER"))) return GameEvent.OpsSpeed;
if (k.Contains("MASTERY")) return GameEvent.Mastery;
if (Regex.IsMatch(k, @"^[0-9OIS]{0,3}X?EXP")) return GameEvent.Exp;
if (k.Contains("SHOP") && (k.Contains("SAIE") || k.Contains("OFF"))) return GameEvent.ShopSale;
if (k.Contains("ENERGY") && (k.Contains("REFIII") || k.Contains("REGEN") || k.Contains("FAST"))) return GameEvent.EnergyRegen;
if (k.Contains("STAMINA") && (k.Contains("REFIII") || k.Contains("REGEN") || k.Contains("FAST"))) return GameEvent.StaminaRegen;
if (k.Contains("HEAITH") && (k.Contains("REFIII") || k.Contains("REGEN") || k.Contains("FAST"))) return GameEvent.HealthRegen;
if (k.Contains("CASH") || k.Contains("PAYDOUBIE")) return GameEvent.Cash;
return GameEvent.Other;
}
public List<ActiveEvent> ReadEvents(List<EventBadge> badges)
{
var events = new List<ActiveEvent>();
foreach (var b in badges)
{
Check();
Win.Hover(b.Box.X + b.Box.Width / 2, b.Box.Y + b.Box.Height / 2);
ActiveEvent e = null;
for (int look = 0; look < 3 && (e == null || e.Kind == GameEvent.Other || e.SecondsLeft < 0); look++)
{
Wait(look == 0 ? 350 : 300);
using (var f = Capture())
{
var again = ReadTooltip(f, b);
if (again != null && (e == null || again.Kind != GameEvent.Other || again.SecondsLeft >= 0)) e = again;
}
}
if (e != null && e.Kind == GameEvent.Other)
{
Wait(1000);
using (var f = Capture())
{
var again = ReadTooltip(f, b);
if (again != null && again.Kind != GameEvent.Other) e = again;
}
}
if (e != null && e.SecondsLeft < 0 && Parse.Key(e.Title).StartsWith(Parse.Key("DAILY BUFF")))
e.SecondsLeft = (int)(DateTime.UtcNow.Date.AddDays(1) - DateTime.UtcNow).TotalSeconds;
if (e != null) events.Add(e);
}
for (int tries = 0; tries < 3; tries++)
{
Win.LeaveToPark();
Wait(400);
EventBadge open;
using (var f = Capture()) open = badges.FirstOrDefault(b => TooltipOpen(f, b));
if (open == null) break;
if (tries == 2) { Snapshot("event tooltip left open", 60); break; }
Win.Hover(open.Box.X + open.Box.Width / 2, open.Box.Y + open.Box.Height / 2);
Wait(300);
}
return events;
}
public static bool TooltipOpen(Frame f, EventBadge b) { return !TooltipFrame(f, b).IsEmpty; }
public sealed class BriefcasePage
{
public bool Page;
public int Free = -1;
public int ButtonFree = -1;
public int NextSeconds = -1;
public int Keys = -1, Left = -1;
public string Paid;
public List<Rectangle> Cases = new List<Rectangle>();
}
static readonly Regex FreePicksRx = new Regex(@"([0-9]{1,2})\s*FREE\s*PICKS?", RegexOptions.IgnoreCase);
static readonly Regex ButtonPicksRx = new Regex(@"([0-9]{1,2})\s*FREE\s*PICKS?");
static readonly Regex NoPickRx = new Regex(@"^\s*FREE\s*PICK\s*[I1l]N\s*(.+)$", RegexOptions.IgnoreCase);
static readonly Regex NextPickRx = new Regex(@"\+\s*1\s*[I1l]N\s*([^)]+)", RegexOptions.IgnoreCase);
static readonly Regex HaveRx = new Regex(@"have\s*([0-9]{1,2}|no)\s*free\s*pick", RegexOptions.IgnoreCase);
static readonly Regex KeysRx = new Regex(@"^\s*([0-9O]{1,3})\s*KEYS?\s*$", RegexOptions.IgnoreCase);
static readonly Regex LeftRx = new Regex(@"^\s*([0-9]{1,2})\s*LEFT\s*$", RegexOptions.IgnoreCase);
static readonly Regex PaidRx = new Regex(@"Case\s*\S{1,3}\s*paid\s*(.+?)\.?\s*(Added|$)", RegexOptions.IgnoreCase);
static bool IsPlate(Color c) { return Math.Abs(c.R - 245) <= 14 && Math.Abs(c.G - 236) <= 14 && Math.Abs(c.B - 215) <= 16; }
public BriefcasePage ReadBriefcases(Frame f)
{
var page = new BriefcasePage();
var area = PageArea(f);
var lines = Ocr.Read(f, area, PageScale(f), Prep.None);
bool title = lines.Any(l => Parse.Key(l.Text).Contains("BRIEFCASES") && l.Box.Y < area.Y + area.Height / 4);
var board = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains("ONTHISBOARD"));
page.Page = title && (board != null || lines.Any(l => Parse.Key(l.Text).Contains("FREEPICK")));
if (!page.Page) return page;
var titleLine = lines.Where(l => Parse.Key(l.Text).Contains("BRIEFCASES")).OrderBy(l => l.Box.Y).First();
int th = Math.Max(10, titleLine.Box.Height);
var band = Rectangle.Intersect(Rectangle.FromLTRB(area.X + area.Width / 2, titleLine.Box.Y - th, area.Right, titleLine.Box.Bottom + th), area);
Func<OcrLine, bool> button = l => (ButtonPicksRx.IsMatch(l.Text) && !Regex.IsMatch(l.Text, @"\bhave\b", RegexOptions.IgnoreCase)) || NoPickRx.IsMatch(l.Text.Trim());
if (!lines.Any(button))
foreach (var t in new[] { Tuple.Create(1, Prep.None), Tuple.Create(2, Prep.Contrast), Tuple.Create(3, Prep.None) })
{
var more = Ocr.Read(f, band, t.Item1, t.Item2, true);
lines.AddRange(more);
if (more.Any(button)) break;
}
foreach (var l in lines)
{
string t = l.Text.Trim();
Match m;
if ((m = FreePicksRx.Match(t)).Success && page.Free < 0) page.Free = int.Parse(m.Groups[1].Value);
if ((m = ButtonPicksRx.Match(t)).Success && page.ButtonFree < 0 && !Regex.IsMatch(t, @"\bhave\b", RegexOptions.IgnoreCase)) page.ButtonFree = int.Parse(m.Groups[1].Value);
if ((m = NoPickRx.Match(t)).Success) { page.Free = page.ButtonFree = 0; page.NextSeconds = Parse.Countdown(m.Groups[1].Value); }
if ((m = NextPickRx.Match(t)).Success && page.NextSeconds < 0) page.NextSeconds = Parse.Countdown(m.Groups[1].Value);
if ((m = HaveRx.Match(t)).Success && page.Free < 0) page.Free = m.Groups[1].Value.ToLowerInvariant() == "no" ? 0 : int.Parse(m.Groups[1].Value);
if ((m = KeysRx.Match(t)).Success) page.Keys = int.Parse(m.Groups[1].Value.Replace('O', '0').Replace('o', '0'));
if ((m = LeftRx.Match(t)).Success) page.Left = int.Parse(m.Groups[1].Value);
if ((m = PaidRx.Match(t)).Success) page.Paid = m.Groups[1].Value.Trim();
}
int right = board != null ? board.Box.X - board.Box.Height : area.X + area.Width * 2 / 3;
int minW = f.Width * 4 / 100, minH = Math.Max(6, (int)(15 * Math.Min(f.Height / 1009.0, f.Width / 1920.0)));
var seen = new List<Rectangle>();
for (int y = area.Y + area.Height / 6; y < area.Bottom; y += 2)
for (int x = area.X; x < right; )
{
if (!IsPlate(f.Pixel(x, y))) { x += 2; continue; }
int s = x;
while (x < right && IsPlate(f.Pixel(x, y))) x++;
if (x - s < minW || seen.Any(r => r.Contains(s + (x - s) / 2, y))) continue;
int top = y, bottom = y, cx = s + Math.Max(2, (x - s) / 20);
while (top > area.Y && IsPlate(f.Pixel(cx, top - 1))) top--;
while (bottom < area.Bottom - 1 && IsPlate(f.Pixel(cx, bottom + 1))) bottom++;
var plate = Rectangle.FromLTRB(s, top, x, bottom + 1);
seen.Add(plate);
if (plate.Height >= minH && plate.Height <= plate.Width) page.Cases.Add(plate);
}
page.Cases = page.Cases.OrderBy(r => r.Y / Math.Max(1, minH * 2)).ThenBy(r => r.X).ToList();
return page;
}
OcrLine EventEntry(Frame f, View v, Func<string, bool> isEntry)
{
var lines = Ocr.Read(f, v.Menu, View.ScaleFor(f, 11, 11.0 * f.Height / 1009), Prep.None);
return lines.FirstOrDefault(l => isEntry(Parse.Key(l.Text)));
}
public bool OpenBriefcases()
{
return OpenEventPage(k => k.Contains("BRIEFCASE"), f => ReadBriefcases(f).Page);
}
bool OpenEventPage(Func<string, bool> isEntry, Func<Frame, bool> isPage)
{
bool rolled = false, unfolded = false;
for (int attempt = 0; attempt < 4; attempt++)
{
OcrLine entry;
View v;
using (var f = Capture())
{
v = ViewOf(f);
if (v.Problem != null) { bool cleared = ClearPopupIfAny(f); ForgetView(); if (!cleared) Wait(1000); continue; }
if (isPage(f)) return true;
entry = EventEntry(f, v, isEntry);
if (entry == null && !unfolded && !v.ReadMenu(f)) { ForgetView(); Wait(1000); continue; }
}
if (entry == null && !unfolded && v.EventsBar != null && !v.EventsOpen)
{
unfolded = true;
Press(v.EventsBar.Text, v.EventsBar.CenterX, v.EventsBar.CenterY, 900);
ForgetView();
attempt--;
continue;
}
if (entry == null)
{
if (rolled) return false;
rolled = true;
Scroll(v.Menu.X + v.Menu.Width / 2, v.Menu.Y + v.Menu.Height / 2, 10);
ForgetView();
continue;
}
if (entry.CenterY < ChatZoneBottom) using (var f = Capture()) if (HideChat(f)) { ForgetView(); continue; }
Press(entry.Text, entry.CenterX, entry.CenterY, 900);
for (int look = 0; look < 3; look++)
{
using (var f = Capture()) if (isPage(f)) return true;
Wait(600);
}
ForgetView();
}
return false;
}
public bool FoldEvents()
{
View v;
using (var f = Capture())
{
v = ViewOf(f);
if (v.Problem != null || !v.ReadMenu(f)) { ForgetView(); return false; }
}
if (v.EventsBar == null || !v.EventsOpen) return false;
Press(v.EventsBar.Text, v.EventsBar.CenterX, v.EventsBar.CenterY, 700);
ForgetView();
return true;
}
public string PickBriefcase(BriefcasePage page, out BriefcasePage after)
{
after = null;
if (page == null || page.Free <= 0 || page.Cases.Count == 0) return null;
var plate = page.Cases[0];
Press("briefcase", plate.X + plate.Width / 2, plate.Y + plate.Height / 2, 900);
for (int look = 0; look < 4; look++)
{
using (var f = Capture())
{
if (look == 0) CheckForPurchasePrompt(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None));
after = ReadBriefcases(f);
if (Picked(page, after))
{
Snapshot("lucky briefcase picked", 0, true);
return after.Paid != null && after.Paid != page.Paid ? after.Paid : "";
}
}
Wait(500);
}
Snapshot("lucky briefcase pick not seen", 60);
return null;
}
internal static bool Picked(BriefcasePage before, BriefcasePage after)
{
if (before == null || after == null || !after.Page) return false;
return (after.Paid != null && after.Paid != before.Paid) || after.Cases.Count < before.Cases.Count
|| (after.Free >= 0 && before.Free >= 0 && after.Free < before.Free) || (after.Left >= 0 && before.Left >= 0 && after.Left < before.Left);
}
public enum BossState { Unknown, Settled, Upcoming, Live }
public sealed class GlobalBossPage
{
public bool Page;
public BossState State;
public int Seconds = -1;
public string Status = "";
public string Boss;
public OcrLine Attack;
public int YourHealth = -1, YourHealthMax = -1;
public bool ClaimReady;
public OcrLine Claim;
public List<string> Received = new List<string>();
}
static readonly Regex BossTimerRx = new Regex(@"(CLOSES|ENDS|STARTS|BEGINS|OPENS|SPAWNS|ARRIVES|NEXT)\s*[I1l]N\s*:?\s*([^)]+)", RegexOptions.IgnoreCase);
static readonly Regex BossNameRx = new Regex(@"(?:DID\s*NOT\s*HIT|YOU\s*HIT)\s+([A-Z][A-Z' .-]{2,30}?)\s*$");
static readonly Regex BossCardHealthRx = new Regex(@"(\d{1,6})?\s*/\s*(\d{2,6})\s*$");
static bool IsGlobalBossEntry(string key)
{
string k = key.Replace('I', 'L').Replace('1', 'L').Replace('|', 'L').Replace('0', 'O');
return k.Contains("GLOBALBOSS") || k.Contains("LOBALBOSS") || k.Contains("GLOBALBO");
}
public GlobalBossPage ReadGlobalBoss(Frame f)
{
var page = new GlobalBossPage();
var area = PageArea(f);
var lines = Ocr.Read(f, area, PageScale(f), Prep.None);
var title = lines.Where(l => IsGlobalBossEntry(Parse.Key(l.Text)) && l.Box.Y < area.Y + area.Height / 5).OrderBy(l => l.Box.Y).FirstOrDefault();
if (title == null) return page;
page.Page = true;
int th = Math.Max(10, title.Box.Height);
var status = lines.Where(l => l != title && Math.Abs(l.CenterY - title.CenterY) < th && l.Box.X > title.Box.X).OrderBy(l => l.Box.X);
string rest = Regex.Replace(title.Text, @"^.*?B\s*[O0]\s*S\s*S", "", RegexOptions.IgnoreCase).Trim();
page.Status = (rest + " " + string.Join(" ", status.Select(l => l.Text))).Trim();
string all = Parse.Key(string.Join(" ", lines.Select(l => l.Text)));
string st = Parse.Key(page.Status);
var m = BossTimerRx.Match(page.Status);
if (m.Success) page.Seconds = Parse.Countdown(m.Groups[2].Value);
string word = m.Success ? m.Groups[1].Value.ToUpperInvariant() : "";
page.ClaimReady = st.Contains(Parse.Key("READY TO CLAIM"));
if (st.Contains(Parse.Key("SETTLED")) || st.Contains(Parse.Key("DEFEATED")) || all.Contains("TAKENDOWN") || all.Contains(Parse.Key("NO MEDALS THIS TIME")) || all.Contains("YOUDIDNOTHIT") || word == "CLOSES")
page.State = BossState.Settled;
else if (word == "STARTS" || word == "BEGINS" || word == "OPENS" || word == "SPAWNS" || word == "ARRIVES" || word == "NEXT"
|| st.Contains("UPCOMING") || st.Contains("SOON"))
page.State = BossState.Upcoming;
else
page.State = BossState.Live;
if (page.State == BossState.Live && word != "ENDS") page.Seconds = -1;
foreach (var l in lines)
{
var n = BossNameRx.Match(l.Text.Trim());
if (n.Success) { page.Boss = n.Groups[1].Value.Trim(); break; }
}
if (page.State == BossState.Settled)
{
page.Claim = GlobalBossClaimButton(lines, page.ClaimReady);
page.Received = GlobalBossReceived(lines);
}
if (page.State != BossState.Live) return page;
page.Attack = lines.Where(l => Parse.Key(l.Text) == "ATTACK" && l.CenterY > area.Y + area.Height / 2 && l.CenterX < area.X + area.Width / 2)
.OrderBy(l => l.Box.Y).FirstOrDefault();
var you = lines.FirstOrDefault(l => Parse.Key(l.Text).StartsWith("YOU") && Parse.Key(l.Text).Length <= 12 && l.CenterY > area.Y + area.Height / 2
&& l.CenterX < area.X + area.Width / 2);
if (you != null)
{
int yh = Math.Max(10, you.Box.Height);
foreach (var l in lines.Where(x => Math.Abs(x.CenterY - you.CenterY) < yh && x.Box.X >= you.Box.X).OrderBy(x => x.Box.X))
{
var hm = BossCardHealthRx.Match(Regex.Replace(l.Text, @"^\s*Y\s*O\s*U\s*", "", RegexOptions.IgnoreCase));
int v, mx;
if (!hm.Success || !int.TryParse(hm.Groups[2].Value, out mx) || mx <= 0) continue;
page.YourHealthMax = mx;
if (hm.Groups[1].Success && int.TryParse(hm.Groups[1].Value, out v) && v <= mx) page.YourHealth = v;
break;
}
}
return page;
}
public void GlobalBossAttack(GlobalBossPage page)
{
if (page != null && page.Attack != null && Parse.Key(page.Attack.Text) == "ATTACK") Press(page.Attack, 250);
}
internal static OcrLine GlobalBossClaimButton(List<OcrLine> lines, bool ready)
{
var whole = lines.FirstOrDefault(l => Parse.Key(l.Text) == Parse.Key("CLAIM DROPS"));
if (whole != null) return whole;
string claim = Parse.Key("CLAIM"), drops = Parse.Key("DROPS");
foreach (var c in lines.Where(l => Parse.Key(l.Text) == claim))
{
int h = Math.Max(10, c.Box.Height);
if (ready || lines.Any(d => Parse.Key(d.Text) == drops && d.Box.Y >= c.Box.Y && d.Box.Y - c.Box.Bottom < 2 * h && Math.Abs(d.CenterX - c.CenterX) < 4 * h)) return c;
}
return null;
}
static readonly Regex BossDropCountRx = new Regex(@"^[xXĂ—]\s*([\dltIOo|]{1,6})$");
internal static List<string> GlobalBossReceived(List<OcrLine> lines)
{
var got = new List<string>();
var head = lines.FirstOrDefault(l => Parse.Key(l.Text).Contains(Parse.Key("YOU RECEIVED")));
if (head == null) return got;
int h = Math.Max(10, head.Box.Height);
Func<OcrLine, bool> inBox = l => l.Box.Y > head.Box.Bottom + 3 * h && l.Box.Y < head.Box.Bottom + 16 * h && Math.Abs(l.CenterX - head.CenterX) < 12 * h;
var counts = lines.Where(l => inBox(l) && BossDropCountRx.IsMatch(l.Text.Trim())).ToList();
var taken = new HashSet<OcrLine>();
foreach (var name in lines.Where(l => inBox(l) && !counts.Contains(l)).OrderBy(l => l.Box.Y / (2 * h)).ThenBy(l => l.Box.X))
{
if (!taken.Add(name)) continue;
string words = name.Text.Trim();
foreach (var more in lines.Where(l => l != name && inBox(l) && !counts.Contains(l) && l.Box.Y > name.Box.Y && l.Box.Y - name.Box.Y < 2 * h && Math.Abs(l.CenterX - name.CenterX) < 2 * h))
if (taken.Add(more)) words += " " + more.Text.Trim();
var c = counts.Where(x => x.Box.Bottom <= name.Box.Y + h && name.Box.Y - x.Box.Bottom < 7 * h && Math.Abs(x.CenterX - name.CenterX) < 5 * h)
.OrderBy(x => Math.Abs(x.CenterX - name.CenterX)).FirstOrDefault();
string n = c == null ? "" : Regex.Replace(BossDropCountRx.Match(c.Text.Trim()).Groups[1].Value, "[ltI|]", "1").Replace('O', '0').Replace('o', '0') + " ";
got.Add(n + words);
}
return got;
}
public GlobalBossPage ClaimGlobalBossDrops(GlobalBossPage page)
{
if (page == null || page.Claim == null) return null;
Press("CLAIM DROPS", page.Claim.CenterX, page.Claim.CenterY, 1500);
GlobalBossPage gone = null;
for (int look = 0; look < 8; look++)
{
GlobalBossPage now;
using (var f = Capture())
{
now = ReadGlobalBoss(f);
if (!now.Page) ClearPopupIfAny(f);
}
ForgetView();
if (now.Page && (now.Claim != null || now.ClaimReady)) gone = null;
else if (now.Page)
{
gone = now;
if (now.Received.Count > 0) return now;
}
Wait(1000);
}
return gone;
}
public static readonly string[] StaminaItems = { "Bag of Peanuts", "Corned Beef Sandwich", "Tin Lunch Box" };
public static readonly string[] HealthItems = { "Bandages", "Small Medkit", "Large Medkit" };
public string UseConsumable(bool stamina, out bool none)
{
none = false;
if (!OpenTab(Tab.Inventory)) return null;
OcrLine tab;
using (var f = Capture()) tab = FindInventoryTab(f, "MISC");
if (tab == null) { log("Consumables: couldn't find the MISC tab in the inventory"); Snapshot("inventory without misc tab", 60); return null; }
Press(tab, 900);
var names = stamina ? StaminaItems : HealthItems;
int tabBottom = tab.Box.Bottom + tab.Box.Height;
ScrollPage(0.6, 15);
for (int look = 0; look < 5; look++)
{
if (look > 0) { ScrollPage(0.6, -3); Wait(500); }
List<OcrLine> lines;
List<Rectangle> cards;
using (var f = Capture())
{
var area = PageArea(f);
lines = Ocr.Read(f, area, PageScale(f), Prep.None);
int top = Math.Max(area.Top, tabBottom);
cards = ItemPictures(f, new Rectangle(area.Left, top, area.Width, area.Bottom - top));
}
string name;
var card = ConsumableCard(lines, cards, names, out name);
if (card == null) continue;
Press("consumable card", card.Value.X + card.Value.Width / 2, card.Value.Y + card.Value.Height / 2, 900);
for (int m = 0; m < 3; m++)
{
OcrLine use;
using (var f = Capture())
{
use = ConsumableUse(Ocr.Read(f, PageArea(f), PageScale(f), Prep.None), name, card);
if (use == null) use = ConsumableUse(Ocr.Read(f, PageArea(f), PageScale(f) + 1, Prep.None), name, card);
}
if (use != null)
{
Snapshot("consumable menu", 0, true);
Press(use, 150);
Snapshot("consumable after use", 0, true);
Wait(750);
for (int c = 0; c < 4; c++)
{
OcrLine yes;
bool asked;
using (var f = Capture()) yes = ConsumableConfirm(Ocr.Read(f, PopupArea(f), PopupScale(f), Prep.None), name, out asked);
if (yes != null) { Press(yes, 900); continue; }
if (!asked) break;
Wait(600);
if (c == 3)
{
log("Consumables: the game's \"USE " + name.ToUpperInvariant() + "\" window showed no USE");
Snapshot("consumable confirm without use", 60);
using (var f = Capture()) ClearPopupIfAny(f);
return null;
}
}
return name;
}
Wait(600);
}
log("Consumables: " + name + " showed no USE");
Snapshot("consumable without use", 60);
return null;
}
none = true;
return null;
}
internal static bool ItemWorked(int before, int after, int max)
{
if (before < 0 || after < 0) return true;
if (max <= 0) return after > before;
return after >= max || (after - before) * 10 >= max;
}
internal static Rectangle? ConsumableCard(List<OcrLine> lines, List<Rectangle> cards, string[] names, out string name)
{
foreach (var n in names)
{
string k = Parse.Key(n);
foreach (var c in cards)
if (Parse.Key(CardName(lines, c)).Contains(k)) { name = n; return c; }
}
name = null;
return null;
}
internal static OcrLine ConsumableUse(List<OcrLine> lines, string name, Rectangle? card = null)
{
string k = Parse.Key(name);
foreach (var u in lines.Where(l => Parse.Key(l.Text) == "USE"))
{
int h = Math.Max(10, u.Box.Height);
var title = lines.Where(t => t != u && t.Box.Bottom <= u.Box.Y + 2 && u.Box.Y - t.Box.Bottom < 5 * h && Math.Abs(t.CenterX - u.CenterX) < 8 * h)
.OrderBy(t => t.Box.Y).ToList();
string all = Parse.Key(string.Join(" ", title.Select(t => t.Text)));
if (title.Any(t => Parse.Key(t.Text).Contains(k)) || name.Split(' ').All(w => all.Contains(Parse.Key(w)))) return u;
if (card.HasValue)
{
var c = card.Value;
bool beside = u.CenterY > c.Top && u.CenterY < c.Bottom + c.Height / 2 && u.CenterX > c.Left && u.CenterX < c.Right + c.Width * 3 / 2;
if (beside && name.Split(' ').Any(w => w.Length >= 5 && all.Contains(Parse.Key(w)))) return u;
if (beside && lines.Any(d => Parse.Key(d.Text) == Parse.Key("DELETE") && d.Box.Y > u.Box.Bottom && d.Box.Y - u.Box.Bottom < 3 * h && Math.Abs(d.CenterX - u.CenterX) < 2 * h)) return u;
}
}
return null;
}
internal static OcrLine ConsumableConfirm(List<OcrLine> lines, string name, out bool asked)
{
string k = Parse.Key(name);
string first = Parse.Key(name.Split(' ')[0]);
var title = lines.FirstOrDefault(l => { string t = Parse.Key(l.Text); return t.StartsWith("USE") && t.Length > 3 && (t.Contains(k) || t.Substring(3).StartsWith(first)); });
asked = title != null;
if (title == null) return null;
int h = Math.Max(10, title.Box.Height);
return lines.Where(u => Parse.Key(u.Text) == "USE" && u.Box.Y > title.Box.Bottom && u.Box.Y - title.Box.Bottom < 12 * h
&& lines.Any(c => Parse.Key(c.Text) == Parse.Key("CANCEL") && c.Box.X > u.Box.Right && Math.Abs(c.CenterY - u.CenterY) < Math.Max(10, u.Box.Height)))
.OrderBy(u => u.Box.Y).FirstOrDefault();
}
public OcrLine FindInventoryTab(Frame f, string word)
{
var strip = TabStrip(PageArea(f));
int s = PageScale(f);
string k = Parse.Key(word);
var others = new[] { "WEAPONS", "ARMOR", "VEHICLES", "CRATES", "COSMETICS", "TROPHIES" }.Select(Parse.Key).ToList();
var row = Ocr.Read(f, strip, s, Prep.None).Where(l => others.Contains(Parse.Key(l.Text))).ToList();
Func<OcrLine, bool> onRow = l => row.Count == 0 || row.Any(r => Math.Abs(r.CenterY - l.CenterY) < Math.Max(8, r.Box.Height));
Func<List<OcrLine>, OcrLine> top = ls => ls.Where(l => Parse.Key(l.Text) == k && onRow(l)).OrderBy(l => l.Box.Y).FirstOrDefault();
return top(Ocr.Read(f, strip, s, Prep.None)) ?? top(Ocr.Read(f, strip, s, Prep.DarkText)) ?? top(Ocr.ReadWords(f, strip, s, Prep.DarkText))
?? top(Ocr.ReadWords(f, strip, s, Prep.None));
}
public bool OpenGlobalBoss()
{
return OpenEventPage(IsGlobalBossEntry, f => ReadGlobalBoss(f).Page);
}
}
}