416 lines
18 KiB
C#
416 lines
18 KiB
C#
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<string> log = new List<string>();
|
||
}
|
||
|
||
// Пошаговый бой: каждый раунд обе стороны одновременно наносят урон.
|
||
// Урон стороны = сумма атак всех воинов * бонус * случайность (±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<string>();
|
||
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<string>();
|
||
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());
|
||
}
|
||
}
|