Решения 04.11: снос убран (только перенос), кристаллы грабятся с технологией Грабёж, офлайн по временным меткам, защита от повторного начисления, тесты
This commit is contained in:
@@ -16,7 +16,7 @@ using UnityEngine.UI;
|
||||
// кнопка "Нанять войска" (панель "Войска").
|
||||
// - Стройка и улучшение идут по времени (BuildTimer, 01.11, ТЕСТ): полоска, сколько осталось, ускорение за кристаллы.
|
||||
// Пока здание строится, в окне только стройка; при улучшении здание работает на старом уровне.
|
||||
// - У построенных зданий (кроме Ратуши) -- кнопки "Перенести" (режим стройки) и "Снести" (часть цены возвращается).
|
||||
// - У построенных зданий (кроме Ратуши) -- кнопка "Перенести" (бесплатно, уровень и стройка сохраняются). Сноса нет (04.11).
|
||||
// Без здания (кнопка "Рабочие" в панели DEV и "Все рабочие" в окне) -- список рабочих по всем профессиям.
|
||||
// Окно -- только интерфейс: рабочих ставит модуль Населения, добычу считает модуль Экономики.
|
||||
// Пока окно открыто, цифры обновляются сами (раз в полсекунды).
|
||||
@@ -573,64 +573,19 @@ public static class BuildingWindow
|
||||
UIKit.SetButton(speed, $"Ускорить за {gems()} кристаллов", true);
|
||||
}
|
||||
|
||||
// ---------------- Перенос и снос ----------------
|
||||
// ---------------- Перенос (решения 04.11: сноса нет) ----------------
|
||||
// Здание можно бесплатно переставить в пределах поселения: уровень, стройка или улучшение сохраняются.
|
||||
// Снести здание и вернуть ресурсы нельзя -- такой кнопки нет.
|
||||
|
||||
static void ActionsBlock(RectTransform card, GameObject building)
|
||||
{
|
||||
var id = building.GetComponent<PlacedBuildingId>();
|
||||
if (id == null || string.IsNullOrEmpty(id.buildingName)) return; // Ратуша и стартовые постройки сцены
|
||||
var row = UIKit.Row(card, 80, 12);
|
||||
var move = UIKit.Button(row, "Перенести", UIKit.Theme.Blue, () =>
|
||||
UIKit.Button(card, "Перенести", UIKit.Theme.Blue, () =>
|
||||
{
|
||||
Close();
|
||||
if (BuildModeController.Instance != null) BuildModeController.Instance.StartMove(building);
|
||||
}, -1, 80, 26);
|
||||
UIKit.Size(move, -1, 80, 1);
|
||||
|
||||
float share = EconomyConfig.Load().construction.demolishRefund;
|
||||
var demolish = UIKit.Button(row, "Снести", UIKit.Theme.Red, () =>
|
||||
{
|
||||
UIKit.Confirm("Снести здание?",
|
||||
$"«{id.buildingName}» будет снесено. Вернётся {Mathf.RoundToInt(share * 100)}% цены постройки: {RefundText(id.buildingName, share)}.",
|
||||
"Снести", () => Demolish(building));
|
||||
}, -1, 80, 26);
|
||||
UIKit.Size(demolish, -1, 80, 1);
|
||||
}
|
||||
|
||||
static ResourceAmount[] Refund(string buildingName, float share)
|
||||
{
|
||||
var data = BuildManager.Instance != null ? BuildManager.Instance.FindData(buildingName) : null;
|
||||
if (data == null || data.cost == null) return new ResourceAmount[0];
|
||||
var result = new ResourceAmount[data.cost.Length];
|
||||
for (int i = 0; i < result.Length; i++) result[i] = new ResourceAmount(data.cost[i].type, Mathf.Floor(data.cost[i].amount * share));
|
||||
return result;
|
||||
}
|
||||
|
||||
static string RefundText(string buildingName, float share)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var a in Refund(buildingName, share)) if (a.Amount > 0) parts.Add($"{a.Amount:0} {GameData.ResName(a.Type)}");
|
||||
return parts.Count > 0 ? string.Join(", ", parts.ToArray()) : "ничего";
|
||||
}
|
||||
|
||||
static void Demolish(GameObject building)
|
||||
{
|
||||
if (building == null) return;
|
||||
var id = building.GetComponent<PlacedBuildingId>();
|
||||
if (id == null || string.IsNullOrEmpty(id.buildingName)) return;
|
||||
string name = id.buildingName;
|
||||
if (GameServices.TryGet(out IEconomy economy))
|
||||
foreach (var a in Refund(name, EconomyConfig.Load().construction.demolishRefund))
|
||||
if (a.Amount > 0) economy.Add(a.Type, a.Amount);
|
||||
|
||||
var timer = building.GetComponent<BuildTimer>();
|
||||
if (timer != null) timer.Discard(); // строитель свободен сразу
|
||||
id.buildingName = null; // сохранение и пересчёт города в этом кадре его уже не видят
|
||||
UnityEngine.Object.Destroy(building);
|
||||
Close();
|
||||
if (CitySync.Instance != null) CitySync.Instance.SyncNow();
|
||||
if (BuildGrid.Instance != null) BuildGrid.Instance.Refresh();
|
||||
UIKit.Toast($"Снесено: {name}");
|
||||
}
|
||||
|
||||
// Цена: чего хватает -- зелёным, чего нет -- красным, со сколько есть
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Стройка и офлайн (решения 04.11): стройка, которая закончилась, пока игра была закрыта, вступает в силу
|
||||
// в тот момент офлайн-расчёта, когда время дошло до её конца. Тогда добыча здания (и его новый уровень)
|
||||
// считается ровно с момента завершения, а не с начала офлайна и не с момента входа в игру.
|
||||
//
|
||||
// Как: модуль Населения на каждом шаге расчёта сообщает, до какого момента дошёл (GameTimeSimulatedEvent).
|
||||
// Здесь завершаются стройки с концом не позже этого момента, и город сразу пересчитывается (CitySync) --
|
||||
// следующий шаг добычи идёт уже с новым зданием. Пока расчёт не дошёл до конца стройки, BuildTimer сам её не
|
||||
// завершает (BuildTimer.MayComplete). Шаг расчёта -- 10 минут, поэтому добыча начинается не раньше конца
|
||||
// стройки и не позже чем через 10 минут после него: лишнего не начисляется никогда.
|
||||
public static class ConstructionClock
|
||||
{
|
||||
// До какого момента (мс) расчёт добычи уже дошёл. 0 -- ещё ни разу не считал (сразу после запуска)
|
||||
public static long SimulatedUntilMs { get; private set; }
|
||||
|
||||
static bool subscribed;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
SimulatedUntilMs = 0;
|
||||
BuildTimer.MayComplete = endMs => endMs <= SimulatedUntilMs;
|
||||
if (subscribed) return;
|
||||
subscribed = true;
|
||||
EventBus.Subscribe<GameTimeSimulatedEvent>(OnSimulated);
|
||||
}
|
||||
|
||||
static void OnSimulated(GameTimeSimulatedEvent e)
|
||||
{
|
||||
long ms = e.UnixNow * 1000;
|
||||
if (ms > SimulatedUntilMs) SimulatedUntilMs = ms; // время расчёта только растёт
|
||||
|
||||
bool any = false;
|
||||
foreach (var timer in BuildTimer.All.ToArray())
|
||||
{
|
||||
if (timer == null || timer.endMs > SimulatedUntilMs) continue;
|
||||
timer.Complete(); // уровень только растёт, второй раз не завершается
|
||||
any = true;
|
||||
}
|
||||
if (any && CitySync.Instance != null) CitySync.Instance.SyncNow();
|
||||
}
|
||||
}
|
||||
@@ -136,8 +136,7 @@ public class EconomyConfig : ScriptableObject
|
||||
public float townHallTimeMultiplier = 2f;
|
||||
[Tooltip("Ускорение: секунд стройки за 1 кристалл. ТЕСТ")]
|
||||
public int secondsPerGem = 30;
|
||||
[Tooltip("Снос здания возвращает такую долю цены постройки (0.5 -- половину). ТЕСТ")]
|
||||
[Range(0f, 1f)] public float demolishRefund = 0.5f;
|
||||
// Сноса зданий нет (решение 04.11): только бесплатный перенос
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -162,6 +161,8 @@ public class EconomyConfig : ScriptableObject
|
||||
public float baseFood = 100f;
|
||||
[Tooltip("+ к защите еды за каждый уровень Амбара. ТЕСТ")]
|
||||
public float perGranaryLevel = 300f;
|
||||
[Tooltip("Кристаллы: без технологии «Грабёж» нападающий не уносит ничего (решение 04.11). С ней -- не больше этой доли запаса. ТЕСТ, точные ограничения определят отдельно")]
|
||||
[Range(0f, 1f)] public float plunderGemsShare = 0.1f;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
@@ -292,7 +293,7 @@ public class EconomyConfig : ScriptableObject
|
||||
case ResourceType.Iron:
|
||||
case ResourceType.Gold: return p.baseGoods + p.perWarehouseLevel * warehouseLevels;
|
||||
case ResourceType.Food: return p.baseFood + p.perGranaryLevel * granaryLevels;
|
||||
case ResourceType.Gems: return float.PositiveInfinity; // кристаллы не грабятся
|
||||
case ResourceType.Gems: return float.PositiveInfinity; // без «Грабежа» кристаллы не грабятся (с ним -- ResourceProtection)
|
||||
default: return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ public static class GameModules
|
||||
|
||||
// Стройка и улучшение по времени, строители (01.11, ТЕСТ): числа -- EconomyConfig, раздел "Стройка"
|
||||
BuildTimer.Builders = Mathf.Max(1, config.construction.builders);
|
||||
ConstructionClock.Init(); // офлайн: стройка вступает в силу в момент своего завершения (решения 04.11)
|
||||
BuildTimer.DurationMs = (townHall, level) => EconomyConfig.Load().ConstructionMs(townHall, level);
|
||||
BuildTimer.Tapped -= OnBuildTimerTapped;
|
||||
BuildTimer.Tapped += OnBuildTimerTapped;
|
||||
|
||||
@@ -19,9 +19,15 @@ public sealed class ResourceProtection : IResourceProtection
|
||||
return EconomyConfig.Load().GetProtected(type, WarehouseLevels, GranaryLevels);
|
||||
}
|
||||
|
||||
public float Lootable(ResourceType type)
|
||||
public float Lootable(ResourceType type) => Lootable(type, false);
|
||||
|
||||
// Кристаллы: без «Грабежа» -- 0; с ним -- не больше доли запаса (EconomyConfig, ТЕСТ, точные ограничения -- позже)
|
||||
public float Lootable(ResourceType type, bool attackerHasPlunder)
|
||||
{
|
||||
if (!GameServices.TryGet(out IEconomy economy)) return 0f;
|
||||
return Mathf.Max(0f, economy.Get(type) - Protected(type));
|
||||
float have = Mathf.Max(0f, economy.Get(type));
|
||||
if (type == ResourceType.Gems)
|
||||
return attackerHasPlunder ? Mathf.Floor(have * Mathf.Clamp01(EconomyConfig.Load().protection.plunderGemsShare)) : 0f;
|
||||
return Mathf.Max(0f, have - Protected(type));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,10 @@ public class BuildModeController : MonoBehaviour
|
||||
if (moving != null)
|
||||
{
|
||||
var building = moving;
|
||||
var id = building != null ? building.GetComponent<PlacedBuildingId>() : null;
|
||||
if (id == null || string.IsNullOrEmpty(id.buildingName)) { CancelGhost(); return; } // здания уже нет
|
||||
// Перенос бесплатный: меняется только место. Уровень, стройка и улучшение остаются у того же здания,
|
||||
// ресурсы не тратятся и не возвращаются (решения 04.11)
|
||||
building.transform.position = ghost.PrefabPosition;
|
||||
CancelGhost(); // снимает "игнор" клеток -- здание уже на новом месте
|
||||
UIKit.Toast("Перенесено: " + d.buildingName);
|
||||
|
||||
@@ -25,10 +25,13 @@ public interface IEconomy
|
||||
// Защита ресурсов от грабежа (01.11, ТЕСТ). Сколько ресурса нельзя отнять в набеге: часть запаса прячут
|
||||
// Склады и Амбары. Добыча набега по ресурсу = Lootable. Реализует слой приложения (App/ResourceProtection)
|
||||
// по зданиям города; модули Боя и Мира берут отсюда, сколько можно унести у города.
|
||||
// Кристаллы (решение 04.11): недоступны нападающему, пока он не изучил технологию «Грабёж».
|
||||
// Технологий в игре ещё нет -- изучена ли она, передаёт тот, кто считает набег (attackerHasPlunder).
|
||||
public interface IResourceProtection
|
||||
{
|
||||
float Protected(ResourceType type); // сколько спрятано (кристаллы -- всё)
|
||||
float Lootable(ResourceType type); // сколько можно унести: max(0, запас - спрятано)
|
||||
float Protected(ResourceType type); // сколько спрятано от нападающего без «Грабежа»
|
||||
float Lootable(ResourceType type); // сколько можно унести без «Грабежа» (кристаллы -- 0)
|
||||
float Lootable(ResourceType type, bool attackerHasPlunder); // с учётом технологии нападающего
|
||||
}
|
||||
|
||||
// Добыча через рабочих (этап 3). Реализует модуль Экономики (ProductionService).
|
||||
|
||||
@@ -32,6 +32,15 @@ public struct GameTimeAdvancedEvent
|
||||
public long UnixNow;
|
||||
}
|
||||
|
||||
// Расчёт игры (модуль Населения: добыча, еда, прирост) дошёл до момента UnixNow (04.11).
|
||||
// Приходит в начале каждого шага расчёта (раз в 10 игровых минут офлайна) и в конце каждого Tick.
|
||||
// Всё, что должно случиться по времени (стройка закончилась), применяется здесь -- тогда следующий шаг
|
||||
// уже считается с новым городом, и добыча идёт ровно с момента завершения, а не с начала офлайна.
|
||||
public struct GameTimeSimulatedEvent
|
||||
{
|
||||
public long UnixNow;
|
||||
}
|
||||
|
||||
// ---------------- Население ----------------
|
||||
// Число людей изменилось (прирост, загрузка, сброс, изменение войска)
|
||||
public struct PopulationChangedEvent
|
||||
|
||||
@@ -221,17 +221,18 @@ public sealed class PopulationService : IPopulation, ISaveParticipant
|
||||
{
|
||||
lastTickUnix = unixNow; // первый расчёт: отсюда и считаем
|
||||
Changed();
|
||||
SimulatedUntil(unixNow);
|
||||
return;
|
||||
}
|
||||
|
||||
long elapsed = unixNow - lastTickUnix;
|
||||
if (elapsed < 0)
|
||||
{
|
||||
// Часы ушли назад (сброс отладочной перемотки, перевод часов на телефоне) -- просто продолжаем с "сейчас"
|
||||
lastTickUnix = unixNow;
|
||||
// Часы ушли назад (сброс отладочной перемотки, перевод часов на телефоне). Метку НЕ двигаем назад:
|
||||
// это время уже посчитано, и когда часы снова дойдут до него, оно не начислится второй раз (04.11).
|
||||
return;
|
||||
}
|
||||
if (elapsed == 0) return;
|
||||
if (elapsed == 0) { SimulatedUntil(unixNow); return; }
|
||||
|
||||
long max = (long)rules.MaxOfflineHours * SecondsPerHour;
|
||||
if (elapsed > max) elapsed = max;
|
||||
@@ -239,6 +240,13 @@ public sealed class PopulationService : IPopulation, ISaveParticipant
|
||||
Simulate(unixNow - elapsed, elapsed);
|
||||
lastTickUnix = unixNow;
|
||||
PruneFamine(unixNow);
|
||||
SimulatedUntil(unixNow);
|
||||
}
|
||||
|
||||
// Расчёт дошёл до момента: то, что случается по времени (стройка закончилась), применяется сейчас (GameTimeSimulatedEvent)
|
||||
static void SimulatedUntil(long unixNow)
|
||||
{
|
||||
EventBus.Publish(new GameTimeSimulatedEvent { UnixNow = unixNow });
|
||||
}
|
||||
|
||||
// ---------------- Расчёт ----------------
|
||||
@@ -260,6 +268,10 @@ public sealed class PopulationService : IPopulation, ISaveParticipant
|
||||
remaining -= step;
|
||||
double hours = step / (double)SecondsPerHour;
|
||||
|
||||
// Расчёт дошёл до начала шага: стройки, закончившиеся к этому моменту, вступают в силу,
|
||||
// и добыча этого шага считается уже с ними (а не с начала офлайна)
|
||||
SimulatedUntil(stepStart);
|
||||
|
||||
// 0. Добыча: рабочие приносят ресурсы (в том числе еду) -- до того, как город поест
|
||||
production?.Produce(hours);
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ using UnityEngine;
|
||||
// (не добывает, не даёт жильё и места, Казармы не учат). Тап по нему -- окно стройки.
|
||||
// - Улучшение (Kind.Upgrade): здание работает на старом уровне, новый уровень -- когда время выйдет.
|
||||
// - Одновременно идёт не больше Builders строек (строители). Ратуша -- тоже стройка.
|
||||
// - Время -- часы игры (GameClock.UnixNowMs): идёт и пока игра закрыта, и при перемотке DEV.
|
||||
// - Время -- реальное (GameClock.UnixNowMs): идёт и пока игра закрыта, и при перемотке DEV.
|
||||
// - Офлайн (04.11): метка конца хранится в сохранении; после входа стройка завершается в тот момент расчёта
|
||||
// добычи, когда время дошло до её конца (App/ConstructionClock) -- добыча здания идёт с момента завершения.
|
||||
// - Сохраняет модуль Поселения вместе со зданием (startMs, endMs, вид, целевой уровень).
|
||||
// Сколько длится стройка и сколько строителей -- задаёт слой приложения (EconomyConfig): модуль чисел не знает.
|
||||
public class BuildTimer : MonoBehaviour
|
||||
@@ -19,6 +21,11 @@ public class BuildTimer : MonoBehaviour
|
||||
|
||||
// Задаёт слой приложения (App/GameModules) из EconomyConfig
|
||||
public static int Builders = 2;
|
||||
// Можно ли завершить стройку, которая кончилась в этот момент (мс). Слой приложения отвечает "да", только когда
|
||||
// расчёт добычи уже дошёл до этого момента (GameTimeSimulatedEvent): иначе после офлайна здание считалось бы
|
||||
// готовым с самого начала офлайна. null -- завершать сразу.
|
||||
public static Func<long, bool> MayComplete;
|
||||
|
||||
// Длительность в мс: (это Ратуша?, до какого уровня; 1 -- постройка). 0 -- мгновенно
|
||||
public static Func<bool, int, long> DurationMs = (townHall, level) => 0;
|
||||
public static int Busy => All.Count;
|
||||
@@ -81,13 +88,14 @@ public class BuildTimer : MonoBehaviour
|
||||
var townHall = GetComponent<TownHall>();
|
||||
if (townHall != null)
|
||||
{
|
||||
townHall.currentLevel = Mathf.Min(townHall.maxLevel, targetLevel);
|
||||
// уровень только растёт: повторное или устаревшее завершение не понизит и не "докупит" уровни
|
||||
townHall.currentLevel = Mathf.Min(townHall.maxLevel, Mathf.Max(townHall.currentLevel, targetLevel));
|
||||
EventBus.Publish(new TownHallLevelChangedEvent { Level = townHall.currentLevel });
|
||||
}
|
||||
else
|
||||
{
|
||||
var leveled = GetComponent<ILeveledBuilding>();
|
||||
if (leveled != null) leveled.Level = targetLevel;
|
||||
if (leveled != null) leveled.Level = Mathf.Max(leveled.Level, targetLevel);
|
||||
}
|
||||
}
|
||||
Completed?.Invoke(this);
|
||||
@@ -122,7 +130,7 @@ public class BuildTimer : MonoBehaviour
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!finished && GameClock.UnixNowMs >= endMs) Complete();
|
||||
if (!finished && GameClock.UnixNowMs >= endMs && (MayComplete == null || MayComplete(endMs))) Complete();
|
||||
}
|
||||
|
||||
// ---------------- Здание, которое строится, не работает ----------------
|
||||
|
||||
@@ -104,7 +104,8 @@ public static class SettlementModule
|
||||
TownHall.Instance.currentLevel = cached.townHallLevel;
|
||||
var oldTimer = BuildTimer.Of(TownHall.Instance.gameObject);
|
||||
if (oldTimer != null) oldTimer.Cancel();
|
||||
if (cached.townHallTimerEndMs > 0)
|
||||
// Проверка таймера из файла: улучшение до уровня выше текущего, конец не раньше начала
|
||||
if (cached.townHallTimerEndMs > 0 && cached.townHallTimerTargetLevel > cached.townHallLevel)
|
||||
BuildTimer.Begin(TownHall.Instance.gameObject, BuildTimer.Kind.Upgrade, cached.townHallTimerTargetLevel,
|
||||
cached.townHallTimerStartMs, cached.townHallTimerEndMs);
|
||||
}
|
||||
@@ -132,7 +133,10 @@ public static class SettlementModule
|
||||
if (leveled != null) leveled.Level = entry.level;
|
||||
|
||||
// Стройка или улучшение продолжается (если время уже вышло -- закончится в первом же кадре)
|
||||
if (entry.timerKind == (int)BuildTimer.Kind.Build || entry.timerKind == (int)BuildTimer.Kind.Upgrade)
|
||||
// Проверка таймера из файла: постройка -- только для 1 уровня, улучшение -- только до уровня выше текущего
|
||||
bool validBuild = entry.timerKind == (int)BuildTimer.Kind.Build && entry.level <= 1;
|
||||
bool validUpgrade = entry.timerKind == (int)BuildTimer.Kind.Upgrade && entry.timerTargetLevel > Mathf.Max(1, entry.level);
|
||||
if ((validBuild || validUpgrade) && entry.timerEndMs > 0)
|
||||
BuildTimer.Begin(built, (BuildTimer.Kind)entry.timerKind, Mathf.Max(1, entry.timerTargetLevel),
|
||||
entry.timerStartMs, entry.timerEndMs);
|
||||
|
||||
|
||||
@@ -456,7 +456,7 @@ public class PopulationTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClockGoesBack_NothingHappens_ThenContinues()
|
||||
public void ClockGoesBack_NothingHappens_CountedTimeNotCountedAgain()
|
||||
{
|
||||
economy.Set(ResourceType.Food, 1000);
|
||||
var p = StartCity();
|
||||
@@ -465,8 +465,12 @@ public class PopulationTests
|
||||
Assert.AreEqual(1000, economy.Get(ResourceType.Food), 0.0001, "время назад -- ничего не съедено");
|
||||
Assert.AreEqual(300, p.Total);
|
||||
|
||||
// Решения 04.11: метка последнего расчёта не уходит назад -- уже посчитанное время второй раз не считается
|
||||
p.Tick(T0 - 5000 + Hour);
|
||||
Assert.AreEqual(1000 - 33, economy.Get(ResourceType.Food), 0.01);
|
||||
Assert.AreEqual(1000, economy.Get(ResourceType.Food), 0.0001, "часы ещё не дошли до уже посчитанного -- ничего");
|
||||
|
||||
p.Tick(T0 + Hour);
|
||||
Assert.AreEqual(1000 - 33, economy.Get(ResourceType.Food), 0.01, "дальше метки -- ровно один час");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -245,4 +245,96 @@ public class ProductionTests
|
||||
Assert.AreEqual(40, copy.Slots(WorkJob.Stonecutter));
|
||||
Assert.AreEqual(1.1f, copy.Efficiency(WorkJob.Stonecutter), 0.0001f);
|
||||
}
|
||||
|
||||
// ---------------- Офлайн по временным меткам (решения 04.11) ----------------
|
||||
// Стройка, закончившаяся в офлайне, вступает в силу в момент расчёта, когда время дошло до её конца
|
||||
// (GameTimeSimulatedEvent). В игре это делает App/ConstructionClock; здесь -- подписка в тесте.
|
||||
|
||||
void CompleteUpgradeAt(long atUnix, List<KeyValuePair<WorkJob, int>> after)
|
||||
{
|
||||
bool done = false;
|
||||
EventBus.Subscribe<GameTimeSimulatedEvent>(e =>
|
||||
{
|
||||
if (done || e.UnixNow < atUnix) return;
|
||||
done = true;
|
||||
production.SetBuildings(after);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Offline_UpgradeFinishedMidway_ProducesNewRateOnlyFromCompletion()
|
||||
{
|
||||
economy.Set(ResourceType.Food, 100000);
|
||||
economy.Set(ResourceType.Wood, 0);
|
||||
production.SetBuildings(Buildings((WorkJob.Woodcutter, 1)));
|
||||
population.SetWorkers(WorkJob.Woodcutter, 30); // 60 дерева в час, на 2 уровне -- 66
|
||||
CompleteUpgradeAt(T0 + 2 * Hour, Buildings((WorkJob.Woodcutter, 2)));
|
||||
|
||||
population.Tick(T0 + 4 * Hour); // 4 часа офлайна, улучшение закончилось через 2 часа
|
||||
|
||||
Assert.AreEqual(2 * 60 + 2 * 66, economy.Get(ResourceType.Wood), 0.01f,
|
||||
"2 часа по 60 + 2 часа по 66, а не все 4 часа по новой норме");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Offline_FinishedInsideStep_CountsFromNextStep_NeverEarlier()
|
||||
{
|
||||
economy.Set(ResourceType.Food, 100000);
|
||||
economy.Set(ResourceType.Wood, 0);
|
||||
production.SetBuildings(Buildings((WorkJob.Woodcutter, 1)));
|
||||
population.SetWorkers(WorkJob.Woodcutter, 30);
|
||||
CompleteUpgradeAt(T0 + 2 * Hour + 5 * 60, Buildings((WorkJob.Woodcutter, 2))); // посреди 10-минутного шага
|
||||
|
||||
population.Tick(T0 + 4 * Hour);
|
||||
|
||||
float wood = economy.Get(ResourceType.Wood);
|
||||
Assert.LessOrEqual(wood, 125 / 60f * 60 + 115 / 60f * 66 + 0.01f, "не больше, чем если бы новая норма шла ровно с конца стройки");
|
||||
Assert.AreEqual(130 / 60f * 60 + 110 / 60f * 66, wood, 0.01f, "с ближайшего шага расчёта после конца стройки");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tick_SameMomentTwice_NoExtraResources()
|
||||
{
|
||||
economy.Set(ResourceType.Food, 100000);
|
||||
production.SetBuildings(Buildings((WorkJob.Woodcutter, 1)));
|
||||
population.SetWorkers(WorkJob.Woodcutter, 30);
|
||||
population.Tick(T0 + Hour);
|
||||
float wood = economy.Get(ResourceType.Wood);
|
||||
|
||||
population.Tick(T0 + Hour);
|
||||
population.Tick(T0 + Hour - 60); // часы ушли назад -- ничего не начисляется
|
||||
population.Tick(T0 + Hour);
|
||||
Assert.AreEqual(wood, economy.Get(ResourceType.Wood), 0.001f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Restore_SameMoment_NoSecondCredit()
|
||||
{
|
||||
economy.Set(ResourceType.Food, 100000);
|
||||
production.SetBuildings(Buildings((WorkJob.Woodcutter, 1)));
|
||||
population.SetWorkers(WorkJob.Woodcutter, 30);
|
||||
population.Tick(T0 + Hour);
|
||||
float wood = economy.Get(ResourceType.Wood);
|
||||
|
||||
var copy = new PopulationService(new PopulationRules());
|
||||
copy.Restore(population.Capture()); // как загрузка сохранения: метка последнего расчёта та же
|
||||
GameServices.Register<IPopulation>(copy);
|
||||
copy.Tick(T0 + Hour);
|
||||
Assert.AreEqual(wood, economy.Get(ResourceType.Wood), 0.001f, "загрузка не начисляет то же время второй раз");
|
||||
|
||||
copy.Tick(T0 + 2 * Hour);
|
||||
Assert.AreEqual(wood + 60, economy.Get(ResourceType.Wood), 0.01f, "а следующий час -- ровно один раз");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SimulatedEvents_GoForwardStepByStep_EndAtNow()
|
||||
{
|
||||
var times = new List<long>();
|
||||
EventBus.Subscribe<GameTimeSimulatedEvent>(e => times.Add(e.UnixNow));
|
||||
population.Tick(T0 + Hour);
|
||||
|
||||
Assert.AreEqual(T0 + Hour, times[times.Count - 1], "последнее событие -- момент Tick");
|
||||
for (int i = 1; i < times.Count; i++) Assert.GreaterOrEqual(times[i], times[i - 1], "время расчёта не идёт назад");
|
||||
Assert.AreEqual(7, times.Count, "6 шагов по 10 минут + конец");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user