IWorld.TryStartScout, MarchData.scout, WorldEventType.ScoutArrived; кнопка «Разведка» в окне атаки; разведка не разбивает лагерь и не занимает точку. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
16 KiB
C#
319 lines
16 KiB
C#
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;
|
|
|
|
// Не задано регламентом -- ТЕСТ, решает команда:
|
|
// Сколько точек одновременно на карте: [ресурс][размер] (решения по 8 вопросам Саши, 31.10, п. 6, ТЕСТ).
|
|
// Всего 24: лес 8, камень 7, металл 5, золото 4; по размерам 12 малых, 8 средних, 4 крупных.
|
|
// Раскладка размеров по ресурсам документом не задана -- задаётся здесь (в игре -- WorldResourceConfig).
|
|
public int[][] Count =
|
|
{
|
|
new[] { 4, 3, 1 }, // лес 8
|
|
new[] { 4, 2, 1 }, // камень 7
|
|
new[] { 2, 2, 1 }, // металл 5
|
|
new[] { 2, 1, 1 } // золото 4
|
|
};
|
|
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);
|
|
}
|
|
|
|
// Точки на карте по таблице Count. Первые -- в момент первого расчёта мира; после загрузки сохранения
|
|
// недостающие добавляются (старое сохранение с 8 точками дополнится до 24, лишние доживут свой срок)
|
|
void EnsureResourcePoints(long nowMs)
|
|
{
|
|
if (pointsInitialized && !pointsTopUp) return;
|
|
pointsInitialized = true;
|
|
pointsTopUp = false;
|
|
SpawnMissingPoints(nowMs);
|
|
}
|
|
|
|
private bool pointsTopUp;
|
|
|
|
// Сколько активных точек каждого ресурса и размера не хватает до таблицы -- столько и появляется
|
|
void SpawnMissingPoints(long nowMs)
|
|
{
|
|
for (int kind = 0; kind < ResourcePointRules.Resources.Length; kind++)
|
|
for (int size = 0; size < 3; size++)
|
|
{
|
|
int have = 0;
|
|
foreach (var p in points) if (p.active && p.kind == kind && p.size == size) have++;
|
|
for (int k = have; k < PointCount(kind, size); k++)
|
|
if (SpawnPoint(kind, size, nowMs) == null) return; // карта переполнена -- попробуем при следующем исчезновении
|
|
}
|
|
}
|
|
|
|
int PointCount(int kind, int size)
|
|
{
|
|
var c = pointRules.Count;
|
|
return c != null && kind < c.Length && c[kind] != null && size < c[kind].Length ? Math.Max(0, c[kind][size]) : 0;
|
|
}
|
|
|
|
// Новая точка этого ресурса и размера в другом месте. Место -- из seed и номера появления (одинаково на сервере)
|
|
ResourcePoint SpawnPoint(int kind, int size, 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;
|
|
|
|
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;
|
|
SpawnMissingPoints(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 && m.gather)) // разведка не мешает
|
|
{
|
|
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;
|
|
pointsTopUp = true; // таблица могла измениться -- при следующем расчёте мира добавить недостающие
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|