157 lines
8.6 KiB
C#
157 lines
8.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace BlockOutReplica.Gameplay
|
|
{
|
|
public enum BoosterType { None, Freeze, Rocket, Ufo }
|
|
public enum GameModal { None, Pause, Restart, Quit, OutOfTime, GiveUp, Failed, PreLevel, BoosterPurchase, Lives, Settings, Info }
|
|
|
|
[Serializable]
|
|
public sealed class BlockOutEconomy
|
|
{
|
|
public int refillPrice = 900, PlayOnPrice = 900, SecondPlayOnPrice = 1900, ThirdSecondPlayOnPrice = 2300;
|
|
public int refillDurationAsMinutes = 30, maxLifeAmount = 5, initialCoin = 1000;
|
|
public int InitialUfoAmount = 3, InitialRocketAmount = 3, InitialTimeFreezeAmount = 3;
|
|
public int UfoPrice = 1200, RocketPrice = 600, TimeFreezePrice = 300;
|
|
public int TimeFreezeDurationAsSeconds = 10, PlayOnExtraTimeSeconds = 30, RegularLevelWinCoinReward = 20;
|
|
public static BlockOutEconomy Load()
|
|
{
|
|
var asset = Resources.Load<TextAsset>("OriginalFlow/GameEconomyModel");
|
|
return asset != null ? JsonUtility.FromJson<BlockOutEconomy>(asset.text) : new BlockOutEconomy();
|
|
}
|
|
public int Price(BoosterType type) => type == BoosterType.Freeze ? TimeFreezePrice : type == BoosterType.Rocket ? RocketPrice : UfoPrice;
|
|
public int ContinuePrice(int used) => used == 0 ? PlayOnPrice : used == 1 ? SecondPlayOnPrice : ThirdSecondPlayOnPrice;
|
|
}
|
|
|
|
// One local save for wallet, inventory and settings. Tests can pass an in-memory
|
|
// JSON writer and clock; they never need to modify the user's PlayerPrefs.
|
|
public sealed class BlockOutPlayerState
|
|
{
|
|
private const string Key = "BlockOutReplica.PlayerState.v1";
|
|
[Serializable] private sealed class Data
|
|
{
|
|
public int coins, lives, freeze, rocket, ufo;
|
|
public bool sounds = true, music = true, haptics = true;
|
|
public long refillUtcTicks, infiniteLivesUntilUtcTicks;
|
|
public List<int> paidJourneyLevels = new List<int>();
|
|
}
|
|
private readonly BlockOutEconomy config;
|
|
private readonly Func<long> now;
|
|
private readonly Action<string> writer;
|
|
private Data data;
|
|
public int Coins => data.coins;
|
|
public int Lives { get { RefreshLives(); return data.lives; } }
|
|
public bool Sounds => data.sounds;
|
|
public bool Music => data.music;
|
|
public bool Haptics => data.haptics;
|
|
public int MaxLives => config.maxLifeAmount;
|
|
public double InfiniteLivesSeconds => Math.Max(0, (data.infiniteLivesUntilUtcTicks - now()) / (double)TimeSpan.TicksPerSecond);
|
|
public bool HasInfiniteLives => InfiniteLivesSeconds > 0;
|
|
public double SecondsUntilLife => data.lives >= MaxLives ? 0 : Math.Max(0,
|
|
(data.refillUtcTicks + TimeSpan.FromMinutes(config.refillDurationAsMinutes).Ticks - now()) / (double)TimeSpan.TicksPerSecond);
|
|
|
|
public BlockOutPlayerState(BlockOutEconomy config, string json = null, Action<string> save = null, Func<long> utcNow = null)
|
|
{
|
|
this.config = config;
|
|
now = utcNow ?? (() => DateTime.UtcNow.Ticks);
|
|
writer = save ?? (value => { PlayerPrefs.SetString(Key, value); PlayerPrefs.Save(); });
|
|
if (json == null && save == null) json = PlayerPrefs.GetString(Key, "");
|
|
try { if (!string.IsNullOrEmpty(json)) data = JsonUtility.FromJson<Data>(json); }
|
|
catch (ArgumentException) { data = null; }
|
|
if (data == null) data = new Data {coins = config.initialCoin, lives = MaxLives,
|
|
freeze = config.InitialTimeFreezeAmount, rocket = config.InitialRocketAmount, ufo = config.InitialUfoAmount,
|
|
refillUtcTicks = now()};
|
|
data.coins = Math.Max(0, data.coins); data.lives = Mathf.Clamp(data.lives, 0, MaxLives);
|
|
data.freeze = Math.Max(0, data.freeze); data.rocket = Math.Max(0, data.rocket); data.ufo = Math.Max(0, data.ufo);
|
|
if (data.refillUtcTicks <= 0 || data.refillUtcTicks > now()) data.refillUtcTicks = now();
|
|
if (data.paidJourneyLevels == null) data.paidJourneyLevels = new List<int>();
|
|
RefreshLives();
|
|
}
|
|
|
|
public string ToJson() => JsonUtility.ToJson(data);
|
|
private void Save() => writer(ToJson());
|
|
public int Amount(BoosterType type) => type == BoosterType.Freeze ? data.freeze : type == BoosterType.Rocket ? data.rocket : type == BoosterType.Ufo ? data.ufo : 0;
|
|
public bool SpendBooster(BoosterType type)
|
|
{
|
|
if (Amount(type) <= 0) return false;
|
|
if (type == BoosterType.Freeze) data.freeze--; else if (type == BoosterType.Rocket) data.rocket--; else data.ufo--;
|
|
Save(); return true;
|
|
}
|
|
public bool CanUseBooster(BoosterType type) => type>=BoosterType.Freeze&&type<=BoosterType.Ufo&&
|
|
(Amount(type)>0||data.coins>=config.Price(type));
|
|
|
|
// Native SpendBooster spends inventory first; at zero inventory it pays
|
|
// for this use directly. No temporary inventory grant or second save.
|
|
public bool SpendBoosterUse(BoosterType type)
|
|
{
|
|
if(!CanUseBooster(type))return false;
|
|
return Amount(type)>0?SpendBooster(type):SpendCoins(config.Price(type));
|
|
}
|
|
public bool BuyBooster(BoosterType type)
|
|
{
|
|
if (type == BoosterType.None || data.coins < config.Price(type)) return false;
|
|
data.coins -= config.Price(type);
|
|
if (type == BoosterType.Freeze) data.freeze++; else if (type == BoosterType.Rocket) data.rocket++; else data.ufo++;
|
|
Save(); return true;
|
|
}
|
|
public bool SpendCoins(int amount)
|
|
{
|
|
if (amount < 0 || data.coins < amount) return false;
|
|
data.coins -= amount; Save(); return true;
|
|
}
|
|
public void AwardCoins(int amount)
|
|
{ if (amount > 0) { data.coins = (int)Math.Min(int.MaxValue, (long)data.coins + amount); Save(); } }
|
|
public bool LoseLife()
|
|
{
|
|
RefreshLives();
|
|
if (HasInfiniteLives) return true;
|
|
if (data.lives <= 0) return false;
|
|
if (data.lives == MaxLives) data.refillUtcTicks = now();
|
|
data.lives--; Save(); return true;
|
|
}
|
|
public bool RefillLives()
|
|
{
|
|
RefreshLives();
|
|
if (data.lives == MaxLives || data.coins < config.refillPrice) return false;
|
|
data.coins -= config.refillPrice;
|
|
data.lives = MaxLives; data.refillUtcTicks = now(); Save(); return true;
|
|
}
|
|
public void RefreshLives()
|
|
{
|
|
long interval = TimeSpan.FromMinutes(Math.Max(1, config.refillDurationAsMinutes)).Ticks;
|
|
if (data.lives >= MaxLives) return;
|
|
long passed = Math.Max(0, now() - data.refillUtcTicks) / interval;
|
|
if (passed <= 0) return;
|
|
data.lives = (int)Math.Min(MaxLives, data.lives + passed);
|
|
data.refillUtcTicks = data.lives >= MaxLives ? now() : data.refillUtcTicks + passed * interval;
|
|
Save();
|
|
}
|
|
public void ToggleSounds() { data.sounds = !data.sounds; Save(); }
|
|
public void ToggleMusic() { data.music = !data.music; Save(); }
|
|
public void ToggleHaptics() { data.haptics = !data.haptics; Save(); }
|
|
|
|
// Wallet receipt and currency are persisted together. If the separate
|
|
// Journey ledger save fails, retry can repair it without paying twice.
|
|
public bool GrantJourneyOnce(int level, BlockOutJourneyReward[] rewards)
|
|
{
|
|
if (data.paidJourneyLevels.Contains(level)) return true;
|
|
if (rewards == null || Array.Exists(rewards, r => r==null || !r.IsSupported)) return false;
|
|
string before = ToJson();
|
|
foreach (var reward in rewards)
|
|
{
|
|
if (reward.IsCoins) data.coins = (int)Math.Min(int.MaxValue, (long)data.coins + Math.Max(0,reward.Amount));
|
|
else if(reward.IsTimedLives) data.infiniteLivesUntilUtcTicks = Math.Max(now(), data.infiniteLivesUntilUtcTicks)
|
|
+ TimeSpan.FromSeconds(reward.DurationSeconds).Ticks;
|
|
else if(reward.Booster==BoosterType.Freeze)data.freeze=(int)Math.Min(int.MaxValue,(long)data.freeze+Math.Max(0,reward.Amount));
|
|
else if(reward.Booster==BoosterType.Rocket)data.rocket=(int)Math.Min(int.MaxValue,(long)data.rocket+Math.Max(0,reward.Amount));
|
|
else if(reward.Booster==BoosterType.Ufo)data.ufo=(int)Math.Min(int.MaxValue,(long)data.ufo+Math.Max(0,reward.Amount));
|
|
}
|
|
data.paidJourneyLevels.Add(level);
|
|
try { Save(); }
|
|
catch { data = JsonUtility.FromJson<Data>(before); throw; }
|
|
return true;
|
|
}
|
|
}
|
|
}
|