using System; using System.Collections.Generic; using System.Text; using UnityEngine; // ЛОГИКА ГЛОБАЛЬНОЙ КАРТЫ: // отправка армии -> марш по маршруту (таймер) -> прибытие -> бой по раундам -> потери, добыча // -> возвращение домой (таймер) -> выжившие снова в городе, добыча на складе, отчёт в почте. // Время реальное (как у обучения войск): поход продолжается, даже если игра закрыта. public static class WorldSystem { static GameProgress GP { get { return GameProgress.Instance; } } static ProgressData D { get { return GameProgress.Instance.Data; } } static long Now { get { return DateTime.UtcNow.Ticks; } } // ================= ПОСЕЛЕНИЯ ================= public static SettlementState State(string id) { var s = D.settlements.Find(x => x.id == id); if (s == null) { s = new SettlementState { id = id }; var def = WorldData.Get(id); if (def != null && def.baseGarrison != null) s.garrison = FullGarrison(def, 1); D.settlements.Add(s); } if (s.garrison == null || s.garrison.Length < 4) s.garrison = new int[4]; return s; } public static int[] FullGarrison(SettlementDef def, int level) { var g = new int[4]; if (def.baseGarrison == null) return g; float m = WorldData.LevelMultiplier(level); for (int i = 0; i < 4 && i < def.baseGarrison.Length; i++) g[i] = Mathf.RoundToInt(def.baseGarrison[i] * m); return g; } public static RewardItem[] FullLoot(SettlementDef def, int level) { if (def.baseLoot == null) return new RewardItem[0]; float m = WorldData.LevelMultiplier(level); var list = new RewardItem[def.baseLoot.Length]; for (int i = 0; i < list.Length; i++) list[i] = new RewardItem(def.baseLoot[i].type, Mathf.RoundToInt(def.baseLoot[i].amount * m)); return list; } // Разграблено и пока пустует? public static bool IsRuined(string id, long at) { return State(id).ruinedUntilTicks > at; } public static double RuinedSecondsLeft(string id) { return TimeSpan.FromTicks(Math.Max(0, State(id).ruinedUntilTicks - Now)).TotalSeconds; } // Восстановление поселений со временем static void RefreshSettlement(SettlementDef def, long at) { if (def.isHome) return; var s = State(def.id); if (s.ruinedUntilTicks > 0 && s.ruinedUntilTicks <= at) { s.ruinedUntilTicks = 0; // отстроились, уже на новом уровне s.garrison = FullGarrison(def, s.level); GP.MarkChanged(); } else if (s.ruinedUntilTicks == 0 && s.lastBattleTicks > 0 && at - s.lastBattleTicks >= TimeSpan.FromMinutes(WorldData.GarrisonRegenMinutes).Ticks) { var full = FullGarrison(def, s.level); bool changed = false; for (int i = 0; i < 4; i++) if (s.garrison[i] != full[i]) { s.garrison[i] = full[i]; changed = true; } s.lastBattleTicks = 0; if (changed) GP.MarkChanged(); } } public static int GarrisonPower(int[] g) { int p = 0; for (int i = 0; i < 4; i++) p += g[i] * WorldData.UnitPower(i); return p; } // ================= МАРШРУТ И ВРЕМЯ ================= public static float Distance(SettlementDef target) { return Vector2.Distance(WorldData.Home.pos, target.pos); } public static float ArmySpeed(int[] units) { float speed = float.MaxValue; for (int i = 0; i < 4; i++) if (units[i] > 0) speed = Mathf.Min(speed, WorldData.Units[i].speed); return speed == float.MaxValue ? 1f : speed; } public static double TravelSeconds(SettlementDef target, int[] units) { return Distance(target) * WorldData.SecondsPerMapUnit / ArmySpeed(units); } public static int Carry(int[] units) { int c = 0; for (int i = 0; i < 4; i++) c += units[i] * WorldData.Units[i].carry; return c; } public static int Count(int[] units) { int n = 0; foreach (var u in units) n += u; return n; } public static int Power(int[] units) { return GarrisonPower(units); } // Где сейчас армия: 0 = дома, 1 = у цели public static float Progress(WorldMarch m) { long now = Now; if (!m.returning) return Mathf.Clamp01((float)(now - m.startTicks) / Math.Max(1, m.arriveTicks - m.startTicks)); // на обратном пути: из точки разворота (1 = от цели, меньше -- если отозвали по дороге) домой float t = Mathf.Clamp01((float)(now - m.arriveTicks) / Math.Max(1, m.returnTicks - m.arriveTicks)); return Mathf.Lerp(m.turnPoint, 0f, t); } public static double SecondsLeft(WorldMarch m) { long end = m.returning ? m.returnTicks : m.arriveTicks; return TimeSpan.FromTicks(Math.Max(0, end - Now)).TotalSeconds; } public static int TroopsInMarches { get { int n = 0; foreach (var m in D.marches) n += Count(m.returning ? m.survivors : m.units); return n; } } // ================= ОТПРАВКА / ОТЗЫВ ================= public static string CanSendReason(string targetId, int[] units) { var def = WorldData.Get(targetId); if (def == null || def.isHome) return "Выберите цель"; if (D.marches.Count >= WorldData.MaxMarches) return "Все отряды уже в походе (макс. " + WorldData.MaxMarches + ")"; if (Count(units) <= 0) return "Выберите войска"; for (int i = 0; i < 4; i++) if (units[i] > D.troops[i]) return "Столько войск нет в городе"; if (IsRuined(targetId, Now)) return "Поселение разграблено, ещё отстраивается"; return null; } public static WorldMarch Send(string targetId, int[] units) { string why = CanSendReason(targetId, units); if (why != null) { UIKit.Toast(why); return null; } var def = WorldData.Get(targetId); var m = new WorldMarch { id = D.nextMarchId++, targetId = targetId, startTicks = Now }; for (int i = 0; i < 4; i++) { m.units[i] = units[i]; D.troops[i] -= units[i]; } m.arriveTicks = m.startTicks + (long)(TravelSeconds(def, units) * TimeSpan.TicksPerSecond); D.marches.Add(m); D.marchesSent++; D.dailyMarches++; UIKit.Toast("Армия выступила: " + def.name + ". Прибытие через " + GameData.FormatTime(SecondsLeft(m))); GP.MarkChanged(); return m; } // Развернуть армию домой, пока она не дошла (обратно идёт столько же, сколько прошла) public static void Recall(WorldMarch m) { if (m == null || m.returning) return; long now = Now; long walked = now - m.startTicks; m.turnPoint = Progress(m); m.returning = true; m.recalled = true; for (int i = 0; i < 4; i++) m.survivors[i] = m.units[i]; m.arriveTicks = now; m.returnTicks = now + walked; UIKit.Toast("Армия разворачивается домой"); GP.MarkChanged(); } // ================= ОБРАБОТКА (вызывается раз в секунду из GameProgress) ================= public static void Process() { long now = Now; foreach (var def in WorldData.Settlements) RefreshSettlement(def, now); // копия списка: внутри можем удалять foreach (var m in D.marches.ToArray()) { if (!m.returning && now >= m.arriveTicks) Arrive(m); if (m.returning && now >= m.returnTicks) ReturnHome(m); } } static void Arrive(WorldMarch m) { var def = WorldData.Get(m.targetId); RefreshSettlement(def, m.arriveTicks); var s = State(m.targetId); long travel = m.arriveTicks - m.startTicks; if (s.ruinedUntilTicks > m.arriveTicks) { // пока шли, поселение уже разграбили -- возвращаемся ни с чем for (int i = 0; i < 4; i++) m.survivors[i] = m.units[i]; m.returning = true; m.returnTicks = m.arriveTicks + travel; AddReport(false, "Поход отменён: " + def.name, "Поселение уже разграблено. Армия возвращается домой без боя."); GP.MarkChanged(); return; } int level = s.level; var defenders = (int[])s.garrison.Clone(); var sim = Simulate(m.units, defenders, GP.ArmyBonus, WorldData.DefenderBonus, true); for (int i = 0; i < 4; i++) m.survivors[i] = sim.attLeft[i]; s.garrison = sim.defLeft; s.lastBattleTicks = m.arriveTicks; var sb = new StringBuilder(); sb.AppendLine("Цель: " + def.name + " (ур. " + level + ")"); sb.AppendLine("Раундов: " + sim.rounds); sb.AppendLine(); sb.AppendLine("Ваша армия: " + UnitsText(m.units)); sb.AppendLine("Ваши потери: " + LossText(m.units, sim.attLeft)); sb.AppendLine("Защитники: " + UnitsText(defenders)); sb.AppendLine("Потери врага: " + LossText(defenders, sim.defLeft)); sb.AppendLine(); foreach (var line in sim.log) sb.AppendLine(line); if (sim.win) { // добыча ограничена грузоподъёмностью выживших var loot = FullLoot(def, level); int total = 0; foreach (var r in loot) total += r.amount; int carry = Carry(sim.attLeft); float k = total > 0 ? Mathf.Min(1f, (float)carry / total) : 0f; m.loot.Clear(); foreach (var r in loot) { int a = Mathf.FloorToInt(r.amount * k); if (a > 0) m.loot.Add(new RewardItem(r.type, a)); } s.timesRaided++; s.level++; s.garrison = new int[4]; s.ruinedUntilTicks = m.arriveTicks + TimeSpan.FromMinutes(def.respawnMinutes).Ticks; D.worldWins++; GP.AddEventPoints(15); sb.AppendLine(); sb.AppendLine("Добыча: " + (m.loot.Count > 0 ? GameData.RewardsText(m.loot.ToArray()) : "нечего унести") + (k < 1f ? " (не хватило грузоподъёмности)" : "")); sb.AppendLine("Поселение отстроится через " + GameData.FormatTime(def.respawnMinutes * 60) + " и станет сильнее."); AddReport(true, "Победа: " + def.name, sb.ToString()); } else { D.worldLosses++; sb.AppendLine(); sb.AppendLine(Count(sim.attLeft) > 0 ? "Армия не смогла взять поселение и отступает." : "Армия разбита."); AddReport(false, "Поражение: " + def.name, sb.ToString()); } if (Count(m.survivors) == 0) { D.marches.Remove(m); // некому возвращаться UIKit.Toast("Армия погибла у «" + def.name + "»"); } else { m.returning = true; m.returnTicks = m.arriveTicks + travel; UIKit.Message(sim.win ? "ПОБЕДА!" : "Поражение", def.name + "\nВаши потери: " + LossText(m.units, sim.attLeft) + (sim.win ? "\nДобыча: " + (m.loot.Count > 0 ? GameData.RewardsText(m.loot.ToArray()) : "-") : "") + "\n\nАрмия возвращается домой: " + GameData.FormatTime(SecondsLeft(m)) + "\nПолный отчёт -- в Почте.", "ОК"); } GP.MarkChanged(); } static void ReturnHome(WorldMarch m) { for (int i = 0; i < 4; i++) D.troops[i] += m.survivors[i]; D.marches.Remove(m); var def = WorldData.Get(m.targetId); if (m.loot.Count > 0) { int xp = 20 + 10 * State(m.targetId).timesRaided; GP.Grant(m.loot.ToArray(), xp, false); UIKit.Toast("Армия вернулась из похода (" + def.name + "). Добыча на складе: " + GameData.RewardsText(m.loot.ToArray())); } else { UIKit.Toast("Армия вернулась домой: " + Count(m.survivors) + " воинов"); } GP.MarkChanged(); } static void AddReport(bool win, string title, string text) { D.reports.Insert(0, new BattleReport { ticks = DateTime.Now.Ticks, title = title, text = text, win = win }); if (D.reports.Count > 15) D.reports.RemoveAt(D.reports.Count - 1); GP.SendMail("Отчёт о бою: " + title, text); } // ================= БОЙ ================= public class SimResult { public bool win; public int rounds; public int[] attLeft = new int[4]; public int[] defLeft = new int[4]; public List log = new List(); } // Пошаговый бой: каждый раунд обе стороны одновременно наносят урон. // Урон стороны = сумма атак всех воинов * бонус * случайность (±15%). // Урон делится между типами врага пропорционально их суммарному здоровью, // защита снижает урон (каждая единица -8%). Погибшие = урон / здоровье. // Победа -- у врага никого не осталось. За 10 раундов не справились -- отступление (поражение). public static SimResult Simulate(int[] attackers, int[] defenders, float attBonus, float defBonus, bool withLog) { var r = new SimResult(); float[] a = new float[4], d = new float[4]; for (int i = 0; i < 4; i++) { a[i] = attackers[i]; d[i] = defenders[i]; } for (int round = 1; round <= WorldData.MaxRounds; round++) { if (Sum(a) < 0.5f || Sum(d) < 0.5f) break; float dmgA = Damage(a, attBonus); float dmgD = Damage(d, defBonus); float killedD = Apply(d, dmgA); float killedA = Apply(a, dmgD); r.rounds = round; if (withLog) r.log.Add(string.Format("Раунд {0}: вы нанесли {1:0} урона (убито {2:0}), враг нанёс {3:0} (убито {4:0})", round, dmgA, killedD, dmgD, killedA)); } for (int i = 0; i < 4; i++) { r.attLeft[i] = Mathf.Clamp(Mathf.RoundToInt(a[i]), 0, attackers[i]); r.defLeft[i] = Mathf.Clamp(Mathf.RoundToInt(d[i]), 0, defenders[i]); } r.win = Count(r.defLeft) == 0 && Count(r.attLeft) > 0; return r; } static float Sum(float[] x) { float s = 0; foreach (var v in x) s += v; return s; } static float Damage(float[] side, float bonus) { float dmg = 0; for (int i = 0; i < 4; i++) dmg += side[i] * WorldData.Units[i].attack; return dmg * (1f + bonus) * UnityEngine.Random.Range(0.85f, 1.15f); } static float Apply(float[] side, float dmg) { float totalHp = 0; for (int i = 0; i < 4; i++) totalHp += side[i] * WorldData.Units[i].hp; if (totalHp <= 0) return 0; float killed = 0; for (int i = 0; i < 4; i++) { if (side[i] <= 0) continue; var u = WorldData.Units[i]; float share = side[i] * u.hp / totalHp; float k = share * dmg / (u.hp * (1f + u.defense * 0.08f)); k = Mathf.Min(k, side[i]); side[i] -= k; killed += k; } return killed; } // Оценка шанса: прогоняем бой много раз и считаем долю побед public static float EstimateWinChance(int[] attackers, string targetId) { if (Count(attackers) == 0) return 0f; var s = State(targetId); int wins = 0; const int runs = 40; for (int k = 0; k < runs; k++) if (Simulate(attackers, s.garrison, GP.ArmyBonus, WorldData.DefenderBonus, false).win) wins++; return (float)wins / runs; } // ================= ТЕКСТЫ ================= public static string UnitsText(int[] units) { var parts = new List(); for (int i = 0; i < 4; i++) if (units[i] > 0) parts.Add(GameData.Troops[i].name + " " + units[i]); return parts.Count == 0 ? "нет" : string.Join(", ", parts.ToArray()); } public static string LossText(int[] before, int[] after) { var parts = new List(); for (int i = 0; i < 4; i++) { int lost = before[i] - after[i]; if (lost > 0) parts.Add(GameData.Troops[i].name + " " + lost); } return parts.Count == 0 ? "нет" : string.Join(", ", parts.ToArray()); } }