Feature/3d hud #18
@@ -40,4 +40,19 @@ MonoBehaviour:
|
||||
- 12
|
||||
- 24
|
||||
- 48
|
||||
pointsPerResource: 2
|
||||
woodCount:
|
||||
- 4
|
||||
- 3
|
||||
- 1
|
||||
stoneCount:
|
||||
- 4
|
||||
- 2
|
||||
- 1
|
||||
metalCount:
|
||||
- 2
|
||||
- 2
|
||||
- 1
|
||||
goldCount:
|
||||
- 2
|
||||
- 1
|
||||
- 1
|
||||
|
||||
@@ -32,9 +32,12 @@ public class WorldResourceConfig : ScriptableObject
|
||||
[Header("Срок жизни точки, РЕАЛЬНЫХ часов с момента появления: малая / средняя / крупная")]
|
||||
public int[] lifetimeRealHours = { 12, 24, 48 };
|
||||
|
||||
[Header("Не задано регламентом -- ТЕСТ")]
|
||||
[Tooltip("Сколько точек каждого ресурса одновременно на карте")]
|
||||
public int pointsPerResource = 2;
|
||||
[Header("Сколько точек на карте: малых / средних / крупных (решения 31.10, п. 6, ТЕСТ)")]
|
||||
[Tooltip("Решения 31.10: всего 24 -- лес 8, камень 7, металл 5, золото 4; по размерам 12 / 8 / 4. Раскладка по ресурсам -- здесь")]
|
||||
public int[] woodCount = { 4, 3, 1 };
|
||||
public int[] stoneCount = { 4, 2, 1 };
|
||||
public int[] metalCount = { 2, 2, 1 };
|
||||
public int[] goldCount = { 2, 1, 1 };
|
||||
|
||||
static WorldResourceConfig cached;
|
||||
|
||||
@@ -59,12 +62,19 @@ public class WorldResourceConfig : ScriptableObject
|
||||
MiningMilliPerGameHour = new[] { Milli(woodPerGameHour), Milli(stonePerGameHour), Milli(metalPerGameHour), Milli(goldPerGameHour) },
|
||||
CargoPerSoldier = new[] { Mathf.Max(0, woodCargo), Mathf.Max(0, stoneCargo), Mathf.Max(0, metalCargo), Mathf.Max(0, goldCargo) },
|
||||
LifetimeRealHours = Three(lifetimeRealHours, d.LifetimeRealHours),
|
||||
PointsPerResource = Mathf.Max(0, pointsPerResource)
|
||||
Count = new[] { Counts(woodCount, d.Count[0]), Counts(stoneCount, d.Count[1]), Counts(metalCount, d.Count[2]), Counts(goldCount, d.Count[3]) }
|
||||
};
|
||||
}
|
||||
|
||||
static int Milli(float perHour) => Mathf.Max(0, Mathf.RoundToInt(perHour * 1000f));
|
||||
|
||||
// Число точек: ровно три значения, не меньше 0 (0 -- такого размера нет), иначе -- значения по умолчанию
|
||||
static int[] Counts(int[] values, int[] fallback)
|
||||
{
|
||||
if (values == null || values.Length != 3) return (int[])fallback.Clone();
|
||||
return new[] { Mathf.Max(0, values[0]), Mathf.Max(0, values[1]), Mathf.Max(0, values[2]) };
|
||||
}
|
||||
|
||||
// Ровно три значения (малая / средняя / крупная), иначе -- значения по умолчанию
|
||||
static int[] Three(int[] values, int[] fallback)
|
||||
{
|
||||
|
||||
@@ -788,7 +788,18 @@ public class ModuleTests
|
||||
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
||||
world.Advance(0, new List<WorldEvent>());
|
||||
var rules = world.PointRules;
|
||||
Assert.AreEqual(ResourcePointRules.Resources.Length * rules.PointsPerResource, world.ResourcePoints.Count);
|
||||
// Решения 31.10, п. 6: 24 точки -- лес 8, камень 7, металл 5, золото 4; размеры 12 / 8 / 4; точек еды нет
|
||||
Assert.AreEqual(24, world.ResourcePoints.Count);
|
||||
var byKind = new int[4];
|
||||
var bySize = new int[3];
|
||||
foreach (var p in world.ResourcePoints) { byKind[p.kind]++; bySize[p.size]++; }
|
||||
Assert.AreEqual(8, byKind[0], "лес");
|
||||
Assert.AreEqual(7, byKind[1], "камень");
|
||||
Assert.AreEqual(5, byKind[2], "металл");
|
||||
Assert.AreEqual(4, byKind[3], "золото");
|
||||
Assert.AreEqual(12, bySize[0], "малых");
|
||||
Assert.AreEqual(8, bySize[1], "средних");
|
||||
Assert.AreEqual(4, bySize[2], "крупных");
|
||||
foreach (var p in world.ResourcePoints)
|
||||
{
|
||||
Assert.IsTrue(p.active);
|
||||
@@ -848,9 +859,11 @@ public class ModuleTests
|
||||
Assert.AreEqual(stock, m.cargo, "унесут весь запас");
|
||||
world.Advance(m.gatherEndMs, new List<WorldEvent>());
|
||||
Assert.IsFalse(gold.active, "точка истощена");
|
||||
int activeGold = 0;
|
||||
foreach (var p in world.ResourcePoints) if (p.active && p.kind == 3) activeGold++;
|
||||
Assert.AreEqual(world.PointRules.PointsPerResource, activeGold, "новая точка золота появилась в другом месте");
|
||||
int activeGold = 0, sameSize = 0;
|
||||
foreach (var p in world.ResourcePoints)
|
||||
if (p.active && p.kind == 3) { activeGold++; if (p.size == gold.size) sameSize++; }
|
||||
Assert.AreEqual(4, activeGold, "новая точка золота появилась в другом месте");
|
||||
Assert.AreEqual(world.PointRules.Count[3][gold.size], sameSize, "того же размера -- раскладка размеров не плывёт");
|
||||
|
||||
string name = world.GetNode(gold.id).displayName;
|
||||
world.Advance(m.returnMs, new List<WorldEvent>());
|
||||
@@ -1567,4 +1580,23 @@ public class ModuleTests
|
||||
Assert.AreNotEqual(MapVisibility.Unexplored, intel.GetVisibility(px, py), $"точка пути {i}/20");
|
||||
}
|
||||
}
|
||||
|
||||
// Старое сохранение (до решений 31.10) с несколькими точками: недостающие до таблицы появляются, свои -- на месте
|
||||
[Test]
|
||||
public void World_ResourcePoints_OldSaveIsToppedUp()
|
||||
{
|
||||
var old = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
||||
old.Advance(0, new List<WorldEvent>());
|
||||
var state = old.GetPointsState();
|
||||
state.points.RemoveRange(2, state.points.Count - 2); // как было в старой игре -- мало точек
|
||||
string keep = state.points[0].id;
|
||||
|
||||
var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem());
|
||||
world.LoadPointsState(state);
|
||||
world.Advance(1000, new List<WorldEvent>());
|
||||
Assert.AreEqual(24, world.ResourcePoints.Count, "дополнено до таблицы");
|
||||
Assert.IsNotNull(world.GetPoint(keep), "старые точки остались");
|
||||
world.Advance(2000, new List<WorldEvent>());
|
||||
Assert.AreEqual(24, world.ResourcePoints.Count, "повторный расчёт ничего не добавляет");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,16 @@ namespace Bogatyri.World
|
||||
public long GameHourMs = 5 * 60 * 1000;
|
||||
|
||||
// Не задано регламентом -- ТЕСТ, решает команда:
|
||||
public int PointsPerResource = 2; // сколько точек каждого ресурса одновременно на карте
|
||||
// Сколько точек одновременно на карте: [ресурс][размер] (решения по 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();
|
||||
@@ -112,24 +121,44 @@ namespace Bogatyri.World
|
||||
return points.Find(p => p.id == id);
|
||||
}
|
||||
|
||||
// Первые точки: по PointsPerResource каждого ресурса, появляются в момент первого расчёта мира
|
||||
// Точки на карте по таблице Count. Первые -- в момент первого расчёта мира; после загрузки сохранения
|
||||
// недостающие добавляются (старое сохранение с 8 точками дополнится до 24, лишние доживут свой срок)
|
||||
void EnsureResourcePoints(long nowMs)
|
||||
{
|
||||
if (pointsInitialized) return;
|
||||
if (pointsInitialized && !pointsTopUp) return;
|
||||
pointsInitialized = true;
|
||||
for (int kind = 0; kind < ResourcePointRules.Resources.Length; kind++)
|
||||
for (int k = 0; k < pointRules.PointsPerResource; k++)
|
||||
SpawnPoint(kind, nowMs);
|
||||
pointsTopUp = false;
|
||||
SpawnMissingPoints(nowMs);
|
||||
}
|
||||
|
||||
// Новая точка того же ресурса в другом месте. Место и размер -- из seed и номера появления (одинаково на сервере)
|
||||
ResourcePoint SpawnPoint(int kind, long 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;
|
||||
|
||||
int size = rng.Range(0, 3); // размер равновероятно -- регламент не задаёт (ТЕСТ)
|
||||
var p = new ResourcePoint
|
||||
{
|
||||
id = "res_" + nextPointId++,
|
||||
@@ -162,13 +191,14 @@ namespace Bogatyri.World
|
||||
};
|
||||
}
|
||||
|
||||
// Точка истощена или исчезла: на её месте больше нечего собирать, новая появляется в другом месте
|
||||
// Точка истощена или исчезла: на её месте больше нечего собирать, новая того же ресурса и размера
|
||||
// появляется в другом месте (если таких точек теперь меньше, чем в таблице)
|
||||
void RetirePoint(ResourcePoint p, long atMs)
|
||||
{
|
||||
if (!p.active) return;
|
||||
p.active = false;
|
||||
p.occupiedBy = 0;
|
||||
SpawnPoint(p.kind, atMs);
|
||||
SpawnMissingPoints(atMs);
|
||||
}
|
||||
|
||||
// Исчезнувшие точки убираются с карты, когда к ним уже никто не идёт и с них никто не возвращается
|
||||
@@ -275,6 +305,7 @@ namespace Bogatyri.World
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -105,6 +105,7 @@ EditMode-тесты: `Assets/Scripts/Modules/Tests/ModuleTests.cs`. Запуск
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 6: 24 ресурсные точки. Обновлён World. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 5: разгром дальше 5:1 (92% / 95% / до 99%). Обновлён Battle. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 4: средняя скорость смешанного войска. Обновлён World. |
|
||||
| 01.11.2026 | Claude | Решения по 8 вопросам Саши (31.10), п. 1: гарнизоны и запасы точек видны только в зоне обзора. Обновлён Intelligence. |
|
||||
|
||||
@@ -108,7 +108,10 @@
|
||||
- **Точка исчезла до прихода** — возврат без добычи (окно «ТОЧКИ НЕТ»). Исчезла во время сбора — уносят, что успели.
|
||||
- **Истощённая или исчезнувшая точка** заменяется новой того же ресурса в другом месте (место и размер — из seed и номера появления, одинаково на сервере). Убирается с карты, когда к ней уже никто не идёт.
|
||||
- **На карте** — ромб цвета ресурса с подписью «запас, сколько осталось жить» или «идёт сбор». Окно отправки показывает запас, добычу и груз на воина, путь и прогноз «унесут N за T, весь поход ~X».
|
||||
- **Открыто (не задано регламентом, ТЕСТ):** сколько точек на карте (сейчас по 2 каждого ресурса), как выбирается размер (сейчас равновероятно), предел добычи на точку («скорость с пределом на точку»). Нет точек еды — в регламенте их нет.
|
||||
- **Сколько точек** (решения по 8 вопросам Саши от 31.10, п. 6, ТЕСТ тестового участка): **24** — лес 8, камень 7, металл 5, золото 4; по размерам 12 малых, 8 средних, 4 крупных. Раскладка по ресурсам (документ её не задаёт, задана так): лес 4/3/1, камень 4/2/1, металл 2/2/1, золото 2/1/1 — `WorldResourceConfig` (`woodCount`, `stoneCount`, `metalCount`, `goldCount`). Исчезла точка — появляется точка того же ресурса и размера в другом месте, раскладка не плывёт. Старое сохранение дополняется до таблицы при загрузке.
|
||||
- На одной точке собирает только одна экспедиция: пока к точке идёт или на ней стоит войско, другое туда не отправить.
|
||||
- Точек еды нет (еда — с ферм). Синие кристаллы — отдельная механика, не в этой сетке.
|
||||
- **Открыто:** предел добычи на точку («скорость с пределом на точку»).
|
||||
- **Позже:** нападение на занятую точку (бой в поле, победитель забирает до 50% накопленного) — когда появятся другие игроки; досрочный отзыв — технология «Связь Волхвов»; переполнение склада при возвращении (5 минут на разгрузку) — вместе с Экономикой.
|
||||
|
||||
**Панель армий** (регламент 31.10, раздел 7: «нужна всем игрокам»): кнопка «Армии» на карте мира, под «Рейтингом» — окно `WorldMap/ArmiesWindow.cs`. Вверху — дома свободно, гарнизон, походов N из M. По каждому войску игрока: куда и что делает (идёт в бой / идёт на сбор / собирает / возвращается), состав по типам, груз, сколько до цели или до конца сбора и когда будет дома. Таймеры обновляются сами, пока окно открыто. До отправки окно атаки показывает длительность всего задания: для боя «туда и обратно ~X», для сбора «весь поход ~X».
|
||||
@@ -144,6 +147,7 @@
|
||||
|
||||
| Дата | Кто | Что |
|
||||
| --- | --- | --- |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 6: 24 ресурсные точки (лес 8, камень 7, металл 5, золото 4; размеры 12 / 8 / 4), число по ресурсу и размеру — в `WorldResourceConfig`; новая точка — того же размера; старые сохранения дополняются. |
|
||||
| 01.11.2026 | Claude | Решения 31.10, п. 4: скорость смешанного войска — средняя по численности с модификаторами (вместо «по самому медленному»), без промежуточного округления. `MarchConfig`: `mixedUsesSlowest` убран, добавлены `typeSpeedBonusPercent`, `armySpeedBonusPercent`. |
|
||||
| 31.10.2026 | Claude | Для Разведки М1: `IWorldMap` (армии на карте в момент времени), `WorldAdvancingEvent`, патрули-заглушки соседей (`MarchData.patrol`, сохранение `patrols`, таблица `MarchConfig`). |
|
||||
| 31.10.2026 | Claude | Панель армий (`WorldMap/ArmiesWindow.cs`, кнопка «Армии» на карте мира): местонахождение и действие каждого войска, состав, груз, время до цели / конца сбора / возвращения. В окне атаки — длительность всего задания для боя. |
|
||||
|
||||
Reference in New Issue
Block a user