14 Commits
Author SHA1 Message Date
ZHU MINCHAOandClaude Sonnet 5.5 45f73580a5 Changelog: phase 1 complete
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:45:50 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e95588a76f Terminal: stablecoin price/status table in Web3; AML preview (window total, taint share, review chance) in the bank withdrawal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:44:49 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 00f7811e7c Terminal: two-column pages with table cells (holdings on the left, trading on the right)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:41:42 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 a2dc1dd2f6 Terminal: buy-average price per coin in Poinbase and Web3; drop the price line on the Poinbase card
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:35:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9441d4d1bd Terminal: home with three entries (Bank, Poinbase, Web3) plus report; entry icons
Bank shows real credits, rent and deposits/withdrawals; Poinbase is the
compliant exchange; Web3 is the wallet and decentralised trading.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:23:15 +08:00
ZHU MINCHAO ce95ec923d Terminal: fixed-width amount field 2026-11-03 15:15:19 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 0560c34231 Halving now raises the price anchor (pass-through 0.6, ramp the week before) with a pre-halving news item
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:13:57 +08:00
ZHU MINCHAO 967077a2db comment: 50% 2026-11-03 15:06:03 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 4b10c6ad7f Buyer spots each hidden flaw with 50% chance
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:06:00 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9165c9edf4 Reputation loss when a buyer catches a false label scales with the price gap (per weekly rent, 1 to 25); drop old-save model fallback
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:03:52 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 393dd8057a Seller claim lapses when the card is bought unless edited with a labeler; test helpers for sell and buy sides
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:57:28 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f564a895d0 Label bar: one model-authenticity row with a hint line; evidence listed after exposure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:30:47 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d6a456c986 Exposing the model reveals all four authenticity rows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:28:39 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 559a702ee3 Label bar: authenticity rows first, quality rows second; single-fan brand text moved off the fan
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:19:43 +08:00
22 changed files with 583 additions and 55 deletions

No files matched your search

