2026-11-03 08:55:31 +08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
|
|
namespace Decentralized.Domain
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 三种稳定币的价格与状态:CRC(上层,托管银行出事时脱锚、可能减记),CRT(黑市,镇压或火并时脱锚,
|
|
|
|
|
/// 镇压期间按黑市声望出卖客户),CRA(革命军,质押付息,补贴池告急或外生触发时进入三天死亡螺旋)。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class Stablecoins
|
|
|
|
|
{
|
|
|
|
|
public double Crc = 1, Crt = 1, Cra = 1;
|
|
|
|
|
public int CrcDepegEnd = -1, CrtDepegEnd = -1;
|
|
|
|
|
public bool CrcHaircutPending;
|
|
|
|
|
public double CraSubsidy = Tuning.CraSubsidy0;
|
|
|
|
|
public int CraWarnDay = -1, CraSpiralDay = -1;
|
|
|
|
|
public double RevPower = 50;
|
2026-11-04 00:06:16 +08:00
|
|
|
/// <summary>平衡代币 SPARK 的价格(信用点);SparkPre 是螺旋开始前一刻的价格,螺旋三天按它的倍数走。</summary>
|
|
|
|
|
public double Spark = Tuning.SparkStart, SparkPre = -1;
|
2026-11-03 08:55:31 +08:00
|
|
|
public List<double> RevHistory = new List<double>();
|
|
|
|
|
|
|
|
|
|
public double Price(string asset)
|
|
|
|
|
{
|
|
|
|
|
switch (asset)
|
|
|
|
|
{
|
|
|
|
|
case Assets.CRC: return Crc;
|
|
|
|
|
case Assets.CRT: return Crt;
|
|
|
|
|
case Assets.CRA: return Cra;
|
2026-11-04 00:06:16 +08:00
|
|
|
case Assets.SPARK: return Spark;
|
2026-11-03 08:55:31 +08:00
|
|
|
default: return 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>减记发生时返回 CRC 持有量要乘的系数(其余情况为 1)。</summary>
|
|
|
|
|
public double OnEventStart(MarketEvent ev, int day, Rng rng)
|
|
|
|
|
{
|
2026-11-04 09:04:50 +08:00
|
|
|
if (ev == MarketEvent.RevRaidUpper && CrcDepegEnd < 0 && rng.Chance(Tuning.CrcDepegChance * StoryMods.Current.CrcDepeg))
|
2026-11-03 08:55:31 +08:00
|
|
|
{
|
|
|
|
|
Crc = rng.Range(Tuning.CrcDepegMin, Tuning.CrcDepegMax);
|
|
|
|
|
CrcDepegEnd = day + rng.Range(Tuning.CrcDepegDaysMin, Tuning.CrcDepegDaysMax);
|
2026-11-04 09:04:50 +08:00
|
|
|
CrcHaircutPending = rng.Chance(Math.Min(1, Tuning.CrcHaircutChance * StoryMods.Current.CrcHaircut));
|
2026-11-03 08:55:31 +08:00
|
|
|
}
|
2026-11-04 09:04:50 +08:00
|
|
|
if ((ev == MarketEvent.GangWar || ev == MarketEvent.Crackdown) && CrtDepegEnd < 0 && rng.Chance(Tuning.CrtDepegChance * StoryMods.Current.CrtDepeg))
|
2026-11-03 08:55:31 +08:00
|
|
|
{
|
|
|
|
|
Crt = rng.Range(Tuning.CrtDepegMin, Tuning.CrtDepegMax);
|
|
|
|
|
CrtDepegEnd = day + rng.Range(Tuning.CrtDepegDaysMin, Tuning.CrtDepegDaysMax);
|
|
|
|
|
}
|
2026-11-04 00:06:16 +08:00
|
|
|
if (ev == MarketEvent.RevRaidUpper) Spark *= Tuning.SparkRaidShock;
|
|
|
|
|
if (ev == MarketEvent.LowerUnrest) Spark *= Tuning.SparkUnrestShock;
|
2026-11-03 08:55:31 +08:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>镇压开始时,玩家持有的 CRT 被冻结一半的概率:黑市声望 0 时 40%,50 以上为 0。</summary>
|
|
|
|
|
public static double CrtFreezeChance(double blackMarketRep) =>
|
2026-11-04 09:04:50 +08:00
|
|
|
Math.Max(0, Tuning.CrtFreezeBase - blackMarketRep * Tuning.CrtFreezePerRep) * StoryMods.Current.CrtFreeze;
|
2026-11-03 08:55:31 +08:00
|
|
|
|
2026-11-03 16:51:12 +08:00
|
|
|
/// <summary>镇压开始时是否冻结玩家的 CRT:按黑市声望,声望越低越容易被黑市"出卖"。</summary>
|
|
|
|
|
public static bool CrtFrozenByCrackdown(double blackMarketRep, Rng rng) => rng.Chance(CrtFreezeChance(blackMarketRep));
|
|
|
|
|
|
2026-11-04 00:06:16 +08:00
|
|
|
/// <summary>铸造:用 CRA 按当前价换 SPARK,收手续费。余额不足返回 -1。</summary>
|
|
|
|
|
public double Mint(Wallet w, double cra)
|
|
|
|
|
{
|
|
|
|
|
if (cra <= 0 || Spark <= 0 || w.Avail(Assets.CRA) + 1e-9 < cra) return -1;
|
|
|
|
|
double got = cra * Cra / Spark * (1 - Tuning.SparkFee);
|
|
|
|
|
w.Add(Assets.CRA, -cra); w.Add(Assets.SPARK, got);
|
|
|
|
|
w.RecordBuy(Assets.SPARK, got, cra * Cra);
|
|
|
|
|
return got;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>销毁:用 SPARK 按当前价换 CRA,收手续费。余额不足返回 -1。</summary>
|
|
|
|
|
public double Burn(Wallet w, double spark)
|
|
|
|
|
{
|
|
|
|
|
if (spark <= 0 || Cra <= 0 || w.Avail(Assets.SPARK) + 1e-9 < spark) return -1;
|
|
|
|
|
double got = spark * Spark / Cra * (1 - Tuning.SparkFee);
|
|
|
|
|
w.Add(Assets.SPARK, -spark); w.Add(Assets.CRA, got);
|
|
|
|
|
w.RecordBuy(Assets.CRA, got, spark * Spark);
|
|
|
|
|
return got;
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-03 08:55:31 +08:00
|
|
|
public bool CraSubsidyLow => CraSubsidy <= Tuning.CraSubsidy0 * Tuning.CraSubsidyWarn;
|
|
|
|
|
|
|
|
|
|
/// <summary>CRA 的预兆:外生螺旋的新闻已出,或补贴池告急(利率下调)。螺旋开始后不再算预兆。</summary>
|
|
|
|
|
public bool CraWarning(int day) => (CraWarnDay >= 0 && CraWarnDay <= day || CraSubsidyLow) && Cra >= 0.97;
|
|
|
|
|
|
|
|
|
|
public double CraRate => CraSubsidyLow ? Tuning.CraRateLow : Tuning.CraRate;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 夜间结算。返回 CRC 持有量要乘的系数(减记兑现时为 0.95,否则为 1)。
|
|
|
|
|
/// playerStaked:玩家的 CRA 质押额,用来算补贴支出(玩家的利息由调用方按 <see cref="CraRate"/> 发放)。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public double Night(int day, Rng rng, double playerStaked)
|
|
|
|
|
{
|
|
|
|
|
double crcFactor = 1;
|
|
|
|
|
if (CrcDepegEnd == day)
|
|
|
|
|
{
|
|
|
|
|
Crc = 1;
|
|
|
|
|
if (CrcHaircutPending) crcFactor = Tuning.CrcHaircut;
|
|
|
|
|
CrcDepegEnd = -1;
|
|
|
|
|
CrcHaircutPending = false;
|
|
|
|
|
}
|
|
|
|
|
if (CrtDepegEnd == day)
|
|
|
|
|
{
|
|
|
|
|
Crt = 1;
|
|
|
|
|
CrtDepegEnd = -1;
|
|
|
|
|
}
|
|
|
|
|
RevHistory.Add(RevPower);
|
|
|
|
|
if (RevHistory.Count > 7) RevHistory.RemoveAt(0);
|
|
|
|
|
RevPower = Math.Max(0, Math.Min(100, RevPower + rng.Gauss() * 3));
|
|
|
|
|
|
|
|
|
|
if (CraSpiralDay < 0)
|
|
|
|
|
{
|
|
|
|
|
CraSubsidy -= (Tuning.CraNpcStake + playerStaked) * CraRate;
|
|
|
|
|
bool revFalling = RevHistory.Count == 7 && RevPower < RevHistory[0] - 5;
|
2026-11-04 09:04:50 +08:00
|
|
|
double weekly = Tuning.CraSpiralWeekly * (revFalling ? 3 : 1) * StoryMods.Current.CraSpiral(day);
|
2026-11-03 08:55:31 +08:00
|
|
|
double daily = 1 - Math.Pow(1 - weekly, 1.0 / 7);
|
|
|
|
|
if (CraWarnDay < 0 && (rng.Chance(daily) || CraSubsidy <= 0))
|
|
|
|
|
{
|
|
|
|
|
CraWarnDay = day;
|
|
|
|
|
CraSpiralDay = day + rng.Range(Tuning.CraWarnLeadMin, Tuning.CraWarnLeadMax);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (CraSpiralDay > 0 && day >= CraSpiralDay)
|
2026-11-04 00:06:16 +08:00
|
|
|
{
|
|
|
|
|
int k = Math.Min(day - CraSpiralDay, Tuning.CraSpiralPath.Length - 1);
|
|
|
|
|
Cra = Tuning.CraSpiralPath[k];
|
|
|
|
|
if (SparkPre < 0) SparkPre = Spark;
|
|
|
|
|
Spark = SparkPre * Tuning.SparkSpiralPath[Math.Min(day - CraSpiralDay, Tuning.SparkSpiralPath.Length - 1)];
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-11-04 09:04:50 +08:00
|
|
|
Spark *= Math.Exp(rng.Gauss() * Tuning.SparkVol) * StoryMods.Current.TakeSparkPush();
|
2026-11-04 00:06:16 +08:00
|
|
|
Spark += (Tuning.SparkStart - Spark) * Tuning.SparkRevert;
|
|
|
|
|
Spark = Math.Max(Tuning.SparkStart * Tuning.SparkMinMult, Math.Min(Tuning.SparkStart * Tuning.SparkMaxMult, Spark));
|
|
|
|
|
}
|
2026-11-03 08:55:31 +08:00
|
|
|
return crcFactor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|