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

416 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}
}