Files
MVP1/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Points.cs
T

288 lines
14 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
namespace Bogatyri.World
{
// Ресурсная точка на карте (регламент 31.10, раздел 7; решение №46: состояние точки ведёт World).
// Состояние меняется со временем, поэтому хранится в сохранении, а не строится из seed, как замок и города.
[Serializable]
public class ResourcePoint
{
public string id;
public int kind; // ResourcePointRules.Resources: 0 дерево, 1 камень, 2 металл, 3 золото
public int size; // 0 малая, 1 средняя, 2 крупная
public int x;
public int y;
public int stock; // сколько осталось
public int stockMax;
public long spawnMs; // появилась (реальное время часов GameClock)
public long expireMs; // исчезнет
public int occupiedBy; // номер похода, который сейчас собирает (0 -- свободна)
public bool active; // false -- истощена или исчезла (остаётся, пока к ней идёт или с неё возвращается войско)
}
// Числа ресурсных точек (регламент 31.10, раздел 7, ТЕСТ). В игре -- из таблицы WorldResourceConfig.
public sealed class ResourcePointRules
{
// Имена ресурсов -- как в ResourceType (Мир про экономику не знает, зачисляет её модуль по имени)
public static readonly string[] Resources = { "Wood", "Stone", "Iron", "Gold" };
public static readonly string[] KindNames = { "Лес", "Каменоломня", "Рудник", "Золотая жила" };
public static readonly string[] SizeNames = { "малая", "средняя", "крупная" };
// Запас: [ресурс][размер] -- дерево 200/600/1500, камень 150/450/1000, металл 80/250/600, золото 30/100/250
public int[][] Stock =
{
new[] { 200, 600, 1500 },
new[] { 150, 450, 1000 },
new[] { 80, 250, 600 },
new[] { 30, 100, 250 }
};
// Добыча одного воина за ИГРОВОЙ час, в тысячных долях: 2 / 1,5 / 1 / 0,5
public int[] MiningMilliPerGameHour = { 2000, 1500, 1000, 500 };
// Груз одного воина: 12 / 10 / 8 / 5. Осадные машины груза не несут (регламент)
public int[] CargoPerSoldier = { 12, 10, 8, 5 };
// Срок жизни точки в РЕАЛЬНЫХ часах с момента появления: малая 12, средняя 24, крупная 48
public int[] LifetimeRealHours = { 12, 24, 48 };
// 1 игровой час = 5 реальных минут (регламент 31.10, раздел 2)
public long GameHourMs = 5 * 60 * 1000;
// Не задано регламентом -- ТЕСТ, решает команда:
public int PointsPerResource = 2; // сколько точек каждого ресурса одновременно на карте
public int Spacing = 2200; // не ближе 2,2 клетки к другим точкам карты
public static ResourcePointRules Default => new ResourcePointRules();
// Сбор с начала startMs: сколько войско унесёт и когда закончит.
// Конец -- груз полон, точка истощена или исчезла (expireMs), что раньше.
public void Gather(int kind, int[] units, int stock, long startMs, long expireMs, out int amount, out long endMs)
{
int miners = 0, carriers = 0;
for (int i = 0; units != null && i < units.Length; i++)
{
if (units[i] <= 0) continue;
miners += units[i];
if (i != TroopType.Siege) carriers += units[i];
}
long target = Math.Min(Math.Max(0, stock), (long)carriers * CargoPerSoldier[kind]);
long rate = (long)miners * MiningMilliPerGameHour[kind]; // тысячных долей за игровой час
if (target <= 0 || rate <= 0 || expireMs <= startMs)
{
amount = 0;
endMs = startMs;
return;
}
long full = (target * 1000 * GameHourMs + rate - 1) / rate; // время до полного груза (вверх)
if (startMs + full <= expireMs)
{
amount = (int)target;
endMs = startMs + full;
}
else
{
endMs = expireMs; // точка исчезла раньше -- уносят, что успели
amount = (int)Math.Min(target, rate * (expireMs - startMs) / (1000 * GameHourMs));
}
}
}
// Состояние ресурсных точек для сохранения (секция "world")
[Serializable]
public class ResourcePointsState
{
public List<ResourcePoint> points = new List<ResourcePoint>();
public int nextPointId = 1;
public int generation; // сколько точек уже появилось -- для детерминированного места следующей
public bool initialized; // первые точки уже выставлены
}
public sealed partial class WorldSimulation
{
private readonly ResourcePointRules pointRules;
private readonly int pointSeed;
private readonly List<ResourcePoint> points = new List<ResourcePoint>();
private int nextPointId = 1;
private int pointGeneration;
private bool pointsInitialized;
public ResourcePointRules PointRules => pointRules;
public IReadOnlyList<ResourcePoint> ResourcePoints => points;
public ResourcePoint GetPoint(string id)
{
return points.Find(p => p.id == id);
}
// Первые точки: по PointsPerResource каждого ресурса, появляются в момент первого расчёта мира
void EnsureResourcePoints(long nowMs)
{
if (pointsInitialized) return;
pointsInitialized = true;
for (int kind = 0; kind < ResourcePointRules.Resources.Length; kind++)
for (int k = 0; k < pointRules.PointsPerResource; k++)
SpawnPoint(kind, nowMs);
}
// Новая точка того же ресурса в другом месте. Место и размер -- из seed и номера появления (одинаково на сервере)
ResourcePoint SpawnPoint(int kind, long nowMs)
{
pointGeneration++;
var rng = new DeterministicRandom(pointSeed ^ 0x2F0E1D ^ pointGeneration * 7919);
if (!WorldMapLayout.TryFindFreeSpot(rng, nodes, out int x, out int y, pointRules.Spacing)) return null;
int size = rng.Range(0, 3); // размер равновероятно -- регламент не задаёт (ТЕСТ)
var p = new ResourcePoint
{
id = "res_" + nextPointId++,
kind = kind,
size = size,
x = x,
y = y,
stock = pointRules.Stock[kind][size],
stockMax = pointRules.Stock[kind][size],
spawnMs = nowMs,
expireMs = nowMs + pointRules.LifetimeRealHours[size] * 3_600_000L,
active = true
};
points.Add(p);
nodes.Add(ToNode(p));
return p;
}
static MapNode ToNode(ResourcePoint p)
{
return new MapNode
{
id = p.id,
displayName = $"{ResourcePointRules.KindNames[p.kind]} ({ResourcePointRules.SizeNames[p.size]})",
type = MapNodeType.ResourcePoint,
x = p.x,
y = p.y,
canBeAttacked = true, // «можно отправить войско»
level = p.size + 1
};
}
// Точка истощена или исчезла: на её месте больше нечего собирать, новая появляется в другом месте
void RetirePoint(ResourcePoint p, long atMs)
{
if (!p.active) return;
p.active = false;
p.occupiedBy = 0;
SpawnPoint(p.kind, atMs);
}
// Исчезнувшие точки убираются с карты, когда к ним уже никто не идёт и с них никто не возвращается
void PurgeRetiredPoints()
{
points.RemoveAll(p =>
{
if (p.active || marches.Exists(m => m.targetNodeId == p.id)) return false;
nodes.RemoveAll(n => n.id == p.id);
return true;
});
}
public bool CanGatherAt(string pointId, long nowMs, out string error)
{
ResourcePoint p = GetPoint(pointId);
if (p == null || !p.active || nowMs >= p.expireMs) { error = "Этой ресурсной точки больше нет"; return false; }
if (p.occupiedBy != 0) { error = "Точку уже занимает войско"; return false; }
if (marches.Exists(m => m.targetNodeId == pointId && m.state == MarchState.Outbound))
{
error = "К этой точке уже идёт войско";
return false;
}
error = null;
return true;
}
// Прогноз сбора для окна отправки: сколько унесут и сколько продлится сбор, если дойдут в arriveMs
public void PreviewGather(string pointId, int[] units, long arriveMs, out int amount, out long gatherMs)
{
ResourcePoint p = GetPoint(pointId);
if (p == null) { amount = 0; gatherMs = 0; return; }
pointRules.Gather(p.kind, units, p.stock, arriveMs, p.expireMs, out amount, out long endMs);
gatherMs = endMs - arriveMs;
}
// Войско дошло до точки: начинает сбор. Точки нет или она занята -- возвращается без добычи
void StartGather(MarchData m)
{
MapNode from = GetNode(m.fromNodeId);
MapNode target = GetNode(m.targetNodeId);
ResourcePoint p = GetPoint(m.targetNodeId);
int[] units = HasUnits(m) ? m.units : new[] { m.soldiers, 0, 0, 0 };
m.victory = false;
m.survivors = m.soldiers; // при сборе боя нет -- все живы
m.survivorUnits = HasUnits(m) ? (int[])m.units.Clone() : null;
m.loot = new List<RewardEntry>();
if (p == null || !p.active || p.occupiedBy != 0 || m.arriveMs >= p.expireMs)
{
m.targetEmpty = true;
m.gatherEndMs = m.arriveMs;
m.returnMs = m.arriveMs + TravelTimeMs(target, from, units);
m.state = MarchState.Returning;
return;
}
pointRules.Gather(p.kind, units, p.stock, m.arriveMs, p.expireMs, out int amount, out long endMs);
m.cargo = amount;
m.gatherEndMs = endMs;
m.state = MarchState.Gathering;
p.occupiedBy = m.id;
}
// Сбор закончен: груз -- в добычу похода (зачислит Экономика по возвращении), войско идёт домой
void FinishGather(MarchData m)
{
MapNode from = GetNode(m.fromNodeId);
MapNode target = GetNode(m.targetNodeId);
ResourcePoint p = GetPoint(m.targetNodeId);
int[] units = HasUnits(m) ? m.units : null;
if (p != null)
{
p.stock = Math.Max(0, p.stock - m.cargo);
if (p.occupiedBy == m.id) p.occupiedBy = 0;
if (m.cargo > 0) m.loot.Add(new RewardEntry { resource = ResourcePointRules.Resources[p.kind], amount = m.cargo });
if (p.stock <= 0 || m.gatherEndMs >= p.expireMs) RetirePoint(p, m.gatherEndMs);
}
m.returnMs = m.gatherEndMs + TravelTimeMs(target, from, units);
m.state = MarchState.Returning;
}
// ---------------- Сохранение ----------------
public ResourcePointsState GetPointsState()
{
var state = new ResourcePointsState
{
nextPointId = nextPointId,
generation = pointGeneration,
initialized = pointsInitialized
};
foreach (var p in points) state.points.Add(p);
return state;
}
// Загружать ДО походов: поход к точке восстанавливается, только если точка есть на карте
public void LoadPointsState(ResourcePointsState state)
{
nodes.RemoveAll(n => n.type == MapNodeType.ResourcePoint);
points.Clear();
nextPointId = state?.nextPointId ?? 1;
pointGeneration = state?.generation ?? 0;
pointsInitialized = state?.initialized ?? false;
if (state?.points == null) return;
foreach (var p in state.points)
{
if (p == null || p.kind < 0 || p.kind >= ResourcePointRules.Resources.Length || p.size < 0 || p.size > 2) continue;
points.Add(p);
nodes.Add(ToNode(p));
}
}
}
}