+13
View File
@@ -2,6 +2,19 @@
## 未发布
### 第一阶段完成(入门、挖矿、显卡鉴定、终端)
以下条目取代下面较早条目里已过时的部分(型号、入口价、终端页面):
- 显卡改成每个牌子四档(低端、中端、高端、旗舰):Probidia RZX 60 / 70 / 80 / 90,PMD RZ 70 / 80 / 800 / 900;型号只是显示名,性能由牌子和档次定;档次对应供电口(6 / 8 / 8 / 8+8 针)和风扇数(1 / 2 / 3 / 3)。显卡 3×2 格,正反面各一张图;价格约为最初的 1.8 倍,出块奖励 90。
- 显卡外观:叶片、金手指、挡板、封条,随每晚损耗变差,影响价值、算力、损耗、暴毙。
- 二手卡贩子(老实人、中间商、骗子)、假卡(低档芯片套高档壳)、序列号(牌子前缀、档次码、工厂码、流水)、背面丝印、鉴定公会收显卡、显卡鉴定手册。
- 放大镜检视显卡:专门的大图和标签栏(型号真伪加叶片、金手指、挡板、封条四项成色),卖家说法与逐项指出、标签机改说法、买家验货与按价差扣信誉。
- 减半抬高价格锚(传导 0.6,减半前一周有预期行情,减半前 3 天上报纸)。
- 交易终端重做:首页三个入口(银行、Poinbase、Web3)加报告;左右分栏表格;买入均价;Web3 页的稳定币价格与状态;银行提取前的审查提示;金额输入框加宽。
- 入口升级价 400,开局送交易终端、矿架、RZX 60 和鉴定手册。
- 经济模拟 v7(运维库 research/sim/out_v7.md):电网电价下 RZX 70 挖矿第 30 天占总收入 18%、第 60 天 20%。
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
- mod 本体(第一阶段的第一部分):人脉网络"Web3 治理社区"(300,整个 mod 的入口)交付交易终端和一张 RZX 50;矿架向杰克逊博士购买;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSC;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSC 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -173,7 +173,7 @@ def gpu_front(accent, accent_dark, brand_text, tier=1, burnt=False):
# 前沿斜切装饰条:品牌色 + 品牌字
for x in range(x0 + 4, x1 - 20):
c.rect(x, y1 - 5, x, y1 - 3, accent if x % 11 else accent_dark)
text57(c, 136 if count == 1 else 112, 84, brand_text, 'h')
text57(c, 30 if count == 1 else 112, 84, brand_text, 'h') # 单风扇的大风扇占了右边,品牌字改放左下
# 顶沿:四根铜热管端头与 8 针供电口
for i in range(4):
x = 38 + i * 9
+81
View File
@@ -30,6 +30,11 @@ C = {
'Y': (0xC8, 0xA4, 0x4A), # 金手指
'c': (0x2E, 0x2A, 0x2A), # 焦黑
'e': (0x6B, 0x4A, 0x3A), # 焦痕
'u': (0x4F, 0x86, 0xD0), # 蓝
'U': (0x2B, 0x4A, 0x82), # 深蓝
'v': (0x9B, 0x76, 0xD6), # 紫
'V': (0x5E, 0x45, 0x8F), # 深紫
'y': (0xF0, 0xCF, 0x64), # 亮金
'p': (0xC0, 0x7A, 0x4A), # 铜
'q': (0x8A, 0x4F, 0x34), # 暗铜
'w': (0xE6, 0xE2, 0xD0), # 白
@@ -514,6 +519,76 @@ def key(model):
return model.replace(' ', '').lower()
def ui_bank():
"""银行图标(32×32):带山花的石柱建筑,门前一枚金币。"""
c = Canvas(32, 32)
for k in range(0, 14): # 山花(三角)
c.rect(16 - k, 4 + k // 2, 16 + k, 4 + k // 2, 'h' if k < 3 else 'l')
c.rect(2, 11, 29, 12, 'b'); c.rect(2, 13, 29, 13, 'd') # 檐口
for x0 in (4, 10, 16, 22): # 四根柱子
c.rect(x0, 14, x0 + 3, 24, 'l')
c.rect(x0, 14, x0, 24, 'h'); c.rect(x0 + 3, 14, x0 + 3, 24, 'd')
c.rect(x0 - 1, 14, x0 + 4, 14, 'b'); c.rect(x0 - 1, 24, x0 + 4, 24, 'b')
c.rect(1, 25, 30, 26, 'b'); c.rect(0, 27, 31, 28, 'm'); c.rect(0, 29, 31, 29, 'd')
c.rect(14, 8, 17, 9, 'y'); c.put(15, 8, 'w') # 山花里的小徽标
for yy in range(19, 27): # 金币
for xx in range(23, 31):
d = (xx - 26.5) ** 2 + (yy - 22.5) ** 2
if d <= 14:
c.put(xx, yy, 'e' if d > 9 else 'y')
c.put(25, 21, 'w'); c.put(26, 21, 'w'); c.put(26, 22, 'y')
outline(c)
return c
def ui_poinbase():
"""Poinbase 图标(32×32):蓝色盾牌,中间一枚金色 P 币,两侧一上一下的箭头。"""
c = Canvas(32, 32)
for y in range(3, 29): # 盾牌
half = 12 if y < 16 else max(0, 12 - (y - 16) * 12 // 12)
for x in range(16 - half, 16 + half + 1):
c.put(x, y, 'u' if x < 16 else 'U')
c.put(16 - half, y, 'h') if half else None
for x in range(4, 29):
c.put(x, 3, 'h'); c.put(x, 4, 'l')
for y in range(7, 21): # 币
for x in range(9, 24):
d = (x - 16) ** 2 + (y - 13.5) ** 2
if d <= 36:
c.put(x, y, 'e' if d > 28 else 'y')
c.put(12, 9, 'w'); c.put(13, 8, 'w'); c.put(12, 10, 'w')
c.rect(13, 9, 14, 18, 'U'); c.rect(15, 9, 18, 10, 'U'); c.rect(18, 11, 19, 13, 'U'); c.rect(15, 14, 18, 15, 'U') # P
for k in range(4): # 下方两个箭头:买入、卖出
c.put(9 + k, 24 - k, 'w') if k < 3 else None
for x in range(8, 13): c.put(x, 22, 'w')
for x in range(19, 24): c.put(x, 22, 'w')
c.put(19, 23, 'w'); c.put(20, 24, 'w'); c.put(23, 23, 'w'); c.put(22, 24, 'w')
outline(c)
return c
def ui_web3():
"""Web3 图标(32×32):六边形节点网络,紫色底。"""
c = Canvas(32, 32)
nodes = [(16, 7), (7, 14), (25, 14), (11, 24), (21, 24), (16, 16)]
edges = [(0, 1), (0, 2), (1, 3), (2, 4), (3, 4), (5, 0), (5, 1), (5, 2), (5, 3), (5, 4)]
for (a, b) in edges:
(x0, y0), (x1, y1) = nodes[a], nodes[b]
n = max(abs(x1 - x0), abs(y1 - y0))
for i in range(n + 1):
px, py = x0 + (x1 - x0) * i // n, y0 + (y1 - y0) * i // n
c.put(px, py, 'v'); c.put(px + 1, py, 'v'); c.put(px, py + 1, 'v')
for i, (x, y) in enumerate(nodes):
r = 3 if i == 5 else 2
for dy in range(-r, r + 1):
for dx in range(-r, r + 1):
if dx * dx + dy * dy <= r * r + 1:
c.put(x + dx, y + dy, 'V' if (dx + dy) > 0 else 'h' if i != 5 else 'y')
c.put(x, y, 'w')
outline(c)
return c
def guide():
"""鉴定手册(16×16,一格):竖放的小册子,封面带品牌色条,书脊在左。"""
c = Canvas(16, 16)
@@ -542,6 +617,8 @@ for _tier in range(4):
SPRITES['gpu_probidia_f_t%d' % _tier] = (lambda t=_tier: gpu_front('G', 'g2', tier=t))
SPRITES['gpu_pmd_f_t%d' % _tier] = (lambda t=_tier: gpu_front('R', 'r2', tier=t))
UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3}
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
OVERLAYS = {
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
@@ -581,6 +658,8 @@ if __name__ == '__main__':
if oc.px[y][x] is not None:
b.px[y][x] = oc.px[y][x]
return b
for name, make in UI_ICONS.items():
make().png(os.path.join(sys.argv[2], name + '.png'), scale=8, bg=(0x2a, 0x2c, 0x34))
stack('gpu_probidia_f_t1', 'gpu_ov_fan2_2', 'gpu_ov_fin1', 'gpu_ov_brk1').png(os.path.join(sys.argv[2], 'prev_front_worn.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
print(sys.argv[2])
else:
@@ -594,6 +673,8 @@ if __name__ == '__main__':
canvas.png(os.path.join(out, name + '.png'))
for name, make in OVERLAYS.items():
make().png(os.path.join(out, name + '.png'))
for name, make in UI_ICONS.items():
make().png(os.path.join(out, name + '.png'))
import big, big_cases
for name, make in big.BIG.items():
make().png(os.path.join(out, name + '.png'))
+6 -4
View File
@@ -42,14 +42,17 @@ namespace Decentralized.Domain
}
/// <summary>
/// 标签栏的数据:八个标签(型号、供电与风扇、风扇叶片、金手指、挡板、背面丝印、序列号、封条),每个标签有卖家的说法和真实情况。
/// 标签栏的数据:五个标签(型号真伪、风扇叶片、金手指、挡板、封条),每个标签有卖家的说法和真实情况;
/// 供电与风扇、背面丝印、序列号三项是型号真伪的依据,只存状态,不单独成行。
/// 一个标签被指出(拆穿)之后,说法改回真实情况;价格按「已指出的标签取真实情况,其余取说法」算,
/// 所以卖家隐瞒的问题要玩家一项项指出来才能把价格拉回真实价值。指出假货的证据(供电与风扇、丝印、序列号)会连带把型号改回真实型号。
/// 所以卖家隐瞒的问题要玩家一项项指出来才能把价格拉回真实价值。指出型号有问题,型号自动改回真实型号。
/// </summary>
public static class CardLabels
{
public const int RowSpec = 1, RowBuild = 2, RowFan = 4, RowFingers = 8, RowBracket = 16, RowPrint = 32, RowSerial = 64, RowSeal = 128, AllRows = 255;
public static readonly int[] Rows = { RowSpec, RowBuild, RowFan, RowFingers, RowBracket, RowPrint, RowSerial, RowSeal };
/// <summary>标签栏的顺序:第一行鉴定真伪(型号;依据是供电口与风扇数、序列号、背面丝印,玩家对着大图自己看),后四行鉴定成色。</summary>
public static readonly int[] Rows = { RowSpec, RowFan, RowFingers, RowBracket, RowSeal };
public const int AuthRows = 1;
/// <summary>各标签各取值对价格的影响(百分比,相加):风扇叶片积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失 −6%。
/// 型号、供电与风扇、丝印、序列号本身不扣价,它们决定的是这块卡是什么芯片。</summary>
@@ -121,7 +124,6 @@ namespace Decentralized.Domain
{
if ((exposed & r) != 0) continue;
if (r == RowSpec) { if (claim.Model != real.Model) lies |= r; }
else if (IsEvidence(r, claim, real)) lies |= r;
else if (claim.Get(r) != real.Get(r) && Pct(r, claim.Get(r)) > Pct(r, real.Get(r))) lies |= r;
}
return lies;
+14 -1
View File
@@ -16,6 +16,8 @@ namespace Decentralized.Domain
public sealed class Market
{
public double Sentiment = 1.0;
/// <summary>供给因子:出块奖励减半抬高挖矿成本,价格锚随之上移;减半前一周有预期行情。每晚向目标靠拢。</summary>
public double Supply = 1.0;
public List<Pool> Pools = new List<Pool>();
/// <summary>开门时的参考价,夜间用它和收盘价算当日均价(合约的标记价)。</summary>
public double OpenPrice = Tuning.PscStartPrice;
@@ -42,9 +44,20 @@ namespace Decentralized.Domain
/// <summary>PSC 的参考价(信用点)。</summary>
public double Price => Main.Price;
/// <summary>供给因子的目标:奖励比起始奖励每少一半,锚上移 2^传导指数;减半前 7 天内再逐日加预期。</summary>
public static double SupplyTarget(int day, double reward)
{
double t = Math.Pow(Tuning.BlockReward / Math.Max(1e-9, reward), Tuning.HalvingPassThrough);
int toHalving = Tuning.HalvingDays - day % Tuning.HalvingDays; // 减半当晚 day % HalvingDays == 0,那天之后又是一个完整周期
if (toHalving <= Tuning.HalvingRampDays) t *= 1 + Tuning.HalvingRamp * (Tuning.HalvingRampDays - toHalving + 1) / Tuning.HalvingRampDays;
return t;
}
public void UpdateSupply(int day, double reward) => Supply += (SupplyTarget(day, reward) - Supply) * Tuning.SupplyAdjust;
public double Anchor(ICollection<MarketEvent> active)
{
double a = Tuning.PscStartPrice * Sentiment;
double a = Tuning.PscStartPrice * Sentiment * Supply;
if (active.Contains(MarketEvent.Blackout)) a *= 1.10;
if (active.Contains(MarketEvent.Crackdown)) a *= 0.92;
return a;
+7
View File
@@ -9,6 +9,13 @@ namespace Decentralized.Domain
public const double PscStartPrice = 100;
public const double BlockReward = 90;
public const int HalvingDays = 90;
/// <summary>减半对币价的传导:奖励减半时,价格锚上移 2^传导指数 倍(0.6 即约 +52%),市场已部分预期到,所以不是翻倍。</summary>
public const double HalvingPassThrough = 0.6;
/// <summary>减半前一周的预期行情:锚逐日抬高,到减半当天为 +8%。</summary>
public const int HalvingRampDays = 7;
public const double HalvingRamp = 0.08;
/// <summary>价格锚向目标的逐晚靠拢比例(约 7 天完成大半)。</summary>
public const double SupplyAdjust = 0.15;
public const double NpcHash0 = 360;
public const double NpcKjPerH = 2.4;
public const double ElecPrice = 1.5;
+19
View File
@@ -19,11 +19,30 @@ namespace Decentralized.Domain
/// <summary>被执法标记。</summary>
public bool Flagged;
/// <summary>买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。</summary>
public Dictionary<string, double> BuyQty = new Dictionary<string, double>(), BuyCost = new Dictionary<string, double>();
public void RecordBuy(string asset, double qty, double costCredits)
{
if (qty <= 0) return;
BuyQty[asset] = (BuyQty.TryGetValue(asset, out var q) ? q : 0) + qty;
BuyCost[asset] = (BuyCost.TryGetValue(asset, out var c) ? c : 0) + Math.Max(0, costCredits);
}
/// <summary>买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。</summary>
public double? AvgBuy(string asset) => BuyQty.TryGetValue(asset, out var q) && q > 1e-9 ? BuyCost[asset] / q : (double?)null;
public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0;
public void Add(string asset, double amount)
{
if (amount == 0) return;
if (amount < 0 && BuyQty.TryGetValue(asset, out var bq))
{
double before = Get(asset), keep = before > 1e-12 ? Math.Max(0, 1 + amount / before) : 0;
BuyQty[asset] = bq * keep; BuyCost[asset] *= keep;
if (BuyQty[asset] < 1e-9) { BuyQty.Remove(asset); BuyCost.Remove(asset); }
}
double v = Get(asset) + amount;
if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足");
if (Math.Abs(v) < 1e-12) Balances.Remove(asset);
+27
View File
@@ -41,6 +41,33 @@ namespace Decentralized
Walk(m.panel.transform, 0);
Log.Info(sb.ToString());
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Edit claim + offer to buyer (test)", () => Log.Guard("卖出侧测试", () =>
{
var rng = Session.Data.Rng;
// 自己的一张真卡:标签机改成旗舰型号并把成色说成没问题,再摆给一位买家
var item = Items.NewCard("RZX70", 55, null, rng);
Items.Give(item);
var u = Items.ToUnit(item);
var c = Claim.Truth(u); c.Model = "RZX90"; c.Fan = 0; c.Fingers = 0; c.Bracket = 0; c.Sticker = 0;
item.EnableTag(Items.EditedTag);
Items.SetClaim(item, c, 0);
var client = new StoreClient { identifier = "pd_test_buyer", displayName = "买家", spriteName = SpriteDict.GetRandomScavSpriteName(), clientIntent = StoreClient.ClientIntent.BUY };
client.CompleteClientCreation();
PlayerStore.Instance.currentClientInstance = StoreClientInstance.CreateClientInstance(client);
Log.Info($"测试:卡 #{item.uniqueId} 说法 {c.Encode()},真实 {Claim.Truth(u).Encode()},价格 {item.unitValue},信誉前");
Items.OfferForTest(item);
Log.Info($"测试:验货后 Exposed={Items.Exposed(item)} 价格 {item.unitValue}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Buy scam card (test)", () => Log.Guard("买入侧测试", () =>
{
var rng = Session.Data.Rng;
var item = Items.NewCard("RZX90", 60, "RZX60", rng, DealerKind.Scammer, 0);
long before = item.unitValue;
GeneralHelper.SetItemOwned(item, true);
Items.Refresh(item);
Items.Give(item);
Log.Info($"买入测试:卡 #{item.uniqueId} 买前报价 {before},买下后价值 {item.unitValue},真实 {CardValue.True(Items.ToUnit(item))},说法 {(Items.GetClaim(item)?.Encode() ?? "无")}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{
EmporiumEntry.Instance.ClearFrontInv();
+29 -7
View File
@@ -16,8 +16,9 @@ namespace Decentralized
/// </summary>
public sealed class GpuInspectView : MonoBehaviour
{
private const float RowH = 27f, CardScale = 2.7f, PanelTop = -86f;
private const int RowCount = 8;
private const float RowH = 30f, CardScale = 3f, PanelTop = -112f, DetailH = 26f, GroupGap = 8f;
private const int RowCount = 5;
private TextMeshProUGUI detailText;
private static GpuInspectView instance;
private RectTransform root, cardRoot;
private Button flip;
@@ -161,7 +162,7 @@ namespace Decentralized
root.localPosition = Vector3.zero;
cardRoot = new GameObject("Card", typeof(RectTransform)).GetComponent<RectTransform>();
cardRoot.SetParent(root, false);
cardRoot.localPosition = new Vector3(0, 95, 0);
cardRoot.localPosition = new Vector3(0, 85, 0);
buttonTemplate = m.appraisalPanel.transform.Find("buttonSend");
// 贴纸上的序列号:随卡变化,运行时用文字写在贴纸的空白处
serialText = MakeText(root, "Serial", Vector2.zero, new Vector2(48 * CardScale, 8 * CardScale), TextAlignmentOptions.Center, 20);
@@ -173,15 +174,17 @@ namespace Decentralized
font = buttonTemplate.GetComponentInChildren<TextMeshProUGUI>()?.font;
if (font != null) serialText.font = font;
// 标签区:深色底加浅色边框,和上面的大图分开
float panelH = RowCount * RowH + 10f, panelY = PanelTop - panelH / 2;
float panelH = RowCount * RowH + 10f + DetailH + GroupGap, panelY = PanelTop - panelH / 2;
MakeRect(root, "LabelFrame", new Vector2(0, panelY), new Vector2(648, panelH + 8), new Color(0.78f, 0.68f, 0.45f, 1f));
MakeRect(root, "LabelBack", new Vector2(0, panelY), new Vector2(640, panelH), new Color(0.10f, 0.11f, 0.14f, 0.96f));
MakeRect(root, "LabelDivider", new Vector2(0, PanelTop - 5 - RowH * CardLabels.AuthRows - DetailH - GroupGap / 2), new Vector2(620, 2), new Color(0.78f, 0.68f, 0.45f, 0.8f));
detailText = MakeText(root, "Detail", new Vector2(-15, PanelTop - 5 - RowH * CardLabels.AuthRows - DetailH / 2), new Vector2(600, DetailH), TextAlignmentOptions.Left, 17);
flip = MakeButton(root, "Flip", new Vector2(0, PanelTop - panelH - 24), new Vector2(200, 30), out flipText);
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
for (int i = 0; i < RowCount; i++)
{
int row = CardLabels.Rows[i];
float y = PanelTop - 5 - RowH * (i + 0.5f);
float y = PanelTop - 5 - RowH * (i + 0.5f) - (i >= CardLabels.AuthRows ? DetailH + GroupGap : 0f);
rowName[i] = MakeText(root, "RowName" + i, new Vector2(-255, y), new Vector2(110, RowH), TextAlignmentOptions.Left, 22);
rowText[i] = MakeText(root, "RowText" + i, new Vector2(-30, y), new Vector2(330, RowH), TextAlignmentOptions.Left, 22);
rowButton[i] = MakeButton(root, "RowButton" + i, new Vector2(245, y), new Vector2(110, RowH - 3), out rowButtonText[i]);
@@ -218,7 +221,8 @@ namespace Decentralized
{
int row = CardLabels.Rows[i];
rowName[i].text = RowName(row);
rowName[i].color = new Color(1f, 0.85f, 0.45f);
rowName[i].color = i < CardLabels.AuthRows ? new Color(1f, 0.85f, 0.45f) : new Color(0.6f, 0.85f, 1f); // 真伪行金黄,成色行浅蓝
rowName[i].fontSize = 22;
string text;
Color color = new Color(0.96f, 0.98f, 1f);
string Show(Claim c) => row == CardLabels.RowSpec ? DisplayModel(c.Model) : ValueText(row, c.Get(row));
@@ -237,6 +241,14 @@ namespace Decentralized
rowButton[i].gameObject.SetActive(canEdit ? editable : canPoint && (exposed & row) == 0);
rowButtonText[i].text = L.T(canEdit ? "lbl.edit" : "lbl.point");
}
// 型号真伪的依据:没拆穿之前提示去看哪里;拆穿之后列出实际查到的问题
if (detailText != null)
{
bool spec = (exposed & CardLabels.RowSpec) != 0;
string detail = claim == null ? "" : !spec ? L.T("lbl.hint") : Evidence(u, claim.Value, real);
detailText.text = detail;
detailText.color = spec ? new Color(1f, 0.7f, 0.6f) : new Color(0.75f, 0.78f, 0.85f);
}
if (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
// 贴纸上的序列号:只在背面显示
if (serialText != null)
@@ -244,10 +256,20 @@ namespace Decentralized
serialText.gameObject.SetActive(Items.IsBack(item) && !u.Dead);
serialText.text = CardLabels.SerialText(u);
// 贴纸空白处中心在大图 (142, 91):卡片坐标以图中心为原点,y 向上
serialText.rectTransform.localPosition = new Vector3((142 - 96) * CardScale, 95 + (64 - 91) * CardScale, 0);
serialText.rectTransform.localPosition = new Vector3((142 - 96) * CardScale, 85 + (64 - 91) * CardScale, 0);
}
}
/// <summary>型号真伪的依据:实物与卖家说法对不上的地方(供电口与风扇数、序列号、背面丝印)。</summary>
private static string Evidence(Unit u, Claim claim, Claim real)
{
var parts = new List<string>();
if (claim.Tier != real.Tier) parts.Add(L.T("lbl.ev.build", L.T("lbl.tier." + real.Tier), Hardware.PowerPins[real.Tier], Hardware.FanCount[real.Tier]));
if (real.Serial != 0) parts.Add(L.T("lbl.ev.serial." + real.Serial));
if (real.Print != 0) parts.Add(L.T("lbl.ev.print." + real.Print));
return parts.Count == 0 ? L.T("lbl.ev.none") : L.T("lbl.ev.prefix") + string.Join(";", parts);
}
private static string DisplayModel(string model) => Items.DisplayModel(model);
private void OnRowButton(int row)
+36 -10
View File
@@ -22,7 +22,7 @@ namespace Decentralized
public const string FakeTag = "PD_FAKE_OF";
public const string RefurbTag = "PD_REFURBISHED";
public const string DeadTag = "PD_DEAD";
public const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", ClaimTag = "PD_CLAIM", ExposedTag = "PD_EXPOSED", MarkupTag = "PD_MARKUP";
public const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", ClaimTag = "PD_CLAIM", ExposedTag = "PD_EXPOSED", MarkupTag = "PD_MARKUP", EditedTag = "PD_EDITED";
public static Action OpenTerminal;
@@ -111,7 +111,11 @@ namespace Decentralized
var item = ModHelper.SpawnItem(GpuId(model));
var u = new Unit { Model = model, FakeOf = fakeOf, Health = health, Look = CardLook.Generate(rng, health, fakeOf != null) };
Store(item, u);
if (seller != null) SetClaim(item, CardLabels.DealerClaim(u, seller.Value, rng), 0, markupPct);
if (seller != null)
{
GeneralHelper.SetItemOwned(item, false); // 卖家的货:买下之前不属于玩家,说法才有效
SetClaim(item, CardLabels.DealerClaim(u, seller.Value, rng), 0, markupPct);
}
return item;
}
@@ -119,15 +123,21 @@ namespace Decentralized
private static object checkedClient;
private static readonly HashSet<int> checkedItems = new HashSet<int>();
public const double CustomerSpotChance = 0.35;
public const double CustomerSpotChance = 0.50;
public const int RepLossMax = 25;
/// <summary>被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。</summary>
public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
/// <summary>
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 35% 的概率看出来;看出来就拆穿这一项(价格回到真实情况),
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来;看出来就拆穿这一项(价格回到真实情况),
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
/// </summary>
private static void OnOffered(GameItem item)
public static void OfferForTest(GameItem item) { checkedItems.Clear(); OnOffered(item, true); }
private static void OnOffered(GameItem item, bool force = false)
{
if (item.IsTag(DeadTag) || !GeneralHelper.IsItemOwned(item) || !item.IsRootedInBackInv()) return;
if (item.IsTag(DeadTag) || !GeneralHelper.IsItemOwned(item) || (!force && !item.IsRootedInBackInv())) return;
var ci = PlayerStore.Instance.currentClientInstance;
var client = ci?.GetClientBlueprint();
if (client == null || (client.clientIntent != StoreClient.ClientIntent.BUY && client.clientIntent != StoreClient.ClientIntent.SELLNBUY)) return;
@@ -148,12 +158,14 @@ namespace Decentralized
if (r != CardLabels.RowSpec && CardLabels.IsEvidence(r, claim.Value, Claim.Truth(u))) found |= CardLabels.RowSpec | CardLabels.RowBuild;
}
if (found == 0) return;
long before = item.unitValue;
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed | found));
Refresh(item);
int rep = RepLoss(before - item.unitValue);
string faction = string.IsNullOrEmpty(client.clientFaction) || StoreReputation.GetStoreReputation(client.clientFaction) == null ? "FACTION_LOWER_LEVEL" : client.clientFaction;
StoreReputation.ModReputation(faction, -2);
StoreReputation.ModReputation(faction, -rep);
EmporiumAudioPlayer.Instance?.audioFail?.Play();
Log.Info($"顾客看出标签不实:{found},价格 {item.unitValue},信誉 −2");
Log.Info($"顾客看出标签不实:{found},价格 {item.unitValue},信誉 −{rep}");
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T((found & CardLabels.RowSpec) != 0 ? "cust.fake." + rng.Range(1, 2) : "cust.worn." + rng.Range(1, 2))));
}
@@ -165,7 +177,13 @@ namespace Decentralized
return string.IsNullOrEmpty(c.Model) ? (Claim?)null : c;
}
public static int Exposed(GameItem item) => item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
/// <summary>已指出的标签。型号一旦被拆穿,四个真伪行(型号、供电与风扇、序列号、丝印)一起亮出真实情况。</summary>
public static int Exposed(GameItem item)
{
int e = item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
if ((e & CardLabels.RowSpec) != 0) e |= CardLabels.RowSpec | CardLabels.RowBuild | CardLabels.RowSerial | CardLabels.RowPrint;
return e;
}
public static void SetClaim(GameItem item, Claim claim, int exposed, int markupPct = 0)
{
@@ -204,6 +222,7 @@ namespace Decentralized
var claim = GetClaim(item) ?? Claim.Truth(u);
claim = row == CardLabels.RowSpec ? new Claim { Model = model ?? claim.Model, Fan = claim.Fan, Fingers = claim.Fingers, Bracket = claim.Bracket, Print = claim.Print, Sticker = claim.Sticker } : claim.With(row, value);
int exposed = Exposed(item) & ~row;
item.EnableTag(EditedTag);
SetClaim(item, claim, exposed);
}
@@ -220,6 +239,13 @@ namespace Decentralized
private static void SyncValue(GameItem item)
{
if (item.IsTag(DeadTag)) return;
// 买下之后卖家的说法作废:卡回到真实情况,除非玩家用标签机自己改过
if (GeneralHelper.IsItemOwned(item) && item.IsTag(ClaimTag) && !item.IsTag(EditedTag))
{
item.ModifyTag(ClaimTag, t => t.Disable());
item.ModifyTag(ExposedTag, t => t.Disable());
item.ModifyTag(MarkupTag, t => t.Disable());
}
var u = ToUnit(item);
string desc = Desc(item, u);
if (item.shortDescription != desc) item.shortDescription = desc;
@@ -233,7 +259,7 @@ namespace Decentralized
public static Unit ToUnit(GameItem item)
{
string model = item.identifier.Substring(GpuPrefix.Length);
model = Hardware.Specs.Keys.FirstOrDefault(k => k.ToLowerInvariant() == model) ?? "RZX60"; // 旧版本存档里的型号已改名,当作低端卡
model = Hardware.Specs.Keys.FirstOrDefault(k => k.ToLowerInvariant() == model) ?? "RZX60";
var u = new Unit { Model = model, Health = 100 };
if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString();
+57 -4
View File
@@ -18,15 +18,12 @@ item.gpu.desc,"Rated hashrate {0} MH/s. Rated draw {1} kJ per day. Condition: {2
item.gpu.burnt.name,Burnt {0} {1}
item.gpu.burnt.desc,This card no longer works.
term.title,Trading Terminal
tab.wallet,Wallet
tab.trade,Trade
tab.report,Report
wallet.line,"{0}: {1} (about {2} credits)"
wallet.empty,The wallet is empty.
wallet.total,Total value: {0} credits
wallet.taint,Tainted value: {0} credits
trade.amount,Amount
trade.hint,Buy spends the amount in the quote coin. Sell sells the amount of the traded coin.
trade.deposit,Deposit credits as CRC
trade.withdraw,Withdraw CRC as credits
trade.pair,"{0}/{1} ({2})"
@@ -94,7 +91,6 @@ cust.fake.1,"That's not what the label says. You trying to pass off a fake?"
cust.fake.2,"The model doesn't match. That isn't the chip it claims."
cust.worn.1,"That's not what you wrote. You left out the flaws."
cust.worn.2,"The condition on the label is false. Not at this price."
lbl.row.spec,"Model"
lbl.row.build,"Power & fans"
lbl.row.fan,"Blades"
lbl.row.fingers,"Contacts"
@@ -119,3 +115,60 @@ lbl.tier.2,"High"
lbl.tier.3,"Flagship"
item.guide.name,"Graphics card appraisal guide"
item.guide.desc,"Tiers: entry has a 6-pin connector and one fan; mid 8-pin and two fans; high 8-pin and three fans; flagship dual 8-pin and three fans. Model names are display only: Probidia RZX 60 / 70 / 80 / 90, PMD RZ 70 / 80 / 800 / 900, lowest to highest tier. Serial: prefix PB (Probidia) or MD (PMD), tier code 1 to 4, a dash, a two-digit factory code, a dash, four digits. Valid factory codes: PB 11, 12, 21, 34, 47; MD 13, 22, 35, 41, 58. A tier code that does not match the model, or a bad factory code or length, means a fake. Appearance discounts: dusty blades -4%, chipped -12%; dull contacts -4%, oxidised -12%; scratched bracket -2%; missing seal -6%."
lbl.row.spec,"Model authenticity"
lbl.hint,"Check: power connector and fan count, serial, back print"
lbl.ev.prefix,"Evidence: "
lbl.ev.none,"Everything matches the model"
lbl.ev.build,"Hardware is {0} ({1}-pin, {2} fans)"
lbl.ev.serial.1,"Serial: bad factory code or length"
lbl.ev.serial.2,"Serial: tier code does not match"
lbl.ev.print.1,"Print shows another model"
lbl.ev.print.2,"Print was redone"
news.prehalving.title,"PSC Halving Ahead"
news.prehalving.desc,"Miners will receive half as many coins per night in three days. Holders expect the price to rise."
tab.home,"Home"
tab.bank,"Bank"
tab.poinbase,"Poinbase"
tab.web3,"Web3"
home.enter,"Enter"
home.summary,"Credits {0} | On-chain assets about {1} credits"
home.lastnight,"Night of day {0}: mined {1} PSC, {2} / {3} rigs powered"
entry.bank,"Bank"
entry.bank.l1,"Deposits, withdrawals, rent"
entry.bank.l2,"Credits {0}"
entry.poinbase,"Poinbase"
entry.poinbase.l1,"Compliant exchange"
entry.poinbase.l2,"PSC {0} credits"
entry.web3,"Web3"
entry.web3.l1,"Wallet and decentralised trading"
entry.web3.l2,"About {0} credits"
bank.cash,"Credits: {0}"
bank.rent,"Rent: {0} credits, due in {1} days"
bank.crc,"Poinbase account (CRC): {0}"
poin.account,"Poinbase account: CRC {0}, PSC {1}"
avg.line,"{0} average buy price: {1} credits"
avg.short,"avg. buy {0}"
avg.none,"—"
col.asset,"Asset"
col.amount,"Amount"
col.value,"Value"
col.avg,"Avg. buy"
bank.cash.k,"Credits"
bank.rent.k,"Rent"
bank.rent.v,"{0}, due in {1} days"
bank.crc.k,"Poinbase account (CRC)"
bank.ops,"Deposit / withdraw"
poin.account.h,"Poinbase account"
trade.h,"Trade"
web3.wallet.h,"On-chain wallet"
trade.hint,"Buying pays in the quote coin; |selling sells the traded coin."
bank.hint,"Deposit turns credits into CRC; |withdrawing turns CRC back into credits |and passes compliance review."
col.peg,"Price"
col.status,"Status"
stable.ok,"Pegged"
stable.depeg,"Depegged"
stable.warn,"Warning: staking subsidy pool low"
stable.spiral,"Collapsing"
aml.window,"Withdrawn in the last {0} days: {1} credits (cumulative line {2})"
aml.taint,"Wallet taint share: {0}%"
aml.chance,"At this amount, estimated review chance: {0}%"
1 # Probably Decentralized localization table: English (en)
18 item.gpu.burnt.name
19 item.gpu.burnt.desc
20 term.title
tab.wallet
tab.trade
21 tab.report
22 wallet.line
23 wallet.empty
24 wallet.total
25 wallet.taint
26 trade.amount
trade.hint
27 trade.deposit
28 trade.withdraw
29 trade.pair
91 cust.fake.2
92 cust.worn.1
93 cust.worn.2
lbl.row.spec
94 lbl.row.build
95 lbl.row.fan
96 lbl.row.fingers
115 lbl.tier.3
116 item.guide.name
117 item.guide.desc
118 lbl.row.spec
119 lbl.hint
120 lbl.ev.prefix
121 lbl.ev.none
122 lbl.ev.build
123 lbl.ev.serial.1
124 lbl.ev.serial.2
125 lbl.ev.print.1
126 lbl.ev.print.2
127 news.prehalving.title
128 news.prehalving.desc
129 tab.home
130 tab.bank
131 tab.poinbase
132 tab.web3
133 home.enter
134 home.summary
135 home.lastnight
136 entry.bank
137 entry.bank.l1
138 entry.bank.l2
139 entry.poinbase
140 entry.poinbase.l1
141 entry.poinbase.l2
142 entry.web3
143 entry.web3.l1
144 entry.web3.l2
145 bank.cash
146 bank.rent
147 bank.crc
148 poin.account
149 avg.line
150 avg.short
151 avg.none
152 col.asset
153 col.amount
154 col.value
155 col.avg
156 bank.cash.k
157 bank.rent.k
158 bank.rent.v
159 bank.crc.k
160 bank.ops
161 poin.account.h
162 trade.h
163 web3.wallet.h
164 trade.hint
165 bank.hint
166 col.peg
167 col.status
168 stable.ok
169 stable.depeg
170 stable.warn
171 stable.spiral
172 aml.window
173 aml.taint
174 aml.chance
+57 -4
View File
@@ -17,15 +17,12 @@ item.gpu.desc,标称算力 {0} MH/s。标称耗电 {1} kJ/天。成色 {2}。
item.gpu.burnt.name,烧毁的 {0} {1}
item.gpu.burnt.desc,此显卡已无法工作。
term.title,交易终端
tab.wallet,钱包
tab.trade,交易
tab.report,报告
wallet.line,{0}:{1}(约 {2} 信用点)
wallet.empty,钱包为空。
wallet.total,总值:{0} 信用点
wallet.taint,污点:{0} 信用点
trade.amount,数量
trade.hint,买入按计价币的数量支付;卖出按所交易币的数量卖出。
trade.deposit,存入信用点(换成 CRC)
trade.withdraw,提取 CRC(换成信用点)
trade.pair,{0}/{1}({2})
@@ -93,7 +90,6 @@ cust.fake.1,"这不是它标的那块。你拿假货糊弄我?"
cust.fake.2,"型号对不上,里面的芯片根本不是这个。"
cust.worn.1,"跟你写的不一样,毛病你没说。"
cust.worn.2,"标签上的成色是假的吧,这个价我不要。"
lbl.row.spec,"型号"
lbl.row.build,"供电与风扇"
lbl.row.fan,"叶片"
lbl.row.fingers,"金手指"
@@ -118,3 +114,60 @@ lbl.tier.2,"高端"
lbl.tier.3,"旗舰"
item.guide.name,"显卡鉴定手册"
item.guide.desc,"档次:低端 6 针供电、单风扇;中端 8 针、双风扇;高端 8 针、三风扇;旗舰双 8 针、三风扇。型号只是显示名:Probidia 为 RZX 60 / 70 / 80 / 90,PMD 为 RZ 70 / 80 / 800 / 900,按档次从低到高。序列号:前缀 PB(Probidia)或 MD(PMD),接档次码 1 至 4,一道横杠,两位工厂码,一道横杠,四位流水。合法工厂码:PB 为 11、12、21、34、47;MD 为 13、22、35、41、58。档次码与型号不符,或工厂码、位数不对,都是假的。外观折扣:叶片积灰 -4%,缺角 -12%;金手指发暗 -4%,氧化 -12%;挡板划痕 -2%;封条缺失 -6%。"
lbl.row.spec,"型号真伪"
lbl.hint,"依据:供电口与风扇数、序列号、背面丝印"
lbl.ev.prefix,"依据:"
lbl.ev.none,"各项依据都与型号相符"
lbl.ev.build,"实物是{0}({1} 针,{2} 风扇)"
lbl.ev.serial.1,"序列号工厂码或位数不合规"
lbl.ev.serial.2,"序列号档次码与型号不符"
lbl.ev.print.1,"丝印印着另一个型号"
lbl.ev.print.2,"丝印重印过"
news.prehalving.title,"PSC 即将减半"
news.prehalving.desc,"三天后矿工每晚获得的币减半。持币者预期币价上行。"
tab.home,"首页"
tab.bank,"银行"
tab.poinbase,"Poinbase"
tab.web3,"Web3"
home.enter,"进入"
home.summary,"信用点 {0} | 链上资产约 {1} 信用点"
home.lastnight,"第 {0} 天夜间:挖出 {1} PSC,{2} / {3} 个矿架有电"
entry.bank,"银行"
entry.bank.l1,"信用点存取、房租"
entry.bank.l2,"信用点 {0}"
entry.poinbase,"Poinbase"
entry.poinbase.l1,"合规交易所"
entry.poinbase.l2,"PSC {0} 信用点"
entry.web3,"Web3"
entry.web3.l1,"钱包与去中心化交易"
entry.web3.l2,"约 {0} 信用点"
bank.cash,"信用点:{0}"
bank.rent,"房租:{0} 信用点,{1} 天后支付"
bank.crc,"Poinbase 账户(CRC):{0}"
poin.account,"Poinbase 账户:CRC {0},PSC {1}"
avg.line,"{0} 买入均价:{1} 信用点"
avg.short,"买入均价 {0}"
avg.none,"—"
col.asset,"币种"
col.amount,"数量"
col.value,"估值"
col.avg,"买入均价"
bank.cash.k,"信用点"
bank.rent.k,"房租"
bank.rent.v,"{0},{1} 天后支付"
bank.crc.k,"Poinbase 账户(CRC)"
bank.ops,"存取"
poin.account.h,"Poinbase 账户"
trade.h,"交易"
web3.wallet.h,"链上钱包"
trade.hint,"买入按计价币的数量支付;|卖出按所交易币的数量卖出。"
bank.hint,"存入把信用点换成 CRC;|提取把 CRC 换回信用点,|提取要过合规审查。"
col.peg,"价格"
col.status,"状态"
stable.ok,"锚定正常"
stable.depeg,"脱锚"
stable.warn,"预兆:质押补贴池告急"
stable.spiral,"崩盘中"
aml.window,"近 {0} 天已提取 {1} 信用点(累计线 {2})"
aml.taint,"钱包污点比例:{0}%"
aml.chance,"按这个数量,预计审查概率:{0}%"
1 # Probably Decentralized 本地化表:简体中文(zh)
17 item.gpu.burnt.name
18 item.gpu.burnt.desc
19 term.title
tab.wallet
tab.trade
20 tab.report
21 wallet.line
22 wallet.empty
23 wallet.total
24 wallet.taint
25 trade.amount
trade.hint
26 trade.deposit
27 trade.withdraw
28 trade.pair
90 cust.fake.2
91 cust.worn.1
92 cust.worn.2
lbl.row.spec
93 lbl.row.build
94 lbl.row.fan
95 lbl.row.fingers
114 lbl.tier.3
115 item.guide.name
116 item.guide.desc
117 lbl.row.spec
118 lbl.hint
119 lbl.ev.prefix
120 lbl.ev.none
121 lbl.ev.build
122 lbl.ev.serial.1
123 lbl.ev.serial.2
124 lbl.ev.print.1
125 lbl.ev.print.2
126 news.prehalving.title
127 news.prehalving.desc
128 tab.home
129 tab.bank
130 tab.poinbase
131 tab.web3
132 home.enter
133 home.summary
134 home.lastnight
135 entry.bank
136 entry.bank.l1
137 entry.bank.l2
138 entry.poinbase
139 entry.poinbase.l1
140 entry.poinbase.l2
141 entry.web3
142 entry.web3.l1
143 entry.web3.l2
144 bank.cash
145 bank.rent
146 bank.crc
147 poin.account
148 avg.line
149 avg.short
150 avg.none
151 col.asset
152 col.amount
153 col.value
154 col.avg
155 bank.cash.k
156 bank.rent.k
157 bank.rent.v
158 bank.crc.k
159 bank.ops
160 poin.account.h
161 trade.h
162 web3.wallet.h
163 trade.hint
164 bank.hint
165 col.peg
166 col.status
167 stable.ok
168 stable.depeg
169 stable.warn
170 stable.spiral
171 aml.window
172 aml.taint
173 aml.chance
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

+2
View File
@@ -108,6 +108,8 @@ namespace Decentralized
if (d.Network.Reward < rewardBefore) News.Show("pd_news_halving_" + day, L.T("news.halving.title"), L.T("news.halving.desc"), 2);
double crcFactor = d.Stable.Night(day, d.Rng, 0);
if (crcFactor < 1) d.Hot.Balances[Assets.CRC] = d.Hot.Get(Assets.CRC) * crcFactor;
d.Market.UpdateSupply(day, d.Network.Reward);
if (Tuning.HalvingDays - day % Tuning.HalvingDays == 3) News.Show("pd_news_prehalving_" + day, L.T("news.prehalving.title"), L.T("news.prehalving.desc"), 3);
d.Market.Night(events, d.Rng, day, Session.Price);
report.Price = d.Market.Price;
d.LastNight = report;
+204 -23
View File
@@ -1,17 +1,21 @@
using System;
using System.Globalization;
using System.Linq;
using UnityEngine;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>交易终端窗口:钱包、交易、报告三页。每次操作后按当前页重建窗口。</summary>
/// <summary>
/// 交易终端窗口:首页三个入口(银行、Poinbase、Web3)加报告。银行是真实的信用点(存取、房租);
/// Poinbase 是合规交易所;Web3 是去中心化交易和链上钱包。每次操作后按当前页重建窗口。
/// </summary>
public static class TerminalWindow
{
public const string Id = "pd_terminal";
private enum Page { Wallet, Trade, Report }
private enum Page { Home, Bank, Poinbase, Web3, Report }
private static Page _page = Page.Wallet;
private static Page _page = Page.Home;
private static string _amount = "";
private static string _message;
@@ -20,7 +24,7 @@ namespace Decentralized
if (Session.Data == null) return;
var b = CustomUIManager.Window(Id, L.T("term.title"));
if (b == null) return;
b.SetSize(620, 560).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
b.SetSize(780, 580).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
b.BeginRow();
foreach (Page p in Enum.GetValues(typeof(Page)))
{
@@ -30,11 +34,13 @@ namespace Decentralized
}
b.End();
b.AddSeparator();
b.BeginScroll(440);
b.BeginScroll(450);
switch (_page)
{
case Page.Wallet: WalletPage(b); break;
case Page.Trade: TradePage(b); break;
case Page.Home: HomePage(b); break;
case Page.Bank: BankPage(b); break;
case Page.Poinbase: ExchangePage(b, Venue.Exchange); break;
case Page.Web3: Web3Page(b); break;
default: ReportPage(b); break;
}
b.End();
@@ -42,30 +48,148 @@ namespace Decentralized
b.Show();
}
private static void WalletPage(CustomUIBuilder b)
private static void Go(Page p) { _page = p; _message = null; Open(); }
private static void Entry(CustomUIBuilder b, string icon, string name, string line1, string line2, Page target)
{
var w = Session.Data.Hot;
if (w.Balances.Count == 0) b.AddLabel(L.T("wallet.empty"));
foreach (var kv in w.Balances.OrderBy(k => k.Key))
b.AddLabel(L.T("wallet.line", kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))));
b.AddSeparator();
b.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
b.BeginPanel(220f, 250f, TextAnchor.UpperCenter, 10f, 6f);
b.AddImage(ModInfo.Id + ":" + icon, 96f, 96f);
b.AddHeader(name);
b.AddLabel(line1);
if (line2 != null) b.AddLabel(line2);
b.AddFlexibleSpace();
b.AddButton(L.T("home.enter"), () => Go(target));
b.End();
}
private static void TradePage(CustomUIBuilder b)
private static void HomePage(CustomUIBuilder b)
{
var d = Session.Data;
double chain = d.Hot.Value(Session.Price);
b.AddLabel(L.T("home.summary", PlayerStore.Instance.playerCash, Fmt.Credits(chain)));
b.AddSpace(6f);
b.BeginRow(8f, TextAnchor.UpperCenter);
Entry(b, "ui_bank", L.T("entry.bank"), L.T("entry.bank.l1"), L.T("entry.bank.l2", PlayerStore.Instance.playerCash), Page.Bank);
Entry(b, "ui_poinbase", L.T("entry.poinbase"), L.T("entry.poinbase.l1"), null, Page.Poinbase);
Entry(b, "ui_web3", L.T("entry.web3"), L.T("entry.web3.l1"), L.T("entry.web3.l2", Fmt.Credits(chain)), Page.Web3);
b.End();
b.AddSpace(8f);
b.AddSeparator();
var r = d.LastNight;
b.AddLabel(r == null ? L.T("report.none") : L.T("home.lastnight", r.Day, Fmt.Num(r.Mined), r.PoweredRigs, r.Rigs));
}
private const float LeftW = 430f, RightW = 300f;
private static void Cell(CustomUIBuilder b, float w, string text, bool header = false)
{
b.BeginPanel(w, 30f, TextAnchor.MiddleLeft, 6f, 0f);
b.AddLabel(header ? "<b>" + text + "</b>" : text);
b.End();
}
private static void TableRow(CustomUIBuilder b, float[] widths, params string[] cells)
{
b.BeginRow(2f);
for (int i = 0; i < widths.Length; i++) Cell(b, widths[i], i < cells.Length ? cells[i] : "");
b.End();
}
private static void TableHeader(CustomUIBuilder b, float[] widths, params string[] cells)
{
b.BeginRow(2f);
for (int i = 0; i < widths.Length; i++) Cell(b, widths[i], cells[i], true);
b.End();
}
/// <summary>左右分栏:左边是账户与持仓表格,右边是操作区。</summary>
private static void Columns(CustomUIBuilder b, Action<CustomUIBuilder> left, Action<CustomUIBuilder> right)
{
b.BeginRow(10f);
b.BeginPanel(LeftW, -1f, TextAnchor.UpperLeft, 8f, 4f);
left(b);
b.End();
b.BeginPanel(RightW, -1f, TextAnchor.UpperLeft, 8f, 6f);
right(b);
b.End();
b.End();
}
private static void AmountRow(CustomUIBuilder b)
{
b.AddLabel(L.T("trade.hint"));
b.BeginRow();
b.AddLabel(L.T("trade.amount"));
b.BeginPanel(RightW - 30f, -1f, TextAnchor.MiddleCenter, 0f, 0f); // 给输入框一个固定宽度,容得下大额数字
b.AddInput("0", _amount, s => _amount = s, "amount", s => _amount = s);
b.End();
b.BeginRow();
b.AddButton(L.T("trade.deposit"), Deposit);
b.AddButton(L.T("trade.withdraw"), Withdraw);
b.End();
}
/// <summary>银行:真实的信用点。现金、房租,以及信用点与 Poinbase 账户(CRC)之间的存取。</summary>
private static void BankPage(CustomUIBuilder b)
{
var ps = PlayerStore.Instance;
var w = new[] { 200f, 220f };
Columns(b, l =>
{
l.AddHeader(L.T("entry.bank"));
TableRow(l, w, L.T("bank.cash.k"), ps.playerCash.ToString());
TableRow(l, w, L.T("bank.rent.k"), L.T("bank.rent.v", Session.Rent(), ps.GetRentDayCounter()));
TableRow(l, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Hot.Get(Assets.CRC)));
}, r =>
{
r.AddHeader(L.T("bank.ops"));
foreach (var line in L.T("bank.hint").Split('|')) r.AddLabel(line);
AmountRow(r);
AmlPreview(r);
r.AddButton(L.T("trade.deposit"), Deposit);
r.AddButton(L.T("trade.withdraw"), Withdraw);
});
}
/// <summary>提取前的合规审查提示:本周已提取的累计、污点比例,以及按当前数量估算的审查概率。</summary>
private static void AmlPreview(CustomUIBuilder b)
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
double rent = Session.Rent();
double recent = d.Exchange.History.Where(h => h.Key > day - Tuning.AmlWindowDays).Sum(h => h.Value);
b.AddSeparator();
foreach (var pool in Session.Data.Market.Pools)
b.AddLabel(L.T("aml.window", Tuning.AmlWindowDays, Fmt.Credits(recent), Fmt.Credits(Tuning.AmlCumRents * rent)));
double total = d.Hot.Value(Session.Price);
double taintRatio = total > 0 ? d.Hot.Taint / total : 0;
b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)));
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0)
{
double value = Math.Min(v, d.Hot.Get(Assets.CRC)) * Session.Price(Assets.CRC);
double taint = total > 0 ? d.Hot.Taint * Math.Min(1, value / total) : 0;
double chance = Exchange.HitChance(value, taint, recent, rent, d.Hot.Flagged);
b.AddLabel(L.T("aml.chance", (int)Math.Round(chance * 100)));
}
}
private static readonly float[] StableW = { 80f, 100f, 220f };
private static string StableStatus(Stablecoins st, string asset, int day)
{
double p = st.Price(asset);
if (asset == Assets.CRA && st.CraWarning(day)) return L.T("stable.warn");
if (asset == Assets.CRA && p < 0.97) return L.T("stable.spiral");
return p < 0.99 ? L.T("stable.depeg") : L.T("stable.ok");
}
/// <summary>稳定币行情:价格与状态(脱锚、预兆)。</summary>
private static void StableTable(CustomUIBuilder b)
{
var st = Session.Data.Stable;
int day = StoreStation.GetDayCounter();
b.AddSeparator();
TableHeader(b, StableW, L.T("col.asset"), L.T("col.peg"), L.T("col.status"));
foreach (var a in new[] { Assets.CRC, Assets.CRT, Assets.CRA })
TableRow(b, StableW, a, Fmt.Num(st.Price(a)), StableStatus(st, a, day));
}
private static void TradeRows(CustomUIBuilder b, Venue venue)
{
foreach (var pool in Session.Data.Market.Pools.Where(x => x.Venue == venue))
{
var p = pool;
b.AddLabel(L.T("trade.pair", p.Base, p.Quote, L.T("venue." + p.Venue)) + " " + L.T("trade.price", Fmt.Num(p.Price)));
@@ -76,6 +200,60 @@ namespace Decentralized
}
}
private static readonly float[] HoldW = { 80f, 100f, 110f, 110f };
private static void Holdings(CustomUIBuilder b, Wallet w, Func<string, bool> show)
{
TableHeader(b, HoldW, L.T("col.asset"), L.T("col.amount"), L.T("col.value"), L.T("col.avg"));
bool any = false;
foreach (var kv in w.Balances.OrderBy(k => k.Key).Where(k => show(k.Key)))
{
any = true;
TableRow(b, HoldW, kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key)), AvgText(w, kv.Key));
}
if (!any) b.AddLabel(L.T("wallet.empty"));
}
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
private static void ExchangePage(CustomUIBuilder b, Venue venue)
{
var w = Session.Data.Hot;
Columns(b, l =>
{
l.AddHeader(L.T("poin.account.h"));
Holdings(l, w, a => a == Assets.CRC || a == Assets.PSC);
}, r =>
{
r.AddHeader(L.T("trade.h"));
foreach (var line in L.T("trade.hint").Split('|')) r.AddLabel(line);
AmountRow(r);
TradeRows(r, venue);
});
}
/// <summary>Web3:链上钱包与去中心化交易。</summary>
private static void Web3Page(CustomUIBuilder b)
{
var w = Session.Data.Hot;
Columns(b, l =>
{
l.AddHeader(L.T("web3.wallet.h"));
Holdings(l, w, a => true);
l.AddSeparator();
l.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
StableTable(l);
}, r =>
{
r.AddHeader(L.T("trade.h"));
foreach (var line in L.T("trade.hint").Split('|')) r.AddLabel(line);
AmountRow(r);
TradeRows(r, Venue.Dex);
});
}
private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none");
private static void ReportPage(CustomUIBuilder b)
{
var r = Session.Data.LastNight;
@@ -109,6 +287,7 @@ namespace Decentralized
ModHelper.AddCredit(-credits, false);
var w = Session.Data.Hot;
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
w.RecordBuy(Assets.CRC, credits / Session.Data.Stable.Crc, credits);
w.Basis += credits;
_message = L.T("msg.deposit", credits);
Session.Save();
@@ -141,8 +320,10 @@ namespace Decentralized
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
if (w.Get(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
double got = buy ? p.BuyBase(v) : p.SellBase(v);
double spent = v * Session.Price(pay);
w.Add(pay, -v);
w.Add(get, got);
w.RecordBuy(get, got, spent);
_message = L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
Session.Save();
Open();
+30 -1
View File
@@ -248,6 +248,35 @@ internal static class Program
Check(hi.Price <= 300 + 1e-9, $"上限 3 倍:{hi.Price:0.0}");
});
Test("钱包:买入均价按买入记录算,卖出和转出按比例减少,挖出的币不计入", () =>
{
var w = new Wallet { Address = "x" };
w.Add("PSC", 10); // 挖出的 10 个,无买入记录
Check(w.AvgBuy("PSC") == null, "没有买入记录时没有均价");
w.Add("PSC", 5); w.RecordBuy("PSC", 5, 400);
w.Add("PSC", 5); w.RecordBuy("PSC", 5, 600);
Check(Near(w.AvgBuy("PSC").Value, 100, 1e-9), "买 5 个花 400 + 买 5 个花 600:均价 100");
w.Add("PSC", -10); // 卖掉一半(20 个里的 10 个)
Check(Near(w.AvgBuy("PSC").Value, 100, 1e-9) && Near(w.BuyQty["PSC"], 5, 1e-9), "卖出后均价不变,买入记录按比例减少");
w.Add("PSC", -10);
Check(w.AvgBuy("PSC") == null, "清仓后记录清除");
});
Test("市场:减半抬高价格锚(逐日靠拢),减半前一周有预期行情", () =>
{
Check(Near(Market.SupplyTarget(10, Tuning.BlockReward), 1.0, 1e-9), "减半前很远:目标因子 1");
double before = Market.SupplyTarget(89, Tuning.BlockReward), start = Market.SupplyTarget(83, Tuning.BlockReward);
Check(start > 1.0 && before > start && before <= 1 + Tuning.HalvingRamp + 1e-9, $"减半前 7 天逐日抬高,最多 +8%:{start:0.000} → {before:0.000}");
double after = Market.SupplyTarget(91, Tuning.BlockReward / 2);
Check(Near(after, Math.Pow(2, Tuning.HalvingPassThrough), 1e-9), $"减半后目标因子 2^0.6 = {after:0.000}");
var m = new Market();
for (int i = 0; i < 7; i++) m.UpdateSupply(91 + i, Tuning.BlockReward / 2);
Check(m.Supply > 1.0 && m.Supply < after, "逐晚靠拢目标,不一次跳到位");
for (int i = 0; i < 40; i++) m.UpdateSupply(100 + i % 5, Tuning.BlockReward / 2);
Check(Near(m.Supply, after, 0.02), "几十晚后基本到位");
Check(m.Anchor(new MarketEvent[0]) > Tuning.PscStartPrice * 1.4, "锚随之上移");
});
Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () =>
{
var m = Market.CreateDefault(1);
@@ -370,7 +399,7 @@ internal static class Program
int ex2 = 0;
var honestUnit = new Unit { Model = "RZX70", Health = 60, Look = new CardLook { Fan = 1 } };
Check(CardLabels.Point(honestUnit, CardLabels.DealerClaim(honestUnit, DealerKind.Honest, new Rng(1)), CardLabels.RowFan, ref ex2) == 0, "老实人说的是实话,指出去没用");
Check(CardLabels.Lies(u, claim, 0) == (CardLabels.RowSpec | CardLabels.RowBuild | CardLabels.RowFan | CardLabels.RowFingers | CardLabels.RowPrint), "骗子隐瞒了型号、供电与风扇、风扇叶片、金手指、丝印");
Check(CardLabels.Lies(u, claim, 0) == (CardLabels.RowSpec | CardLabels.RowFan | CardLabels.RowFingers), "骗子隐瞒了型号、叶片、金手指");
Check(CardLabels.Lies(u, claim, ex) == 0, "指出之后不再算隐瞒");
Check(Claim.Decode(claim.Encode()).Encode() == claim.Encode(), "说法能存取");
});