Files
MVP1/Assets/Scripts/Meta/World/WorldSystem.cs
T

416 lines
18 KiB
C#
Raw Normal View History

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());
}
}