From 3a86eadc0b304aa5ab8e8a71da03085e60141e5e Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 1 Nov 2026 13:08:51 +0100 Subject: [PATCH] =?UTF-8?q?24=20=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=2031.10=20(?= =?UTF-8?q?=D0=BF.=206):=20=D0=BB=D0=B5=D1=81=208,=20=D0=BA=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=8C=207,=20=D0=BC=D0=B5=D1=82=D0=B0=D0=BB=D0=BB?= =?UTF-8?q?=205,=20=D0=B7=D0=BE=D0=BB=D0=BE=D1=82=D0=BE=204;=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D1=8B=2012/8/4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Число по ресурсу и размеру -- в WorldResourceConfig; новая точка -- того же размера; старые сохранения дополняются до таблицы. Co-Authored-By: Claude Opus 5.5 --- Assets/Resources/WorldResourceConfig.asset | 17 +++++- Assets/Scripts/App/WorldResourceConfig.cs | 18 +++++-- Assets/Scripts/Modules/Tests/ModuleTests.cs | 40 ++++++++++++-- .../Simulation/WorldSimulation.Points.cs | 53 +++++++++++++++---- Docs/Modules/README.md | 1 + Docs/Modules/World.md | 6 ++- 6 files changed, 114 insertions(+), 21 deletions(-) diff --git a/Assets/Resources/WorldResourceConfig.asset b/Assets/Resources/WorldResourceConfig.asset index 574f0b6..720061e 100644 --- a/Assets/Resources/WorldResourceConfig.asset +++ b/Assets/Resources/WorldResourceConfig.asset @@ -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 diff --git a/Assets/Scripts/App/WorldResourceConfig.cs b/Assets/Scripts/App/WorldResourceConfig.cs index 4c2a05e..f6befa2 100644 --- a/Assets/Scripts/App/WorldResourceConfig.cs +++ b/Assets/Scripts/App/WorldResourceConfig.cs @@ -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) { diff --git a/Assets/Scripts/Modules/Tests/ModuleTests.cs b/Assets/Scripts/Modules/Tests/ModuleTests.cs index 6070dde..ffaf4ae 100644 --- a/Assets/Scripts/Modules/Tests/ModuleTests.cs +++ b/Assets/Scripts/Modules/Tests/ModuleTests.cs @@ -788,7 +788,18 @@ public class ModuleTests var world = new WorldSimulation(WorldMapLayout.CreateDefault(), new BattleSystem()); world.Advance(0, new List()); 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()); 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()); @@ -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()); + 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()); + Assert.AreEqual(24, world.ResourcePoints.Count, "дополнено до таблицы"); + Assert.IsNotNull(world.GetPoint(keep), "старые точки остались"); + world.Advance(2000, new List()); + Assert.AreEqual(24, world.ResourcePoints.Count, "повторный расчёт ничего не добавляет"); + } } diff --git a/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Points.cs b/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Points.cs index 2cf9b91..f47cb16 100644 --- a/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Points.cs +++ b/Assets/Scripts/Modules/World/Simulation/WorldSimulation.Points.cs @@ -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) { diff --git a/Docs/Modules/README.md b/Docs/Modules/README.md index 6e2fdfd..b6c5a00 100644 --- a/Docs/Modules/README.md +++ b/Docs/Modules/README.md @@ -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. | diff --git a/Docs/Modules/World.md b/Docs/Modules/World.md index 027e058..7aaf48f 100644 --- a/Docs/Modules/World.md +++ b/Docs/Modules/World.md @@ -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`, кнопка «Армии» на карте мира): местонахождение и действие каждого войска, состав, груз, время до цели / конца сбора / возвращения. В окне атаки — длительность всего задания для боя. |