Feature/3d hud #18

Merged
Alexander merged 59 commits from feature/3d-hud into develop 2026-11-06 18:09:33 +03:00
6 changed files with 114 additions and 21 deletions
Showing only changes of commit 3a86eadc0b - Show all commits
+16 -1
View File
@@ -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
+14 -4
View File
@@ -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)
{
+36 -4
View File
@@ -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)
{
+1
View File
@@ -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. |
+5 -1
View File
@@ -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`, кнопка «Армии» на карте мира): местонахождение и действие каждого войска, состав, груз, время до цели / конца сбора / возвращения. В окне атаки — длительность всего задания для боя. |