217 Commits
Author SHA1 Message Date
ZHU MINCHAOandClaude Opus 5.5 685762d110 2.0.0:版本号、CHANGELOG 整理成发布版
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:54:31 +08:00
ZHU MINCHAOandClaude Opus 5.5 67df561a5f 兑换、黑市自动换币、铸 CRA、CRA 排队订单按脏币比例带过去;持仓表列宽;中文统一叫“脏币”
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:12:15 +08:00
ZHU MINCHAOandClaude Opus 5.5 34b70023c9 污点按币种记脏枚数、只在 Web3 钱包里;Poinbase 只收干净的币;删除出金审查;永续操纵只记罪证;泄露记罪证;剧情替换;存档迁移到数据版本 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:04:09 +08:00
ZHU MINCHAOandClaude Opus 5.5 f0a6051591 补测:钓鱼混币器提回时日志只说地址被标记(不写制裁);测试按钮:钱包加 CRC、自营混币器风声拉满、立刻换成钓鱼混币器、联系人 + 明天镇压
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 19:03:14 +08:00
ZHU MINCHAOandClaude Opus 5.5 8afdfa606c CHANGELOG:Tumbler 外观修正
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 18:46:08 +08:00
ZHU MINCHAOandClaude Opus 5.5 e88fab3f1b Tumbler 扩展实测修正:没有出款可还原时单独一句日志;每次上门是同一个外观;测试按钮“剧情重来”
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 18:45:54 +08:00
ZHU MINCHAOandClaude Opus 5.5 a37dd4ca7c Tumbler 扩展:治安部探员与搜查、密钥泄露还原出款污点、审判预测盘、黑市跑路、报复、长期联系人、混币器换皮重开
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 18:30:28 +08:00
ZHU MINCHAOandClaude Opus 5.5 0f26d88c03 自营混币器取回:照页面上四舍五入的可取回数量填写时按全部取回
实测发现:可取回显示 25.75,实际 25.749…,照填被判超出。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:57:13 +08:00
ZHU MINCHAOandClaude Opus 5.5 64f330ef25 自营混币器:大池子更容易被盯上(藏身半值 40,000 → 160,000)
- 查封概率随本金上升(8 万约是 5 千的 4 倍),净收益仍随本金增长、增速递减(mixsim out_v3)
- 手册改为"池子大能藏得深一点,但客流也大,总体更容易被盯上",8 种语言

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:36:12 +08:00
ZHU MINCHAOandClaude Opus 5.5 c2a6d8832a 自营混币器调整:利润翻倍、查封减半以上、大池子更难追查
- 基础需求 ×2(OwnMixDemand0 40,000、OwnMixDemandPerDay 1,400)
- 查封系数 0.01 → 0.002;风声累积除以 1 + 匿名集 / OwnMixHeatAnon0(40,000),本金加大时查封概率不再跟着翻倍
- mixsim out_v2:注资 1 万 / 2 万 / 8 万第 61–90 天手续费每天约 317 / 580 / 1,000,90 天被查封 0.08 / 0.13 / 0.19 次(原 0.31 / 0.50 / 1.14),净收益随本金单调增长
- 手册补"池子越大越难追查",8 种语言

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:31:27 +08:00
ZHU MINCHAOandClaude Opus 5.5 71002cbced 第十一阶段 stage 3:自营混币器
- Domain/OwnMixer:注资开张、NPC 客户存入、按匿名集洗污点出款(和普通混币器同一套抽象,匿名集主要是本金和收入)、手续费收入取回全额带污点、本金带自己的污点;客流按"费率 + 出款剩下的污点"和 Tumbler 比,乘黑市声望;风声累积按概率查封(全部没收、记 FENCING、停业)、便衣存入(记 FENCING)、卷款跑路(全额污点、记 FRAUD、黑市声望下降、永久关闭)
- 灰网页"自营混币器"标签(买下场外换币台就有;TerminalWindow 拆成 partial,页面在 TerminalWindow.OwnMix.cs):状态、费率、口碑、风声、匿名集、各币种本金/收入/客户在途;费率、开张/暂停、注资(账单转账)、取回、卷款跑路(合约签名)
- 夜间结算、日志、报纸新闻;浏览器显示"自营混币器"地址名
- Tuning.OwnMix*(research/mixsim 标定),单元测试,手册与 8 种语言
- 实测:注资 20 PSC 开张,三晚各一笔存入、一次便衣

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:14:28 +08:00
ZHU MINCHAOandClaude Opus 5.5 9d95e1f760 换币客户改在交易终端成交:按比例部分成交、买币随机出价、弹出钱包
- 对话只剩"到终端上谈 / 这单我不做":交易终端打开柜台交易页(标签"柜台",只在有客户时出现),客户在柜台前等着
- 柜台交易页:客户来意、数量、市值、出价/要价、单价、地址(卖币可"查地址"进浏览器),25/50/75/100% 或手输数量部分成交(Otc.Part),成交 / 放弃
- 买币客户随机出价(OtcBidMin −5% … OtcBidMax +30%),去掉三档报价与成交概率(OtcPremium、OtcAccept);成交后弹出 PLEDGER,账单 Prefill 填好客户地址
- 卖币:成交后付现金、币进已连接的钱包;客户说最后一句离开(DialogUIManager.StartDialogue)
- 客户被打发或离开时交易作废;不再有"当晚未付作废"
- 增量翻译:只是和已删旧键同文的新键照常翻译
- 实测:买币客户 25% 部分成交(先在 Web3 换出 CRA),PLEDGER 预填地址签名,到账 +668,客户离开;涡轮增压器作用于矿架立刻挖一轮

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:46:14 +08:00
ZHU MINCHAOandClaude Opus 5.5 2badc45a5a 涡轮增压器可以作用于矿架和矿机:立刻挖一轮
- 原来挡掉增压器(白天多扣一次电、挖矿不多);改为照原版机器规则多跑一轮:增压器作用于矿机时打标记,矿架、矿机的夜间回调据此立刻结算一轮(TurboMine)——扣一晚的电、按全网份额出块发到收款地址、算一次损耗和暴毙
- 增压器的判断、消耗、充电走原版;夜间按机器发放抽成 Night.PayMachine 供两处共用
- 手册矿机一章、夜间日志 nightlog.turbo,8 种语言

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:19:36 +08:00
ZHU MINCHAOandClaude Opus 5.5 7269c253af 场外换币台实测修正:三个选项、换币账单不独占终端
- 原版对话最多三个选项:买币对话去掉"不卖"(第四个选项会越界),不想卖就打发客户;选项那句话带上报价和溢价,删除 otc.tier
- 换币账单单独挂着(OtcClient.Standing),WalletWindow.Pending 没有终端账单时回落到它:不独占 Web3、灰网页,换币、混币开的账单不会把它覆盖;签名后清掉,被挤出内存池时放回
- 到账后刷新终端;手册补一句"不想卖就把客户打发走"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:10:03 +08:00
ZHU MINCHAOandClaude Opus 5.5 ec578a39d9 第十一阶段 stage 2:场外换币台(暗灯招换币客户)
- 新人脉升级“场外换币台”(OtcUnlockPrice 500),附赠一盏暗灯
- 包装原版暗灯工厂:切换顺序末尾加目标 pd_otc(只接管进出这一步,其余交给原切换函数);顾客工厂写进公开的 StoreClientListDict.storeClientDict,顾客面板与概率走原版逻辑
- 换色:底图换成原版熄灭的暗灯,叠加层画黄绿色光晕(otc_lamp_on),不进存档
- 换币客户(OtcClient)一半卖币、一半买币:卖币并入原收黑币(Domain/Otc.cs 取代 HotCoins.cs,HotCoinSeller 删除),污点比例随机、越脏越便宜、地址来历写进账本;买币三档报价,PLEDGER 转账确认后现金到账,当晚未付作废
- 只在生成顾客的回调窗口里生成报价、写账本(原版取名字也会调用工厂)
- 存档数据版本 4:旧存档出块奖励、NPC 算力按 MiningScale 放大
- 8 种语言文字、手册“换币客户”一节;单元测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:29:49 +08:00
ZHU MINCHAOandClaude Opus 5.5 bc23ae056b 第十一阶段 stage 1:污点代替没收、模块不设上限、出块 ×10
- 预测市场内幕单改为确定性记污点:结算或卖出时照常兑付,内幕份额对应的兑付记收款钱包污点;删除 PredictInsiderShare、PredictInsiderCatch
- 操纵检出被查后照常兑付,预测兑付记钱包污点、永续盈利记 Poinbase 账户污点;删除 ManipFreezeDays
- Wallet.AddTaintCapped:污点不超过钱包当时的价值,超出部分按 PredictFraudShare / PredictFraudCap 记诈骗罪证(TaintCrime.Commit)
- 模块效果不设上限,只留算力、耗电、损耗乘数的下限(ModuleHashFloor / ModuleKjFloor / ModuleWearFloor)
- BlockReward 100 → 1000,NpcHash0 360 → 3600
- 修正内幕卖出日志"只退本金"的错误说法;8 种语言文字与手册更新

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:17:41 +08:00
ZHU MINCHAOandClaude Opus 5.5 45a6ec2f77 版本 1.1.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:46:50 +08:00
ZHU MINCHAOandClaude Opus 5.5 709339b00c 实测修正:交易对行去掉交易所名、互换损耗两位小数、读档时 DEX 侧池按新深度重设
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:28:41 +08:00
ZHU MINCHAOandClaude Opus 5.5 fe82395d32 Poinbase 只收 PSC、CRC、SPARK,新增 SPARK/CRC;DEX 稳定币对改为 StableSwap
- 钱包拦下往 Poinbase 充值地址转 CRT、CRA;资金页按上架币种列出
- 现货交易对跟着行情图选中的币(PSC/CRC、SPARK/CRC)
- CRT/CRC、CRA/CRC 用 StableSwap:手续费 0.04%、A=200;脱锚 A=20,崩盘 A=1 且深度四分之一
- 旧存档读档时补 SPARK/CRC、升级稳定币池

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:03:59 +08:00
ZHU MINCHAOandClaude Opus 5.5 6d8b05215c Poinbase 订单簿、永续滑点减半、预测盘滑点调整、浏览器统一记账
- Poinbase 主池改为订单簿冲击曲线:第 38 天 2 PSC 约 0.1%、200 PSC 约 4%
- 永续成交深度 50,000 → 100,000,滑点减半
- 预测盘 b = 1.8 × 额度 / ln 2(5% 盘口押 50 约贵 10%),消息价 2% / 5% / 12%
- 统一记账函数 Chain:钱包每一笔进出写进浏览器和钱包记录,合约调用按执行前后余额差记账;Ponero 不上浏览器;账本上限 500
- 存档数据版本 3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:39:18 +08:00
ZHU MINCHAOandClaude Opus 5.5 f0bbba2305 现货深度、保险基金、爆仓新闻、预测盘流动性、收黑币、赃物污点、升级降价
- 主池深度 50,000 → 600,000,现货与出金手续费 0.1%;永续成交深度单独计算,滑点不变
- Poinbase 现货买卖都按标的币数量输入,显示手续费,滑点不含手续费
- 保险基金初值 200,000,NPC 按概率爆仓(剩余保证金进基金)或穿仓(基金垫付)
- 单日涨跌 15% 以上上报纸:全网爆仓人数和金额(含玩家)
- 预测盘流动性与先验脱钩,份额上限按单盘额度,手续费 1%;查处概率、消息价相应上调
- 收黑币:事件期间上门折价出手 CRT / PSC,兑换处开放后有 Ponero;便衣
- 灰网出售赃物按热度带污点
- 混币器 300、黑市 600
- 存档数据版本 2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 22:24:36 +08:00
ZHU MINCHAO 68eca64944 更新日志:去掉工具名 2026-11-05 23:17:12 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 48b41cbc5b 版本 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 23:14:22 +08:00
ZHU MINCHAO 9866fc693e 8 种语言文案审查与改进 2026-11-05 22:55:21 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 edadcade65 版本 1.0.0-rc11
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 22:47:10 +08:00
ZHU MINCHAO df57c417d9 下单预览实时更新:现货买卖、永续开平仓、互换 2026-11-05 22:41:00 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 465e46c6bb 版本 1.0.0-rc10
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 22:32:38 +08:00
ZHU MINCHAO 694581ca28 现货池基础深度改回 50000;免查线说明按新深度 2026-11-05 22:32:01 +08:00
ZHU MINCHAO 1b789d9ad8 操纵检出免查线 16%:第 0 天现货 10000 以内不查 2026-11-05 22:28:55 +08:00
ZHU MINCHAO cc3b74e044 操纵检出:免查线 15%,斜率 3;测试按相对值 2026-11-05 22:27:31 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 96cadfa411 浏览器分栏与悬停提示;ASIC 状态栏;收款地址读档后刷新;加深市场深度
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 22:25:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 7641336d60 版本 1.0.0-rc9
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 21:58:23 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f5beeea900 终端只重建内容区并合并同帧刷新;平台费页文案;按住签名随 Gas 档位缩短
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 21:57:15 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 876a8c4daa 浏览器:所有地址都显示缩写地址和名称(Poinbase、混币器、平台、兑换处、商家)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 21:43:04 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 c3bc42b57a 浏览器:转账记录加天数;矿机收益从 RatDigg 矿池地址转出
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 21:37:53 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 93e26fc4f5 版本 1.0.0-rc8:卖货橱窗重构、收款币种、Gas 档位速度、区块自然增长、钱包持仓
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 21:32:30 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 018ee7d0d2 版本 1.0.0-rc7
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 17:11:51 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e746024422 首页加挖矿、灰网、钱包入口卡片和图标
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 16:50:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 264252881c 终端页签重组:挖矿提到一级,混币器和商店并入灰网,浏览器进钱包页,连接钱包挪到顶栏
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 16:31:42 +08:00
ZHU MINCHAO dada962fe6 版本 1.0.0-rc6 2026-11-05 07:50:17 +08:00
ZHU MINCHAO ae044dd868 手册:链上手续费改称 Gas 费(全部语言);混币器章挪到出金审查之前 2026-11-05 07:46:31 +08:00
ZHU MINCHAO fbdf99e494 钱包与资金页的链上手续费改称 Gas 费 2026-11-05 07:43:05 +08:00
ZHU MINCHAO ac825b42e4 版本 1.0.0-rc5:第十阶段 stage 1 与 stage 2 2026-11-05 07:38:52 +08:00
ZHU MINCHAO a71e1f0885 校对第十阶段 stage 2 译文:铸造/销毁沿用术语表,页面名与界面一致 2026-11-05 07:28:46 +08:00
ZHU MINCHAO 44bccb5b87 第十阶段 stage 2(五):手册与相关文案按部分抵押、资金页、浏览器改写(8 种语言) 2026-11-05 07:25:40 +08:00
ZHU MINCHAO a3e955ca59 第十阶段 stage 2(四):浏览器页(全局账本)、Web3 页签折行 2026-11-05 07:22:01 +08:00
ZHU MINCHAO e3e5ec2d6e 第十阶段 stage 2(三):Poinbase 资金页(入金、出金、确认进度条)、银行页只读、Poinbase 接收所有币 2026-11-05 07:19:38 +08:00
ZHU MINCHAO d0dfa93226 第十阶段 stage 2(二):CRA 部分抵押(储备、抵押率、挪用与崩盘线、劫掠补储备)、官方铸造与销毁排队、官方跑路、质押多币种入口 2026-11-05 07:14:38 +08:00
ZHU MINCHAO 3758049dcb 第十阶段 stage 2(一):交易对改为 X/CRC 池,任意币互换经 CRC 两跳路由,币价表改为 CRC 价格 2026-11-05 07:09:05 +08:00
ZHU MINCHAO 75fb6d6b2a ASIC 矿机去掉修理槽:电子元件拖到机器上修 2026-11-05 01:46:34 +08:00
ZHU MINCHAO 59dd3c05bc 第十阶段 stage 1(三):钱包物品化、连接钱包、空投逐个授权、矿机收款地址 2026-11-05 01:37:23 +08:00
ZHU MINCHAO f732304f38 第十阶段 stage 1(二):永续合约单仓位、保证金金额一行、名义价值 2026-11-05 01:25:46 +08:00
ZHU MINCHAO ad60b07ca9 第十阶段 stage 1(一):解锁物品放前台、首页帮助入口、鉴定手册用原版文档模型、涡轮不能作用于矿机 2026-11-05 01:21:35 +08:00
ZHU MINCHAO 00a000e3f9 版本 1.0.0-rc4:Web3 升级放进基础页的一栏 2026-11-04 23:51:36 +08:00
ZHU MINCHAO c4aee5e53b 版本 1.0.0-rc3:手册占位符取值改查表,扫描结果无权限 2026-11-04 23:23:59 +08:00
ZHU MINCHAO e8532f9e1b 版本 1.0.0-rc2:去掉反射,清单不再声明能力 2026-11-04 23:11:11 +08:00
ZHU MINCHAO 3a97e3701b 版本 1.0.0-rc1 2026-11-04 22:33:31 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 8147ace455 更新日志:旧存档兼容与 20 天长局、存档往返验证
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 22:29:11 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 c10cf1ea19 旧存档的假卡补上风扇数、供电口破绽
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 22:24:19 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 39ab7822ba 鉴定指南与检视面板:各语言排版核对与修正、示例数字、去掉卖家说法文字、波兰语挡板误译
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 21:50:35 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 564bf2d588 重译矿架升级提示与指出页;矿架状态标签校对;清理旧键
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 20:34:55 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9ab1e3cbf8 解锁时矿架放进机器区;在干净存档走通三项 Web3 升级
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 20:26:28 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 6d9725e825 测试按钮:清桌面、储藏区与机器区各去掉一半
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 20:20:43 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 66b01d5111 矿架改回 4×4(5×4 放不进任何容器);测试按钮清柜台和一半储物箱
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 20:12:00 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 7d89ccfeac 矿架升级:给原版电子元件加上作用于矿架的判断,5 槽流程走通
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:59:55 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 ed7d2bd088 矿架 5×4、拖电子元件升级、状态面板;型号鉴定统一为一个指出型号按钮
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:50:46 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f40436b7d2 鉴定改版:风扇数、供电口、丝印、序列号四项真伪依据在前,假卡依据各自独立(平均 1.5 项);指南去档次、去卖家说法
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:40:50 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 a8d7bc3427 检视面板显示标签价;鉴定指南五个示例;展示样例排队按钮
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:30:15 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 55e7640390 显卡鉴定指南改为原版纸质指南样式(9 页、插图、折价标注、鉴定示例),8 种语言
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 18:56:24 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 2acdf05977 二手贩子的卡按卖家调整健康度:成色问题项数 0.5 / 1.5 / 2.5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 18:20:54 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9f93c3f3cf 显卡鉴定手册:8 页阅读窗口、8 种语言;外观折价进 Tuning
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 18:13:08 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 6f121a6899 鉴定样例测试按钮(展示页用)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 18:03:24 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 5745b5a9c6 鉴定弹窗:型号真伪改为型号,去掉指出前的依据提示
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 17:21:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 7f7891cb85 多语言第二轮审校(子代理交叉复审)、首尾空格拼接修复、GPU 检视行名缩字
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 17:20:05 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e500dd263c 多语言审校:采用 Gemini 审校建议 116 条;波兰语空投页标题
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 16:52:50 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 06c6e0f855 大窗口钱包表:列宽与按钮区重新分配,各语言不再截断
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 16:23:41 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f3b7c14363 大窗口下钱包表名称与污点列加宽
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 15:53:07 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 861acac240 终端窗口可调大小(默认 1200×900)与布局放宽;mod 设置:界面、剧情、难度
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 15:34:38 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 fa8f416545 多语言布局:自动缩字、按语言放宽钱包表与手册列表、缩短过长标签
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 14:55:11 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f5de68aea9 多语言:de fr es pl ru ja 六种语言表;匿名币改称隐私币;英文手册数字改占位符
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 14:26:21 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 0520895b18 卖货页与订单页分栏,永续表头列宽,手册合并为 16 章
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 13:37:54 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 cfe598d2fc 文案审查:中文界面与手册改为中立陈述
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 13:16:14 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 548d104b5b 写死的数字进 Tuning;手册剧情联动等用占位符
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 12:52:43 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d28ca4e50c 手册数值改为占位符,渲染时从 Tuning 取值;加规则文件和测试
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 12:45:29 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 38f9ea9daf 手册章节行高 30,选中行文字不再被截
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 12:40:05 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 40d37a4544 手册加 PSC 全称说明
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 12:36:37 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d21dcebd18 Web3 手册改为章节阅读窗口,修复手册文本引号转义
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 12:23:16 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 80aa82c8f0 ui: shorter seller labels in the shop list; verification notes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 11:50:23 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 00182c4ce6 ui: checkout pays seller by seller only (drop unified checkout), per-seller auto-convert
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 11:25:06 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 2d40df81bd ui: Web3 shop rename, merchant info header, shopping cart with floating button and two-column checkout
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 11:18:45 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9d24840561 ui: black market buy page with seller list, payment panel, per-merchant accepted coins and auto-convert; wallet row keys beside the data cells
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 11:06:38 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 12a63c562f ui: rounded touch keys, vertically centered row keys, right-aligned connection status on the wallets page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:59:06 +08:00
ZHU MINCHAO 8645298b41 Merge agy/ui round 2 2026-11-04 10:38:06 +08:00
ZHU MINCHAO 431111a857 ui: fix long label clipping in intel tips and rebalance table column widths 2026-11-04 10:34:49 +08:00
ZHU MINCHAO 7ba1381cf0 ui: remove aml withdrawal quota and hit chance from bank page preview 2026-11-04 10:34:06 +08:00
ZHU MINCHAO a5ff9418ab ui: invert child action buttons in selected table rows to light on dark 2026-11-04 10:33:20 +08:00
ZHU MINCHAO 4bf7e58b36 ui: slim down vertical scrollbars to 6px dark ink handles with transparent tracks 2026-11-04 10:32:52 +08:00
ZHU MINCHAO 9f0bc5d180 ui: style all text inputs as light e-ink grooves with dark borders 2026-11-04 10:32:20 +08:00
ZHU MINCHAO 6cc546cd67 ui: stack bank deposit and withdraw buttons vertically to fit labels 2026-11-04 10:31:09 +08:00
ZHU MINCHAO 58bf83d08e ui: avoid alpha 0 on images with Mask dependency across PledgerSkin 2026-11-04 10:30:47 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 5486a9c033 ui: keep the scroll viewport mask visible (alpha 0 clipped all content)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:17:32 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 05f1e056fd ui: keep the scroll viewport mask visible (alpha 0 clipped all content)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:16:53 +08:00
ZHU MINCHAO dbaaefdd7e Merge agy/ui (trial) 2026-11-04 10:13:31 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 76d97706a6 Starter economy: stronger low-end cards; handbook: withdrawal screening rules and wallet destroy; revert wallet-destroy guard
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:13:16 +08:00
ZHU MINCHAO 66a8f6e903 ui: skin black market buy, sell, orders and pay pages 2026-11-04 10:10:34 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f2ee57c89a Tuning: gas market blind weight 0.15 (financial review)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:09:57 +08:00
ZHU MINCHAO 6b7919c476 fin: increase SideArbRate from 0.5 to 0.75 to accelerate secondary pool convergence and mitigate AMM lag arbitrage 2026-11-04 10:09:35 +08:00
ZHU MINCHAO 039669961c fin: adjust CrcHaircut from 0.95 to 0.85 to introduce real downside risk to depeg arbitrage 2026-11-04 10:09:35 +08:00
ZHU MINCHAO 96be937daa ui: skin web3 airdrop, predict, intel and mixer pages 2026-11-04 10:06:07 +08:00
ZHU MINCHAO b217620d40 ui: skin web3 trade, stake and ponzi pages 2026-11-04 10:04:00 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 4501807cd2 Fix wallet destroy fund loss, Tumbler progress timing, settle idempotence, bounds guards (from external review)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:02:02 +08:00
ZHU MINCHAO 68a53f6097 ui: skin poinbase spot and perp pages with crosshair pickers and right-aligned tables 2026-11-04 10:01:41 +08:00
ZHU MINCHAO 698849c112 ui: skin wallets page with right-aligned list and action buttons 2026-11-04 10:00:25 +08:00
ZHU MINCHAO 37c5362913 ui: skin bank page with right-aligned table and touch ops 2026-11-04 09:59:45 +08:00
ZHU MINCHAO 6926f994fd ui: reskin terminal window shell, navigation tabs, and home page 2026-11-04 09:59:03 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d0054443ec Final verification pass: forged transfers, insider catch, rug pool, funding, news; debug helpers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 09:58:18 +08:00
ZHU MINCHAO d7d2fec536 ui: extract PledgerSkin common visual components and refactor WalletWindow 2026-11-04 09:57:18 +08:00
ZHU MINCHAOandClaude Opus 5.5 fb29b61a6a UI copy: plain noun labels and declarative messages for yield, predict, intel, airdrop, chart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:38:26 +08:00
ZHU MINCHAOandClaude Opus 5.5 5c7e7bf360 Merge third-party yield pools and own scheme into one Yield page with two sub-tabs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:32:59 +08:00
ZHU MINCHAOandClaude Opus 5.5 f8bbf295e3 Verification pass: crimes, reputation, approvals, timelock, English UI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:27:57 +08:00
ZHU MINCHAOandClaude Opus 5.5 fe4a2399ba Mixer status shows shut down after Tumbler is reported; crime/reputation debug dump
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:23:48 +08:00
ZHU MINCHAOandClaude Opus 5.5 c2f12620f9 CHANGELOG: 9.3 economy review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:14:22 +08:00
ZHU MINCHAOandClaude Opus 5.5 a5f9639970 Stage 9.2: mixer operator Tumbler storyline with key drive item
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:12:47 +08:00
ZHU MINCHAOandClaude Opus 5.5 62bd641283 Stage 9.1: story links (Serpico, Eleanor, Officer Lun) and newspaper importance fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:04:50 +08:00
ZHU MINCHAOandClaude Opus 5.5 96e91a4884 Candlestick chart: price axis, last-price tag, volume max and day labels in a pixel font
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:54:39 +08:00
ZHU MINCHAOandClaude Opus 5.5 e1390a6921 Candles and volume for every traded asset, chart asset switcher
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:51:31 +08:00
ZHU MINCHAOandClaude Opus 5.5 b8308e1010 PSC daily candle history and pixel candlestick chart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:47:34 +08:00
ZHU MINCHAOandClaude Opus 5.5 7b5080984a Stage 7 numeric pass and market legibility (price moves, history, insider exits)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:36:17 +08:00
ZHU MINCHAOandClaude Opus 5.5 6c5db17758 Stage 7.3: NPC scams (wicks, phishing airdrops, rug pools, fake support, forged transfers)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:28:43 +08:00
ZHU MINCHAOandClaude Opus 5.5 4279d48374 Ponzi via signed wallet calls; counter informants guarantee prediction markets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:16:57 +08:00
ZHU MINCHAO 9484de2f1f CHANGELOG heading 2026-11-04 08:11:07 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 5b0af9af8f Stage 7.2: high-yield scheme (Ponzi), prediction/manipulation docs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 08:11:03 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 786de325da Stage 7.2: manipulation detection (player price push, prediction/perp forfeits)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 08:03:44 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 002606056d Prediction markets: news reaction, clues, gas/CRA/era markets, intel broker, bigger stakes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 07:53:12 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 ca3230e7d2 Prediction UI: show cost and payout instead of share counts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 07:39:03 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 723724aa4b Prediction markets use on-chain wallets (signed contract calls) instead of the Poinbase account
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 07:33:42 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e5a6e3e22c Stage 7.1: prediction markets (LMSR, event/faction/purity/price markets, insider check)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 07:26:27 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 7d13ae2fe8 Stage 6.3: RatDigg ASIC miners, miner dealers on the black market, miner era event
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 00:58:14 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 b249747855 Stage 6.2: Poinbase perpetual futures (PSC, SPARK) with slippage, funding, insurance fund, ADL, debt and seizure
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 00:26:50 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 98e80412e6 Stage 6.1: CRA staking pool, SPARK balance token with mint/burn
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 00:06:16 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 58dd93eca6 Stage 5 wrap-up testing; debug: give terminal, unlock mixer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 23:55:37 +08:00
ZHU MINCHAOandClaude Opus 5.5 8e585393a7 Flagged status follows traceable transfers; wallet split review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:36:20 +08:00
ZHU MINCHAOandClaude Opus 5.5 50c119697e Gas price index on a 7-day cycle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:32:50 +08:00
ZHU MINCHAOandClaude Opus 5.5 dbba8b4376 Chain drama: fee tiers, congestion, mempool drops, address poisoning, clipboard hijack, phishing; selected-button style
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:29:54 +08:00
ZHU MINCHAOandClaude Opus 5.5 2b781acd82 Wallets: expected-days drain, copy/paste, three-column selectable balances, rename and destroy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:05:13 +08:00
ZHU MINCHAOandClaude Opus 5.5 783f73989d Multiple hardware wallets: per-wallet devices, wallets page, Poinbase custodial account, PLEDGER item with supply-chain poisoning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 22:58:24 +08:00
ZHU MINCHAOandClaude Opus 5.5 add61a6b5e Hardware wallet window: invoices paid from PLEDGER, checksummed addresses, DEX signing, block confirmation animation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 22:29:42 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 5b7013f240 Black market tabs: Buy / Sell / Orders; Ponero swap moved into the Web3 trade page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 22:04:31 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 776f66fd2c Price entry for listings with tiers as helpers; exclude storage containers from selling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:58:19 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 ce02773738 Move black market random-detail constants into Tuning
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:53:00 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 a44e4008c1 Reprice listed goods; honeypot stays at 1%
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:50:26 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 a76f04c75e Trap grouping and caps after economy simulation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:44:19 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 205c321840 Cancel honeypot traces for orders that were never delivered
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:41:12 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 2d3302a2f7 Type the payee address before releasing payments in the Web3 page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:31:50 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 4645c5a97d Merchant hash names get a 0x prefix
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:26:54 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 4422bd99ab Black market: anonymous hash merchant names with type tag
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:25:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 96d8b7cf3b Black market: elite shop for expensive goods, private sellers, weekly catalog value ~20k
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:12:35 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 61ec91b34d Black market: 18 merchant types from the game's items and player professions
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 21:02:54 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 378203d04d Limit active listings to 10
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:54:17 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d6b344f70b Black market merchant shelves, bulk discount, bulk merchants, volume-driven taint, list-all
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:50:58 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 d35425933c Ponero fee 6%/cap 1000, mixer fee 3%, brand-new GPUs on the black market
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:38:22 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 45a5bfba37 Stage 4 wrap-up: Ponero fee 10%/cap 600, live crackdown check, verification notes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:35:28 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 cc4d570b52 Sell page: drop explanatory info line
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:25:06 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 502366e7c4 Black market: useful goods only, player selling with price-dependent speed and taint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:23:21 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 5b57f3eedc Stage 4: honeypot, seller busts, controlled delivery; mining report moved into Web3 page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:13:18 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e44a4439ed Black market in Web3 page, shelf UI, no dead drop, tech and GPU goods; rules moved to Web3 handbook
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 20:06:32 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 6bc479f710 Stage 4: black market orders, escrow, delivery, seller scams, platform rug
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 19:48:26 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f43cd487e5 Stage 4: Web3 black market spot counter and Ponero desk
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 19:35:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 74f0dac57e Shorten coin-payment deal line so the sale panel does not hide it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 19:11:19 +08:00
ZHU MINCHAO e362551501 Changelog: in-game verification of coin premium 2026-11-03 18:49:43 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 b28a03f565 Fold scattered constants into Tuning
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 18:36:01 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 81b42ac2d3 Rename GPU models: RZX→RTX, RZ→RX
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 18:34:57 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 7255befa01 Stage 3: block reward 100, random halving pass-through, RZ800 14kJ, low-tier fakes, tier-mismatch always spotted
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 18:20:30 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 c9dd900b16 Coin payment premium 12%
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 18:11:20 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 adda3a4e3a Tuning: mixer fee 4%, higher coin-payment chances
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 18:03:34 +08:00
ZHU MINCHAO 4bb1484b4e Move unlock prices into Tuning 2026-11-03 17:31:12 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f14d00e93e Test entries: morning report and newspaper; verified night log and news in game
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 17:30:06 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 4c23d3559d Mixer fee 6% per round
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 17:21:15 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 ad12918511 Test entries for running nights and crackdowns; night logging; changelog with in-game verification
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 17:20:08 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 c295e72a48 Phase 2: CRT freeze during crackdowns (frozen balance, thaw, news, test entry)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:51:12 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 be18078f69 Mixer moves into the terminal's Web3 page as a sub-page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:43:30 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 09a5cae69c Phase 2: mixer (Web3 tab in the network, terminal page, sanctions, night return), unlock registration per upgrade
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:39:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 133367b58d Coin buyer test entry: mark seeds as stolen to exercise taint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:25:38 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 b24dd8b9f2 Coin customers: buyer says the coin amount on deal; test entry spawns buyer through the bell queue
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:20:58 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 090a973d23 Phase 2: customers paying in coin (CRC/CRT/CRA/PSC), taint from stolen goods
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 16:01:20 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 1ba0652705 Phase 1 cleanup: stale unlock text, unused code, README layout
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 15:46:47 +08:00
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
ZHU MINCHAOandClaude Sonnet 5.5 7f458b0139 Tiers drive physical traits and serial checks; guide item
Fans (1/2/3/3) and power connectors (6/8/8/8+8) follow the real chip's tier,
so a fake shows its real tier physically. Serial numbers (brand prefix, tier
code, factory code, four digits) are drawn on the sticker from card state;
the label bar gets power-and-fans and serial rows, eight in total, with a
framed panel. A graphics card appraisal guide item holds the rules.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:18:01 +08:00
ZHU MINCHAO 9d19df407a Labels: eight rows (model, build, fan blades, contacts, bracket, print, serial, seal); serial text rules by brand and tier 2026-11-03 14:07:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 202bdbbfbc Four tiers per brand: RZX 60/70/80/90 and RZ 70/80/800/900
Model name is display only; performance and price follow brand and tier.
Tier physical traits: power pins 6/8/8/8+8 and fans 1/2/3/3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 14:05:47 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 0ba2845609 Inspection: hide the tray's own magnifier icon for cards; fill the gaps between back gold fingers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:55:22 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 fe72ec0577 Inspection tray: framed label panel with bright text, flip button at the bottom
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:53:56 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 b359a00789 Label bar in the magnifier tray: seller's claims per label, point out, edit with a labeler
Six labels (model, fan, contacts, bracket, back print, sticker). Dealers
state claims (honest tell the truth, middlemen hide 70%, scammers hide
everything and name the stamped model); the price follows claims until a
label is pointed out, then follows the real state. Pointing at a fake's
evidence exposes the model label too. A wrong accusation costs reputation.
Owned cards can have claims edited freely with a labeler present; a buyer
spots each hidden flaw with 35% chance, which exposes it and costs
reputation. Dimensions are additive percent adjustments on the model price.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:50:24 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 f93dbc6eec Cards: appearance follows wear, hash depends on fan and contacts, seller confrontation
Fan, contact and scratch levels are recomputed from health and a per-card
seed, so a card looks older as it wears. Fan and contacts scale hashrate.
Each inspection-tray detail now maps to exactly one dimension value. Quoting
dealers price the card as if its condition were fine; the player points out
fan, contacts, bracket, print or sticker in the tray to take the matching
discount or expose a fake (a wrong accusation costs reputation like the
vanilla confrontation). Unowned cards show a coarse grade and rated specs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:37:48 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 68265095a8 Cards: coarse condition grade in the description until the card is owned
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:23:54 +08:00
ZHU MINCHAO 821eb1b960 Appraisal test helpers: put a client with appraisal cards in front of the counter 2026-11-03 13:20:11 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 2be4c90c8e GPU art: drop the diagonal stub at the end of the accent stripe; appraisal test helpers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:11:41 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 6b2d394023 Inspection overlays designed by agy; appearance dimensions tied to value, wear and death
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 13:04:45 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 dd95aad7aa Dedicated large inspection art for graphics cards
192x128 drawings made for the magnifier tray instead of enlarging the
48x32 item sprite: nine-blade fans, fin stack, heat pipes, power connector,
PCB parts, per-contact gold fingers; back with brushed backplate, QR and
barcode sticker, seal. Appearance overlays share coordinates with the base.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:21:40 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 c05fe41ca9 Magnifier inspection view for graphics cards
Cards are inspectable. While the magnifier inspection tray shows a card, a
mod-owned panel under the tray draws the card enlarged with its appearance
overlays and a turn-over button. No Harmony patches: a resident component
watches the inspection manager.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:08:51 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 e643332a41 GPU art: redraw front and back with real detail
Six-blade fans with rim, air duct and hub, graphite shroud with fin stack,
copper heat-pipe ends and an 8-pin connector, PCB with parts and per-contact
gold fingers; back has brushed backplate, vents, screws, recessed silkscreen
plate, QR/barcode sticker and a seal. Overlays realigned.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:04:08 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 de7d5caafe Used cards: counter quote tag instead of client-temporary feature; item type Material
Client-temporary features are stripped when an item sits on the front
counter, so the scam markup never applied. The quote now lives in a tag and
counts only while the item is not owned. GPU item type ELECTRONIC has no
translation in the game, so cards are typed as Material.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 12:00:39 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 9b48d7bf0e Used-hardware dealer customer and GPU appraisal registration
A scav-type seller visits now and then with three to five used cards. An
honest dealer sells at true value, a middleman adds 8%, a scammer mixes in
fakes (relabelled cheap chips) priced at 90% of the labelled model's value.
The markup is a client-temporary item feature, so a bought card is only
worth its real value. Cards are registered as appraisal items; the correct
value is the same function as the sale value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 11:40:10 +08:00
ZHU MINCHAOandClaude Sonnet 5.5 42608a4acd Cards: 3x2 front/back sprites, appearance overlays, flip via the Toggle menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 11:26:11 +08:00
232 changed files with 27112 additions and 489 deletions

No files matched your search

+449 -3
View File
@@ -1,10 +1,456 @@
# Changelog
## 未发布
## 2.0.0
污点规则重做(按币种记脏币,只有干净的币能进 Poinbase 和银行,出金审查删除)、OTC Desk 场外换币台、自营混币器、Tumbler 支线扩展、挖矿去模块上限。存档兼容,读旧存档时自动升级。
### 内幕与操纵、挖矿
- 预测市场的内幕单不再按概率查处、没收:结算或卖出时照常兑付,内幕份额对应的兑付算脏币,夜间日志写明“警方怀疑这是内幕交易”。原来“内幕持仓达到上限的 40%、70% 概率被查”的规则(`PredictInsiderShare`、`PredictInsiderCatch`)删除,每一份内幕份额都算脏币,先买后卖也一样。
- 操纵检出:被查的概率不变,被查后照常兑付,预测盘的这笔兑付算脏币,永续的这笔盈利记诈骗罪证(见下面的污点一节);永续账户不再冻结 3 天(`ManipFreezeDays` 删除)。报纸的调查新闻改为“涉事账户已被标记”。
- 矿架、矿机的模块效果不设上限(原来算力最多 +20%、省电最多 25%、损耗最多 −30%,以及耗电最多 ×2 的隐含限制):9 个无限制神经核心算力 ×5.5、耗电 ×3.25、暴毙率 ×5.5。只保留防出错的下限:算力乘数 ≥ 0、耗电乘数 ≥ 0.2、损耗乘数 ≥ 0。
- 出块奖励 100 → 1000 PSC,NPC 初始算力 360 → 3600:均衡时每 H 收入不变(开局 1 H 每晚仍约 26.9 信用点),全网算力放大,玩家算力占比变低。模拟:普通玩法第 30 天挖矿占比 19% → 21%,第 120 天玩家算力占比 33% → 约 2%;大矿工收入明显变高。
- 修正:内幕卖出的日志原来写“只退本金”,实际是全部没收。
- 手册对应段落 8 种语言更新。
- 涡轮增压器可以对矿架和矿机用(原来被挡掉),规则和原版机器一样:用一次就多跑一轮,也就是立刻挖一晚——扣一晚的电,产出按当前全网算力计算、发到这台机器的收款地址,同样算一次损耗和暴毙判定,不影响当晚照常挖矿。增压器本身的消耗、充电、可用次数照原版。
### OTC Desk 场外换币台
- 新人脉升级“场外换币台”(500,前提是 Web3 治理社区),附赠一盏暗灯。原版暗灯双击切换目标时多一个“换币客户”(解锁后才有),放进展示柜亮黄绿色的光;招客概率、顾客面板、灯的提示都沿用原版暗灯(每盏 15% + 黑市声望的一半,封顶 95%)。
- 换币客户一半来卖币、一半来买币,都用现金。客户说明来意后选“到终端上谈”,交易终端打开柜台交易页(标签“柜台”),客户在柜台前等着;可以按 25% / 50% / 75% / 100% 或手输数量只成交一部分,现金按比例折算,客户要的数量随房租增长。选“放弃”或把客户打发走就作废。
- 卖币(原来的收黑币并进来,不再单独上门):成交后付现金,币直接进已连接的钱包。帮派火并、镇压期间多是 CRT,袭击上层、下层动荡期间多是 PSC,这类币全额污点;平时在 PSC、CRT、CRA 里随机,污点占市值 20%–90%,要价从市值的 80% 降到 45%。交易页上有客户地址,“查地址”打开浏览器看它的来历(混币器、黑市平台、骗局地址或 Poinbase、DEX)。Ponero 兑换处开放后偶尔有人出手 Ponero;便衣规则不变。
- 买币:什么币都可能要,数量按房租 2–8 倍,出价是市价加一个随机溢价(−5% 到 +30%),成不成交由玩家决定。成交后 PLEDGER 弹出,收款地址和数量已填好,签名确认后客户付现金离开。卖出的币不追查污点。换币账单不独占终端的 Web3、灰网页,也不会被换币、混币这些终端账单覆盖,可以先在 DEX 换出客户要的币再成交。
- 浏览器里换币客户的地址显示为“换币客户”。
- 存档数据版本 4:读旧存档时出块奖励和 NPC 算力按 stage 1 的新数值放大 10 倍(保留减半进度)。
- 手册“收黑币”一节改写为“换币客户”,8 种语言。
### 自营混币器
- 买下场外换币台就开放,灰网页多一个“自营混币器”标签。注资(PSC、CRC、CRT、CRA,账单转账)后开张,NPC 客户把带污点的币存进来,几天后出款,你收手续费;费率可选 1% / 2% / 3% / 5% / 8% / 12%,可以暂停营业。
- 和普通混币器一样,每一轮按匿名集洗掉一部分污点;匿名集主要是你的本金和没取走的收入(客户在途的币按 25% 算),每轮清洗率 = 35% × 匿名集 / (匿名集 + 20,000)。客户拿“费率 + 出款剩下的污点 × 0.1”和 Tumbler 的混币器比,便宜就多来,黑市声望越高客人越多(Tumbler 关停或被制裁时客人更多)。
- 手续费收入留在池子里,取回时先取收入(全额带污点),再取本金(带本金自己的污点)。客户在途的币不能取。
- 风险:风声随客流累积(池子大能藏得深一点:累积量除以 1 + 匿名集 / 160,000,但客流也大,总体大池子更容易被盯上)、每晚衰减,按风声有概率被治安部查封(本金、收入和客户的币全部没收,记销赃罪证 = 池子价值的 1%、封顶 400,停业 7 天,上报纸);营业的晚上有 3% 概率来一笔便衣存入(记销赃罪证)。随时可以卷款跑路(点两次确认、签名):连同客户的币全部卷走、全额带污点,记诈骗罪证 400、黑市声望 −25,混币器永久关闭。
- 模拟(research/mixsim out_v3,费率 3%,第 61–90 天):注资 1 万每天手续费约 316、2 万约 574、4 万约 807、8 万约 991;90 天被查封 1 万 0.12 次、2 万 0.21 次、4 万和 8 万 0.35 次,净收益随本金增长、增速递减(1 万约 2.8 万,8 万约 6.3 万)。
- 手册“换币客户”一节加一段自营混币器,8 种语言;夜间日志和报纸新闻。
### Tumbler 支线扩展
- 治安部探员:Tumbler 第 2 次上门之后的第二天起来店里问一次,他来过之后 Tumbler 才来第 3 次。承认 Tumbler 来过:治安部声望 +5;否认:没有影响。承认过又帮他藏了密钥盘的,探员在 Tumbler 来取的前一天上门搜查,搜到就没收、记违禁品罪证 300,Tumbler 第 4 次来时说他听说了。
- 密钥落到治安部(举报、被搜走、Tumbler 来取时盘不在手上):混币记录被还原,按最近 30 天混币器出款洗掉的污点记诈骗罪证(× 50%,单次最多 400);夜间日志和报纸。卖给黑市不算。
- 举报之后有审判:第 2 天开一个“Tumbler 被判有罪”的预测盘(先验 70%),第 7 天宣判;报纸每天登一条庭审进展,偏向控方或辩方,七成偏向真实判决,盘口的价格跟着新闻走,线索列出两边各占上风几天。无罪获释的话,3 天后他会报复。
- 卖给黑市的:黑市在 10–20 天后卷款跑路,混币器里在途的币全部丢失,混币器关停;3 天后 Tumbler 报复。
- 报复:门缝里塞进来一张纸条,之后一半可能是当前钱包被植入木马(和商城的投毒钱包一样:剪贴板劫持,钱够多以后被转空),否则每个钱包都会收到伪装成最近转账对象的 0 币转入(地址投毒)。
- 帮他藏好并还给他:Tumbler 成为长期联系人,治安部镇压开始的前一晚在夜间日志里报信。
- 混币器关停 15 天后换名字(Vortex)、换地址重新上线,费率和跑路概率回到原样;有 25% 是治安部的钓鱼混币器,从它提回的币整笔算脏币,费率只有一半。
- 卖给黑市那笔 CRT 的污点比例收进配置(`TumblerSellTaint`)。
- 修正:Tumbler 每次上门的外观原来是随机的,现在第一次上门定下后一直不变。
- 手册剧情联动一节补一段,8 种语言。
### 污点:按币种记脏币,删除出金审查
- 污点只在 Web3 钱包里,每种币分别记脏币数量,钱包页、PLEDGER 的余额里能看到;钱包总表的“脏币”列按现价显示。原来整个钱包记一笔信用点的污点不再使用。
- 花掉、转出、付手续费时按这种币的脏币比例带走脏币;兑换成别的币时脏币比例跟过去(60% 是脏币的 CRT 换成 PSC,PSC 也是 60% 脏币);兑入 Ponero 时脏币消失,Ponero 是干净的。
- Poinbase 只收干净的币:钱包往充值地址每种币最多转干净的部分,超出时 PLEDGER 拦下并写明能充多少;资金页“从钱包入金”也列出每种币能充多少。Poinbase 账户和银行没有污点。
- 出金审查整套删除:出金到银行不再有被查概率、没收和销赃罪证,只收手续费 0.1%、对超出成本的部分征税 8%。从 Ponero 兑出、刚从混币器提出的币也不再“出金加重”。
- 带污点的收入都改成“收到的这批币里有多少脏币”:换币客户卖来的币、用币付款的顾客(按赃物热度)、灰网卖赃物的货款、预测盘内幕单和操纵的兑付、理财卷款、自营混币器的收入和卷款、卖密钥盘换来的 CRT。“加污点不能超过钱包价值、超出部分记诈骗罪证”的规则随之删除。
- 永续被判操纵:盈利照常留在账户,只记诈骗罪证(盈利 × 50%,单次最多 400),不再加污点。
- 混币器:存入时带走这种币的脏币,每轮减少;制裁期间到期的出款、从钓鱼混币器提出的币不洗,整笔算脏币。
- Tumbler 的密钥泄露:不再往钱包里加回污点,改为按最近 30 天混币器出款洗掉的污点记诈骗罪证(× 50%,单次最多 400)。
- 剧情联动:塞尔皮科的证据交给治安部,原来的“出金审查更严”换成 30 天内混币器每轮清洗率 ×0.7;伦警官被捕,原来的“出金审查更严”换成 14 天内换币客户里的便衣 ×1.3。
- 存档数据版本 5:读旧档时,每个钱包原来的污点按当前价格摊成各币的脏币;Poinbase 账户和永续仓位的污点挪到 1 号钱包一起摊(超出的作废);混币器、黑市订单、质押、空投池、预测持仓、自营混币器本金里托管的污点换成对应币的脏币。
- 模拟(research/launder):混币器小额 1–3 轮洗钱成本 10%–15%,Ponero 约 12%、每天约 500,买币的换币客户是负成本但受客流限制;数值都不用改。
- 手册“出金审查”一节改写为“污点”,相关段落 8 种语言同步改。
## 1.1.0
市场与洗钱的平衡调整:Poinbase 改为订单簿、DEX 稳定币对改为稳定币池、保险基金和爆仓新闻、预测市场流动性、收黑币、浏览器记录钱包的每一笔进出。存档兼容,读旧存档时自动升级。
- Poinbase 现货改为订单簿:价格只取决于当天的累计净买入量,价格 = 参考价 × exp(1.54 × |净买入名义 / 深度|^0.8),冲击随单子变大而增长得比线性慢;拆单不占便宜,同量买进再卖出回到原价。第 38 天买 2 PSC 的平均滑点约 0.1%(原来约 0.24%),卖 200 PSC 约 4%(原来约 21%)。基础深度 50,000 → 600,000,DEX 侧池仍是恒定乘积池,按比例跟着变深;现货与出金手续费 0.2% → 0.1%。NPC 大户每笔 2,000–8,000 → 24,000–96,000,对价格的影响和原来相近。
- Poinbase 只上架 PSC、CRC、SPARK:作为合规交易所不收黑市的 CRT 和革命军的 CRA,往充值地址转这两种币时钱包拦下(“Poinbase 不收 CRT。”);资金页入金只列这三种,出金另外列出账户里还有余额的其他币(旧存档里已经充进来的)。现货新增 SPARK/CRC 订单簿(深度为主池的一半),行情图只有 PSC、SPARK、CRC,右边的交易对跟着选中的币切换。交易对一行不再写“(Poinbase)”,互换预览的损耗显示两位小数。
- DEX 稳定币对(CRT/CRC、CRA/CRC)改为 StableSwap 池(Curve 的曲线):手续费 0.3% → 0.04%(参考 Curve 3pool;Uniswap v3 稳定币对用 0.01% / 0.05% 档),放大系数 200,挂钩附近几乎没有滑点。第 38 天 998 CRT 换 CRC 损耗 0.041%(1.0.0 约 4%),5 万约 0.08%;998 CRT 换 PSC 约 0.68%(PSC 池那一跳)。参考价偏离挂钩超过 1% 为脱锚,放大系数降到 20(脱锚到 0.92 时卖 5 万约 0.44%);超过 15% 为崩盘,放大系数 1、深度只剩四分之一(CRA 跌到 0.3 时卖 5 万约 8.7%)。脱锚幅度参考 USDC 2023 年 3 月(最低约 0.87,两三天回到 1)。
- 永续的成交深度单独计算,基数 50,000 → 100,000,滑点减半(perpsim:均值回归策略 3,000 本金 120 天 4,261 → 5,467)。
- Poinbase 交易界面:买入和卖出都输入标的币数量,标签“数量(PSC)”;买入预览改为“约需 X CRC”;交易对一行显示手续费;均价含手续费,滑点不含手续费。
- 保险基金:初值 10,000 → 200,000(约 20 天的全网产出)。去掉“每晚固定补 150、NPC 大行情按冲击平方扣”的旧规则,改为 NPC 仓位随行情爆仓:亏损一侧的持仓为现货池深的 1 倍(SPARK 再乘 0.15),涨跌幅达到 25% 全部爆仓,之前按比例;大部分正常爆仓,剩下的保证金(名义价值的 0.5%)进基金;涨跌超过 12% 后穿仓的比例开始上升(5% 起,每多 1% 加 1.5%),缺口由基金垫付。离线模拟(300 局 × 200 天,带插针、事件、SPARK):基金缓慢上升,没有一局见底,最坏的单晚流出约 17 万。
- 爆仓新闻:标记价单日涨跌达到 15% 时,报纸报道全网爆仓的人数和金额(含玩家被强平的仓位)以及保险基金余额;模拟里平均 10 天左右一次。
- 预测市场:流动性与先验脱钩,b = 1.8 × 单盘额度 / ln 2(低先验盘不再更浅:第 38 天在 5% 的盘口押 50,均价从约 +67% 降到约 +10%,50% 的盘口约 +0.5%);份额上限改为 2 × 单盘额度;手续费 2% → 1%。为保持平衡:内幕查处概率 50% → 70%(否则买满上限的期望高于压线),内幕消息价格 1% / 2.5% / 5% → 2% / 5% / 12% 的单盘额度(predsim:高档消息押 1,000 在 50% 盘口平均 +335,原来 +338)。旧存档里的盘口按原来的额度计算。
- 收黑币:帮派火并、镇压期间有人上门折价出手 CRT,革命军袭击上层、下层动荡期间是 PSC(每天 35%),Ponero 兑换处开放后偶尔有人出手 Ponero(每天 5%)。只收现金,币进已连接的钱包;市值为房租的 4–15 倍,PSC、CRT 要价是市值的 45–60%,按市值全额带污点;Ponero 要价 70–80%,不带污点。卖家有 5% 是便衣(镇压期间 15%):收下就没收现金并记销赃罪证。开发者模式 F9 有测试入口(Hot coin seller)。
- 灰网出售:赃物的货款按“货款 × 赃物热度%”带污点,不再随机。
- 人脉升级降价:混币器 700 → 300,Web3 黑市 1,500 → 600。
- 手册:交易数量的说明、Poinbase 收的币和交易对、稳定币池、NPC 爆仓与爆仓新闻、保险基金见底的条件、赃物出售的污点、收黑币一节、浏览器显示的范围,8 种语言。
- 浏览器:钱包的每一笔进出都记账(统一的记账函数,同时写浏览器和钱包自己的转账记录)。原来只有钱包签名转账、Poinbase 出金和矿机收益,现在还有 DEX 兑换(含黑市自动换币)、质押与解锁、CRA 铸造与销毁、预测盘下注与兑付、情报费、空投领取与钓鱼转走、收益池存取、高息理财、黑市货款放款与退款、混币器提回、Ponero 兑换处换出的币、Tumbler 的谢礼和卖密钥盘的 CRT、用币付款的顾客、收黑币、投毒钱包被转空、地址投毒的 0 币转入。合约和收款方显示名称(DEX、革命军质押池、官方铸造、预测市场、情报贩子、高息理财、Tumbler、空投与收益池的名字)。Ponero 是隐私币,不上浏览器。账本上限 200 → 500 条。
- 存档数据版本 3:读旧存档时 Poinbase 主池换成订单簿(按当前价格、新手续费),补上 SPARK/CRC 订单簿,DEX 的 CRT、CRA 池换成稳定币池(保持当前价),其他 DEX 池按新深度重设,保险基金补到新初值。
## 1.0.0
正式版:相对 rc11 只有文案审查(见下),功能与 rc11 相同。
- 8 种语言文案审查:核对上次审校之后新增和改过的 215 条与整段手册,统一术语(储备覆盖率、质押、革命军、状态/成色、确认数等)、去掉残留的旧说法、修正语法和不自然的措辞;入口说明补上挖矿、钱包页;删掉重复键 `black.sell.mult`;中文“显卡鉴定手册”统一为“指南”。
## 1.0.0-rc11
- 下单时的预览改成输入时实时更新(不重建页面,输入框不会失焦):Poinbase 现货每个交易对显示买入、卖出的预估得到数量、平均成交价和滑点;永续开仓显示名义价值、预估平均成交价(含滑点)、手续费和强平价;有仓位时显示平仓的预估成交价、手续费、回到账户的 CRC 和盈亏;Web3 互换的预览也改为实时。
## 1.0.0-rc10
- 浏览器:转出、转入分成两列,按列宽显示完整地址和名称,放不下时先去名称、再用缩写,鼠标悬停显示完整地址和名称。
- RatDigg 矿机的状态面板加了算力(GH/s)、耗电和成色(烧毁显示“已烧毁”),和矿架一致。
- 矿机、矿架状态面板里的收款地址在读档、睡觉之后会按标签重新读取,不再显示成“未设置”(地址本来就保存在物品标签里)。
- 操纵检出:免查线 2% → 16%,斜率 6 → 3。推价按对数幅度累计,每晚衰减 15%;16% 约对应第 0 天(池深 50,000)一次现货买入 3,900 信用点,随天数池变深免查额度也跟着涨(第 60 天约 8,000);超线后被查概率 = 25% + 3 × (幅度 − 16%)。
- 市场深度:永续成交深度倍数 3 → 5,永续单边滑点约为原来的 60%(20,000 名义价值、第 0 天约 8%,原来约 13%);现货池基础深度保持 50,000。
## 1.0.0-rc9
- 终端刷新只重建页签栏、滚动内容和消息行,窗口框架和滚动视图保留:滚动位置不再回到顶部(换页或换子页签时才回顶);同一帧里的多次刷新合并成一次。
- 货架付平台费页:标题“支付平台费”,去掉“付款币种”字样。
- 按住 ✓ 签名的时间随 Gas 档位变化:快档 0.6 秒、普通档 1.2 秒、慢档 1.8 秒(`Tuning.TxHoldSecs` × 档位速度倍数)。
## 1.0.0-rc8
- 灰网商店卖货重构:卖货默认是已上架的橱窗(同一商品聚合成一张卡,同一商品倍率相同);“上架更多”进备货页,左边选标价倍率,货架卡片显示聚合库存,用 − / + 调整本次要上架的数量;右下角“货架 N”进付费页,在清单里核对、选币付上架费后才上架。橱窗里每点一次“下架”预备 +1,右下角“确定下架 N”确认,货回储藏区,上架费不退。上架上限改为同时最多 6 种、每种最多 5 件(原来总共 10 件);倍率表不再显示成交天数。
- 购物车按钮只在买货页;订单页只显示在途订单(买入未取、卖出待托管放款);取消改价。
- 收款币种:橱窗页的“收款币种”选愿意接受的币(PSC、CRT、CRA、Ponero,默认只有 CRT)。每接受一种币,每件在架货每晚多一轮成交判定(轮次概率 = 成交概率 × 该币买家权重,顺序随机),货款以命中那一轮的币到账。
- Gas 档位越高,确认越快、进度条走得越快(快档约普通档的一半时间,慢档约一倍半)。
- PLEDGER 区块高度每晚随机自然增长 120–170。
- 钱包页:选中(连接)的钱包下方列出它持有的币和数量。
- 手册出售一节按新流程改写(8 种语言)。
## 1.0.0-rc7
- 终端页签重新分组:一级页签为首页、银行、Poinbase、Web3、挖矿、灰网、钱包。挖矿(原 Web3 的挖矿页)提到一级;混币器和商店合成“灰网”(解锁了任一项才出现,两项都解锁时里面再分两页);浏览器挪进钱包页,和钱包列表并成两个子页;Web3 页剩交易、质押、空投、预测、理财。
- 首页入口卡片增加挖矿、灰网(解锁后出现)、钱包,各有图标(32×32 像素图 ui_mining、ui_gray、ui_wallets);超过四张分两行,卡片和图标缩小。Web3 卡片的说明改为“去中心化交易与质押”。
- “连接钱包”挪到终端顶栏右端,所有页都能连接或断开;没有连接时,Web3、灰网页仍显示提示。
- 手册、混币器和商店的解锁说明按新位置改写(8 种语言),术语表加“灰网”。
## 1.0.0-rc6
- 钱包和资金页的链上手续费改称 Gas 费(界面和 8 种语言手册);手册里混币器一章挪到出金审查之前;空投范围文案改为“不要代币额度”。
## 1.0.0-rc5
### 第十阶段 stage 2:资金页、交易对与部分抵押
- Poinbase 新增“资金”页(默认页,在现货和永续左边):入金和出金,先选币种(PSC、CRC、CRT、CRA、SPARK)。CRC 可选银行账户或钱包,其他币只能走钱包;入金选钱包时显示 Poinbase 的充值地址(现在五种币都收),出金到钱包要填地址(或选自己的钱包),几秒钟确认进度条走完才到账,手续费固定普通档从转出的币里扣,动画走完前币还在账户里。地址格式或校验位不对拒绝,Poinbase 自己的地址和已知的商家、混币器地址拒绝,格式对但没人用的地址币进黑洞。
- 银行页改成只读(现金、房租、CRC 余额);存入和提取挪到资金页的银行账户选项;Poinbase 现货页去掉充值地址和“提到钱包”。
- Web3 交易页:交易对改成 X/CRC 池(PSC、CRT、CRA、SPARK 各一个,SPARK 池比较薄),去掉 PSC/CRT、PSC/CRA;支付币和获得币任选,实际经 CRC 中转(CRC 在一端一个池,否则两个池、两次手续费、滑点叠加),预览显示得到数量、池数和损耗;币价表改成“币种 | CRC 价格”。
- CRA 改成部分抵押:流通量、CRC 储备、抵押率(铸造要求的 CRC 份额);质押利息以无抵押的新 CRA 付出,稀释储备覆盖率,质押越多越快;革命军挪用储备(先有风声,4~7 天)后覆盖率跌破崩盘线进入三天螺旋,没跌破只是按覆盖率折价;劫掠上层把流通量 9% 的 CRC 并入储备。价格由覆盖率算出,不再按剧本脱锚。
- 官方铸造与销毁(质押页):存入 CRC 和 SPARK(比例按抵押率)排 1~2 天铸出 CRA,铸出的直接进质押;销毁换回 CRC 和 SPARK,等待同解锁期,储备不足时 CRC 份额按覆盖率打折;排队单每晚按“系数 × (1 − CRA 价格)”的概率被官方黑掉,整笔丢失(夜间日志提示)。去掉 CRA 与 SPARK 的互换铸销。
- 质押可以存 CRA、CRC、CRT、SPARK:后三种先经 DEX 路由换成 CRA 再质押。
- Web3 新增“浏览器”页签:按地址查转账记录(自己的钱包、出金、矿机收益等真实发生的转账,全局账本最多 200 条);不需要连接钱包。Web3 页签放不下时折成两行。
- 译文校对:铸造 / 销毁沿用术语表(Mint / Burn),页面名与界面一致;空投的“不需要授权”改称“不要代币额度”,避免和钱包授权混淆。
- 数值模拟见运维仓库 research/crasim(Python):兑付 CRC 份额取 min(c, R/S) 才不会因抵押率上调被挤兑抽干;挪用比例 40%–80%;游戏内每 200 天崩盘比例约 46%(无质押)到 66%(质押 10 万),预兆到崩盘平均 5.5 天。
### 第十阶段 stage 1:钱包物品化与小改
- 解锁 Web3 的物品改放前台(终端、硬件钱包、RTX 60、鉴定指南);离店和睡觉前,前台上属于本 mod 的物品自动收进储藏区,不会被游戏清掉。
- 终端首页加第 4 个入口“帮助”,打开 Web3 手册;去掉 Web3 手册这件物品。显卡鉴定手册改用原版鉴定指南的纸张图和“文档”类型。
- 涡轮增压器(三种)不能作用于矿架和 ASIC 矿机(原版涡轮会手动触发夜间结算,等于白天多扣一次电);夜间扣电每台机器每晚只算一次。
- 永续合约:同一时间只有一个仓位,有仓位后只能追加保证金;保证金和数量合并为一行“保证金金额”;“名义”改为“名义价值”。
- 钱包物品化:开局不再自带钱包,解锁送一件硬件钱包,拖到终端上加载(第一个自动连接);钱包页的“销毁”对所有钱包可用,里面的币永久丢失(点两次确认);没有钱包或没有连接时,Web3 页只能看,余额都是 0,用币付款的顾客不再出现。
- Web3 页标签栏最右:已连接显示钱包名、地址和“断开连接”,未连接显示“连接钱包”;和钱包页的“已连接”同一份状态。
- 空投逐个授权:每个空投先“授权钱包”(在钱包上签授权合约,摘要写明授权范围),再“领取”。
- ASIC 矿机去掉修理槽:烧毁后把电子元件(普通、稀有、超科技)直接拖到机器上修,每次收一件,按步骤攒够即修好;不是当前步骤要的元件拖不上去。
- 矿机收款地址:矿架和 ASIC 矿机各自在状态面板里设置收款地址(选自己的钱包或手输),挖矿产出每晚按算力占比发到各自的地址;默认不设置,没设置或地址对应的钱包已销毁,这一份产出丢失(夜间日志会提示)。夜间收益不再退回当前钱包,地址对应的钱包没了就是丢了。
## 1.0.0-rc4
- 人脉网络里 Web3 的三个升级(入口、混币器、黑市)从单独的 WEB3 标签挪进“基础”页,自成一栏(栏名 Web3),像水商升级那样连成一条。
## 1.0.0-rc3
- 去掉反射:游戏的模组扫描把运行时反射算作未声明的权限,rc1 因此被拒绝启用。两处都改掉:电子元件升级矿架时,原版元件工厂改用游戏公开的 `DirectoryMaster.GetFactory` 取得;手册占位符取值(`TuningText`)从反射改成生成的查表(`TuningTable.cs`,单元测试核对它和 `Tuning` 一致)。清单不声明任何能力,扫描结果为无。升级流程重新走通:普通 ×4 解锁第 3 槽,稀有 ×1 第 4 槽,稀有 ×2 第 5 槽。
## 1.0.0-rc1
### 第九阶段:剧情与收尾
- 多语言第二轮审校(逐语言通读 UI 与 16 章手册并交叉复审):采用 de 39、fr 56、es 34、pl 37、ru 45、ja 142 条(ja 里约一半是全角标点、×、〜 等机械统一)。要点:天数不再缩写、Tumbler 称谓与 Key-Drive 统一、手册页签名对齐界面、ASIC 时代统一、修正数处误译(俄语手册首章“卖出”写成用交易币付款、永续/挖矿章、日语质押章“向池子付款”等)。
- 真伪依据改版:检视面板最前面四行是风扇数、供电口、背面丝印、序列号,四项全部符合标称型号才是正品,任何一项不符即为假冒;其后是型号和四项成色。风扇数、供电口各自独立(假卡不再固定两项都对不上),假卡四项各自露出破绽,至少一项,平均约 1.5 项(`Tuning.FakeSignChance`)。去掉检视面板和指南里的“卖家的说法”,该机制只在后台生效。指南里不再用“档次”,改称型号;背面图上画出序列号。
- 矿架改版:图 4×4 格(原 3×3;5×4 放不进任何容器,改回 4×4);去掉升级槽位,改成把电子元件拖到矿架上就投入升级(每次一件,和游戏里别的机器一致);原升级槽位那一栏换成只读的状态面板(显卡数、耗电、算力);不显示显卡磨损外观。
- 修正:电子元件拖到矿架上原来会被元件自己的判断否决(游戏取三方判断的与),改为在原版电子元件(普通、稀有)上追加“作用于矿架”的判断,升级才真正可触发。升级流程用探查命令和真实鼠标拖拽都走通(4 次拖拽解锁第 3 槽):普通 ×4 解锁第 3 槽,稀有 ×1 第 4 槽,稀有 ×2 第 5 槽,满级后不再收;5 张卡放满窗口显示 5/5。
- 解锁“Web3 治理社区”时,矿架优先放进机器区(没有位置才退回原来的送货位置)。在干净存档里走通:人脉网络 Web3 页买下 400 / 700 / 1500 三项,扣 2600,终端、显卡、鉴定指南、Web3 手册到手,矿架在机器区,终端里混币器和商店页签出现。
- 重译:矿架升级提示(拖到矿架上)、鉴定指南“指出”一页(改后的按钮规则);矿架状态面板标签校对(俄语“状态”避开与成色同词,德语、俄语、日语“显卡数”改用各语言已有的显卡词,法语、波兰语算力标签缩短)。删除不再使用的旧键 18 个(档次、依据、旧升级槽位等)。
- 各语言下核对鉴定指南与检视面板的排版:示例页的“最终价值”改为各项折价相加(原来用带种子的卡现算,被健康度重算覆盖,数字对不上);长语言下第 1、3、4、5、6 页的插图按剩余空间自动缩小(原来德语、俄语等超出纸面);小节标题折行时下推;检视面板名称列加宽(德语“Rückseiten-Aufdruck”不再被截),去掉成色行末尾的“(卖家说:…)”;波兰语挡板译成“Zaślepka”(原来误译成“鲱鱼”)。
- 旧存档兼容:外观只有 5、6 位的旧版假卡读取时按旧规则补上风扇数、供电口破绽(档次相同的不补),单元测试覆盖。
- 长局与存档往返验证(探查命令推进,不含顾客流程):在干净存档里真实买下三项升级后推进到第 20 天,夜间结算、挖矿、黑市出售与蜜罐、矿机时代、停电、预测与骗局事件都按日运行,没有异常;存档并重新读档后,钱包价值、污点、币价、报告页、矿架升级进度(3/4)、假卡的 8 位外观都一致。
- 检视面板的型号鉴定只留一个“指出型号”按钮(第一行,型号);风扇数、供电口、背面丝印、序列号四行只读,四项全部符合才是正品。
- 检视面板的型号行右侧显示“标签价”(当前报价,随指出和改标签实时变化),鉴定客户的估价框同步。
- 显卡鉴定指南的鉴定示例由 1 个增至 5 个(成色很差、假冒、轻微使用、全新),共 13 页。
- 二手贩子的卡按卖家调整健康度:老实人 72–97、中间商 55–85、骗子 42–72,平均每张卡成色有问题的项(叶片、金手指、挡板、封条,共 4 项)约 0.5 / 1.5 / 2.5(原来三者都是 25–85,约 2.4)。范围进 `Tuning.UsedHealthMin/Max`。
- 显卡鉴定指南改为游戏原版的纸质指南样式(笔记本背景、委员会页眉、居中标题、插图加图注、“价格不变 / -n%”标注、末页鉴定示例,共 9 页,插图用检视台的大图),文案改成名词标题加陈述句,用词与原版鉴定指南一致(假冒、品相、价格不变)。
- (已被上一条取代)显卡鉴定手册曾做成阅读窗口(和 Web3 手册同一个窗口,共 8 页,8 种语言);外观折价六个数字进 `Tuning`(`CardFanDust` 等),手册用占位符取值。物品提示缩短为一句。
- 鉴定样例测试按钮(`Showcase: next dealer / appraisal case`),展示页的显卡鉴定专页用它截图。
- 鉴定显卡的检视弹窗:第一行标签“型号真伪”改为“型号”(各语言同改),指出之前不再写“检查:供电口与风扇数、序列号、背面丝印”这行依据;指出之后仍在下方列出查到的问题。
- 带首尾空格的拼接文字(“ (seller said: {0})”等 10 条):英文原先有的没加引号,被游戏读表时吃掉;其余语言翻译时也丢了。已补引号、按英文补回空格,并加测试。
- GPU 检视的行名放不下时自动缩字,“型号真伪”各语言改短。
- 多语言审校:对 de fr es pl ru ja 各审一轮,共提出 125 条修改,人工核对后采用 116 条(拒绝:俄语一条漏译成英文、日语把按钮缩得丢了 CRC、两处手册章节被整段改写、少数理由不成立的)。主要是矿工译法(Miner / майнеры)、天数缩写、术语不一致(Compliance-Prüfung 等)、稳定币 Pegged 的译法、语法错误。
- 交易终端窗口加大:默认 1200×900,mod 设置里可选 828×680(旧大小)、1200×900、1400×1000。窗口大时左右栏、表格列、货架列数、K 线图(3 倍像素)、首页卡片、订单两栏、钱包表按多出的宽度放宽,文字放大到约 1.24 倍(上限 1.3,行高和商品卡同步)。
- mod 设置(分类:界面、剧情、难度):终端窗口大小;Tumbler 剧情开关;风险倍率(骗局、跑路、钱包投毒、地址劫持、钓鱼概率)和挖矿产出倍率,档位 ×0.5 / ×1 / ×1.5 / ×2,领域层 `Scales` 读取。分类名要等本地化表可用后再登记。
- 多语言:新增 de fr es pl ru ja 六种语言表(连同 zh en 共 8 种,对应游戏主菜单的语言)。英文表是源文,其他语言由 AI 分块译出(术语表 `docs/i18n-glossary.md` 在运营仓库,游戏已有的词用官方译名),未经人工校对,文件头有说明。键集合、占位符、`§ ¶ \\n` 标记由单元测试逐语言核对,非中日语言里不得出现汉字或假名。
- 多语言布局:表格单元、按钮文字放不下时自动缩小字号(最小 68%)再省略,商品名限定两行内缩放;de fr es pl ru 的钱包表、手册章节列表、硬件钱包粘贴键按语言放宽;ru de 等少数过长标签改短(永续“加保证金”键、收益池列头)。
- 中文“匿名币”改称“隐私币”(英文 privacy coin),手册、黑市标签同步。
- 英文手册里写死的“六天”“7 天”改为占位符 `{BmBulkDays}`、`{AmlWindowDays}`,和中文一致。
- 手册里剩下的写死数字也进了 `Tuning`:出金审查污点系数 `AmlTaintMult` 与超额权重 `AmlExcessWeight`、出售速度界线 `BmSellFastMult` / `BmSellDay1Mult`;剧情联动的期限和涨幅改用已有的 `Story*` / `Tumbler*` 常量占位。占位符新增 `{Name:+%}`(倍数显示成涨幅)。
- 文案审查(逐行审查后人工核对):中文界面约 70 行改为中立陈述(去掉括号说明、祈使句、口语;夜间日志里的“黑市:”改“商店:”;“安保部”统一为“治安部”),手册里的口语和劝说句改写。保留“跑路”(题材用语)、“健康”(游戏用语)。
- 商店卖货页改为左右分栏(和买货页一致):左栏标价倍率表(含预计成交天数)、好评、上架数、全部上架,右栏货架;订单页改为买入、卖出两栏。倍率档位 `BmSellMults` 进 `Tuning`。永续持仓表列宽调整,表头不再重叠。手册合并为 16 章(混币器与匿名币、订单与出售),章节列表一屏放得下。
- 手册“银行与交易”章加 PSC 全称(Probably Stolen Coin)说明,只在此处解释一次。
- Web3 手册改为阅读窗口:使用手册物品打开 PLEDGER 风格窗口,左边 18 个章节,右边章节全文;章节在 `guide.web3.text` 里用 `§` 分隔(`¶` 为段落)。手册里的"黑市"(指商店处)改称"商店"。修复手册文本里未转义的双引号(英文手册在游戏里只读到 1,610 字符)。新增调试按钮"Give handbooks (test)"。
- 商店补验证(游戏内):逐家自动换币(购物车里一家商家用 CRA 换 PSC 付清,160.92 CRA → 1.493 PSC,那一家从购物车里消失,另一家保留);账单页(平台);英文界面(Shop 页签、卖家列表、商家信息头、购物车、付款面板都正常)。卖家列表的缩写地址改成类型加后四位(如"Used GPUs ·2077"),英文长名不再折行重叠。
- Web3 商店改版(`ui-trial` 分支):
- "Web3 黑市"改名"Web3 商店"(页签、升级项、提示;黑市声望是游戏派系,名字不变)。
- 点左栏卖家后,右栏最前面是商家信息:名称与地址、成交数、好评、到货天数、收的币、是否接受提前放款;平台是"立即到货"和收的币。
- 购物车:货架上的"购买"改成加入购物车(同一件再点数量 +1,封顶库存);商店页右下角悬浮购物车按钮("购物车 N",点开进结算页,结算后清零;读档清空)。
- 结算页两栏:左边购物车详情(按商家分组,每行 -、数量、+、删除,接受提前放款的商家可选托管 / 提前放款,合计);右边付款:每家商家的收款地址不同,所以只做逐家付款——每家选它收的币、生成账单、在钱包里转账;钱包里别的币够付时,每家还有自动换币(多付 8% 滑点、签名一次付清这一家)。统一结算已去掉。
- 游戏内验证:平台加两件、再加一家商家的货,购物车 3 件;结算页两栏显示正常,每家商家各一块付款选项(收的币、需付金额、钱包余额、生成账单、够付时的自动换币),点平台的"生成账单"得到平台收款地址和金额。自动换币在改成逐家之前验证过(66.96 CRA → 0.6212 PSC 后成交);改后的逐家自动换币和账单付清没有再走一遍,也没验证英文界面。
- 黑市买货页重排(`ui-trial` 分支):左栏是卖家列表(平台和各商家,选中行反色,商家写类型和缩写地址),右栏是 3 列货架;币种和数量不再摆在货架上方。点"购买"后右栏换成付款面板:数量(库存内 ×1 / ×5 / ×10)、付款方式(商家接受提前放款时才有)、合计、付款币种(该商家收的币)和"生成账单";下面是自动换币——钱包里别的币(够付)按 8% 滑点换成商家收的币,一次合约签名完成(`BmAutoSwapSlip`);没有够用的币时写明。上架费、改价费、一键上架同样在付款面板里选币(平台收 PSC、CRT、CRA)。各类商家收的币:硬件技术类 PSC / CRT,违禁和个人卖家 CRT / 匿名币,农业养殖水务酿酒 PSC / CRA(`Merchants.Accepts`,界面里限制,域层下单不限)。游戏内验证:平台买一件商品,CRC 自动换币 224.64 CRC → 2.0842 PSC(208 信用点等值)后成交,货进储藏区;直接生成账单显示收款地址和金额;商家货架切换和付款面板正常。没有验证:卖货页改价、一键上架的付款面板、英文界面、数量选择(库存只有 1 件)。
- 钱包页按钮:操作键放到数据格右边的纸面上(和顶栏页签同样的黑色触控键,不再压在污点字段上);选中行只反色数据格;状态"已连接"靠右。
- 新手期调整:低端显卡算力提高——RTX 60 1.0 → 1.8 H、RX 70 0.8 → 1.4 H(价格、耗电不变)。开局送的一矿架一卡,第 1–7 天日均净收入 14 → 33(电网电价),第 15–30 天 6 → 18;再买满 4 张同款卡的矿架 33 → 82/天,每加一张约 10 天回本(原来 24 天)。每张卡每千瓦时的产出仍低于中高端,一个矿架只有 4 个槽位,满槽 4×RTX60 约 7 H,4×RTX70 是 20 H,中后期自然要换更高档次。ops `research/starter` 有各档位的逐段结果。
- 手册补写出金审查的全部规则(界面不显示额度和命中概率:单笔 5 倍房租、7 天累计 15 倍房租、污点 10% 起、混币和被标记地址 +30%、上限 90%、轻度 / 重度的后果、手续费和税)和"销毁钱包前自己转出持仓与存款"。
- 金融审查(独立的数值审查,核实后采纳一部分):
- 采纳:`CrcHaircut` 0.95 → 0.85(减记后的持有量比例高于脱锚买入价 0.905,抄底即使遇到减记也不亏,减记形同虚设;现在遇到减记亏约 6%,没遇到仍赚约 10%);`SideArbRate` 0.5 → 0.75(CRA 螺旋时 PSC/CRA 池滞后公允价 43–80%,持 CRA 的人能榨池子);gas 盘公开价"不知道相位"的权重 0.5 → 0.15(`PredictGasBlind`,原来数清周期的玩家胜率 96%、每周净赚约 1,400)。
- 没有采纳:`PerpOpenBuffer` 0.02 → 0.01(只让名义 1.5 万以上的 5 倍大单能开,而且贴着强平线开,收益小风险大);派系盘"队列为空必为否"——这个结论的前提是玩家看得到排期,而排期对没有特权的玩家是隐藏的,对有传闻纸条、特权或买了情报的人本来就是内幕单,会被查处;先验每天 6% 是估计值,要用真实的游戏观察校准;NPC 开局算力偏低(回本约 10 天)和前期节奏偏快——设计时的取舍,保持。
- 综合模拟(portfoliosim,120 天):纯挖矿第 30 天 22%(符合目标);全做合规约 34%;"粗心出金"(脏币直接出金 + 理财卷走)罪证峰值 750 以上,第 35–45 天逮捕率 100%。
- 外部评审(整体评审报告,逐条核实后)修复的真实问题:
- 销毁钱包留在里面的持仓和存款会随钱包没有:评审建议拦截或回退,用户决定保持现状(和真实钱包一样,要自己先转出),所以撤回了我加的拦截和回退,只在手册里写明。
- Tumbler 剧情进度在生成顾客时就推进:当天没聊完、被清场或读档重来,进度已经走了,最糟的是第 3 次(三选一)没选就跳到第 4 次的"没做选择"分支,剧情报废。改成对话真正走完才推进;第 3 次在做出选择时推进;没聊完明天照样来。游戏内验证:第 1 次聊完日志"进度 1",没聊完的第 2 次不推进。
- 夜间结算加幂等保护:同一天已经结算过就跳过,防止其他 mod 或调试让钩子触发两次造成双重扣费。
- 预测盘口读游戏的 `futurEvents` / `futurEventsDay` 两个并行列表时加了下标保护。
- 核实后没有采纳:静态变量串档(`ClosePrice` 等每晚结算前先赋值再用,`Pending` 读档时已清;实际无串档)、除零(SPARK 螺旋也只到约 0.5、CRA 最低 0.02、全网算力有 0.8 的可用性下限,不会为零)、全局随机数流拆分(设计取舍)、`Charged` 字典增长(体积极小)。报告里还有几处引用错误(页签数量、不存在的 `out_fullsim.md`)。
- 收尾补验证(游戏内,用游戏自己的开日流程:调试"Run night"和 `day 1` 过夜,墙上的卷帘门按钮开门):
- 伪造转账顾客:新增调试入口"Coin buyer forged"。开口"CRT 我已经转过去了,截图给你看……"(正常顾客说"我用 CRT 付,链上确认要一会儿");成交 4 件共 118 信用点后现金没变(信用点被扣回),日志逐件记"伪造转账",晨间日志列出四条"拿走了……说付了……链上没有这笔"。
- 内幕被查:线人排期的停电盘口押「是」花 600(2,046 份,超过上限的 40%,标内幕),结算夜"内幕下注被查:本金和收益 2,046 全部没收,记诈骗罪证",罪证账本 FRAUD 380。
- 骗局池跑路:第一晚"收益池「Test Scam」跑路,主钱包 存的 563 CRC 没了"。钓鱼授权不撤销时"主钱包 的 9,000 CRT 被「P0inbase」用授权转走了"。
- 永续资金费:多头 2 倍、资金费率 0.3%:保证金 1000 → 994.06 → 987.97 → 981.73(每晚约扣名义价值的 0.3%)。夜里在日志里记各仓位的保证金、资金费率和标记价。
- 报纸:调试"Chain drama"后报纸上出现"链上拥堵"和"POINBASE 公告:充值地址迁移"(钓鱼公告)。
- Tumbler 自然开始:解锁混币器、第 35 天开门,日志"Tumbler 剧情开始:第 36 天第一次上门",同时当天的预测盘口补盘和开盘钩子正常。
- 密钥盘:`inspect` 看到 CONTRABAND_ITEM_TAG、等级 CONTRABAND_LEVEL_HIGH、类型 CONTRABAND(游戏自己的搜查查的就是这几个标签);真实被搜出来的路径没有触发。新增调试入口"Give key drive""Perp: PSC long 1000 x2"。
- 发现:`Run night`、`day N` 都不走开门(OnShutterOpened),所以以前只靠它们的测试没有跑过每天开门的逻辑(市场开盘波动、K 线开盘、预测补盘、Tumbler 开始、顾客相关钩子);这些要点墙上的卷帘门按钮开门才会触发。
- 仍没有在游戏里验证:ASIC 自然暴毙和黑市收货、永续穿仓后 30 天拉黑和没收单、杰克逊柜台上钱包的标价、游戏输入框里 Ctrl+V、密钥盘被真实搜查翻出、Serpico / Eleanor 剧情在游戏自己的对话里选择后的联动(只用调试设置过选择)。
- 界面文案去口语化:理财子页改为"收益池""发行理财";高息理财页的"风声:有人在议论"改为"举报风险:低 / 中 / 高","开张"改为"发行","昨晚新存入""今晚要付利息""自己垫付的利息"改为"昨日存入""今晚应付利息""自付利息合计";预测、情报、空投、收益池、K 线图、混币器的提示句一并改成陈述句(如"今日无开放盘口""需用授权钱包签名")。对话(Tumbler、假客服、顾客)不改。
- 理财页合并:Web3"理财"页签分两个子页——"存钱(别人的池子)"是 NPC 开的第三方收益池(原来在质押页下面,挪过来了),"发行(自己当庄)"是自己开的高息理财。页签一直显示,第 15 天前"发行"子页只写哪天起能开。手册里两处位置说明一并改了。游戏内验证:两个子页切换、收益池表格和存入金额、发行页开张选项都正常。
- 补验证(游戏内,阶段 5–9 只有单元测试的路径):
- 罪证与声望:高息理财被举报崩盘后罪证账本 FRAUD 400;手动卷走后下层声望 45 → 0、FRAUD 400;Tumbler 举报结局现金 +1,200、治安部声望 0 → 12;卖给黑市结局黑市声望 75 → 82(游戏的声望计算有自己的加成,比设定的 +5 多)。新增调试入口"Dump crimes and reputation"(罪证账本、证据、三派声望、现金写进日志)。
- 骗局:签名撤销钓鱼授权后(PLEDGER"授权已撤销"),三晚 CRT 没被转走(2,998.5,只扣了 1.5 手续费);时间锁池子取款两晚后到账("收益池「Test Lock」取款到账:主钱包 收到 500 CRC")。骗局池三晚没跑路(每晚 18%,三晚都没跑的概率约 55%),跑路仍只有单元测试。新增调试入口"Scams: phishing approval + scam pool"。
- 英文界面:终端、空投、预测(含 K 线图和阈值线)、Poinbase 现货与 K 线图都正常;"On-chain wallet Main wallet: CRC"改成"Main wallet: CRC"。测完已把游戏语言切回中文。
- 仍没有在游戏里验证:伪造转账顾客(要自然出现的用币顾客)、内幕被查、骗局池跑路、密钥盘被搜查翻出、剧情的自然触发(第 30 / 40 天以后)。
- 9.1 剧情联动:每晚读游戏剧情线的选择,每个只生效一次,改动本 mod 世界的倍率(存档里的 `StoryMods`),第二天报纸头版登一条新闻。选择是玩家自己做的,市场要过几天才反应完(SPARK 分 5 晚走完),可以抢在前面用永续和预测盘下注。
- 塞尔皮科的证据交给革命军:CRA 补贴池 +10,000,30 天内螺旋概率减半,SPARK 五晚内涨 20%;交给黑市:CRT 脱锚、镇压时被黑市出卖的概率永久减半;交给治安部:30 天内出金审查命中率 ×1.5、混币器制裁概率 ×1.5、操纵检出和内幕查处 ×1.3;举报他或他死了:没有影响。
- 埃莉诺泄露数据盘:CRC 托管银行减记的概率翻倍,SPARK 五晚内涨 15%;交回数据盘:CRC 脱锚概率减半。
- 伦警官被捕:14 天内出金审查、操纵检出、内幕查处 ×1.3(和治安部整顿叠加)。
- 无尽模式关闭角色剧情线,这些选择不会出现。放贷人 Lou 被捕没有接(借贷暂缓)。
- 游戏内验证:调试入口设置"证据给革命军、泄露数据盘、伦警官被捕"后过一夜("Run night (test)"),日志三条"剧情联动生效",第二天报纸头版登出三条新闻。没有在游戏里验证:真实剧情对话里做出选择后的联动、各倍率在游戏里的实际效果(单元测试覆盖:出金命中率上升、CRT 脱锚减半、SPARK 推涨合计 ×1.2、叠加)。
- 9.2 本 mod 自己的剧情线:混币器运营者 Tumbler。解锁混币器、第 30 天起,四次上门(游戏的对话顾客,开始日、+3、+5、+9 天,错过的往后补):
1. 自我介绍:混币器是他写的。
2. 治安部在查链上混币服务。当晚混币器节点被查封 4 天,报纸登"治安部查封混币节点"。
3. 求你藏密钥盘,三选一(游戏自己的选项界面):
- 藏好:给你一件"密钥盘"(违禁品,高级,价值 600,游戏自己的搜查会翻出来记罪证)。第 4 次他来取:密钥盘还在就收走,留 1,000 CRT 谢礼进当前钱包,混币器手续费永久减半、匿名集 ×1.5;弄丢了(被搜走、卖掉)他只说"算了"。
- 举报:马上拿 1,200 信用点赏金、治安部声望 +8,混币器永久关停(在途的照常提回,不能再存),报纸"混币器运营者落网",第 4 次不再上门。
- 卖给黑市:3,000 CRT 进当前钱包(一半带污点)、黑市声望 +5,之后混币器手续费 ×1.5、跑路概率 ×4,报纸"混币服务易主";第 4 次他上门质问。
- 新画了密钥盘的像素图(深紫数据棒、金属接口、红色指示灯)。
- 补测(游戏内):举报结局现金 +1,200(1,000,080 → 1,001,280)、治安部声望 0 → 12(加 8,游戏声望计算自带调整)、没有第 4 次上门、混币器页"状态:已关停"并拒收新存入;卖给黑市结局黑市声望 75 → 82(加 5,同样有调整),第 4 次上门质问。高息理财:被举报崩盘后罪证账本出现 FRAUD 400;手动卷走后下层声望 45 → 0、FRAUD 400。
- 游戏内验证:调试入口"Tumbler: next visit now"依次叫进四次,第 1、2、3 次对话正常,第 3 次出现三个选项;选"藏好"后日志"选择 1";第 4 次取回密钥盘,晨间日志"Tumbler 取回了密钥盘,留下 1,000 CRT 谢礼(已转入 主钱包),混币器手续费减半"。没有在游戏里验证:举报和卖给黑市两条结局(单元测试只覆盖倍率)、第 2 次之后的查封和新闻、搜查翻出密钥盘、自然触发(第 30 天、已解锁混币器)。
- 9.3 收尾经济复核(ops `research/fullsim`):挖矿收入全部再投资、钱够就买矿机,120 天平均只买到 3.4–3.9 台(矿机商每周 25%、一次一台),没有滚雪球;第 30 天金融线占总收入 19–22%(目标 20–30%),专心买矿机的玩家第 80–90 天峰值 37–44%,第 90 天减半和矿机时代后回落。没有改数值。
- 报纸修复:报纸按重要度排序、最多登 4 条;mod 新闻原来的重要度是 2,游戏事件一多就被挤掉(此前各阶段"报纸新闻没有验证"的原因)。改为默认 5(游戏事件多在 4–7),剧情联动 9。
### 行情图
- K 线图标数字:图里用一套 3×5 像素数字字体(放大 2 倍后约 6×10),和像素风格一致。右侧刻度栏(宽 22 像素)标三条网格线的价格;最新收盘价是黄底价签,网格刻度离它太近时让开;预测阈值线的价格用蓝色标在刻度栏;成交量区右上标这一屏最大的成交额(≥ 1 万写成 "36k");底部每逢天数是 5 的倍数的蜡烛下面标天数并画一个刻度点。价格位数随大小变:≥ 100 不带小数,≥ 10 一位,≥ 1 两位,小于 1 三位(".995")。图宽 206 像素(显示 412)。游戏内验证:PSC 刻度 113 / 81.3、价签 91.8、量柱 36k、日期 −10 … 15(测试存档在第 16 天,调试入口往前补了 30 天,所以出现负数;真实历史从第 1 天开始)。
- 所有可交易资产都有日 K 和成交量:PSC、SPARK、CRT、CRA、CRC、Ponero。开门时每个币开一根(开盘接昨天收盘),夜间结算在价格更新前收盘;稳定币和 SPARK 的价格在夜里变,变动算进第二天那根。成交量:市场的量 = 主池深度 × 各币比例(PSC 12%、CRC 10%、CRT 6%、CRA 4%、SPARK 3%、Ponero 1%)×(1 + 8 × 当天的涨跌与振幅)× 0.6–1.4 的随机,波动越大成交越多;你的量包括 Poinbase / DEX 兑换(两边的币都算)、SPARK 铸销(SPARK 和 CRA)、Ponero 兑换、永续开平仓(按名义价值,计入 PSC 或 SPARK)。
- 图上方加一排币种按钮(Poinbase 现货页、Web3 交易页各自记住选的币,Ponero 要黑市解锁后才出现);永续页显示当前选中的品种;预测页仍是 PSC。均线改为先画,蜡烛盖在上面,贴着锚的稳定币不会被均线遮住。
- 游戏内验证:Web3 交易页切换 PSC / SPARK / CRT 都正常,SPARK 有完整的蜡烛和放量,CRT 在锚上是一条线(30 天里没脱锚)。没有在游戏里验证:脱锚时稳定币的图、Ponero、玩家各类成交计入量柱(只看过 PSC 现货的青色柱)。
- PSC 日 K:存档里新增逐日 K 线历史(`CandleBook`,保留 60 天)。开盘接前一天的收盘价(币是全天候交易的,夜里的回归和开门波动算进当天这根,没有跳空);最高 / 最低只取白天真实发生过的价格(开门波动、NPC 大户、插针、玩家在 Poinbase 的大单),不编造日内走势;收盘是夜间结算前、插针回去之后的价格。成交量分大户和玩家两份。旧存档没有历史,从接入当天开始记录。
- K 线图:运行时画在一张 188×97 的纹理上,点采样放大 2 倍显示,和游戏的像素风格一致。最近 30 根,涨绿跌红、影线、黄色 7 日均线(永续资金费按它算),下方成交量柱(灰 大户 / 青 你的成交),当天还没收盘的蜡烛画成空心;上方一行写最高、最低、收盘和涨跌幅。出现在 Poinbase 现货页、永续页,以及预测页选中 PSC 价格盘时(加一条蓝色阈值虚线)。
- 调试入口"Chart: run market 30 days":用真实的行情过程往前跑 30 天并记录蜡烛(会改动这一局的 PSC 价格,只在测试存档用)。
- 游戏内验证:三处都显示正常,30 根蜡烛、均线、成交量柱、阈值虚线(PSC 价格 ≥ 104)。没有在游戏里验证:按真实日程一天天积累(只用调试入口跑过)、英文界面。
### 第七阶段:预测市场、操纵与骗局(7.1、7.2、7.3)
- 阶段 7 数值总复核(ops `research/stage7sim`,用真实的域层,30 天平均每天净收益,对照原版第 30 天约 1,990 / 天的毛收入)。改动:
- 预测内幕被查改为本金和收益全部没收(原来只退本金,等于没有风险);先买后卖、赶在结算前离场,卖出有赚时同样按内幕查处(原来卖出不查,"开盘买、隔夜涨价卖"是零风险的 +47~+70 / 天)。复核后:每 5 天一条消息、5% 盘口下 300 持有到结算约 +230 / 天、亏钱概率约 0;下 1500 持有到结算平均 +3(第 30 天)、亏钱概率 64%——小单安全,大单被查。
- 高息理财的罪证 1200 → 400:单次记 1200 会直接越过 300 的逮捕线(calibration §1.4),卷走一次等于游戏结束;400 和预测、操纵的罪证上限一致,需要叠加其他罪证才危险。
- 第三方收益池单池上限 2000 + 60 × 天数 → 4000 + 120 × 天数:原来最好的策略每天只赚 40,没人会去碰;现在"只存骗局概率低的"约 +80~+114 / 天、30 天亏钱概率约 9%,全存约 −25~−34 / 天、亏钱概率约一半。
- 水纯度盘的阈值从各 4 档减到 2 档(下层 8300 / 8700,上层 9050 / 9100):纯度先验精确、没有信息可挖,原来占了候选盘口的大头。
- gas 盘的公开价改为一半按真实相位、一半按不知道相位:七天周期是链上公开的,原来盘口完全不看相位,数清周期的人每盘都能稳赚。
- 空投:钓鱼空投改为七成要无上限授权、三成要奖励 20–100 倍的大额授权(看着像"有限授权"),转走时不超过授权额度。全领 30 天约 −94 / 天、几乎必亏;只领没问题的约 +14 / 天。
- 插针抄底:向下插针买 3000 / 10000、第二天卖回,约 +13 / +29 每天,亏钱概率约一成;保持小额,不改。
- 交互:让玩家看得懂市场怎么动:
- 盘口列表上,夜里价格变化超过 0.02 的写"(昨晚 0.03 ↑)";变化超过 0.15 的进晨间日志,附上第一条线索作为原因,例如"预测盘「第 19 天:出现「停电」」价格 0.03 → 0.59(排期:第 19 天开始)"。
- 预测页左栏底部"历史(开盘均价 / 实际发生)":按盘口类型统计开过几个、开盘均价、实际发生的比例,玩家能看出哪类盘口系统性地定价偏高或偏低(例如新闻事件开盘均价 0.03、实际 33%)。
- 可交易的盘口排在列表前面,已封盘的排后面。
- 游戏内验证(用"Run night (test)"走游戏自己的开日流程):调试排一个第 19 天的已知停电 → 第 16 天开盘 0.03 → 第 17 天晨间日志"0.03 → 0.59(排期:第 19 天开始)"→ 第 18 天"0.59 → 0.82"→ 第 19 天夜里结算"是",历史面板"新闻事件:3 个,开盘均价 0.03,发生 33%"。注意:AutoMod 探查的 `day N` 不走游戏的开日流程,排期事件不会触发,测这类盘口要用"Run night (test)"。
- 7.3 NPC 骗局:
- 插针:第 10 天起,每天开门 7% 概率价格被瞬间打上或砸下 15–30%(PSC 主池);七成当晚回去 60–100%,三成是真的(不回去)。永续当晚的标记价仍用"开门价与收盘价的平均",开门价取插针前的价格,所以强平只吃一半;预测盘和夜间结算读回去之后的价格。
- 空投与钓鱼授权(Web3 新增"空投"页签):每晚 35% 概率出一个空投,2 天内可领;四成是钓鱼。正规空投不要授权或只要 10–60 的小额授权,奖励 20–120 CRC;钓鱼空投七成要"无上限授权"、三成要奖励 20–100 倍的大额授权,名字六成是仿冒(0/o、1/l、m/n 之类),领到的只有标称奖励的 5%,1–2 晚后在授权额度内把那个钱包里的这种币转走(污点按比例一起走)。领取是合约调用,PLEDGER 核对页写明授权范围;右栏列出当前钱包的授权,可以签名撤销,撤销后就转不走。
- 第三方收益池(质押页下面):同时最多 3 个,每晚 25% 概率新开;日息 1 / 2 / 3%,储备、时间锁、审计看得见,是否骗局看不见:骗局概率按日息 15 / 40 / 75%,有时间锁 −25%,有审计 −20%。正规池从储备付利息,储备付不起时退本金关闭;骗局池照常显示利息,每晚 18% 概率跑路,存款和在途取款全没。有时间锁的池子取款要等 2 天(等的时候跑路也没了)。单池上限 2000 + 60 × 天数。存入、取出都是合约调用,污点与成本随存款进出。
- 假客服:第 10 天起每天 4% 概率,一位"Poinbase 客服"上门说账户被风控,要把 300–1500 CRC 转到"安全地址"。地址记为骗子地址,转过去的币就没了,晨间日志点明。
- 伪造转账:第 10 天起用币付款的顾客里 8% 是骗子,开口说"已经转过去了,截图给你看,赶紧给货"(正常顾客说"链上确认要一会儿");成交后信用点被扣回、币不到账,晨间日志列出谁拿走了什么。
- 假买墙、貔貅币依赖 NPC 项目币,挪到阶段 8 和发币一起做。
- 游戏内验证:空投页列出 4 个空投(含"Revo1ution DAO 授权 PSC 9,120""PSC Foundation 授权 CRT 无上限");签名领取钓鱼空投只得 4 CRC,授权出现在右栏;领"Revo1ution DAO"后过夜,晨间日志"主钱包 的 20 PSC 被「Revo1ution DAO」用授权转走了",钱包 PSC 归零;收益池表格(日息 / 储备 / 时间锁 / 审计 / 存款)显示正常,签名存入 500 到 3% 的池子,三晚后 546;插针让 PSC 估值 2000 → 2445;假客服上门对话。没有在游戏里验证:撤销授权、骗局池跑路和正规池退本金(单元测试覆盖)、时间锁取款到账、伪造转账顾客(要自然出现的用币顾客)、转给假客服地址后的日志、英文界面。
#### 7.1、7.2
- 高息理财改用链上钱包:开张(宣传费 800 CRC)和卷走都是合约调用,用当前钱包签名;产品记下发行钱包,每晚垫付的利息从它出,卷走的池子进它(污点全额),卷走要用发行钱包签名。游戏内验证:主钱包签名开张 6%,三夜后签名卷走 1634,钱包 CRC 1000 → 200 → 1833,污点 1634,卷走手续费 1.13 CRT。
- 柜台线人接进预测市场:游戏里卖消息的顾客(乞丐、线人,提前 1–4 天说出新闻事件)、传闻纸条、派系特权都会把事件以"已知"排进日程;每天开门补盘时,对每个这样的、开始日在后天及以后的事件保证有一个盘口(窗口就是开始那天),开盘价按公开信息下的基础概率(不看冷却,每天有事件的概率取 35%),不因为已经排期而抬高——当晚价格才向 92% 靠拢,知道的人能赚到中间的差;押「是」算内幕单。游戏内验证:调试排一个 3 天后的已知停电,开盘出现"第 19 天:出现「停电」",是 0.03,线索"排期:第 19 天开始"。
- 高息理财(庞氏,Web3 "理财"页签,一个存档一次):选日息 2 / 4 / 6 / 8 / 10%,付 800 CRC 宣传费(Poinbase 账户)开张。每晚下层新存入 (300 + 15 × 天数) × (日息 / 4%)^1.5 × 声望系数 × 0.6–1.4 的 CRC,池子上限 6000 + 200 × 天数,每晚被取走 3%;利息先用新存入的付,不够从 Poinbase 账户补,补不上挤兑崩盘;每晚被举报概率 = 2% + 0.5 × 日息 + 4% × 池子占上限的比例(封顶 90%),被举报同样崩盘。崩盘或被举报:池子没收,记 FRAUD 1200(单件上限之外,按游戏罪证口径记一条),下层声望清零,报纸报道。随时可"卷走"(点两次确认):池子进 Poinbase 账户、污点全额,同样记罪证、清零声望。页面显示池子、昨晚存入、今晚利息、自己垫付的利息和"风声"(举报风险的三档提示)。
- 复核(ops `research/ponzisim`,下层声望 50,每格 2000 局):日息越高越赚得快也越容易爆;开张第 40 天、日息 6%,熬 5 夜卷走平均净赚约 4000(崩盘概率 30%),熬 10 夜约 4800(53%),熬 20 夜约 2400(80%);日息 10% 熬 5 夜约 6800(41%),熬 20 夜约 900(89%)。每个选择的标准差和均值相当,"什么时候卷走"才是决策。
- 游戏内验证:理财页签按日显示;4% 开张后一晚池子 372(昨晚存入 201、今晚利息 14.88);点两次卷走后 Poinbase CRC 4200 → 4572,污点 372;另一次 6% 开张过 4 夜被举报,池子 456 被没收,产品关闭,页面显示"每个存档只能开一次",晨间日志与新闻有记录。没有在游戏里验证:下层声望清零的实际数值(用游戏的 `SetReputation(0)`,没读回来看)、罪证数额的实际入账、付不出利息的挤兑路径(单元测试覆盖)、英文界面。
- 操纵检出(7.2 前半):Poinbase 主池上玩家自己的现货单会累计"推价幅度"(对数),每晚随价格回归衰减,预测盘线索里能看到。幅度超过 2% 后,价格盘押中且方向和推价一致、或 PSC 永续平仓盈利且方向一致,按幅度被查(25% + 6 × (幅度 − 2%),封顶 95%):预测盘本金和收益全部没收、永续盈利没收,收益的一半记 FRAUD(上限 400),永续账户冻结 3 天,报纸出一条调查新闻。把规则从最初设想的"单日推价 ≥ 15%"改成了 2% 起:manipsim 显示价格盘阈值离现价只有 3–10%,尾盘买 58 的成本就能赢上千,≥ 15% 的线形同虚设。
- 复核(ops `research/manipsim`,第 30 天):先空后砸本身不赚——永续单边滑点加现货来回价差,砸盘 4000–20000 配 5000–20000 的空单平均净亏 66–4400,只有砸盘 4 万(推价 57%)配小空单才勉强为正;尾盘拉价格过价格盘阈值在没有检出时几乎稳赢(成本 40–125,净赚 570–1390,亏钱概率 ≈ 0),加检出后下注 ≤ 1000 平均只赚 100–240、亏钱概率 40–60%,下注 3000 平均亏 400–1500。
- 游戏内验证:Poinbase 买 4000 CRC 的 PSC,推价累计 +15.1%,价格盘线索栏显示"你推价 +15.1%,易被查"。没有在游戏里验证:被查后的没收、新闻、永续冻结(单元测试覆盖预测盘判定;永续平仓的判定只有代码路径)。
#### 7.1
- Web3 页"预测"子页(游戏第 8 天起,Web3 治理社区已买下):每个盘口是一道是非题,开盘当天到窗口开始前可以买卖,窗口最后一天夜里结算;每天开门后补盘,同时开放 3–6 个,每天最多新开 2 个。资金走链上钱包的 CRC:买入、全部卖出都是合约调用,要在钱包上签名(和质押一样;买入的手续费在核对页选档、用 CRC 付,卖出用价值最高的币付);持仓记在签名的钱包名下,结算时兑付回这个钱包,同一盘口各钱包只能押同一边,持仓上限和内幕判定按盘口合计(拆到几个钱包躲不过)。污点与成本按花掉的价值比例随资金进持仓,赢的钱继承押注资金的污点比例,卖回时按比例退。
- 盘口类型:新闻事件("出现某常规事件",先验 = 游戏掷骰规则:5 天后每天 50 / 35 / 25% 有事件,再按权重抽)、派系行动("某派系有行动开始",每天开始概率 6%,估计值)、水纯度(明天上层 / 下层水纯度 ≥ 阈值,先验精确)、PSC 价格(窗口最后一天夜间结算前的价格 ≥ 阈值,先验用价格过程的正态近似)。事件和派系行动靠夜间对比"昨晚生效的事件"得到当天新出现的;窗口里缺任何一天的记录(没睡觉结算)盘口作废、退回本金。
- 界面不再出现"份":持仓直接写成本和"押中得多少"(每份押中兑付 1 信用点),盘口列表、右栏的各钱包持仓和买入预览都是这种写法,右栏另给"现在卖出得多少"。
- 玩法打磨:
- 界面:盘口行和右栏显示每份现价(是 / 否,等于概率)、持仓份数、均价、成本、押中得多少、现在卖出得多少;买入预览带均价。
- 风险放大:单盘最大亏损改为 800 + 25 × 天数(第 30 天 1550),押错本金全亏;盘口价格每晚除了向合理概率靠拢还带随机噪声;买卖各 2%。
- 消息反应:每晚还没封盘的盘口向"公开信息下的合理概率"靠拢一半(报纸上已排期的事件 / 派系行动 → 92%,进行中或冷却中 → 2%,行情盘按最新价格,gas 盘按周期,CRA 盘按补贴池水位,矿机时代按预警),再加 logit 噪声 0.35。价格在夜里才反应,白天看到消息的人可以抢在前面买、涨价后卖,不必持有到结算;开盘到窗口开始的天数从 1–2 天改为 1–3 天,让反应有时间发生。
- 线索栏:盘口面板列出和它有关的已知信息:排期(传闻、派系特权)、事件进行中或冷却、派系行动进行中、减半日与已排期的镇压 / 停电、价格锚、近几天 gas、CRA 补贴池水位和每晚消耗、矿机时代预告、手里的内幕消息。
- 新盘口:gas 指数(明天 ≥ 阈值,7 天周期,先验是七个相位的平均,周期是玩家看得出来的);CRA 预警(补贴池每晚被消耗,归零必预警,质押越多越快);矿机时代(预告前按 50–90 天均匀,预告后落在 4–7 天里,要 day + 4 ≥ 50 才开,避免泄露)。CRA 和矿机时代读 mod 状态,所以预测结算挪到夜间结算的最后(行情盘、gas 盘用结算开始时记下的收盘数据)。
- 玩家能推动的:PSC 价格盘能被自己的大单推动(操纵检出放在 7.2);CRA 盘靠质押量推动补贴池消耗。
- 情报页(Web3 黑市解锁且声望 ≥ 20 才出现):局势简报(120 CRT)列出五个势力值和每个派系按游戏阈值现在能发起的行动,派系行动已排期 / 进行中的也标出,10% 概率是假情报(数值是编的);内幕消息只卖已经定了的盘口(排进日程的派系行动、矿机时代日子),三档准确率约 60 / 80 / 95%,价格是盘口最大亏损的 1 / 2.5 / 5%(CRT),一成是假情报(说的和真相相反);买了消息再押同一边算内幕单。付款是 CRT 的合约调用,要签名,花掉的币带走对应比例的污点。
- 复核(predsim v2):不知情随机押仍是负期望;低先验盘口买消息基本亏;中先验盘口(20–50%)高档消息下注 400 以上才赚,低档消息几乎都是亏的陷阱;单盘输赢在上千,亏钱的概率可观。
- 定价:LMSR,初始价 = 先验,单盘系统最大亏损 = 300 + 10 × 天数,流动性参数由它反推;买卖各收 2%;单人单盘份额上限 = 最大亏损 × 3;一个盘口只持有一边。
- 内幕:下注时这个事件(或派系行动)已经以"玩家看得到"的形式排进日程(传闻纸条、派系特权),而且落在窗口里,押「是」算内幕单。内幕持仓达到份额上限的 40% 后,结算时 50% 概率被查:只退本金,收益的一半(上限 400)记 FRAUD 罪证。情报顾客没接。
- 复核(ops `research/predsim`):不知情随机押期望为负;谨慎的内幕(压在查处线下)在低先验盘口一盘净赚 270–520(第 30 天最大亏损 600),买满上限的期望反而更低。行情先验和真实 `Market` 过程的蒙特卡洛对得上(单元测试,误差 < 4%)。
- 游戏内验证:新开盘口列表显示问题、先验、持仓;在 PLEDGER 上签名买入 100 CRC 得 117–424 份并涨价(钱包 CRC 1000 → 899,含 1.5 手续费)、签名全部卖出收回 96.08(扣 2%);调试推进几天后押中的纯度盘兑付 117 回到钱包(CRC 995 → 1,012),晨间日志"预测押中";此前用 Poinbase 账户的版本还验证过价格盘兑付 199;一个真实的革命军行动被观测到并结算为"是";没押的盘口也进"最近结果"。没有在游戏里验证:常规新闻事件盘口的自然结算(本次测试存档里没碰上)、作废、内幕判定与被查、手册文字、英文界面。派系每天 6% 的先验没有真实数据校准。
- 单元测试 11647 通过(含多钱包持仓:各回各的钱包、限额与内幕合计;消息反应、情报、gas / CRA / 矿机时代先验)。
- 游戏内验证(玩法打磨):价格显示、情报页、局势简报(真实读到安保行动已排期等)、三档内幕消息购买与签名、线索栏显示情报与矿机时代预告、买「是」押矿机时代后结算兑付 945。没有在游戏里验证:夜间消息反应的实际价格变化(只有单元测试,夜里跑过没报错)、gas / CRA 盘口的结算(只有单元测试)、报纸排期引起的 92% 靠拢、假情报、被查、英文界面。
### 第六阶段:高收益与高风险(进行中)
- RatDigg 矿机(M1 / M2 / M3,11,000 / 27,000 / 72,000,50 / 120 / 300 H,60 / 110 / 220 kJ/天):独立的 Machinery,窗口里有电池槽、模块槽(和矿架同一套效果与上限)、修理槽,没有显卡槽;夜间和矿架一样先从电池扣电、有电才挖,算入全网算力。损耗每天 0.8,暴毙系数 0.5;没有外观、没有真假。暴毙后成"损坏的矿机"(残值 15%),往修理槽按顺序放电子元件修好(M1:4 普通 + 1 稀有;M2:2 稀有;M3:1 超科技 + 1 稀有,约机价的 4%),修好健康 −20。新画了三张像素图(M1 绿、M2 蓝、M3 金的指示灯与风扇轴心)。
- 来源只在 Web3 黑市:新增商家类型"矿机商",声望 ≥ 40 起每周 25% 出现,一次一台,70% 全新、其余二手(健康 40–85,价格按健康折算);型号按声望放开(M1 40、M2 60、M3 80),出现权重 1 / 0.6 / 0.4;价格系数同尖端店铺 0.85–1.00。可以在"卖货"页转手,损坏的不能上架。
- "矿机时代":第 50–90 天之间随机一天(存档接入得晚则至少留出预兆天数),提前 4–7 天上报纸预告,到点 NPC 矿工耗电效率翻倍(`NpcKj` 2.4 → 1.2),永久。NPC 愿意挖得更久,均衡算力上升,显卡矿工的收益被摊薄。
- 复核(ops `research/asicsim`,真实 Network 与 Market,每组 150 局,玩家另有一个 20 H 的矿架,电费按电网价 1.5):买入后累计回本——M1 第 20 天买 20 天、第 60 天买 22 天(有矿机时代 28 天)、第 100 天买 32 天(有矿机时代 61 天);M2 21 / 23 / 34 天(有时代 28 / 59);M3 26 / 28 / 42 天(有时代 37 / 65)。矿机时代之后一台 M1 每天净赚约 140–210,仍然为正。M3 要 7.2 万现金和声望 80,实际上早期买不起。
- 游戏内验证:三台矿机可以造出、放进柜台、打开窗口(电池 / 模块 / 修理三个面板);放自充电池后夜里 M1 挖出 11.83 PSC(= 50/410 × 100 × 0.97);手动标记损坏后,修理槽只收这一步需要的元件(M2 拒收普通元件,M1 收下 4 个普通元件进入下一步,M3 收下超科技元件进入下一步),M2 放两个稀有元件后"修好了,健康 80";"矿机时代"调试入口:第 3 天日志"矿机时代到来";黑市"矿机商"货架:「RatDigg M1 矿机 矿机 全新 剩 1 件 11148 信用点等值」带图标。没有在游戏里验证:真实暴毙(只有单元测试)、报纸上的两条矿机时代新闻、从黑市买下矿机后的收货、模块对矿机的效果。
- Poinbase 永续合约(Poinbase 页"永续"子页):PSC-PERP 最高 5 倍、单仓名义上限 20000,SPARK-PERP 最高 3 倍、上限 5000;保证金用 Poinbase 账户的 CRC,一个品种一个净仓位(同品种不能多空同开),托管账户不用签名。成交价按名义价值占现货池深的比例滑点(深度倍数 3:名义 5000、池深 5 万时单边约 3.3%),开平仓各收 0.1%。
- 标记价 = 当日均价(开盘价与收盘价的平均,SPARK 取当天价),强平只在夜间检查:权益低于名义价值的 10% 时强平,保证金没了,剩余权益(扣 2% 强平费后)进保险基金。每晚资金费 = 0.1 × (标记价 / 7 日均价 − 1),上下限 ±0.3%,价格偏高时多头付空头。开仓时按现价算权益若已低于强平线再加 2%,直接拒绝(滑点太大,开了就是被强平)。
- 保险基金初始 10000,每晚 +150,手续费的 30% 进基金;NPC 在大行情(标记价日涨跌超过 6%)里穿仓会按涨跌幅的平方抽走基金(SPARK 崩盘能抽干)。基金见底时盈利仓位被自动减仓,少拿一半盈利。强平穿仓的缺口先由基金垫,不够的记成玩家的欠款。
- 欠款每天计息 2%,每晚先用 Poinbase 账户里的 CRC 还;有欠款不能开仓、不能出金;7 天没还清,用游戏自己的没收接口(`FindRepoItems` / `GetAllRepoEligibleItems`,和房贷逾期同一套,不碰放贷人 Lou)没收店里物品抵债,抵不够的核销,永续账户拉黑 30 天。污点随保证金进仓位,平仓时按剩下的比例退回,盈利是干净的新币。
- 校准(ops `research/perpsim`,真实 Market 和 PerpBook,120 天,起始 3000):没有滑点时"看 7 日均线反向做"+75%、"跟着资金费走"+46%、知道价格锚的上限 +148%——PSC 每晚向锚回归 15% 的规律被杠杆放大,资金费在这个方向上反而补贴了回归的一边(价格偏高时空头收钱)。加滑点后深度倍数 3:看均线 +19%、跟着资金费 +26%、上限 +88%,随机交易 −66%、5 倍死扛 −72%;资金费系数 0.03–0.25 几乎没有影响。
- 游戏内验证:PSC 5× 满额(名义 20000,滑点 13%)开仓当场强平价 101 高于现价 100,因此加了开仓校验;SPARK 多头强平后日志"SPARK 多被强平,保证金 1,666 没了";砸 SPARK 两次、提光账户现金后:欠款 2,627,每天 +2%(2,679 … 2,958),第 7 天夜里没收物品抵 3,018,欠款清零;PSC 强平的剩余权益进基金,基金见底时盈利仓位被自动减仓的路径只有单元测试。没有在游戏里验证:没收单物品(没在探查里找到,可能柜台满了)、资金费真实收付、拉黑 30 天。
- CRA 质押(Web3 页"质押"子页):把 CRA 质押进革命军质押池,日息 0.5%(补贴池告急降到 0.2%),利息并入本金;取出要先申请,4 天后自动回到质押时的钱包,等待期间不计息。每个钱包一个仓位,污点与成本随币存入、按比例随币取出;仓位和解锁队列在质押页列出。质押、申请取出都是合约调用,要在钱包上签名;申请取出不花币,手续费用钱包里价值最高的币付。补贴池按玩家质押量一并消耗。
- 预兆新闻:螺旋预兆出现的那晚上报纸("质押池传出异动"),补贴池告急降息时再上一条。
- 平衡代币 SPARK:起始价 10 信用点,日波动 8%,每晚向 10 回归 3%,范围 3–30;革命军袭击上层 +15%,下层动荡 −10%;CRA 死亡螺旋三天里按螺旋前价格的 ×0.8 / ×0.3 / ×0.05 暴跌。可以和 CRA 按价格互相铸销,手续费 0.3%,也是合约调用。钱包的币种里多了 SPARK。
- 游戏内验证:质押 400 CRA(手续费 1.5 CRA),申请取出 100(第 5 天),铸造 100 CRA 得 9.97 SPARK;第 6 天夜里 100 CRA 回到主钱包,利息 +1.5(300 × 0.5%);触发螺旋预兆后三晚:CRA 0.85 → … → 0.02,SPARK 5.94 → 0.371,质押仓位的数量继续涨(利息)但价格归零。没有在游戏里验证:报纸上的预兆新闻(没有打开报纸)、补贴池自然见底降息。
- 夜间结算此前在测试存档里没有跑起来:调试面板的"CRA + PSC"只给币,不会解锁 Web3,夜间逻辑要先解锁("Black market +50 rep")。
### 第五阶段:硬件钱包(进行中)
- 硬件钱包 PLEDGER:终端顶栏"钱包"打开的独立窗口。像素画的圆角石墨机身、黑色屏幕边框、浅色墨水屏,下沿两个物理按键(左 × 拒绝 / 返回,右 ✓ 下一步,签名要按住 1.2 秒,显示游戏自带的按住圆环)。屏幕页:主钱包(地址、余额、合计、污点)、发送、核对、交易、已确认。
- 链上地址改成 "0x" + 6 位 + 2 位校验位(FNV-1a 高 8 位)。输错一位几乎都被拦下("地址校验失败"),约 1/256 碰巧合法;合法但没人用的地址,核对页提示"没有交易记录",签名后币转不回来。旧存档的商家和收款地址载入时换成新格式。
- 付款改成账单:黑市现货、商家订单、上架费、改价费、批量上架、混币器、Ponero 兑换在终端开账单(收款地址、币种、金额),"打开钱包转账"在钱包里预填币种和金额,地址要手输。付给有账单的地址才执行;币种不对、金额不足在发送页拦下;多付的部分不退;对有往来但没有账单的地址拒绝发送。账单不随切换终端页面作废,可以手动作废。
- DEX 兑换改成合约调用:在终端点买入 / 卖出后钱包弹出签名请求,按住 ✓ 签名后才成交,× 拒绝。Poinbase 是托管账户,仍直接成交。
- 签名后的动画:签名中 → 已广播(内存池)→ 打包进区块 #N → 确认 1/3、2/3、3/3,约 5 秒,可跳过;窗口关着也会走完。区块高度存档。
- 游戏内验证:DEX 10 CRT 兑 PSC(签名 → 确认 → CRT 4000 → 3990);黑市现货手枪:输错末位被校验位拦下,改对后付 156 CRT,库存 3 → 2,货进储藏区,污点按比例减少;向 0x12345699 转 1 CRT,提示转不回来,余额减 1;Ponero 兑换 10 PSC 得 18.8 Ponero,当天额度用完。没有在游戏里验证:混币器账单(测试存档没解锁混币器)、商家订单、上架 / 改价费账单(代码路径和现货相同)。
- 多钱包:每个硬件钱包的余额、污点、成本、冻结各算各的。终端新增"钱包"页:列出全部钱包(名称、地址、估值、污点),"打开"弹出这个钱包自己的 PLEDGER 窗口(几台可以同时开),"连接"设为当前钱包。Web3 页的交易、黑市、混币器用当前钱包;用币付款的顾客、挖矿收益进当前钱包;订单退款、出售回款、混币提回回到当初付款的钱包。账单可以用任何一台钱包付。钱包之间转账走签名,污点按比例跟着走。
- Poinbase 拆成托管账户:银行存入进 Poinbase,出金审查只看 Poinbase 账户的污点;Poinbase 页"提到当前钱包"直接转,不用签名;钱包充值要转到 Poinbase 的充值地址(页面上显示),只收 CRC 和 PSC。
- PLEDGER 硬件钱包(物品):和游戏的升级材料一样,拖到交易终端上用掉,钱包 +1,最多 5 个。杰克逊博士进货时(钱包没满)50% 上一个,价值 240,正品;Web3 黑市平台现货每周 50% 上 1–2 个,150–180,其中 20% 被投毒。外观和说明完全一样。被投毒的钱包币的总价值到 3000 起,每晚有概率被转空(1/7 起,按价值线性升到 20000 时当晚必转),晨间日志只写"钱包 n:币被转走了"。
- 被投毒的钱包按期望天数算:门槛(3000)处期望 7 天,按价值线性降到 20000 处 1 天,每晚概率 = 1 / 期望天数(中间值期望 4 天、每晚 25%,模拟 4000 次平均 4 天)。
- 复制粘贴:终端账单的收款地址、Poinbase 充值地址、钱包页每个钱包、PLEDGER 主页的本钱包地址都有"复制";PLEDGER 发送页收款地址旁有"粘贴"。用的是系统剪贴板。
- PLEDGER 主页余额改成三列(币种 | 数量 | 估值)右对齐,五种币全部显示(含 0,冻结的另注);点一行选中,整行反色高亮,发送时默认用选中的币。
- 钱包页:每个钱包可以改名(留空恢复默认,最长 16 字),名字在终端和 PLEDGER 上都用;非主钱包可以销毁(点两次确认,钱包里还有币时不让销毁)。钱包编号固定,销毁别的钱包后名字不会错位。
- 游戏内验证(这一轮):复制钱包 3 地址后在主钱包发送页粘贴,地址正确;选中 CRT 行高亮,进发送页默认 CRT;向钱包 3 转 1 CRT 后点销毁被拦下;钱包 2 二次确认后销毁,钱包 3 仍叫"钱包 3";钱包 2 改名为 cold,终端与 PLEDGER 都显示新名字;三列余额对齐。PLEDGER 上名字被标题样式转成大写,已改成普通粗体(编译通过,没重进游戏看)。没有验证:游戏输入框里直接 Ctrl+V。
- 转账手续费档位(核对页选 慢 / 普通 / 快):0.5 / 1.5 / 4 信用点等值,用转出的币扣(DEX 签名用付出的币);需要的确认数随金额:3 次,≥ 2000 时 6 次,≥ 10000 时 12 次,动画随之变长。
- Gas 价格指数:7 天一个周期正弦涨跌(0.5×–1.5×),每天再乘 1 ± 15% 随机波动,夜间结算算出第二天的值;手续费 = 档位基础价 × gas(拥堵时再 ×4),核对页显示"Gas ×n"。游戏内验证:过一夜 gas ×1.34,普通档手续费 1.5 → 2.01 信用点。
- 网络拥堵:每晚 12% 开始,持续 1–2 天,上报纸;拥堵时手续费 ×4,普通档确认 ×2 慢、慢档 ×3.5 慢,慢档 30% 被挤出内存池(未成交,手续费照扣,账单留着可以重付)。PLEDGER 主页和核对页显示"网络拥堵"。
- 地址投毒:每笔转给外部地址的转账有 35% 被盯上,1–2 天后钱包收到一笔来自相似地址的 0 币转入(前 3 位和末 2 位校验位一样,缩写完全相同)。PLEDGER 发送页新增"最近"列表(← 转入 / → 转出,缩写地址),点一下填入地址,投毒地址混在里面;核对页对有往来的地址显示"有往来记录",投毒地址也会显示。
- 剪贴板木马:被投毒的钱包上点"粘贴",30% 把地址换成相似地址(和一次转空并存)。
- 钓鱼公告:每晚 5%,报纸上"Poinbase 充值地址迁移"或黑市论坛上"混币器新地址",给的是没人用的地址,转进去就没了。
- PLEDGER 屏上的选择按钮(币种、手续费档位):选中的一个白底黑框黑字,四角带取景框角标,不再用方括号。
- 游戏内验证(第三批):手续费 1.5 CRT 扣除;拥堵时核对页手续费 ×4、慢档提示;最近列表出现投毒的"← 0x123…99 0 PSC",点它进核对页显示真实地址 0x12342499 与"有往来记录",签名后币转进投毒地址;慢档连续 13 次里 4 次"未成交",手续费照扣;论坛出现钓鱼帖;选中样式截图看过。没有在游戏里看到:报纸上的拥堵 / 钓鱼新闻(F9 面板那次没打开,代码走的是已用过的新闻接口)、剪贴板木马(只有被投毒的钱包触发,没专门测)、大额转账的 6 / 12 次确认动画(单元测试覆盖确认数)。
- 被标记的钱包转出可追踪的币(CRC、PSC、CRT、CRA),收款钱包也被标记(含 Poinbase 账户);Ponero 不带标记。之前分成多钱包后,混币器在制裁期提回的币转进 Poinbase 就不再被标记,出金审查少了 30% 的加成。
- 多钱包分流复核(ops `research/walletsim`,真实 Wallet / Exchange 类,每种做法 300 局 × 60 天):把干净和脏收入分到两个钱包、每天按 9.9% 污点比例往 Poinbase 掺脏币,可以零风险每天洗出约 36 污点(60 天约 2200),上限约等于干净出金量的 11%;同样的进账用单钱包出金能洗出更多污点(2400–4100),但会被没收、脏币多时会记罪证。没有干净收入时分流洗不出任何东西;而阶段 5 之前就能做到的"银行存入干净信用点稀释"60 天洗出 1350。结论:多钱包没有打开新的大口子,零风险的涓流来自 10% 以下审查项为 0 的这道坎,阶段 5 之前就存在。
- 阶段 5 收尾补测(游戏内):用鼠标把两件硬件钱包(一正一毒)拖到终端上,各消耗一件、钱包页添加;杰克逊上货时出现了硬件钱包(需要多次触发,柜台满时会写"柜台已满");混币器账单、商家订单(模组,托管,订单页显示在途)、上架费、改价费(40 → 36)四种账单都在钱包里手输地址、签名后生效;夜里自然触发了地址投毒(刚付过的商家 0x149caf58 → 相似地址 0x149ff558 转入 0 币);被投毒的钱包上连续粘贴 10 次,有 5 次地址被换成相似地址(前 3 位与末位相同)。没有在游戏里验证:杰克逊柜台上钱包的标价(探查通道看不到价格,只有物品没有标价标签)、被标记状态传播(只有单元测试)、报纸上的拥堵 / 钓鱼新闻(夜里没抽到)、Ctrl+V 直接粘贴到输入框。调试面板新增"Give terminal",调试解锁同时解锁混币器。
- 存档不兼容旧版:链上的币归到新建的主钱包,Poinbase 账户从 0 开始。
- 游戏内验证(多钱包):两件硬件钱包(一正一投毒)作用于终端,钱包页出现钱包 2、钱包 3,标记看不出来;同时打开两台 PLEDGER;主钱包向钱包 3 转 100 PSC,核对页显示"自己的钱包:钱包 3",污点 5000 按 10000/16000 带走 3125;过一夜钱包 3 被转空(约 9393),晨间日志有记录;银行存入 500 进 Poinbase,提 200 CRC 到主钱包,再从主钱包向充值地址转 100 CRC,Poinbase 300 → 400,带过去污点 31。商城平台现货出现硬件钱包(158)。没有在游戏里验证:用鼠标拖拽(用探查通道执行了同一个"物品作用于物品"接口)、杰克逊柜台上的钱包和它的标价、Poinbase 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。
### 第四阶段:Web3 黑市与 Ponero(进行中)
- 人脉网络新增"Web3 黑市"(前置:Web3 治理社区,1500):黑市派系在链上的实体;放在终端 Web3 页的"黑市"子页里,需要黑市声望 ≥ 10,镇压期间下线。
- 现货柜台:每周刷新一批违禁品(游戏已有的 32 种,低 / 中 / 高三档随声望 10 / 40 / 80 放开;件数 4 + 声望/40),价格 = 游戏基础价 × 1.3,只收 PSC / CRT / CRA / Ponero,不收 CRC。付出去的币连同按比例的污点留在卖家那边,货放进储藏区,不带赃物标签。
- Ponero 兑换处(声望 ≥ 30):任意币兑入 Ponero 时这部分价值的污点留在原地,兑入、兑出各收 10%,每天额度 600 信用点(收尾核算后由 8% / 1000 调整);Ponero 价 = PSC × 0.5。从 Ponero 兑出的币在出金审查里按"刚出混币器"加重。
- 游戏内验证:声望 75 时上架 5 种;用 CRT 买一把左轮 312 点,余额 3000 → 2688,污点 2000 → 1875(按付款占钱包的比例);5 PSC 兑入得 9.2 Ponero(500 × 0.92 / 50),当天额度剩 500。
- 订单:黑市页新增"订单"和"我的订单与情报"。目录每周刷新,4 个卖家(成交数、好评率公开,15% 是骗子,多半是新号,也有刷过好评的老号),价格 = 基础价 × 0.85。托管多付 5%,收货后放款,卖家跑路时退款(污点与成本一并还回,不能借退款洗钱);提前放款再打九折,只有成交 ≥ 30 的卖家接受,骗子拿钱就跑。
- 配送:送货上门 3 天,到货后开店时快递员作为顾客上门,对话结束把货放进储藏区;死投点 2 天,到货后晚上去垃圾场,货进外出背包。上门被截获 8%,死投 3%,死投另有 12% 被别人先挖走;镇压期间到期顺延、截获 +15%。
- 平台跑路:每晚概率 0.2% + 在途款项每万信用点 0.4%(封顶 5%),跑路后在途款项归零、目录与现货清空、入口关闭 10 天。论坛情报:骗子卖家有一半概率被人点名,跑路风险升高时出现提币变慢的帖子。
- 游戏内验证:下单后在途显示在"我的订单";过夜到期,两单到货,一单骗子卖家托管退款(CRT +143,污点 +57);快递员对话走完,货进储藏区,快递员自动离开。没有在游戏里验证:死投点取货(要去垃圾场)、平台跑路与截获的实际触发(只有单元测试)。
- 商城改成货架式:每行 4 张商品卡(游戏物品贴图、名称、类别、库存或卖家、价格、按钮);删掉死投点,只剩送货上门;货架上除违禁品外偶尔有高科技物品(声望 ≥ 40,加价 50%)和显卡(加价 20%,20% 是假卡)。
- 界面不放说明文字:银行、交易、混币器、黑市、Ponero 的规则说明都移进新物品"Web3 手册"(购买 Web3 治理社区时一并交付)。
- 游戏内验证:货架显示贴图与成色,用 CRT 买一张 RTX 60(232)进储藏区。
- 第三批:执法蜜罐——每晚有概率接管平台(1% + 治安声望 × 0.04%,封顶 6%),持续 6 天,期间下的单和之后 4 天到期记罪证(违禁品 / 高科技记 `CONTRABAND_TRAFFICKING`,显卡记 `FENCING`,金额 = 货款,上限 400),论坛出现"最近太顺了"。卖家落网——近期成交的卖家每晚 0.8% 被抓,他留存的订单 4 天后连累买家,论坛有传闻。
- 受控交付:被截获的包裹有 35% 变成执法人员上门:快递员神色异常,出现"签收。/不收,退回去。"两个选项;签收当场记罪证并查封,拒签只损失货款。
- 挖矿报告并进 Web3 页的"挖矿"子页,终端顶栏不再有"报告"。
- 游戏内验证:受控交付的对话与两个选项显示,拒签后快递员离开、下一位顾客进来。没在游戏里验证:签收的罪证、蜜罐与落网的夜间追查(只有单元测试)。
- 货架重做:违禁品池只留能转手的货(枪械、钥匙卡、密码解序器芯片、爆炸物),弹药、毒品、药品、化学品、种子全部移除;高科技物品池换成矿机能用的东西(7 种系统模组、能量电池、能量阵列、高级模组提取器、矿架);显卡与高科技各占 30%,高科技不再需要中档声望。
- 玩家出售:黑市页新增"出售"子页。储藏区里的违禁品、高科技物品和显卡做成货架,选标价倍率 0.6–1.8(相对参考价;显卡按标称型号与成色)上架,收 5% 上架费。每晚按期望天数成交:≤0.9 倍 1 天,1.0 倍 2 天,1.8 倍 14 天,之间插值。成交扣 8% 抽成,货款托管 3 天后以 CRT 进钱包。货款带污点的概率和比例随倍率从 20%/20% 涨到 90%/80%。
- 出售的风险:假卡有 50% 被买家看穿退货,好评 −10(好评影响成交速度,每笔成交 +1);平台跑路时上架的货和未放款的货款都没了;蜜罐期间成交的货款和近期买家落网会被追查记罪证;镇压期间平台不成交。"我的订单与情报"页多出"我的上架",可以撤下。
- 游戏内验证(全部商家逐家点开截图):21 家商家(18 类 + 大宗 + 尖端店铺 + 2 位个人卖家)的货架都显示正常;尖端店铺卖全新 RTX 90(5139)和超科技储藏区(10265);个人卖家混着药、模组、水培、显卡,价格各异。
- 游戏内验证:货架显示显卡、模组、能量电池;买一张卡后出售页列出储藏区里的货,上架(标价 400,上架费 20),过夜成交(货款 368),3 天后到账(CRT 2804 → 3172,这次随机没带污点)。没有在游戏里验证:假卡退货、跑路时的出售损失(只有单元测试)。
- 收尾核算(`research/sim/bm_econ.py`,按 Tuning 常量算期望):Ponero 来回 15.4%,与混币器洗掉 80% 污点的 15–18% 相当,但当天完成、清得彻底、不受制裁,所以手续费 8% → 10%(来回 19%),日额度 1000 → 600(日额度按钱包污点占比估,约够清掉稳态的污点流入,后期会成为瓶颈);出售的最优倍率取决于玩家的资金机会成本:每天 1% 时 ×1.8,2–3% 时 ×1.5,倍率不是越高越好;订单:托管期望净 +0.02、提前放款 +0.07(现货 −0.30),所以订单是便宜进货,不是赚钱路线;订单进货再挂高价出售的套利每件约 +0.13–0.18(20 天一轮,资金占用),量小,暂不限制上架数。
- 订单改成商家货架(用户意见):黑市页"订单"改名"商家",每周从 6 种类型里抽 5 家(全新矿卡经销商、二手矿卡贩子、模组高科技、军火、证件芯片、药物),另有 25% 的周出现一家大宗商家(库存 10–25 件、价格系数 0.55–0.75、送货 6 天)。每家有自己的价格系数(订单价 = 基础价 × 系数,一般 0.70–0.95,全新卡 0.85–1.00,二手卡 0.65–0.90),每家 4 种货、库存 3–6 件。一次订 5 件以上 95 折、10 件以上 9 折。界面:件数按钮 ×1 / ×5 / ×10,商家按钮,所选商家的货架。药物(药片、梦尘、管制化学品)只在药物商家和大宗商家出现,也能转手出售。每周货架总值在游戏里抽样 60 周平均 13.2k(目标 1.4 万)。
- 商家类型扩到 18 种 + 大宗(用户意见,对照游戏的物品目录和玩家职业):全新矿卡经销商、二手矿卡贩子、矿架与能源、模组、军火(枪、手雷、炸药、刀具、电击枪)、枪匠配件(枪械配件、3D 打印芯片与模组、枪械零件)、证件芯片、药物(药片、注射器、血袋、手术工具)、化学品(化学用品、助熔剂、化学设备模组、量筒试剂瓶)、酿酒(酵母、荧光莓、啤酒、酒瓶)、水培农场(种子、水培模组、植物分析仪)、养殖(鱼卵、鱼子酱、循环水养殖系统、血清、屠宰机)、矿工(矿石、金属锭、电子元件、拾荒扫描仪)、水务(滤水、净水、储水、检测)、仓储(走私暗格、储藏区扩建、军用背包)、工具(涡轮增压器、等离子切割机、X 射线扫描仪、心智重编程器)、奢侈品(香水、精华液、香烟)。每周 7 家:2 家从矿卡 / 矿架 / 模组 4 类里抽(保证矿工路线总有货),其余 5 家从其余类型里抽;每家 3 种货,库存 2–5 件,单件基础价超过 4000 的不上。游戏里抽样 60 周货架总值平均 13.2k(目标 1.4 万)。药物改成并入药物商家,现货柜台的货不变。所有货都能转手出售。
- 尖端店铺与个人卖家(用户意见):贵货(单件基础价 ≥ 2000:超科技涡轮增压器、超科技储藏区、心智重编程器、顶级显卡等)不再被一刀切掉,改由"尖端店铺"卖:每周只有 15% 的概率出现,2 种货、库存 1–2 件、价格系数 0.85–1.00、送货 5 天;顶级全新显卡也在这里。个人卖家:每周固定 2 位,什么都卖(可能是药、模组、显卡、仍有 10% 混进贵货),每件价格系数各自随机 0.50–1.20(捡漏或被宰),库存 1–2 件,是骗子的概率 25%,都是新号所以只接受托管。常规商家仍只卖基础价 ≤ 4000 的货。
- 黑市标签整理(用户意见):原来的"现货柜台 / 商家 / 出售 / 我的订单与情报 / Ponero 兑换处"改成三个对称的标签"买货 / 卖货 / 订单"。买货页里平台现货是第一家卖家("[平台] 0x…",立即到货,选它时不显示托管 / 提前放款),后面是各商家,件数按钮 ×1 / ×5 / ×10 现货也适用(一次买多件走一次付款确认);论坛情报放到买货页最下面;订单页分"买入"(订单)和"卖出"(上架、改价、撤下)上下两块。Ponero 不再是黑市里的标签:并进 Web3"交易"页的右栏,作为一个带"[匿名币]"标签的通用兑换块(选 PSC / CRT / CRA,兑入 / 兑出,要黑市声望,仍有每日额度和手输地址确认)。游戏内验证:新的三个标签和平台现货货架、订单页、交易页的 Ponero 块,兑入进入付款确认。
- 定价改成直接输价格(用户意见):点货架上的"上架"或"我的订单"里的"改价",出现"设定标价"页,显示参考价(真实价)和输入框;×0.6–×1.8 档位只是帮忙把价格填进输入框,也可以手输整数,输入后显示"标价 83(×1.38)";价格要在参考价的 0.6–1.8 倍之间(显示可输范围),确定后走付款确认(上架费 5% / 改价费 2% 按新标价算)。成交速度和货款污点都按 标价 ÷ 参考价 的实际比例连续计算(不再只有档位)。"我的订单"里每件上架的货显示"参考 60 标价 83(×1.38)",下面是"改价"和"撤下"。出售页顶上的档位按钮保留,作为默认倍率(预填价格、"全部上架"用)。游戏内验证:档位填价(×1.5 → 90)、手输 83(×1.38,上架费 5)、范围外(200)被拒并提示 36–108。
- 自己的储藏区和背包不再出现在可上架的货里,有内容的容器也不行(之前储藏区(扩建)会被列出来,上架会把里面的东西一起带走)。
- 上架后可以改价(用户意见):"我的订单与情报"页每件上架的货下面一排倍率按钮(0.6–1.8,当前的带方括号)和"撤下",点倍率先走付款确认(改价费 2%,`BmRepriceFee`,手输平台地址),标价按参考价重算,成交概率按新倍率走,上架日不变。游戏内验证:0.6 倍的 RTX 70 改成 ×1.2,改价费 12,标价 600。蜜罐基础概率维持 1%(模拟里进取策略罪证 ≥ 300 约 48 天,用户认为可接受)。
- 追查调整(经济模拟后):同一晚到期的追查按来源合成一次事件,记入货款的 40%(`BmTrapShare`),同一次蜜罐 / 落网累计最多 400(`BmTrapWindowCap`);没收到货的订单(骗子退款、被截获、跑路、拒签、被查封)取消对应追查。原来每单各记一次、每晚上限 400,一个蜜罐窗口能连续记几千,模拟里一局 120 天几乎一直在罪证线以上。
- 手输收款地址(用户意见):Web3 里的付款与转账——黑市现货、商家下单、上架费、混币器存入、Ponero 兑换——点下去先出"确认付款"页,显示摘要和收款地址(商家是名字里的 0x… ,平台、混币器、Ponero 兑换处各有自己的随机地址,存档保存),要手输地址才放款,不分大小写,输错提示"地址不符,没有付款",可取消。DEX 兑换和银行存取不加(没有收款方);交易所的转账等阶段 5 多钱包时再接同一个确认页。游戏内验证:现货买超频模组,输错一位被拒、输对成交;Ponero 兑换处进入确认页。
- 商家名用随机哈希匿名,前面用方括号标类型(用户意见,试过每类型 10 个主题店名后撤回):如 "[全新矿卡] 0xdf9ec7";选中的商家按钮前有 "> "。
- 每周货架总值放宽到 2 万左右(用户意见):库存范围 2–5 → 2–6,游戏里抽样 60 周平均 22.2k,尖端店铺出现概率保持 15% 不变,不靠它拉高周转量。
- 同时上架最多 10 件(用户意见),出售页显示"上架 n/10",超出时提示。
- 出售:货款污点随近 7 天成交额加重(成交额达 1.4 万时概率与比例各加 30%);出售页加"全部上架"按钮。套利核算(`bm_econ.py`,商家系数均值 0.825 再加批量折扣):普通商家提前放款进货 + ×1.5 出售每件约 +21%,大宗商家约 +33%,一周可买的货约 1.3 万,带污点的货款约够吃满 Ponero 日额度。
- 调整(用户意见):Ponero 手续费 10% → 6%(一进一出约 12%),日额度 600 → 1000;混币器每轮手续费 4% → 3%。黑市货架上的显卡有 15% 是全新卡(成色 100,封条完好,不会是假卡),标签显示"全新"。全新卡只有单元测试,没有在游戏里见过。
- 收尾验证(游戏内):黑市每周刷新(第 8 天自动刷出 5 种现货、8 种订单目录);蜜罐追查到期记罪证 300;假卡上架 8 件,有一件被看穿退货(好评下降);其余成交后到账,其中一笔带污点;受控交付选"签收。"记罪证 136、货被查封;镇压期间黑市页显示下线(改成实时读取游戏事件,原来要等过夜才更新)。
- 没有在游戏里验证:平台跑路时订单和出售一起清零(单元测试覆盖)、死投点已删除。
### 第三阶段:数值调优(进行中)
- 混币器每轮手续费 6% → 4%;用币付款的顾客概率提到上层 30%、黑市 50%、革命派 50%、下层 25%,并多付 12%(溢价只加在币数上,污点仍按货值算)。
- 区块奖励 90 → 100;RX800 耗电 12 → 14 kJ。
- 减半对币价的传导改成每次在 0.15–0.85 的指数里随机(锚 +11% 到 +80%),历次累计存档。
- 低档次的卡也会被冒充:新增废片芯片 RX10(不上架,只作假卡背后的真实芯片)。
- 型号档次与实物不符(供电口、风扇数)的卡,摆给顾客时必被看穿并扣信誉(原来 50%),堵住用标签机把低档卡改成高档型号转卖的漏洞。
- 显卡型号改名:Probidia RZX → RTX,PMD RZ → RX(RTX 60/70/80/90,RX 70/80/800/900);贴图重新生成。旧存档里的旧型号物品不再识别。
- 散落的常量收进 `Tuning.cs`:买家看穿率与信誉封顶、杰克逊博士上货、二手贩子来访 / 种类 / 加价 / 假卡 / 隐瞒、显卡价值下限、非主交易对套利与深度。型号表、成色折价、外观阈值是数据表,仍留在原文件。
- 单元测试 8909 项通过。游戏内验证:模组载入、型号改名后的卡名、币付款成交(4 件种子 118 信用点 → 132.16 CRT = ×1.12,污点按热度 50% 计 59)。成交后顾客那句话也看到了画面:原来的整句太长,被出售面板挡住,改成"成交,132 CRT。",累计一单的总数。没有在游戏里跑到:废片假卡、减半随机幅度(只有单元测试)。
### 第二阶段(进行中)
- 开发者模式 F9 新增 `Run night (test)`、`Morning report (test)`、`Newspaper (test)`(走游戏自己的夜间流程并进入下一天,事件按天数过期)和 `Start crackdown (test)`,用来测夜间逻辑;`Tainted wallet (test)` 现在同时给 1000 CRT。
- CRT 冻结:镇压事件开始时,按黑市声望(声望 0 时 40%,每点 −0.8%,50 以上不会)冻结钱包里一半的 CRT,3 天后解冻;冻结部分仍计入余额与估值,但不能交易、不能存混币器。Web3 页钱包下显示"CRT 冻结 N,第 X 天解冻";上报纸,夜间日志写冻结与解冻。开发者模式 F9 有 `Frozen CRT (test)`。游戏内验证了冻结显示、晨间日志里的冻结那行、超额卖出被拒、镇压事件触发冻结(黑市声望 0,多次镇压里命中一次)和 3 天后夜间解冻。税与稽查不做(游戏本身没有税)。
- 人脉网络里本 mod 单独一页"Web3":入口(400)和混币器(700,需先加入)都放在这页。
- 混币器:终端 Web3 页里多一个"混币器"子页(买了升级才出现,和"交易"并列)。选币种、数量、1–5 轮,存入后每轮扣 6% 手续费并降低污点(基准每轮 35%,存入越大清洗越差,匿名集 = 2 万信用点背景量 + 存入量),轮数即等待天数,到期提回同一个地址;提回后一周内出金审查按"来自混币器"加重。镇压事件开始时 50% 被制裁 5 天,期间提出的币地址被标记,并上报纸;每晚 0.4% 跑路。夜间日志写提回与污点变化。开发者模式 F9 有 `Tainted wallet (test)`。游戏内验证了解锁、页面、存入与污点带走,夜间提回(50 PSC 存两轮,当时手续费 2%,提回 48.02,污点 1500 → 777,与预览一致)和镇压触发制裁,报纸上的制裁新闻。
- 用币付款的顾客:排队的买家里,上层付 CRC(12%)、黑市付 CRT(25%)、革命派付 CRA(25%)、下层付 PSC(10%)。成交时扣回信用点(现金不变),按当日币价换成币进热钱包,顾客当场说一句"成交,N CRT 已转入热钱包";赃物款项不记销赃,改成币带污点(货值 × 赃物热度%);夜间日志汇总当天用币结算的额度。开发者模式 F9 有测试入口(Coin buyer)。已在游戏里实测:成交后现金不变,四笔种子按币价记入 CRT,顾客对话显示。
### 第一阶段完成(入门、挖矿、显卡鉴定、终端)
以下条目取代下面较早条目里已过时的部分(型号、入口价、终端页面):
- 显卡改成每个牌子四档(低端、中端、高端、旗舰):Probidia RTX 60 / 70 / 80 / 90,PMD RX 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,开局送交易终端、矿架、RTX 60 和鉴定手册。
- 经济模拟 v7(运维库 research/sim/out_v7.md):电网电价下 RTX 70 挖矿第 30 天占总收入 18%、第 60 天 20%。
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
- mod 本体(第一阶段的第一部分):人脉网络"Web3 治理社区"(300,整个 mod 的入口)交付交易终端和一张 RZX 50;矿架向杰克逊博士购买;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSC;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSC 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。
- mod 本体(第一阶段的第一部分):人脉网络"Web3 治理社区"(300,整个 mod 的入口)交付交易终端和一张 RTX 50;矿架向杰克逊博士购买;矿架是带电池槽和 4 个显卡槽的机器,每晚从电池扣电挖 PSC;显卡按型号显示算力、耗电与健康度,每晚损耗并可能烧毁;交易终端有钱包、交易(四个交易对、存入信用点、提取 CRC 过合规审查)、报告三页;夜间结算写晨间日志;PSC 交易所开业与减半上报纸;状态存进游戏的 mod 存档数据。中英双语。
- 物品图:art/sprites.py 生成显卡(Probidia、PMD、烧毁)、矿架、交易终端。
- 矿架加入游戏原有的通用模块槽:性能、效率、品质三项百分比折算 1/4 后作用于算力、耗电和显卡损耗,算力最多 +20%、省电最多 25%、损耗最多 −30%;性能加成同时提高损耗与暴毙率。取代原计划的 mod 自带超频模块。
- 杰克逊博士上货(游戏给 mod 的进货钩子,夜间去他店里或他每周来本店摆摊时):没有矿架时必有一个,有了之后 30% 再上;基础显卡每次两张;进阶显卡第 10 天起 60%(再 30% 多一张);旗舰第 30 天起 25%,特供货事件时第 20 天起必有。全新真货,价格为物品价值加他的加价。加入 Web3 治理社区后才上货。开发者模式 F9 有测试入口。
@@ -16,4 +462,4 @@
- 矿架默认 2 个显卡槽,投入电子元件升级,最多 5 个:第 3 槽 4 个普通电子元件,第 4 槽 1 个稀有,第 5 槽 2 个稀有。窗口里新增"升级槽位",只收下一级需要的元件,放进去立即消耗并累计进度;显卡格子的形状随槽数变化,槽数与进度存在矿架标签上。
- 矿架的"基础耗电"标签随显卡变化(机身 + 各卡耗电,不含模块),游戏装卸模块时据此重算,不会把耗电改回只有机身的 2 kJ。
- Web3 治理社区改为同时送一个矿架:交易终端、矿架、一张 RZX 50,送的货值 520;解锁价定为 400(见运维库定价说明)。
- Web3 治理社区改为同时送一个矿架:交易终端、矿架、一张 RTX 50,送的货值 520;解锁价定为 400(见运维库定价说明)。
+12
View File
@@ -0,0 +1,12 @@
# 项目规则
## 数值
- 数值全部放在同一个配置类里(`src/Decentralized.Domain/Tuning.cs`,可以用嵌套类按主题划分),代码、测试里不写魔数。
- 手册、说明文字里的数值不手写:用占位符 `{Name}`、`{Name:%}`、`{Name[2]:%}`(嵌套类用 `{Section.Name}`),由 `TuningText.Render` 在显示时从配置类取值。数值一改,文字跟着变。
- 新增数值:先加进 `Tuning`,再登记到运营仓库的 `docs/tuning-index.md`,说明文字里用占位符引用。
- 单元测试(`TuningText` 那一条)检查中英文手册的占位符都取得到值。
## 文案
- 界面标签用名词,提示用陈述句,不口语化;NPC 对话和报纸正文除外。界面不放说明文字,规则说明写进手册。
+20 -5
View File
@@ -1,6 +1,6 @@
# Probably Decentralized
A mod for *Probably Stolen*: crypto mining, a fictional coin market, stablecoins, laundering and its risks, built on the game's own power, events and law systems. Work in progress; nothing is released yet.
A mod for *Probably Stolen*: crypto mining, a fictional coin market, stablecoins, laundering and its risks, a Web3 black market, built on the game's own power, events and law systems. Current version: 2.0.0.
All coins, exchanges and events are fictional and have no real-world value.
@@ -8,10 +8,25 @@ All coins, exchanges and events are fictional and have no real-world value.
| Path | Content |
|---|---|
| `src/Decentralized.Domain` | Pure logic (assets and ledger, AMM pairs, market, mining). No game or Unity references. |
| `src/Decentralized` | The mod: items (trading terminal, mining rig, graphics cards), nightly settlement, terminal window, news. Builds into `<game>/Mods/ProbablyDecentralized` (`dotnet build src/Decentralized -p:GameDir=...`). |
| `src/Decentralized.Domain` | Pure logic (assets and ledger, AMM pairs, market, mining, graphics card tiers, appearance, labels and serials). No game or Unity references. |
| `src/Decentralized` | The mod: items (trading terminal, mining rig, graphics cards, appraisal guide), used-card dealer, magnifier inspection view with the label bar, nightly settlement, terminal (bank, Poinbase, Web3 with trading, mining, mixer and the black market), black-market shelves, orders, courier and selling, Ponero desk, Web3 handbook, news. Builds into `<game>/Mods/ProbablyDecentralized` (`dotnet build src/Decentralized -p:GameDir=...`). |
| `tests/Decentralized.Domain.Tests` | Console test runner: `dotnet run --project tests/Decentralized.Domain.Tests` |
| `art/sprites.py` | Generates the item sprites into `src/Decentralized/ModFiles/Textures/Items`. |
| `art/sprites.py`, `art/big.py`, `art/big_cases.py` | Generate the item sprites, the large inspection art and its overlays, and the terminal icons into `src/Decentralized/ModFiles/Textures/Items`. |
## Settings
Mod settings are in the game's mod list (select the mod, gear button); they apply to all saves.
| Section | Setting | Values |
|---|---|---|
| Interface | Trading terminal size | 828×680, 1200×900 (default), 1400×1000. Layout and text scale with the window; applies when the terminal is next opened. |
| Story | Tumbler storyline | on / off. Off stops new visits; progress already made is kept. |
| Difficulty | Risk multiplier | ×0.5 / ×1 / ×1.5 / ×2 on the chance of scams, rug pulls, wallet poisoning, address hijacking and phishing. |
| Difficulty | Mining reward multiplier | ×0.5 / ×1 / ×1.5 / ×2 on the block reward paid to the player's rigs. |
## Languages
The interface follows the game language: en, zh, de, fr, es, pl, ru, ja. English is the source text; zh is hand-written; the other six are AI translations and have not been proofread. Tables are in `src/Decentralized/ModFiles/Localization/<code>.csv`.
## License
@@ -19,4 +34,4 @@ MIT, see `LICENSE`.
---
《Probably Stolen》的 mod:挖矿、虚构币市场、稳定币、洗钱及其风险,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。
《Probably Stolen》的 mod:挖矿、虚构币市场、稳定币、洗钱及其风险、Web3 黑市,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。
Binary file not shown.
Binary file not shown.
Binary file not shown.
+443
View File
@@ -0,0 +1,443 @@
#!/usr/bin/env python3
"""显卡检视大图(192×128):放大镜检视台里用的细节图,不是小图的放大版。
正面:七叶风扇、鳍片、热管、供电口、PCB 元件与逐根金手指;背面:拉丝背板、丝印凹槽、二维码与条码贴纸、封条。
叠加图与底图同尺寸、坐标一致,只画变化的像素。"""
import math
import random
from sprites import Canvas, C, key, MODELS
BW, BH = 192, 128
# 风扇布局(按风扇数):中心与半径;FANS / FR 保留为双风扇的值,给叠加图模块当默认
FAN_LAYOUT = {1: ([(105, 55)], 34), 2: ([(68, 51), (142, 51)], 30), 3: ([(52, 51), (96, 51), (140, 51)], 21)}
FANS, FR = FAN_LAYOUT[2][0], FAN_LAYOUT[2][1]
SH0, SH1 = (24, 14), (186, 96) # 散热罩 左上 / 右下
FING_Y0, FING_Y1 = 109, 122
NOTCH_X = 112
# 5×7 像素字(丝印与品牌字)
F57 = {
'R': ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
'Z': ['11111', '00001', '00010', '00100', '01000', '10000', '11111'],
'X': ['10001', '10001', '01010', '00100', '01010', '10001', '10001'],
'P': ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
'M': ['10001', '11011', '10101', '10101', '10001', '10001', '10001'],
'D': ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
'T': ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
'i': ['00100', '00000', '01100', '00100', '00100', '00100', '01110'],
'O': ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
'B': ['11110', '10001', '10001', '11110', '10001', '10001', '11110'],
'I': ['01110', '00100', '00100', '00100', '00100', '00100', '01110'],
'A': ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
'0': ['01110', '10001', '10011', '10101', '11001', '10001', '01110'],
'1': ['00100', '01100', '00100', '00100', '00100', '00100', '01110'],
'2': ['01110', '10001', '00001', '00110', '01000', '10000', '11111'],
'3': ['11110', '00001', '00001', '01110', '00001', '00001', '11110'],
'4': ['00010', '00110', '01010', '10010', '11111', '00010', '00010'],
'5': ['11111', '10000', '11110', '00001', '00001', '10001', '01110'],
'6': ['00110', '01000', '10000', '11110', '10001', '10001', '01110'],
'7': ['11111', '00001', '00010', '00100', '01000', '01000', '01000'],
'8': ['01110', '10001', '10001', '01110', '10001', '10001', '01110'],
'9': ['01110', '10001', '10001', '01111', '00001', '00010', '01100'],
}
def text57(c, x, y, s, color, scale=1, skew=False, gap=1):
i = 0
for ch in s:
if ch == ' ':
x += 3 * scale
continue
dy = (i % 2) * scale if skew else 0
for r, row in enumerate(F57[ch]):
for k, bit in enumerate(row):
if bit == '1':
c.rect(x + k * scale, y + r * scale + dy, x + k * scale + scale - 1, y + r * scale + scale - 1 + dy, color)
x += (5 + gap) * scale + (scale if skew and i % 3 == 2 else 0)
i += 1
def outline(c, col='O'):
solid = lambda v: v is not None
get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None
edge = [(x, y) for y in range(c.h) for x in range(c.w)
if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))]
for x, y in edge:
c.put(x, y, col)
def bracket(c):
"""左端 I/O 挡板:折边、三个接口(HDMI 与两个 DP,内腔与触片)、散热网孔、底部螺丝。"""
c.rect(2, 8, 22, 112, 'b')
c.rect(2, 8, 4, 112, 'l'); c.rect(2, 8, 2, 112, 'h') # 受光折边
c.rect(19, 8, 22, 112, 'd'); c.rect(21, 8, 22, 112, 'g') # 背光折边
for x in range(5, 19): # 拉丝
for y in range(9, 112, 2):
c.put(x, y, 'b' if (x + y) % 5 else 'l')
for i, y in enumerate((16, 36, 56)): # 三个接口
c.rect(6, y, 17, y + 13, 'k')
c.rect(6, y, 17, y, 'g'); c.rect(6, y, 6, y + 13, 'g')
c.rect(7, y + 1, 16, y + 12, 'n')
if i == 0: # HDMI:两侧斜角
for k in range(3):
c.put(7 + k, y + 12 - k, 'k'); c.put(16 - k, y + 12 - k, 'k')
c.rect(9, y + 4, 14, y + 6, 'z')
else: # DP:一边切角
c.put(16, y + 12, 'k'); c.put(15, y + 12, 'k'); c.put(16, y + 11, 'k')
c.rect(9, y + 4, 14, y + 5, 'z')
for x in range(8, 16, 2):
c.put(x, y + 9, 'Y')
for y in range(78, 104, 4): # 散热网孔
for x in range(7, 17, 3):
c.rect(x, y, x + 1, y + 2, 'k')
c.put(x, y, 'x')
c.rect(9, 106, 14, 110, 'm'); c.rect(10, 107, 13, 109, 'g') # 底部螺丝
c.put(11, 108, 'k'); c.put(12, 108, 'k'); c.put(11, 107, 'b')
c.rect(9, 10, 14, 12, 'k')
def fan(c, cx, cy, accent, accent_dark, R=None):
"""风扇:九片弯曲扇叶(前缘高光、叶面渐变、叶尖暗边),外圈带四个固定螺丝,轴心带品牌圆标。"""
R = R or FR
for y in range(cy - R - 1, cy + R + 2):
for x in range(cx - R - 1, cx + R + 2):
dx, dy = x - cx, y - cy
r = math.hypot(dx, dy)
if r > R + 0.5:
continue
ang = (math.atan2(dy, dx) / (2 * math.pi)) % 1.0
light = -(dx + dy) / (1.4 * R) # 左上为正
if r >= R - 3.2: # 外圈
c.put(x, y, 'h' if light > 0.5 else 'l' if light > 0.1 else 'b' if light > -0.4 else 'm')
elif r >= R - 5.5: # 风道阴影
c.put(x, y, 'n' if light < 0.2 else 'k')
elif r <= 6.5: # 轴心
if r <= 4.3:
c.put(x, y, accent if light < 0.3 else accent_dark if r > 3 else 'w')
else:
c.put(x, y, 'h' if light > 0.2 else 'm' if light > -0.3 else 'd')
else:
t = (ang * 9 + (r - 7) * 0.060) % 1.0
if t < 0.62:
if t < 0.06:
col = 'h'
elif t < 0.40:
col = 'l' if light > 0.25 else 'b' if light > -0.2 else 'm'
else:
col = 'b' if light > 0.2 else 'm' if light > -0.3 else 'g'
if r > R - 8:
col = 'g' if col in ('m', 'g') else 'm'
c.put(x, y, col)
else:
c.put(x, y, 'n' if t < 0.9 else 'k')
for k in range(4): # 外圈固定螺丝
a = math.pi / 4 + k * math.pi / 2
sx, sy = int(cx + math.cos(a) * (R - 1.5)), int(cy + math.sin(a) * (R - 1.5))
c.rect(sx - 1, sy - 1, sx + 1, sy + 1, 'k')
c.put(sx, sy, 'g')
def gpu_front(accent, accent_dark, brand_text, tier=1, burnt=False, count=None, pin=None):
"""tier 决定实物特征:风扇数(1/2/3/3)与供电口(6 针 / 8 针 / 8 针 / 双 8 针)。"""
count = count or (1, 2, 3, 3)[tier]
pin = (0, 1, 1, 2)[tier] if pin is None else pin
fans, R = FAN_LAYOUT[count]
c = Canvas(BW, BH)
bracket(c)
x0, y0 = SH0
x1, y1 = SH1
# 散热罩:石墨底,顶部受光亮边,左缘高光,右下阴影;右上角切斜
for y in range(y0, y1 + 1):
for x in range(x0, x1 + 1):
if x > x1 - 8 and y < y0 + (x - (x1 - 8)): # 右上切角
continue
base = 'z'
if y < y0 + 3: base = 'h' if x < x1 - 40 else 'l'
elif y < y0 + 6: base = 'l' if x < x1 - 40 else 'b'
elif y < y0 + 9: base = 'x'
elif y > y1 - 4: base = 'n'
c.put(x, y, base)
for y in range(y0 + 3, y1): # 左缘
c.put(x0, y, 'l'); c.put(x0 + 1, y, 'b'); c.put(x0 + 2, y, 'x')
# 右端鳍片
for x in range(x1 - 18, x1 + 1):
for y in range(y0 + 10, y1 - 4):
if x > x1 - 8 and y < y0 + (x - (x1 - 8)) + 2:
continue
c.put(x, y, 'x' if (x - (x1 - 18)) % 3 == 0 else 'g' if (x - (x1 - 18)) % 3 == 1 else 'n')
c.rect(x1 - 20, y0 + 10, x1 - 19, y1 - 4, 'x')
# 风扇之间的中筋
for a, b in zip(fans, fans[1:]):
mx = (a[0] + b[0]) // 2 + 1
c.rect(mx - 1, y0 + 8, mx, y1 - 5, 'k'); c.rect(mx + 1, y0 + 8, mx + 1, y1 - 5, 'x')
for cx, cy in fans:
fan(c, cx, cy, accent, accent_dark, R)
# 前沿斜切装饰条:品牌色 + 品牌字
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, 30 if count == 1 else 112, 84, brand_text, 'h') # 单风扇的大风扇占了右边,品牌字改放左下
# 顶沿:四根铜热管端头与 8 针供电口
for i in range(4):
x = 38 + i * 9
c.rect(x, 7, x + 6, y0 - 1, 'p'); c.rect(x, 7, x + 1, y0 - 1, 'h' if i == 0 else 'p')
c.rect(x + 5, 7, x + 6, y0 - 1, 'q'); c.rect(x, 7, x + 6, 7, 'q')
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针
for left, pins in ((((118, 6),) if pin == 0 else ((112, 8),) if pin == 1 else ((78, 8), (126, 8)))):
right = left + pins * 5 + 2
c.rect(left, 5, right, y0 - 1, 'n'); c.rect(left, 5, right, 6, 'z')
for i in range(pins):
c.rect(left + 2 + i * 5, 7, left + 4 + i * 5, 9, 'k'); c.put(left + 3 + i * 5, 8, 'Y')
# PCB:阻焊绿、边缘高光、元件与走线
for y in range(y1 + 1, FING_Y0):
for x in range(x0, x1 + 1):
c.put(x, y, 'G' if y < y1 + 4 else 'g2')
for x in range(x0, x1 + 1):
c.put(x, y1 + 1, 'T') if False else None
random.seed(7)
for i in range(14):
sx = x0 + 6 + i * 11
c.rect(sx, y1 + 3, sx + 3, y1 + 5, 'k' if i % 3 else 'b')
c.rect(sx, y1 + 3, sx + 3, y1 + 3, 'x')
for k in range(4):
c.put(sx + k, y1 + 7, 'Y' if k % 2 == 0 else 'g2')
# 金手指:逐根(3 像素宽,1 像素缝),PCIe 键位缺口
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(FING_Y0, FING_Y1 + 1):
c.put(x, y, 'h' if y == FING_Y0 else 'Y' if (x - x0) % 4 != 0 else 'p')
c.put(x, FING_Y1, 'e')
c.rect(x0, FING_Y0, x1, FING_Y0, 'g2')
for y in range(FING_Y0 + 1, FING_Y1 + 1): # 缝里露出 PCB
for x in range(x0 + 2, x1 - 2):
if c.px[y][x] is None and not (NOTCH_X - 2 <= x <= NOTCH_X + 3):
c.put(x, y, 'g2')
if burnt:
for cx, cy in FANS:
for dx, dy in ((0, 0), (5, -5), (-6, 6), (8, 4), (-9, -3), (2, 10)):
c.rect(cx + dx - 2, cy + dy - 2, cx + dx + 2, cy + dy + 2, 'c')
for x in range(x0 + 4, x1 - 20):
c.rect(x, y1 - 5, x, y1 - 3, 'e')
for dx, dy in ((30, 70), (90, 40), (150, 80), (60, 30), (120, 88)):
c.rect(dx, dy, dx + 4, dy + 3, 'c')
outline(c)
return c
def screw(c, x, y):
c.rect(x - 3, y - 3, x + 3, y + 3, 'm')
c.rect(x - 3, y - 3, x - 1, y - 3, 'h')
c.rect(x - 2, y - 2, x + 2, y + 2, 'b')
c.rect(x - 2, y, x + 2, y, 'k'); c.rect(x, y - 2, x, y + 2, 'k')
c.rect(x + 2, y + 2, x + 3, y + 3, 'd')
def qr(c, x, y, seed):
rnd = random.Random(seed)
n = 21
c.rect(x - 1, y - 1, x + n, y + n, 'w')
for j in range(n):
for i in range(n):
on = rnd.random() < 0.5
for fx, fy in ((0, 0), (n - 7, 0), (0, n - 7)):
if fx <= i < fx + 7 and fy <= j < fy + 7:
li, lj = i - fx, j - fy
on = li in (0, 6) or lj in (0, 6) or (2 <= li <= 4 and 2 <= lj <= 4)
elif fx - 1 <= i <= fx + 7 and fy - 1 <= j <= fy + 7:
on = False
if on:
c.put(x + i, y + j, 'n')
def sticker(c, seed):
"""序列号贴纸(右下):二维码、条码、两行小字。"""
c.rect(86, 64, 172, 100, 'w')
c.rect(86, 64, 172, 65, 'h'); c.rect(86, 99, 172, 100, 'l')
c.rect(86, 64, 87, 100, 'h'); c.rect(171, 64, 172, 100, 'l')
qr(c, 91, 70, seed)
for x in range(118, 166):
w = 1 + ((x * 7 + seed) % 3)
if (x * 5 + seed) % 4 != 0:
c.rect(x, 70, x, 84, 'n')
def seal(c):
"""封条(红底白字):压住贴纸右边缘。"""
c.rect(174, 60, 182, 104, 'R')
c.rect(174, 60, 175, 104, 'r2'); c.rect(181, 60, 182, 104, 'r2')
for y in range(66, 100, 8):
c.rect(176, y, 180, y + 3, 'w')
c.put(178, y + 1, 'R')
def gpu_back(accent, accent_dark, burnt=False):
c = Canvas(BW, BH)
bracket(c)
x0, y0 = SH0
x1, y1 = SH1
for y in range(y0, y1 + 12):
for x in range(x0, x1 + 1):
c.put(x, y, 'd' if y % 2 else 'm' if (x // 3) % 4 else 'd')
for y in range(y0, y1 + 12):
c.put(x0, y, 'l'); c.put(x0 + 1, y, 'b'); c.put(x1, y, 'k'); c.put(x1 - 1, y, 'z')
for x in range(x0, x1 + 1):
c.put(x, y0, 'h'); c.put(x, y0 + 1, 'l'); c.put(x, y0 + 2, 'b')
# 丝印凹槽(型号叠加图写在这里)
c.rect(36, 24, 178, 52, 'z')
c.rect(36, 24, 178, 25, 'k'); c.rect(36, 24, 37, 52, 'k')
c.rect(36, 52, 178, 52, 'x'); c.rect(178, 24, 178, 52, 'x')
# 品牌色斜条
for x in range(x0 + 4, x1 - 4):
c.rect(x, 56, x, 59, accent)
c.put(x, 60, accent_dark)
# 通风槽(左下)
for y in range(70, 100, 6):
c.rect(x0 + 10, y, x0 + 50, y + 3, 'k')
c.rect(x0 + 10, y + 4, x0 + 50, y + 4, 'x')
# 四角螺丝
for sx, sy in ((x0 + 12, y0 + 10), (x1 - 12, y0 + 10), (x0 + 12, y1 - 2), (x1 - 12, y1 - 2)):
screw(c, sx, sy)
sticker(c, 5)
seal(c)
# 背面金手指(暗一档):触点之间是 PCB 的深绿,只有键位缺口是空的
for y in range(y1 + 12, FING_Y1 + 1):
for x in range(x0, x1 + 1):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 and y > y1 + 13:
continue
c.put(x, y, 'g2')
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(y1 + 14, FING_Y1 + 1):
c.put(x, y, 'Y' if (x - x0) % 4 else 'p')
if burnt:
for dx, dy in ((60, 30), (130, 40), (50, 80), (100, 70), (150, 90)):
c.rect(dx, dy, dx + 6, dy + 4, 'c')
outline(c)
return c
def new():
return Canvas(BW, BH)
# ---------- 叠加图 ----------
def ov_fan(kind):
c = new()
rnd = random.Random(11 + kind)
for cx, cy in FANS:
if kind == 1: # 积灰:叶片上一层灰点和灰絮
for _ in range(190):
a = rnd.random() * math.tau
r = 8 + rnd.random() * (FR - 12)
x, y = int(cx + math.cos(a) * r), int(cy + math.sin(a) * r)
c.put(x, y, 'l' if rnd.random() < 0.6 else 'h')
if rnd.random() < 0.3:
c.put(x + 1, y, 'l')
for _ in range(10): # 絮状长条
a = rnd.random() * math.tau
r = 10 + rnd.random() * (FR - 16)
x, y = int(cx + math.cos(a) * r), int(cy + math.sin(a) * r)
for k in range(5):
c.put(x + k, y + (k // 2), 'w' if k % 2 == 0 else 'l')
else: # 缺角:一片叶尖缺一块,露出风道黑底
a0 = 0.9 if cx < 100 else 3.2
for r in range(10, FR - 3):
for da in (-0.20, -0.17, -0.13, -0.10, -0.06, -0.02, 0.02, 0.06, 0.10, 0.14):
if r > FR - 14 + (da * 30) and r > 17:
c.put(int(cx + math.cos(a0 + da) * r), int(cy + math.sin(a0 + da) * r), 'n')
for r in range(17, FR - 3):
c.put(int(cx + math.cos(a0 - 0.22) * r), int(cy + math.sin(a0 - 0.22) * r), 'h')
return c
def ov_fingers(kind):
c = new()
rnd = random.Random(21 + kind)
x0, x1 = SH0[0], SH1[0]
for x in range(x0 + 2, x1 - 2):
if NOTCH_X - 2 <= x <= NOTCH_X + 3 or (x - x0) % 4 == 3:
continue
for y in range(FING_Y0 + 1, FING_Y1 + 1):
if kind == 1: # 发暗:触点变褐
c.put(x, y, 'e' if (x + y) % 3 else 'q')
else: # 氧化:发绿的斑块
if rnd.random() < 0.65:
c.put(x, y, 'g2' if rnd.random() < 0.5 else 'G')
return c
def ov_bracket(kind):
c = new()
rnd = random.Random(31)
if kind == 1: # 划痕
for _ in range(9):
x, y = rnd.randint(5, 17), rnd.randint(14, 100)
for k in range(rnd.randint(5, 12)):
c.put(x + k // 2, y + k, 'h' if k % 2 else 'k')
else: # 接口数不对:第三个接口被板子封死
c.rect(6, 56, 17, 69, 'b')
c.rect(6, 56, 17, 56, 'h'); c.rect(6, 56, 6, 69, 'l'); c.rect(17, 56, 17, 69, 'd')
for x in range(7, 17, 2):
c.put(x, 62, 'm')
return c
def ov_text(model, skew):
c = new()
text57(c, 46, 31, model, 'l' if skew else 'h', scale=2, skew=skew, gap=1)
return c
def ov_sticker(kind):
c = new()
if kind == 1: # 手写序列号
c.rect(86, 64, 172, 100, 'w')
c.rect(86, 64, 172, 65, 'h'); c.rect(86, 99, 172, 100, 'l')
c.rect(86, 64, 87, 100, 'h'); c.rect(171, 64, 172, 100, 'l')
rnd = random.Random(41)
for row in (74, 84, 92):
x = 92
while x < 164:
w = rnd.randint(3, 7)
for k in range(w):
c.put(x + k, row + int(math.sin(k + x) * 1.5), 'O')
c.put(x + k, row + 1 + int(math.sin(k + x) * 1.5), 'O')
x += w + rnd.randint(2, 4)
else: # 封条缺失:残留的胶痕与撕口
c.rect(174, 60, 182, 104, 'd')
for y in range(60, 105, 3):
c.rect(174, y, 178 + (y % 5), y + 1, 'm')
for y in range(62, 104, 6):
c.rect(179, y, 181, y + 1, 'x')
return c
BIG = {}
for _b, (_a, _ad, _t) in {'probidia': ('G', 'g2', 'PROBIDIA'), 'pmd': ('R', 'r2', 'PMD')}.items():
for _tier in range(4):
BIG['gpu_big_%s_f_t%d' % (_b, _tier)] = (lambda a=_a, ad=_ad, t=_t, k=_tier: gpu_front(a, ad, t, k))
for _pin in range(3): # 供电口与风扇数各自独立(假卡可以只对不上其中一项)
for _n in range(1, 4):
BIG['gpu_big_%s_f_p%dn%d' % (_b, _pin, _n)] = (lambda a=_a, ad=_ad, t=_t, p=_pin, n=_n: gpu_front(a, ad, t, 1, count=n, pin=p))
BIG['gpu_big_%s_b' % _b] = (lambda a=_a, ad=_ad: gpu_back(a, ad))
BIG['gpu_big_burnt_f'] = lambda: gpu_front('e', 'c', 'PROBIDIA', 1, burnt=True)
BIG['gpu_big_burnt_b'] = lambda: gpu_back('e', 'c', burnt=True)
BIG_OV = {}
if __name__ == '__main__':
import sys, os
if len(sys.argv) == 3 and sys.argv[1] == '--preview':
d = sys.argv[2]
def stack(base, *ovs):
b = BIG[base]()
for o in ovs:
oc = BIG_OV[o]()
for y in range(oc.h):
for x in range(oc.w):
if oc.px[y][x] is not None:
b.px[y][x] = oc.px[y][x]
return b
bg = (0x6A, 0x72, 0x70)
for t in range(4):
stack('gpu_big_probidia_f_t%d' % t).png(os.path.join(d, 'big_f_t%d.png' % t), scale=3, bg=bg)
+1159
View File
File diff suppressed because it is too large. Load diff
+637 -76
View File
@@ -30,6 +30,23 @@ 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), # 白
'n': (0x1E, 0x1B, 0x25), # 近黑
'x': (0x4A, 0x4E, 0x58), # 石墨亮边
'z': (0x2D, 0x2F, 0x38), # 石墨暗
'P': (0xD8, 0xDA, 0xD0), # 墨水屏
'K': (0x3C, 0x3F, 0x48), # 石墨机身
'E': (0x5A, 0x5E, 0x6A), # 机身受光边
# 换币灯的黄绿色霓虹(带透明度:光晕由外向内 20% / 40% / 60% / 80%,灯芯不透明)
'N1': (0xB6, 0xF0, 0x5A, 52), 'N2': (0xB6, 0xF0, 0x5A, 103), 'N3': (0xB6, 0xF0, 0x5A, 153), 'N4': (0xB6, 0xF0, 0x5A, 204),
'N5': (0xD8, 0xFF, 0x7A), 'N6': (0x6E, 0xA8, 0x2E), 'N7': (0x3E, 0x6A, 0x1E),
}
@@ -59,7 +76,7 @@ class Canvas:
for row in self.px:
line = bytearray()
for c in row:
r, g, b, a = (0, 0, 0, 0) if c is None else C[c] + (255,)
r, g, b, a = (0, 0, 0, 0) if c is None else (C[c] + (255,))[:4]
if bg is not None:
r, g, b = (round(v * a / 255 + w * (255 - a) / 255) for v, w in zip((r, g, b), bg))
a = 255
@@ -77,86 +94,308 @@ class Canvas:
f.write(data)
def gpu(accent, accent_dark, burnt=False):
"""显卡(32×32,2×2 格),略微俯视:顶面是带两个风扇的散热罩,左端金属挡板(带接口),
前沿露出 PCB 和金手指,右端一道暗侧面,顶上一排供电口。每格中心都不透明。"""
c = Canvas(32, 32)
TOP0, TOP1 = 4, 22 # 顶面行范围
L, R = 6, 29 # 顶面左右(左边是挡板)
# 金属 I/O 挡板(左端竖条,带两个接口孔)
c.rect(1, 2, 5, 27, 'b')
c.rect(1, 2, 1, 27, 'h'); c.rect(5, 2, 5, 27, 'd')
for y in (6, 12): # 两个视频接口(深色矩形孔,带高光边)
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'g')
c.put(3, 20, 'k'); c.put(3, 22, 'k'); c.put(3, 24, 'k') # 下部散热孔
# 散热罩顶面
for y in range(TOP0, TOP1 + 1):
for x in range(L, R + 1):
c.put(x, y, 'm')
c.put(R, y, 'd'); c.put(R - 1, y, 'd')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
# 品牌色条(沿罩顶前沿)
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, accent)
c.put(L + 1, TOP1, accent_dark); c.put(R - 2, TOP1, accent_dark)
# 两个风扇(圆形,叶片用十字 + 斜线)
for cx in (11, 22):
cy = 13
for y in range(cy - 6, cy + 7):
for x in range(cx - 6, cx + 7):
d2 = (x - cx) ** 2 + (y - cy) ** 2
if d2 <= 36:
c.put(x, y, 'k')
if 20 <= d2 <= 30:
c.put(x, y, 'g')
for k in range(-4, 5):
if abs(k) >= 1:
c.put(cx + k, cy, 'g'); c.put(cx, cy + k, 'g')
c.put(cx - 2, cy - 2, 'g'); c.put(cx + 2, cy + 2, 'g'); c.put(cx + 2, cy - 2, 'g'); c.put(cx - 2, cy + 2, 'g')
c.rect(cx - 1, cy - 1, cx + 1, cy + 1, 'b')
c.put(cx, cy, 'h')
# 前沿:PCB 绿色薄边 + 金手指
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'g2')
c.put(x, TOP1 + 2, 'g2')
for x in range(L, R + 1): # 金手指:连续金色带,中间一道缺口(PCIe 键位),带深色触点线
c.put(x, TOP1 + 3, 'Y'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'e')
for y in (TOP1 + 3, TOP1 + 4):
c.put(L + 13, y, 'k'); c.put(L + 14, y, 'k')
# 顶上一排供电口(两个黑色插口)
for x0 in (L + 3, L + 9):
c.rect(x0, TOP0 - 1, x0 + 4, TOP0 - 1, 'k')
# 保证每格中心不透明:下半部(第二行格子,y 16..31 的中心 y=24)落在金手指/PCB 上
for x in range(L, R + 1):
for y in range(TOP1 + 1, 28):
if c.px[y][x] is None:
c.put(x, y, 'k')
if burnt:
for x, y in [(9, 8), (10, 9), (13, 17), (24, 8), (25, 9), (27, 18), (17, 12), (18, 11)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 1):
c.put(x, TOP1 - 1, 'e')
for x, y in [(15, 5), (16, 5), (20, 21), (26, 14)]:
c.put(x, y, 'c')
# 轮廓
FONT = { # 3×5 像素字(丝印用)
'R': '110101110101101', 'Z': '111001010100111', 'X': '101101010101101', 'P': '110101110100100',
'M': '101111111101101', 'D': '110101101101110', 'T': '111010010010010', 'i': '010000010010010',
'0': '111101101101111', '1': '010110010010111', '2': '110001010100111', '3': '110001010001110',
'4': '101101111001001', '5': '111100110001110', '6': '011100111101111', '7': '111001010010010',
'8': '111101111101111', '9': '111101111001110',
}
def text(c, x, y, s, color, skew=False):
"""在画布上写 3×5 像素字;skew 为重新印过的效果:字一高一低、间距不匀。"""
i = 0
for ch in s:
if ch == ' ':
x += 2
continue
dy = (i % 2) if skew else 0
for k, bit in enumerate(FONT[ch]):
if bit == '1':
c.put(x + k % 3, y + k // 3 + dy, color)
x += 4 + (1 if skew and i % 3 == 2 else 0)
i += 1
W, H = 48, 32
TOP0, TOP1 = 4, 22 # 顶面行范围
L, R = 6, 45 # 顶面左右(左边是挡板)
FANS = ((17, 13), (34, 13)) # 两个风扇中心
def outline(c):
solid = lambda v: v is not None
get = lambda x, y: c.px[y][x] if 0 <= x < c.w and 0 <= y < c.h else None
edge = [(x, y) for y in range(c.h) for x in range(c.w)
if solid(get(x, y)) and any(not solid(get(x + dx, y + dy)) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)))]
for x, y in edge:
c.put(x, y, 'O')
def bracket(c):
"""左端金属挡板:带折边、网孔、三个接口(带高光边与内壁)、底部固定螺丝。"""
c.rect(1, 2, 5, 27, 'b')
c.rect(1, 2, 1, 27, 'h'); c.rect(2, 2, 2, 27, 'l'); c.rect(5, 2, 5, 27, 'd'); c.rect(4, 2, 4, 27, 'm')
for y in (5, 11, 17): # 三个接口:外框亮边、内腔深色、中间一条触片
c.rect(2, y, 4, y + 3, 'k')
c.rect(2, y, 4, y, 'h'); c.put(2, y + 1, 'g'); c.put(4, y + 1, 'g')
c.put(3, y + 2, 'd'); c.put(3, y + 3, 'g')
for y in range(20, 27, 2): # 散热网孔
c.put(2, y, 'k'); c.put(4, y, 'k')
c.put(3, y + 1, 'k')
c.put(3, 3, 'k'); c.put(3, 27, 'g') # 顶部卡扣与底部螺丝
c.put(2, 27, 'h'); c.put(4, 27, 'd')
def fan(c, cx, cy, accent, burnt=False, R=7):
"""风扇:深色风道里六片弯曲扇叶(前缘高光、叶面按受光渐变),外圈细边,轴心带品牌色点;光从左上来。"""
import math
k = 7.0 / R # 按半径折算到 R=7 的画法
for y in range(cy - R, cy + R + 1):
for x in range(cx - R, cx + R + 1):
dx, dy = x - cx, y - cy
r = math.hypot(dx, dy) * k
if r > 7.4:
continue
ang = (math.atan2(dy, dx) / (2 * math.pi)) % 1.0
lit = (dx + dy) < 0
if r >= 6.5: # 外圈细边
c.put(x, y, 'l' if dx + dy < -3 else ('b' if dx + dy < 3 else 'g'))
elif r >= 5.6: # 风道
c.put(x, y, 'n')
elif r <= 2.3: # 轴心
c.put(x, y, 'h' if (dx + dy) < -1 else 'b' if dx + dy < 2 else 'm')
else:
t = (ang * 6 + (r - 3) * 0.16) % 1.0
if t < 0.55:
c.put(x, y, 'h' if t < 0.09 else ('l' if lit else 'b') if t < 0.30 else ('b' if lit else 'm'))
else:
c.put(x, y, 'n' if t < 0.85 else 'k')
c.put(cx, cy, accent)
c.put(cx - 1, cy - 1, 'w')
FAN_LAYOUT_SMALL = {1: ([(26, 13)], 9), 2: ([(17, 13), (34, 13)], 7), 3: ([(14, 13), (26, 13), (38, 13)], 5)}
def gpu_front(accent, accent_dark, burnt=False, tier=1, count=None, pin=None):
"""显卡正面(48×32,3×2 格),略微俯视:石墨色散热罩带斜切角与品牌色斜条,两个七叶风扇,
右端露出鳍片,顶沿露出铜热管和 8 针供电口,前沿是带元件的 PCB 和逐根画出的金手指。每格中心都不透明。"""
c = Canvas(W, H)
bracket(c)
# 散热罩底色:左上亮、右下暗的渐变,右上角切角
for y in range(TOP0, TOP1 + 1):
for x in range(L, R + 1):
c.put(x, y, 'x' if y < TOP0 + 3 else 'd' if (x - L) < 4 else 'z')
for x in range(L, R + 1):
c.put(x, TOP0, 'h' if x < R - 5 else 'l'); c.put(x, TOP0 + 1, 'l' if x < R - 8 else 'b')
for y in range(TOP0, TOP1 + 1): # 左缘受光
c.put(L, y, 'l'); c.put(L + 1, y, 'b')
# 右端鳍片(竖向细条)
for x in range(R - 4, R + 1):
for y in range(TOP0 + 2, TOP1):
c.put(x, y, 'g' if (x - (R - 4)) % 2 == 0 else 'k')
c.rect(R - 5, TOP0 + 2, R - 5, TOP1 - 1, 'x')
count = count or (1, 2, 3, 3)[tier]
pin = (0, 1, 1, 2)[tier] if pin is None else pin
fans, fr = FAN_LAYOUT_SMALL[count]
# 风扇之间的筋条
for a, b in zip(fans, fans[1:]):
mx = (a[0] + b[0]) // 2
for y in range(TOP0 + 3, TOP1 - 2):
c.put(mx, y, 'k'); c.put(mx + 1, y, 'x')
# 品牌色斜条(从前沿向左上延伸)与前沿色条
for x in range(L + 1, R - 5):
c.put(x, TOP1 - 1, accent if x % 9 else accent_dark)
c.put(x, TOP1, accent_dark)
for cx, cy in fans:
fan(c, cx, cy, accent, R=fr)
if count < 3:
for dx, dy in ((-7, -7), (7, -7), (-7, 7), (7, 7)): # 风扇四角的固定孔
c.put(cx + dx, cy + dy, 'n' if dx + dy else 'k')
# 顶沿:铜热管端头 + 8 针供电口(带针脚)
for x0 in (L + 6, L + 8, L + 10, L + 12):
c.put(x0, TOP0 - 1, 'p'); c.put(x0 + 1, TOP0 - 1, 'q')
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针(每针占两像素)
for left, pins in (((L + 22, 6),) if pin == 0 else ((L + 18, 8),) if pin == 1 else ((L + 8, 8), (L + 25, 8))):
for x in range(left, left + pins * 2):
c.put(x, TOP0 - 1, 'n')
for x in range(left, left + pins * 2, 2):
c.put(x, TOP0 - 2, 'k')
for x in range(left + 1, left + pins * 2, 2):
c.put(x, TOP0 - 2, 'Y')
# 前沿:PCB(绿)带元件,之后金手指
for x in range(L, R + 1):
c.put(x, TOP1 + 1, 'G' if x % 7 else 'g2'); c.put(x, TOP1 + 2, 'g2')
for x0 in (L + 4, L + 11, L + 22, L + 29):
c.put(x0, TOP1 + 1, 'b'); c.put(x0 + 1, TOP1 + 1, 'g')
for x in range(L, R + 1): # 金手指:每一根带深色分隔
if x in (L + 17, L + 18):
continue
c.put(x, TOP1 + 3, 'Y' if x % 2 == 0 else 'e'); c.put(x, TOP1 + 4, 'Y' if x % 2 == 0 else 'q')
c.put(x, TOP1 + 5, 'e' if x % 2 == 0 else 'k')
for x in range(L, R + 1):
for y in range(TOP1 + 1, 28):
if c.px[y][x] is None:
c.put(x, y, 'k')
if burnt:
for x, y in [(12, 8), (13, 9), (18, 17), (35, 8), (36, 9), (40, 18), (24, 12), (25, 11), (19, 9), (33, 16)]:
c.put(x, y, 'c')
for x in range(L + 1, R - 5):
c.put(x, TOP1 - 1, 'e')
for cx, cy in fans:
for dx, dy in ((0, 0), (1, 1), (-1, 1), (2, -2), (-2, -2)):
c.put(cx + dx, cy + dy, 'c')
outline(c)
return c
def screw(c, x, y):
c.put(x, y, 'b'); c.put(x + 1, y, 'g'); c.put(x, y + 1, 'g'); c.put(x + 1, y + 1, 'k')
def gpu_back(accent, accent_dark, burnt=False):
"""显卡背面(翻面后):左端仍是挡板;拉丝金属背板,带通风槽、四角螺丝、品牌色斜条;
中上是一块凹进去的丝印区(型号用叠加图写在这里),右下是带二维码和条码的序列号贴纸,贴纸边上一道封条。"""
c = Canvas(W, H)
bracket(c)
for y in range(TOP0, 28):
for x in range(L, R + 1):
c.put(x, y, 'd' if (y % 3) else 'm' if x % 2 else 'd')
c.put(R, y, 'k'); c.put(R - 1, y, 'z')
for x in range(L, R + 1):
c.put(x, TOP0, 'l'); c.put(x, TOP0 + 1, 'b')
for y in range(TOP0, 28):
c.put(L, y, 'b'); c.put(L + 1, y, 'x')
# 丝印凹槽
c.rect(9, 7, 43, 13, 'z')
c.rect(9, 7, 43, 7, 'k'); c.rect(9, 7, 9, 13, 'k')
c.rect(9, 13, 43, 13, 'x')
# 品牌色斜条与前沿色条
for x in range(L + 1, R - 1):
c.put(x, 16, accent)
c.put(L + 1, 17, accent_dark); c.put(R - 2, 17, accent_dark)
# 通风槽(左下一组)
for y in (19, 21, 23, 25):
for x in range(L + 4, L + 14):
c.put(x, y, 'k')
for x in range(L + 4, L + 14):
c.put(x, y + 1, 'x') if y + 1 < 27 else None
# 四角螺丝
for x, y in ((L + 2, TOP0 + 3), (R - 5, TOP0 + 3), (L + 2, 24), (R - 5, 24)):
screw(c, x, y)
# 序列号贴纸:左边二维码,右边条码与小字
c.rect(22, 17, 41, 23, 'w')
c.rect(22, 17, 41, 17, 'h'); c.rect(22, 23, 41, 23, 'l')
qr = ['1110101', '1010011', '1011101', '0001010', '1101111', '0100101', '1110011']
for j, row in enumerate(qr):
for i, ch in enumerate(row):
if ch == '1':
c.put(23 + i, 17 + j - (1 if j == 6 else 0) * 0, 'n') if j < 6 else None
for x in range(31, 41, 2):
c.rect(x, 18, x, 20, 'n')
for x in range(32, 41, 3):
c.put(x, 18, 'w'); c.put(x, 19, 'w')
for x in range(31, 41):
if x % 3 != 0:
c.put(x, 22, 'm')
# 封条(红底白字)
c.rect(43, 16, 44, 24, 'R')
c.rect(43, 16, 43, 24, 'r2')
for y in (18, 20, 22):
c.put(44, y, 'w')
# 背面金手指一行
for x in range(L, R + 1):
if x in (L + 17, L + 18):
continue
c.put(x, 27, 'Y' if x % 2 == 0 else 'e')
if burnt:
for x, y in [(14, 9), (15, 10), (30, 11), (36, 13), (20, 14), (26, 21), (12, 22)]:
c.put(x, y, 'c')
outline(c)
return c
def overlay():
return Canvas(W, H)
def ov_fan(kind, count=2):
"""风扇叠加:1 积灰(叶片上一层灰),2 叶片缺角(边缘缺一块并露出黑底)。"""
c = overlay()
for cx, cy in FAN_LAYOUT_SMALL[count][0]:
if kind == 1:
for dx, dy in ((-4, -2), (-2, -4), (3, -3), (4, 1), (-3, 3), (2, 4), (-5, 0), (0, 5), (1, -5), (-1, 2), (5, -1), (3, 2)):
c.put(cx + dx, cy + dy, 'l')
for dx, dy in ((-4, -1), (-1, -4), (4, -2), (2, 3), (-3, 4)):
c.put(cx + dx, cy + dy, 'h')
else:
for dx, dy in ((4, -4), (5, -4), (5, -3), (4, -3), (3, -4), (5, -5), (6, -3)):
c.put(cx + dx, cy + dy, 'k')
c.put(cx - 5, cy + 4, 'k'); c.put(cx - 4, cy + 5, 'k'); c.put(cx - 5, cy + 5, 'k')
return c
def ov_fingers(kind):
"""金手指叠加:1 发暗(触点变褐),2 氧化发绿。"""
c = overlay()
for x in range(L, R + 1):
for y in (TOP1 + 3, TOP1 + 4):
if x in (L + 17, L + 18):
continue
if kind == 1:
c.put(x, y, 'e' if (x + y) % 3 else 'Y')
else:
c.put(x, y, 'g2' if (x + y) % 2 else 'G')
return c
def ov_bracket(kind):
"""挡板叠加:1 划痕;2 接口数量不对(第三个接口被封成一块平板,只剩两个)。"""
c = overlay()
if kind == 1:
for x, y in ((2, 9), (3, 10), (4, 11), (2, 15), (3, 15), (3, 16), (2, 20), (3, 21), (4, 22), (4, 25), (3, 25)):
c.put(x, y, 'l' if (x + y) % 2 else 'd')
else:
c.rect(2, 17, 4, 20, 'b')
c.rect(2, 17, 4, 17, 'h')
return c
def ov_text(model, skew):
"""背面丝印叠加:写型号。skew 为重新印过的版本(字高低不齐)。"""
c = overlay()
text(c, 10, 8, model, 'l' if skew else 'h', skew)
return c
def ov_sticker(kind):
"""贴纸叠加:1 手写序列号(条码被盖住、写了歪歪扭扭的字);2 封条缺失(只剩撕过的残胶)。"""
c = overlay()
if kind == 1:
c.rect(22, 17, 41, 23, 'w')
c.rect(22, 17, 41, 17, 'h'); c.rect(22, 23, 41, 23, 'l')
for x, y in ((24, 20), (25, 19), (26, 20), (27, 19), (29, 20), (30, 19), (30, 21), (32, 19), (33, 20), (34, 19), (35, 21), (36, 20), (37, 19), (38, 20), (39, 21), (24, 22), (26, 21), (28, 22), (31, 22), (35, 22)):
c.put(x, y, 'O')
else:
c.rect(43, 16, 44, 24, 'd')
for y in (16, 18, 20, 22, 24):
c.put(44, y, 'm')
c.put(43, 17, 'm'); c.put(43, 21, 'm'); c.put(43, 23, 'x')
return c
MODELS = ['RTX 60', 'RTX 70', 'RTX 80', 'RTX 90', 'RX 70', 'RX 80', 'RX 800', 'RX 900']
def rig():
"""矿架(48×48,3×3 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
"""矿架(64×64,4×4 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
c = Canvas(48, 48)
FL, FR, FY, DEP = 3, 44, 24, 19 # 正面左右边、顶面前沿所在行、顶面深度(顶面后沿在 y=6,中心行 y=8 落在顶面上)
BOT = 44 # 正面底边
inset = lambda v: v // 6 # 顶面后部只略微收窄:原版机器的顶面也近似矩形,并且要让左右两格的中心落在顶面内
c = Canvas(64, 64)
FL, FR, FY, DEP = 3, 60, 36, 31 # 正面左右边、顶面前沿所在行、顶面深度(顶面后沿在 y=6,中心行 y=8 落在顶面上)
BOT = 60 # 正面底边
inset = lambda v: v // 8 # 顶面后部只略微收窄:原版机器的顶面也近似矩形,并且要让左右两格的中心落在顶面内
# 顶面:后部受光、前部稍暗;左缘亮、右缘暗
for v in range(DEP):
@@ -169,8 +408,8 @@ def rig():
for x in range(FL + inset(DEP - 1), FR - inset(DEP - 1) + 1):
c.put(x, FY - DEP + 1, 'l')
# 顶面上的散热格栅和一个小指示灯
for v in range(4, 16, 3):
for u in range(6, 24):
for v in range(4, 26, 3):
for u in range(6, 34):
c.put(FL + inset(v) + u, FY - v, 'g')
c.put(FR - 9, FY - 8, 'G'); c.put(FR - 8, FY - 8, 'G')
# 正面:中间色,左端受光、右端暗;顶部一条暗折线
@@ -190,7 +429,7 @@ def rig():
c.put(x, BOT - 1, 'd')
# 正面里的四层显卡位(深色槽,上沿亮、下沿暗,右端一颗状态灯)
# 正面四层显卡位(深色槽,右端一颗状态灯);间隔行用中间色,保证每格中心都不透明
for i, y in enumerate((FY + 3, FY + 8, FY + 13)):
for i, y in enumerate((FY + 3, FY + 8, FY + 13, FY + 18)):
for x in range(FL + 3, FR - 4):
for dy in range(3):
c.put(x, y + dy, 'k')
@@ -283,13 +522,298 @@ def terminal():
return c
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)
c.box(2, 1, 13, 14, 'b')
c.rect(3, 2, 4, 13, 'd') # 书脊
c.rect(5, 2, 12, 2, 'h')
c.rect(6, 4, 11, 5, 'G') # 封面色块
c.rect(6, 4, 11, 4, 'g2')
for y in (7, 9, 11): # 文字行
c.rect(6, y, 11 if y != 11 else 9, y, 'm')
c.put(12, 13, 'd')
outline(c)
return c
def asic(model):
"""RatDigg 矿机(48×32,3×2 格):宽扁的金属机箱,正面两个大风扇,顶上一条铭牌带三个指示灯和型号;
指示灯与风扇轴心的颜色区分档次(M1 绿、M2 蓝、M3 金)。光从左上来。"""
c = Canvas(48, 32)
accent = {'M1': 'G', 'M2': 'u', 'M3': 'y'}[model]
c.rect(3, 1, 44, 5, 'l') # 顶面
c.rect(3, 1, 44, 1, 'h')
c.rect(43, 2, 44, 5, 'b')
c.rect(2, 6, 45, 29, 'm') # 正面
c.rect(2, 6, 2, 29, 'b'); c.rect(45, 6, 45, 29, 'd'); c.rect(2, 29, 45, 29, 'd')
c.rect(4, 7, 43, 10, 'n') # 铭牌带
for x in (6, 9, 12):
c.put(x, 8, accent); c.put(x, 9, accent if x == 6 else 'k')
text(c, 32, 7, model.replace('M', 'M'), 'w')
for fx in (14, 34): # 风扇:方框风道 + 圆扇
c.rect(fx - 9, 12, fx + 9, 28, 'n')
c.rect(fx - 9, 12, fx + 9, 12, 'g'); c.rect(fx - 9, 12, fx - 9, 28, 'g')
fan(c, fx, 20, accent, R=8)
c.rect(22, 12, 26, 28, 'd') # 两扇之间的散热栅
for y in range(13, 28, 2):
c.rect(23, y, 25, y, 'k')
c.rect(4, 30, 9, 31, 'k'); c.rect(38, 30, 43, 31, 'k') # 脚垫
c.rect(10, 30, 37, 30, 'd')
outline(c)
return c
def hw_wallet():
"""PLEDGER 硬件钱包(16×16,一格):竖放的石墨卡片,大块浅色墨水屏,右侧一个侧键。"""
c = Canvas(16, 16)
c.rect(3, 1, 12, 14, 'K')
for (x, y) in ((3, 1), (12, 1), (3, 14), (12, 14)):
c.put(x, y, None)
c.rect(4, 2, 4, 13, 'E'); c.rect(4, 2, 11, 2, 'E') # 左上受光
c.rect(11, 3, 11, 13, 'z'); c.rect(5, 13, 11, 13, 'z') # 右下背光
c.rect(5, 3, 10, 11, 'P') # 墨水屏
c.rect(6, 5, 9, 5, 'm'); c.rect(6, 7, 8, 7, 'm'); c.rect(6, 9, 9, 9, 'm') # 屏上的字
c.rect(13, 5, 13, 7, 'x') # 侧键
outline(c)
return c
def key_drive():
"""Tumbler 的密钥盘(16×16,一格):斜放的旧式数据棒,深紫机身、金属接口、一枚红色指示灯和手写标签。"""
c = Canvas(16, 16)
c.rect(3, 4, 12, 11, 'V') # 机身
c.rect(3, 4, 12, 4, 'v'); c.rect(3, 4, 3, 11, 'v') # 受光边
c.rect(4, 11, 12, 11, 'n'); c.rect(12, 5, 12, 11, 'n') # 背光边
c.rect(13, 6, 15, 9, 'l'); c.rect(13, 9, 15, 9, 'm') # 金属接口
c.put(14, 7, 'k'); c.put(14, 8, 'k')
c.rect(5, 6, 9, 9, 'w') # 手写标签
c.rect(6, 7, 8, 7, 'k'); c.rect(6, 8, 7, 8, 'k')
c.put(11, 6, 'R') # 指示灯
outline(c)
return c
def otc_lamp_on():
"""换币灯的点亮叠加图(32×48,和原版暗灯同尺寸,(0,0) 对齐):只画灯罩下面的光晕和灯芯,叠在熄灭的暗灯上。
光晕是灯罩下沿的椭圆(第 25–40 行),越靠中心越不透明;灯芯是中间一根竖条,底下一圈灯座光。"""
c = Canvas(32, 48)
rows = { # 每行光晕的左右边界(含)
25: (5, 26), 26: (4, 27), 27: (4, 27), 28: (3, 28), 29: (3, 28), 30: (3, 28), 31: (2, 29), 32: (2, 29),
33: (2, 29), 34: (3, 28), 35: (3, 28), 36: (4, 27), 37: (5, 26), 38: (6, 25), 39: (9, 22), 40: (13, 18),
}
for y, (x0, x1) in rows.items():
for x in range(x0, x1 + 1):
if y == 25 and 7 <= x <= 24:
continue # 灯罩下沿正中还是灯罩
d = (((x - 15.5) / 14) ** 2 + ((y - 32) / 8.5) ** 2) ** 0.5
c.put(x, y, 'N4' if d < 0.45 else 'N3' if d < 0.65 else 'N2' if d < 0.85 else 'N1')
for y in range(27, 37): # 灯芯
c.put(15, y, 'N5'); c.put(16, y, 'N6')
for y, (x0, x1) in {37: (13, 18), 38: (11, 20), 39: (11, 20), 40: (13, 18)}.items():
for x in range(x0, x1 + 1): # 灯座反光
c.put(x, y, 'N6' if y < 40 else 'N7')
for x in range(14, 18):
c.put(x, 38, 'N5')
return c
SPRITES = {
'gpu_probidia': lambda: gpu('G', 'g2'),
'gpu_pmd': lambda: gpu('R', 'r2'),
'gpu_burnt': lambda: gpu('e', 'c', burnt=True),
'gpu_burnt_f': lambda: gpu_front('e', 'c', burnt=True),
'gpu_burnt_b': lambda: gpu_back('e', 'c', burnt=True),
'gpu_probidia_b': lambda: gpu_back('G', 'g2'),
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
'rig': rig,
'terminal': terminal,
'hw_wallet': hw_wallet,
'key_drive': key_drive,
'asic_m1': lambda: asic('M1'),
'asic_m2': lambda: asic('M2'),
'asic_m3': lambda: asic('M3'),
}
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))
for _pin in range(3):
for _n in range(1, 4):
SPRITES['gpu_probidia_f_p%dn%d' % (_pin, _n)] = (lambda p=_pin, n=_n: gpu_front('G', 'g2', pin=p, count=n))
SPRITES['gpu_pmd_f_p%dn%d' % (_pin, _n)] = (lambda p=_pin, n=_n: gpu_front('R', 'r2', pin=p, count=n))
def hw_button(symbol):
"""硬件钱包的物理按键(24×24):石墨外圈、左上受光的金属环,中间凹下去的按键面,刻着 ✓ 或 ✕。"""
c = Canvas(24, 24)
for y in range(24):
for x in range(24):
d = ((x - 11.5) ** 2 + (y - 11.5) ** 2) ** 0.5
if d <= 11.5:
c.put(x, y, 'x' if x + y < 23 else 'z') # 外圈:左上亮、右下暗
if d <= 9.5:
c.put(x, y, 'k' if x + y < 23 else 'n') # 凹槽:反过来,左上暗、右下亮一点
if d <= 8.5:
c.put(x, y, 'z') # 按键面
mark = 'G' if symbol == 'ok' else 'R'
if symbol == 'ok':
pts = [(7, 12), (8, 13), (9, 14), (10, 15), (11, 14), (12, 13), (13, 12), (14, 11), (15, 10), (16, 9)]
else:
pts = [(8 + k, 8 + k) for k in range(8)] + [(15 - k, 8 + k) for k in range(8)]
for (x, y) in pts:
c.put(x, y, mark); c.put(x, y + 1, mark)
outline(c)
return c
def rounded(size, r, fill, light=None, dark=None):
"""圆角矩形(九宫格用):圆角半径 r,外轮廓 O;可选左上一圈受光、右下一圈背光。"""
c = Canvas(size, size)
def inside(x, y, rr):
cx = min(max(x, rr), size - 1 - rr); cy = min(max(y, rr), size - 1 - rr)
return (x - cx) ** 2 + (y - cy) ** 2 <= rr * rr + rr * 0.6
for y in range(size):
for x in range(size):
if inside(x, y, r):
c.put(x, y, fill)
if light or dark:
for y in range(size):
for x in range(size):
if c.px[y][x] is None:
continue
edge = any(not (0 <= x + dx < size and 0 <= y + dy < size) or c.px[y + dy][x + dx] is None for dx, dy in ((2, 0), (-2, 0), (0, 2), (0, -2)))
if edge:
c.put(x, y, light if x + y < size - 1 else dark)
outline(c)
return c
def hw_selected():
"""PLEDGER 屏上选中的按钮(16×16,九宫格边 5):墨水屏白底、1 像素黑框,四角向外加粗的取景框角标(准星)。"""
c = Canvas(16, 16)
c.rect(0, 0, 15, 15, 'P')
for i in range(16):
c.put(i, 1, 'n'); c.put(i, 14, 'n'); c.put(1, i, 'n'); c.put(14, i, 'n')
for (x, y, dx, dy) in ((0, 0, 1, 1), (15, 0, -1, 1), (0, 15, 1, -1), (15, 15, -1, -1)):
for k in range(4):
c.put(x + dx * k, y, 'n'); c.put(x, y + dy * k, 'n')
return c
def hw_groove():
"""PLEDGER 墨水屏输入框凹槽(12×12,九宫格边 2):浅色底、1 像素深墨边。"""
c = Canvas(12, 12)
c.rect(0, 0, 11, 11, 'n')
c.rect(1, 1, 10, 10, 'P')
return c
UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3}
UI_ICONS['ui_hw_shell'] = lambda: rounded(32, 7, 'K', 'E', 'z')
UI_ICONS['ui_hw_bezel'] = lambda: rounded(16, 4, 'n')
UI_ICONS['ui_hw_paper'] = lambda: rounded(12, 2, 'P')
UI_ICONS['ui_hw_sel'] = hw_selected
def hw_key():
"""触控键底图(九宫格,白色,可着色):圆角 3 像素,没有描边,颜色由界面按状态着色。"""
size, r = 12, 3
c = Canvas(size, size)
for y in range(size):
for x in range(size):
cx = min(max(x, r), size - 1 - r); cy = min(max(y, r), size - 1 - r)
if (x - cx) ** 2 + (y - cy) ** 2 <= r * r + r * 0.6:
c.put(x, y, 'w')
return c
UI_ICONS['ui_hw_key'] = hw_key
UI_ICONS['ui_hw_groove'] = hw_groove
UI_ICONS['ui_hw_ok'] = lambda: hw_button('ok')
UI_ICONS['ui_hw_no'] = lambda: hw_button('no')
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
OVERLAYS = {
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
'gpu_ov_brk1': lambda: ov_bracket(1),
'gpu_ov_stk2': lambda: ov_sticker(2),
'otc_lamp_on': otc_lamp_on,
}
for _n in (1, 2, 3):
OVERLAYS['gpu_ov_fan1_%d' % _n] = (lambda n=_n: ov_fan(1, n))
OVERLAYS['gpu_ov_fan2_%d' % _n] = (lambda n=_n: ov_fan(2, n))
for _m in MODELS:
OVERLAYS['gpu_ov_txt_' + key(_m)] = (lambda m=_m: ov_text(m, False))
OVERLAYS['gpu_ov_txs_' + key(_m)] = (lambda m=_m: ov_text(m, True))
def check_cells(canvas, name):
"""游戏按 mod 物品图每格的中心像素是否不透明来判断占格(行从图的下沿往上数)。有空格就报错。"""
@@ -306,11 +830,48 @@ if __name__ == '__main__':
canvas = make()
check_cells(canvas, name)
canvas.png(os.path.join(sys.argv[2], name + '.png'), scale=6, bg=(0xA0, 0xA8, 0x9C))
# 预览:把各种叠加图叠在底图上
def stack(base, *ovs):
b = SPRITES[base]()
for o in ovs:
oc = OVERLAYS[o]()
for y in range(oc.h):
for x in range(oc.w):
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:
out = os.path.normpath(os.path.join(here, '..', 'src', 'Decentralized', 'ModFiles', 'Textures', 'Items'))
for f in os.listdir(out): # 清掉旧的显卡图,避免留下不再使用的文件
if f.startswith('gpu_'):
os.remove(os.path.join(out, f))
for name, make in SPRITES.items():
canvas = make()
check_cells(canvas, name)
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'))
for name, make in big_cases.CASES.items():
if name.startswith('gpu_big_ov_fan_'):
continue
if not name.startswith('gpu_big_ov_print_') and name in ('gpu_big_ov_fin_dim', 'gpu_big_ov_fin_oxide', 'gpu_big_ov_brk_scratch', 'gpu_big_ov_seal_missing'):
make().png(os.path.join(out, name + '.png'))
# 风扇叠加按风扇数各出一套:叠加模块里的风扇位置是模块级变量,换成对应布局后再画
for count, (centres, radius) in big.FAN_LAYOUT.items():
big_cases.FANS = list(centres) if len(centres) > 1 else [centres[0], centres[0]]
big_cases.FR = radius
for fname, short in (('fan_dust1', 'fan_dust1'), ('fan_dust2', 'fan_dust2'), ('fan_chip', 'fan_chip')):
getattr(big_cases, fname)().png(os.path.join(out, 'gpu_big_ov_%s_%d.png' % (short, count)))
for m in MODELS:
for style in ('normal', 'skew', 'stamp'):
big_cases.print_overlay(m, style).png(os.path.join(out, 'gpu_big_ov_print_%s_%s.png' % (style, key(m))))
print(out)
+115 -3
View File
@@ -5,8 +5,12 @@ namespace Decentralized.Domain
public enum Venue { Exchange, Dex, Darknet }
/// <summary>
/// 一个交易对 = 一个恒定乘积池(x·y = k)。Base 是被交易的币,Quote 是计价币;价格 = Quote 储备 / Base 储备。
/// 每个交易对各有深度;夜间由 NPC 套利者把价格按比例拉向目标价。
/// 一个交易对。DEX 是恒定乘积池(x·y = k):Base 是被交易的币,Quote 是计价币,价格 = Quote 储备 / Base 储备。
/// Poinbase 主池是订单簿(<see cref="Book"/>):价格只取决于当天的累计净买入量,冲击随单子变大而增长得比线性慢
/// (价格 = 参考价 × exp(κ · sign(x) · |x|^α),x = 净买入的名义价值 / 深度),拆单不占便宜,同量买进再卖出回到原价。
/// 稳定币对(<see cref="Stable"/>)是 StableSwap 池(Curve 的曲线,放大系数 A):挂钩附近几乎没有滑点,偏离越大越接近恒定乘积;
/// 储备按"价值单位"记(Base 数量 × 挂钩价),挂钩价夜里随参考价重设。
/// 每个交易对各有深度;夜间由 NPC 套利者把价格按比例拉向目标价,订单簿的累计量清零。
/// </summary>
public sealed class Pool
{
@@ -14,10 +18,87 @@ namespace Decentralized.Domain
public Venue Venue;
public double BaseReserve, QuoteReserve;
public double Fee;
/// <summary>订单簿模式:参考价、当天累计净买入的 Base 数量、深度(Quote)。</summary>
public bool Book;
public double Ref, Net, Depth;
/// <summary>StableSwap 模式:放大系数、挂钩价(储备里 Base 一侧按 数量 × 挂钩价 记)。</summary>
public bool Stable;
public double Amp, Peg;
public string Id => $"{Venue}:{Base}/{Quote}";
public double Price => QuoteReserve / BaseReserve;
public double Price => Book ? PriceAt(Net) : Stable ? Peg * StableSlope(BaseReserve, QuoteReserve) : QuoteReserve / BaseReserve;
public static Pool CreateStable(Venue venue, string baseAsset, string quote, double price, double depth, double fee, double amp) =>
new Pool { Venue = venue, Base = baseAsset, Quote = quote, Stable = true, Amp = amp, Peg = price, BaseReserve = depth, QuoteReserve = depth, Fee = fee };
// ——— StableSwap(两个币):Ann(x + y) + D = Ann·D + D³ / (4xy),Ann = 4A ———
private double Ann => 4 * Amp;
private double StableD(double x, double y)
{
double s = x + y, d = s;
if (s <= 0) return 0;
for (int i = 0; i < 255; i++)
{
double dp = d * d * d / (4 * x * y), prev = d;
d = (Ann * s + 2 * dp) * d / ((Ann - 1) * d + 3 * dp);
if (Math.Abs(d - prev) <= 1e-12 * d) break;
}
return d;
}
/// <summary>已知一侧储备 x 和不变量 D,求另一侧。</summary>
private double StableOther(double x, double d)
{
double c = d * d / (2 * x) * d / (2 * Ann), b = x + d / Ann, y = d;
for (int i = 0; i < 255; i++)
{
double prev = y;
y = (y * y + c) / (2 * y + b - d);
if (Math.Abs(y - prev) <= 1e-12 * y) break;
}
return y;
}
/// <summary>边际价格(每个价值单位的 Base 换多少 Quote):不变量对两侧偏导之比。平衡时为 1。</summary>
private double StableSlope(double x, double y)
{
double d = StableD(x, y), d3 = d * d * d;
return (Ann + d3 / (4 * x * x * y)) / (Ann + d3 / (4 * x * y * y));
}
public static Pool CreateBook(Venue venue, string baseAsset, string quote, double price, double depth, double fee) =>
new Pool { Venue = venue, Base = baseAsset, Quote = quote, Book = true, Ref = price, Depth = depth, Fee = fee };
private double PriceAt(double net)
{
double x = net * Ref / Depth;
return Ref * Math.Exp(Math.Sign(x) * Tuning.BookK * Math.Pow(Math.Abs(x), Tuning.BookAlpha));
}
/// <summary>订单簿上累计净买入从 a 走到 b 的成交额(Quote,不含手续费;b &lt; a 为负)。辛普森积分,同一区间正反两个方向结果互为相反数。</summary>
private double Cost(double a, double b)
{
const int n = 128;
double h = (b - a) / n, sum = PriceAt(a) + PriceAt(b);
for (int i = 1; i < n; i++) sum += PriceAt(a + i * h) * (i % 2 == 1 ? 4 : 2);
return sum * h / 3;
}
/// <summary>订单簿:花 quote(不含手续费)能买到多少 Base。牛顿迭代解 Cost(Net, Net + b) = quote。</summary>
private double BookBaseFor(double quote)
{
double b = quote / Price;
for (int i = 0; i < 30; i++)
{
double step = (Cost(Net, Net + b) - quote) / PriceAt(Net + b);
b = Math.Max(0, b - step);
if (Math.Abs(step) < 1e-10 * (1 + b)) break;
}
return b;
}
public static Pool Create(Venue venue, string baseAsset, string quote, double price, double quoteDepth, double fee) =>
new Pool { Venue = venue, Base = baseAsset, Quote = quote, QuoteReserve = quoteDepth, BaseReserve = quoteDepth / price, Fee = fee };
@@ -25,6 +106,8 @@ namespace Decentralized.Domain
/// <summary>预估:卖出 baseIn 个 Base 能得到多少 Quote(不改池子)。</summary>
public double QuoteOut(double baseIn)
{
if (Book) return baseIn > 0 ? Cost(Net - baseIn, Net) * (1 - Fee) : 0;
if (Stable) return baseIn > 0 ? QuoteReserve - StableOther(BaseReserve + baseIn * Peg * (1 - Fee), StableD(BaseReserve, QuoteReserve)) : 0;
double x = BaseReserve + baseIn * (1 - Fee);
return QuoteReserve - BaseReserve * QuoteReserve / x;
}
@@ -32,15 +115,33 @@ namespace Decentralized.Domain
/// <summary>预估:花 quoteIn 个 Quote 能买到多少 Base(不改池子)。</summary>
public double BaseOut(double quoteIn)
{
if (Book) return quoteIn > 0 ? BookBaseFor(quoteIn * (1 - Fee)) : 0;
if (Stable) return quoteIn > 0 ? (BaseReserve - StableOther(QuoteReserve + quoteIn * (1 - Fee), StableD(BaseReserve, QuoteReserve))) / Peg : 0;
double y = QuoteReserve + quoteIn * (1 - Fee);
return BaseReserve - BaseReserve * QuoteReserve / y;
}
/// <summary>预估:买到 baseOut 个 Base 要付多少 Quote(含手续费,不改池子);超出池子储备返回 +∞。</summary>
public double QuoteIn(double baseOut)
{
if (baseOut <= 0) return 0;
if (Book) return Cost(Net, Net + baseOut) / (1 - Fee);
if (Stable)
{
double x = BaseReserve - baseOut * Peg;
return x <= 0 ? double.PositiveInfinity : (StableOther(x, StableD(BaseReserve, QuoteReserve)) - QuoteReserve) / (1 - Fee);
}
if (baseOut >= BaseReserve) return double.PositiveInfinity;
return (BaseReserve * QuoteReserve / (BaseReserve - baseOut) - QuoteReserve) / (1 - Fee);
}
/// <summary>卖出 Base,返回得到的 Quote。手续费留在池里。</summary>
public double SellBase(double baseIn)
{
if (baseIn <= 0) return 0;
double out_ = QuoteOut(baseIn);
if (Book) { Net -= baseIn; return out_; }
if (Stable) { BaseReserve += baseIn * Peg; QuoteReserve -= out_; return out_; }
BaseReserve += baseIn;
QuoteReserve -= out_;
return out_;
@@ -51,6 +152,8 @@ namespace Decentralized.Domain
{
if (quoteIn <= 0) return 0;
double out_ = BaseOut(quoteIn);
if (Book) { Net += out_; return out_; }
if (Stable) { QuoteReserve += quoteIn; BaseReserve -= out_ * Peg; return out_; }
QuoteReserve += quoteIn;
BaseReserve -= out_;
return out_;
@@ -60,6 +163,13 @@ namespace Decentralized.Domain
public double ImpactOfBuy(double quoteIn)
{
double before = Price;
if (Book) return PriceAt(Net + BaseOut(quoteIn)) / before - 1;
if (Stable)
{
var copy = (Pool)MemberwiseClone();
copy.BuyBase(quoteIn);
return copy.Price / before - 1;
}
double y = QuoteReserve + quoteIn * (1 - Fee);
double x = BaseReserve * QuoteReserve / y;
return (y / x) / before - 1;
@@ -77,6 +187,8 @@ namespace Decentralized.Domain
public void SetPrice(double price, double quoteDepth)
{
if (Book) { Ref = price; Net = 0; Depth = quoteDepth; return; }
if (Stable) { Peg = price; BaseReserve = quoteDepth; QuoteReserve = quoteDepth; return; }
QuoteReserve = quoteDepth;
BaseReserve = quoteDepth / price;
}
+44
View File
@@ -0,0 +1,44 @@
using System;
namespace Decentralized.Domain
{
/// <summary>
/// RatDigg 矿机暴毙后的修理:往机器的修理槽里按顺序放电子元件,约占机价的 4%。每一步只收这一步需要的那种元件,攒够这一步再进下一步,
/// 全部攒够就修好(健康扣 <see cref="Tuning.AsicRepairHealthCut"/> 点)。游戏里电子元件的价值是 33 / 350 / 2,500(普通 / 稀有 / 超科技)。
/// </summary>
public static class AsicRepair
{
public static (string Item, int Count)[] Steps(string model)
{
switch (model)
{
case "M1": return new[] { ("common_electronic", 4), ("rare_electronic", 1) };
case "M2": return new[] { ("rare_electronic", 2) };
case "M3": return new[] { ("hypertech_electronic", 1), ("rare_electronic", 1) };
default: throw new ArgumentOutOfRangeException(nameof(model));
}
}
/// <summary>当前这一步需要的元件;已修好(step 越界)返回 null。</summary>
public static (string Item, int Count)? Need(string model, int step)
{
var steps = Steps(model);
return step < steps.Length ? steps[step] : ((string, int)?)null;
}
/// <summary>
/// 投入 count 件 item:只收当前这一步需要的那种,攒够就进下一步。返回实际收下的件数;step 达到总步数表示修好了。
/// </summary>
public static int Insert(string model, ref int step, ref int progress, string item, int count)
{
var need = Need(model, step);
if (need == null || count <= 0 || item != need.Value.Item) return 0;
int take = Math.Min(count, need.Value.Count - progress);
progress += take;
if (progress >= need.Value.Count) { step++; progress = 0; }
return take;
}
public static bool Done(string model, int step) => step >= Steps(model).Length;
}
}
+173
View File
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>货的种类:违禁品、高科技物品、显卡。</summary>
public enum GoodKind { Contraband, Tech, Gpu, Goods }
/// <summary>黑市货架上的一种货。显卡的 Item 是 "gpu:型号",Model 是标称型号,FakeOf 非空表示背后是别的芯片(买家看不到)。</summary>
public class BmGood
{
public string Item;
public GoodKind Kind;
public int Level;
public int Price;
public string Model, FakeOf;
public double Health;
}
/// <summary>现货柜台上的一件货:剩余件数、含溢价的单价(信用点)。</summary>
public sealed class BmOffer : BmGood
{
public int Qty;
}
/// <summary>
/// Web3 黑市:黑市派系在链上的实体。货架上主要是违禁品(档次随黑市声望放开),偶尔有高科技物品和显卡(加价),
/// 现货只收币,脏币也收,付出去的币连同按比例的污点一起留在卖家那边;Ponero 兑换处把任意币兑成无法追踪的 Ponero(这部分价值的污点被清除),有每日额度和手续费。
/// 镇压期间整个平台下线。
/// </summary>
public sealed partial class BlackMarket
{
public List<BmOffer> Offers = new List<BmOffer>();
public int RefreshedDay = -999;
/// <summary>Ponero 兑换处当天已兑换的价值(信用点)及其日期。</summary>
public double DeskToday;
public int DeskDay = -1;
/// <summary>违禁品池:游戏物品 id 与违禁等级(1 低、2 中、3 高)。都是能转手的货(枪械、钥匙卡、密码解序器芯片、爆炸物);弹药、毒品、药品玩家用不上,不上架。</summary>
public static readonly KeyValuePair<string, int>[] Pool =
{
P("handmade_pistol", 1), P("printed_gun", 1), P("stun_baton", 1),
P("eng_keycard", 1), P("med_keycard", 1), P("ser_keycard", 1), P("sup_keycard", 1), P("crypto_module_ser", 1), P("crypto_module_sup", 1),
P("revolver", 2), P("heavy_handmade_pistol", 2), P("flashbang_grenade", 2), P("smoke_grenade", 2),
P("sci_keycard", 2), P("sec_keycard", 2), P("crypto_module_eng", 2), P("crypto_module_med", 2),
P("shotgun", 3), P("smg", 3), P("c4", 3), P("cmd_keycard", 3), P("crypto_module_sec", 3),
};
/// <summary>高科技物品池(不是违禁品,黑市上要加价):矿机能用上的系统模组、能量电池、能量阵列,和矿架本身。</summary>
public static readonly string[] TechPool =
{
"system_module_overclock", "system_module_performance", "system_module_quality", "system_module_eco", "system_module_efficiancy",
"system_module_fineness", "system_module_streamlining", "energy_credit_ext", "energy_credit_hyp", "energy_credit_breeder",
"powerblock", "module_extractor_advanced", "pd_rig",
};
private static KeyValuePair<string, int> P(string id, int level) => new KeyValuePair<string, int>(id, level);
public static bool Open(int rep) => rep >= Tuning.BmOpenRep;
public static int MaxLevel(int rep) => rep >= Tuning.BmHighRep ? 3 : rep >= Tuning.BmMidRep ? 2 : rep >= Tuning.BmOpenRep ? 1 : 0;
public static bool DeskOpen(int rep) => rep >= Tuning.PoneroOpenRep;
public static int StockCount(int rep) => Tuning.BmStockBase + (int)Math.Max(0, rep / Tuning.BmStockPerRep);
/// <summary>货的加价:违禁品 0,高科技、显卡各有额外加价(在现货溢价 / 订单折扣之外)。</summary>
public static double Extra(GoodKind k) => k == GoodKind.Tech ? Tuning.BmTechMarkup : k == GoodKind.Gpu ? Tuning.BmGpuMarkup : 0;
/// <summary>现货单价(信用点):基础价 ×(1 + 溢价 + 货的加价)。</summary>
public static int PriceOf(long baseValue, GoodKind k = GoodKind.Contraband) => (int)Math.Max(1, Math.Round(baseValue * (1 + Tuning.BmPremium + Extra(k))));
/// <summary>抽 count 种货:高科技(中档声望起)和显卡按概率换掉违禁品。value 给出游戏物品的基础价;显卡的基础价按标称型号与成色算。</summary>
private List<BmGood> Roll(int count, int day, int rep, Rng rng, Func<string, long> value, Func<long, GoodKind, int> price)
{
var goods = new List<BmGood>();
var pool = Pool.Where(x => x.Value <= MaxLevel(rep)).ToList();
var tech = TechPool.ToList();
var gpus = UsedDealers.Pool(day);
for (int i = 0; i < count; i++)
{
double r = rng.NextDouble();
if (r < Tuning.BmGpuChance)
{
bool fresh = rng.Chance(Tuning.BmGpuNew);
var c = UsedDealers.Card(gpus, fresh ? 0 : Tuning.BmGpuFake, 0, rng);
if (fresh) c.Health = 100;
goods.Add(new BmGood { Item = "gpu:" + c.Model, Kind = GoodKind.Gpu, Level = 2, Model = c.Model, FakeOf = c.FakeOf, Health = c.Health, Price = price(CardValue.Of(c.Model, c.Health), GoodKind.Gpu) });
}
else if (r < Tuning.BmGpuChance + Tuning.BmTechChance && tech.Count > 0)
{
int k = rng.Range(0, tech.Count - 1);
string id = tech[k];
tech.RemoveAt(k);
long v = value(id);
if (v > 0) goods.Add(new BmGood { Item = id, Kind = GoodKind.Tech, Level = 3, Price = price(v, GoodKind.Tech) });
}
else if (pool.Count > 0)
{
int k = rng.Range(0, pool.Count - 1);
var pick = pool[k];
pool.RemoveAt(k);
long v = value(pick.Key);
if (v > 0) goods.Add(new BmGood { Item = pick.Key, Kind = GoodKind.Contraband, Level = pick.Value, Price = price(v, GoodKind.Contraband) });
}
}
return goods;
}
/// <summary>每周刷新一次(或首次开放时):按声望开放的档次抽 StockCount 种货,每种 1–3 件(高档与显卡、高科技 1 件)。</summary>
public bool Refresh(int day, int rep, Rng rng, Func<string, long> value)
{
if (!Open(rep) || day - RefreshedDay < Tuning.BmRefreshDays) return false;
RefreshedDay = day;
Offers.Clear();
foreach (var g in Roll(StockCount(rep), day, rep, rng, value, PriceOf))
Offers.Add(new BmOffer { Item = g.Item, Kind = g.Kind, Level = g.Level, Price = g.Price, Model = g.Model, FakeOf = g.FakeOf, Health = g.Health, Qty = g.Kind != GoodKind.Contraband || g.Level == 3 ? 1 : rng.Range(1, Tuning.BmSpotQtyMax) });
return true;
}
/// <summary>买一件:付 asset 币。付出的币按它的脏比例带走脏枚数,成本按价值比例留在卖家那边。成功返回 true,库存减一。</summary>
public bool Buy(BmOffer o, Wallet w, string asset, Func<string, double> price)
{
if (o == null || o.Qty <= 0 || asset == Assets.CRC) return false;
double p = price(asset);
if (p <= 0) return false;
double qty = o.Price / p;
if (w.Avail(asset) + 1e-9 < qty) return false;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, o.Price / total) : 0;
w.Basis -= w.Basis * share;
w.Add(asset, -qty);
o.Qty--;
if (o.Qty <= 0) Offers.Remove(o);
return true;
}
/// <summary>Ponero 对信用点的价格:跟随 PSC。</summary>
public static double PoneroPrice(double pscPrice) => pscPrice * Tuning.PoneroPscRatio;
public double DeskLeft(int day) => Math.Max(0, Tuning.PoneroDailyCap - (DeskDay == day ? DeskToday : 0));
/// <summary>兑入 Ponero:from 币 amount 个,扣手续费后按 Ponero 价到账;兑掉的币连同其中的脏枚数一起消失,换来的 Ponero 是干净的。返回到账数量,额度或余额不足返回 -1。</summary>
public double ToPonero(Wallet w, string from, double amount, int day, Func<string, double> price)
{
double value = amount * price(from);
if (from == Assets.Ponero || amount <= 0 || value <= 0 || w.Avail(from) + 1e-9 < amount || value > DeskLeft(day) + 1e-9) return -1;
w.Add(from, -amount);
double got = value * (1 - Tuning.PoneroFee) / price(Assets.Ponero);
w.Add(Assets.Ponero, got);
w.RecordBuy(Assets.Ponero, got, value);
Spend(day, value);
return got;
}
/// <summary>Ponero 兑出成 to 币:手续费同上,计入当天额度。返回到账数量,不足返回 -1。</summary>
public double FromPonero(Wallet w, string to, double amount, int day, Func<string, double> price)
{
double value = amount * price(Assets.Ponero);
if (to == Assets.Ponero || amount <= 0 || value <= 0 || w.Avail(Assets.Ponero) + 1e-9 < amount || value > DeskLeft(day) + 1e-9 || price(to) <= 0) return -1;
w.Add(Assets.Ponero, -amount);
double got = value * (1 - Tuning.PoneroFee) / price(to);
w.Add(to, got);
w.RecordBuy(to, got, value);
Spend(day, value);
return got;
}
private void Spend(int day, double value)
{
if (DeskDay != day) { DeskDay = day; DeskToday = 0; }
DeskToday += value;
}
}
}
+91
View File
@@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>商家货架上的一项货:游戏物品 id、货的种类、违禁等级(只对违禁品有意义,随黑市声望放开)。</summary>
public struct BmEntry
{
public string Id;
public GoodKind Kind;
public int Level;
}
/// <summary>
/// 商家类型的货表。除了矿卡,其余都按玩家能走的路线配货:矿工、酿酒师、化学师、药剂师、枪械师、水培农场主、养殖户、水务、仓储、工具、奢侈品,
/// 加上违禁品(军火、证件芯片)。大宗商家什么都卖。
/// </summary>
public static class Merchants
{
private static BmEntry C(string id, int level) => new BmEntry { Id = id, Kind = GoodKind.Contraband, Level = level };
private static BmEntry T(string id) => new BmEntry { Id = id, Kind = GoodKind.Tech, Level = 3 };
private static BmEntry G(string id) => new BmEntry { Id = id, Kind = GoodKind.Goods, Level = 0 };
private static BmEntry[] Gs(params string[] ids) => ids.Select(G).ToArray();
public static readonly Dictionary<MerchantType, BmEntry[]> Table = new Dictionary<MerchantType, BmEntry[]>
{
[MerchantType.Rigs] = new[] { T("pd_rig"), T("energy_credit_ext"), T("energy_credit_hyp"), T("energy_credit_breeder"), T("powerblock"), T("recharger"), T("current_converter") },
[MerchantType.Modules] = new[] { T("system_module_overclock"), T("system_module_performance"), T("system_module_quality"), T("system_module_eco"), T("system_module_efficiancy"),
T("system_module_fineness"), T("system_module_streamlining"), T("module_extractor_advanced"), T("module_bay_expansion_kit") },
[MerchantType.Arms] = new[] { C("handmade_pistol", 1), C("printed_gun", 1), C("stun_baton", 1), C("revolver", 2), C("heavy_handmade_pistol", 2), C("flashbang_grenade", 2), C("smoke_grenade", 2),
C("shotgun", 3), C("smg", 3), C("c4", 3), G("combat_knife"), G("combat_machete"), G("hatchet"), G("crowbar"), G("kitchen_cleaver"), G("skinning_knife"), G("stun_gun"), G("tazer") },
[MerchantType.GunMods] = Gs("silencer2", "laser2", "match_barrel", "trigger_group", "mag_10mm_high", "mag_22_high", "mag_10mm_smg_high", "rds", "compensator", "stock", "grip", "bipod", "barrel",
"printer_chip_gun_part", "printer_chip_silencer", "printer_chip_handmade_pistol_frame", "printer_chip_heavy_handmade_pistol_frame", "printer_module_cnc", "printer_module_metal", "gun_part", "printer_plastic"),
[MerchantType.Docs] = new[] { C("eng_keycard", 1), C("med_keycard", 1), C("ser_keycard", 1), C("sup_keycard", 1), C("crypto_module_ser", 1), C("crypto_module_sup", 1),
C("sci_keycard", 2), C("sec_keycard", 2), C("crypto_module_eng", 2), C("crypto_module_med", 2), C("cmd_keycard", 3), C("crypto_module_sec", 3) },
[MerchantType.Pharma] = new[] { C("unlicensed_phagimycin_pill", 2), C("dream_dust", 2), C("fanta_pill", 3), C("nightmare_dust", 3), C("zyanide_pill", 2),
G("black_injector"), G("pure_white_injector"), G("large_purple_injector"), G("pink_injector"), G("pink_alt_injector"), G("blue_blood_bag"), G("surgery_tool"), G("zerochew"), G("phagimycin_pill") },
[MerchantType.Chem] = new[] { C("restricted_chemical", 2), G("common_chemical"), G("flux_agent"), G("advanced_flux_agent"), G("chem_module"), G("evaporator_module_solvent_recovery"),
G("chem_scanner"), G("med_bottle_blue"), G("med_bottle_orange"), G("med_bottle_red"), G("med_bottle_small_pink"), G("med_bottle_violet"), G("100ml_cylinder"), G("5ml_dropper") },
[MerchantType.Brewer] = new[] { G("wine_yeast"), G("wine_superyeast"), G("wine_yeast_infinite"), C("wine_yeast_red", 1), G("wine_berry"), G("bloomberry_seed"), G("nudka"), G("red_beer"), G("beerCase"), G("wine_bottle") },
[MerchantType.Farm] = new[] { G("nutrifruit_seed"), G("kotton_seed"), G("fillerweed_seed"), G("hydroreed_seed"), G("bloomberry_seed"), C("dream_cap_seed", 1),
G("farm_module_nutrifruit"), G("farm_module_kotton_loom"), G("farm_module_fillerweed_production"), G("farm_module_hydroreed_filter"), G("farm_module_dream_cap"), G("farm_module_smuggler"),
G("plant_analyser"), G("plant_dna_disk"), G("nutrient_tablet"), G("hydroreed_filter") },
[MerchantType.Husbandry] = Gs("tilapia_roe", "sturgeon_roe", "caviar", "ras", "serum_green", "serum_red",
"livestock_feed_dispenser", "auto_butcher", "industrial_fish_feed", "animal_cage"),
[MerchantType.Miner] = Gs("common_ore", "rare_ore", "metal_ingot", "scrap_metal", "common_electronic", "rare_electronic", "hypertech_electronic", "metal_scanner", "wire", "advanced_flux_agent"),
[MerchantType.Water] = Gs("water_filter_adv", "portable_water_purifier", "water_tank", "uv_filter", "water_tablet_container", "water_test_container", "aquascan", "bottled_water_premium", "water_jug", "water_ration"),
[MerchantType.Storage] = Gs("smuggler_bay", "smuggler_bay_mod", "smuggler_bay_mini", "storage_bay_large", "storage_bay_expansion_kit", "hypertech_bay", "machine_bay_ext", "storage_bay_chem",
"storage_bay_gun", "backpack_large_military"),
[MerchantType.Tools] = Gs("turbo_booster", "turbo_booster_adv", "turbo_booster_hypertech", "welder", "xray_scanner", "magnifier", "labeler_blue", "toolbox", "wire_cutter", "mind_reprogrammer"),
[MerchantType.Luxury] = new[] { C("pheromone_perfume", 2), G("neuroactive_perfume"), G("self_refilling_neuroactive_perfume"), G("skincare_cream"), G("packet_red_cigarette"), G("gamblers_coin"), G("kotton_fabric"), G("armor_lining") },
};
/// <summary>
/// 各类商家收的币:硬件和技术类收 PSC,违禁类收 CRT 和隐私币,农业养殖水务收 PSC 和 CRA,个人卖家收 CRT 和隐私币。
/// 平台现货收 PSC、CRT、CRA。不在表里的币要先换:Web3 内置的自动换币要多付滑点(<see cref="Tuning.BmAutoSwapSlip"/>),也可以自己去 DEX 换。
/// </summary>
public static readonly string[] PlatformAccepts = { Assets.PSC, Assets.CRT, Assets.CRA };
public static string[] Accepts(MerchantType type)
{
switch (type)
{
case MerchantType.Rigs: case MerchantType.Modules: case MerchantType.Miner: case MerchantType.Tools: case MerchantType.Storage:
case MerchantType.GpuNew: case MerchantType.GpuUsed: case MerchantType.Asic: case MerchantType.Elite: case MerchantType.Bulk:
return new[] { Assets.PSC, Assets.CRT };
case MerchantType.Arms: case MerchantType.GunMods: case MerchantType.Docs: case MerchantType.Pharma: case MerchantType.Chem:
case MerchantType.Luxury: case MerchantType.Personal:
return new[] { Assets.CRT, Assets.Ponero };
default: // Brewer、Farm、Husbandry、Water
return new[] { Assets.PSC, Assets.CRA };
}
}
private static readonly Dictionary<string, BmEntry> ById = BuildIndex();
private static Dictionary<string, BmEntry> BuildIndex()
{
var d = new Dictionary<string, BmEntry>();
foreach (var e in Table.Values.SelectMany(x => x)) d[e.Id] = e;
foreach (var kv in BlackMarket.Pool) d[kv.Key] = new BmEntry { Id = kv.Key, Kind = GoodKind.Contraband, Level = kv.Value };
foreach (var id in BlackMarket.TechPool) d[id] = new BmEntry { Id = id, Kind = GoodKind.Tech, Level = 3 };
return d;
}
/// <summary>这个物品 id 在黑市上是不是一件货(现货柜台、商家货架里出现过的,都能转手出售)。</summary>
public static bool IsGood(string id) => ById.ContainsKey(id);
public static BmEntry Info(string id) => ById[id];
}
}
+359
View File
@@ -0,0 +1,359 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
public enum PayMode { Escrow, Advance }
/// <summary>商家类型:矿卡(全新 / 二手)、矿架能源、模组、军火、枪匠配件、证件芯片、药物、化学、酿酒、水培、养殖、矿工、水务、仓储、工具、奢侈品;大宗商家、尖端店铺(只卖贵货,概率低)、个人卖家(什么都卖,价格随心所欲)随机出现。</summary>
public enum MerchantType { GpuNew, GpuUsed, Rigs, Modules, Arms, GunMods, Docs, Pharma, Chem, Brewer, Farm, Husbandry, Miner, Water, Storage, Tools, Luxury, Bulk, Elite, Personal, Asic }
public enum OrderState { Pending, Ready, Done, Refunded, Lost }
/// <summary>黑市上的卖家:成交数与好评率公开;是不是骗子是隐藏的。</summary>
public sealed class BmSeller
{
public string Name;
public int Trades;
public int Rating; // 好评率 %
public bool Scam;
public MerchantType Type;
/// <summary>商家的价格系数(订单价 = 基础价 × 系数,各家不同)。</summary>
public double Factor;
public int Days => Type == MerchantType.Bulk ? Tuning.BmBulkDays : Type == MerchantType.Elite ? Tuning.BmEliteDays : Tuning.BmDoorDays;
/// <summary>只有成交数够多的卖家才接受提前放款。</summary>
public bool Advance => Trades >= Tuning.BmAdvanceMinTrades;
}
/// <summary>商家货架上的一种货:卖家、货、单价(订单价)、库存。</summary>
public sealed class BmListing : BmGood
{
public int Seller;
public int Qty;
}
public sealed class BmOrder
{
public int Id;
public string Item;
public string Seller;
public bool SellerScam;
public PayMode Mode;
public int Count = 1;
public string Model, FakeOf;
public double Health;
public int Price;
public string Asset;
public double Qty;
/// <summary>付款的钱包地址:退款回这里。</summary>
public string Wallet;
/// <summary>付款的币里脏的枚数和成本:退款时一并还回,免得"付脏币再退款"洗钱。</summary>
public double Dirty, Basis;
/// <summary>旧存档(数据版本 5 之前)的污点额(信用点),读档迁移时换成 <see cref="Dirty"/>。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
public int PlacedDay, DueDay;
/// <summary>被截获后执法选择了上门:到货时快递员是执法人员,签收即记罪证。</summary>
public bool Controlled;
public OrderState State = OrderState.Pending;
/// <summary>终态的原因:done / scam / rug / intercept / theft / refund。</summary>
public string Why;
}
/// <summary>一笔会被追查的交易:到期日、金额(信用点)、来源(honeypot / seller / controlled)。</summary>
public sealed class BmTrap
{
public int Day;
public double Amount;
public string Why;
public string Item;
/// <summary>对应的订单(没收到货的订单不会被追查);0 表示不挂订单(出售记录、落网)。</summary>
public int OrderId;
/// <summary>同一次蜜罐 / 同一次落网的追查归为一组,累计记罪证有上限。</summary>
public int Group;
}
/// <summary>夜间发生的订单事件,供日志与新闻使用。</summary>
public sealed class BmEvent
{
public string Kind; // ready / scam_refund / scam_loss / rug / intercept / reopen
public BmOrder Order;
public BmSale Sale;
public double Amount;
}
public sealed partial class BlackMarket
{
public List<BmSeller> Sellers = new List<BmSeller>();
public List<BmListing> Catalog = new List<BmListing>();
public List<BmOrder> Orders = new List<BmOrder>();
public List<string> Forum = new List<string>();
/// <summary>平台跑路后关闭到这一天(含)。</summary>
public int ClosedUntil = -1;
/// <summary>执法接管平台的最后一天(含);期间的交易之后会被追查。</summary>
public int HoneypotUntil = -1;
public int HoneypotStart = -1;
/// <summary>每组追查已经记过的罪证。</summary>
public Dictionary<int, double> Charged = new Dictionary<int, double>();
public List<BmTrap> Traps = new List<BmTrap>();
public int NextOrderId = 1;
/// <summary>订单没有交付(退款、被截获、骗子、平台跑路、拒签、被查封):对应的追查一并取消。</summary>
private void DropTraps(BmOrder o) => Traps.RemoveAll(t => t.OrderId == o.Id && o.Id != 0);
public bool Honeypot(int day) => day <= HoneypotUntil;
public static double HoneypotChance(int secRep) => Math.Min(Tuning.BmHoneypotMax, Tuning.BmHoneypotBase + Math.Max(0, secRep) * Tuning.BmHoneypotPerSecRep);
public bool Closed(int day) => day <= ClosedUntil;
/// <summary>托管中和已提前放款但尚未交付的资金(信用点),平台跑路的概率随它增加。</summary>
public double Held => Orders.Where(o => o.State == OrderState.Pending).Sum(o => (double)o.Price) + HeldSales;
/// <summary>批量折扣:一次订 5 件以上 95 折,10 件以上 9 折。</summary>
public static double BulkDiscount(int count) => count >= Tuning.BmBulk2Count ? Tuning.BmBulk2Discount : count >= Tuning.BmBulk1Count ? Tuning.BmBulk1Discount : 0;
public static int OrderTotal(int unit, int count) => (int)Math.Max(1, Math.Round(unit * count * (1 - BulkDiscount(count))));
public static int EscrowFee(int price) => (int)Math.Ceiling(price * Tuning.BmEscrowFee);
/// <summary>下单实付(信用点):托管 = 订单价 + 托管费;提前放款 = 订单价打折。</summary>
public static int Pay(int price, PayMode m) => m == PayMode.Escrow ? price + EscrowFee(price) : (int)Math.Round(price * (1 - Tuning.BmAdvanceDiscount));
public double RugChance => Scales.R(Math.Min(Tuning.BmRugMax, Tuning.BmRugBase + Held / 10000.0 * Tuning.BmRugPerTenK));
/// <summary>每周必有的"矿工路线"商家类型(从中抽 2 家),其余按全部类型抽。</summary>
private static readonly MerchantType[] Core = { MerchantType.GpuNew, MerchantType.GpuUsed, MerchantType.Rigs, MerchantType.Modules };
private static readonly MerchantType[] Others = { MerchantType.Arms, MerchantType.GunMods, MerchantType.Docs, MerchantType.Pharma, MerchantType.Chem, MerchantType.Brewer,
MerchantType.Farm, MerchantType.Husbandry, MerchantType.Miner, MerchantType.Water, MerchantType.Storage, MerchantType.Tools, MerchantType.Luxury };
private static double F(Rng rng, double lo, double hi) => lo + rng.NextDouble() * (hi - lo);
private static List<BmEntry> AllEntries() => Merchants.Table.Values.SelectMany(x => x).GroupBy(x => x.Id).Select(g => g.First()).ToList();
private static string[] HighGpus => Hardware.Specs.Values.Where(x => !x.Asic && !x.Scrap && x.Tier >= Tuning.BmEliteGpuTier).Select(x => x.Model).ToArray();
/// <summary>商家类型对应的货(含货种、库存),违禁品按声望放开等级。个人卖家什么都卖、每件各自定价;尖端店铺只卖贵货。</summary>
private List<BmListing> MerchantGoods(int idx, BmSeller s, int day, int rep, Rng rng, Func<string, long> value)
{
var list = new List<BmListing>();
if (s.Type == MerchantType.Asic) return AsicGoods(idx, s, rep, rng);
bool gpuKind = s.Type == MerchantType.GpuNew || s.Type == MerchantType.GpuUsed;
int n = s.Type == MerchantType.Bulk ? 2 : s.Type == MerchantType.Elite ? Tuning.BmEliteListings : s.Type == MerchantType.Personal ? Tuning.BmPersonalListings : Tuning.BmMerchantListings;
List<BmEntry> entries = gpuKind ? new List<BmEntry>() : s.Type == MerchantType.Bulk || s.Type == MerchantType.Personal || s.Type == MerchantType.Elite ? AllEntries() : Merchants.Table[s.Type].ToList();
entries = entries.Where(e => e.Kind != GoodKind.Contraband || e.Level <= MaxLevel(rep)).ToList();
var gpus = UsedDealers.Pool(day);
for (int i = 0; i < n; i++)
{
int qty = s.Type == MerchantType.Bulk ? rng.Range(Tuning.BmBulkQtyMin, Tuning.BmBulkQtyMax)
: s.Type == MerchantType.Elite ? rng.Range(1, Tuning.BmEliteQtyMax)
: s.Type == MerchantType.Personal ? rng.Range(1, Tuning.BmPersonalQtyMax)
: s.Type == MerchantType.GpuNew ? rng.Range(Math.Max(1, Tuning.BmQtyMin - Tuning.BmNewGpuQtyMinCut), Tuning.BmQtyMax - Tuning.BmNewGpuQtyMaxCut) : rng.Range(Tuning.BmQtyMin, Tuning.BmQtyMax);
double factor = s.Type == MerchantType.Personal ? F(rng, Tuning.BmPersonalFactorMin, Tuning.BmPersonalFactorMax) : s.Factor * F(rng, 1 - Tuning.BmPriceJitter, 1 + Tuning.BmPriceJitter);
BmListing l = null;
bool eliteGpu = s.Type == MerchantType.Elite && rng.Chance(Tuning.BmEliteGpuChance);
bool personalGpu = s.Type == MerchantType.Personal && rng.Chance(Tuning.BmPersonalGpuChance);
if (gpuKind || eliteGpu || personalGpu)
{
bool fresh = s.Type == MerchantType.GpuNew || eliteGpu || (personalGpu && rng.Chance(Tuning.BmPersonalGpuNew));
var pool = eliteGpu ? HighGpus : gpus;
var c = UsedDealers.Card(pool, fresh ? 0 : Tuning.BmGpuFake, 0, rng);
if (fresh) c.Health = 100;
l = new BmListing { Item = "gpu:" + c.Model, Kind = GoodKind.Gpu, Level = 2, Model = c.Model, FakeOf = c.FakeOf, Health = c.Health, Price = (int)Math.Max(1, Math.Round(CardValue.Of(c.Model, c.Health) * factor)) };
}
else
{
bool wantElite = s.Type == MerchantType.Elite || (s.Type == MerchantType.Personal && rng.Chance(Tuning.BmPersonalEliteShare));
BmEntry pick = default; long v = 0; bool found = false;
for (int tries = 0; tries < 40 && entries.Count > 0 && !found; tries++)
{
int k = rng.Range(0, entries.Count - 1);
var e = entries[k];
long val = value(e.Id);
if (val <= 0) { entries.RemoveAt(k); continue; }
bool ok = s.Type == MerchantType.Elite ? val >= Tuning.BmEliteMinValue : wantElite ? val > Tuning.BmMaxUnitValue : val <= Tuning.BmMaxUnitValue;
if (!ok) continue;
pick = e; v = val; found = true; entries.RemoveAt(k);
}
if (!found) continue;
l = new BmListing { Item = pick.Id, Kind = pick.Kind, Level = pick.Level, Price = (int)Math.Max(1, Math.Round(v * factor)) };
}
l.Seller = idx; l.Qty = qty;
list.Add(l);
}
return list;
}
public static int AsicRep(string model) => model == "M3" ? Tuning.AsicRepM3 : model == "M2" ? Tuning.AsicRepM2 : Tuning.AsicRepM1;
private static double AsicWeight(string model) => model == "M3" ? Tuning.AsicWeightM3 : model == "M2" ? Tuning.AsicWeightM2 : Tuning.AsicWeightM1;
/// <summary>矿机商:一台 RatDigg 矿机,型号按黑市声望放开,新机(70%)或二手;价格 = 机价(二手按健康度折算)× 商家系数。</summary>
private List<BmListing> AsicGoods(int idx, BmSeller s, int rep, Rng rng)
{
var models = new[] { "M1", "M2", "M3" }.Where(m => rep >= AsicRep(m)).ToList();
if (models.Count == 0) return new List<BmListing>();
double roll = rng.NextDouble() * models.Sum(AsicWeight);
string model = models.Last();
foreach (var m in models) { roll -= AsicWeight(m); if (roll <= 0) { model = m; break; } }
bool fresh = rng.Chance(Tuning.AsicNewShare);
double health = fresh ? 100 : rng.Range(Tuning.AsicUsedHealthMin, Tuning.AsicUsedHealthMax);
double factor = s.Factor * F(rng, 1 - Tuning.BmPriceJitter, 1 + Tuning.BmPriceJitter);
return new List<BmListing>
{
new BmListing { Seller = idx, Item = "gpu:" + model, Kind = GoodKind.Gpu, Level = 2, Model = model, Health = health, Qty = model == "M1" ? rng.Range(1, 2) : 1,
Price = (int)Math.Max(1, Math.Round(CardValue.Of(model, health) * factor)) },
};
}
/// <summary>每周重新生成商家、货架和论坛情报(与现货同一天刷新):从 6 种类型里抽 BmMerchantCount 家,另有概率出现一家大宗商家。</summary>
public void RefreshOrders(int day, int rep, Rng rng, Func<string, long> value, bool allTypes = false)
{
Sellers.Clear(); Catalog.Clear(); Forum.Clear();
var types = Core.OrderBy(_ => rng.NextDouble()).Take(Tuning.BmCoreMerchants).ToList();
types.AddRange(Others.OrderBy(_ => rng.NextDouble()).Take(Tuning.BmMerchantCount - Tuning.BmCoreMerchants));
if (allTypes) { types = ((MerchantType[])Enum.GetValues(typeof(MerchantType))).ToList(); types.Add(MerchantType.Personal); }
else
{
if (rng.Chance(Tuning.BmBulkChance)) types.Add(MerchantType.Bulk);
if (rng.Chance(Tuning.BmEliteChance)) types.Add(MerchantType.Elite);
if (rep >= Tuning.AsicRepM1 && rng.Chance(Tuning.AsicChance)) types.Add(MerchantType.Asic);
for (int i = 0; i < Tuning.BmPersonalCount; i++) types.Add(MerchantType.Personal);
}
foreach (var t in types)
{
bool personal = t == MerchantType.Personal;
bool scam = rng.Chance(personal ? Tuning.BmPersonalScam : Tuning.BmScamSellerShare);
// 骗子多半是新号,也有刷过好评的老号;个人卖家都是新号
bool fresh = personal || (scam ? rng.Chance(Tuning.BmScamFresh) : rng.Chance(Tuning.BmHonestFresh));
double lo = t == MerchantType.Elite || t == MerchantType.Asic ? Tuning.BmFactorEliteMin : t == MerchantType.GpuNew ? Tuning.BmFactorNewMin : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMin : t == MerchantType.Bulk ? Tuning.BmFactorBulkMin : Tuning.BmFactorMin;
double hi = t == MerchantType.Elite || t == MerchantType.Asic ? Tuning.BmFactorEliteMax : t == MerchantType.GpuNew ? Tuning.BmFactorNewMax : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMax : t == MerchantType.Bulk ? Tuning.BmFactorBulkMax : Tuning.BmFactorMax;
var s = new BmSeller
{
Name = ChainAddress.Make(rng.NextUInt()),
Scam = scam, Type = t, Factor = personal ? 1.0 : F(rng, lo, hi),
Trades = fresh ? rng.Range(0, Tuning.BmFreshTradesMax) : rng.Range(Tuning.BmOldTradesMin, Tuning.BmOldTradesMax),
Rating = scam && !fresh ? rng.Range(Tuning.BmScamOldRatingMin, Tuning.BmScamOldRatingMax) : fresh ? rng.Range(Tuning.BmFreshRatingMin, Tuning.BmFreshRatingMax) : rng.Range(Tuning.BmOldRatingMin, Tuning.BmOldRatingMax),
};
Sellers.Add(s);
if (scam && rng.Chance(Tuning.BmForumScamHint)) Forum.Add("scam:" + s.Name);
}
for (int i = 0; i < Sellers.Count; i++) Catalog.AddRange(MerchantGoods(i, Sellers[i], day, rep, rng, value));
if (RugChance > Scales.R(Tuning.BmRugBase * Tuning.BmForumRugFactor)) Forum.Add("rug");
}
/// <summary>目录里全部货按订单价算的总值(信用点),用来核对每周周转量。</summary>
public double CatalogValue => Catalog.Sum(l => (double)l.Price * l.Qty);
/// <summary>下单:钱包里的币按实付折算扣除,脏枚数与成本按比例留在卖家那边(记在订单上)。成功返回订单。</summary>
public BmOrder Place(BmListing l, int count, PayMode mode, Wallet w, string asset, int day, Func<string, double> price)
{
if (l == null || count < 1 || count > l.Qty || asset == Assets.CRC || Closed(day)) return null;
var seller = Sellers[l.Seller];
if (mode == PayMode.Advance && !seller.Advance) return null;
double p = price(asset);
if (p <= 0) return null;
int credits = Pay(OrderTotal(l.Price, count), mode);
double qty = credits / p;
if (w.Avail(asset) + 1e-9 < qty) return null;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, credits / total) : 0;
double dirty = qty * w.DirtyRatio(asset), basis = w.Basis * share;
w.Basis -= basis;
w.Add(asset, -qty);
var o = new BmOrder
{
Id = NextOrderId++, Item = l.Item, Seller = seller.Name, SellerScam = seller.Scam, Mode = mode, Count = count, Model = l.Model, FakeOf = l.FakeOf, Health = l.Health, Price = credits, Asset = asset, Qty = qty,
Dirty = dirty, Basis = basis, PlacedDay = day, DueDay = day + seller.Days, Wallet = w.Address,
};
l.Qty -= count;
if (l.Qty <= 0) Catalog.Remove(l);
Orders.Add(o);
if (Honeypot(day)) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = credits, Why = "honeypot", Item = l.Item, OrderId = o.Id, Group = HoneypotStart });
return o;
}
private static void Refund(Wallet w, BmOrder o)
{
w.AddDirty(o.Asset, o.Qty, o.Dirty);
w.Basis += o.Basis;
}
/// <summary>每晚推进订单:平台跑路、到期的订单(卖家跑路、被截获、死投被挖走)、转入可取状态。镇压期间到期顺延一天。</summary>
public List<BmEvent> Night(int day, bool crackdown, Wallet w, Rng rng, int secRep = 0, Func<string, double> price = null) => Night(day, crackdown, _ => w, rng, secRep, price);
/// <summary>每晚(多钱包):退款回付款的钱包,出售回款进上架时付费的钱包。</summary>
public List<BmEvent> Night(int day, bool crackdown, Func<string, Wallet> w, Rng rng, int secRep = 0, Func<string, double> price = null)
{
var ev = new List<BmEvent>();
if (!Honeypot(day) && rng.Chance(HoneypotChance(secRep))) { HoneypotUntil = day + Tuning.BmHoneypotDays; HoneypotStart = day; if (!Forum.Contains("easy")) Forum.Add("easy"); }
if (ClosedUntil == day) ev.Add(new BmEvent { Kind = "reopen" });
if (!Closed(day) && (Orders.Any(o => o.State == OrderState.Pending) || Sales.Any(x => x.State == SaleState.Listed || x.State == SaleState.Sold)) && rng.Chance(RugChance))
{
foreach (var o in Orders.Where(o => o.State == OrderState.Pending))
{
o.State = OrderState.Lost; o.Why = "rug"; DropTraps(o);
ev.Add(new BmEvent { Kind = "rug", Order = o, Amount = o.Price });
}
foreach (var sl in Sales.Where(x => x.State == SaleState.Listed || x.State == SaleState.Sold))
{
ev.Add(new BmEvent { Kind = "sale_rug", Sale = sl, Amount = sl.State == SaleState.Sold ? sl.Proceeds : sl.ListPrice });
sl.State = SaleState.Lost; sl.Why = "rug";
}
ClosedUntil = day + Tuning.BmRugClosedDays;
Offers.Clear(); Catalog.Clear(); Sellers.Clear();
return ev;
}
foreach (var o in Orders.Where(x => x.State == OrderState.Pending).ToList())
{
if (day < o.DueDay) continue;
if (crackdown) { o.DueDay = day + 1; continue; }
if (o.SellerScam)
{
if (o.Mode == PayMode.Escrow) { Refund(w(o.Wallet), o); o.State = OrderState.Refunded; o.Why = "scam"; DropTraps(o); ev.Add(new BmEvent { Kind = "scam_refund", Order = o, Amount = o.Price }); }
else { o.State = OrderState.Lost; o.Why = "scam"; DropTraps(o); ev.Add(new BmEvent { Kind = "scam_loss", Order = o, Amount = o.Price }); }
continue;
}
double ic = Tuning.BmInterceptDoor + (crackdown ? Tuning.BmInterceptCrackdown : 0);
if (rng.Chance(ic))
{
if (rng.Chance(Tuning.BmControlled)) { o.Controlled = true; o.State = OrderState.Ready; ev.Add(new BmEvent { Kind = "ready", Order = o }); continue; }
o.State = OrderState.Lost; o.Why = "intercept"; DropTraps(o); ev.Add(new BmEvent { Kind = "intercept", Order = o, Amount = o.Price }); continue;
}
o.State = OrderState.Ready;
ev.Add(new BmEvent { Kind = "ready", Order = o });
}
if (price != null) SalesNight(day, crackdown, w, price, rng, ev);
// 卖家落网:近期成交过的卖家各有小概率被抓,他们留存的订单记录之后会连累买家
foreach (var name in Orders.Where(x => x.State == OrderState.Done && day - x.DueDay <= 14).Select(x => x.Seller).Distinct().ToList())
{
if (!rng.Chance(Tuning.BmSellerBust)) continue;
foreach (var o in Orders.Where(x => x.State == OrderState.Done && x.Seller == name && day - x.DueDay <= 14))
Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = o.Price, Why = "seller", Item = o.Item, Group = -day });
Forum.Add("busted:" + name);
}
// 同一晚到期的追查按来源(同一次蜜罐 / 落网)合成一次事件;记入货款的一部分,同一次累计有上限
var due = Traps.Where(x => day >= x.Day).ToList();
foreach (var g in due.GroupBy(x => new { x.Why, x.Group }))
{
Charged.TryGetValue(g.Key.Group, out double done);
double amount = Math.Min(g.Sum(x => x.Amount) * Tuning.BmTrapShare, Tuning.BmTrapWindowCap - done);
if (amount < 1) continue;
Charged[g.Key.Group] = done + amount;
ev.Add(new BmEvent { Kind = "trap", Amount = amount, Order = new BmOrder { Item = g.First().Item, Seller = g.Key.Why } });
}
Traps.RemoveAll(x => day >= x.Day);
Orders.RemoveAll(o => o.State != OrderState.Pending && o.State != OrderState.Ready && day - o.DueDay > 14);
return ev;
}
/// <summary>受控交付:签收则当场记罪证(返回金额)并查封,不交货;拒签则损失货款,不留罪证。</summary>
public double Sign(BmOrder o, bool accept)
{
o.State = OrderState.Lost;
o.Why = accept ? "seized" : "refused";
DropTraps(o);
return accept ? o.Price : 0;
}
public void Deliver(BmOrder o) { o.State = OrderState.Done; o.Why = "done"; }
public IEnumerable<BmOrder> Ready => Orders.Where(o => o.State == OrderState.Ready);
}
}
+184
View File
@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
public enum SaleState { Listed, Sold, Paid, Lost, Delisted }
/// <summary>玩家在黑市上架的一件货。货交给平台(从储藏区拿走),成交后货款托管几天再到钱包,可能带污点。</summary>
public sealed class BmSale
{
public string Item;
public string Model, FakeOf;
public double Health;
/// <summary>标价倍率与标价(信用点)。</summary>
public double Mult;
/// <summary>参考价:改价时按它重算标价。</summary>
public long Ref;
public int ListPrice;
/// <summary>上架时付费的钱包地址:回款进这里。</summary>
public string Wallet;
public int ListedDay, PayDay;
public SaleState State = SaleState.Listed;
/// <summary>成交后扣掉抽成的货款(信用点)与其中带的污点。</summary>
public int Proceeds;
public double Taint;
/// <summary>上架那件货的赃物热度(0–100):赃物的货款按"货款 × 热度%"带污点,不走随机。</summary>
public double Heat;
/// <summary>成交那一轮的收款币(空 = CRT,旧存档):货款到期后以这个币付给钱包。</summary>
public string PayAsset;
/// <summary>终态原因:fake(买家看穿假卡退货)/ rug / delist。</summary>
public string Why;
/// <summary>聚合键:同一种商品(标识、型号、真伪、成色取整)算一组,同组倍率相同。</summary>
public static string KeyOf(string item, string model, string fakeOf, double health) => item + "|" + model + "|" + fakeOf + "|" + Math.Round(health);
public string Key => KeyOf(Item, Model, FakeOf, Health);
}
public sealed partial class BlackMarket
{
public List<BmSale> Sales = new List<BmSale>();
/// <summary>玩家作为卖家的好评(%):影响成交速度;卖假卡被看穿会下降,每笔成交回升一点。</summary>
public int Rating = Tuning.BmRatingStart;
/// <summary>期望成交天数:倍率不高于 BmSellFastMult 取 BmSellDaysFast,BmSellDay1Mult 取 BmSellDays1,BmSellMax 取 BmSellDaysMax,之间插值(快档到一档线性,一档以上指数)。</summary>
public static double ExpectedDays(double m)
{
if (m <= Tuning.BmSellFastMult) return Tuning.BmSellDaysFast;
if (m < Tuning.BmSellDay1Mult) return Tuning.BmSellDaysFast + (m - Tuning.BmSellFastMult) / (Tuning.BmSellDay1Mult - Tuning.BmSellFastMult) * (Tuning.BmSellDays1 - Tuning.BmSellDaysFast);
return Tuning.BmSellDays1 * Math.Pow(Tuning.BmSellDaysMax / Tuning.BmSellDays1, (m - Tuning.BmSellDay1Mult) / (Tuning.BmSellMax - Tuning.BmSellDay1Mult));
}
private static double T(double m) => Math.Max(0, Math.Min(1, (m - Tuning.BmSellMin) / (Tuning.BmSellMax - Tuning.BmSellMin)));
/// <summary>货款带污点的概率与(带的时候)污点占货款的比例:都随标价倍率上升。</summary>
public static double TaintChance(double m) => Tuning.BmSaleTaintChanceMin + T(m) * (Tuning.BmSaleTaintChanceMax - Tuning.BmSaleTaintChanceMin);
public static double TaintRatio(double m) => Tuning.BmSaleTaintRatioMin + T(m) * (Tuning.BmSaleTaintRatioMax - Tuning.BmSaleTaintRatioMin);
/// <summary>每晚成交的概率:1 / 期望天数,乘好评系数(好评 90 为 1,封顶 1.1),最多 1。</summary>
public double SaleChance(double m) => Math.Min(1, 1 / ExpectedDays(m) * Math.Min(1.1, Rating / (double)Tuning.BmRatingStart));
/// <summary>近 7 天成交额推高的污点加成:出售量越大,货款越容易带污点、比例越高。</summary>
public double VolumeTaint(int day)
{
double v = Sales.Where(x => (x.State == SaleState.Sold || x.State == SaleState.Paid) && day - (x.PayDay - Tuning.BmSaleHold) <= 7).Sum(x => (double)x.Proceeds);
return Math.Min(Tuning.BmVolumeTaintMax, v / Tuning.BmVolumeRef * Tuning.BmVolumeTaintMax);
}
/// <summary>收款币:愿意接受的币(存档),至少一种;默认只有 CRT。</summary>
public List<string> SellAccepts = new List<string> { Assets.CRT };
public static readonly string[] SellCoins = { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero };
public static double CoinWeight(string asset) =>
asset == Assets.PSC ? Tuning.BmWeightPsc : asset == Assets.CRT ? Tuning.BmWeightCrt : asset == Assets.CRA ? Tuning.BmWeightCra : asset == Assets.Ponero ? Tuning.BmWeightPonero : 0;
public IEnumerable<string> AcceptedCoins => SellAccepts.Count == 0 ? new[] { Assets.CRT } : SellAccepts.Where(a => CoinWeight(a) > 0).Distinct().ToArray();
/// <summary>开关一种收款币;最后一种不能关掉。</summary>
public bool ToggleCoin(string asset)
{
if (SellAccepts.Count == 0) SellAccepts.Add(Assets.CRT);
if (SellAccepts.Contains(asset)) { if (SellAccepts.Count <= 1) return false; SellAccepts.Remove(asset); return true; }
if (CoinWeight(asset) <= 0) return false;
SellAccepts.Add(asset);
return true;
}
/// <summary>在架的件数(全部)、种数、某一组的件数与倍率(没有在架返回 null)。</summary>
public int ActiveListings => Sales.Count(x => x.State == SaleState.Listed);
public int ActiveKinds => Sales.Where(x => x.State == SaleState.Listed).Select(x => x.Key).Distinct().Count();
public int ActiveOf(string key) => Sales.Count(x => x.State == SaleState.Listed && x.Key == key);
public double? MultOf(string key) { var s = Sales.FirstOrDefault(x => x.State == SaleState.Listed && x.Key == key); return s == null ? (double?)null : s.Mult; }
/// <summary>某一组再上架 add 件、倍率 m 时的限制:null = 可以;stack = 超过每种上限;kinds = 超过种数上限;mult = 与已在架的同种货倍率不同。</summary>
public string ListBlock(string key, double m, int add = 1)
{
int have = ActiveOf(key);
if (have + add > Tuning.BmMaxStack) return "stack";
if (have == 0 && ActiveKinds >= Tuning.BmMaxKinds) return "kinds";
var cur = MultOf(key);
if (cur != null && Math.Abs(cur.Value - m) > 1e-9) return "mult";
return null;
}
public static int ListPriceOf(long refValue, double m) => (int)Math.Max(1, Math.Round(refValue * m));
public static int ListFee(int listPrice) => (int)Math.Ceiling(listPrice * Tuning.BmListFee);
/// <summary>上架:交付一件货,收上架费(付给 feeAsset 币,污点与成本按比例带走)。heat 是这件货的赃物热度(0–100)。倍率超出范围、平台关闭或余额不足返回 null。</summary>
public BmSale List(string item, string model, string fakeOf, double health, long refValue, double m, Wallet w, string feeAsset, int day, Func<string, double> price, double heat = 0)
{
if (m < Tuning.BmSellMin - 1e-9 || m > Tuning.BmSellMax + 1e-9 || refValue <= 0 || Closed(day) || feeAsset == Assets.CRC || ListBlock(BmSale.KeyOf(item, model, fakeOf, health), m) != null) return null;
int listPrice = ListPriceOf(refValue, m);
double p = price(feeAsset);
if (p <= 0) return null;
double qty = ListFee(listPrice) / p;
if (w.Avail(feeAsset) + 1e-9 < qty) return null;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, ListFee(listPrice) / total) : 0;
w.Basis -= w.Basis * share;
w.Add(feeAsset, -qty);
var s = new BmSale { Item = item, Model = model, FakeOf = fakeOf, Health = health, Ref = refValue, Mult = m, ListPrice = listPrice, ListedDay = day, Wallet = w.Address, Heat = Math.Max(0, Math.Min(100, heat)) };
Sales.Add(s);
return s;
}
public void Delist(BmSale s) { s.State = SaleState.Delisted; s.Why = "delist"; }
/// <summary>未到账的货款(信用点):跑路时一并归零,也推高跑路概率。</summary>
public double HeldSales => Sales.Where(x => x.State == SaleState.Sold).Sum(x => (double)x.Proceeds);
/// <summary>每晚推进出售:上架的货按概率成交(假卡可能被看穿退货),成交的货款托管到期后进钱包。镇压期间平台下线,不推进。</summary>
private void SalesNight(int day, bool crackdown, Func<string, Wallet> wallet, Func<string, double> price, Rng rng, List<BmEvent> ev)
{
if (crackdown) return;
foreach (var s in Sales.Where(x => x.State == SaleState.Listed && x.ListedDay < day).ToList())
{
// 每接受一种币算一轮成交:轮次顺序随机,任一轮命中即成交,货款用命中的币付
string hit = null;
double chance = SaleChance(s.Mult);
foreach (var c in AcceptedCoins.OrderBy(_ => rng.NextDouble()).ToList())
if (rng.Chance(Math.Min(1, chance * CoinWeight(c)))) { hit = c; break; }
if (hit == null) continue;
s.PayAsset = hit;
if (s.FakeOf != null && rng.Chance(Tuning.BmFakeDetect))
{
s.State = SaleState.Delisted; s.Why = "fake";
Rating = Math.Max(0, Rating - Tuning.BmRatingFakeLoss);
ev.Add(new BmEvent { Kind = "sale_fake", Sale = s });
continue;
}
s.State = SaleState.Sold;
s.Proceeds = (int)Math.Round(s.ListPrice * (1 - Tuning.BmCommission));
double vb = VolumeTaint(day);
s.Taint = s.Heat > 0 ? s.Proceeds * s.Heat / 100
: rng.Chance(Math.Min(1, TaintChance(s.Mult) + vb)) ? s.Proceeds * Math.Min(1, TaintRatio(s.Mult) + vb) : 0;
s.PayDay = day + Tuning.BmSaleHold;
Rating = Math.Min(100, Rating + 1);
if (Honeypot(day)) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = s.Proceeds, Why = "honeypot", Item = s.Item, Group = HoneypotStart });
ev.Add(new BmEvent { Kind = "sale_sold", Sale = s, Amount = s.Proceeds });
}
foreach (var s in Sales.Where(x => x.State == SaleState.Sold && day >= x.PayDay).ToList())
{
string pa = s.PayAsset ?? Assets.CRT;
double p = price(pa);
if (p <= 0) continue;
var w = wallet(s.Wallet);
// 回款的币按 污点 / 回款额 的比例是脏的
double got = s.Proceeds / p;
w.AddDirty(pa, got, s.Proceeds > 0 ? got * Math.Min(1, s.Taint / s.Proceeds) : 0);
w.Basis += s.Proceeds;
s.State = SaleState.Paid;
ev.Add(new BmEvent { Kind = "sale_paid", Sale = s, Amount = s.Proceeds });
}
// 买家落网:近期到账的出售记录会连累卖家(玩家)
var recent = Sales.Where(x => x.State == SaleState.Paid && day - x.PayDay <= 14).ToList();
if (recent.Count > 0 && rng.Chance(Tuning.BmSellerBust))
{
foreach (var s in recent) Traps.Add(new BmTrap { Day = day + Tuning.BmTrapDelay, Amount = s.Proceeds, Why = "seller", Item = s.Item, Group = -day });
Forum.Add("busted_buyer");
}
Sales.RemoveAll(x => (x.State == SaleState.Paid || x.State == SaleState.Delisted || x.State == SaleState.Lost) && day - Math.Max(x.ListedDay, x.PayDay) > 14);
}
}
}
+86
View File
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>一天一根蜡烛:开盘接前一天的收盘价、包括夜间变动和白天真实发生过的最高 / 最低、夜间结算前的收盘价;成交额分玩家和 NPC 大户(信用点)。</summary>
public sealed class Candle
{
public int Day;
public double O, H, L, C, VolPlayer, VolNpc;
public bool Closed;
}
/// <summary>
/// 日 K 历史(按币种)。开门时开一根,白天的价格变动(开门波动、插针、大户、玩家大单)更新高低和现价,夜间结算时收盘。
/// 没有发生过的日内走势不编造:没有事件的日子高低就等于开收。
/// </summary>
public sealed class CandleBook
{
public const int Keep = 60;
public Dictionary<string, List<Candle>> Series = new Dictionary<string, List<Candle>>();
public List<Candle> Of(string asset) => Series.TryGetValue(asset, out var s) ? s : new List<Candle>();
/// <summary>今天的蜡烛:没有就按现价开一根(读档时补上,开盘价就是读档时的价格)。</summary>
public Candle Today(string asset, int day, double price)
{
if (!Series.TryGetValue(asset, out var s)) Series[asset] = s = new List<Candle>();
var c = s.Count > 0 ? s[s.Count - 1] : null;
if (c != null && c.Day == day) return c;
c = new Candle { Day = day, O = price, H = price, L = price, C = price };
s.Add(c);
while (s.Count > Keep) s.RemoveAt(0);
return c;
}
/// <summary>
/// 开门:新开一根。币是全天候交易的,所以开盘价接昨天的收盘价,夜里的回归和开门波动都算进这根蜡烛(没有跳空);
/// 没有昨天的(刚开始记录)就从现价开。
/// </summary>
public void Open(string asset, int day, double price)
{
var s = Of(asset);
var prev = s.Count > 0 && s[s.Count - 1].Day < day ? s[s.Count - 1] : null;
var c = Today(asset, day, price);
if (c.Closed) return;
double o = prev != null ? prev.C : price;
c.O = o; c.H = Math.Max(o, price); c.L = Math.Min(o, price); c.C = price;
}
/// <summary>白天的价格变动。</summary>
public void Tick(string asset, int day, double price, double volume = 0, bool player = false)
{
var c = Today(asset, day, price);
if (c.Closed) return;
c.H = Math.Max(c.H, price); c.L = Math.Min(c.L, price); c.C = price;
if (player) c.VolPlayer += Math.Abs(volume); else c.VolNpc += Math.Abs(volume);
}
/// <summary>夜间结算:收盘。</summary>
public void Close(string asset, int day, double price)
{
Tick(asset, day, price);
Today(asset, day, price).Closed = true;
}
/// <summary>NPC 当天的成交额(信用点):基础量随池深,波动越大成交越多。</summary>
public static double NpcVolume(double depth, double share, double absMove, Rng rng) =>
depth * share * (1 + Tuning.ChartVolMove * absMove) * rng.Range(0.6, 1.4);
/// <summary>收盘价的 n 日均线(不足 n 根时用已有的);每根蜡烛一个值。</summary>
public static List<double> Ma(IList<Candle> s, int n)
{
var r = new List<double>();
for (int i = 0; i < s.Count; i++)
{
int a = Math.Max(0, i - n + 1);
double sum = 0;
for (int j = a; j <= i; j++) sum += s[j].C;
r.Add(sum / (i - a + 1));
}
return r;
}
}
}
+191
View File
@@ -0,0 +1,191 @@
using System;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>卖家的说法:每个标签一项。型号是文字,其余取值与 <see cref="CardLook"/> 一致;「供电与风扇」由说法里的型号决定档次。</summary>
public struct Claim
{
public string Model;
public int Fan, Fingers, Bracket, Print, Sticker, Serial;
/// <summary>风扇数、供电口:0 表示由型号的档次决定(说法里写的型号该有的),否则是写明的值(风扇数 1 到 3;供电口 1 到 3,对应 0 到 2 加一)。</summary>
public int CountSet, PinSet;
public int Tier => Hardware.Specs[Model].Tier;
public int Count => CountSet != 0 ? CountSet : Hardware.FanCount[Tier];
public int Pin => PinSet != 0 ? PinSet - 1 : Hardware.PinClass[Tier];
public int Get(int row) => row == CardLabels.RowCount ? Count : row == CardLabels.RowPin ? Pin : row == CardLabels.RowFan ? Fan : row == CardLabels.RowFingers ? Fingers
: row == CardLabels.RowBracket ? Bracket : row == CardLabels.RowPrint ? Print : row == CardLabels.RowSerial ? Serial : Sticker;
public Claim With(int row, int value)
{
var c = this;
if (row == CardLabels.RowFan) c.Fan = value; else if (row == CardLabels.RowFingers) c.Fingers = value;
else if (row == CardLabels.RowBracket) c.Bracket = value; else if (row == CardLabels.RowPrint) c.Print = value;
else if (row == CardLabels.RowSerial) c.Serial = value; else if (row == CardLabels.RowSeal) c.Sticker = value;
else if (row == CardLabels.RowCount) c.CountSet = value; else if (row == CardLabels.RowPin) c.PinSet = value + 1;
return c;
}
public string Encode() => $"{Model}:{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}{CountSet}{PinSet}";
public static Claim Decode(string s)
{
var p = (s ?? "").Split(':');
if (p.Length != 2 || (p[1].Length != 6 && p[1].Length != 8) || !Hardware.Specs.ContainsKey(p[0])) return default;
int D(int i) => Math.Max(0, Math.Min(2, p[1][i] - '0'));
int E(int i) => p[1].Length > i ? Math.Max(0, Math.Min(3, p[1][i] - '0')) : 0;
return new Claim { Model = p[0], Fan = D(0), Fingers = D(1), Bracket = D(2), Print = D(3), Sticker = D(4), Serial = D(5), CountSet = E(6), PinSet = E(7) };
}
/// <summary>按真实情况写出的说法(没有隐瞒)。</summary>
public static Claim Truth(Unit u)
{
var l = u.Appearance;
return new Claim { Model = u.FakeOf ?? u.Model, Fan = l.Fan, Fingers = l.Fingers, Bracket = l.Bracket, Print = l.Print, Sticker = l.Sticker, Serial = l.Serial, CountSet = u.ObservedCount, PinSet = u.ObservedPin + 1 };
}
}
/// <summary>
/// 标签栏的数据:五个标签(型号真伪、风扇叶片、金手指、挡板、封条),每个标签有卖家的说法和真实情况;
/// 供电与风扇、背面丝印、序列号三项是型号真伪的依据,只存状态,不单独成行。
/// 一个标签被指出(拆穿)之后,说法改回真实情况;价格按「已指出的标签取真实情况,其余取说法」算,
/// 所以卖家隐瞒的问题要玩家一项项指出来才能把价格拉回真实价值。指出型号有问题,型号自动改回真实型号。
/// </summary>
public static class CardLabels
{
public const int RowSpec = 1, RowCount = 2, RowFan = 4, RowFingers = 8, RowBracket = 16, RowPrint = 32, RowSerial = 64, RowSeal = 128, RowPin = 256, AllRows = 511;
/// <summary>标签栏的顺序:风扇数、供电口、背面丝印、序列号(真伪依据),型号,叶片、金手指、挡板、封条(成色)。</summary>
public static readonly int[] Rows = { RowSpec, RowCount, RowPin, RowPrint, RowSerial, RowFan, RowFingers, RowBracket, RowSeal };
/// <summary>第一行是型号(唯一的“指出型号”按钮),接着四行是真伪依据(风扇数、供电口、背面丝印、序列号,只读,四项全部符合标称型号才是正品),之后四行是成色。</summary>
public const int AuthRows = 5;
public static bool IsAuth(int row) => row == RowCount || row == RowPin || row == RowPrint || row == RowSerial;
/// <summary>各标签各取值对价格的影响(百分比,相加):风扇叶片积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失 −6%。
/// 型号、供电与风扇、丝印、序列号本身不扣价,它们决定的是这块卡是什么芯片。</summary>
public static double Pct(int row, int value)
{
switch (row)
{
case RowFan: return value == 1 ? -Tuning.CardFanDust * 100 : value == 2 ? -Tuning.CardFanChipped * 100 : 0;
case RowFingers: return value == 1 ? -Tuning.CardFingersDark * 100 : value == 2 ? -Tuning.CardFingersOxidized * 100 : 0;
case RowBracket: return value == 1 ? -Tuning.CardBracketScratch * 100 : 0;
case RowSeal: return value == 2 ? -Tuning.CardSealMissing * 100 : 0;
}
return 0;
}
/// <summary>这一行是不是假货的证据:说法与真实情况不同,并且真实情况本身不该出现在真卡上。</summary>
public static bool IsEvidence(int row, Claim claim, Claim real)
{
switch (row)
{
case RowCount: return claim.Count != real.Count;
case RowPin: return claim.Pin != real.Pin;
case RowPrint: return real.Print != 0 && claim.Print != real.Print;
case RowSerial: return real.Serial != 0 && claim.Serial != real.Serial;
}
return false;
}
/// <summary>
/// 价值:claim 为空表示没有人说过什么,一律按真实情况;exposed 里的位表示已指出的标签(取真实情况)。
/// 型号取「说法里的型号」,型号标签被拆穿之后取真实芯片。价值 = 型号定价 ×(0.25 + 0.75 × 健康度)×(1 + 各标签折扣之和)。
/// </summary>
public static long Value(Unit u, Claim? claim, int exposed)
{
var real = Claim.Truth(u);
Claim believed = claim == null ? real : claim.Value;
double sum = 0;
foreach (int r in new[] { RowFan, RowFingers, RowBracket, RowSeal })
{
int v = claim == null || (exposed & r) != 0 ? real.Get(r) : believed.Get(r);
sum += Pct(r, v);
}
string model = claim == null || (exposed & RowSpec) != 0 ? real.Model : believed.Model;
double value = CardValue.Of(model, u.Health) * Math.Max(0, 1 + sum / 100.0);
return Math.Max(1, (long)Math.Round(value));
}
/// <summary>指出一个标签:返回 0 没问题(说法属实)、1 隐瞒了对价格不利的情况、2 拆穿了假货(型号一并改回真实型号)。</summary>
public static int Point(Unit u, Claim claim, int row, ref int exposed)
{
var real = Claim.Truth(u);
bool wasExposed = (exposed & row) != 0;
exposed |= row;
if (row == RowSpec) return claim.Model != real.Model ? 2 : 0;
if (IsEvidence(row, claim, real))
{
exposed |= RowSpec | RowCount | RowPin | RowPrint | RowSerial;
return 2;
}
if (wasExposed) return 0;
return claim.Get(row) != real.Get(row) ? 1 : 0;
}
/// <summary>说法里隐瞒了什么:说法比真实情况价值高的标签,型号不符,以及假货的证据。</summary>
public static int Lies(Unit u, Claim claim, int exposed)
{
var real = Claim.Truth(u);
int lies = 0;
foreach (int r in Rows)
{
if ((exposed & r) != 0) continue;
if (r == RowSpec) { if (claim.Model != real.Model) lies |= r; }
else if (IsAuth(r)) { if (claim.Get(r) != real.Get(r)) lies |= r; }
else if (claim.Get(r) != real.Get(r) && Pct(r, claim.Get(r)) > Pct(r, real.Get(r))) lies |= r;
}
return lies;
}
/// <summary>卖家怎么说:老实人如实说;中间商每项有七成概率说成没问题;骗子全部说成没问题。假卡的型号一律说成标称型号。</summary>
public static Claim DealerClaim(Unit u, DealerKind kind, Rng rng)
{
var c = Claim.Truth(u);
if (kind == DealerKind.Honest) return c;
c.Model = u.Model;
double hide = kind == DealerKind.Scammer ? 1.0 : Tuning.UsedMiddlemanHide;
c.CountSet = 0; c.PinSet = 0;
foreach (int r in new[] { RowFan, RowFingers, RowBracket, RowPrint, RowSerial, RowSeal })
if (rng.Chance(hide)) c = c.With(r, 0);
return c;
}
// ---------- 序列号 ----------
/// <summary>各牌子的序列号前缀与合法工厂码。序列号格式:前缀 + 档次码(1 到 4)+ "-" + 工厂码(两位)+ "-" + 四位流水。</summary>
public static string BrandCode(string brand) => brand == Hardware.Probidia ? "PB" : "MD";
public static readonly int[] ProbidiaFactories = { 11, 12, 21, 34, 47 }, PmdFactories = { 13, 22, 35, 41, 58 };
public static int[] Factories(string brand) => brand == Hardware.Probidia ? ProbidiaFactories : PmdFactories;
private static uint Mix(uint seed, int salt)
{
uint x = seed * 2654435761u + (uint)salt * 40503u + 777u;
x ^= x >> 15; x *= 2246822519u; x ^= x >> 13; x *= 3266489917u; x ^= x >> 16;
return x;
}
/// <summary>
/// 卡上贴纸的序列号。合规:前缀(牌子)+ 标称型号的档次码 + 合法工厂码 + 四位流水。
/// 不合规(贴纸是后补的):工厂码不在合法范围,或流水只有三位。档次码不符:贴的是原芯片的序列号,档次码是真实芯片的档次。
/// </summary>
public static string SerialText(Unit u)
{
var label = Hardware.Specs[u.Model];
var chip = Hardware.Specs[u.FakeOf ?? u.Model];
uint seed = u.Look.Seed == 0 ? 12345u : u.Look.Seed;
int kind = u.Appearance.Serial;
int[] ok = Factories(label.Brand);
int tier = (kind == 2 ? chip.Tier : label.Tier) + 1;
int factory = ok[(int)(Mix(seed, 1) % (uint)ok.Length)];
int number = 1000 + (int)(Mix(seed, 2) % 9000);
if (kind == 1)
{
if (Mix(seed, 3) % 2 == 0) { int[] bad = { 0, 9, 66, 77, 88, 99 }; factory = bad[(int)(Mix(seed, 4) % (uint)bad.Length)]; }
else number = 100 + (int)(Mix(seed, 5) % 900);
}
return $"{BrandCode(label.Brand)}{tier}-{factory:00}-{number}";
}
}
}
+91 -35
View File
@@ -13,54 +13,113 @@ namespace Decentralized.Domain
public int Fan;
/// <summary>金手指:0 光亮,1 发暗,2 氧化发绿(老化)。</summary>
public int Fingers;
/// <summary>挡板:0 完好,1 划痕(老化),2 接口数量不对(假卡)。</summary>
/// <summary>挡板:0 完好,1 划痕(老化)。</summary>
public int Bracket;
/// <summary>背面丝印:0 与标称型号一致,1 印的是真实芯片的型号(假卡),2 重新印过、字歪(假卡)。</summary>
public int Print;
/// <summary>背面贴纸与封条:0 印刷序列号、封条完好,1 手写序列号(假卡),2 封条缺失(旧卡或翻新卡)。</summary>
/// <summary>封条:0 完好,2 缺失(开过封的旧卡或翻新卡)。</summary>
public int Sticker;
/// <summary>序列号:0 合规(格式对、工厂码在合法范围、档次码与型号一致),1 不合规(工厂码不在合法范围或位数不对,贴纸是后补的),
/// 2 档次码与标称型号不符(贴的是原芯片的序列号)。后两种只出现在假卡上。</summary>
public int Serial;
/// <summary>风扇数对不上标称型号(实物是真实芯片的风扇数):0 对得上,1 对不上。只出现在假卡上。</summary>
public int CountBad;
/// <summary>供电口对不上标称型号(实物是真实芯片的供电口):0 对得上,1 对不上。只出现在假卡上。</summary>
public int PinBad;
public const int Variants = 3;
/// <summary>每张卡自己的随机种子:老化类维度(风扇、金手指、挡板划痕)由它和当前健康度决定,每晚损耗后重新算,
/// 所以越旧的卡看着越旧。为 0 时表示外观是写死的(测试用)。</summary>
public uint Seed;
private static double U(uint seed, int salt)
{
uint x = seed * 2654435761u + (uint)salt * 40503u + 12345u;
x ^= x >> 15; x *= 2246822519u; x ^= x >> 13; x *= 3266489917u; x ^= x >> 16;
return (x >> 8) * (1.0 / (1 << 24));
}
/// <summary>按当前健康度重新算出的外观:磨损度 = 1 − 健康度 / 100,再加上这张卡自己的偏差(±0.15)。
/// 风扇磨损 ≥ 0.25 积灰、≥ 0.55 缺角;金手指 ≥ 0.30 发暗、≥ 0.60 氧化;挡板磨损 ≥ 0.45 出划痕(接口数不对是假卡的固定特征,不变)。</summary>
public CardLook Appearance(double health)
{
if (Seed == 0) return this;
double worn = 1 - Math.Max(0, Math.Min(100, health)) / 100.0;
var l = this;
double fan = worn + (U(Seed, 1) - 0.5) * 0.3, fin = worn + (U(Seed, 2) - 0.5) * 0.3, brk = worn + (U(Seed, 3) - 0.5) * 0.3;
l.Fan = fan >= 0.55 ? 2 : fan >= 0.25 ? 1 : 0;
l.Fingers = fin >= 0.60 ? 2 : fin >= 0.30 ? 1 : 0;
l.Bracket = brk >= 0.45 ? 1 : 0;
return l;
}
private static readonly double[] FanHash = { 1.00, 0.97, 0.92 }, FingerHash = { 1.00, 0.98, 0.94 };
/// <summary>算力倍数:风扇积灰 ×0.97、缺角 ×0.92(降频);金手指发暗 ×0.98、氧化 ×0.94(接触不良掉速)。</summary>
public double HashFactor => FanHash[Fan] * FingerHash[Fingers];
/// <summary>六位数字,存在物品标签上:风扇、金手指、挡板、丝印、封条、序列号。</summary>
public string Encode() => $"{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}{CountBad}{PinBad}";
/// <summary>
/// 旧版存档的外观只有 5 或 6 位(没有风扇数、供电口两位):旧规则里假卡的风扇数和供电口只要档次不同就一定对不上。
/// 这里按旧规则补上,旧存档里的假卡仍然露出同样的破绽。新格式(8 位)不动。
/// </summary>
public static CardLook Legacy(string encoded, CardLook look, string label, string chip)
{
if (encoded == null || encoded.Length >= 8 || string.IsNullOrEmpty(chip) || chip == label) return look;
var l = Hardware.Specs[label]; var c = Hardware.Specs[chip];
look.CountBad = Hardware.FanCount[l.Tier] != Hardware.FanCount[c.Tier] ? 1 : 0;
look.PinBad = Hardware.PinClass[l.Tier] != Hardware.PinClass[c.Tier] ? 1 : 0;
return look;
}
public static CardLook Decode(string s)
{
if (s == null || s.Length < 5 || s.Length > 8) return default;
int D(int i) => Math.Max(0, Math.Min(Variants - 1, s[i] - '0'));
int B(int i) => s.Length > i && s[i] == '1' ? 1 : 0;
return new CardLook { Fan = D(0), Fingers = D(1), Bracket = D(2), Print = D(3), Sticker = D(4), Serial = s.Length > 5 ? D(5) : 0, CountBad = B(6), PinBad = B(7) };
}
// 外观维度对应卡的真实状态:损耗、暴毙概率随之变化,价值走标签(见 CardLabels)
private static readonly double[] FanWear = { 1.00, 1.10, 1.30 }, FanDeath = { 1.00, 1.00, 1.20 };
private static readonly double[] FingerDeath = { 1.00, 1.10, 1.30 };
/// <summary>每晚损耗倍数:风扇积灰散热差 ×1.1,缺角 ×1.3。</summary>
public double WearFactor => FanWear[Fan];
/// <summary>暴毙概率倍数:风扇缺角 ×1.2;金手指发暗 ×1.1、氧化 ×1.3(接触不良)。</summary>
public double DeathFactor => FanDeath[Fan] * FingerDeath[Fingers];
/// <summary>只在假卡上出现的取值:玩家一旦看到,就能肯定是假的。</summary>
public bool HasFakeSign => Bracket == 2 || Print != 0 || Sticker == 1;
public bool HasFakeSign => Print != 0 || Serial != 0 || CountBad != 0 || PinBad != 0;
/// <summary>
/// 生成外观。真卡:老化维度按健康度出现,封条缺失在二手卡上很常见,不会出现破绽类取值。
/// 假卡:在三类破绽(接口数、丝印、手写序列号)里随机挑 1–3 处,再叠加老化与封条缺失。
/// </summary>
public static CardLook Generate(Rng rng, double health, bool fake)
public static CardLook Generate(Rng rng, double health, bool fake, int labelTier = 3, int chipTier = 0)
{
double worn = 1 - Math.Max(0, Math.Min(100, health)) / 100.0;
var look = new CardLook
var look = new CardLook { Seed = rng.NextUInt() | 1, Sticker = rng.Chance(fake ? 0.35 : 0.15 + 0.30 * (1 - Math.Max(0, Math.Min(100, health)) / 100.0)) ? 2 : 0 };
if (fake)
{
Fan = Pick(rng, 0.10 + 0.55 * worn, 0.35 * worn),
Fingers = Pick(rng, 0.08 + 0.55 * worn, 0.40 * worn),
Bracket = rng.Chance(0.10 + 0.50 * worn) ? 1 : 0,
Sticker = rng.Chance(fake ? 0.35 : 0.15 + 0.30 * worn) ? 2 : 0,
};
if (!fake) return look;
int signs = rng.Chance(0.55) ? 1 : rng.Chance(0.6) ? 2 : 3;
var order = new[] { 0, 1, 2 };
for (int i = order.Length - 1; i > 0; i--)
{
int j = rng.Range(0, i);
(order[i], order[j]) = (order[j], order[i]);
}
for (int k = 0; k < signs; k++)
{
switch (order[k])
// 四项真伪依据(风扇数、供电口、背面丝印、序列号)各自独立地露出破绽,至少一项;平均每张假卡约 1.5 项
bool countCan = Hardware.FanCount[chipTier] != Hardware.FanCount[labelTier], pinCan = Hardware.PinClass[chipTier] != Hardware.PinClass[labelTier];
double p = Tuning.FakeSignChance;
for (int tries = 0; tries < 30 && !look.HasFakeSign; tries++)
{
case 0: look.Bracket = 2; break;
case 1: look.Print = rng.Chance(0.6) ? 1 : 2; break;
default: look.Sticker = 1; break;
look.CountBad = countCan && rng.Chance(p) ? 1 : 0;
look.PinBad = pinCan && rng.Chance(p) ? 1 : 0;
look.Print = rng.Chance(p) ? (rng.Chance(0.6) ? 1 : 2) : 0;
look.Serial = rng.Chance(p) ? (rng.Chance(0.55) ? 2 : 1) : 0;
}
if (!look.HasFakeSign) look.Serial = 1;
}
return look;
return look.Appearance(health);
}
/// <summary>p1 的概率取 1,再以 p2 的概率升到 2。</summary>
private static int Pick(Rng rng, double p1, double p2) => rng.Chance(p1) ? (rng.Chance(p2 / Math.Max(p1, 1e-9)) ? 2 : 1) : 0;
}
/// <summary>
@@ -73,14 +132,11 @@ namespace Decentralized.Domain
public static long Of(string model, double health)
{
var spec = Hardware.Specs[model];
double f = 0.25 + 0.75 * Math.Max(0, Math.Min(100, health)) / 100.0;
double f = Tuning.CardValueFloor + (1 - Tuning.CardValueFloor) * Math.Max(0, Math.Min(100, health)) / 100.0;
return Math.Max(1, (long)Math.Round(spec.Price * f));
}
/// <summary>真实价值(芯片型号 + 健康度)。</summary>
public static long True(Unit u) => Of(u.FakeOf ?? u.Model, u.Health);
/// <summary>标称价:贩子报的价,按标称型号算;二手卡也折旧,但只按贩子"自称"的健康度。</summary>
public static long Quoted(Unit u, double claimedHealth) => Of(u.Model, claimedHealth);
/// <summary>真实价值:全部标签都按真实情况算。</summary>
public static long True(Unit u) => CardLabels.Value(u, null, 0);
}
}
+71
View File
@@ -0,0 +1,71 @@
namespace Decentralized.Domain
{
/// <summary>
/// 链上地址:"0x" + 6 位随机十六进制 + 2 位校验位。手输地址时输错一位,校验位几乎总能发现(约 1/256 恰好也合法,币就转进了没人用的地址)。
/// </summary>
public static class ChainAddress
{
public const int BodyLength = 6, CheckLength = 2, Length = 2 + BodyLength + CheckLength;
public static string Make(uint r)
{
string body = (r & 0xFFFFFF).ToString("x6");
return "0x" + body + Check(body);
}
/// <summary>校验位:地址主体的 FNV-1a 散列取高 8 位(低位对单字符改动是一一对应的,永远不会碰巧合法)。</summary>
public static string Check(string body)
{
uint h = 2166136261;
foreach (char c in body) { h ^= c; h *= 16777619; }
return (h >> 24).ToString("x2");
}
public static string Normalize(string s) => (s ?? "").Trim().ToLowerInvariant();
/// <summary>格式对(0x 开头、十六进制、长度对)。</summary>
public static bool WellFormed(string s)
{
s = Normalize(s);
if (s.Length != Length || !s.StartsWith("0x")) return false;
for (int i = 2; i < s.Length; i++)
if (!(s[i] >= '0' && s[i] <= '9' || s[i] >= 'a' && s[i] <= 'f')) return false;
return true;
}
/// <summary>格式对且校验位对得上。</summary>
public static bool Valid(string s)
{
s = Normalize(s);
return WellFormed(s) && Check(s.Substring(2, BodyLength)) == s.Substring(2 + BodyLength);
}
public static bool Same(string a, string b) => Normalize(a) == Normalize(b);
/// <summary>缩写(钱包列表里的样子):0x + 主体前 3 位 + … + 校验位,例如 0x4b9…17。</summary>
public static string Short(string a)
{
a = Normalize(a);
return WellFormed(a) ? a.Substring(0, 5) + "…" + a.Substring(2 + BodyLength) : a;
}
/// <summary>
/// 相似地址(地址投毒、剪贴板木马用):主体前 3 位和校验位都和原地址一样,缩写看起来完全相同,中间几位不同;本身是合法地址,
/// 币转过去就进了骗子手里。找不到校验位相同的就只保留前 3 位。
/// </summary>
public static string Lookalike(string a, Rng rng)
{
a = Normalize(a);
if (!Valid(a)) return Make(rng.NextUInt());
string prefix = a.Substring(2, 3), check = a.Substring(2 + BodyLength), body0 = a.Substring(2, BodyLength);
for (int i = 0; i < 20000; i++)
{
string body = prefix + (rng.NextUInt() & 0xFFF).ToString("x3");
if (body != body0 && Check(body) == check) return "0x" + body + check;
}
string fallback;
do fallback = prefix + (rng.NextUInt() & 0xFFF).ToString("x3"); while (fallback == body0);
return "0x" + fallback + Check(fallback);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
using System;
namespace Decentralized.Domain
{
/// <summary>
/// 顾客用币付款:成交的信用点在店里扣回,按当前币价换成币进热钱包。
/// 收赃物的款项不再记销赃,改为币带污点:货值 × 赃物热度%。
/// </summary>
public static class CoinPayment
{
/// <summary>各派系用什么币付款(其余派系不用币)。</summary>
public static string AssetFor(string faction)
{
switch (faction)
{
case "FACTION_UPPER_LEVEL": return Assets.CRC;
case "FACTION_BLACK_MARKET": return Assets.CRT;
case "FACTION_REVOLUTION": return Assets.CRA;
case "FACTION_LOWER_LEVEL": return Assets.PSC;
default: return null;
}
}
/// <summary>一位顾客用币付款的概率。</summary>
public static double Chance(string asset)
{
switch (asset)
{
case Assets.CRC: return Tuning.CoinPayCrc;
case Assets.CRT: return Tuning.CoinPayCrt;
case Assets.CRA: return Tuning.CoinPayCra;
default: return Tuning.CoinPayPsc;
}
}
/// <summary>收款:返回收到的币数。heatPct 是物品的赃物热度(0–100):按货值 × 热度那部分的币是脏的;多付的溢价是顾客给的额外收入,干净。</summary>
public static double Receive(Wallet w, string asset, double credits, double price, double heatPct)
{
if (credits <= 0 || price <= 0) return 0;
double qty = credits * (1 + Tuning.CoinPayPremium) / price; // 溢价只加在币数上,污点与成本仍按货值算
w.AddDirty(asset, qty, credits * Math.Max(0, Math.Min(100, heatPct)) / 100.0 / price);
w.Basis += credits;
return qty;
}
}
}
+5 -5
View File
@@ -9,12 +9,12 @@ namespace Decentralized.Domain
{
public const string Rig = "RIG";
public static readonly string[] Basic = { "RZX50", "RZX60", "RZ470", "RZ580" };
public static readonly string[] Advanced = { "RZX70", "RZX80Ti", "RZ6800" };
public static readonly string[] Flagship = { "RZX90", "RZ7900" };
public static readonly string[] Basic = { "RTX60", "RX70" };
public static readonly string[] Advanced = { "RTX70", "RX80", "RTX80", "RX800" };
public static readonly string[] Flagship = { "RTX90", "RX900" };
public const int AdvancedFromDay = 10, FlagshipFromDay = 30, SpecialFlagshipFromDay = 20;
public const double RigRestockChance = 0.30, AdvancedChance = 0.60, AdvancedSecondChance = 0.30, FlagshipChance = 0.25;
public const int AdvancedFromDay = Tuning.DealerAdvancedFromDay, FlagshipFromDay = Tuning.DealerFlagshipFromDay, SpecialFlagshipFromDay = Tuning.DealerSpecialFlagshipFromDay;
public const double RigRestockChance = Tuning.DealerRigRestock, AdvancedChance = Tuning.DealerAdvanced, AdvancedSecondChance = Tuning.DealerAdvancedSecond, FlagshipChance = Tuning.DealerFlagship;
public static List<string> Stock(int day, bool ownsRig, bool specialStock, Rng rng)
{
+19 -43
View File
@@ -1,73 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
public enum AmlOutcome { Clear, Light, Heavy }
/// <summary>一次合规出金的结果。罪证由接入层记进游戏的罪证账本(销赃)。</summary>
/// <summary>一次出金到银行的结果。</summary>
public sealed class CashoutResult
{
public double Requested;
public double Received;
public double Confiscated;
public double Crime;
public double Tax;
public double HitChance;
public AmlOutcome Outcome;
}
/// <summary>
/// 合规交易所出金:CRC 兑回信用点,过 AML。审查按单笔金额、7 天累计金额、污点比例、是否来自混币器或被标记地址判定。
/// 轻度命中(污点 &lt; 30%)没收污点部分、不记罪证;重度命中没收污点部分,并把污点额记为销赃罪证。
/// 合规交易所 Poinbase:只收干净的币(钱包往充值地址最多转“价值 − 污点”,见 <see cref="Wallet.TransferClean"/>),账户没有污点。
/// 出金到银行把 CRC 兑回信用点,扣手续费,只对超出成本的部分计税,不做审查。
/// </summary>
public sealed class Exchange
{
/// <summary>最近的出金记录:(天, 金额)。</summary>
public List<KeyValuePair<int, double>> History = new List<KeyValuePair<int, double>>();
/// <summary>Poinbase 上架的币:只有这些能充值、能在现货交易。黑市的 CRT、革命军的 CRA 合规交易所不收。</summary>
public static readonly string[] Listed = { Assets.PSC, Assets.CRC, Assets.SPARK };
public static double HitChance(double amount, double taint, double recentTotal, double rent, bool fromMixerOrFlagged)
/// <summary>存档迁移(数据版本 5):旧存档里 Poinbase 账户和永续仓位带的污点额(信用点),清零后返回总数,由调用方挪到 1 号钱包一起摊。</summary>
public static double TakeLegacyTaint(Wallet account, System.Collections.Generic.IEnumerable<PerpPosition> positions)
{
if (amount <= 0) return 0;
double ratio = taint / amount;
double big = Tuning.AmlBigRents * rent, cum = Tuning.AmlCumRents * rent;
double p = 0;
if (ratio >= Tuning.AmlTaintRatio) p += ratio * 1.5;
p += Math.Max(0, (amount - big) / big) * 0.2;
p += Math.Max(0, (recentTotal + amount - cum) / cum) * 0.2;
if (fromMixerOrFlagged) p += Tuning.AmlMixerBonus;
return Math.Min(Tuning.AmlMaxChance, p);
double t = account.LegacyTaint;
account.LegacyTaint = 0;
foreach (var p in positions) { t += p.LegacyTaint; p.LegacyTaint = 0; }
return t;
}
/// <summary>
/// 从地址里取出 amount 个 CRC 兑成信用点(按 CRC 当前价格,平时为 1;脱锚期间按市价),扣手续费。污点与成本按比例随取出部分计算;
/// 从账户里取出 amount 个 CRC 兑成信用点(按 CRC 当前价格,平时为 1;脱锚期间按市价),扣手续费。成本按取出部分的价值比例带走,
/// 只对超出成本的部分计税。
/// </summary>
public CashoutResult Cashout(Wallet wallet, double amount, Func<string, double> price, int day, double rent, bool fromMixer, Rng rng)
public CashoutResult Cashout(Wallet account, double amount, Func<string, double> price)
{
amount = Math.Min(amount, wallet.Get(Assets.CRC));
amount = Math.Min(amount, account.Get(Assets.CRC));
var r = new CashoutResult { Requested = amount };
if (amount <= 0) return r;
double total = wallet.Value(price);
double share = total > 0 ? Math.Min(1, amount * price(Assets.CRC) / total) : 0;
double taint = wallet.Taint * share, basis = wallet.Basis * share;
History.RemoveAll(h => h.Key <= day - Tuning.AmlWindowDays);
double recent = History.Sum(h => h.Value);
double total = account.Value(price);
double value = amount * price(Assets.CRC);
r.HitChance = HitChance(value, taint, recent, rent, fromMixer || wallet.Flagged);
wallet.Add(Assets.CRC, -amount);
wallet.Taint -= taint;
wallet.Basis -= basis;
History.Add(new KeyValuePair<int, double>(day, value));
double share = total > 0 ? Math.Min(1, value / total) : 0;
double basis = account.Basis * share;
account.Add(Assets.CRC, -amount);
account.Basis -= basis;
double got = value * (1 - Tuning.ExchangeFee);
if (rng.Chance(r.HitChance))
{
r.Outcome = taint / value >= Tuning.AmlHeavyRatio ? AmlOutcome.Heavy : AmlOutcome.Light;
r.Confiscated = Math.Min(got, taint);
got -= r.Confiscated;
if (r.Outcome == AmlOutcome.Heavy) r.Crime = taint;
}
r.Tax = Math.Max(0, got - basis) * Tuning.TaxRate;
r.Received = got;
return r;
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>浏览器里的一条链上转账:区块、天、转出方、转入方、币种、数额、手续费。</summary>
public sealed class LedgerEntry
{
public long Block;
public int Day;
public string From, To, Asset;
public double Amount, Fee;
}
/// <summary>
/// 全局账本:只记真实发生的转账(玩家钱包的每一笔进出,含合约调用和地址投毒的 0 币转入),不生成 NPC 流水;最新的在前,超过 <see cref="Tuning.LedgerMax"/> 条丢最旧的。
/// </summary>
public sealed class Ledger
{
public List<LedgerEntry> Entries = new List<LedgerEntry>();
public void Add(LedgerEntry e)
{
if (e == null || e.Amount < 0) return;
Entries.Insert(0, e);
if (Entries.Count > Tuning.LedgerMax) Entries.RemoveRange(Tuning.LedgerMax, Entries.Count - Tuning.LedgerMax);
}
/// <summary>某个地址相关(转出或转入)的记录,最新的在前,最多 limit 条。address 为空时返回全部最近的。</summary>
public List<LedgerEntry> Of(string address, int limit)
{
if (string.IsNullOrWhiteSpace(address)) return Entries.Take(limit).ToList();
return Entries.Where(e => ChainAddress.Same(e.From, address) || ChainAddress.Same(e.To, address)).Take(limit).ToList();
}
}
}
+135 -10
View File
@@ -4,6 +4,14 @@ using System.Linq;
namespace Decentralized.Domain
{
/// <summary>一次互换的报价:得到数量、经过几个池、相对中间价的损失(手续费加滑点)。</summary>
public sealed class SwapQuote
{
public string From, To;
public double In, Out, Impact;
public int Hops;
}
/// <summary>
/// 影响行情的游戏事件种类。接入层把游戏的新闻事件与派系行动映射到这里(例如全站停电、电力短缺 → Blackout)。
/// </summary>
@@ -16,35 +24,142 @@ 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;
/// <summary>玩家自己的现货大单把价格推高(正)或压低(负)的对数幅度,每晚随价格回归一起衰减。操纵检出和预测盘用它估"没有玩家时的价格"。</summary>
public double PlayerPush;
/// <summary>今天的插针(对数幅度,0 = 没有)和当晚回去的比例(0 = 真跌真涨)。</summary>
public double Wick, WickBack;
public const double ArbRate = 0.5; // 非主交易对每晚被拉回参考价的比例
public const double SideDepthShare = 0.3; // 非主交易对的深度占主交易对的比例
/// <summary>夜间结算开始时:插针按比例回去(结算、预测盘读的是回去后的价格)。</summary>
public void UndoWick(int day)
{
if (Wick == 0) return;
Main.SetPrice(Price * Math.Exp(-Wick * WickBack), Tuning.PoolDepth(day));
Wick = 0; WickBack = 0;
}
/// <summary>
/// 插针:开门后价格瞬间被打到一边(大户扫强平、砸盘吸筹),大部分当晚回去,一部分是真的。
/// 标记价用开门前的价格和收盘价平均,强平只吃一半。返回幅度(0 = 今天没有)。
/// </summary>
public double TryWick(Rng rng, int day)
{
Wick = 0; WickBack = 0;
if (!rng.Chance(Tuning.WickChance)) return 0;
double size = rng.Range(Tuning.WickMin, Tuning.WickMax) * (rng.Chance(0.5) ? 1 : -1);
Wick = size;
WickBack = rng.Chance(Tuning.WickReal) ? 0 : rng.Range(Tuning.WickRevertMin, Tuning.WickRevertMax);
Main.SetPrice(Price * Math.Exp(size), Tuning.PoolDepth(day));
return size;
}
public double NeutralPrice => Price * Math.Exp(-PlayerPush);
/// <summary>被查概率:推价幅度(绝对值)超过 ManipFree 后随幅度升高。</summary>
public static double ManipChance(double push)
{
push = Math.Abs(push);
return push < Tuning.ManipFree ? 0 : Math.Min(Tuning.ManipMax, Tuning.ManipBase + Tuning.ManipSlope * (push - Tuning.ManipFree));
}
public const double ArbRate = Tuning.SideArbRate; // 非主交易对每晚被拉回参考价的比例
public const double SideDepthShare = Tuning.SideDepthShare; // 非主交易对的深度占主交易对的比例
public static Market CreateDefault(int day)
{
var m = new Market();
double depth = Tuning.PoolDepth(day), p = Tuning.PscStartPrice;
m.Pools.Add(Pool.Create(Venue.Exchange, Assets.PSC, Assets.CRC, p, depth, Tuning.ExchangeFee));
m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRT, p, depth * SideDepthShare, Tuning.DexFee));
m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRA, p, depth * SideDepthShare, Tuning.DexFee));
m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee));
m.Pools.Add(Pool.CreateBook(Venue.Exchange, Assets.PSC, Assets.CRC, p, depth, Tuning.ExchangeFee));
m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, p, depth * SideDepthShare, Tuning.DexFee));
m.Pools.Add(Pool.CreateStable(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.StableFee, Tuning.StableAmp));
m.Pools.Add(Pool.CreateStable(Venue.Dex, Assets.CRA, Assets.CRC, 1, depth * SideDepthShare, Tuning.StableFee, Tuning.StableAmp));
m.Pools.Add(Pool.Create(Venue.Dex, Assets.SPARK, Assets.CRC, Tuning.SparkStart, depth * Tuning.SparkDepthShare, Tuning.DexFee));
m.AddPoinSpark(Tuning.SparkStart, day);
return m;
}
public Pool Main => Pools[0];
/// <summary>旧存档:稳定币对是恒定乘积池,按当前价换成 StableSwap 池;其他 DEX 池按新深度重设(不等到夜里)。</summary>
public void UpgradeDexPools(int day)
{
for (int i = 0; i < Pools.Count; i++)
{
var p = Pools[i];
if (p.Venue != Venue.Dex || p.Stable) continue;
if (p.Base == Assets.CRT || p.Base == Assets.CRA)
Pools[i] = Pool.CreateStable(Venue.Dex, p.Base, p.Quote, p.Price, Tuning.PoolDepth(day) * SideDepthShare, Tuning.StableFee, Tuning.StableAmp);
else p.SetPrice(p.Price, Tuning.PoolDepth(day) * DepthShare(p));
}
}
/// <summary>Poinbase 的 SPARK/CRC 订单簿(旧存档没有时按当前价补上)。</summary>
public void AddPoinSpark(double price, int day)
{
if (Find(Venue.Exchange, Assets.SPARK, Assets.CRC) == null)
Pools.Add(Pool.CreateBook(Venue.Exchange, Assets.SPARK, Assets.CRC, price, Tuning.PoolDepth(day) * Tuning.PoinSparkDepthShare, Tuning.ExchangeFee));
}
/// <summary>非主交易对的深度占主池的比例。</summary>
private static double DepthShare(Pool p) =>
p.Base != Assets.SPARK ? SideDepthShare : p.Venue == Venue.Exchange ? Tuning.PoinSparkDepthShare : Tuning.SparkDepthShare;
/// <summary>DEX 上 X/CRC 的池(所有币都以 CRC 计价)。</summary>
public Pool DexPool(string asset) => Find(Venue.Dex, asset, Assets.CRC);
/// <summary>
/// 任意两币互换的报价:CRC 在一端走一个池,否则 X → CRC → Y 两跳(两次手续费、滑点叠加)。
/// 不改池子;没有路由(同币、没有池)返回 null。
/// </summary>
public SwapQuote Quote(string from, string to, double amount)
{
if (from == to || amount <= 0) return null;
var a = from == Assets.CRC ? null : DexPool(from);
var b = to == Assets.CRC ? null : DexPool(to);
if ((from != Assets.CRC && a == null) || (to != Assets.CRC && b == null)) return null;
double crc = a == null ? amount : a.QuoteOut(amount);
double mid = a == null ? amount : amount * a.Price;
double outAmt = b == null ? crc : b.BaseOut(crc);
double ideal = b == null ? mid : mid / b.Price;
return new SwapQuote { From = from, To = to, In = amount, Out = outAmt, Hops = (a != null ? 1 : 0) + (b != null ? 1 : 0), Impact = ideal > 0 ? Math.Max(0, 1 - outAmt / ideal) : 0 };
}
/// <summary>成交:按 <see cref="Quote"/> 的路由改动池子,返回得到的数量(没有路由返回 0)。</summary>
public double Swap(string from, string to, double amount)
{
if (Quote(from, to, amount) == null) return 0;
double crc = from == Assets.CRC ? amount : DexPool(from).SellBase(amount);
return to == Assets.CRC ? crc : DexPool(to).BuyBase(crc);
}
public Pool Find(Venue venue, string baseAsset, string quote) =>
Pools.FirstOrDefault(x => x.Venue == venue && x.Base == baseAsset && x.Quote == quote);
/// <summary>PSC 的参考价(信用点)。</summary>
public double Price => Main.Price;
/// <summary>历次减半抽到的传导指数之和:价格锚的供给因子目标为 2^此值。</summary>
public double HalvingLog;
/// <summary>减半当晚:抽本次的传导指数。</summary>
public void OnHalving(Rng rng) => HalvingLog += Tuning.HalvingPassMin + rng.NextDouble() * (Tuning.HalvingPassMax - Tuning.HalvingPassMin);
/// <summary>供给因子的目标:已发生的减半各自抬高锚;减半前 7 天内再逐日加预期。</summary>
public double SupplyTarget(int day)
{
double t = Math.Pow(2, HalvingLog);
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) => Supply += (SupplyTarget(day) - 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;
@@ -66,11 +181,11 @@ namespace Decentralized.Domain
OpenPrice = p;
}
/// <summary>NPC 大户:每周约 2 笔、每笔 2,000–8,000 信用点,随机买或卖。返回成交额(正为买入),没有交易时为 0。</summary>
/// <summary>NPC 大户:每周约 2 笔、每笔 <see cref="Tuning.WhaleMin"/>–<see cref="Tuning.WhaleMax"/> 信用点,随机买或卖。返回成交额(正为买入),没有交易时为 0。</summary>
public double Whale(Rng rng)
{
if (!rng.Chance(2.0 / 7)) return 0;
double size = rng.Range(2000.0, 8000.0);
double size = rng.Range(Tuning.WhaleMin, Tuning.WhaleMax);
if (rng.Chance(0.5)) { Main.BuyBase(size); return size; }
Main.SellBase(size / Price);
return -size;
@@ -85,6 +200,8 @@ namespace Decentralized.Domain
/// </summary>
public void Night(ICollection<MarketEvent> active, Rng rng, int day, Func<string, double> stablePrice)
{
if (Wick != 0) { Main.SetPrice(Price * Math.Exp(-Wick * WickBack), Tuning.PoolDepth(day)); Wick = 0; WickBack = 0; }
PlayerPush *= 1 - Tuning.Revert;
Sentiment *= Math.Exp(Tuning.SentimentKappa * -Math.Log(Sentiment) + rng.Gauss() * Tuning.SentimentVol);
double anchor = Anchor(active);
double lp = Math.Log(Price);
@@ -97,7 +214,15 @@ namespace Decentralized.Domain
double baseCredits = pool.Base == Assets.PSC ? p : stablePrice(pool.Base);
double quoteCredits = pool.Quote == Assets.PSC ? p : stablePrice(pool.Quote);
if (quoteCredits <= 0) continue;
pool.Revert(baseCredits / quoteCredits, ArbRate, depth * SideDepthShare / quoteCredits);
double share = DepthShare(pool);
// 稳定币对:参考价偏离挂钩超过界线时流动性变差(放大系数降低),回到挂钩附近恢复
if (pool.Stable)
{
double dev = Math.Abs(baseCredits / quoteCredits - 1);
pool.Amp = dev > Tuning.StableBrokenBand ? Tuning.StableAmpBroken : dev > Tuning.StableDepegBand ? Tuning.StableAmpDepeg : Tuning.StableAmp;
if (dev > Tuning.StableBrokenBand) share *= Tuning.StableBrokenDepth;
}
pool.Revert(baseCredits / quoteCredits, ArbRate, depth * share / quoteCredits);
}
}
}
+72 -19
View File
@@ -13,6 +13,10 @@ namespace Decentralized.Domain
public double Kj;
public int Price;
/// <summary>暴毙概率的品牌系数。</summary>
/// <summary>档次:0 低端、1 中端、2 高端、3 旗舰(显卡)。实物特征(供电口、风扇数)只看档次。</summary>
public int Tier;
/// <summary>废片:只会作为假卡背后的真实芯片出现,不上架、不单独出现。低档次的卡也能被冒充靠它。</summary>
public bool Scrap;
public double DeathFactor;
/// <summary>每天损耗的系数。</summary>
public double WearFactor;
@@ -25,23 +29,38 @@ namespace Decentralized.Domain
public static class Hardware
{
/// <summary>各档次的实物特征:供电口针数(旗舰是两个 8 针)与风扇数量。</summary>
public static readonly string[] PowerPins = { "6", "8", "8", "8+8" };
public static readonly int[] FanCount = { 1, 2, 3, 3 };
/// <summary>各档次的供电口:0 = 6 针,1 = 8 针,2 = 双 8 针。</summary>
public static readonly int[] PinClass = { 0, 1, 1, 2 };
public const string Probidia = "Probidia", Pmd = "PMD", RatDigg = "RatDigg";
public static readonly IReadOnlyDictionary<string, HardwareSpec> Specs = new[]
{
Gpu("RZX50", Probidia, 1.0, 4, 220), Gpu("RZX60", Probidia, 1.5, 5, 330), Gpu("RZX70", Probidia, 5.0, 12, 800),
Gpu("RZX80Ti", Probidia, 8.0, 18, 1300), Gpu("RZX90", Probidia, 20.0, 30, 5000),
Gpu("RZ470", Pmd, 0.8, 2.5, 170), Gpu("RZ580", Pmd, 1.2, 3, 250), Gpu("RZ6800", Pmd, 4.0, 8, 680),
Gpu("RZ7900", Pmd, 15.0, 20, 4000),
// 四个档次(低端、中端、高端、旗舰),两个牌子通用:型号只是显示名,性能和价格由「牌子 + 档次」决定
Gpu("RTX60", Probidia, 0, 1.8, 4, 220), Gpu("RTX70", Probidia, 1, 5.0, 12, 800),
Gpu("RTX80", Probidia, 2, 8.0, 18, 1300), Gpu("RTX90", Probidia, 3, 20.0, 30, 5000),
Gpu("RX70", Pmd, 0, 1.4, 2.5, 170), Gpu("RX80", Pmd, 1, 4.0, 8, 680),
Gpu("RX800", Pmd, 2, 8.0, 14, 1500), Gpu("RX900", Pmd, 3, 15.0, 20, 4000),
Scrap("RX10", Pmd, 0, 0.3, 2, 60),
Asic("M1", 50, 60, 11000), Asic("M2", 120, 110, 27000), Asic("M3", 300, 220, 72000),
}.ToDictionary(s => s.Model);
private static HardwareSpec Gpu(string model, string brand, double h, double kj, int price) => new HardwareSpec
private static HardwareSpec Gpu(string model, string brand, int tier, double h, double kj, int price) => new HardwareSpec
{
Model = model, Brand = brand, Hash = h, Kj = kj, Price = price,
Model = model, Brand = brand, Tier = tier, Hash = h, Kj = kj, Price = price,
DeathFactor = brand == Probidia ? 0.7 : 1.0, WearFactor = brand == Probidia ? 1.0 : 1.2,
};
private static HardwareSpec Scrap(string model, string brand, int tier, double h, double kj, int price)
{
var s = Gpu(model, brand, tier, h, kj, price);
s.Scrap = true;
return s;
}
private static HardwareSpec Asic(string model, double h, double kj, int price) => new HardwareSpec
{
Model = model, Brand = RatDigg, Hash = h, Kj = kj, Price = price, DeathFactor = 0.5, WearFactor = 0.8, Asic = true,
@@ -49,9 +68,8 @@ namespace Decentralized.Domain
}
/// <summary>
/// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 <see cref="Tuning.ModuleRatio"/> 后生效,
/// 并有上限,避免模块把挖矿收益放大到破坏经济:算力最多 +20%,省电最多 25%,损耗最多降 30%。
/// 性能加成同时按比例提高损耗与暴毙率。负数(例如节能模块的负性能、超频模块的负效率)照常生效,耗电增加不设上限。
/// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 <see cref="Tuning.ModuleRatio"/> 后生效,不设上限(强力模块是玩家自己拿到的)。
/// 性能加成同时按比例提高损耗与暴毙率。三个乘数各有防出错的下限:算力不低于 <see cref="Tuning.ModuleHashFloor"/>,耗电不低于 <see cref="Tuning.ModuleKjFloor"/>,损耗不低于 <see cref="Tuning.ModuleWearFloor"/>。
/// </summary>
public struct ModuleBonus
{
@@ -65,14 +83,17 @@ namespace Decentralized.Domain
public static ModuleBonus From(int performance, int efficiency, int quality)
{
double r = Tuning.ModuleRatio * 100.0;
double hash = Clamp(performance / r, -0.5, Tuning.ModuleHashCap);
double save = Clamp(efficiency / r, -1.0, Tuning.ModuleKjCap);
double wearCut = Clamp(quality / r, -1.0, Tuning.ModuleWearCap);
double hash = performance / r, save = efficiency / r, wearCut = quality / r;
double heat = 1 + Math.Max(0, hash);
return new ModuleBonus { Hash = 1 + hash, Kj = 1 - save, Wear = (1 - wearCut) * heat, Death = heat };
return new ModuleBonus
{
Hash = Math.Max(Tuning.ModuleHashFloor, 1 + hash),
Kj = Math.Max(Tuning.ModuleKjFloor, 1 - save),
Wear = Math.Max(Tuning.ModuleWearFloor, (1 - wearCut) * heat),
Death = heat,
};
}
private static double Clamp(double v, double lo, double hi) => Math.Max(lo, Math.Min(hi, v));
}
/// <summary>一张显卡或一台矿机的实例状态(存进物品标签)。</summary>
@@ -84,15 +105,24 @@ namespace Decentralized.Domain
public double Health = 100;
public bool Dead;
public bool Refurbished;
/// <summary>外观;新卡(正规渠道)没有记录,按全新画。</summary>
public CardLook Look;
/// <summary>按当前健康度算出的外观。</summary>
public CardLook Appearance => Look.Appearance(Health);
public HardwareSpec Label => Hardware.Specs[Model];
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? Model];
/// <summary>实物上的风扇数:对得上标称型号时是标称档次的,对不上(假卡的破绽)时是真实芯片的。</summary>
public int ObservedCount => Hardware.FanCount[(Look.CountBad != 0 ? Chip : Label).Tier];
/// <summary>实物上的供电口:0 = 6 针,1 = 8 针,2 = 双 8 针;规则同风扇数。</summary>
public int ObservedPin => Hardware.PinClass[(Look.PinBad != 0 ? Chip : Label).Tier];
/// <summary>当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。</summary>
public double Hash(ModuleBonus b)
{
if (Dead) return 0;
return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash;
return Chip.Hash * (Health >= 50 ? 1.0 : 0.6 + 0.4 * Health / 50) * b.Hash * Appearance.HashFactor;
}
public double Kj(ModuleBonus b) => Dead ? 0 : Chip.Kj * b.Kj;
@@ -101,14 +131,14 @@ namespace Decentralized.Domain
public double DeathChance(ModuleBonus b)
{
double p = Tuning.DeathBase + Tuning.DeathWorn * Math.Pow(1 - Health / 100, 2);
return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death;
return p * Label.DeathFactor * (FakeOf != null ? Tuning.FakeDeathFactor : 1) * b.Death * Appearance.DeathFactor;
}
/// <summary>一夜的损耗与暴毙判定;返回是否在今晚暴毙。</summary>
public bool Night(ModuleBonus b, Rng rng)
{
if (Dead) return false;
Health = Math.Max(0, Health - Label.WearFactor * b.Wear);
Health = Math.Max(0, Health - Label.WearFactor * b.Wear * Appearance.WearFactor);
if (rng.Chance(DeathChance(b)))
{
Dead = true;
@@ -131,6 +161,10 @@ namespace Decentralized.Domain
public sealed class Network
{
public double NpcHash = Tuning.NpcHash0;
/// <summary>NPC 矿工每 H 的耗电(kJ):矿机时代到来后减半,他们能挖得更久,全网算力均衡点升高。</summary>
public double NpcKj = Tuning.NpcKjPerH;
public int EraDay = -1, EraWarnDay = -1;
public bool EraWarned, EraDone;
public double Reward = Tuning.BlockReward;
/// <summary>停电、动荡等事件对 NPC 算力的可用比例。</summary>
public double Availability = 1.0;
@@ -140,7 +174,26 @@ namespace Decentralized.Domain
{
if (playerHash <= 0) return 0;
double total = NpcHash * Availability + playerHash;
return Reward * playerHash / total * (1 - Tuning.PoolFee);
return Reward * playerHash / total * (1 - Tuning.PoolFee) * Scales.Mining;
}
public enum EraStep { None, Warn, Start }
/// <summary>
/// "矿机时代":第一次调用时在 EraMin–EraMax 天之间随机定一天(至少留出预兆的天数),提前 4–7 天预兆,到点 NPC 耗电效率翻倍,永久。
/// 返回今晚发生的一步(预兆 / 开始)。
/// </summary>
public EraStep EraNight(int day, Rng rng)
{
if (EraDone) return EraStep.None;
if (EraDay < 0)
{
EraDay = Math.Max(day + Tuning.EraWarnMax + 1, rng.Range(Tuning.EraMin, Tuning.EraMax));
EraWarnDay = EraDay - rng.Range(Tuning.EraWarnMin, Tuning.EraWarnMax);
}
if (day >= EraDay) { NpcKj *= Tuning.EraKjMult; EraDone = true; return EraStep.Start; }
if (!EraWarned && day >= EraWarnDay) { EraWarned = true; return EraStep.Warn; }
return EraStep.None;
}
/// <summary>
@@ -151,7 +204,7 @@ namespace Decentralized.Domain
if (day > 0 && day % Tuning.HalvingDays == 0) Reward /= 2;
double total = NpcHash * Availability + playerHash;
double perH = Reward * price / total;
double ratio = perH / (Tuning.NpcKjPerH * Tuning.ElecPrice);
double ratio = perH / (NpcKj * Tuning.ElecPrice);
double adj = Math.Max(-Tuning.NpcAdj, Math.Min(Tuning.NpcAdj, Tuning.NpcAdj * (ratio / Tuning.NpcTargetRatio - 1)));
NpcHash *= 1 + adj;
}
+148
View File
@@ -0,0 +1,148 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>混币器里一笔在途的存入。</summary>
public sealed class MixDeposit
{
public string Asset;
public double Amount;
/// <summary>存入的币里脏的枚数,和一并带走的成本(信用点)。</summary>
public double Dirty, Basis;
/// <summary>旧存档(数据版本 5 之前)的污点额(信用点),读档迁移时换成 <see cref="Dirty"/>。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
public int Rounds;
public int ReadyDay;
/// <summary>存入的钱包地址:到期提回这里。</summary>
public string Wallet;
}
/// <summary>一笔到期提回的出款记录(泄露时按它记罪证):Cleaned 是这笔洗掉的污点,按提回时的价格折成信用点。</summary>
public sealed class MixReturn
{
public int Day;
public string Wallet, Asset;
public double Amount, Cleaned;
}
/// <summary>泄露的结果:最近几笔出款,一共洗掉了多少污点(信用点),折成的诈骗罪证。</summary>
public sealed class MixLeak
{
public int Count;
public double Cleaned, Fraud;
}
/// <summary>一笔混币结束时的结果,供夜间日志使用。</summary>
public sealed class MixResult
{
public bool Rugged;
public string Asset;
public double Amount, TaintBefore, TaintAfter;
/// <summary>被标记(制裁期间提出、从钓鱼混币器提出):不洗,整笔都是脏的。</summary>
public bool Flagged;
}
/// <summary>
/// 混币器:把币存进去,每轮扣手续费、降低污点,轮数 = 等待天数,到期提回同一个地址。
/// 匿名集 = NPC 背景量 + 玩家存入量,玩家占比越大清洗效果越差。镇压事件开始时可能被制裁:
/// 制裁期间提出的币视为被标记(出金审查按"来自混币器或被标记地址"加重,且地址被标记)。每晚有极小概率跑路。
/// </summary>
public sealed class Mixer
{
public List<MixDeposit> Pending = new List<MixDeposit>();
public int SanctionedUntil = -1;
/// <summary>最近的出款记录(Tumbler 的密钥落到治安部时用来还原)。</summary>
public List<MixReturn> Returns = new List<MixReturn>();
/// <summary>
/// 泄露:最近 <see cref="Tuning.TumblerLeakDays"/> 天的出款被还原,按这些出款洗掉的污点记诈骗罪证(<see cref="Wallet.FraudFor"/>)。记录清空。
/// </summary>
public MixLeak Leak(int day)
{
var recent = Returns.Where(x => day - x.Day <= Tuning.TumblerLeakDays).ToList();
double cleaned = recent.Sum(x => x.Cleaned);
Returns.Clear();
return new MixLeak { Count = recent.Count, Cleaned = cleaned, Fraud = Wallet.FraudFor(cleaned) };
}
private void Remember(MixReturn ret)
{
Returns.Add(ret);
Returns.RemoveAll(x => ret.Day - x.Day > Tuning.TumblerLeakDays);
while (Returns.Count > Tuning.TumblerReturnsMax) Returns.RemoveAt(0);
}
/// <summary>黑市卷款跑路:在途的存入全部丢失。</summary>
public List<MixResult> RugAll()
{
var list = Pending.Select(p => new MixResult { Rugged = true, Asset = p.Asset, Amount = p.Amount }).ToList();
Pending.Clear();
return list;
}
public bool Sanctioned(int day) => day <= SanctionedUntil;
/// <summary>一轮的清洗率:基准 × 匿名集系数。</summary>
/// <summary>每轮手续费(剧情可能改动)。</summary>
public static double Fee => Math.Min(0.5, Tuning.MixerFee * StoryMods.Current.MixerFee);
public static double RoundRate(double depositValue, int day = -1) => Tuning.MixerCleanRate * (day >= 0 ? StoryMods.Current.MixClean(day) : 1) * Tuning.MixerNpcVolume * StoryMods.Current.MixerVolume / (Tuning.MixerNpcVolume * StoryMods.Current.MixerVolume + Math.Max(0, depositValue));
/// <summary>存入:按这种币的脏比例带走脏枚数,按价值比例带走成本。轮数超出范围或余额不足返回 null。</summary>
public MixDeposit Deposit(Wallet w, string asset, double amount, int rounds, int day, Func<string, double> price)
{
if (rounds < 1 || rounds > Tuning.MixerMaxRounds || amount <= 0 || w.Avail(asset) + 1e-9 < amount) return null;
double value = amount * price(asset);
if (value < Tuning.MixerMin) return null;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, value / total) : 0;
double dirty = amount * w.DirtyRatio(asset), basis = w.Basis * share;
w.Add(asset, -amount);
w.Basis -= basis;
var d = new MixDeposit { Asset = asset, Amount = amount, Dirty = dirty, Basis = basis, Rounds = rounds, ReadyDay = day + rounds, Wallet = w.Address };
Pending.Add(d);
return d;
}
/// <summary>镇压事件开始:有一定概率被制裁,持续若干天。</summary>
public void OnEvent(MarketEvent ev, int day, Rng rng)
{
if (ev == MarketEvent.Crackdown && rng.Chance(Math.Min(1, Tuning.MixerSanctionChance * StoryMods.Current.Sanction(day)))) SanctionedUntil = day + Tuning.MixerSanctionDays;
}
/// <summary>每晚:到期的存入提回钱包(污点按轮数降低),在途的各有小概率跑路。</summary>
public List<MixResult> Night(Wallet w, int day, Func<string, double> price, Rng rng) => Night(_ => w, day, price, rng);
/// <summary>每晚(多钱包):到期的存入按存入时的地址提回原钱包。</summary>
public List<MixResult> Night(Func<string, Wallet> wallet, int day, Func<string, double> price, Rng rng)
{
var results = new List<MixResult>();
foreach (var d in Pending.ToList())
{
if (rng.Chance(Scales.R(Tuning.MixerRugChance * StoryMods.Current.MixerRug)))
{
Pending.Remove(d);
results.Add(new MixResult { Rugged = true, Asset = d.Asset, Amount = d.Amount });
continue;
}
if (day < d.ReadyDay) continue;
Pending.Remove(d);
// 制裁期间提出的、从治安部的钓鱼混币器提出的,都被标记:不洗,这笔币按全额污点带回
bool flagged = Sanctioned(day) || StoryMods.Current.MixerHoneypot;
double rate = flagged ? 0 : RoundRate(d.Amount * price(d.Asset), day);
double amount = d.Amount * Math.Pow(1 - Fee, d.Rounds);
double dirty = flagged ? amount : Math.Min(amount, d.Dirty * Math.Pow(1 - rate, d.Rounds));
var w = wallet(d.Wallet);
w.AddDirty(d.Asset, amount, dirty);
w.Basis += d.Basis;
double p = price(d.Asset);
Remember(new MixReturn { Day = day, Wallet = d.Wallet, Asset = d.Asset, Amount = amount, Cleaned = Math.Max(0, d.Dirty - dirty) * p });
results.Add(new MixResult { Asset = d.Asset, Amount = amount, TaintBefore = d.Dirty * p, TaintAfter = dirty * p, Flagged = flagged });
}
return results;
}
}
}
+131
View File
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>换币客户的来意:卖币给玩家(玩家付现金收币),或者向玩家买币(玩家转币收现金)。</summary>
public enum OtcSide { Sell, Buy }
/// <summary>
/// 一位换币客户的报价。Sell:客户手里的币(Qty、按现价的市值 Value、污点比例 TaintRatio)和他要的现金 Cash;Sting 是便衣(收下就记罪证,币不会到)。
/// Buy:客户要买的币和数量,Value 是按现价的市值,Cash 是他出的现金(市价加一个随机的溢价,可能低于市价)。
/// 玩家可以只成交一部分(<see cref="Otc.Part"/>),现金、市值按数量比例折算。
/// </summary>
public sealed class OtcOffer
{
public OtcSide Side;
public string Asset;
public double Qty, Value, TaintRatio;
public int Cash;
public bool Sting;
}
/// <summary>换币台的累计统计(存档):成交额(信用点)、笔数、客户地址编号;混币器提议是否已经发过(第三阶段用)。</summary>
public sealed class OtcDesk
{
public double Volume;
public int Deals, Seq;
public bool MixerOffered;
public void Record(double cash) { Volume += Math.Max(0, cash); Deals++; }
}
/// <summary>
/// 换币客户(场外换币台):小偷灯切到"换币客户"后招来,一半来卖币、一半来买币。
/// 卖币沿用原来的收黑币:帮派火并、镇压期间多是 CRT,革命军袭击上层、下层动荡期间多是 PSC,这类币全额带污点;
/// 其他时候在 PSC、CRT、CRA 里随机,污点比例随机,现金价按污点比例打折;Ponero 兑换处开放后偶尔有人出手 Ponero(不带污点)。
/// 买币:什么币都可能要,出价是市价加一个随机溢价,玩家自己决定成不成交;卖出的币不追查污点。两边都可以只成交一部分。
/// </summary>
public static class Otc
{
/// <summary>客户卖币时可能出手的币(没有相关事件时随机)。</summary>
public static readonly string[] SellAssets = { Assets.PSC, Assets.CRT, Assets.CRA };
/// <summary>客户买币时可能要的币。</summary>
public static readonly string[] BuyAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.SPARK };
public static OtcSide PickSide(Rng rng) => rng.Chance(Tuning.OtcSellShare) ? OtcSide.Sell : OtcSide.Buy;
/// <summary>和当前事件有关的币(帮派火并、镇压 → CRT;袭击上层、下层动荡 → PSC),没有返回 null。</summary>
public static string EventAsset(ICollection<MarketEvent> active, Rng rng)
{
var c = new List<string>();
if (active.Contains(MarketEvent.GangWar) || active.Contains(MarketEvent.Crackdown)) c.Add(Assets.CRT);
if (active.Contains(MarketEvent.RevRaidUpper) || active.Contains(MarketEvent.LowerUnrest)) c.Add(Assets.PSC);
return c.Count == 0 ? null : c[rng.Range(0, c.Count - 1)];
}
/// <summary>
/// 卖币客户的报价。有相关事件时按 OtcEventChance 出手事件里来的币(全额污点);Ponero 兑换处开放时按 OtcPoneroChance 出手 Ponero;
/// 否则在 PSC、CRT、CRA 里随机,污点比例在 OtcTaintMin–OtcTaintMax 之间。市值 = 房租 × 区间;现金价 = 市值 × 折扣,折扣按污点比例从 OtcCutClean 线性降到 OtcCutDirty。
/// crackdown:镇压期间便衣更多。
/// </summary>
public static OtcOffer MakeSell(ICollection<MarketEvent> active, bool poneroOpen, Func<string, double> price, double rent, Rng rng, double stingMult = 1)
{
if (rent <= 0) return null;
string asset = null; double ratio;
string ev = EventAsset(active, rng);
if (ev != null && rng.Chance(Tuning.OtcEventChance)) { asset = ev; ratio = 1; }
else if (poneroOpen && rng.Chance(Tuning.OtcPoneroChance)) { asset = Assets.Ponero; ratio = 0; }
else { asset = SellAssets[rng.Range(0, SellAssets.Length - 1)]; ratio = rng.Range(Tuning.OtcTaintMin, Tuning.OtcTaintMax); }
double p = price(asset);
if (p <= 0) return null;
double value = rent * rng.Range(Tuning.OtcValueMinRents, Tuning.OtcValueMaxRents);
double cut = asset == Assets.Ponero ? rng.Range(Tuning.OtcPoneroCutMin, Tuning.OtcPoneroCutMax) : CutFor(ratio);
bool crackdown = active.Contains(MarketEvent.Crackdown);
return new OtcOffer
{
Side = OtcSide.Sell, Asset = asset, Qty = value / p, Value = value, TaintRatio = ratio, Cash = (int)Math.Round(value * cut),
Sting = rng.Chance(Math.Min(1, (Tuning.OtcSting + (crackdown ? Tuning.OtcStingCrackdown : 0)) * stingMult)),
};
}
/// <summary>现金价占市值的比例:污点比例 0 时 OtcCutClean,1 时 OtcCutDirty,之间线性。</summary>
public static double CutFor(double taintRatio) => Tuning.OtcCutClean + (Tuning.OtcCutDirty - Tuning.OtcCutClean) * Math.Max(0, Math.Min(1, taintRatio));
/// <summary>买币客户的来意:随机一种币,市值 = 房租 × OtcBuyMinRents–OtcBuyMaxRents,出价 = 市值 × (1 + OtcBidMin–OtcBidMax 之间的随机溢价)。</summary>
public static OtcOffer MakeBuy(Func<string, double> price, double rent, Rng rng)
{
if (rent <= 0) return null;
string asset = BuyAssets[rng.Range(0, BuyAssets.Length - 1)];
double p = price(asset);
if (p <= 0) return null;
double value = rent * rng.Range(Tuning.OtcBuyMinRents, Tuning.OtcBuyMaxRents);
return new OtcOffer { Side = OtcSide.Buy, Asset = asset, Qty = value / p, Value = value, Cash = (int)Math.Round(value * (1 + rng.Range(Tuning.OtcBidMin, Tuning.OtcBidMax))) };
}
/// <summary>只成交 qty(不超过报价的数量):现金、市值按数量比例折算,污点比例和便衣不变。qty 不合法返回 null。</summary>
public static OtcOffer Part(OtcOffer o, double qty)
{
if (o == null || qty <= 0 || o.Qty <= 0) return null;
double f = Math.Min(1, qty / o.Qty);
return new OtcOffer { Side = o.Side, Asset = o.Asset, Qty = o.Qty * f, Value = o.Value * f, TaintRatio = o.TaintRatio, Cash = (int)Math.Round(o.Cash * f), Sting = o.Sting };
}
/// <summary>收下卖币客户的币:币进钱包,其中 数量 × 污点比例 枚是脏的;成本是付出的现金。</summary>
public static void Receive(Wallet w, OtcOffer o)
{
w.AddDirty(o.Asset, o.Qty, o.Qty * o.TaintRatio);
w.Basis += o.Cash;
}
/// <summary>
/// 客户地址的来历(写进链上账本,玩家在浏览器里能查到):1–OtcHistoryMax 笔转入,每一笔按污点比例的概率来自"脏"的来源(混币器、黑市平台、骗局地址),否则来自 Poinbase 或 DEX。
/// 返回 (来源地址, 数量占比) 列表;dirty 和 clean 由接入层给出地址。
/// </summary>
public static List<KeyValuePair<string, double>> History(OtcOffer o, IList<string> dirty, IList<string> clean, Rng rng)
{
var list = new List<KeyValuePair<string, double>>();
int n = rng.Range(1, Tuning.OtcHistoryMax);
double left = 1;
for (int i = 0; i < n; i++)
{
bool isDirty = dirty.Count > 0 && rng.Chance(o.TaintRatio);
var src = isDirty ? dirty[rng.Range(0, dirty.Count - 1)] : clean[rng.Range(0, clean.Count - 1)];
double share = i == n - 1 ? left : left * rng.Range(0.3, 0.7);
left -= share;
list.Add(new KeyValuePair<string, double>(src, share));
}
return list;
}
}
}
+226
View File
@@ -0,0 +1,226 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>自营混币器里一种币:玩家的本金(数量、其中脏的枚数)和还没取走的手续费收入(取走时整笔是脏的)。</summary>
public sealed class MixPool
{
public double Capital, CapDirty, Earned;
/// <summary>旧存档(数据版本 5 之前)本金的污点额(信用点),读档迁移时换成 <see cref="CapDirty"/>。</summary>
[JsonProperty("CapTaint")] public double LegacyCapTaint;
}
/// <summary>NPC 客户的一笔存入:扣掉手续费后的数量、存入时的污点额(信用点)、轮数、到期日。</summary>
public sealed class OwnMixDeposit
{
public string Asset;
public double Amount, Taint;
public int Rounds, ReadyDay;
}
/// <summary>一晚发生的事。Kind:deposit(今晚存入合计)/ payout(今晚出款合计,Ratio 是出款剩下的污点比例)/ sting(便衣存入)/ seized(被查封)。</summary>
public sealed class OwnMixEvent
{
public string Kind;
public double Value, Ratio, Crime;
public int Count;
}
/// <summary>
/// 自营混币器:玩家注资开张,NPC 客户把带污点的币存进来,几天后出款,玩家收手续费。和普通混币器一样,每一轮按匿名集洗掉一部分污点:
/// 匿名集主要是玩家的本金和没取走的收入(客户在途的币按一个比例折算),注资越多洗得越干净,客户越愿意来。
/// 手续费是玩家的收入,留在池子里,取走时全额带污点;本金取回时带着它自己的污点(注资用脏币,取回的也脏)。
/// 客流 = 基础需求(天) × 黑市声望 × 和 Tumbler 比的吸引力(费率加上出款剩下的污点,近期出款的滑动平均就是口碑)。
/// 风声随客流累积(池子越大越难追查,累积得越慢)、每晚衰减,按风声有概率被查封:池子和在途的存入全部没收,记销赃罪证,停业若干天。营业的晚上可能有便衣存入,记销赃罪证。
/// 玩家可以随时卷款跑路:池子连同客户的币全部进钱包、全额带污点,记诈骗罪证,混币器永久关闭。
/// </summary>
public sealed class OwnMixer
{
public static readonly string[] AssetsList = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA };
public bool Open;
public bool Closed;
public int SeizedUntil = -1;
public double Fee = Tuning.OwnMixFeeDefault;
/// <summary>口碑:近期出款剩下的污点比例(滑动平均),越低越好;开张时按当时的匿名集估。</summary>
public double Rating = 1;
public double Heat;
public Dictionary<string, MixPool> Pools = new Dictionary<string, MixPool>();
public List<OwnMixDeposit> Pending = new List<OwnMixDeposit>();
/// <summary>累计手续费收入(信用点,按入账时的价格),统计用。</summary>
public double FeesTotal;
public bool Seized(int day) => day <= SeizedUntil;
public bool Running(int day) => Open && !Closed && !Seized(day);
public MixPool Pool(string asset)
{
if (!Pools.TryGetValue(asset, out var p)) Pools[asset] = p = new MixPool();
return p;
}
/// <summary>一种币在池子里的总数:本金、收入、客户在途的币。</summary>
public double Total(string asset) => Pool(asset).Capital + Pool(asset).Earned + Pending.Where(x => x.Asset == asset).Sum(x => x.Amount);
/// <summary>匿名集(信用点):玩家的本金和收入,加上客户在途的币按 OwnMixPendingWeight 折算(主要靠注资撑起来)。</summary>
public double Anonymity(Func<string, double> price) => AssetsList.Sum(a => (Free(a) + Tuning.OwnMixPendingWeight * Pending.Where(x => x.Asset == a).Sum(x => x.Amount)) * price(a));
/// <summary>每一轮洗掉的污点比例:基准清洗率 × 匿名集 / (匿名集 + OwnMixAnon0)。</summary>
public static double RoundRate(double anonymity) => Tuning.MixerCleanRate * anonymity / (anonymity + Tuning.OwnMixAnon0);
/// <summary>按平均轮数出款时剩下的污点比例。</summary>
public static double Remain(double rate) => Math.Pow(1 - rate, Tuning.OwnMixAvgRounds);
/// <summary>玩家能取走的:本金加收入(客户在途的币不能动)。</summary>
public double Free(string asset) => Pool(asset).Capital + Pool(asset).Earned;
/// <summary>开张(或停业后重新开张):口碑按现在的匿名集估。</summary>
public void Start(Func<string, double> price)
{
Open = true;
Rating = Remain(RoundRate(Anonymity(price)));
}
/// <summary>注资:币从钱包进本金,按这种币的脏比例带上脏枚数。</summary>
public bool Inject(Wallet w, string asset, double amount, Func<string, double> price)
{
if (amount <= 0 || w.Avail(asset) + 1e-9 < amount || !AssetsList.Contains(asset)) return false;
double total = w.Value(price), share = total > 0 ? Math.Min(1, amount * price(asset) / total) : 0;
double dirty = amount * w.DirtyRatio(asset), basis = w.Basis * share;
w.Add(asset, -amount); w.Basis -= basis;
var p = Pool(asset);
p.Capital += amount; p.CapDirty += dirty;
return true;
}
/// <summary>取回:不超过 <see cref="Free"/>。先取手续费收入(整笔是脏的),再取本金(按本金的脏比例)。</summary>
public bool Withdraw(Wallet w, string asset, double amount)
{
var p = Pool(asset);
if (amount <= 0 || amount > Free(asset) + 1e-9) return false;
double fromFees = Math.Min(amount, p.Earned), fromCap = Math.Min(p.Capital, amount - fromFees);
double capDirty = p.Capital > 1e-12 ? p.CapDirty * fromCap / p.Capital : 0;
p.Earned -= fromFees; p.Capital -= fromCap; p.CapDirty -= capDirty;
w.AddDirty(asset, fromFees + fromCap, fromFees + capDirty);
return true;
}
/// <summary>卷款跑路:本金、收入和客户在途的币全部进钱包,整笔是脏的,永久关闭。返回 (币种, 数量)。</summary>
public List<KeyValuePair<string, double>> Rug(Wallet w)
{
var got = new List<KeyValuePair<string, double>>();
foreach (var a in AssetsList)
{
double amt = Total(a);
if (amt <= 1e-9) continue;
w.AddDirty(a, amt, amt);
got.Add(new KeyValuePair<string, double>(a, amt));
}
Pools.Clear(); Pending.Clear();
Open = false; Closed = true;
return got;
}
/// <summary>今晚的基础需求(信用点):随天数增长。</summary>
public static double Demand(int day) => Tuning.OwnMixDemand0 + Tuning.OwnMixDemandPerDay * day;
/// <summary>
/// 和 Tumbler 的混币器比,分到的客流(0–1):客户的成本 = 费率 + 出款剩下的污点比例 × OwnMixTaintWeight,比对手便宜就多分。
/// rivalRemain 是对手出款剩下的污点比例;对手关停时客户的另一条路是不混(成本再加 OwnMixRivalGone)。
/// </summary>
public double Share(double rivalFee, double rivalRemain, bool rivalOpen)
{
double mine = Fee + Rating * Tuning.OwnMixTaintWeight;
double rival = rivalFee + rivalRemain * Tuning.OwnMixTaintWeight + (rivalOpen ? 0 : Tuning.OwnMixRivalGone);
return 1 / (1 + Math.Exp((mine - rival) / Tuning.OwnMixFeeScale));
}
/// <summary>
/// 夜间:到期的存入出款(按匿名集洗掉污点),口碑跟着出款走;营业中接新的存入;风声累积、按风声查封。
/// bmRep:黑市声望(0–100);rivalFee、rivalRemain、rivalOpen:Tumbler 混币器的费率、出款剩下的污点比例、是否在营业;enforce:剧情带来的执法倍数;crackdown:镇压期间。
/// </summary>
public List<OwnMixEvent> Night(int day, Func<string, double> price, int bmRep, double rivalFee, double rivalRemain, bool rivalOpen, double enforce, bool crackdown, Rng rng)
{
var evs = new List<OwnMixEvent>();
if (Closed || Seized(day)) return evs;
// 出款(客户的币回到客户手里,污点按匿名集洗掉一部分)
double rate = RoundRate(Anonymity(price));
double paid = 0; int paidN = 0;
foreach (var dep in Pending.Where(x => x.ReadyDay <= day).ToList())
{
Pending.Remove(dep);
paid += dep.Amount * price(dep.Asset);
paidN++;
}
if (paidN > 0)
{
double remain = Remain(rate);
Rating += Tuning.OwnMixRatingSpeed * (remain - Rating);
evs.Add(new OwnMixEvent { Kind = "payout", Value = paid, Ratio = remain, Count = paidN });
}
// 存入
double inValue = 0; int n = 0;
if (Open)
{
double repF = Tuning.OwnMixRepMin + (1 - Tuning.OwnMixRepMin) * Math.Max(0, Math.Min(100, bmRep)) / 100.0;
double demand = Demand(day) * repF * Share(rivalFee, rivalRemain, rivalOpen);
double left = demand * rng.Range(1 - Tuning.OwnMixNoise, 1 + Tuning.OwnMixNoise);
while (left > Tuning.MixerMin)
{
double v = Math.Min(left, Tuning.OwnMixTicket * rng.Range(0.5, 1.5));
left -= v;
string asset = Pick(rng);
double px = price(asset);
if (px <= 0) continue;
double amt = v / px;
Pool(asset).Earned += amt * Fee;
FeesTotal += v * Fee;
int rounds = rng.Range(1, Tuning.MixerMaxRounds);
Pending.Add(new OwnMixDeposit { Asset = asset, Amount = amt * (1 - Fee), Taint = v * rng.Range(Tuning.OwnMixInTaintMin, Tuning.OwnMixInTaintMax), Rounds = rounds, ReadyDay = day + rounds });
inValue += v; n++;
}
if (n > 0)
{
evs.Add(new OwnMixEvent { Kind = "deposit", Value = inValue, Count = n });
// 便衣:营业的晚上按概率有一笔存入来自治安部
if (rng.Chance(Tuning.OwnMixSting))
{
double v = inValue / n;
evs.Add(new OwnMixEvent { Kind = "sting", Value = v, Crime = Math.Min(Tuning.CrimeEventCap, v * Tuning.OwnMixStingCrime) });
}
}
}
// 风声与查封
// 池子越大,客户的币越好藏,同样的客流引起的风声越小
Heat = Heat * Tuning.OwnMixHeatKeep + inValue / Tuning.OwnMixHeatScale / (1 + Anonymity(price) / Tuning.OwnMixHeatAnon0);
if (rng.Chance(SeizeChance(enforce, crackdown)))
{
double lost = AssetsList.Sum(a => Total(a) * price(a));
evs.Add(new OwnMixEvent { Kind = "seized", Value = lost, Crime = Math.Min(Tuning.CrimeEventCap, lost * Tuning.OwnMixSeizeCrime) });
Pools.Clear(); Pending.Clear();
SeizedUntil = day + Tuning.OwnMixSeizeDays;
Heat = 0; Open = false;
}
return evs;
}
/// <summary>今晚被查封的概率:风声 × 系数 × 执法倍数(镇压期间再乘),封顶。</summary>
public double SeizeChance(double enforce, bool crackdown) => Math.Min(Tuning.OwnMixSeizeMax, Tuning.OwnMixSeizeRate * Heat * enforce * (crackdown ? Tuning.OwnMixSeizeCrackdown : 1));
/// <summary>风声的三档提示:0 低、1 中、2 高(按今晚的查封概率)。</summary>
public int HeatLevel(double enforce) { double s = SeizeChance(enforce, false); return s < Tuning.OwnMixHeatMid ? 0 : s < Tuning.OwnMixHeatHigh ? 1 : 2; }
private static string Pick(Rng rng)
{
double r = rng.NextDouble() * Tuning.OwnMixAssetWeights.Sum();
for (int i = 0; i < AssetsList.Length; i++) { r -= Tuning.OwnMixAssetWeights[i]; if (r <= 0) return AssetsList[i]; }
return AssetsList[AssetsList.Length - 1];
}
}
}
+254
View File
@@ -0,0 +1,254 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>永续合约的一个净仓位。Size 是标的数量,Margin 是保证金(CRC,信用点等值),Taint / Basis 是存入时带来的污点与成本。</summary>
public sealed class PerpPosition
{
public string Product;
public bool Long;
public double Size, Entry, Margin, Basis;
/// <summary>旧存档(数据版本 5 之前)保证金带的污点额:读档迁移时挪到 1 号钱包,之后为 0(Poinbase 没有污点)。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
public double Notional(double price) => Size * price;
public double Pnl(double price) => (Long ? 1 : -1) * Size * (price - Entry);
public double Equity(double price) => Margin + Pnl(price);
public double Leverage => Margin > 0 ? Size * Entry / Margin : 0;
/// <summary>强平价:权益跌到名义价值的维持比例时。</summary>
public double LiqPrice => Long
? Math.Max(0, (Size * Entry - Margin) / (Size * (1 - Tuning.PerpMaint)))
: (Margin + Size * Entry) / (Size * (1 + Tuning.PerpMaint));
}
public enum PerpResult { Ok, Banned, Debt, Leverage, Size, Funds, Opposite, NoPosition, Margin, Thin, Single }
/// <summary>夜间发生的事:强平、自动减仓、催收没收、核销。</summary>
public sealed class PerpEvent
{
public string Kind; // liq / adl / seize / writeoff / interest / wipe
public string Product;
public bool Long;
public double Amount; // 损失、减记、或欠款(信用点);wipe 是全网爆仓的名义价值(含玩家)
public int Count; // wipe:爆仓人数(含玩家)
public double Move; // wipe:标记价的涨跌幅
}
/// <summary>
/// Poinbase 永续合约(PSC-PERP、SPARK-PERP):保证金用 Poinbase 账户里的 CRC,一个品种一个净仓位。
/// 标记价是当日均价,强平只在夜间检查(白天插针不触发)。强平后的剩余权益进保险基金,权益为负先由基金垫付,
/// 基金不够的缺口记成玩家的欠款。NPC 仓位随行情爆仓:大部分正常爆仓,剩余保证金进基金;行情越快穿仓越多,缺口由基金垫付。
/// 基金见底时盈利的仓位被自动减仓(盈利打折)。
/// 欠款每天计息,每晚先用账户里的 CRC 还;逾期没还清由接入层没收物品抵债,剩下的核销并拉黑永续账户一段时间。
/// </summary>
public sealed class PerpBook
{
public List<PerpPosition> Positions = new List<PerpPosition>();
public double InsuranceFund = Tuning.PerpFund0;
public double Debt;
public int DebtSince = -1, BannedUntil = -1;
public Dictionary<string, List<double>> Marks = new Dictionary<string, List<double>>();
public Dictionary<string, double> Funding = new Dictionary<string, double>();
/// <summary>资金费系数(费率 = 系数 × 标记价偏离 7 日均价的比例)。默认取配置,模拟里用来校准。</summary>
public double FundCoef = Tuning.PerpFundCoef;
/// <summary>成交深度倍数(滑点 = 名义价值 / (倍数 × 永续深度)),模拟里用来校准。</summary>
public double DepthMult = Tuning.PerpDepthMult;
/// <summary>成交价:买入(开多、平空)往上滑,卖出往下滑,滑点随名义价值占永续深度的比例增大,封顶 50%。</summary>
public double Exec(double price, double notional, bool buy, int day)
{
double slip = Math.Min(0.5, notional / (DepthMult * Tuning.PerpDepth(day)));
return price * (1 + (buy ? slip : -slip));
}
public static readonly string[] Products = { "PSC", "SPARK" };
public static int MaxLeverage(string product) => product == "SPARK" ? Tuning.PerpSparkLev : Tuning.PerpPscLev;
public static double MaxNotional(string product) => product == "SPARK" ? Tuning.PerpSparkCap : Tuning.PerpPscCap;
public PerpPosition Of(string product) => Positions.Find(p => p.Product == product);
public bool Banned(int day) => day <= BannedUntil;
public double Rate(string product) => Funding.TryGetValue(product, out var r) ? r : 0;
private static double Share(Wallet acct, double amount, Func<string, double> price)
{
double total = acct.Value(price);
return total > 0 ? Math.Min(1, amount * price(Assets.CRC) / total) : 0;
}
/// <summary>开仓(同一时间只有一个仓位,已有仓位时只能 <see cref="AddMargin"/>):保证金 + 手续费从账户的 CRC 里扣,污点与成本按保证金的价值比例随保证金进仓位。</summary>
public PerpResult Open(Wallet acct, string product, bool isLong, double margin, int leverage, double price, int day, Func<string, double> pr)
{
if (Banned(day)) return PerpResult.Banned;
if (Debt > 1e-9) return PerpResult.Debt;
if (leverage < 1 || leverage > MaxLeverage(product)) return PerpResult.Leverage;
if (margin <= 0 || price <= 0) return PerpResult.Margin;
var pos = Of(product);
if (pos != null && pos.Long != isLong) return PerpResult.Opposite;
if (Positions.Count > 0) return PerpResult.Single; // 只支持逐仓、一个仓位:有仓位后只能追加保证金
double notional = margin * leverage, fee = notional * Tuning.PerpFee;
double spot = price;
price = Exec(price, notional, isLong, day);
if ((pos?.Notional(price) ?? 0) + notional > MaxNotional(product) + 1e-9) return PerpResult.Size;
if (acct.Avail(Assets.CRC) + 1e-9 < margin + fee) return PerpResult.Funds;
// 滑点太大:按现价算权益已经低于强平线(再留一点余地),开了就是被强平
double sizeNew = notional / price, sizeAll = (pos?.Size ?? 0) + sizeNew;
double entryAll = ((pos?.Size ?? 0) * (pos?.Entry ?? 0) + sizeNew * price) / sizeAll;
double eq0 = (pos?.Margin ?? 0) + margin + (isLong ? 1 : -1) * sizeAll * (spot - entryAll);
if (eq0 < (Tuning.PerpMaint + Tuning.PerpOpenBuffer) * sizeAll * spot) return PerpResult.Thin;
double share = Share(acct, margin, pr);
double basis = acct.Basis * share;
acct.Add(Assets.CRC, -(margin + fee)); acct.Basis -= basis;
InsuranceFund += fee * Tuning.PerpFeeToFund;
if (pos == null) Positions.Add(pos = new PerpPosition { Product = product, Long = isLong, Entry = price });
double size = notional / price;
pos.Entry = (pos.Size * pos.Entry + size * price) / (pos.Size + size);
pos.Size += size; pos.Margin += margin; pos.Basis += basis;
return PerpResult.Ok;
}
/// <summary>追加保证金(不收手续费),把强平价推远。</summary>
public PerpResult AddMargin(Wallet acct, string product, double amount, Func<string, double> pr)
{
var pos = Of(product);
if (pos == null) return PerpResult.NoPosition;
if (amount <= 0) return PerpResult.Margin;
if (acct.Avail(Assets.CRC) + 1e-9 < amount) return PerpResult.Funds;
double share = Share(acct, amount, pr);
double basis = acct.Basis * share;
acct.Add(Assets.CRC, -amount); acct.Basis -= basis;
pos.Margin += amount; pos.Basis += basis;
return PerpResult.Ok;
}
/// <summary>平仓(按白天的现价):权益减手续费回到账户,成本按剩下的比例退回。权益不够付时缺口按穿仓处理。返回回到账户的 CRC。</summary>
public double Close(Wallet acct, string product, double price, int day, out double deficit)
{
deficit = 0;
var pos = Of(product);
if (pos == null) return 0;
Positions.Remove(pos);
price = Exec(price, pos.Notional(price), !pos.Long, day);
double fee = pos.Notional(price) * Tuning.PerpFee;
double payout = pos.Equity(price) - fee;
InsuranceFund += Math.Min(fee, Math.Max(0, pos.Equity(price))) * Tuning.PerpFeeToFund;
if (payout < 0) { deficit = -payout; Cover(deficit, day); return 0; }
Return(acct, pos, payout);
return payout;
}
private void Return(Wallet acct, PerpPosition pos, double payout)
{
double keep = pos.Margin > 0 ? Math.Min(1, payout / pos.Margin) : 0;
acct.Add(Assets.CRC, payout); acct.Basis += pos.Basis * keep;
}
/// <summary>穿仓缺口:保险基金先垫,不够的记成欠款。</summary>
private double Cover(double deficit, int day)
{
double take = Math.Min(InsuranceFund, deficit);
InsuranceFund -= take;
double rest = deficit - take;
if (rest > 1e-9) { Debt += rest; if (DebtSince < 0) DebtSince = day; }
return rest;
}
/// <summary>
/// NPC 仓位在一晚涨跌幅 move 下对保险基金的净流入(可为负),以及爆仓的名义价值。oi 是亏损一侧的持仓。
/// 爆仓量 = oi × min(1, move / PerpNpcLiqScale);其中穿仓的比例随涨跌幅升高,正常爆仓交出剩余保证金,穿仓的缺口由基金垫付。
/// </summary>
public static double NpcFlow(double move, double oi, out double liquidated)
{
move = Math.Abs(move);
liquidated = oi * Math.Min(1, move / Tuning.PerpNpcLiqScale);
double bank = Math.Min(Tuning.PerpNpcBankMax, Tuning.PerpNpcBankBase + Tuning.PerpNpcBankSlope * Math.Max(0, move - Tuning.PerpNpcBankFree));
return liquidated * ((1 - bank) * Tuning.PerpNpcLeft - bank * move * Tuning.PerpNpcGap);
}
/// <summary>NPC 在某个品种上单边的持仓(信用点名义价值)。</summary>
public static double NpcOi(string product, int day) => Tuning.PerpNpcOi * Tuning.PoolDepth(day) * (product == "SPARK" ? Tuning.SparkDepthShare : 1);
/// <summary>
/// 夜间结算。marks:各品种今天的标记价(日均价)。顺序:资金费 → NPC 爆仓与穿仓进出基金 → 强平 → 基金见底时自动减仓盈利仓位 →
/// 欠款计息并用账户 CRC 偿还 → 逾期的触发没收。标记价涨跌达到 <see cref="Tuning.PerpNewsMove"/> 的品种另记一条 wipe(全网爆仓,含玩家)。
/// </summary>
public List<PerpEvent> Night(int day, IDictionary<string, double> marks, Wallet acct, Func<string, double> pr)
{
var evs = new List<PerpEvent>();
var wipes = new Dictionary<string, PerpEvent>();
bool adl = false;
foreach (var kv in marks)
{
string product = kv.Key; double mark = kv.Value;
if (!Marks.TryGetValue(product, out var hist)) Marks[product] = hist = new List<double>();
double prev = hist.Count > 0 ? hist[hist.Count - 1] : mark;
double rate = 0;
if (hist.Count >= 3) rate = Math.Max(-Tuning.PerpFundCap, Math.Min(Tuning.PerpFundCap, FundCoef * (mark / hist.Average() - 1)));
Funding[product] = rate;
hist.Add(mark); if (hist.Count > 7) hist.RemoveAt(0);
var pos = Of(product);
if (pos != null) pos.Margin -= (pos.Long ? 1 : -1) * rate * pos.Notional(mark);
double ret = prev > 0 ? mark / prev - 1 : 0;
InsuranceFund += NpcFlow(ret, NpcOi(product, day), out double liq);
if (Math.Abs(ret) >= Tuning.PerpNewsMove)
wipes[product] = new PerpEvent { Kind = "wipe", Product = product, Long = ret < 0, Amount = liq, Count = (int)Math.Round(liq / Tuning.PerpNpcAvgPos), Move = ret };
}
if (InsuranceFund < 0) { InsuranceFund = 0; adl = true; }
foreach (var pos in Positions.ToList())
{
if (!marks.TryGetValue(pos.Product, out var mark)) continue;
double eq = pos.Equity(mark), notional = pos.Notional(mark);
if (eq >= Tuning.PerpMaint * notional) continue;
Positions.Remove(pos);
double rem = eq - Tuning.PerpLiqFee * notional;
if (rem >= 0) InsuranceFund += rem; else Cover(-rem, day);
evs.Add(new PerpEvent { Kind = "liq", Product = pos.Product, Long = pos.Long, Amount = pos.Margin });
if (wipes.TryGetValue(pos.Product, out var wp) && wp.Long == pos.Long) { wp.Amount += notional; wp.Count++; }
}
evs.AddRange(wipes.Values);
if (adl)
foreach (var pos in Positions.ToList())
{
if (!marks.TryGetValue(pos.Product, out var mark) || pos.Pnl(mark) <= 0) continue;
double pnl = pos.Pnl(mark), cut = pnl * Tuning.PerpAdlHaircut;
Positions.Remove(pos);
Return(acct, pos, pos.Margin + pnl - cut);
evs.Add(new PerpEvent { Kind = "adl", Product = pos.Product, Long = pos.Long, Amount = cut });
}
if (Debt > 1e-9)
{
double interest = Debt * Tuning.PerpDebtInterest;
Debt += interest;
double repay = Math.Min(Debt, acct.Avail(Assets.CRC));
if (repay > 1e-9)
{
double share = Share(acct, repay, pr);
double basis = acct.Basis * share;
acct.Add(Assets.CRC, -repay); acct.Basis -= basis;
Debt -= repay;
evs.Add(new PerpEvent { Kind = "repaid", Amount = repay });
}
if (Debt < 1e-6) { Debt = 0; DebtSince = -1; }
else
{
evs.Add(new PerpEvent { Kind = "interest", Amount = Debt });
if (day - DebtSince >= Tuning.PerpDebtDays) evs.Add(new PerpEvent { Kind = "seize", Amount = Debt });
}
}
return evs;
}
/// <summary>没收物品抵债之后:抵掉的价值从欠款里扣,还剩的核销并拉黑永续账户。返回核销的数额。</summary>
public double ApplySeizure(double value, int day)
{
Debt -= value;
double writtenOff = Math.Max(0, Debt);
if (writtenOff > 0.5) BannedUntil = day + Tuning.PerpBanDays;
Debt = 0; DebtSince = -1;
return writtenOff;
}
}
}
+83
View File
@@ -0,0 +1,83 @@
using System;
namespace Decentralized.Domain
{
/// <summary>夜间发生的事。Kind:collapse(付不出利息,挤兑)/ reported(被举报);Amount 是被没收的池子。</summary>
public sealed class PonziEvent
{
public string Kind;
public double Amount;
}
/// <summary>
/// 高息理财(庞氏):玩家定一个日息,下层的人按日息和下层声望往池子里存 CRC。每晚先拿新存入的钱付利息,不够的从发行人钱包的 CRC 补,
/// 补不上就挤兑崩盘;池子越大、日息越高越容易被举报,被举报同样崩盘。崩盘时池子被没收;玩家也可以随时卷走池子(污点全额)。
/// 每个存档只能开一次。
/// </summary>
public sealed class Ponzi
{
public bool Open, Done;
public double Rate, Pool, OwnPaid, AdCost, Inflow;
/// <summary>发行人的钱包地址:宣传费、垫付的利息从它出,卷走的池子进它。</summary>
public string Wallet;
public int OpenDay = -1;
public static double Cap(int day) => Tuning.PonziCap0 + Tuning.PonziCapPerDay * day;
/// <summary>每晚新存入(均值):基数随天数,日息高于 4% 时按指数放大,下层声望越高越多。</summary>
public static double InflowMean(int day, double rate, int lowerRep) =>
(Tuning.PonziBase0 + Tuning.PonziBasePerDay * day) * Math.Pow(rate / 0.04, Tuning.PonziRateExp) * Math.Max(0.3, Math.Min(1.5, 0.5 + lowerRep / 100.0));
public static double ReportChance(double rate, double pool, int day) =>
Math.Min(0.9, Tuning.PonziReport0 + Tuning.PonziReportRate * rate + Tuning.PonziReportSize * Math.Min(1, pool / Cap(day)));
public bool CanOpen(int day) => !Open && !Done && day >= Tuning.PonziMinDay;
/// <summary>开张:宣传费从发行人钱包的 CRC 里扣。</summary>
public bool Start(Wallet acct, double rate, int day)
{
if (!CanOpen(day) || acct.Avail(Assets.CRC) + 1e-9 < Tuning.PonziAdCost) return false;
acct.Add(Assets.CRC, -Tuning.PonziAdCost);
Open = true; Rate = rate; Pool = 0; OwnPaid = 0; AdCost = Tuning.PonziAdCost; OpenDay = day; Wallet = acct.Address;
return true;
}
/// <summary>夜间结算。返回崩盘 / 被举报事件(没有事就返回 null)。</summary>
public PonziEvent Night(int day, int lowerRep, Wallet acct, Rng rng)
{
if (!Open) return null;
double mean = InflowMean(day, Rate, lowerRep);
double inflow = Math.Min(Cap(day) - Pool, mean * rng.Range(0.6, 1.4));
inflow = Math.Max(0, inflow); Inflow = inflow;
double interest = Pool * Rate;
double fromInflow = Math.Min(interest, inflow), rest = interest - fromInflow;
if (rest > acct.Avail(Assets.CRC) + 1e-9)
{
var ev = new PonziEvent { Kind = "collapse", Amount = Pool + inflow - fromInflow };
Open = false; Done = true; Pool = 0;
return ev;
}
if (rest > 0) { acct.Add(Assets.CRC, -rest); OwnPaid += rest; }
Pool += inflow - fromInflow;
Pool -= Pool * Tuning.PonziWithdraw;
if (rng.Chance(ReportChance(Rate, Pool, day)))
{
var ev = new PonziEvent { Kind = "reported", Amount = Pool };
Open = false; Done = true; Pool = 0;
return ev;
}
return null;
}
/// <summary>卷走:池子全部进账户,污点全额(骗局所得)。返回卷走的数额。</summary>
/// <summary>卷走:池子全部进发行人钱包,污点全额(骗局所得)。</summary>
public double Rug(Wallet acct)
{
if (!Open) return 0;
double take = Pool;
acct.AddDirty(Assets.CRC, take, take);
Open = false; Done = true; Pool = 0;
return take;
}
}
}
+457
View File
@@ -0,0 +1,457 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
public enum PredictKind { Event, Faction, Purity, Price, Gas, Cra, Era, Trial }
public enum PredictResult { Ok, Closed, Amount, Funds, Cap, Opposite, NoPosition }
/// <summary>
/// 一个盘口:问一个是非题,开盘当天可以买卖,窗口(FromDay..ToDay)里的结果由游戏或本 mod 的数值决定,ToDay 夜间结算。
/// 定价用 LMSR:初始价等于先验概率 P0,流动性参数 B 按开盘日的单盘额度(<see cref="Budget"/>)定,与先验无关;
/// 额度决定份额上限(限制内幕单能赚多少)和内幕消息的价格。
/// </summary>
public sealed class PredictMarket
{
public int Id;
public PredictKind Kind;
/// <summary>事件 / 派系标识;纯度盘是 "gutter" 或 "rust";行情盘是 "PSC"。</summary>
public string Key, Title;
/// <summary>阈值(纯度、价格盘)。</summary>
public double Param;
public int OpenDay, FromDay, ToDay;
public double P0, B, QYes, QNo;
/// <summary>单盘额度(开盘日的 <see cref="LossFor"/>)。旧存档里的盘口没有这个字段,按当时"额度 = B × ln(1/先验)"回推。</summary>
public double Budget;
/// <summary>昨晚消息反应之前的价格(-1 = 还没反应过):列表里显示涨跌,让玩家看到消息怎样推动价格。</summary>
public double PrevP = -1;
/// <summary>-1 未结算,0 否,1 是,2 作废(缺观测数据,退还本金)。</summary>
public int Outcome = -1;
public double Loss => Budget > 0 ? Budget : B * Math.Log(1 / Math.Min(P0, 1 - P0));
public double ShareCap => Tuning.PredictCap * Loss;
public bool IsOpen(int day) => Outcome < 0 && day < FromDay;
public double PYes => 1 / (1 + Math.Exp((QNo - QYes) / B));
public static double LossFor(int day) => Tuning.PredictLoss0 + Tuning.PredictLossPerDay * day;
public static PredictMarket Create(int id, PredictKind kind, string key, string title, double param, int openDay, int fromDay, int toDay, double p0)
{
p0 = Math.Max(Tuning.PredictPMin, Math.Min(Tuning.PredictPMax, p0));
double budget = LossFor(openDay), b = Tuning.PredictDepth * budget / Math.Log(2);
return new PredictMarket { Id = id, Kind = kind, Key = key, Title = title, Param = param, OpenDay = openDay, FromDay = fromDay, ToDay = toDay, P0 = p0, B = b, Budget = budget, QYes = b * Math.Log(p0 / (1 - p0)) };
}
/// <summary>LMSR 成本函数(数值稳定写法)。</summary>
public double C(double qy, double qn)
{
double m = Math.Max(qy, qn);
return m + B * Math.Log(Math.Exp((qy - m) / B) + Math.Exp((qn - m) / B));
}
/// <summary>某一边买入 shares 份要付的成本(不含手续费)。</summary>
public double BuyCost(bool yes, double shares) => yes ? C(QYes + shares, QNo) - C(QYes, QNo) : C(QYes, QNo + shares) - C(QYes, QNo);
/// <summary>某一边卖出 shares 份能收回的金额(不含手续费)。</summary>
public double SellValue(bool yes, double shares) => yes ? C(QYes, QNo) - C(QYes - shares, QNo) : C(QYes, QNo) - C(QYes, QNo - shares);
/// <summary>花 cost(不含手续费)能买到的份数:LMSR 成本函数的闭式反解。</summary>
public double SharesFor(bool yes, double cost)
{
double y = yes ? QYes : QNo, o = yes ? QNo : QYes;
double m = Math.Max(y, o);
double a = Math.Exp((y - m) / B), oo = Math.Exp((o - m) / B);
double x = Math.Exp(Math.Min(cost / B, 700)) * (a + oo) - oo;
return m + B * Math.Log(x) - y;
}
public double PriceOf(bool yes) => yes ? PYes : 1 - PYes;
}
/// <summary>玩家在某个盘口的持仓(一个盘口只能持有一边)。Insider 是内幕单买入的份数。</summary>
public sealed class PredictPos
{
public int Market;
/// <summary>持仓所在的钱包地址(下注签名的钱包,结算和卖出都回到它)。</summary>
public string Wallet;
public bool Yes;
/// <summary>Dirty:下注付出的 CRC 里脏的枚数(兑付时按比例带回)。</summary>
public double Shares, Cost, Dirty, Basis, Insider;
/// <summary>旧存档(数据版本 5 之前)的污点额(信用点),读档迁移时换成 <see cref="Dirty"/>。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
}
/// <summary>情报贩子卖的内幕消息:对某个盘口给出"是 / 否"的说法,三档准确率,可能是假的(与真相相反)。</summary>
public sealed class PredictTip
{
public int Market, Tier;
public bool Claim;
public double Paid;
}
public sealed class PredictStat
{
public int Count, Hits;
public double SumP0;
public double AvgP0 => Count > 0 ? SumP0 / Count : 0;
public double HitRate => Count > 0 ? Hits / (double)Count : 0;
}
/// <summary>一次夜间价格反应。</summary>
public sealed class PredictMove
{
public PredictMarket Market;
public double From, To;
}
/// <summary>结算记录(盘口列表下面的"最近结果")。</summary>
public sealed class PredictRecord
{
public int Day;
public string Title;
public int Outcome;
public double Pnl;
public bool Held;
}
/// <summary>夜间结算发生的事。Kind:win / lose / void / insider(内幕份额对应的兑付记成脏币)/ manip(被判操纵,整笔兑付记成脏币)。
/// Amount 是兑付或本金;Taint 是因此记成脏的 CRC 枚数。</summary>
public sealed class PredictEvent
{
public string Kind, Title;
public PredictMarket Market;
public double Amount, Taint;
}
/// <summary>候选盘口(接入层按游戏当前状态给出):先验概率按窗口算。</summary>
public sealed class PredictCandidate
{
public PredictKind Kind;
public string Key, Title;
public double Param;
/// <summary>true:只问明天一天(纯度盘)。</summary>
public bool Tomorrow;
public Func<int, int, double> Prob;
}
public sealed class PredictBook
{
public List<PredictMarket> Markets = new List<PredictMarket>();
public List<PredictPos> Positions = new List<PredictPos>();
public List<PredictRecord> Recent = new List<PredictRecord>();
public List<PredictTip> Tips = new List<PredictTip>();
/// <summary>按盘口类型的历史:开过几个、开盘价之和、实际发生几次。玩家据此看出哪类盘口系统性地定价偏高或偏低。</summary>
public Dictionary<string, PredictStat> Stats = new Dictionary<string, PredictStat>();
public int Seq;
/// <summary>每晚观测:当天新出现的事件标识(和 "F:派系")。窗口里缺任何一天的观测,盘口作废退款。</summary>
public Dictionary<int, List<string>> Started = new Dictionary<int, List<string>>();
public List<string> Prev;
public PredictMarket Find(int id) => Markets.Find(m => m.Id == id);
public PredictPos PosOf(int id, string wallet) => Positions.Find(p => p.Market == id && p.Wallet == wallet);
public List<PredictPos> PosAll(int id) => Positions.Where(p => p.Market == id).ToList();
private static double Share(Wallet acct, double amount, Func<string, double> price)
{
double total = acct.Value(price);
return total > 0 ? Math.Min(1, amount * price(Assets.CRC) / total) : 0;
}
/// <summary>
/// 买入:花 spend(含手续费)的 CRC。CRC 的脏枚数按比例、成本按花掉的价值比例随资金进持仓。insider:这笔单下单时玩家已经知道答案(传闻纸条、派系特权)。
/// </summary>
public PredictResult Buy(Wallet acct, PredictMarket m, bool yes, double spend, int day, Func<string, double> pr, bool insider, out double shares)
{
shares = 0;
if (!m.IsOpen(day)) return PredictResult.Closed;
if (spend <= 0) return PredictResult.Amount;
if (acct.Avail(Assets.CRC) + 1e-9 < spend) return PredictResult.Funds;
var all = PosAll(m.Id);
if (all.Any(x => x.Yes != yes)) return PredictResult.Opposite;
var pos = PosOf(m.Id, acct.Address);
double cost = spend / (1 + Tuning.PredictFee);
shares = m.SharesFor(yes, cost);
if (all.Sum(x => x.Shares) + shares > m.ShareCap + 1e-9) { shares = 0; return PredictResult.Cap; }
double share = Share(acct, spend, pr);
double dirty = spend * acct.DirtyRatio(Assets.CRC), basis = acct.Basis * share;
acct.Add(Assets.CRC, -spend); acct.Basis -= basis;
if (yes) m.QYes += shares; else m.QNo += shares;
if (pos == null) Positions.Add(pos = new PredictPos { Market = m.Id, Yes = yes, Wallet = acct.Address });
pos.Shares += shares; pos.Cost += cost; pos.Dirty += dirty; pos.Basis += basis;
if (insider) pos.Insider += shares;
return PredictResult.Ok;
}
/// <summary>全部卖出这个钱包在盘口里的持仓(盘口关闭前):收回 LMSR 的卖价减手续费,脏枚数按比例带回,赚的部分是干净的。返回收回的 CRC。</summary>
public PredictResult SellAll(Wallet acct, PredictMarket m, int day, Func<string, double> price, out double got) => SellAll(acct, m, day, price, out got, out _);
/// <summary>
/// 全部卖出;持仓里的内幕份额(下单时已经知道答案)对应的那部分卖出所得记成脏的 CRC,taint 是记成脏的枚数。先买后卖、赶在结算前离场也一样。
/// </summary>
public PredictResult SellAll(Wallet acct, PredictMarket m, int day, Func<string, double> price, out double got, out double taint)
{
got = 0; taint = 0;
var pos = PosOf(m.Id, acct.Address);
if (pos == null) return PredictResult.NoPosition;
if (!m.IsOpen(day)) return PredictResult.Closed;
double value = m.SellValue(pos.Yes, pos.Shares);
got = value * (1 - Tuning.PredictFee);
if (pos.Yes) m.QYes -= pos.Shares; else m.QNo -= pos.Shares;
Positions.Remove(pos);
double back = Pay(acct, pos, got);
taint = MarkInsider(acct, pos, got, back);
return PredictResult.Ok;
}
/// <summary>兑付进钱包:下注时带进来的脏枚数按兑付占成本的比例带回(不超过兑付)。返回带回的脏枚数。</summary>
private static double Pay(Wallet acct, PredictPos pos, double payout)
{
double keep = pos.Cost > 0 ? Math.Min(1, payout / pos.Cost) : 0;
double dirty = Math.Min(payout, pos.Dirty * keep);
acct.AddDirty(Assets.CRC, payout, dirty); acct.Basis += pos.Basis * keep;
return dirty;
}
/// <summary>这笔兑付里还干净的部分(兑付 − 已经带回的脏枚数)里,再记 qty 枚为脏的。返回记上的枚数。</summary>
private static double MarkPayout(Wallet acct, double payout, double alreadyDirty, double qty)
{
double mark = Math.Max(0, Math.Min(qty, payout - alreadyDirty));
return mark - acct.MarkDirty(Assets.CRC, mark);
}
/// <summary>内幕份额对应的兑付记成脏的 CRC。返回记上的枚数。</summary>
private static double MarkInsider(Wallet acct, PredictPos pos, double payout, double alreadyDirty)
{
if (pos.Insider <= 0 || pos.Shares <= 0 || payout <= 0) return 0;
return MarkPayout(acct, payout, alreadyDirty, payout * Math.Min(1, pos.Insider / pos.Shares));
}
/// <summary>
/// 夜间:先登记今晚的观测,再结算 ToDay 到了的盘口(赢的钱回到下注的钱包,wallet 按地址找)。outcome 由接入层给出(事件 / 派系读观测,纯度和行情读游戏数值),null 表示拿不到数据。
/// 赢家每份兑付 1 信用点,照常兑付;内幕份额对应的兑付记成脏的 CRC,行情盘被判操纵时整笔兑付都记成脏的。
/// </summary>
public List<PredictEvent> Night(int day, IEnumerable<string> activeNow, Func<PredictMarket, bool?> outcome, Func<string, Wallet> wallet, Func<string, double> price, Rng rng, double push = 0)
{
var now = activeNow.Distinct().ToList();
if (Prev != null) Started[day] = now.Where(x => !Prev.Contains(x)).ToList();
Prev = now;
foreach (var k in Started.Keys.Where(k => k < day - 12).ToList()) Started.Remove(k);
var evs = new List<PredictEvent>();
foreach (var m in Markets.Where(x => x.Outcome < 0 && x.ToDay <= day).ToList())
{
bool? res = outcome(m);
var all = PosAll(m.Id);
foreach (var pos in all) Positions.Remove(pos);
double cost = all.Sum(x => x.Cost);
if (res == null)
{
m.Outcome = 2;
foreach (var pos in all) Pay(wallet(pos.Wallet), pos, pos.Cost);
if (all.Count > 0) evs.Add(new PredictEvent { Kind = "void", Title = m.Title, Market = m, Amount = cost });
Record(day, m, 2, 0, all.Count > 0);
continue;
}
m.Outcome = res.Value ? 1 : 0;
string sk = m.Kind.ToString();
if (!Stats.TryGetValue(sk, out var st)) Stats[sk] = st = new PredictStat();
st.Count++; st.SumP0 += m.P0; if (res.Value) st.Hits++;
var winners = all.Where(x => x.Yes == res.Value).ToList();
if (all.Count > 0 && winners.Count == 0)
{
evs.Add(new PredictEvent { Kind = "lose", Title = m.Title, Market = m, Amount = cost });
Record(day, m, m.Outcome, -cost, true);
}
else if (winners.Count > 0)
{
double payout = winners.Sum(x => x.Shares), profit = payout - cost;
// 操纵:行情盘上,赢家的方向和玩家推价的方向一致时按推价幅度被查——照常兑付,整笔兑付记成脏的
bool manip = m.Kind == PredictKind.Price && winners.Any(x => x.Yes ? push > 0 : push < 0) && rng.Chance(Math.Min(1, Market.ManipChance(push) * StoryMods.Current.Enforce(day)));
double taint = 0;
bool insider = false;
foreach (var pos in winners)
{
var w = wallet(pos.Wallet);
double back = Pay(w, pos, pos.Shares);
// 内幕份额(下单时已经知道答案)对应的兑付记成脏的,每个钱包各算各的
if (manip) taint += MarkPayout(w, pos.Shares, back, pos.Shares);
else if (pos.Insider > 0) { insider = true; taint += MarkInsider(w, pos, pos.Shares, back); }
}
string kind = manip ? "manip" : insider ? "insider" : "win";
evs.Add(new PredictEvent { Kind = kind, Title = m.Title, Market = m, Amount = payout, Taint = taint });
Record(day, m, m.Outcome, profit, true);
}
else Record(day, m, m.Outcome, 0, false);
}
Markets.RemoveAll(m => m.Outcome >= 0);
Tips.RemoveAll(t => Find(t.Market) == null);
return evs;
}
private void Record(int day, PredictMarket m, int outcome, double pnl, bool held)
{
Recent.Add(new PredictRecord { Day = day, Title = m.Title, Outcome = outcome, Pnl = pnl, Held = held });
while (Recent.Count > Tuning.PredictHistory) Recent.RemoveAt(0);
}
public PredictTip TipFor(int market) => Tips.Find(t => t.Market == market);
/// <summary>造一条内幕消息:假情报(概率 IntelFake)说的与真相相反,否则按这一档的准确率说对或说错。</summary>
public static PredictTip MakeTip(PredictMarket m, bool truth, int tier, Rng rng)
{
bool claim;
if (rng.Chance(Tuning.IntelFake)) claim = !truth;
else claim = rng.Chance(Tuning.IntelTipAcc[tier]) ? truth : !truth;
return new PredictTip { Market = m.Id, Tier = tier, Claim = claim, Paid = Tuning.IntelTipPrice[tier] * m.Loss };
}
/// <summary>
/// 夜间的消息反应:还能交易的盘口(明天仍开着)向"公开信息下的合理概率"靠拢 PredictDrift,再加 logit 噪声(NPC 流量)。
/// 价格在夜里才反应,所以白天先看到报纸、传闻的人可以抢在前面买、等涨价再卖。target 返回 null 表示这个盘口没有新信息。
/// </summary>
public List<PredictMove> Drift(int day, Func<PredictMarket, double?> target, Rng rng)
{
var moves = new List<PredictMove>();
foreach (var m in Markets.Where(x => x.Outcome < 0 && x.FromDay > day + 1))
{
m.PrevP = m.PYes;
double p = m.PYes, t = target(m) ?? p;
t = Math.Max(Tuning.PredictPMin, Math.Min(Tuning.PredictPMax, t));
double pt = p + Tuning.PredictDrift * (t - p);
double lp = Math.Log(pt / (1 - pt)) + Tuning.PredictNoise * rng.Gauss();
pt = Math.Max(Tuning.PredictPMin, Math.Min(Tuning.PredictPMax, 1 / (1 + Math.Exp(-lp))));
m.QYes = m.QNo + m.B * Math.Log(pt / (1 - pt));
moves.Add(new PredictMove { Market = m, From = m.PrevP, To = m.PYes });
}
return moves;
}
/// <summary>读观测:窗口里每一天都有记录才能判断;有任何一天缺就返回 null(作废)。</summary>
public bool? Happened(PredictMarket m)
{
string tag = m.Kind == PredictKind.Faction ? "F:" + m.Key : m.Key;
bool any = false;
for (int d = m.FromDay; d <= m.ToDay; d++)
{
if (!Started.TryGetValue(d, out var list)) return null;
if (list.Contains(tag)) any = true;
}
return any;
}
/// <summary>
/// 每天开门后补盘:开放的盘口少于下限时补到下限,之后每天再随机新开一两个,总数不超过上限。同类同键的盘口不重复开。
/// 返回新开的盘口。
/// </summary>
public List<PredictMarket> Refill(int day, Rng rng, IList<PredictCandidate> candidates)
{
var made = new List<PredictMarket>();
int open = Markets.Count(m => m.Outcome < 0);
int want = open < Tuning.PredictOpenMin ? Tuning.PredictOpenMin - open : 0;
want = Math.Max(want, rng.Range(1, Tuning.PredictNewPerDay));
want = Math.Min(want, Tuning.PredictOpenMax - open);
var pool = candidates.Where(c => !Markets.Any(m => m.Outcome < 0 && m.Kind == c.Kind && m.Key == c.Key)).ToList();
while (want-- > 0 && pool.Count > 0)
{
var c = pool[rng.Range(0, pool.Count - 1)];
pool.RemoveAll(x => x.Kind == c.Kind && x.Key == c.Key);
int from = c.Tomorrow ? day + 1 : day + 1 + rng.Range(0, 2);
int to = c.Tomorrow ? from : from + rng.Range(0, 2);
var m = PredictMarket.Create(++Seq, c.Kind, c.Key, c.Title, c.Param, day, from, to, c.Prob(from, to));
Markets.Add(m); made.Add(m);
}
return made;
}
}
/// <summary>盘口先验概率:事件按游戏的掷骰规则,纯度按均匀分布,行情按本 mod 的价格过程。</summary>
public static class PredictMath
{
/// <summary>某个常规事件在窗口里出现的概率:每天先过"有事件"这一关(chance),再按权重在可选事件里抽中。</summary>
public static double EventProb(double dayChance, double odd, double totalOdd, int days)
{
double p1 = totalOdd > 0 ? dayChance * odd / totalOdd : 0;
return 1 - Math.Pow(1 - p1, days);
}
/// <summary>某天 gas 不低于阈值的概率(已知相位):周期值 × (1 ± 噪声) 均匀分布。</summary>
public static double GasAtLeast(int day, int phase, double threshold)
{
double c = TxRules.GasCycle(day, phase), hi = c * (1 + Tuning.TxGasNoise), lo = c * (1 - Tuning.TxGasNoise);
return Math.Max(0, Math.Min(1, (hi - threshold) / (hi - lo)));
}
/// <summary>不知道相位时的先验:七个相位等可能。</summary>
public static double GasFlat(int day, double threshold)
{
double s = 0;
for (int ph = 0; ph < Tuning.TxGasPeriod; ph++) s += GasAtLeast(day, ph, threshold);
return s / Tuning.TxGasPeriod;
}
/// <summary>
/// CRA 预警在 [from, to] 夜里发出的概率:每晚补贴池先被消耗,归零那晚必发,否则按日概率;
/// 窗口之前已经发出的不算(那就是"否")。subsidy / drain 是今天的补贴池和每晚消耗,today 是今晚。
/// </summary>
public static double CraWarnProb(double subsidy, double drain, int today, int from, int to, double dailyHazard)
{
double noWarn = 1, inWindow = 0;
for (int d = today; d <= to; d++)
{
subsidy -= drain;
double h = subsidy <= 0 ? 1 : dailyHazard;
if (d >= from) inWindow += noWarn * h;
noWarn *= 1 - h;
}
return inWindow;
}
/// <summary>
/// 矿机时代在 [from, to] 开始的概率:没出预警时在 [eraMin, eraMax] 里均匀;预警已出(warnDay >= 0)时落在预警后 EraWarnMin..EraWarnMax 天。
/// 只算还没过去的日子(today 之后)。
/// </summary>
public static double EraProb(int today, int from, int to, int warnDay)
{
int lo = warnDay >= 0 ? warnDay + Tuning.EraWarnMin : Tuning.EraMin, hi = warnDay >= 0 ? warnDay + Tuning.EraWarnMax : Tuning.EraMax;
lo = Math.Max(lo, today + 1);
if (hi < lo) return 0;
int a = Math.Max(lo, from), b = Math.Min(hi, to);
return b < a ? 0 : (b - a + 1) / (double)(hi - lo + 1);
}
public static double FactionProb(int days) => 1 - Math.Pow(1 - Tuning.PredictFactionDaily, days);
/// <summary>均匀整数 lo..hi 里不低于 threshold 的概率。</summary>
public static double PurityAtLeast(double threshold, int lo, int hi)
{
double n = hi - lo + 1, k = hi - Math.Ceiling(threshold) + 1;
return Math.Max(0, Math.Min(1, k / n));
}
public static double Phi(double x)
{
double t = 1 / (1 + 0.2316419 * Math.Abs(x));
double d = 0.3989423 * Math.Exp(-x * x / 2);
double p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
return x > 0 ? 1 - p : p;
}
/// <summary>
/// 价格在 nights 个夜晚之后(每晚向锚回归 revert、加 vol 的对数噪声,每个早上再加 vol/2 的开门波动)不低于 threshold 的概率。
/// 对数价格近似正态;锚按今天的值外推(事件和减半的变化不计)。
/// </summary>
public static double PriceAtLeast(double price, double anchor, double vol, double revert, int nights, double threshold)
{
double m = Math.Log(price), la = Math.Log(anchor), v = 0;
for (int i = 0; i < nights; i++)
{
m += revert * (la - m);
v = (1 - revert) * (1 - revert) * v + vol * vol + vol * vol / 4;
}
double sd = Math.Sqrt(Math.Max(v, 1e-12));
return 1 - Phi((Math.Log(threshold) - m) / sd);
}
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ namespace Decentralized.Domain
public static class RigSlots
{
public const int Base = 2, Max = 5;
public const int CellWidth = 2, CellsPerSlot = 2;
public const int CellWidth = 3, CellsPerSlot = 2;
/// <summary>升级到第 n 个槽需要的电子元件(物品 id,件数)。n 从 3 开始。</summary>
public static (string Item, int Count) Cost(int slot)
@@ -45,7 +45,7 @@ namespace Decentralized.Domain
return take;
}
/// <summary>显卡格子的形状字符串:宽 2,高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。</summary>
/// <summary>显卡格子的形状字符串:宽 3(一张卡 3×2 格),高 2 × 最大槽数;前 slots × 2 行可放(0),其余不可放(1)。</summary>
public static string Shape(int slots)
{
var sb = new System.Text.StringBuilder();
+26
View File
@@ -0,0 +1,26 @@
using System;
namespace Decentralized.Domain
{
/// <summary>mod 设置里的倍率(全局,不随存档)。接入层读设置后写进来;领域层在对应概率和产出处乘上。</summary>
public static class Scales
{
/// <summary>风险倍率:骗局、跑路、钱包投毒、地址劫持、钓鱼的发生概率。</summary>
public static double Risk = 1;
/// <summary>挖矿产出倍率:玩家矿机拿到的区块奖励。</summary>
public static double Mining = 1;
/// <summary>概率乘风险倍率,封顶 1。</summary>
public static double R(double chance) => Math.Min(1, chance * Risk);
/// <summary>设置里的倍率档位(“×1.5”)。</summary>
public static readonly string[] Choices = { "×0.5", "×1", "×1.5", "×2" };
public const string Default = "×1";
public static double Parse(string s)
{
if (s != null && s.StartsWith("×") && double.TryParse(s.Substring(1), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v) && v >= 0) return v;
return 1;
}
}
}
+246
View File
@@ -0,0 +1,246 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>空投:签名领取。正规空投不要授权或只要有限授权,给一点币;钓鱼空投要求对某个币"无上限授权",几晚后把这个钱包里的这种币转走。</summary>
public sealed class Airdrop
{
public int Id, Expire;
public string Name, Asset;
/// <summary>领取要授权的额度:0 = 不要授权;double.PositiveInfinity = 无上限。</summary>
public double Approve;
public double Reward;
public bool Scam, Claimed;
/// <summary>已为这个空投授权的钱包地址(签过授权合约);空 = 还没授权。领取要用授权过的钱包。</summary>
public string AuthWallet;
}
/// <summary>钱包对某个合约的授权。钓鱼授权到 DrainDay 那晚把这种币转走,撤销后作废。</summary>
public sealed class Approval
{
public int Id, DrainDay = -1;
public string Wallet, Asset, Spender;
public double Limit;
public bool Scam;
}
/// <summary>第三方收益池(CRC):日息、储备、有没有时间锁、有没有审计都看得见;是不是骗局看不见,跑路时存款全部没了。</summary>
public sealed class YieldPool
{
public int Id, OpenDay;
public string Name;
public double Rate, Reserve, Cap;
public bool Timelock, Audited, Scam, Closed;
public Dictionary<string, double> Deposits = new Dictionary<string, double>();
/// <summary>每个钱包存入的 CRC 里脏的枚数、成本:取出时带回。</summary>
public Dictionary<string, double> Dirty = new Dictionary<string, double>(), Basis = new Dictionary<string, double>();
/// <summary>旧存档(数据版本 5 之前)的污点额(信用点),读档迁移时换成 <see cref="Dirty"/>。</summary>
[JsonProperty("Taint")] public Dictionary<string, double> LegacyTaint = new Dictionary<string, double>();
/// <summary>时间锁池子的取款队列:钱包 → (到账日, 数额)。</summary>
public List<YieldWithdraw> Pending = new List<YieldWithdraw>();
public double Total => Deposits.Values.Sum();
public double Of(string wallet) => Deposits.TryGetValue(wallet, out var v) ? v : 0;
}
public sealed class YieldWithdraw
{
public string Wallet; public int Day; public double Amount, Dirty, Basis;
/// <summary>旧存档(数据版本 5 之前)的污点额(信用点),读档迁移时换成 <see cref="Dirty"/>。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
}
/// <summary>夜间发生的事。Kind:drain(钓鱼授权转走)/ rug(收益池跑路)/ dry(正规池储备耗尽,退本金)/ paid(取款到账)/ wickback(插针回去了)。</summary>
public sealed class ScamEvent
{
public string Kind, Name, Wallet, Asset;
public double Amount;
}
/// <summary>NPC 骗局:空投与授权、第三方收益池。插针在 <see cref="Market"/> 里。</summary>
public sealed class Scams
{
public List<Airdrop> Airdrops = new List<Airdrop>();
public List<Approval> Approvals = new List<Approval>();
public List<YieldPool> Pools = new List<YieldPool>();
public int Seq;
/// <summary>骗子用过的收款地址(假客服、钓鱼):转到这些地址时晨间日志说明被骗。</summary>
public List<string> ScamAddrs = new List<string>();
private static readonly string[] AirdropNames = { "Poinbase", "PSC Foundation", "RatDigg", "Revolution DAO", "Gutter Labs", "Station Rewards" };
private static readonly string[] PoolNames = { "Nebula Yield", "Rustwater Vault", "Orbit Farm", "Deck 9 Savings", "Gutterflow Pool", "Halo Staking", "Ironclad Earn" };
public static double ScamChance(double rate, bool timelock, bool audited)
{
int i = Array.IndexOf(Tuning.YieldRates, rate);
double p = i >= 0 ? Tuning.YieldScamByRate[i] : 0.5;
if (timelock) p -= Tuning.YieldScamTimelock;
if (audited) p -= Tuning.YieldScamAudit;
return Math.Max(0.02, Math.Min(0.95, p));
}
// ——— 空投 ———
/// <summary>新空投。钓鱼的名字常是正规名字的仿冒(一个字符不同),要求对一种币无上限授权,奖励看起来更高。</summary>
public Airdrop NewAirdrop(int day, Rng rng, string[] assets)
{
bool scam = rng.Chance(Scales.R(Tuning.AirdropScam));
string name = AirdropNames[rng.Range(0, AirdropNames.Length - 1)];
if (scam && rng.Chance(0.6)) name = Typo(name, rng);
string asset = assets[rng.Range(0, assets.Length - 1)];
double reward = Math.Round(rng.Range(Tuning.AirdropRewardMin, Tuning.AirdropRewardMax));
if (scam) reward = Math.Round(reward * rng.Range(1.5, 3.0));
// 钓鱼:多数要无上限授权,三成要一个看着像有限、其实远大于奖励的额度
double approve = scam ? (rng.Chance(0.3) ? Math.Round(reward * rng.Range(20.0, 100.0) / 10) * 10 : double.PositiveInfinity) : rng.Chance(0.5) ? 0 : Math.Round(rng.Range(10.0, 60.0));
var a = new Airdrop { Id = ++Seq, Name = name, Asset = asset, Approve = approve, Reward = reward, Scam = scam, Expire = day + Tuning.AirdropDays };
Airdrops.Add(a);
return a;
}
private static string Typo(string s, Rng rng)
{
var swaps = new Dictionary<char, char> { ['o'] = '0', ['i'] = '1', ['l'] = '1', ['e'] = '3', ['a'] = '4', ['s'] = '5', ['n'] = 'm' };
var idx = Enumerable.Range(0, s.Length).Where(i => swaps.ContainsKey(char.ToLowerInvariant(s[i]))).ToList();
if (idx.Count == 0) return s + "s";
int k = idx[rng.Range(0, idx.Count - 1)];
return s.Substring(0, k) + swaps[char.ToLowerInvariant(s[k])] + s.Substring(k + 1);
}
/// <summary>这个钱包是否已为这个空投授权。</summary>
public static bool Authorized(Airdrop a, Wallet w) => a != null && w != null && !string.IsNullOrEmpty(a.AuthWallet) && ChainAddress.Same(a.AuthWallet, w.Address);
/// <summary>为空投授权钱包(签完授权合约后调用);换了钱包要重新授权。</summary>
public static bool Authorize(Airdrop a, Wallet w, int day)
{
if (a == null || w == null || a.Claimed || day > a.Expire) return false;
a.AuthWallet = w.Address;
return true;
}
/// <summary>领取:正规空投给奖励(干净的 CRC),钓鱼空投给一点点诱饵;有授权的记一条授权,钓鱼授权定下转走的那晚。</summary>
public Approval Claim(Airdrop a, Wallet w, int day, Rng rng, out double got)
{
got = 0;
if (a.Claimed || day > a.Expire) return null;
a.Claimed = true;
got = a.Scam ? Math.Round(a.Reward * 0.05, 2) : a.Reward;
w.Add(Assets.CRC, got); w.Basis += got;
if (a.Approve <= 0) return null;
var ap = new Approval { Id = ++Seq, Wallet = w.Address, Asset = a.Asset, Spender = a.Name, Limit = a.Approve, Scam = a.Scam, DrainDay = a.Scam ? day + rng.Range(Tuning.AirdropDrainMin, Tuning.AirdropDrainMax) : -1 };
Approvals.Add(ap);
return ap;
}
public bool Revoke(int id) => Approvals.RemoveAll(x => x.Id == id) > 0;
// ——— 收益池 ———
public YieldPool NewPool(int day, Rng rng)
{
double rate = Tuning.YieldRates[rng.Range(0, Tuning.YieldRates.Length - 1)];
bool timelock = rng.Chance(0.5), audited = rng.Chance(0.4);
bool scam = rng.Chance(ScamChance(rate, timelock, audited));
double cap = Tuning.YieldCap0 + Tuning.YieldCapPerDay * day;
var used = Pools.Where(p => !p.Closed).Select(p => p.Name).ToList();
var names = PoolNames.Where(n => !used.Contains(n)).ToList();
var p2 = new YieldPool
{
Id = ++Seq, OpenDay = day, Name = names.Count > 0 ? names[rng.Range(0, names.Count - 1)] : "Pool " + Seq,
Rate = rate, Timelock = timelock, Audited = audited, Scam = scam, Cap = cap,
// 正规池的储备够付若干天满额利息;骗局池报一个好看的数字(不会真付到那么多天,跑路前一直照付)
Reserve = Math.Round(rate * cap * Tuning.YieldReserveDays * (scam ? rng.Range(0.8, 1.6) : rng.Range(0.6, 1.2))),
};
Pools.Add(p2);
return p2;
}
public bool Deposit(YieldPool p, Wallet w, double amount, Func<string, double> pr)
{
if (p.Closed || amount <= 0 || w.Avail(Assets.CRC) + 1e-9 < amount || p.Total + amount > p.Cap + 1e-9) return false;
double total = w.Value(pr), share = total > 0 ? Math.Min(1, amount * pr(Assets.CRC) / total) : 0;
double dirty = amount * w.DirtyRatio(Assets.CRC), basis = w.Basis * share;
w.Add(Assets.CRC, -amount); w.Basis -= basis;
p.Deposits[w.Address] = p.Of(w.Address) + amount;
p.Dirty[w.Address] = (p.Dirty.TryGetValue(w.Address, out var t) ? t : 0) + dirty;
p.Basis[w.Address] = (p.Basis.TryGetValue(w.Address, out var b) ? b : 0) + basis;
return true;
}
/// <summary>取出:没有时间锁的马上到账;有时间锁的进队列,几天后到账(跑路了就没了)。返回马上到账的数额。</summary>
public double Withdraw(YieldPool p, Wallet w, int day)
{
double amt = p.Of(w.Address);
if (amt <= 0 || p.Closed) return 0;
double dirty = p.Dirty.TryGetValue(w.Address, out var t) ? t : 0, basis = p.Basis.TryGetValue(w.Address, out var b) ? b : 0;
p.Deposits.Remove(w.Address); p.Dirty.Remove(w.Address); p.Basis.Remove(w.Address);
if (p.Timelock) { p.Pending.Add(new YieldWithdraw { Wallet = w.Address, Day = day + Tuning.YieldLockDays, Amount = amt, Dirty = dirty, Basis = basis }); return 0; }
Pay(w, amt, dirty, basis);
return amt;
}
private static void Pay(Wallet w, double amt, double dirty, double basis)
{
w.AddDirty(Assets.CRC, amt, dirty); w.Basis += basis;
}
// ——— 夜间 ———
/// <summary>
/// 夜间:钓鱼授权到期转走;收益池计息、储备耗尽的正规池退本金关闭、骗局池按概率跑路;时间锁取款到账;过期空投清掉;随机新空投和新收益池。
/// </summary>
public List<ScamEvent> Night(int day, Rng rng, Func<string, Wallet> own, string[] airdropAssets, Func<string, double> pr = null)
{
var evs = new List<ScamEvent>();
foreach (var ap in Approvals.Where(x => x.Scam && x.DrainDay >= 0 && x.DrainDay <= day).ToList())
{
Approvals.Remove(ap);
var w = own(ap.Wallet);
if (w == null) continue;
double amt = Math.Min(ap.Limit, w.Avail(ap.Asset));
if (amt <= 1e-9) continue;
if (pr != null) w.Spend(ap.Asset, amt, pr); else w.Add(ap.Asset, -amt);
evs.Add(new ScamEvent { Kind = "drain", Name = ap.Spender, Wallet = ap.Wallet, Asset = ap.Asset, Amount = amt });
}
foreach (var p in Pools.Where(x => !x.Closed).ToList())
{
if (p.Scam && p.Total > 0 && day > p.OpenDay && rng.Chance(Scales.R(Tuning.YieldRugNightly)))
{
foreach (var kv in p.Deposits) evs.Add(new ScamEvent { Kind = "rug", Name = p.Name, Wallet = kv.Key, Asset = Assets.CRC, Amount = kv.Value });
foreach (var q in p.Pending) evs.Add(new ScamEvent { Kind = "rug", Name = p.Name, Wallet = q.Wallet, Asset = Assets.CRC, Amount = q.Amount });
p.Deposits.Clear(); p.Pending.Clear(); p.Closed = true;
continue;
}
double interest = p.Total * p.Rate;
if (!p.Scam && interest > p.Reserve)
{
foreach (var kv in p.Deposits.ToList())
{
var w = own(kv.Key);
if (w != null) Pay(w, kv.Value, p.Dirty.TryGetValue(kv.Key, out var t) ? t : 0, p.Basis.TryGetValue(kv.Key, out var b) ? b : 0);
evs.Add(new ScamEvent { Kind = "dry", Name = p.Name, Wallet = kv.Key, Asset = Assets.CRC, Amount = kv.Value });
}
p.Deposits.Clear(); p.Closed = true;
}
else
{
p.Reserve -= interest;
foreach (var k in p.Deposits.Keys.ToList()) p.Deposits[k] *= 1 + p.Rate;
}
foreach (var q in p.Pending.Where(x => x.Day <= day).ToList())
{
p.Pending.Remove(q);
var w = own(q.Wallet);
if (w != null) Pay(w, q.Amount, q.Dirty, q.Basis);
evs.Add(new ScamEvent { Kind = "paid", Name = p.Name, Wallet = q.Wallet, Asset = Assets.CRC, Amount = q.Amount });
}
}
Pools.RemoveAll(p => p.Closed && p.Pending.Count == 0 && day - p.OpenDay > 20);
Airdrops.RemoveAll(a => a.Expire < day + 1);
if (rng.Chance(Tuning.AirdropChance)) NewAirdrop(day + 1, rng, airdropAssets);
if (Pools.Count(p => !p.Closed) < Tuning.YieldPoolMax && rng.Chance(Tuning.YieldPoolChance)) NewPool(day + 1, rng);
return evs;
}
}
}
+175 -20
View File
@@ -1,20 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>
/// 三种稳定币的价格与状态:CRC(上层,托管银行出事时脱锚、可能减记),CRT(黑市,镇压或火并时脱锚,
/// 镇压期间按黑市声望出卖客户),CRA(革命军,质押付息,补贴池告急或外生触发时进入三天死亡螺旋)。
/// 镇压期间按黑市声望出卖客户),CRA(革命军,部分抵押:流通量 S、储备 R(CRC)、抵押率 c;质押利息以无抵押的新 CRA 付出,
/// 稀释储备覆盖率 R/S;革命军挪用储备时 R/S 低于崩盘线进入三天死亡螺旋,劫掠上层补回储备)。
/// </summary>
public enum CraOrderKind { Minted, Burned, Lost }
/// <summary>官方一级市场的排队单:铸造(存入 CRC 和 SPARK)或销毁(存入 CRA),到期结算,官方可能把它黑掉。</summary>
public sealed class CraOrder
{
public int Id, Due;
public string Wallet;
public bool Mint;
public double Cra, Crc, Spark, C;
/// <summary>付出去的币的脏比例(按价值加权):到期换回的币按这个比例是脏的。</summary>
public double DirtyRatio;
}
public sealed class CraOrderEvent
{
public CraOrder Order;
public CraOrderKind Kind;
public double Cra, Crc, Spark;
}
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;
/// <summary>CRA 流通量与储备(CRC);c 是现在铸造要求的 CRC 份额(0~1)。</summary>
public double Supply = Tuning.CraS0, Reserve = Tuning.CraS0 * Tuning.CraC0, C = Tuning.CraC0;
/// <summary>预兆日(新闻发出的那天)、预定的挪用日、崩盘开始日(-1 = 没有)。</summary>
public int CraWarnDay = -1, CraEventDay = -1, CraCrashDay = -1;
public List<CraOrder> Orders = new List<CraOrder>();
public int OrderSeq;
public double RevPower = 50;
/// <summary>平衡代币 SPARK 的价格(信用点);SparkPre 是螺旋开始前一刻的价格,螺旋三天按它的倍数走。</summary>
public double Spark = Tuning.SparkStart, SparkPre = -1;
public List<double> RevHistory = new List<double>();
public double Price(string asset)
@@ -24,6 +52,7 @@ namespace Decentralized.Domain
case Assets.CRC: return Crc;
case Assets.CRT: return Crt;
case Assets.CRA: return Cra;
case Assets.SPARK: return Spark;
default: return 1;
}
}
@@ -31,34 +60,94 @@ namespace Decentralized.Domain
/// <summary>减记发生时返回 CRC 持有量要乘的系数(其余情况为 1)。</summary>
public double OnEventStart(MarketEvent ev, int day, Rng rng)
{
if (ev == MarketEvent.RevRaidUpper && CrcDepegEnd < 0 && rng.Chance(Tuning.CrcDepegChance))
if (ev == MarketEvent.RevRaidUpper && CrcDepegEnd < 0 && rng.Chance(Tuning.CrcDepegChance * StoryMods.Current.CrcDepeg))
{
Crc = rng.Range(Tuning.CrcDepegMin, Tuning.CrcDepegMax);
CrcDepegEnd = day + rng.Range(Tuning.CrcDepegDaysMin, Tuning.CrcDepegDaysMax);
CrcHaircutPending = rng.Chance(Tuning.CrcHaircutChance);
CrcHaircutPending = rng.Chance(Math.Min(1, Tuning.CrcHaircutChance * StoryMods.Current.CrcHaircut));
}
if ((ev == MarketEvent.GangWar || ev == MarketEvent.Crackdown) && CrtDepegEnd < 0 && rng.Chance(Tuning.CrtDepegChance))
if ((ev == MarketEvent.GangWar || ev == MarketEvent.Crackdown) && CrtDepegEnd < 0 && rng.Chance(Tuning.CrtDepegChance * StoryMods.Current.CrtDepeg))
{
Crt = rng.Range(Tuning.CrtDepegMin, Tuning.CrtDepegMax);
CrtDepegEnd = day + rng.Range(Tuning.CrtDepegDaysMin, Tuning.CrtDepegDaysMax);
}
if (ev == MarketEvent.RevRaidUpper) { Spark *= Tuning.SparkRaidShock; if (CraCrashDay < 0) Reserve += Tuning.CraLoot * Supply; }
if (ev == MarketEvent.LowerUnrest) Spark *= Tuning.SparkUnrestShock;
return 1;
}
/// <summary>镇压开始时,玩家持有的 CRT 被冻结一半的概率:黑市声望 0 时 40%,50 以上为 0。</summary>
public static double CrtFreezeChance(double blackMarketRep) =>
Math.Max(0, Tuning.CrtFreezeBase - blackMarketRep * Tuning.CrtFreezePerRep);
Math.Max(0, Tuning.CrtFreezeBase - blackMarketRep * Tuning.CrtFreezePerRep) * StoryMods.Current.CrtFreeze;
public bool CraSubsidyLow => CraSubsidy <= Tuning.CraSubsidy0 * Tuning.CraSubsidyWarn;
/// <summary>镇压开始时是否冻结玩家的 CRT:按黑市声望,声望越低越容易被黑市"出卖"。</summary>
public static bool CrtFrozenByCrackdown(double blackMarketRep, Rng rng) => rng.Chance(CrtFreezeChance(blackMarketRep));
/// <summary>储备覆盖率 R/S。</summary>
public double Ratio => Supply > 0 ? Reserve / Supply : 1;
/// <summary>崩盘线之上的储备(CRC):吃空即必然崩盘。预测盘和线索用它估预兆。</summary>
public double Cushion => Reserve - Tuning.CraCrashRatio * Supply;
public static double Cushion0 => Tuning.CraS0 * (Tuning.CraC0 - Tuning.CraCrashRatio);
/// <summary>每晚的垫子消耗:利息按无抵押的新 CRA 付出,每份把崩盘线抬高 CraCrashRatio。</summary>
public static double CushionDrain(double stakedTotal, double rate) => stakedTotal * rate * Tuning.CraCrashRatio;
/// <summary>储备覆盖率对应的价格:高于安全线为 1,线性降到崩盘线的 1 − CraDiscountMax。</summary>
public static double PriceOfRatio(double r)
{
if (r >= Tuning.CraSafeRatio) return 1;
double t = Math.Max(0, (r - Tuning.CraCrashRatio) / (Tuning.CraSafeRatio - Tuning.CraCrashRatio));
return 1 - Tuning.CraDiscountMax * (1 - t);
}
/// <summary>铸造 cra 个 CRA 要的 CRC 和 SPARK 数量(SPARK 部分按现价折算)。</summary>
public void Collateral(double cra, out double crc, out double spark)
{
crc = cra * C;
spark = Spark > 0 ? cra * (1 - C) / Spark : 0;
}
/// <summary>
/// 铸造申请:从钱包扣 CRC 和 SPARK,排队 CraMintDaysMin..Max 天,到期铸出 CRA。余额不足返回 null。
/// </summary>
public CraOrder RequestMint(Wallet w, double cra, int day, Rng rng)
{
if (cra <= 0) return null;
Collateral(cra, out double crc, out double spark);
if (w.Avail(Assets.CRC) + 1e-9 < crc || w.Avail(Assets.SPARK) + 1e-9 < spark) return null;
double sv = spark * Spark, total = crc + sv;
double ratio = total > 0 ? (crc * w.DirtyRatio(Assets.CRC) + sv * w.DirtyRatio(Assets.SPARK)) / total : 0;
w.Add(Assets.CRC, -crc); w.Add(Assets.SPARK, -spark);
var o = new CraOrder { Id = ++OrderSeq, Wallet = w.Address, Mint = true, Cra = cra, Crc = crc, Spark = spark, C = C, DirtyRatio = ratio, Due = day + rng.Range(Tuning.CraMintDaysMin, Tuning.CraMintDaysMax) };
Orders.Add(o);
return o;
}
/// <summary>销毁申请:从钱包扣 CRA,排队 CraUnlockDays 天,到期换回 CRC 和 SPARK。余额不足返回 null。</summary>
public CraOrder RequestBurn(Wallet w, double cra, int day)
{
if (cra <= 0 || w.Avail(Assets.CRA) + 1e-9 < cra) return null;
double ratio = w.DirtyRatio(Assets.CRA);
w.Add(Assets.CRA, -cra);
var o = new CraOrder { Id = ++OrderSeq, Wallet = w.Address, Mint = false, Cra = cra, C = C, DirtyRatio = ratio, Due = day + Tuning.CraUnlockDays };
Orders.Add(o);
return o;
}
/// <summary>官方每晚黑掉一笔排队单的概率:系数 × (1 − CRA 价格),价格不低于 1 时为 0。</summary>
public double RugChance => Math.Max(0, 1 - Cra) * Tuning.CraRugPerDepeg;
public bool CraSubsidyLow => Cushion <= Cushion0 * Tuning.CraSubsidyWarn;
/// <summary>CRA 的预兆:外生螺旋的新闻已出,或补贴池告急(利率下调)。螺旋开始后不再算预兆。</summary>
public bool CraWarning(int day) => (CraWarnDay >= 0 && CraWarnDay <= day || CraSubsidyLow) && Cra >= 0.97;
public bool CraWarning(int day) => (CraWarnDay >= 0 && CraWarnDay <= day || CraSubsidyLow) && CraCrashDay < 0 && Cra >= 0.97;
public double CraRate => CraSubsidyLow ? Tuning.CraRateLow : Tuning.CraRate;
/// <summary>
/// 夜间结算。返回 CRC 持有量要乘的系数(减记兑现时为 0.95,否则为 1)。
/// playerStaked:玩家的 CRA 质押额,用来算补贴支出(玩家的利息由调用方按 <see cref="CraRate"/> 发放)。
/// playerStaked:玩家的 CRA 质押额,用来算利息带来的稀释(玩家的利息由调用方按 <see cref="CraRate"/> 发放)。
/// </summary>
public double Night(int day, Rng rng, double playerStaked)
{
@@ -79,21 +168,87 @@ namespace Decentralized.Domain
if (RevHistory.Count > 7) RevHistory.RemoveAt(0);
RevPower = Math.Max(0, Math.Min(100, RevPower + rng.Gauss() * 3));
if (CraSpiralDay < 0)
if (CraCrashDay < 0)
{
CraSubsidy -= (Tuning.CraNpcStake + playerStaked) * CraRate;
bool revFalling = RevHistory.Count == 7 && RevPower < RevHistory[0] - 5;
double weekly = Tuning.CraSpiralWeekly * (revFalling ? 3 : 1);
double daily = 1 - Math.Pow(1 - weekly, 1.0 / 7);
if (CraWarnDay < 0 && (rng.Chance(daily) || CraSubsidy <= 0))
double interest = (Tuning.CraNpcStake + playerStaked) * CraRate;
Supply += interest;
// 抵押率调整:价格低于 1 上调,回到 1 下调;兑付的 CRC 份额不超过储备均值(见 <see cref="Redeem"/>)
if (Cra < 0.995) C = Math.Min(1, C + Tuning.CraCrStep);
else if (Cra >= 0.999) C = Math.Max(Tuning.CraCrMin, C - Tuning.CraCrStep);
if (CraEventDay < 0)
{
CraWarnDay = day;
CraSpiralDay = day + rng.Range(Tuning.CraWarnLeadMin, Tuning.CraWarnLeadMax);
bool revFalling = RevHistory.Count == 7 && RevPower < RevHistory[0] - 5;
double weekly = Tuning.CraSpiralWeekly * (revFalling ? 3 : 1) * StoryMods.Current.CraSpiral(day);
double daily = 1 - Math.Pow(1 - weekly, 1.0 / 7);
if (rng.Chance(daily) || Cushion <= 0)
{
CraWarnDay = day;
CraEventDay = day + rng.Range(Tuning.CraWarnLeadMin, Tuning.CraWarnLeadMax);
}
}
else if (day >= CraEventDay)
{
// 挪用:储备被革命军拿走一部分,R/S 掉到崩盘线以下就是挤兑
Reserve *= 1 - rng.Range(Tuning.CraEmbezzleMin, Tuning.CraEmbezzleMax);
CraEventDay = -1; CraWarnDay = -1;
if (Ratio < Tuning.CraCrashRatio) CraCrashDay = day;
}
}
if (CraSpiralDay > 0 && day >= CraSpiralDay)
Cra = Tuning.CraSpiralPath[Math.Min(day - CraSpiralDay, Tuning.CraSpiralPath.Length - 1)];
if (CraCrashDay >= 0)
{
int k = Math.Min(day - CraCrashDay, Tuning.CraSpiralPath.Length - 1);
Cra = Tuning.CraSpiralPath[k];
if (SparkPre < 0) SparkPre = Spark;
Spark = SparkPre * Tuning.SparkSpiralPath[Math.Min(day - CraCrashDay, Tuning.SparkSpiralPath.Length - 1)];
}
else
{
Cra = PriceOfRatio(Ratio);
Spark *= Math.Exp(rng.Gauss() * Tuning.SparkVol) * StoryMods.Current.TakeSparkPush();
Spark += (Tuning.SparkStart - Spark) * Tuning.SparkRevert;
Spark = Math.Max(Tuning.SparkStart * Tuning.SparkMinMult, Math.Min(Tuning.SparkStart * Tuning.SparkMaxMult, Spark));
}
return crcFactor;
}
/// <summary>
/// 销毁一份 CRA 的兑付:CRC 份额 = min(申请时的 c, 储备均值 R/S),其余按现价用新增发的 SPARK 补。
/// 返回 (CRC 数量, SPARK 数量),并扣减储备和流通量。
/// </summary>
public void Redeem(double cra, double c, out double crc, out double spark)
{
double share = Math.Min(c, Ratio);
crc = Math.Min(Reserve, cra * share);
spark = Spark > 0 ? cra * (1 - crc / Math.Max(1e-9, cra)) / Spark : 0;
Reserve -= crc; Supply = Math.Max(0, Supply - cra);
}
/// <summary>
/// 夜间处理排队单:先对每一笔掷官方跑路(整笔丢失),再结算到期的。
/// 铸造到期:CRC 进储备、SPARK 销毁、流通量增加;销毁到期:按 <see cref="Redeem"/> 兑付。
/// </summary>
public List<CraOrderEvent> NightOrders(int day, Rng rng)
{
var evs = new List<CraOrderEvent>();
foreach (var o in Orders.ToList())
{
if (rng.Chance(RugChance)) { Orders.Remove(o); evs.Add(new CraOrderEvent { Order = o, Kind = CraOrderKind.Lost }); continue; }
if (day < o.Due) continue;
Orders.Remove(o);
var ev = new CraOrderEvent { Order = o };
if (o.Mint)
{
Supply += o.Cra; Reserve += o.Crc;
ev.Kind = CraOrderKind.Minted; ev.Cra = o.Cra;
}
else
{
Redeem(o.Cra, o.C, out ev.Crc, out ev.Spark);
ev.Kind = CraOrderKind.Burned;
}
evs.Add(ev);
}
return evs;
}
}
}
+93
View File
@@ -0,0 +1,93 @@
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>一个钱包在革命军质押池里的仓位:计息的本金、排队等解锁的部分,以及存入时带来的脏枚数与成本(利息是干净的)。</summary>
public sealed class StakeLot
{
public string Wallet;
public double Amount;
public List<KeyValuePair<int, double>> Unlocking = new List<KeyValuePair<int, double>>();
public double Dirty, Basis;
/// <summary>旧存档(数据版本 5 之前)的污点额(信用点),读档迁移时换成 <see cref="Dirty"/>。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
public double Total => Amount + Unlocking.Sum(u => u.Value);
}
/// <summary>一次到期或利息的结果,供夜间日志使用。</summary>
public sealed class StakeEvent
{
public string Wallet;
public double Interest, Released;
}
/// <summary>
/// 革命军 CRA 质押池:质押的 CRA 每晚按日息计息(利息并入本金),取出要先申请,等 <see cref="Tuning.CraUnlockDays"/> 天,期间不计息、
/// 也不能提前取。到期自动回到质押时的钱包。质押的币不会被冻结,但仓位里仍是 CRA:螺旋来临时到期取回的币只值当时的价格。
/// 污点与成本随币存入、按比例随币取出。
/// </summary>
public sealed class Staking
{
public List<StakeLot> Lots = new List<StakeLot>();
public StakeLot Of(string wallet) => Lots.Find(l => ChainAddress.Same(l.Wallet, wallet));
/// <summary>全部计息中的质押量,补贴池按它算支出。</summary>
public double Earning => Lots.Sum(l => l.Amount);
/// <summary>质押:从钱包取出 CRA,脏枚数按 CRA 的脏比例、成本按价值比例随币留在仓位里。余额不足返回 false。</summary>
public bool Stake(Wallet w, double amount, Func<string, double> price)
{
if (amount <= 0 || w.Avail(Assets.CRA) + 1e-9 < amount) return false;
double total = w.Value(price);
double share = total > 0 ? Math.Min(1, amount * price(Assets.CRA) / total) : 0;
double dirty = amount * w.DirtyRatio(Assets.CRA), basis = w.Basis * share;
w.Add(Assets.CRA, -amount); w.Basis -= basis;
var lot = Of(w.Address) ?? AddLot(w.Address);
lot.Amount += amount; lot.Dirty += dirty; lot.Basis += basis;
return true;
}
private StakeLot AddLot(string wallet) { var l = new StakeLot { Wallet = wallet }; Lots.Add(l); return l; }
/// <summary>申请取出:这部分从计息本金里挪到排队区,day + 解锁天数 到期。额度不足返回 false。</summary>
public bool RequestUnstake(string wallet, double amount, int day)
{
var lot = Of(wallet);
if (lot == null || amount <= 0 || lot.Amount + 1e-9 < amount) return false;
lot.Amount -= amount;
lot.Unlocking.Add(new KeyValuePair<int, double>(day + Tuning.CraUnlockDays, amount));
return true;
}
/// <summary>每晚:计息(日息 rate,利息并入本金),到期的部分回到钱包。返回有变化的仓位。</summary>
public List<StakeEvent> Night(int day, double rate, Func<string, Wallet> wallet)
{
var evs = new List<StakeEvent>();
foreach (var lot in Lots.ToList())
{
var ev = new StakeEvent { Wallet = lot.Wallet };
ev.Interest = lot.Amount * rate;
lot.Amount += ev.Interest;
foreach (var u in lot.Unlocking.Where(x => day >= x.Key).ToList())
{
double total = lot.Total;
double share = total > 0 ? Math.Min(1, u.Value / total) : 0;
double dirty = lot.Dirty * share, basis = lot.Basis * share;
lot.Dirty -= dirty; lot.Basis -= basis;
lot.Unlocking.Remove(u);
var w = wallet(lot.Wallet);
w.AddDirty(Assets.CRA, u.Value, dirty); w.Basis += basis;
ev.Released += u.Value;
}
if (lot.Total < 1e-9) Lots.Remove(lot);
if (ev.Interest > 0 || ev.Released > 0) evs.Add(ev);
}
return evs;
}
}
}
+261
View File
@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>
/// 游戏剧情选择对本 mod 世界的影响:一组倍率和期限,随存档保存。各处的概率读 <see cref="Current"/>(接入层读档时指向存档里的这一份)。
/// 每个选择只生效一次(Applied 记下键)。
/// </summary>
public sealed class StoryMods
{
/// <summary>当前生效的那一份。接入层读档时设成存档里的;测试里可以换。</summary>
public static StoryMods Current = new StoryMods();
public List<string> Applied = new List<string>();
/// <summary>CRC 脱锚、CRC 托管银行减记、CRT 脱锚、镇压时 CRT 被出卖的概率倍率(永久)。</summary>
public double CrcDepeg = 1, CrcHaircut = 1, CrtDepeg = 1, CrtFreeze = 1;
/// <summary>到这一天之前 CRA 螺旋概率减半。</summary>
public int CraSafeUntil = -1;
/// <summary>治安部整顿到这一天:混币器清洗变差,制裁概率、操纵查处概率上调。</summary>
public int SecUntil = -1;
/// <summary>伦警官被捕后的立威期到这一天:操纵查处概率、换币客户里便衣的概率上调。</summary>
public int LunUntil = -1;
/// <summary>SPARK 每晚额外乘的倍数,还剩几晚。</summary>
public double SparkPush = 1;
public int SparkPushNights;
/// <summary>混币器:手续费、匿名集(NPC 混币量)、跑路概率的倍率;被举报后永久关闭(在途的照常提回,不能再存)。</summary>
public double MixerFee = 1, MixerVolume = 1, MixerRug = 1;
public bool MixerClosed;
/// <summary>混币器换皮重开过(换了名字、地址);重开的是治安部的钓鱼混币器(提回的币一律被标记)。</summary>
public bool MixerRebranded, MixerHoneypot;
/// <summary>本 mod 自己的剧情线:混币器运营者 Tumbler。</summary>
public Tumbler Tumbler = new Tumbler();
/// <summary>Tumbler 四次上门之后的剧情:探员、泄露、审判、黑市跑路、报复、长期联系人、换皮重开。</summary>
public TumblerAfter After = new TumblerAfter();
public bool Has(string key) => Applied.Contains(key);
public double CraSpiral(int day) => day < CraSafeUntil ? Tuning.StoryCraSpiralMult : 1;
/// <summary>混币器每轮清洗率的倍率(治安部监控资金流向期间洗得更差)。</summary>
public double MixClean(int day) => day < SecUntil ? Tuning.StorySecMixClean : 1;
/// <summary>换币客户里便衣概率的倍率(伦警官被捕后的立威期)。</summary>
public double OtcSting(int day) => day < LunUntil ? Tuning.StoryLunMult : 1;
public double Sanction(int day) => day < SecUntil ? Tuning.StorySecSanctionMult : 1;
/// <summary>操纵检出、内幕查处的概率倍率。</summary>
public double Enforce(int day) => (day < SecUntil ? Tuning.StorySecEnforceMult : 1) * (day < LunUntil ? Tuning.StoryLunMult : 1);
/// <summary>每晚取 SPARK 的额外倍数(推完就是 1)。</summary>
public double TakeSparkPush()
{
if (SparkPushNights <= 0) return 1;
SparkPushNights--;
return SparkPush;
}
/// <summary>
/// 记下一个剧情选择并设置倍率。返回 false 表示已经生效过。CRA 补贴池的注资由接入层做(这里只管倍率)。
/// key:serpico_rev / serpico_bm / serpico_sec / eleanor_leak / eleanor_return / lun_arrested。
/// </summary>
public bool Apply(string key, int day)
{
if (Has(key)) return false;
Applied.Add(key);
switch (key)
{
case "serpico_rev":
CraSafeUntil = day + Tuning.StorySecDays;
PushSpark(Tuning.StorySerpicoSpark);
break;
case "serpico_bm":
CrtDepeg *= Tuning.StoryBmCrtMult; CrtFreeze *= Tuning.StoryBmCrtMult;
break;
case "serpico_sec":
SecUntil = day + Tuning.StorySecDays;
break;
case "eleanor_leak":
CrcHaircut *= Tuning.StoryLeakHaircutMult;
PushSpark(Tuning.StoryLeakSpark);
break;
case "eleanor_return":
CrcDepeg *= Tuning.StoryReturnCrcMult;
break;
case "lun_arrested":
LunUntil = day + Tuning.StoryLunDays;
break;
case "tumbler_kept":
MixerFee *= Tuning.TumblerKeptFee; MixerVolume *= Tuning.TumblerKeptVolume;
break;
case "tumbler_reported":
MixerClosed = true;
break;
case "tumbler_sold":
MixerFee *= Tuning.TumblerSoldFee; MixerRug *= Tuning.TumblerSoldRug;
break;
case "tumbler_bm_rug":
MixerClosed = true;
break;
}
return true;
}
/// <summary>混币器换皮重开:重新开放,费率、匿名集、跑路倍率回到 1;钓鱼混币器的费率打折(引人上钩)。</summary>
public void Rebrand(bool honeypot)
{
MixerClosed = false;
MixerRebranded = true;
MixerHoneypot = honeypot;
MixerFee = honeypot ? Tuning.TumblerHoneypotFee : 1;
MixerVolume = 1; MixerRug = 1;
}
/// <summary>SPARK 在接下来几晚里一共涨到 total 倍(每晚乘相同的倍数)。</summary>
private void PushSpark(double total)
{
int n = Tuning.StorySparkNights;
// 已经在推:把剩下的叠上去
double left = SparkPushNights > 0 ? Math.Pow(SparkPush, SparkPushNights) : 1;
SparkPush = Math.Pow(left * total, 1.0 / n);
SparkPushNights = n;
}
}
/// <summary>
/// 混币器运营者 Tumbler 的剧情线:解锁混币器、第 <see cref="Tuning.TumblerMinDay"/> 天起开始,四次上门。
/// 1 自我介绍 → 2 说治安部在追查混币器(当晚混币器被制裁)→ 3 求你藏密钥盘(三选一:藏 / 举报 / 卖给黑市)→ 4 结局。
/// 只记进度,对话和物品在接入层。
/// </summary>
public sealed class Tumbler
{
public const int Hide = 1, Report = 2, Sell = 3;
public int StartDay = -1, Stage, Choice, VisitedDay = -1;
public bool Done;
/// <summary>Tumbler 的外观(第一次上门时随机定下,之后每次都一样)。</summary>
public string Sprite;
/// <summary>第几次上门在哪一天(相对开始日)。</summary>
public static readonly int[] Offsets = { 0, 3, 5, 9 };
public int NextVisitDay => StartDay < 0 || Stage >= Offsets.Length ? -1 : StartDay + Offsets[Stage];
/// <summary>每天开门时问:是否开始(明天第一次上门)。</summary>
public bool TryStart(int day, bool mixerUnlocked)
{
if (StartDay >= 0 || Done || !mixerUnlocked || day < Tuning.TumblerMinDay) return false;
StartDay = day + 1;
return true;
}
/// <summary>
/// 今天该上门的是第几次(1–4),没有就返回 0(第 4 次举报过的话由接入层改成新闻:他被抓了)。
/// after 不为空时:第 3 次要等治安部探员来过(第二天起),配合过探员又选了藏的,第 4 次要等搜查过(第二天起)。
/// </summary>
public int Due(int day, TumblerAfter after = null)
{
if (Done || StartDay < 0 || Stage >= Offsets.Length || VisitedDay == day) return 0;
if (day < NextVisitDay) return 0;
if (after != null && Stage == 2 && (after.Agent == 0 || day <= after.AgentDay)) return 0;
if (after != null && Stage == 3 && after.NeedSearch(this) && (!after.Searched || day <= after.SearchDay)) return 0;
return Stage + 1;
}
/// <summary>这一次上门已经安排了。</summary>
public void Visited(int day)
{
VisitedDay = day;
Stage++;
Done = Stage >= Offsets.Length;
}
}
/// <summary>
/// Tumbler 四次上门之后的剧情(原来的进度类不动):
/// 治安部探员在第 2、3 次上门之间来一次(配合 / 否认);配合过又选了藏的,第 4 次前一天来搜查;
/// 举报、被搜走、第 4 次盘不在手上 → 密钥落到治安部,混币记录泄露;举报 → 审判盘;卖给黑市 → 黑市跑路;
/// 无罪获释、黑市跑路 → Tumbler 报复;藏好归还 → 长期联系人;混币器关停若干天后换皮重开。只记进度,对话、物品、夜间效果在接入层。
/// </summary>
public sealed class TumblerAfter
{
public const int Cooperate = 1, Deny = 2;
/// <summary>探员上门的结果(0 = 还没来)和那一天。</summary>
public int Agent, AgentDay = -1;
/// <summary>探员的外观(问话和搜查是同一个人)。</summary>
public string AgentSprite;
/// <summary>搜查过(不管有没有搜到)、搜走了密钥盘、哪一天。</summary>
public bool Searched, Seized;
public int SearchDay = -1;
/// <summary>哪一晚泄露(-1 = 没安排);已经泄露过(整局一次)。</summary>
public int LeakDay = -1;
public bool Leaked;
/// <summary>藏好并归还:成为长期联系人;已经提示过的镇压开始日。</summary>
public bool Contact;
public List<int> Tipped = new List<int>();
/// <summary>审判:开盘日、宣判日、判决(1 定罪,0 无罪,-1 没有审判);每晚的庭审新闻偏向(true = 控方占上风);最后一条新闻是哪晚。</summary>
public int TrialOpenDay = -1, TrialDay = -1, Verdict = -1, HintDay = -1;
public List<bool> Hints = new List<bool>();
public bool TrialMarket, TrialDone;
/// <summary>报复在哪一晚(-1 = 没有)、已经报复过。</summary>
public int RevengeDay = -1;
public bool Revenged;
/// <summary>黑市卷款跑路在哪一晚(-1 = 没有)、已经跑路。</summary>
public int BmRugDay = -1;
public bool BmRugged;
/// <summary>混币器关停的那天(-1 = 没关停);换皮重开过。</summary>
public int ClosedDay = -1;
public bool Rebranded;
/// <summary>探员今天该不该来:第 2 次上门之后的第二天起,还没来过。</summary>
public bool AgentDue(Tumbler t, int day) => !t.Done && t.Stage == 2 && Agent == 0 && day > t.VisitedDay;
/// <summary>配合过探员、又选了藏,第 4 次上门前要先来搜查。</summary>
public bool NeedSearch(Tumbler t) => Agent == Cooperate && t.Choice == Tumbler.Hide;
/// <summary>搜查今天该不该来:第 4 次上门的前一天起(第 3 次之后的第二天起)。</summary>
public bool SearchDue(Tumbler t, int day) => !t.Done && t.Stage == 3 && NeedSearch(t) && !Searched && day >= t.NextVisitDay - 1 && day > t.VisitedDay;
/// <summary>安排今晚泄露(整局一次)。</summary>
public void LeakTonight(int day)
{
if (!Leaked && LeakDay < 0) LeakDay = day;
}
/// <summary>举报:掷出判决,安排开盘和宣判;混币器从今天起关停。</summary>
public void StartTrial(int day, Rng rng)
{
if (Verdict >= 0) return;
Verdict = rng.Chance(Tuning.TumblerTrialConvict) ? 1 : 0;
TrialOpenDay = day + Tuning.TumblerTrialOpen;
TrialDay = day + Tuning.TumblerTrialDays;
ClosedDay = day;
}
/// <summary>今晚的庭审新闻(开盘前一晚起到宣判前一晚,每晚一条):偏向真实判决的概率 HintAcc。没有就返回 null。</summary>
public bool? Hint(int day, Rng rng)
{
if (Verdict < 0 || TrialDone || day < TrialOpenDay - 1 || day >= TrialDay || HintDay >= day) return null;
HintDay = day;
bool truth = Verdict == 1;
bool h = rng.Chance(Tuning.TumblerTrialHintAcc) ? truth : !truth;
Hints.Add(h);
return h;
}
/// <summary>公开信息下的定罪概率:先验按已经登出的庭审新闻做贝叶斯更新。</summary>
public double TrialPublic()
{
double acc = Tuning.TumblerTrialHintAcc, lr = Math.Log(acc / (1 - acc)), p0 = Tuning.TumblerTrialConvict;
double l = Math.Log(p0 / (1 - p0));
foreach (var h in Hints) l += h ? lr : -lr;
return 1 / (1 + Math.Exp(-l));
}
/// <summary>卖给黑市:安排黑市卷款跑路的那一晚。</summary>
public void StartSold(int day, Rng rng)
{
if (BmRugDay < 0) BmRugDay = day + rng.Range(Tuning.TumblerBmRugMin, Tuning.TumblerBmRugMax);
}
/// <summary>今晚该不该换皮重开:关停满 RebrandDays 天,只一次。</summary>
public bool RebrandDue(int day) => !Rebranded && ClosedDay >= 0 && day >= ClosedDay + Tuning.TumblerRebrandDays;
}
}
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>
/// 存档迁移(数据版本 5):旧存档的污点是一笔信用点(整个钱包、托管里各一笔),换成按币种记的脏枚数。
/// 钱包:按当前价格摊到各种可追踪的币上(<see cref="Wallet.MigrateTaint"/>);Poinbase 账户和永续仓位的污点先挪到 1 号钱包一起摊(超出的作废)。
/// 托管(混币器在途、黑市订单、质押、空投池、预测持仓、自营混币器本金):按当前价格换成那种币的脏枚数,不超过数量。旧字段清零。
/// </summary>
public static class TaintMigration
{
private static double Qty(Func<string, double> price, string asset, double amount, double legacy)
{
double p = price(asset);
return legacy > 0 && p > 0 ? Math.Min(Math.Max(0, amount), legacy / p) : 0;
}
/// <summary>迁移全部,返回旧存档里的污点总额(信用点,日志用)。</summary>
public static double Run(IList<Wallet> wallets, Wallet poinbase, IEnumerable<PerpPosition> perps, Mixer mixer, BlackMarket black,
Staking staking, Scams scams, PredictBook predict, OwnMixer own, Func<string, double> price)
{
double total = wallets.Sum(w => w.LegacyTaint) + poinbase.LegacyTaint + perps.Sum(p => p.LegacyTaint);
double extra = Exchange.TakeLegacyTaint(poinbase, perps);
for (int i = 0; i < wallets.Count; i++) wallets[i].MigrateTaint(price, i == 0 ? extra : 0);
foreach (var d in mixer.Pending) { total += d.LegacyTaint; d.Dirty += Qty(price, d.Asset, d.Amount, d.LegacyTaint); d.LegacyTaint = 0; }
foreach (var o in black.Orders) { total += o.LegacyTaint; o.Dirty += Qty(price, o.Asset, o.Qty, o.LegacyTaint); o.LegacyTaint = 0; }
foreach (var l in staking.Lots) { total += l.LegacyTaint; l.Dirty += Qty(price, Assets.CRA, l.Total, l.LegacyTaint); l.LegacyTaint = 0; }
foreach (var p in scams.Pools)
{
foreach (var kv in p.LegacyTaint)
{
total += kv.Value;
p.Dirty[kv.Key] = (p.Dirty.TryGetValue(kv.Key, out var d) ? d : 0) + Qty(price, Assets.CRC, p.Of(kv.Key), kv.Value);
}
p.LegacyTaint.Clear();
foreach (var q in p.Pending) { total += q.LegacyTaint; q.Dirty += Qty(price, Assets.CRC, q.Amount, q.LegacyTaint); q.LegacyTaint = 0; }
}
foreach (var p in predict.Positions) { total += p.LegacyTaint; p.Dirty += Qty(price, Assets.CRC, p.Cost * (1 + Tuning.PredictFee), p.LegacyTaint); p.LegacyTaint = 0; }
foreach (var kv in own.Pools) { total += kv.Value.LegacyCapTaint; kv.Value.CapDirty += Qty(price, kv.Key, kv.Value.Capital, kv.Value.LegacyCapTaint); kv.Value.LegacyCapTaint = 0; }
return total;
}
}
}
+322 -13
View File
@@ -5,25 +5,47 @@ namespace Decentralized.Domain
/// </summary>
public static class Tuning
{
/// <summary>人脉网络升级价格:Web3 治理社区(入口)、混币器。</summary>
public const int UnlockPrice = 400, MixerUnlockPrice = 300;
// 挖矿
public const double PscStartPrice = 100;
public const double BlockReward = 90;
/// <summary>第十一阶段把出块奖励和 NPC 初始算力同乘的倍数(100 → 1000、360 → 3600);读旧存档时按它放大全网。</summary>
public const double MiningScale = 10;
public const double BlockReward = 1000;
public const int HalvingDays = 90;
public const double NpcHash0 = 360;
/// <summary>减半对币价的传导:每次减半在区间内随机一个指数,价格锚上移 2^指数 倍(0.15–0.85 即约 +11% 到 +80%)。
/// 参考比特币:每次减半后的涨幅差别很大,市场也已部分预期到,所以既不固定也不翻倍。</summary>
public const double HalvingPassMin = 0.15, HalvingPassMax = 0.85;
/// <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 = 3600;
public const double NpcKjPerH = 2.4;
public const double ElecPrice = 1.5;
public const double NpcTargetRatio = 3.5;
public const double NpcAdj = 0.05;
public const double PoolFee = 0.03;
public const int RigPrice = 400, RigKj = 2;
/// <summary>矿架上模块百分比的折算系数(原版机器用 2–4),以及各项上限。</summary>
public const double ModuleRatio = 4, ModuleHashCap = 0.20, ModuleKjCap = 0.25, ModuleWearCap = 0.30;
/// <summary>矿架上模块百分比的折算系数(原版机器用 2–4)。模块效果不设上限,只有防出错的下限:算力、耗电、每日损耗三个乘数各自不低于下面的值。</summary>
public const double ModuleRatio = 4, ModuleHashFloor = 0, ModuleKjFloor = 0.2, ModuleWearFloor = 0;
public const double DeathBase = 0.001, DeathWorn = 0.03, FakeDeathFactor = 2;
public const double RefurbishHealth = 30;
// 市场
public const double PoolDepth0 = 50000;
public const double DexFee = 0.003, ExchangeFee = 0.002;
/// <summary>交易对信用点一侧的基础深度(DEX 侧池按比例取)。</summary>
public const double PoolDepth0 = 600000;
/// <summary>Poinbase 订单簿的冲击曲线:价格 = 参考价 × exp(κ · |x|^α),x = 当天净买入的名义价值 / 深度。第 38 天买 2 PSC 平均滑点约 0.1%,卖 200 PSC 约 4%。</summary>
public const double BookK = 1.54, BookAlpha = 0.8;
/// <summary>DEX 手续费;Poinbase 现货与出金的手续费(对照头部交易所 taker 约 0.1%)。</summary>
public const double DexFee = 0.003, ExchangeFee = 0.001;
/// <summary>DEX 稳定币对(CRT/CRC、CRA/CRC)的 StableSwap 池:手续费(参考 Curve 3pool 0.04%)、放大系数;
/// 参考价偏离挂钩超过 1% 是脱锚(流动性变差,A 降到 20,参考 USDC 2023 年 3 月最低约 0.87、两三天回到 1),超过 15% 是崩盘(流动性被撤走,A 降到 1、深度只剩四分之一)。</summary>
public const double StableFee = 0.0004, StableAmp = 200, StableAmpDepeg = 20, StableAmpBroken = 1, StableDepegBand = 0.01, StableBrokenBand = 0.15, StableBrokenDepth = 0.25;
/// <summary>NPC 大户每笔成交额(信用点)的范围。</summary>
public const double WhaleMin = 24000, WhaleMax = 96000;
public const double Revert = 0.15;
public const double PscVol = 0.04;
public const double SentimentKappa = 0.02, SentimentVol = 0.02;
@@ -32,7 +54,7 @@ namespace Decentralized.Domain
// 稳定币
public const double CrcDepegChance = 0.30, CrcDepegMin = 0.88, CrcDepegMax = 0.93;
public const int CrcDepegDaysMin = 2, CrcDepegDaysMax = 3;
public const double CrcHaircutChance = 0.15, CrcHaircut = 0.95;
public const double CrcHaircutChance = 0.15, CrcHaircut = 0.85;
public const double CrtDepegChance = 0.40, CrtDepegMin = 0.92, CrtDepegMax = 0.96;
public const int CrtDepegDaysMin = 1, CrtDepegDaysMax = 2;
public const double CrtFreezeBase = 0.40, CrtFreezePerRep = 0.008, CrtFreezeShare = 0.5;
@@ -40,20 +62,307 @@ namespace Decentralized.Domain
public const double CrtSwapFee = 0.02;
public const double CraRate = 0.005, CraRateLow = 0.002;
public const int CraUnlockDays = 4;
public const double CraSubsidy0 = 30000, CraSubsidyWarn = 0.2, CraNpcStake = 20000;
public const double CraSpiralWeekly = 0.02;
public const double CraSubsidyWarn = 0.2, CraNpcStake = 20000;
/// <summary>CRA 部分抵押:初始流通量与抵押率、抵押率下限与每晚调整步长;R/S 安全线、崩盘线、安全线到崩盘线之间的最大折价;
/// 挪用储备的比例范围;劫掠上层补回的储备(占流通量);一级市场铸造等待天数;官方跑路系数(× (1 − CRA 价格))。数值由 research/crasim 定。</summary>
public const double CraS0 = 60000, CraC0 = 0.8, CraCrMin = 0.5, CraCrStep = 0.02;
public const double CraSafeRatio = 0.6, CraCrashRatio = 0.35, CraDiscountMax = 0.15;
public const double CraEmbezzleMin = 0.4, CraEmbezzleMax = 0.8, CraLoot = 0.09;
public const int CraMintDaysMin = 1, CraMintDaysMax = 2;
public const double CraRugPerDepeg = 1;
/// <summary>Poinbase 出金到钱包的确认动画:基础秒数,加每个确认的秒数(确认数同 PLEDGER 的 TxConfirm*)。</summary>
public const double FundsBaseSecs = 1.2, FundsConfSecs = 0.6;
/// <summary>浏览器账本最多保留的转账条数,一次最多显示的条数。</summary>
public const int LedgerMax = 500, LedgerShow = 12;
public const double CraSpiralWeekly = 0.03;
public const int CraWarnLeadMin = 4, CraWarnLeadMax = 7;
public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 };
/// <summary>平衡代币 SPARK:起始价与长期锚(信用点)、日波动率、每晚向锚回归的比例、价格上下限(锚的倍数);铸销手续费;
/// 革命军袭击上层 / 下层动荡开始时的冲击;CRA 死亡螺旋三天里相对螺旋前价格的倍数。</summary>
public const double SparkStart = 10, SparkVol = 0.08, SparkRevert = 0.03, SparkMinMult = 0.3, SparkMaxMult = 3, SparkFee = 0.003;
public const double SparkRaidShock = 1.15, SparkUnrestShock = 0.90;
public static readonly double[] SparkSpiralPath = { 0.8, 0.3, 0.05 };
// 合规出金与 AML(阈值以当前房租为单位)
public const double AmlBigRents = 5, AmlCumRents = 15;
public const int AmlWindowDays = 7;
public const double AmlTaintRatio = 0.10, AmlHeavyRatio = 0.30, AmlMixerBonus = 0.30, AmlMaxChance = 0.9;
/// <summary>出金到银行:超出成本的部分征税。</summary>
public const double TaxRate = 0.08;
/// <summary>混币器:每轮手续费、基准清洗率、NPC 背景量(信用点)、最大轮数、最小存入、制裁概率与天数、每晚跑路概率。</summary>
public const double MixerFee = 0.03, MixerCleanRate = 0.35, MixerNpcVolume = 20000, MixerMin = 100, MixerSanctionChance = 0.5, MixerRugChance = 0.004;
public const int MixerMaxRounds = 5, MixerSanctionDays = 5;
/// <summary>各派系顾客用币付款的概率。</summary>
public const double CoinPayCrc = 0.30, CoinPayCrt = 0.50, CoinPayCra = 0.50, CoinPayPsc = 0.25;
/// <summary>用币付款的溢价:顾客多付的比例,补偿持币的污点与兑出的手续费。</summary>
public const double CoinPayPremium = 0.12;
/// <summary>顾客验货:说法里每一项隐瞒被看穿的概率;型号档次不符(供电口、风扇数一眼可见)必被看穿;一次看穿最多扣的信誉。</summary>
public const double CustomerSpotChance = 0.50, CustomerSpotTierMismatch = 1.0;
public const int RepLossMax = 25;
/// <summary>杰克逊博士上货:进阶 / 旗舰货的起始天(有特殊进货渠道时旗舰第 20 天起),矿架补货、进阶货(及第二件)、旗舰货的概率。</summary>
public const int DealerAdvancedFromDay = 10, DealerFlagshipFromDay = 30, DealerSpecialFlagshipFromDay = 20;
public const double DealerRigRestock = 0.30, DealerAdvanced = 0.60, DealerAdvancedSecond = 0.30, DealerFlagship = 0.25;
/// <summary>二手贩子:每天来访概率与最早来访日;骗子占比 = 基础 + 每天增量,封顶;中间商占比;中间商加价;中间商 / 骗子夹带假卡的比例;
/// 中间商把每项问题说成没问题的概率(骗子全说)。</summary>
public const double UsedVisitChance = 0.05, UsedScamBase = 0.10, UsedScamPerDay = 0.004, UsedScamMax = 0.30, UsedMiddlemanShare = 0.30;
public const double UsedMiddlemanMarkup = 0.08, UsedFakeMiddleman = 0.10, UsedFakeScammer = 0.45, UsedMiddlemanHide = 0.70;
public const int UsedFirstDay = 3;
/// <summary>显卡价值 = 型号定价 ×(下限 + (1 − 下限) × 健康度)。</summary>
public const double CardValueFloor = 0.25;
/// <summary>假卡的四项真伪依据(风扇数、供电口、背面丝印、序列号)各自露出破绽的概率(至少露出一项);约对应平均 1.5 项。</summary>
public const double FakeSignChance = 0.27;
/// <summary>黑市、鉴定等处二手卡的健康度范围(均匀取)。</summary>
public const double CardHealthMin = 25, CardHealthMax = 85;
/// <summary>二手贩子带来的卡的健康度范围,按老实人、中间商、骗子:越不老实卡越旧,平均每张卡约 0.5 / 1.5 / 2.5 项成色问题(叶片、金手指、挡板、封条)。</summary>
public static readonly double[] UsedHealthMin = { 72, 55, 42 }, UsedHealthMax = { 97, 85, 72 };
/// <summary>外观折价(占价值的比例):叶片积灰 / 缺角,金手指发暗 / 氧化,挡板划痕,封条缺失。</summary>
public const double CardFanDust = 0.04, CardFanChipped = 0.12, CardFingersDark = 0.04, CardFingersOxidized = 0.12, CardBracketScratch = 0.02, CardSealMissing = 0.06;
/// <summary>非主交易对每晚被拉回参考价的比例,及其深度占主交易对的比例。</summary>
public const double SideArbRate = 0.75, SideDepthShare = 0.3, SparkDepthShare = 0.15;
/// <summary>Poinbase 的 SPARK/CRC 订单簿深度占主池深度的比例。</summary>
public const double PoinSparkDepthShare = 0.5;
/// <summary>Web3 黑市:开放所需黑市声望,中 / 高档货所需声望,现货每周刷新的天数与基础件数(每高 BmStockPerRep 声望多一件),现货溢价。</summary>
public const int BmOpenRep = 10, BmMidRep = 40, BmHighRep = 80, BmRefreshDays = 7, BmStockBase = 4;
public const double BmStockPerRep = 40, BmPremium = 0.30;
/// <summary>Ponero 兑换处:开放所需黑市声望、每次兑入 / 兑出的手续费、每天可兑换的额度(信用点)、对 PSC 的价格比。</summary>
public const int PoneroOpenRep = 30;
public const double PoneroFee = 0.06, PoneroDailyCap = 1000, PoneroPscRatio = 0.5;
/// <summary>黑市订单:订单价相对游戏基础价的折扣、托管费、提前放款再折扣、提前放款所需卖家成交数、上门的天数、
/// 上门被截获的概率(镇压期间再加)、骗子卖家占比、每晚平台跑路基础概率与每万信用点托管资金加的概率(封顶)、跑路后关闭的天数。</summary>
public const double BmEscrowFee = 0.05, BmAdvanceDiscount = 0.10, BmInterceptDoor = 0.08, BmInterceptCrackdown = 0.15;
public const double BmScamSellerShare = 0.15, BmRugBase = 0.002, BmRugPerTenK = 0.004, BmRugMax = 0.05;
public const int BmAdvanceMinTrades = 30, BmDoorDays = 3, BmRugClosedDays = 10;
/// <summary>蜜罐:每晚平台被执法接管的基础概率、每点治安声望加的概率、封顶、持续天数;之后订单被追查的延迟天数;
/// 订单被截获时变成受控交付的概率;每晚某个近期卖家落网的概率。</summary>
public const double BmHoneypotBase = 0.01, BmHoneypotPerSecRep = 0.0004, BmHoneypotMax = 0.06, BmControlled = 0.35, BmSellerBust = 0.008;
public const int BmHoneypotDays = 6, BmTrapDelay = 4;
/// <summary>商家货架的随机细节:现货每种的库存上限、每件价格的随机抖动(±)、全新矿卡的库存比常规少多少;尖端店铺里卖显卡的概率与显卡档次下限;
/// 个人卖家里卖显卡的概率与其中全新的比例;骗子 / 诚实卖家是新号(成交少、好评随意)的概率;新号与老号的成交数、好评范围;论坛点名骗子的概率;
/// 托管资金推高跑路概率超过基础的几倍时论坛出现"提币变慢"。</summary>
public const int BmSpotQtyMax = 3, BmNewGpuQtyMinCut = 1, BmNewGpuQtyMaxCut = 2, BmEliteGpuTier = 3;
public const double BmPriceJitter = 0.03, BmEliteGpuChance = 0.4, BmPersonalGpuChance = 0.15, BmPersonalGpuNew = 0.3, BmScamFresh = 0.6, BmHonestFresh = 0.25, BmForumScamHint = 0.5, BmForumRugFactor = 3;
public const int BmFreshTradesMax = 12, BmOldTradesMin = 25, BmOldTradesMax = 220, BmScamOldRatingMin = 94, BmScamOldRatingMax = 99, BmFreshRatingMin = 70, BmFreshRatingMax = 100, BmOldRatingMin = 90, BmOldRatingMax = 99;
/// <summary>追查记罪证:记入的比例(货款 × 比例)、同一次蜜罐(或一次落网)累计最多记多少。</summary>
public const double BmTrapShare = 0.4, BmTrapWindowCap = 400;
/// <summary>货架上除违禁品外的两类货:高科技物品(需中档声望)和显卡的出现概率、加价(在现货溢价 / 订单价之外再加),显卡里夹假卡的比例、是全新卡(成色 100、封条完好、不会是假卡)的概率。</summary>
public const double BmTechChance = 0.30, BmGpuChance = 0.30, BmTechMarkup = 0.5, BmGpuMarkup = 0.2, BmGpuFake = 0.20, BmGpuNew = 0.15;
/// <summary>玩家上架出售:标价倍率范围(相对参考价)、上架费、成交抽成、成交后货款托管天数、假卡被买家看穿的概率;
/// 倍率 ≤0.9 / 1.0 / 1.8 时的期望成交天数(之间插值);货款带污点的概率与比例随倍率从下限涨到上限;卖家好评的初始值、看穿一次扣的点数、同时上架的件数上限。</summary>
public const double BmListFee = 0.05, BmCommission = 0.08, BmSellMin = 0.6, BmSellMax = 1.8, BmFakeDetect = 0.5;
/// <summary>出售成交天数的倍率界线:不高于 BmSellFastMult 倍用 BmSellDaysFast 天,BmSellDay1Mult 倍用 BmSellDays1 天,之间线性,再往上指数到 BmSellMax。</summary>
public const double BmSellFastMult = 0.9, BmSellDay1Mult = 1.0;
/// <summary>出售页可选的标价倍率档位。</summary>
public static readonly double[] BmSellMults = { 0.6, 0.8, 0.9, 1.0, 1.2, 1.5, 1.8 };
public const double BmSellDaysFast = 1, BmSellDays1 = 2, BmSellDaysMax = 14;
public const double BmSaleTaintChanceMin = 0.2, BmSaleTaintChanceMax = 0.9, BmSaleTaintRatioMin = 0.2, BmSaleTaintRatioMax = 0.8;
public const int BmSaleHold = 3, BmRatingStart = 90, BmRatingFakeLoss = 10;
/// <summary>同时上架的货:最多几种(按商品标识),每种最多几件。</summary>
public const int BmMaxKinds = 6, BmMaxStack = 5;
/// <summary>收款币的买家权重:每接受一种币,夜间多一轮成交判定,轮次概率 = 成交概率 × 权重(CRT 为 1 即原有节奏)。</summary>
public const double BmWeightPsc = 0.8, BmWeightCrt = 1, BmWeightCra = 0.6, BmWeightPonero = 0.4;
/// <summary>PLEDGER 区块高度每晚自然增长的范围(区块数,含两端)。</summary>
public const int BlocksPerDayMin = 120, BlocksPerDayMax = 170;
/// <summary>商家货架:每周的商家数(其中 2 家从矿卡 / 矿架 / 模组 4 类里抽,其余从其他类型里抽)、每家的货种数、每种货的库存范围;大宗商家出现概率、库存范围、送货天数;
/// 各类商家的价格系数范围(订单价 = 基础价 × 系数);批量折扣档(件数、折扣)。</summary>
public const int BmMerchantCount = 7, BmCoreMerchants = 2, BmMerchantListings = 3, BmQtyMin = 2, BmQtyMax = 6, BmBulkQtyMin = 10, BmBulkQtyMax = 25, BmBulkDays = 6;
public const double BmBulkChance = 0.25, BmFactorMin = 0.70, BmFactorMax = 0.95, BmFactorNewMin = 0.85, BmFactorNewMax = 1.00, BmFactorUsedMin = 0.65, BmFactorUsedMax = 0.90, BmFactorBulkMin = 0.55, BmFactorBulkMax = 0.75;
/// <summary>商家货架上单件基础价的上限(过贵的货不上,免得一件把周转量撑歪)。</summary>
public const long BmMaxUnitValue = 4000;
/// <summary>尖端店铺:每周出现概率、货种数、库存范围、单件基础价下限、价格系数范围、送货天数。</summary>
public const double BmEliteChance = 0.15, BmFactorEliteMin = 0.85, BmFactorEliteMax = 1.00;
public const int BmEliteListings = 2, BmEliteQtyMax = 2, BmEliteDays = 5;
public const long BmEliteMinValue = 2000;
/// <summary>个人卖家:每周家数、货种数、库存上限、每件的价格系数范围(各件独立随机)、是骗子的概率、混进尖端货的概率。</summary>
public const int BmPersonalCount = 2, BmPersonalListings = 3, BmPersonalQtyMax = 2;
public const double BmPersonalFactorMin = 0.50, BmPersonalFactorMax = 1.20, BmPersonalScam = 0.25, BmPersonalEliteShare = 0.10;
public const int BmBulk1Count = 5, BmBulk2Count = 10;
public const double BmBulk1Discount = 0.05, BmBulk2Discount = 0.10;
/// <summary>出售量越大货款越容易带污点:近 7 天成交额达到参考量时,污点概率与比例各加最多 BmVolumeTaintMax。</summary>
public const double BmVolumeRef = 14000, BmVolumeTaintMax = 0.30;
/// <summary>黑市人脉升级的价格。</summary>
public const int BmUnlockPrice = 600;
/// <summary>单个金融违规事件记入的罪证上限。</summary>
public const double CrimeEventCap = 400;
/// <summary>硬件钱包:最多几个(含主钱包);杰克逊博士的售价与每次进货上架的概率;Web3 商城的价格范围、每周平台现货上架的概率与件数上限;商城货里被投毒的比例。</summary>
public const int WalletMax = 5, WalletJacksonPrice = 240, WalletMallMin = 150, WalletMallMax = 180, WalletMallQtyMax = 2;
public const double WalletJacksonChance = 0.5, WalletMallChance = 0.5, WalletPoisonShare = 0.2;
/// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:期望天数从门槛处的 SlowDays(7 天)按价值线性降到 DrainFull 处的 1 天,每晚概率 = 1 / 期望天数。</summary>
public const double WalletDrainMin = 3000, WalletDrainFull = 20000, WalletDrainSlowDays = 7;
/// <summary>转账手续费(信用点等值,按转出的币扣):慢 / 普通 / 快;拥堵时乘以倍数。</summary>
public const double TxFeeSlow = 0.5, TxFeeNormal = 1.5, TxFeeFast = 4, TxCongestedFeeMult = 4;
/// <summary>Gas 档位的确认速度倍数(时间乘它):Gas 越高确认越快、进度条走得越快;Poinbase 出金固定普通档。</summary>
public const double TxSpeedSlow = 1.5, TxSpeedNormal = 1, TxSpeedFast = 0.5;
/// <summary>PLEDGER 按住 ✓ 签名的基础秒数(普通档),乘档位速度倍数:快档更短、慢档更长。</summary>
public const double TxHoldSecs = 1.2;
/// <summary>Gas 价格指数:以 GasPeriod 天为周期正弦涨跌,振幅 GasAmp(0.5×–1.5×),每天再乘 1 ± GasNoise 的随机波动。</summary>
public const int TxGasPeriod = 7;
public const double TxGasAmp = 0.5, TxGasNoise = 0.15;
/// <summary>网络拥堵:每晚开始拥堵的概率、持续天数上限;拥堵时普通档、慢档的确认间隔倍数,慢档被挤出内存池的概率。</summary>
public const double TxCongestionChance = 0.12, TxCongestedNormalSlow = 2, TxCongestedSlowSlow = 3.5, TxSlowDrop = 0.3;
public const int TxCongestionDaysMax = 2;
/// <summary>确认数:默认 3;金额(信用点)≥ MidValue 时 6,≥ BigValue 时 12。</summary>
public const int TxConfirmBase = 3, TxConfirmMid = 6, TxConfirmBig = 12;
public const double TxConfirmMidValue = 2000, TxConfirmBigValue = 10000;
/// <summary>地址投毒:每笔转出后被盯上的概率,0 币转入在 1–2 天后出现。剪贴板木马(被投毒的钱包):粘贴时换成相似地址的概率。钓鱼公告:每晚出现的概率。</summary>
public const double TxPoisonChance = 0.35, TxHijackChance = 0.3, TxPhishChance = 0.05;
public const int TxPoisonDelayMax = 2;
/// <summary>永续合约:开平仓手续费、强平费、维持保证金比例;保险基金初值(约 20 天的全网产出)、手续费进基金的比例;
/// 基金见底时盈利仓位自动减仓的盈利折扣;资金费系数与上下限(每晚);欠款日息、没收宽限天数、核销后拉黑天数;两个品种的最高杠杆与单仓名义价值上限。</summary>
public const double PerpFee = 0.001, PerpLiqFee = 0.02, PerpMaint = 0.10;
public const double PerpFund0 = 200000, PerpFeeToFund = 0.3, PerpAdlHaircut = 0.5;
/// <summary>NPC 永续仓位(每晚按标记价的涨跌结算,进出保险基金):亏损一侧的持仓占现货池深的倍数(SPARK 再乘 SparkDepthShare);
/// 涨跌幅达到 PerpNpcLiqScale 时亏损一侧全部爆仓,之前按比例爆仓;爆仓里穿仓的比例 = 基础 + 斜率 × (涨跌幅 − 免穿仓幅度),封顶;
/// 正常爆仓交给基金的剩余保证金(占名义价值);穿仓的缺口(占名义价值)= 涨跌幅 × 系数。</summary>
public const double PerpNpcOi = 1.0, PerpNpcLiqScale = 0.25, PerpNpcBankBase = 0.05, PerpNpcBankSlope = 1.5, PerpNpcBankFree = 0.12, PerpNpcBankMax = 0.95;
public const double PerpNpcLeft = 0.005, PerpNpcGap = 0.3;
/// <summary>爆仓新闻:标记价单日涨跌达到多少上报纸;NPC 平均每个仓位的名义价值(折算人数)。</summary>
public const double PerpNewsMove = 0.15, PerpNpcAvgPos = 1500;
public const double PerpFundCoef = 0.10, PerpFundCap = 0.003;
/// <summary>永续的成交深度占现货主池的比例:开平仓的平均滑点 = 名义价值 / (深度倍数 × 池深),和现货一样大单更贵。</summary>
public const double PerpDepthMult = 5.0;
/// <summary>永续成交深度的基数(与现货分开;原来 50,000,减半滑点后 100,000,校准见 research/perpsim)。</summary>
public const double PerpDepth0 = 100000;
/// <summary>开仓时按现价算权益至少要高出强平线多少(占名义价值的比例),否则拒绝(滑点太大,开了就被强平)。</summary>
public const double PerpOpenBuffer = 0.02;
public const double PerpDebtInterest = 0.02;
public const int PerpDebtDays = 7, PerpBanDays = 30;
public const int PerpPscLev = 5, PerpSparkLev = 3;
public const double PerpPscCap = 20000, PerpSparkCap = 5000;
/// <summary>RatDigg 矿机(ASIC):黑市矿机商每周出现的概率;M1 / M2 / M3 需要的黑市声望与出现权重;新机占比与二手的健康度范围。</summary>
public const double AsicChance = 0.25, AsicNewShare = 0.7;
public const int AsicRepM1 = 40, AsicRepM2 = 60, AsicRepM3 = 80;
public const double AsicWeightM1 = 1.0, AsicWeightM2 = 0.6, AsicWeightM3 = 0.4;
public const int AsicUsedHealthMin = 40, AsicUsedHealthMax = 85;
/// <summary>修好之后健康扣的点数;损坏的机器剩余价值占机价的比例。</summary>
public const int AsicRepairHealthCut = 20;
public const double AsicDeadValue = 0.15;
/// <summary>"矿机时代":第几天到第几天之间随机发生一次(永久),提前几天上报纸;NPC 矿工的耗电乘数(耗电效率翻倍 = 0.5)。</summary>
public const int EraMin = 50, EraMax = 90, EraWarnMin = 4, EraWarnMax = 7;
public const double EraKjMult = 0.5;
/// <summary>预测市场:游戏第几天起开放;手续费;单盘额度 = 基础 + 每天增量 × 天数;所有钱包在单盘合计的份额上限(额度的倍数);
/// 同时开放的盘口数量范围、每天最多新开几个;按信用点额折诈骗罪证的比例与单次上限(永续被判操纵、Tumbler 泄露用);
/// 派系行动的日均开始概率(先验);先验概率的夹紧范围;结算记录保留条数。</summary>
public const int PredictUnlockDay = 8;
public const double PredictFee = 0.01, PredictLoss0 = 800, PredictLossPerDay = 25, PredictCap = 2;
/// <summary>预测盘的流动性:LMSR 的 b = 倍数 × 单盘额度 / ln 2,与先验无关(低先验盘不再更浅)。单盘额度只决定份额上限和消息价。</summary>
public const double PredictDepth = 1.8;
public const int PredictOpenMin = 3, PredictOpenMax = 6, PredictNewPerDay = 2;
public const double PredictFraudShare = 0.5, PredictFraudCap = 400;
public const double PredictFactionDaily = 0.06, PredictPMin = 0.03, PredictPMax = 0.97;
public const int PredictHistory = 8;
/// <summary>gas 盘的公开价里"不知道相位"的权重:越大,数清七天周期的玩家越赚。金融审查(agy_fin/subsim)发现 0.5 时 gas 盘胜率 96%、每周净赚约 1,400,调到 0.15。</summary>
public const double PredictGasBlind = 0.15;
/// <summary>盘口对消息的反应:每晚向"公开信息下的合理概率"靠拢的比例,以及盘口 logit 上的随机噪声(NPC 流量)。</summary>
public const double PredictDrift = 0.5, PredictNoise = 0.35;
/// <summary>夜里价格变化超过多少(概率)写进晨间日志。</summary>
public const double PredictMoveLog = 0.15;
/// <summary>情报贩子:局势简报价格(CRT)、三档内幕消息的价格(占盘口最大亏损的比例)与准确率、假情报概率(claim 与真相相反)、需要的黑市声望。</summary>
public const double IntelBriefPrice = 120, IntelFake = 0.10;
public static readonly double[] IntelTipPrice = { 0.02, 0.05, 0.12 }, IntelTipAcc = { 0.60, 0.80, 0.95 };
public const int IntelRep = 20;
/// <summary>操纵检出:玩家自己的大单把价格推离"没有你时的水平"的对数幅度(取绝对值)低于 ManipFree 不查;
/// 超过后被查概率 = ManipBase + ManipSlope × (幅度 − ManipFree),封顶 ManipMax;被查后照常兑付,赚到的钱记污点。</summary>
public const double ManipFree = 0.16, ManipBase = 0.25, ManipSlope = 3, ManipMax = 0.95;
/// <summary>高息理财(庞氏):第几天起开放、开张的宣传费(CRC)、可选日息、每晚新存入的基数(随天数)和对日息的敏感指数、
/// 池子上限(随天数)、每晚被取走的比例、每晚被举报概率(基础 + 日息系数 + 池子占上限的比例系数)、卷走或崩盘时记的罪证。</summary>
public const int PonziMinDay = 15;
public const double PonziAdCost = 800;
public static readonly double[] PonziRates = { 0.02, 0.04, 0.06, 0.08, 0.10 };
public const double PonziBase0 = 300, PonziBasePerDay = 15, PonziRateExp = 1.5;
public const double PonziCap0 = 6000, PonziCapPerDay = 200, PonziWithdraw = 0.03;
public const double PonziReport0 = 0.02, PonziReportRate = 0.5, PonziReportSize = 0.04;
public const double PonziCrime = 400;
/// <summary>插针:每天开门时出现的概率、幅度范围(对数价格的绝对值)、当晚回去的比例范围、不回去(真跌真涨)的概率。</summary>
public const double WickChance = 0.07, WickMin = 0.15, WickMax = 0.30, WickRevertMin = 0.6, WickRevertMax = 1.0, WickReal = 0.3;
/// <summary>空投:每晚出现新空投的概率、有效天数、骗局占比、正规空投奖励范围(信用点等值)、钓鱼授权后被转走的延迟(晚)。</summary>
public const double AirdropChance = 0.35, AirdropScam = 0.4, AirdropRewardMin = 20, AirdropRewardMax = 120;
public const int AirdropDays = 2, AirdropDrainMin = 1, AirdropDrainMax = 2;
/// <summary>第三方收益池:同时最多几个、每晚新开的概率;日息档位;按日息的骗局概率,时间锁、审计各减多少;骗局池每晚跑路概率;
/// 单池存款上限(随天数);正规池的储备(日息 × 存款上限 × 天数);时间锁取款等待天数。</summary>
public const int YieldPoolMax = 3, YieldLockDays = 2;
public const double YieldPoolChance = 0.25, YieldRugNightly = 0.18, YieldCap0 = 4000, YieldCapPerDay = 120, YieldReserveDays = 25;
public static readonly double[] YieldRates = { 0.01, 0.02, 0.03 }, YieldScamByRate = { 0.15, 0.40, 0.75 };
public const double YieldScamTimelock = 0.25, YieldScamAudit = 0.20;
/// <summary>换币客户(场外换币台):人脉升级价格;来卖币的比例(其余来买币)。</summary>
public const int OtcUnlockPrice = 500;
public const double OtcSellShare = 0.5;
/// <summary>卖币客户:有相关事件时出手事件里来的币(全额污点)的概率,Ponero 兑换处开放后出手 Ponero 的概率;币的市值(房租的倍数);
/// 其他币的污点比例范围;现金价占市值的比例(污点比例 0 时、1 时,之间线性;Ponero 单独一档);便衣的概率与镇压期间的加成;客户地址来历的最多笔数。</summary>
public const double OtcEventChance = 0.7, OtcPoneroChance = 0.15, OtcValueMinRents = 4, OtcValueMaxRents = 15;
public const double OtcTaintMin = 0.2, OtcTaintMax = 0.9, OtcCutClean = 0.80, OtcCutDirty = 0.45, OtcPoneroCutMin = 0.70, OtcPoneroCutMax = 0.80;
public const double OtcSting = 0.05, OtcStingCrackdown = 0.10;
public const int OtcHistoryMax = 3;
/// <summary>买币客户:要买的币的市值(房租的倍数);出价相对市价的随机溢价范围(可能低于市价,成不成交由玩家决定)。</summary>
public const double OtcBuyMinRents = 2, OtcBuyMaxRents = 8;
public const double OtcBidMin = -0.05, OtcBidMax = 0.30;
/// <summary>
/// 自营混币器(买下场外换币台就开放):默认费率、可选费率;口碑(近期出款剩下的污点比例)跟出款走的速度、客户把剩下的污点比例折成费率的权重、Tumbler 关停时客户不混的额外成本、匿名集的半饱和值(每轮清洗率 = 基准 × 匿名集 /(匿名集 + 它))、平均轮数、客户在途的币计入匿名集的比例;
/// 基础需求(信用点,随天数增长)、黑市声望 0 时的需求系数、客流对成本差的灵敏度、每晚需求的随机幅度、单笔存入的平均额;
/// 客户存入的币的污点比例范围;各币种的需求权重(PSC、CRC、CRT、CRA);营业时每晚有便衣存入的概率和记罪证的比例(按一笔存入的额);
/// 风声每晚保留的比例和累积的标度、池子越大越难追查的半值(风声累积 = 存入 / 标度 / (1 + 匿名集 / 它))、按风声的查封概率系数与上限、镇压期间的倍数、查封记罪证的比例、停业天数、风声提示的两档界线;卷款跑路记的诈骗罪证和黑市声望损失。
/// </summary>
public const double OwnMixFeeDefault = 0.03;
public static readonly double[] OwnMixFees = { 0.01, 0.02, 0.03, 0.05, 0.08, 0.12 };
public const double OwnMixRatingSpeed = 0.2, OwnMixTaintWeight = 0.1, OwnMixRivalGone = 0.03, OwnMixAnon0 = 20000, OwnMixAvgRounds = 3, OwnMixPendingWeight = 0.25;
public const double OwnMixDemand0 = 40000, OwnMixDemandPerDay = 1400, OwnMixRepMin = 0.3, OwnMixFeeScale = 0.015, OwnMixNoise = 0.3, OwnMixTicket = 800;
public const double OwnMixInTaintMin = 0.4, OwnMixInTaintMax = 1.0;
public static readonly double[] OwnMixAssetWeights = { 0.3, 0.1, 0.4, 0.2 };
public const double OwnMixSting = 0.03, OwnMixStingCrime = 0.1;
public const double OwnMixHeatKeep = 0.9, OwnMixHeatScale = 100000, OwnMixHeatAnon0 = 160000, OwnMixSeizeRate = 0.002, OwnMixSeizeMax = 0.5, OwnMixSeizeCrackdown = 2, OwnMixSeizeCrime = 0.01, OwnMixHeatMid = 0.01, OwnMixHeatHigh = 0.03;
public const int OwnMixSeizeDays = 7, OwnMixRugRep = 25;
public const double OwnMixRugCrime = 400;
/// <summary>假客服:每天上门的概率;伪造转账:用币付款的顾客里是骗子的比例;两者都从第几天起。</summary>
public const double FakeSupportChance = 0.04, FakeTransferShare = 0.08;
public const int ScamMinDay = 10;
/// <summary>Web3 内置自动换币:商家不收手里的币时,按这个比例多付(DEX 手续费 0.3% 加上高滑点),省得自己去换。</summary>
public const double BmAutoSwapSlip = 0.08;
/// <summary>剧情联动:塞尔皮科证据交给革命军(CRA 补贴池注资、螺旋概率倍率与期限、SPARK 几晚内涨到的倍数)、交给黑市(CRT 脱锚与被出卖的倍率)、
/// 交给治安部(期限内混币器每轮清洗率、制裁、操纵查处的倍率);埃莉诺泄露数据盘(CRC 托管减记倍率、SPARK 涨幅)、交回(CRC 脱锚倍率);
/// 伦警官被捕(期限与倍率:操纵查处、换币客户里的便衣);SPARK 推涨分几晚走完。</summary>
public const double StoryRevSubsidy = 10000, StoryCraSpiralMult = 0.5, StorySerpicoSpark = 1.2;
public const double StoryBmCrtMult = 0.5;
public const int StorySecDays = 30;
public const double StorySecMixClean = 0.7, StorySecSanctionMult = 1.5, StorySecEnforceMult = 1.3;
public const double StoryLeakHaircutMult = 2, StoryLeakSpark = 1.15, StoryReturnCrcMult = 0.5;
public const int StoryLunDays = 14, StorySparkNights = 5;
/// <summary>混币器运营者 Tumbler:第几天起开始;帮他藏好密钥盘后的混币器手续费、匿名集倍率和谢礼(CRT);举报的赏金(信用点)和治安部声望;
/// 卖给黑市得的 CRT(一半带污点)、黑市声望、之后混币器的手续费和跑路倍率;密钥盘的价值(违禁品,搜到按它记罪证)。</summary>
public const int TumblerMinDay = 30;
public const double TumblerKeptFee = 0.5, TumblerKeptVolume = 1.5, TumblerGift = 1000;
public const int TumblerBounty = 1200, TumblerSecRep = 8, TumblerBmRep = 5;
public const double TumblerSellCrt = 3000, TumblerSoldFee = 1.5, TumblerSoldRug = 4;
public const int TumblerDriveValue = 600;
/// <summary>卖给黑市得的 CRT 里带污点的比例。</summary>
public const double TumblerSellTaint = 0.5;
/// <summary>Tumbler 的后续剧情:混币器出款记录保留几天、最多几条(泄露时这些出款洗掉的污点加回原钱包);
/// 治安部探员:配合时的治安部声望,之后搜走密钥盘记的违禁品罪证。</summary>
public const int TumblerLeakDays = 30, TumblerReturnsMax = 300, TumblerAgentSecRep = 5, TumblerSearchCrime = 300;
/// <summary>审判盘:举报后第几天开盘、第几天宣判;定罪的概率(举报时掷出);每晚庭审新闻偏向真实判决的概率。</summary>
public const int TumblerTrialOpen = 2, TumblerTrialDays = 7;
public const double TumblerTrialConvict = 0.7, TumblerTrialHintAcc = 0.7;
/// <summary>报复:获释或黑市跑路后几天动手;给当前钱包植入木马(否则地址投毒)的概率;地址投毒每个钱包最多几笔。</summary>
public const int TumblerRevengeDelay = 3, TumblerRevengePoison = 3;
public const double TumblerRevengeWalletShare = 0.5;
/// <summary>卖给黑市后,黑市卷款跑路在第几天(随机,含两端)。</summary>
public const int TumblerBmRugMin = 10, TumblerBmRugMax = 20;
/// <summary>换皮重开:混币器关停后几天;是治安部钓鱼混币器的概率;钓鱼混币器的费率倍率。</summary>
public const int TumblerRebrandDays = 15;
public const double TumblerHoneypot = 0.25, TumblerHoneypotFee = 0.5;
public const double StoryLunMult = 1.3;
/// <summary>行情图的 NPC 成交量:每天的基础量占主池深度的比例(按币种),价格波动越大成交越多(|涨跌幅| × 倍数),再乘 0.6–1.4 的随机。</summary>
public const double ChartVolPsc = 0.12, ChartVolCrc = 0.10, ChartVolCrt = 0.06, ChartVolCra = 0.04, ChartVolSpark = 0.03, ChartVolPonero = 0.01, ChartVolMove = 8;
/// <summary>主交易对(信用点一侧)的深度随天数增长。</summary>
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
/// <summary>永续的成交深度,同样随天数增长。</summary>
public static double PerpDepth(int day) => PerpDepth0 * (1 + day / 60.0);
}
}
+540
View File
@@ -0,0 +1,540 @@
// 由测试项目生成:dotnet run --project tests/Decentralized.Domain.Tests -c Release -- --gen-tuning-table
// Tuning 增删数值后要重新生成(单元测试会核对)。
using System;
using System.Collections.Generic;
namespace Decentralized.Domain
{
internal static class TuningTable
{
public static readonly Dictionary<string, double> Scalars = new Dictionary<string, double>
{
["AirdropChance"] = Tuning.AirdropChance,
["AirdropDays"] = Tuning.AirdropDays,
["AirdropDrainMax"] = Tuning.AirdropDrainMax,
["AirdropDrainMin"] = Tuning.AirdropDrainMin,
["AirdropRewardMax"] = Tuning.AirdropRewardMax,
["AirdropRewardMin"] = Tuning.AirdropRewardMin,
["AirdropScam"] = Tuning.AirdropScam,
["AsicChance"] = Tuning.AsicChance,
["AsicDeadValue"] = Tuning.AsicDeadValue,
["AsicNewShare"] = Tuning.AsicNewShare,
["AsicRepM1"] = Tuning.AsicRepM1,
["AsicRepM2"] = Tuning.AsicRepM2,
["AsicRepM3"] = Tuning.AsicRepM3,
["AsicRepairHealthCut"] = Tuning.AsicRepairHealthCut,
["AsicUsedHealthMax"] = Tuning.AsicUsedHealthMax,
["AsicUsedHealthMin"] = Tuning.AsicUsedHealthMin,
["AsicWeightM1"] = Tuning.AsicWeightM1,
["AsicWeightM2"] = Tuning.AsicWeightM2,
["AsicWeightM3"] = Tuning.AsicWeightM3,
["BlockReward"] = Tuning.BlockReward,
["BlocksPerDayMax"] = Tuning.BlocksPerDayMax,
["BlocksPerDayMin"] = Tuning.BlocksPerDayMin,
["BmAdvanceDiscount"] = Tuning.BmAdvanceDiscount,
["BmAdvanceMinTrades"] = Tuning.BmAdvanceMinTrades,
["BmAutoSwapSlip"] = Tuning.BmAutoSwapSlip,
["BmBulk1Count"] = Tuning.BmBulk1Count,
["BmBulk1Discount"] = Tuning.BmBulk1Discount,
["BmBulk2Count"] = Tuning.BmBulk2Count,
["BmBulk2Discount"] = Tuning.BmBulk2Discount,
["BmBulkChance"] = Tuning.BmBulkChance,
["BmBulkDays"] = Tuning.BmBulkDays,
["BmBulkQtyMax"] = Tuning.BmBulkQtyMax,
["BmBulkQtyMin"] = Tuning.BmBulkQtyMin,
["BmCommission"] = Tuning.BmCommission,
["BmControlled"] = Tuning.BmControlled,
["BmCoreMerchants"] = Tuning.BmCoreMerchants,
["BmDoorDays"] = Tuning.BmDoorDays,
["BmEliteChance"] = Tuning.BmEliteChance,
["BmEliteDays"] = Tuning.BmEliteDays,
["BmEliteGpuChance"] = Tuning.BmEliteGpuChance,
["BmEliteGpuTier"] = Tuning.BmEliteGpuTier,
["BmEliteListings"] = Tuning.BmEliteListings,
["BmEliteMinValue"] = Tuning.BmEliteMinValue,
["BmEliteQtyMax"] = Tuning.BmEliteQtyMax,
["BmEscrowFee"] = Tuning.BmEscrowFee,
["BmFactorBulkMax"] = Tuning.BmFactorBulkMax,
["BmFactorBulkMin"] = Tuning.BmFactorBulkMin,
["BmFactorEliteMax"] = Tuning.BmFactorEliteMax,
["BmFactorEliteMin"] = Tuning.BmFactorEliteMin,
["BmFactorMax"] = Tuning.BmFactorMax,
["BmFactorMin"] = Tuning.BmFactorMin,
["BmFactorNewMax"] = Tuning.BmFactorNewMax,
["BmFactorNewMin"] = Tuning.BmFactorNewMin,
["BmFactorUsedMax"] = Tuning.BmFactorUsedMax,
["BmFactorUsedMin"] = Tuning.BmFactorUsedMin,
["BmFakeDetect"] = Tuning.BmFakeDetect,
["BmForumRugFactor"] = Tuning.BmForumRugFactor,
["BmForumScamHint"] = Tuning.BmForumScamHint,
["BmFreshRatingMax"] = Tuning.BmFreshRatingMax,
["BmFreshRatingMin"] = Tuning.BmFreshRatingMin,
["BmFreshTradesMax"] = Tuning.BmFreshTradesMax,
["BmGpuChance"] = Tuning.BmGpuChance,
["BmGpuFake"] = Tuning.BmGpuFake,
["BmGpuMarkup"] = Tuning.BmGpuMarkup,
["BmGpuNew"] = Tuning.BmGpuNew,
["BmHighRep"] = Tuning.BmHighRep,
["BmHonestFresh"] = Tuning.BmHonestFresh,
["BmHoneypotBase"] = Tuning.BmHoneypotBase,
["BmHoneypotDays"] = Tuning.BmHoneypotDays,
["BmHoneypotMax"] = Tuning.BmHoneypotMax,
["BmHoneypotPerSecRep"] = Tuning.BmHoneypotPerSecRep,
["BmInterceptCrackdown"] = Tuning.BmInterceptCrackdown,
["BmInterceptDoor"] = Tuning.BmInterceptDoor,
["BmListFee"] = Tuning.BmListFee,
["BmMaxKinds"] = Tuning.BmMaxKinds,
["BmMaxStack"] = Tuning.BmMaxStack,
["BmMaxUnitValue"] = Tuning.BmMaxUnitValue,
["BmMerchantCount"] = Tuning.BmMerchantCount,
["BmMerchantListings"] = Tuning.BmMerchantListings,
["BmMidRep"] = Tuning.BmMidRep,
["BmNewGpuQtyMaxCut"] = Tuning.BmNewGpuQtyMaxCut,
["BmNewGpuQtyMinCut"] = Tuning.BmNewGpuQtyMinCut,
["BmOldRatingMax"] = Tuning.BmOldRatingMax,
["BmOldRatingMin"] = Tuning.BmOldRatingMin,
["BmOldTradesMax"] = Tuning.BmOldTradesMax,
["BmOldTradesMin"] = Tuning.BmOldTradesMin,
["BmOpenRep"] = Tuning.BmOpenRep,
["BmPersonalCount"] = Tuning.BmPersonalCount,
["BmPersonalEliteShare"] = Tuning.BmPersonalEliteShare,
["BmPersonalFactorMax"] = Tuning.BmPersonalFactorMax,
["BmPersonalFactorMin"] = Tuning.BmPersonalFactorMin,
["BmPersonalGpuChance"] = Tuning.BmPersonalGpuChance,
["BmPersonalGpuNew"] = Tuning.BmPersonalGpuNew,
["BmPersonalListings"] = Tuning.BmPersonalListings,
["BmPersonalQtyMax"] = Tuning.BmPersonalQtyMax,
["BmPersonalScam"] = Tuning.BmPersonalScam,
["BmPremium"] = Tuning.BmPremium,
["BmPriceJitter"] = Tuning.BmPriceJitter,
["BmQtyMax"] = Tuning.BmQtyMax,
["BmQtyMin"] = Tuning.BmQtyMin,
["BmRatingFakeLoss"] = Tuning.BmRatingFakeLoss,
["BmRatingStart"] = Tuning.BmRatingStart,
["BmRefreshDays"] = Tuning.BmRefreshDays,
["BmRugBase"] = Tuning.BmRugBase,
["BmRugClosedDays"] = Tuning.BmRugClosedDays,
["BmRugMax"] = Tuning.BmRugMax,
["BmRugPerTenK"] = Tuning.BmRugPerTenK,
["BmSaleHold"] = Tuning.BmSaleHold,
["BmSaleTaintChanceMax"] = Tuning.BmSaleTaintChanceMax,
["BmSaleTaintChanceMin"] = Tuning.BmSaleTaintChanceMin,
["BmSaleTaintRatioMax"] = Tuning.BmSaleTaintRatioMax,
["BmSaleTaintRatioMin"] = Tuning.BmSaleTaintRatioMin,
["BmScamFresh"] = Tuning.BmScamFresh,
["BmScamOldRatingMax"] = Tuning.BmScamOldRatingMax,
["BmScamOldRatingMin"] = Tuning.BmScamOldRatingMin,
["BmScamSellerShare"] = Tuning.BmScamSellerShare,
["BmSellDay1Mult"] = Tuning.BmSellDay1Mult,
["BmSellDays1"] = Tuning.BmSellDays1,
["BmSellDaysFast"] = Tuning.BmSellDaysFast,
["BmSellDaysMax"] = Tuning.BmSellDaysMax,
["BmSellFastMult"] = Tuning.BmSellFastMult,
["BmSellMax"] = Tuning.BmSellMax,
["BmSellMin"] = Tuning.BmSellMin,
["BmSellerBust"] = Tuning.BmSellerBust,
["BmSpotQtyMax"] = Tuning.BmSpotQtyMax,
["BmStockBase"] = Tuning.BmStockBase,
["BmStockPerRep"] = Tuning.BmStockPerRep,
["BmTechChance"] = Tuning.BmTechChance,
["BmTechMarkup"] = Tuning.BmTechMarkup,
["BmTrapDelay"] = Tuning.BmTrapDelay,
["BmTrapShare"] = Tuning.BmTrapShare,
["BmTrapWindowCap"] = Tuning.BmTrapWindowCap,
["BmUnlockPrice"] = Tuning.BmUnlockPrice,
["BmVolumeRef"] = Tuning.BmVolumeRef,
["BmVolumeTaintMax"] = Tuning.BmVolumeTaintMax,
["BmWeightCra"] = Tuning.BmWeightCra,
["BmWeightCrt"] = Tuning.BmWeightCrt,
["BmWeightPonero"] = Tuning.BmWeightPonero,
["BmWeightPsc"] = Tuning.BmWeightPsc,
["BookAlpha"] = Tuning.BookAlpha,
["BookK"] = Tuning.BookK,
["CardBracketScratch"] = Tuning.CardBracketScratch,
["CardFanChipped"] = Tuning.CardFanChipped,
["CardFanDust"] = Tuning.CardFanDust,
["CardFingersDark"] = Tuning.CardFingersDark,
["CardFingersOxidized"] = Tuning.CardFingersOxidized,
["CardHealthMax"] = Tuning.CardHealthMax,
["CardHealthMin"] = Tuning.CardHealthMin,
["CardSealMissing"] = Tuning.CardSealMissing,
["CardValueFloor"] = Tuning.CardValueFloor,
["ChartVolCra"] = Tuning.ChartVolCra,
["ChartVolCrc"] = Tuning.ChartVolCrc,
["ChartVolCrt"] = Tuning.ChartVolCrt,
["ChartVolMove"] = Tuning.ChartVolMove,
["ChartVolPonero"] = Tuning.ChartVolPonero,
["ChartVolPsc"] = Tuning.ChartVolPsc,
["ChartVolSpark"] = Tuning.ChartVolSpark,
["CoinPayCra"] = Tuning.CoinPayCra,
["CoinPayCrc"] = Tuning.CoinPayCrc,
["CoinPayCrt"] = Tuning.CoinPayCrt,
["CoinPayPremium"] = Tuning.CoinPayPremium,
["CoinPayPsc"] = Tuning.CoinPayPsc,
["CraC0"] = Tuning.CraC0,
["CraCrMin"] = Tuning.CraCrMin,
["CraCrStep"] = Tuning.CraCrStep,
["CraCrashRatio"] = Tuning.CraCrashRatio,
["CraDiscountMax"] = Tuning.CraDiscountMax,
["CraEmbezzleMax"] = Tuning.CraEmbezzleMax,
["CraEmbezzleMin"] = Tuning.CraEmbezzleMin,
["CraLoot"] = Tuning.CraLoot,
["CraMintDaysMax"] = Tuning.CraMintDaysMax,
["CraMintDaysMin"] = Tuning.CraMintDaysMin,
["CraNpcStake"] = Tuning.CraNpcStake,
["CraRate"] = Tuning.CraRate,
["CraRateLow"] = Tuning.CraRateLow,
["CraRugPerDepeg"] = Tuning.CraRugPerDepeg,
["CraS0"] = Tuning.CraS0,
["CraSafeRatio"] = Tuning.CraSafeRatio,
["CraSpiralWeekly"] = Tuning.CraSpiralWeekly,
["CraSubsidyWarn"] = Tuning.CraSubsidyWarn,
["CraUnlockDays"] = Tuning.CraUnlockDays,
["CraWarnLeadMax"] = Tuning.CraWarnLeadMax,
["CraWarnLeadMin"] = Tuning.CraWarnLeadMin,
["CrcDepegChance"] = Tuning.CrcDepegChance,
["CrcDepegDaysMax"] = Tuning.CrcDepegDaysMax,
["CrcDepegDaysMin"] = Tuning.CrcDepegDaysMin,
["CrcDepegMax"] = Tuning.CrcDepegMax,
["CrcDepegMin"] = Tuning.CrcDepegMin,
["CrcHaircut"] = Tuning.CrcHaircut,
["CrcHaircutChance"] = Tuning.CrcHaircutChance,
["CrimeEventCap"] = Tuning.CrimeEventCap,
["CrtDepegChance"] = Tuning.CrtDepegChance,
["CrtDepegDaysMax"] = Tuning.CrtDepegDaysMax,
["CrtDepegDaysMin"] = Tuning.CrtDepegDaysMin,
["CrtDepegMax"] = Tuning.CrtDepegMax,
["CrtDepegMin"] = Tuning.CrtDepegMin,
["CrtFreezeBase"] = Tuning.CrtFreezeBase,
["CrtFreezePerRep"] = Tuning.CrtFreezePerRep,
["CrtFreezeShare"] = Tuning.CrtFreezeShare,
["CrtSwapFee"] = Tuning.CrtSwapFee,
["CrtUnfreezeAfter"] = Tuning.CrtUnfreezeAfter,
["CustomerSpotChance"] = Tuning.CustomerSpotChance,
["CustomerSpotTierMismatch"] = Tuning.CustomerSpotTierMismatch,
["DealerAdvanced"] = Tuning.DealerAdvanced,
["DealerAdvancedFromDay"] = Tuning.DealerAdvancedFromDay,
["DealerAdvancedSecond"] = Tuning.DealerAdvancedSecond,
["DealerFlagship"] = Tuning.DealerFlagship,
["DealerFlagshipFromDay"] = Tuning.DealerFlagshipFromDay,
["DealerRigRestock"] = Tuning.DealerRigRestock,
["DealerSpecialFlagshipFromDay"] = Tuning.DealerSpecialFlagshipFromDay,
["DeathBase"] = Tuning.DeathBase,
["DeathWorn"] = Tuning.DeathWorn,
["DexFee"] = Tuning.DexFee,
["ElecPrice"] = Tuning.ElecPrice,
["EraKjMult"] = Tuning.EraKjMult,
["EraMax"] = Tuning.EraMax,
["EraMin"] = Tuning.EraMin,
["EraWarnMax"] = Tuning.EraWarnMax,
["EraWarnMin"] = Tuning.EraWarnMin,
["ExchangeFee"] = Tuning.ExchangeFee,
["FakeDeathFactor"] = Tuning.FakeDeathFactor,
["FakeSignChance"] = Tuning.FakeSignChance,
["FakeSupportChance"] = Tuning.FakeSupportChance,
["FakeTransferShare"] = Tuning.FakeTransferShare,
["FundsBaseSecs"] = Tuning.FundsBaseSecs,
["FundsConfSecs"] = Tuning.FundsConfSecs,
["HalvingDays"] = Tuning.HalvingDays,
["HalvingPassMax"] = Tuning.HalvingPassMax,
["HalvingPassMin"] = Tuning.HalvingPassMin,
["HalvingRamp"] = Tuning.HalvingRamp,
["HalvingRampDays"] = Tuning.HalvingRampDays,
["IntelBriefPrice"] = Tuning.IntelBriefPrice,
["IntelFake"] = Tuning.IntelFake,
["IntelRep"] = Tuning.IntelRep,
["LedgerMax"] = Tuning.LedgerMax,
["LedgerShow"] = Tuning.LedgerShow,
["ManipBase"] = Tuning.ManipBase,
["ManipFree"] = Tuning.ManipFree,
["ManipMax"] = Tuning.ManipMax,
["ManipSlope"] = Tuning.ManipSlope,
["MiningScale"] = Tuning.MiningScale,
["MixerCleanRate"] = Tuning.MixerCleanRate,
["MixerFee"] = Tuning.MixerFee,
["MixerMaxRounds"] = Tuning.MixerMaxRounds,
["MixerMin"] = Tuning.MixerMin,
["MixerNpcVolume"] = Tuning.MixerNpcVolume,
["MixerRugChance"] = Tuning.MixerRugChance,
["MixerSanctionChance"] = Tuning.MixerSanctionChance,
["MixerSanctionDays"] = Tuning.MixerSanctionDays,
["MixerUnlockPrice"] = Tuning.MixerUnlockPrice,
["ModuleHashFloor"] = Tuning.ModuleHashFloor,
["ModuleKjFloor"] = Tuning.ModuleKjFloor,
["ModuleRatio"] = Tuning.ModuleRatio,
["ModuleWearFloor"] = Tuning.ModuleWearFloor,
["NpcAdj"] = Tuning.NpcAdj,
["NpcHash0"] = Tuning.NpcHash0,
["NpcKjPerH"] = Tuning.NpcKjPerH,
["NpcTargetRatio"] = Tuning.NpcTargetRatio,
["OtcBidMax"] = Tuning.OtcBidMax,
["OtcBidMin"] = Tuning.OtcBidMin,
["OtcBuyMaxRents"] = Tuning.OtcBuyMaxRents,
["OtcBuyMinRents"] = Tuning.OtcBuyMinRents,
["OtcCutClean"] = Tuning.OtcCutClean,
["OtcCutDirty"] = Tuning.OtcCutDirty,
["OtcEventChance"] = Tuning.OtcEventChance,
["OtcHistoryMax"] = Tuning.OtcHistoryMax,
["OtcPoneroChance"] = Tuning.OtcPoneroChance,
["OtcPoneroCutMax"] = Tuning.OtcPoneroCutMax,
["OtcPoneroCutMin"] = Tuning.OtcPoneroCutMin,
["OtcSellShare"] = Tuning.OtcSellShare,
["OtcSting"] = Tuning.OtcSting,
["OtcStingCrackdown"] = Tuning.OtcStingCrackdown,
["OtcTaintMax"] = Tuning.OtcTaintMax,
["OtcTaintMin"] = Tuning.OtcTaintMin,
["OtcUnlockPrice"] = Tuning.OtcUnlockPrice,
["OtcValueMaxRents"] = Tuning.OtcValueMaxRents,
["OtcValueMinRents"] = Tuning.OtcValueMinRents,
["OwnMixAnon0"] = Tuning.OwnMixAnon0,
["OwnMixAvgRounds"] = Tuning.OwnMixAvgRounds,
["OwnMixDemand0"] = Tuning.OwnMixDemand0,
["OwnMixDemandPerDay"] = Tuning.OwnMixDemandPerDay,
["OwnMixFeeDefault"] = Tuning.OwnMixFeeDefault,
["OwnMixFeeScale"] = Tuning.OwnMixFeeScale,
["OwnMixHeatAnon0"] = Tuning.OwnMixHeatAnon0,
["OwnMixHeatHigh"] = Tuning.OwnMixHeatHigh,
["OwnMixHeatKeep"] = Tuning.OwnMixHeatKeep,
["OwnMixHeatMid"] = Tuning.OwnMixHeatMid,
["OwnMixHeatScale"] = Tuning.OwnMixHeatScale,
["OwnMixInTaintMax"] = Tuning.OwnMixInTaintMax,
["OwnMixInTaintMin"] = Tuning.OwnMixInTaintMin,
["OwnMixNoise"] = Tuning.OwnMixNoise,
["OwnMixPendingWeight"] = Tuning.OwnMixPendingWeight,
["OwnMixRatingSpeed"] = Tuning.OwnMixRatingSpeed,
["OwnMixRepMin"] = Tuning.OwnMixRepMin,
["OwnMixRivalGone"] = Tuning.OwnMixRivalGone,
["OwnMixRugCrime"] = Tuning.OwnMixRugCrime,
["OwnMixRugRep"] = Tuning.OwnMixRugRep,
["OwnMixSeizeCrackdown"] = Tuning.OwnMixSeizeCrackdown,
["OwnMixSeizeCrime"] = Tuning.OwnMixSeizeCrime,
["OwnMixSeizeDays"] = Tuning.OwnMixSeizeDays,
["OwnMixSeizeMax"] = Tuning.OwnMixSeizeMax,
["OwnMixSeizeRate"] = Tuning.OwnMixSeizeRate,
["OwnMixSting"] = Tuning.OwnMixSting,
["OwnMixStingCrime"] = Tuning.OwnMixStingCrime,
["OwnMixTaintWeight"] = Tuning.OwnMixTaintWeight,
["OwnMixTicket"] = Tuning.OwnMixTicket,
["PerpAdlHaircut"] = Tuning.PerpAdlHaircut,
["PerpBanDays"] = Tuning.PerpBanDays,
["PerpDebtDays"] = Tuning.PerpDebtDays,
["PerpDebtInterest"] = Tuning.PerpDebtInterest,
["PerpDepth0"] = Tuning.PerpDepth0,
["PerpDepthMult"] = Tuning.PerpDepthMult,
["PerpFee"] = Tuning.PerpFee,
["PerpFeeToFund"] = Tuning.PerpFeeToFund,
["PerpFund0"] = Tuning.PerpFund0,
["PerpFundCap"] = Tuning.PerpFundCap,
["PerpFundCoef"] = Tuning.PerpFundCoef,
["PerpLiqFee"] = Tuning.PerpLiqFee,
["PerpMaint"] = Tuning.PerpMaint,
["PerpNewsMove"] = Tuning.PerpNewsMove,
["PerpNpcAvgPos"] = Tuning.PerpNpcAvgPos,
["PerpNpcBankBase"] = Tuning.PerpNpcBankBase,
["PerpNpcBankFree"] = Tuning.PerpNpcBankFree,
["PerpNpcBankMax"] = Tuning.PerpNpcBankMax,
["PerpNpcBankSlope"] = Tuning.PerpNpcBankSlope,
["PerpNpcGap"] = Tuning.PerpNpcGap,
["PerpNpcLeft"] = Tuning.PerpNpcLeft,
["PerpNpcLiqScale"] = Tuning.PerpNpcLiqScale,
["PerpNpcOi"] = Tuning.PerpNpcOi,
["PerpOpenBuffer"] = Tuning.PerpOpenBuffer,
["PerpPscCap"] = Tuning.PerpPscCap,
["PerpPscLev"] = Tuning.PerpPscLev,
["PerpSparkCap"] = Tuning.PerpSparkCap,
["PerpSparkLev"] = Tuning.PerpSparkLev,
["PoinSparkDepthShare"] = Tuning.PoinSparkDepthShare,
["PoneroDailyCap"] = Tuning.PoneroDailyCap,
["PoneroFee"] = Tuning.PoneroFee,
["PoneroOpenRep"] = Tuning.PoneroOpenRep,
["PoneroPscRatio"] = Tuning.PoneroPscRatio,
["PonziAdCost"] = Tuning.PonziAdCost,
["PonziBase0"] = Tuning.PonziBase0,
["PonziBasePerDay"] = Tuning.PonziBasePerDay,
["PonziCap0"] = Tuning.PonziCap0,
["PonziCapPerDay"] = Tuning.PonziCapPerDay,
["PonziCrime"] = Tuning.PonziCrime,
["PonziMinDay"] = Tuning.PonziMinDay,
["PonziRateExp"] = Tuning.PonziRateExp,
["PonziReport0"] = Tuning.PonziReport0,
["PonziReportRate"] = Tuning.PonziReportRate,
["PonziReportSize"] = Tuning.PonziReportSize,
["PonziWithdraw"] = Tuning.PonziWithdraw,
["PoolDepth0"] = Tuning.PoolDepth0,
["PoolFee"] = Tuning.PoolFee,
["PredictCap"] = Tuning.PredictCap,
["PredictDepth"] = Tuning.PredictDepth,
["PredictDrift"] = Tuning.PredictDrift,
["PredictFactionDaily"] = Tuning.PredictFactionDaily,
["PredictFee"] = Tuning.PredictFee,
["PredictFraudCap"] = Tuning.PredictFraudCap,
["PredictFraudShare"] = Tuning.PredictFraudShare,
["PredictGasBlind"] = Tuning.PredictGasBlind,
["PredictHistory"] = Tuning.PredictHistory,
["PredictLoss0"] = Tuning.PredictLoss0,
["PredictLossPerDay"] = Tuning.PredictLossPerDay,
["PredictMoveLog"] = Tuning.PredictMoveLog,
["PredictNewPerDay"] = Tuning.PredictNewPerDay,
["PredictNoise"] = Tuning.PredictNoise,
["PredictOpenMax"] = Tuning.PredictOpenMax,
["PredictOpenMin"] = Tuning.PredictOpenMin,
["PredictPMax"] = Tuning.PredictPMax,
["PredictPMin"] = Tuning.PredictPMin,
["PredictUnlockDay"] = Tuning.PredictUnlockDay,
["PriceCeiling"] = Tuning.PriceCeiling,
["PriceFloor"] = Tuning.PriceFloor,
["PscStartPrice"] = Tuning.PscStartPrice,
["PscVol"] = Tuning.PscVol,
["RefurbishHealth"] = Tuning.RefurbishHealth,
["RepLossMax"] = Tuning.RepLossMax,
["Revert"] = Tuning.Revert,
["RigKj"] = Tuning.RigKj,
["RigPrice"] = Tuning.RigPrice,
["ScamMinDay"] = Tuning.ScamMinDay,
["SentimentKappa"] = Tuning.SentimentKappa,
["SentimentVol"] = Tuning.SentimentVol,
["SideArbRate"] = Tuning.SideArbRate,
["SideDepthShare"] = Tuning.SideDepthShare,
["SparkDepthShare"] = Tuning.SparkDepthShare,
["SparkFee"] = Tuning.SparkFee,
["SparkMaxMult"] = Tuning.SparkMaxMult,
["SparkMinMult"] = Tuning.SparkMinMult,
["SparkRaidShock"] = Tuning.SparkRaidShock,
["SparkRevert"] = Tuning.SparkRevert,
["SparkStart"] = Tuning.SparkStart,
["SparkUnrestShock"] = Tuning.SparkUnrestShock,
["SparkVol"] = Tuning.SparkVol,
["StableAmp"] = Tuning.StableAmp,
["StableAmpBroken"] = Tuning.StableAmpBroken,
["StableAmpDepeg"] = Tuning.StableAmpDepeg,
["StableBrokenBand"] = Tuning.StableBrokenBand,
["StableBrokenDepth"] = Tuning.StableBrokenDepth,
["StableDepegBand"] = Tuning.StableDepegBand,
["StableFee"] = Tuning.StableFee,
["StoryBmCrtMult"] = Tuning.StoryBmCrtMult,
["StoryCraSpiralMult"] = Tuning.StoryCraSpiralMult,
["StoryLeakHaircutMult"] = Tuning.StoryLeakHaircutMult,
["StoryLeakSpark"] = Tuning.StoryLeakSpark,
["StoryLunDays"] = Tuning.StoryLunDays,
["StoryLunMult"] = Tuning.StoryLunMult,
["StoryReturnCrcMult"] = Tuning.StoryReturnCrcMult,
["StoryRevSubsidy"] = Tuning.StoryRevSubsidy,
["StorySecDays"] = Tuning.StorySecDays,
["StorySecEnforceMult"] = Tuning.StorySecEnforceMult,
["StorySecMixClean"] = Tuning.StorySecMixClean,
["StorySecSanctionMult"] = Tuning.StorySecSanctionMult,
["StorySerpicoSpark"] = Tuning.StorySerpicoSpark,
["StorySparkNights"] = Tuning.StorySparkNights,
["SupplyAdjust"] = Tuning.SupplyAdjust,
["TaxRate"] = Tuning.TaxRate,
["TumblerAgentSecRep"] = Tuning.TumblerAgentSecRep,
["TumblerBmRep"] = Tuning.TumblerBmRep,
["TumblerBmRugMax"] = Tuning.TumblerBmRugMax,
["TumblerBmRugMin"] = Tuning.TumblerBmRugMin,
["TumblerBounty"] = Tuning.TumblerBounty,
["TumblerDriveValue"] = Tuning.TumblerDriveValue,
["TumblerGift"] = Tuning.TumblerGift,
["TumblerHoneypot"] = Tuning.TumblerHoneypot,
["TumblerHoneypotFee"] = Tuning.TumblerHoneypotFee,
["TumblerKeptFee"] = Tuning.TumblerKeptFee,
["TumblerKeptVolume"] = Tuning.TumblerKeptVolume,
["TumblerLeakDays"] = Tuning.TumblerLeakDays,
["TumblerMinDay"] = Tuning.TumblerMinDay,
["TumblerRebrandDays"] = Tuning.TumblerRebrandDays,
["TumblerReturnsMax"] = Tuning.TumblerReturnsMax,
["TumblerRevengeDelay"] = Tuning.TumblerRevengeDelay,
["TumblerRevengePoison"] = Tuning.TumblerRevengePoison,
["TumblerRevengeWalletShare"] = Tuning.TumblerRevengeWalletShare,
["TumblerSearchCrime"] = Tuning.TumblerSearchCrime,
["TumblerSecRep"] = Tuning.TumblerSecRep,
["TumblerSellCrt"] = Tuning.TumblerSellCrt,
["TumblerSellTaint"] = Tuning.TumblerSellTaint,
["TumblerSoldFee"] = Tuning.TumblerSoldFee,
["TumblerSoldRug"] = Tuning.TumblerSoldRug,
["TumblerTrialConvict"] = Tuning.TumblerTrialConvict,
["TumblerTrialDays"] = Tuning.TumblerTrialDays,
["TumblerTrialHintAcc"] = Tuning.TumblerTrialHintAcc,
["TumblerTrialOpen"] = Tuning.TumblerTrialOpen,
["TxConfirmBase"] = Tuning.TxConfirmBase,
["TxConfirmBig"] = Tuning.TxConfirmBig,
["TxConfirmBigValue"] = Tuning.TxConfirmBigValue,
["TxConfirmMid"] = Tuning.TxConfirmMid,
["TxConfirmMidValue"] = Tuning.TxConfirmMidValue,
["TxCongestedFeeMult"] = Tuning.TxCongestedFeeMult,
["TxCongestedNormalSlow"] = Tuning.TxCongestedNormalSlow,
["TxCongestedSlowSlow"] = Tuning.TxCongestedSlowSlow,
["TxCongestionChance"] = Tuning.TxCongestionChance,
["TxCongestionDaysMax"] = Tuning.TxCongestionDaysMax,
["TxFeeFast"] = Tuning.TxFeeFast,
["TxFeeNormal"] = Tuning.TxFeeNormal,
["TxFeeSlow"] = Tuning.TxFeeSlow,
["TxGasAmp"] = Tuning.TxGasAmp,
["TxGasNoise"] = Tuning.TxGasNoise,
["TxGasPeriod"] = Tuning.TxGasPeriod,
["TxHijackChance"] = Tuning.TxHijackChance,
["TxHoldSecs"] = Tuning.TxHoldSecs,
["TxPhishChance"] = Tuning.TxPhishChance,
["TxPoisonChance"] = Tuning.TxPoisonChance,
["TxPoisonDelayMax"] = Tuning.TxPoisonDelayMax,
["TxSlowDrop"] = Tuning.TxSlowDrop,
["TxSpeedFast"] = Tuning.TxSpeedFast,
["TxSpeedNormal"] = Tuning.TxSpeedNormal,
["TxSpeedSlow"] = Tuning.TxSpeedSlow,
["UnlockPrice"] = Tuning.UnlockPrice,
["UsedFakeMiddleman"] = Tuning.UsedFakeMiddleman,
["UsedFakeScammer"] = Tuning.UsedFakeScammer,
["UsedFirstDay"] = Tuning.UsedFirstDay,
["UsedMiddlemanHide"] = Tuning.UsedMiddlemanHide,
["UsedMiddlemanMarkup"] = Tuning.UsedMiddlemanMarkup,
["UsedMiddlemanShare"] = Tuning.UsedMiddlemanShare,
["UsedScamBase"] = Tuning.UsedScamBase,
["UsedScamMax"] = Tuning.UsedScamMax,
["UsedScamPerDay"] = Tuning.UsedScamPerDay,
["UsedVisitChance"] = Tuning.UsedVisitChance,
["WalletDrainFull"] = Tuning.WalletDrainFull,
["WalletDrainMin"] = Tuning.WalletDrainMin,
["WalletDrainSlowDays"] = Tuning.WalletDrainSlowDays,
["WalletJacksonChance"] = Tuning.WalletJacksonChance,
["WalletJacksonPrice"] = Tuning.WalletJacksonPrice,
["WalletMallChance"] = Tuning.WalletMallChance,
["WalletMallMax"] = Tuning.WalletMallMax,
["WalletMallMin"] = Tuning.WalletMallMin,
["WalletMallQtyMax"] = Tuning.WalletMallQtyMax,
["WalletMax"] = Tuning.WalletMax,
["WalletPoisonShare"] = Tuning.WalletPoisonShare,
["WhaleMax"] = Tuning.WhaleMax,
["WhaleMin"] = Tuning.WhaleMin,
["WickChance"] = Tuning.WickChance,
["WickMax"] = Tuning.WickMax,
["WickMin"] = Tuning.WickMin,
["WickReal"] = Tuning.WickReal,
["WickRevertMax"] = Tuning.WickRevertMax,
["WickRevertMin"] = Tuning.WickRevertMin,
["YieldCap0"] = Tuning.YieldCap0,
["YieldCapPerDay"] = Tuning.YieldCapPerDay,
["YieldLockDays"] = Tuning.YieldLockDays,
["YieldPoolChance"] = Tuning.YieldPoolChance,
["YieldPoolMax"] = Tuning.YieldPoolMax,
["YieldReserveDays"] = Tuning.YieldReserveDays,
["YieldRugNightly"] = Tuning.YieldRugNightly,
["YieldScamAudit"] = Tuning.YieldScamAudit,
["YieldScamTimelock"] = Tuning.YieldScamTimelock,
};
public static readonly Dictionary<string, double[]> Arrays = new Dictionary<string, double[]>
{
["BmSellMults"] = Array.ConvertAll(Tuning.BmSellMults, v => (double)v),
["CraSpiralPath"] = Array.ConvertAll(Tuning.CraSpiralPath, v => (double)v),
["IntelTipAcc"] = Array.ConvertAll(Tuning.IntelTipAcc, v => (double)v),
["IntelTipPrice"] = Array.ConvertAll(Tuning.IntelTipPrice, v => (double)v),
["OwnMixAssetWeights"] = Array.ConvertAll(Tuning.OwnMixAssetWeights, v => (double)v),
["OwnMixFees"] = Array.ConvertAll(Tuning.OwnMixFees, v => (double)v),
["PonziRates"] = Array.ConvertAll(Tuning.PonziRates, v => (double)v),
["SparkSpiralPath"] = Array.ConvertAll(Tuning.SparkSpiralPath, v => (double)v),
["UsedHealthMax"] = Array.ConvertAll(Tuning.UsedHealthMax, v => (double)v),
["UsedHealthMin"] = Array.ConvertAll(Tuning.UsedHealthMin, v => (double)v),
["YieldRates"] = Array.ConvertAll(Tuning.YieldRates, v => (double)v),
["YieldScamByRate"] = Array.ConvertAll(Tuning.YieldScamByRate, v => (double)v),
};
}
}
+53
View File
@@ -0,0 +1,53 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Decentralized.Domain
{
/// <summary>
/// 文本里的数值占位符:<c>{Name}</c>、<c>{Name:%}</c>、<c>{Name[2]:%}</c>,也可以走嵌套类 <c>{Section.Name}</c>。
/// 手册等说明文字不直接写数字,渲染时从 <see cref="Tuning"/> 取值,数值一改文字跟着变。
/// <c>%</c> 把小数按百分比显示(0.03 → 3%),<c>+%</c> 把倍数显示成涨幅(1.2 → 20%);不带格式时去掉多余的零。
/// </summary>
public static class TuningText
{
private static readonly Regex Placeholder = new Regex(@"\{([A-Za-z0-9_.]+)(?:\[(\d+)\])?(?::(\+?%))?\}", RegexOptions.Compiled);
public static string Render(string text)
{
if (string.IsNullOrEmpty(text)) return text;
return Placeholder.Replace(text, m => TryValue(m.Groups[1].Value, m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : -1, out var v)
? Format(v, m.Groups[3].Value)
: m.Value);
}
/// <summary>文本里取不到值的占位符(测试用)。</summary>
public static System.Collections.Generic.List<string> Unresolved(string text)
{
var bad = new System.Collections.Generic.List<string>();
foreach (Match m in Placeholder.Matches(text ?? ""))
if (!TryValue(m.Groups[1].Value, m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : -1, out _)) bad.Add(m.Value);
return bad;
}
private static string Format(double v, string format)
{
bool percent = format == "%" || format == "+%";
if (format == "+%") v -= 1; // 倍数 1.2 → 20%
if (percent) v *= 100;
return Math.Round(v, 4).ToString("0.####", CultureInfo.InvariantCulture) + (percent ? "%" : "");
}
private static bool TryValue(string path, int index, out double value)
{
value = 0;
if (TuningTable.Arrays.TryGetValue(path, out var arr))
{
if (index < 0 || index >= arr.Length) return false;
value = arr[index];
return true;
}
return index < 0 && TuningTable.Scalars.TryGetValue(path, out value);
}
}
}
+55
View File
@@ -0,0 +1,55 @@
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>钱包里的一条转账记录:对方地址、币种、数量、哪天、转入还是转出。地址投毒的 0 币转入也记在这里。</summary>
public sealed class TxRecord
{
public string Addr, Asset;
public double Amount;
public int Day;
public bool In;
}
/// <summary>手续费档位:慢最便宜,网络拥堵时可能被挤出内存池;快最贵,拥堵时也照常确认。</summary>
public enum FeeTier { Slow, Normal, Fast }
/// <summary>链上转账的规则:手续费、需要的确认数、拥堵时慢档被丢弃的概率。</summary>
public static class TxRules
{
/// <summary>手续费(信用点等值,按转出的币扣):档位基础价 × 拥堵倍数。</summary>
public static double FeeCredits(FeeTier t, bool congested, double gas = 1) =>
(t == FeeTier.Slow ? Tuning.TxFeeSlow : t == FeeTier.Normal ? Tuning.TxFeeNormal : Tuning.TxFeeFast) * gas * (congested ? Tuning.TxCongestedFeeMult : 1);
/// <summary>Gas 周期部分:phase 是这一局的起始相位(0..周期-1)。</summary>
public static double GasCycle(int day, int phase) => 1 + Tuning.TxGasAmp * System.Math.Sin(2 * System.Math.PI * (day + phase) / Tuning.TxGasPeriod);
/// <summary>某天的 gas 指数:周期 × (1 ± 随机波动)。</summary>
public static double Gas(int day, int phase, Rng rng) => GasCycle(day, phase) * (1 + Tuning.TxGasNoise * (rng.NextDouble() * 2 - 1));
/// <summary>需要的确认数:金额越大越多。</summary>
public static int Confirmations(double valueCredits) =>
valueCredits >= Tuning.TxConfirmBigValue ? Tuning.TxConfirmBig : valueCredits >= Tuning.TxConfirmMidValue ? Tuning.TxConfirmMid : Tuning.TxConfirmBase;
/// <summary>Gas 档位本身的速度倍数:越高越快。</summary>
public static double TierSpeed(FeeTier t) => t == FeeTier.Fast ? Tuning.TxSpeedFast : t == FeeTier.Normal ? Tuning.TxSpeedNormal : Tuning.TxSpeedSlow;
/// <summary>整条确认时间线的时间倍数:档位速度 × 拥堵时慢档、普通档变慢(快档不受拥堵影响)。</summary>
public static double Slowdown(FeeTier t, bool congested) => TierSpeed(t) * (!congested ? 1 : t == FeeTier.Fast ? 1 : t == FeeTier.Normal ? Tuning.TxCongestedNormalSlow : Tuning.TxCongestedSlowSlow);
/// <summary>拥堵时慢档交易被挤出内存池的概率(交易作废,手续费照扣)。</summary>
public static double DropChance(FeeTier t, bool congested) => congested && t == FeeTier.Slow ? Tuning.TxSlowDrop : 0;
/// <summary>夜间区块高度的自然增长。</summary>
public static int BlocksPerDay(Rng rng) => rng.Range(Tuning.BlocksPerDayMin, Tuning.BlocksPerDayMax);
/// <summary>转账记录最多保留几条。</summary>
public const int HistoryMax = 20;
public static void Record(List<TxRecord> history, TxRecord r)
{
history.Insert(0, r);
if (history.Count > HistoryMax) history.RemoveRange(HistoryMax, history.Count - HistoryMax);
}
}
}
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>二手显卡贩子的类型:老实人按真实价卖;中间商加价;骗子夹带假卡,按标称型号的价卖。</summary>
public enum DealerKind { Honest, Middleman, Scammer }
/// <summary>贩子带来的一张卡:型号、健康度、是不是假卡。怎么说由 <see cref="CardLabels.DealerClaim"/> 决定,中间商另加 <see cref="MarkupPct"/>% 的价。</summary>
public sealed class UsedCard
{
public string Model;
public string FakeOf;
public double Health;
public int MarkupPct;
}
public static class UsedDealers
{
public const double MiddlemanMarkup = Tuning.UsedMiddlemanMarkup;
public const double FakeShareMiddleman = Tuning.UsedFakeMiddleman, FakeShareScammer = Tuning.UsedFakeScammer;
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
public static DealerKind Kind(int day, Rng rng)
{
double scam = Scales.R(Math.Min(Tuning.UsedScamMax, Tuning.UsedScamBase + day * Tuning.UsedScamPerDay));
double r = rng.NextDouble();
return r < scam ? DealerKind.Scammer : r < scam + Tuning.UsedMiddlemanShare ? DealerKind.Middleman : DealerKind.Honest;
}
/// <summary>能出现在二手市场上的型号:按天数放开,旗舰第 30 天起。</summary>
public static string[] Pool(int day)
{
var list = new List<string>(Dealer.Basic);
if (day >= Dealer.AdvancedFromDay) list.AddRange(Dealer.Advanced);
if (day >= Dealer.FlagshipFromDay) list.AddRange(Dealer.Flagship);
return list.ToArray();
}
/// <summary>从池里抽一张:随机型号与成色;按 fakeShare 的概率是假卡(背后是便宜得多的真实芯片,废片只给低档标称)。</summary>
public static UsedCard Card(string[] pool, double fakeShare, int markupPct, Rng rng, double minHealth = Tuning.CardHealthMin, double maxHealth = Tuning.CardHealthMax)
{
var label = pool[rng.Range(0, pool.Length - 1)];
var card = new UsedCard { Model = label, Health = minHealth + rng.NextDouble() * (maxHealth - minHealth), MarkupPct = markupPct };
var cheaper = Hardware.Specs.Values.Where(s => !s.Asic && s.Price * 2 <= Hardware.Specs[label].Price && (!s.Scrap || Hardware.Specs[label].Tier == 0)).Select(s => s.Model).ToArray();
if (cheaper.Length > 0 && rng.Chance(fakeShare)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
return card;
}
public static List<UsedCard> Cards(DealerKind kind, int day, Rng rng)
{
var pool = Pool(day);
int count = rng.Range(3, 5);
var cards = new List<UsedCard>();
for (int i = 0; i < count; i++)
{
double share = kind == DealerKind.Scammer ? FakeShareScammer : kind == DealerKind.Middleman ? FakeShareMiddleman : 0;
cards.Add(Card(pool, share, kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0, rng, Tuning.UsedHealthMin[(int)kind], Tuning.UsedHealthMax[(int)kind]));
}
return cards;
}
}
}
+191 -12
View File
@@ -1,54 +1,233 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace Decentralized.Domain
{
/// <summary>
/// 一个链上地址:各币种余额,加上整个地址的污点额与成本(信用点等值)。
/// 污点和成本按转出的价值比例随币走。
/// 一个链上地址:各币种余额,每种币里有几枚是脏的(<see cref="Dirty"/>),加上整个地址的成本(信用点)。
/// 污点随具体的币进出:收到带污点的币时记下其中几枚脏;花掉、转出这种币时按它的脏比例带走;兑换成别的币时脏比例跟过去。
/// 成本按转出的价值比例随币走。
/// </summary>
public sealed class Wallet
{
public string Address;
public Dictionary<string, double> Balances = new Dictionary<string, double>();
/// <summary>污点额(信用点等值)。</summary>
public double Taint;
/// <summary>每种币里脏的枚数(0 ≤ 脏 ≤ 余额)。</summary>
public Dictionary<string, double> Dirty = new Dictionary<string, double>();
/// <summary>旧存档(数据版本 5 之前)整个钱包的污点额(信用点):只在读档迁移时读一次,之后为 0。</summary>
[JsonProperty("Taint")] public double LegacyTaint;
/// <summary>成本(信用点):出金时只对超出成本的部分征税。</summary>
public double Basis;
/// <summary>被执法标记。</summary>
public bool Flagged;
/// <summary>钱包编号("钱包 n"),添加时定下,销毁别的钱包也不变。</summary>
public string Name;
/// <summary>玩家起的名字;空时用默认名。</summary>
public string Label;
/// <summary>转账记录(最新的在前),PLEDGER 发送页的"最近"列表从这里取。</summary>
public List<TxRecord> History = new List<TxRecord>();
/// <summary>硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。</summary>
public bool Poisoned;
/// <summary>被投毒的钱包被转空前的期望天数:门槛处 7 天,按价值线性降到满额处 1 天;不到门槛不会被转。</summary>
public static double DrainDays(double value)
{
double t = Math.Min(1, (value - Tuning.WalletDrainMin) / (Tuning.WalletDrainFull - Tuning.WalletDrainMin));
return Tuning.WalletDrainSlowDays - (Tuning.WalletDrainSlowDays - 1) * t;
}
/// <summary>被投毒的钱包今晚被转空的概率 = 1 / 期望天数(每晚独立掷一次)。</summary>
public static double DrainChance(double value) => value < Tuning.WalletDrainMin ? 0 : 1 / DrainDays(value);
/// <summary>转空:全部余额(含冻结的)连同污点、成本一起转走,返回转走的价值(信用点)。</summary>
public double Drain(Func<string, double> price)
{
double v = Value(price);
Balances.Clear(); Frozen.Clear(); FrozenUntil.Clear(); BuyQty.Clear(); BuyCost.Clear();
Dirty.Clear(); Basis = 0;
return v;
}
/// <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;
/// <summary>被发行方冻结的数量(只有可冻结的币:CRT 在镇压期间、CRC),和解冻的日子。冻结的币仍计入余额与估值,但不能花、不能转。</summary>
public Dictionary<string, double> Frozen = new Dictionary<string, double>();
public Dictionary<string, int> FrozenUntil = new Dictionary<string, int>();
/// <summary>可动用的余额(扣掉冻结部分)。</summary>
public double Avail(string asset) => Math.Max(0, Get(asset) - (Frozen.TryGetValue(asset, out var f) ? f : 0));
/// <summary>冻结该币当前余额的 share 比例,直到 untilDay(含)。已冻结的不重复冻。返回冻结的数量。</summary>
public double Freeze(string asset, double share, int untilDay)
{
double amount = Avail(asset) * Math.Max(0, Math.Min(1, share));
if (amount <= 1e-9) return 0;
Frozen[asset] = (Frozen.TryGetValue(asset, out var f) ? f : 0) + amount;
FrozenUntil[asset] = Math.Max(untilDay, FrozenUntil.TryGetValue(asset, out var u) ? u : 0);
return amount;
}
/// <summary>解冻到期的币,返回 (币种, 数量) 列表。</summary>
public List<KeyValuePair<string, double>> Thaw(int day)
{
var list = new List<KeyValuePair<string, double>>();
foreach (var a in new List<string>(FrozenUntil.Keys))
if (day >= FrozenUntil[a])
{
list.Add(new KeyValuePair<string, double>(a, Frozen.TryGetValue(a, out var f) ? f : 0));
Frozen.Remove(a); FrozenUntil.Remove(a);
}
return list;
}
public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0;
public void Add(string asset, double amount)
{
if (amount == 0) return;
double v = Get(asset) + amount;
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 had = Get(asset), v = had + amount;
if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足");
if (Math.Abs(v) < 1e-12) Balances.Remove(asset);
else Balances[asset] = v;
// 拿走的币按这种币的脏比例带走脏枚数
if (amount < 0 && Dirty.TryGetValue(asset, out var d)) SetDirty(asset, had > 1e-12 ? d * Math.Max(0, v) / had : 0);
}
/// <summary>这种币里脏的枚数(不超过余额)。</summary>
public double DirtyOf(string asset) => Dirty.TryGetValue(asset, out var d) ? Math.Max(0, Math.Min(d, Get(asset))) : 0;
/// <summary>这种币的脏比例(0–1)。</summary>
public double DirtyRatio(string asset) { double g = Get(asset); return g > 1e-12 ? DirtyOf(asset) / g : 0; }
/// <summary>这种币里干净的、能动用的枚数(不超过可动用余额)。钱包往 Poinbase 最多只能充这么多。</summary>
public double CleanAvail(string asset) => Math.Max(0, Math.Min(Avail(asset), Get(asset) - DirtyOf(asset)));
/// <summary>按现价算的脏币总价值(信用点):钱包总表、日志用。</summary>
public double Taint(Func<string, double> price) => Balances.Keys.Sum(a => DirtyOf(a) * price(a));
private void SetDirty(string asset, double d)
{
d = Math.Max(0, Math.Min(d, Get(asset)));
if (d < 1e-9) Dirty.Remove(asset); else Dirty[asset] = d;
}
/// <summary>收到 amount 枚币,其中 dirty 枚是脏的(不超过 amount)。</summary>
public void AddDirty(string asset, double amount, double dirty)
{
if (amount <= 0) return;
Add(asset, amount);
if (dirty > 0) SetDirty(asset, DirtyOf(asset) + Math.Min(dirty, amount));
}
/// <summary>把这种币里干净的 qty 枚记成脏的(刚收到的兑付被判内幕、操纵时用);不够的部分不记,返回没记上的枚数。</summary>
public double MarkDirty(string asset, double qty)
{
if (qty <= 0) return 0;
double clean = Math.Max(0, Get(asset) - DirtyOf(asset)), mark = Math.Min(qty, clean);
SetDirty(asset, DirtyOf(asset) + mark);
return qty - mark;
}
/// <summary>余额按比例缩水(CRC 托管银行减记):脏枚数同比例缩水。</summary>
public void Scale(string asset, double factor)
{
double g = Get(asset);
if (g <= 0) return;
double d = DirtyOf(asset);
Add(asset, g * Math.Max(0, factor) - g);
SetDirty(asset, d * Math.Max(0, factor));
}
/// <summary>兑换:付出 amount 枚 from(按它的脏比例带走脏枚数),收到 got 枚 to,脏比例跟过去;clean 为真时(兑入 Ponero)收到的是干净的。</summary>
public void Convert(string from, double amount, string to, double got, bool clean = false)
{
double r = DirtyRatio(from);
Add(from, -amount);
AddDirty(to, got, clean ? 0 : got * r);
}
/// <summary>花掉(销毁)一笔币:脏枚数按这种币的脏比例带走,成本按这笔币占钱包总价值的比例带走。</summary>
public void Spend(string asset, double amount, Func<string, double> price)
{
double total = Value(price), share = total > 0 ? Math.Min(1, amount * price(asset) / total) : 0;
double basis = Basis * share;
Add(asset, -amount); Basis -= basis;
}
/// <summary>按价格表估值(信用点)。</summary>
public double Value(Func<string, double> price) => Balances.Sum(kv => kv.Value * price(kv.Key));
/// <summary>按信用点额折成的诈骗罪证:额 × <see cref="Tuning.PredictFraudShare"/>,单次封顶 <see cref="Tuning.PredictFraudCap"/>(永续被判操纵、Tumbler 泄露用)。</summary>
public static double FraudFor(double credits) => Math.Min(Tuning.PredictFraudCap, Math.Max(0, credits) * Tuning.PredictFraudShare);
/// <summary>
/// 转出:taint 和 basis 按转出价值占地址总价值的比例带走。Ponero 等不可追踪资产转入时,污点不随之进入目标地址。
/// 只转干净的币(往 Poinbase 充值):脏枚数全部留在原钱包,成本按价值比例带走。数量超过 <see cref="CleanAvail"/> 的部分不转,返回实际转的数量。
/// </summary>
public static double TransferClean(Wallet from, Wallet to, string asset, double amount, Func<string, double> price)
{
amount = Math.Min(amount, from.CleanAvail(asset));
if (amount <= 0) return 0;
double total = from.Value(price);
double share = total > 0 ? Math.Min(1.0, amount * price(asset) / total) : 0;
double basis = from.Basis * share, dirty = from.DirtyOf(asset);
from.Add(asset, -amount);
from.SetDirty(asset, dirty);
from.Basis -= basis;
to.Add(asset, amount);
to.Basis += basis;
return amount;
}
/// <summary>
/// 转出:这种币按它的脏比例把脏枚数带过去,成本按转出价值占地址总价值的比例带走。
/// Ponero 等不可追踪资产转入时,不带脏枚数。
/// </summary>
public static void Transfer(Wallet from, Wallet to, string asset, double amount, Func<string, double> price)
{
if (amount <= 0) return;
double total = from.Value(price);
double share = total > 0 ? Math.Min(1.0, amount * price(asset) / total) : 0;
double taint = from.Taint * share, basis = from.Basis * share;
double basis = from.Basis * share, dirty = amount * from.DirtyRatio(asset);
from.Add(asset, -amount);
from.Taint -= taint;
from.Basis -= basis;
to.Add(asset, amount);
var spec = Assets.Get(asset);
if (spec == null || spec.Traceable) to.Taint += taint;
to.AddDirty(asset, amount, spec == null || spec.Traceable ? dirty : 0);
to.Basis += basis;
}
/// <summary>
/// 存档迁移:旧的整钱包污点额(信用点)按当前价格摊成各币的脏枚数:脏比例 = min(1, 污点 / 可追踪币的价值),每种可追踪的币按这个比例记脏。
/// extra 是另外挪进来的旧污点(Poinbase 账户、永续仓位),一起摊;超出的部分作废。旧字段清零。
/// </summary>
public void MigrateTaint(Func<string, double> price, double extra = 0)
{
double t = LegacyTaint + extra;
LegacyTaint = 0;
if (t <= 0) return;
var traceable = Balances.Keys.Where(a => Assets.Get(a) == null || Assets.Get(a).Traceable).ToList();
double v = traceable.Sum(a => Get(a) * price(a));
if (v <= 0) return;
double r = Math.Min(1, t / v);
foreach (var a in traceable) SetDirty(a, DirtyOf(a) + Get(a) * r);
}
}
}
+175
View File
@@ -0,0 +1,175 @@
using System;
using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
using UnityEngine;
namespace Decentralized
{
/// <summary>
/// RatDigg 矿机(M1 / M2 / M3):独立的机器,窗口里有电池槽、模块槽(和矿架同一套效果)和修理槽,没有显卡槽。
/// 夜间和矿架一样先从电池扣电、有电才挖;损耗与暴毙沿用 <see cref="Unit"/>(暴毙系数 0.5,每天损耗 0.8)。
/// 暴毙后成了"损坏的矿机":往修理槽里按顺序放电子元件修好(见 <see cref="AsicRepair"/>),健康扣 20 点。
/// </summary>
public static class AsicItems
{
public const string Prefix = "pd_asic_";
public const string StepTag = "PD_REPAIR_STEP", ProgTag = "PD_REPAIR_PROG";
public static string Id(string model) => Prefix + model.ToLowerInvariant();
public static bool Is(GameItem it) => it != null && it.identifier != null && it.identifier.StartsWith(Prefix);
public static void Register(ModItemDirectory dir)
{
foreach (var spec in Hardware.Specs.Values.Where(s => s.Asic))
{
var s = spec;
dir.Add(Id(s.Model), () => Create(s));
}
}
private static int Step(GameItem m) => m.IsTag(StepTag) ? m.GetTagReadonly(StepTag).GetInt() : 0;
private static int Prog(GameItem m) => m.IsTag(ProgTag) ? m.GetTagReadonly(ProgTag).GetInt() : 0;
private static readonly ConditionalWeakTable<GameItem, GameInventory> ModulesOf = new ConditionalWeakTable<GameItem, GameInventory>();
/// <summary>这件电子元件能不能修这台矿机:机器烧坏了,而且这一步要的就是这种元件。电子元件拖到机器上修,没有修理槽。</summary>
public static bool CanRepairWith(GameItem m, GameItem src)
{
if (!Is(m) || src == null || !GeneralHelper.IsItemOwned(src) || !GeneralHelper.IsItemOwned(m)) return false;
var u = Items.ToUnit(m);
var need = AsicRepair.Need(u.Model, Step(m));
return u.Dead && need != null && src.identifier == need.Value.Item;
}
/// <summary>修理:每次收一件,攒够这一步再进下一步,全部攒够就修好。</summary>
public static void TakeRepair(GameItem m, GameItem item)
{
if (!CanRepairWith(m, item) || !ModulesOf.TryGetValue(m, out var modules)) return;
var u = Items.ToUnit(m);
int step = Step(m), prog = Prog(m);
int took = AsicRepair.Insert(u.Model, ref step, ref prog, item.identifier, 1);
if (took <= 0) return;
m.ModifyTag(StepTag, s => s.Enable().SetInt(step));
m.ModifyTag(ProgTag, s => s.Enable().SetInt(prog));
if (item.unitCount <= 1) item.Destroy(); else item.GetSplitItem(1).Destroy();
if (AsicRepair.Done(u.Model, step))
{
u.Dead = false;
u.Health = Math.Max(10, u.Health - Tuning.AsicRepairHealthCut);
m.ModifyTag(Items.DeadTag, s => s.Disable());
m.ModifyTag(StepTag, s => s.Enable().SetInt(0));
m.ModifyTag(ProgTag, s => s.Enable().SetInt(0));
Items.Store(m, u);
Log.Info($"矿机 {u.Model} 修好了,健康 {u.Health:0}");
}
EmporiumAudioPlayer.Instance?.ratchet?.Play();
Update(m, modules);
}
private static GameItem Create(HardwareSpec spec)
{
var battery = new GameSlotInventory();
var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7);
GameItem machine = null;
var stHash = new MachineryText("", 6, TMPro.HorizontalAlignmentOptions.Left, false);
var stPower = new MachineryText("", 6, TMPro.HorizontalAlignmentOptions.Left, false);
var stCond = new MachineryText("", 6, TMPro.HorizontalAlignmentOptions.Left, false);
var addr = new MachineryStack(true, 6).Add(stHash).Add(stPower).Add(stCond).Add(Payout.Build(() => machine, out var stAddr));
var window = MachineHelper.CreateMachineryWindow(true,
MachineHelper.PowerPanel(battery),
MachineHelper.Panel(L.T("rig.label.modules"), modules, "module"),
MachineHelper.Panel(L.T("rig.label.status"), addr, "status", false));
var m = ItemDirectory.CreateEmptyItem(Id(spec.Model)).SetContentWindow(window);
machine = m;
Payout.Register(m, stAddr);
RigStatus.Add(m, new[] { stHash, stPower, stCond });
m.SetSpriteAndShapeFromMod(ModInfo.Id, "asic_" + spec.Model.ToLowerInvariant());
m.name = L.T("item.asic.name", spec.Model);
m.unitValue = spec.Price;
MachineHelper.SetupBatterySlot(battery, m);
m.EnableTag("CONTAINER_TAG");
m.EnableTag("STANDARD_MACHINE_TAG");
MachineryHelper.InitMachinery(m, (int)Math.Ceiling(spec.Kj));
MachineHelper.SetupModuleBay(modules, m, () => Update(m, modules));
ModulesOf.Remove(m); ModulesOf.Add(m, modules);
m.onLoaded = _ => Update(m, modules);
m.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿机扣电", () => NightDraw(m, battery, modules));
ModHelper.SetSource(m, ModInfo.Id, ModInfo.Name);
Update(m, modules);
return m;
}
/// <summary>名字、说明、价值与耗电(耗电按模块效果重算)。</summary>
private static void Update(GameItem m, GameInventory modules)
{
var u = Items.ToUnit(m);
var b = Items.Bonus(m, modules);
int baseKj = (int)Math.Ceiling(u.Label.Kj);
m.ModifyTag("MACHINERY_DRAWN_POWER_BASE", s => s.Enable().SetInt(baseKj));
m.ModifyTag(MachineryHelper.MACHINERY_DRAWN_POWER, s => s.Enable().SetInt(u.Dead ? 0 : (int)Math.Ceiling(u.Kj(b))));
RigStatus.ShowAsic(m, u.Dead ? 0 : u.Hash(b) * 10 / 1000, u.Dead ? 0 : (int)Math.Ceiling(u.Kj(b)), u.Health, u.Dead);
Payout.Refresh(m);
Describe(m, u);
}
/// <summary>按标签刷新名字、说明、价值(夜间损耗之后也走这里)。</summary>
public static void Refresh(GameItem m) => Describe(m, Items.ToUnit(m));
private static void Describe(GameItem m, Unit u)
{
var spec = u.Label;
m.name = L.T(u.Dead ? "item.asic.dead.name" : "item.asic.name", spec.Model);
if (u.Dead)
{
var need = AsicRepair.Need(u.Model, Step(m));
m.shortDescription = need == null ? L.T("item.asic.dead.fixed") : L.T("item.asic.dead.desc", L.T("mat." + need.Value.Item), need.Value.Count - Prog(m));
m.unitValue = Math.Max(1, (long)Math.Round(spec.Price * Tuning.AsicDeadValue));
return;
}
string cond = GeneralHelper.IsItemOwned(m)
? Mathf.RoundToInt((float)u.Health) + "%"
: L.T(u.Health >= 85 ? "item.gpu.grade.1" : u.Health >= 60 ? "item.gpu.grade.2" : u.Health >= 35 ? "item.gpu.grade.3" : "item.gpu.grade.4");
m.shortDescription = L.T("item.asic.desc", Fmt.Num(spec.DisplayHash), Fmt.Num(spec.Kj), cond);
m.unitValue = CardValue.Of(spec.Model, u.Health);
}
private static void NightDraw(GameItem m, GameSlotInventory battery, GameInventory modules)
{
if (TurboMine.Boosting) { Boost(m, battery, modules); return; }
if (!Session.TonightDrawn.Add(m.uniqueId)) return;
var u = Items.ToUnit(m);
Session.TonightRigs++;
if (u.Dead) return;
var b = Items.Bonus(m, modules);
int kj = (int)Math.Ceiling(u.Kj(b));
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return;
Session.TonightPoweredRigs++;
Session.TonightKj += kj;
Session.TonightCards.Add(new KeyValuePair<GameItem, ModuleBonus>(m, b));
Session.TonightMachine[m] = m;
Update(m, modules);
}
/// <summary>涡轮增压:扣一晚的电,立刻挖一轮(不影响今晚照常挖矿)。</summary>
private static void Boost(GameItem m, GameSlotInventory battery, GameInventory modules)
{
var u = Items.ToUnit(m);
if (u.Dead) return;
var b = Items.Bonus(m, modules);
int kj = (int)Math.Ceiling(u.Kj(b));
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) { Log.Info("涡轮增压:矿机电池电量不够,这一轮没挖"); return; }
TurboMine.Round(m, new List<KeyValuePair<GameItem, ModuleBonus>> { new KeyValuePair<GameItem, ModuleBonus>(m, b) });
Update(m, modules);
}
/// <summary>造一台矿机:按健康度写标签。</summary>
public static GameItem New(string model, double health)
{
var item = ModHelper.SpawnItem(Id(model));
Items.Store(item, new Unit { Model = model, Health = health });
return item;
}
}
}
+223
View File
@@ -0,0 +1,223 @@
using System;
using System.Linq;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// Web3 黑市的货物进出:每周刷新现货与订单目录,夜间推进订单并写夜间日志,
/// 上门(快递员当顾客上门)把到货交给玩家。
/// </summary>
public static class BlackDelivery
{
public static void Subscribe()
{
ModHook.OnGenerateCustomerNormal += OnGenerate;
}
public static void Unsubscribe()
{
ModHook.OnGenerateCustomerNormal -= OnGenerate;
}
private static readonly System.Collections.Generic.Dictionary<string, long> ValueCache = new System.Collections.Generic.Dictionary<string, long>();
public static long Value(string id)
{
if (ValueCache.TryGetValue(id, out var cached)) return cached;
var it = ModHelper.SpawnItem(id);
long v = 0;
if (it != null) { v = it.GetBaseValue(); it.Destroy(); }
ValueCache[id] = v;
return v;
}
/// <summary>货架上的货对应的游戏物品 id:显卡是 "gpu:型号",对应本 mod 的显卡物品。</summary>
public static string GameId(string id) => id.StartsWith("gpu:") ? (Hardware.Specs.TryGetValue(id.Substring(4), out var sp) && sp.Asic ? AsicItems.Id(sp.Model) : Items.GpuId(id.Substring(4))) : id;
/// <summary>造出这件货:显卡按成色与(可能的)假卡真身造,不带卖家的说法;其余直接造。</summary>
public static GameItem Make(string id, string model, string fakeOf, double health)
{
if (id.StartsWith("gpu:")) return Hardware.Specs[model].Asic ? AsicItems.New(model, health) : Items.NewCard(model, health, fakeOf, Session.Data.Rng);
if (id == Items.HwWallet) return Items.NewHwWallet(Session.Data.Rng.Chance(Scales.R(Tuning.WalletPoisonShare)));
return ModHelper.SpawnItem(id);
}
/// <summary>商家名:随机的匿名哈希,类型另外标注。</summary>
public static string DisplayName(string name) => name;
public static string ItemName(string id)
{
var it = ModHelper.SpawnItem(GameId(id));
if (it == null) return id;
string n = it.name;
it.Destroy();
return n;
}
// ---- 玩家出售:从储藏区挑能上架的货 ----
/// <summary>这件物品在黑市上的货 id(显卡是 "gpu:型号",其余是物品 id);不能上架返回 null。</summary>
public static string SellId(GameItem it)
{
if (it == null || it.identifier == null) return null;
if (Items.IsGpu(it) || AsicItems.Is(it)) return "gpu:" + Items.ToUnit(it).Model;
string id = it.identifier;
return Merchants.IsGood(id) ? id : null;
}
/// <summary>自己的储藏区、背包不能上架:卖掉会带走里面的东西,也会丢掉容量。</summary>
private static readonly System.Collections.Generic.HashSet<string> StorageIds = new System.Collections.Generic.HashSet<string>(Merchants.Table[MerchantType.Storage].Select(e => e.Id));
public static System.Collections.Generic.List<GameItem> Sellable() =>
GraphUtils.FindAllChildrenType<GameItem>(EmporiumEntry.Instance.invElement, i => GeneralHelper.IsItemOwned(i) && SellId(i) != null && !i.IsTag(Items.DeadTag) && !StorageIds.Contains(i.identifier) && i.FindAllChildItems().Count == 0, i => i.identifier != Items.Rig);
/// <summary>参考价:显卡按标称型号与成色,其余按游戏基础价。</summary>
public static long RefValue(GameItem it)
{
string id = SellId(it);
if (id == null) return 0;
if (Items.IsGpu(it) || AsicItems.Is(it)) { var u = Items.ToUnit(it); return CardValue.Of(u.Model, u.Health); }
return Value(id);
}
public static GoodKind KindOf(string id) => id.StartsWith("gpu:") ? GoodKind.Gpu : Merchants.Info(id).Kind;
/// <summary>每周刷新:现货与订单目录一起。返回是否刷新了。</summary>
public static bool Restock(SaveData d, int day, bool force = false)
{
int rep = StoreReputation.GetBMReputation();
if (d.Black.Closed(day)) return false;
if (force) d.Black.RefreshedDay = -999;
if (!d.Black.Refresh(day, rep, d.Rng, Value)) return false;
d.Black.RefreshOrders(day, rep, d.Rng, Value);
// 平台现货偶尔有硬件钱包:比杰克逊博士便宜,外观一样,其中一部分被动过手脚
if (d.Rng.Chance(Tuning.WalletMallChance))
d.Black.Offers.Add(new BmOffer { Item = Items.HwWallet, Kind = GoodKind.Tech, Price = d.Rng.Range(Tuning.WalletMallMin, Tuning.WalletMallMax), Qty = d.Rng.Range(1, Tuning.WalletMallQtyMax) });
Log.Info($"黑市刷新(第 {day} 天):现货 {d.Black.Offers.Count} 种,商家 {d.Black.Sellers.Count} 家({string.Join("/", d.Black.Sellers.Select(x => x.Type))}),货架 {d.Black.Catalog.Count} 种共 {d.Black.CatalogValue:0} 信用点,高科技 {d.Black.Offers.Count(o => o.Kind == GoodKind.Tech)},显卡 {d.Black.Offers.Count(o => o.Kind == GoodKind.Gpu)}");
return true;
}
public static void Night(SaveData d, int day, bool crackdown)
{
var ps = PlayerStore.Instance;
foreach (var e in d.Black.Night(day, crackdown, d.WalletAt, d.Rng, StoreReputation.GetSecReputation(), Session.Price))
{
if (e.Sale != null)
{
string sname = ItemName(e.Sale.Item);
Log.Info($"黑市出售事件:{e.Kind} {e.Sale.Item} ×{e.Sale.Mult} 货款 {e.Amount:0} 污点 {e.Sale.Taint:0}");
switch (e.Kind)
{
case "sale_sold": ps.AddNightLog(L.T("nightlog.bm.sale_sold", sname, Fmt.Num(e.Amount), e.Sale.PayDay), "99bb66"); break;
case "sale_paid": ps.AddNightLog(L.T("nightlog.bm.sale_paid", sname, Fmt.Num(e.Amount), Fmt.Num(e.Sale.Taint)), "99bb66"); break;
case "sale_fake":
Items.Give(Make(e.Sale.Item, e.Sale.Model, e.Sale.FakeOf, e.Sale.Health));
ps.AddNightLog(L.T("nightlog.bm.sale_fake", sname), "cc7755");
break;
case "sale_rug": ps.AddNightLog(L.T("nightlog.bm.sale_rug", sname, Fmt.Num(e.Amount)), "cc7755"); break;
}
if (e.Kind == "sale_rug") News.Show("pd_news_bmrug_" + day, L.T("news.bmrug.title"), L.T("news.bmrug.desc"), 4);
continue;
}
string name = e.Order != null && e.Kind != "trap" ? ItemName(e.Order.Item) : null;
Log.Info($"黑市订单事件:{e.Kind} {e.Order?.Item} {e.Order?.Seller} {e.Order?.Mode} 受控 {e.Order?.Controlled} 金额 {e.Amount:0}");
switch (e.Kind)
{
case "ready": ps.AddNightLog(L.T("nightlog.bm.ready", name), "99bb66"); break;
case "scam_refund": ps.AddNightLog(L.T("nightlog.bm.scam_refund", DisplayName(e.Order.Seller), name), "cc7755"); break;
case "scam_loss": ps.AddNightLog(L.T("nightlog.bm.scam_loss", DisplayName(e.Order.Seller), name, Fmt.Num(e.Amount)), "cc7755"); break;
case "intercept": ps.AddNightLog(L.T("nightlog.bm.intercept", name, Fmt.Num(e.Amount)), "cc7755"); break;
case "rug": ps.AddNightLog(L.T("nightlog.bm.rug", name, Fmt.Num(e.Amount)), "cc7755"); break;
case "trap":
{
int amount = (int)Math.Min(Tuning.CrimeEventCap, e.Amount);
ps.secData.CommitCrime(e.Order.Item != null && e.Order.Item.StartsWith("gpu:") ? "FENCING" : "CONTRABAND_TRAFFICKING", amount);
Log.Info($"黑市追查:记罪证 {amount}({e.Order.Seller})");
ps.AddNightLog(L.T("nightlog.bm.trap." + e.Order.Seller, ItemName(e.Order.Item), amount), "cc7755");
break;
}
case "reopen": ps.AddNightLog(L.T("nightlog.bm.reopen"), "99bb66"); break;
}
if (e.Kind == "rug") Night_RugNews(day);
}
if (Unlock.BlackUnlocked && !crackdown) Restock(d, day);
}
private static void Night_RugNews(int day) => News.Show("pd_news_bmrug_" + day, L.T("news.bmrug.title"), L.T("news.bmrug.desc"), 4);
// ---- 上门:开门后有到货的当天,快递员排进顾客队伍 ----
private static int courierDay = -1;
private static void OnGenerate(StoreClientManager manager)
{
if (Session.Data == null || !Unlock.BlackUnlocked) return;
Log.Guard("黑市快递员", () =>
{
int day = StoreStation.GetDayCounter();
if (day == courierDay || !Session.Data.Black.Ready.Any()) return;
courierDay = day;
manager.AddClient(CreateCourier());
});
}
public static StoreClient CreateCourier()
{
var rng = Session.Data.Rng;
var c = new StoreClient
{
identifier = "pd_bm_courier",
displayName = L.T("courier.name"),
spriteName = SpriteDict.GetRandomScavSpriteName(),
clientIntent = StoreClient.ClientIntent.SELL,
};
StoreClientFactionSetup.InitScav(c);
var trap = Session.Data.Black.Ready.FirstOrDefault(o => o.Controlled);
Action finish = () => { PlayerStore.Instance.DismissCurrentClient(); };
if (trap == null)
{
c.mainDialogue.SetText(c.displayName, L.T("courier.line1." + rng.Range(1, 2))).NextDialogue().SetText(c.displayName, L.T("courier.line2." + rng.Range(1, 2)))
.SetEndAction(() => Log.Guard("黑市快递交货", () => { DeliverDoor(); finish(); }));
}
else
{
// 受控交付:快递员神色异常,签收就是当场记罪证并查封,拒签只损失货款
var sign = new Dialogue();
sign.SetText("PLAYER", L.T("courier.sign.say"));
sign.NextDialogue().SetText(c.displayName, L.T("courier.signed")).SetEndAction(() => Log.Guard("受控交付签收", () =>
{
int amount = (int)Math.Min(Tuning.CrimeEventCap, Session.Data.Black.Sign(trap, true));
PlayerStore.Instance.secData.CommitCrime(trap.Item.StartsWith("gpu:") ? "FENCING" : "CONTRABAND_TRAFFICKING", amount);
Log.Info($"受控交付:签收,记罪证 {amount},货被查封");
DeliverDoor(); finish();
}));
var refuse = new Dialogue();
refuse.SetText("PLAYER", L.T("courier.refuse.say"));
refuse.NextDialogue().SetText(c.displayName, L.T("courier.refused")).SetEndAction(() => Log.Guard("受控交付拒签", () => { Session.Data.Black.Sign(trap, false); Log.Info("受控交付:拒签"); DeliverDoor(); finish(); }));
c.mainDialogue.SetText(c.displayName, L.T("courier.odd1")).NextDialogue().SetText(c.displayName, L.T("courier.odd2"))
.AddChoice(DirectoryMaster.Item("yes"), L.T("courier.sign"), "", sign, null)
.AddChoice(DirectoryMaster.Item("cancel"), L.T("courier.refuse"), "", refuse, null);
}
c.AddBasicDialog();
c.CompleteClientCreation();
return c;
}
private static void DeliverDoor()
{
var d = Session.Data;
foreach (var o in d.Black.Ready.Where(x => !x.Controlled).ToList())
{
for (int i = 0; i < o.Count; i++)
{
var made = Make(o.Item, o.Model, o.FakeOf, o.Health);
Items.Give(made);
if (o.Item.StartsWith("gpu:")) { var u = Items.ToUnit(made); Log.Info($"黑市到货显卡:标称 {u.Model} 真身 {(u.FakeOf ?? "(真卡)")} 成色 {u.Health:0} 外观 {u.Look.Encode()}"); }
}
d.Black.Deliver(o);
Log.Info($"黑市快递:{o.Item} ×{o.Count}{(o.FakeOf != null ? "(假卡,真身 " + o.FakeOf + ")" : "")} 已放进储藏区");
}
Session.Save();
}
}
}
+249
View File
@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Decentralized.Domain;
using UnityEngine;
namespace Decentralized
{
/// <summary>
/// 日 K 图:运行时画到一张小 Texture2D 上(一根蜡烛 5 像素宽、间隔 1),点采样放大 2 倍显示,和游戏的像素风格一致。
/// 上面是价格(涨绿跌红、影线、7 日均线、可选阈值虚线),下面是成交量(玩家青色叠在大户灰色上)。纹理按用途缓存复用。
/// </summary>
public static class CandleChart
{
/// <summary>画图的币种:全部可交易资产。</summary>
public static readonly string[] ChartAssets = { Assets.PSC, Assets.SPARK, Assets.CRT, Assets.CRA, Assets.CRC, Assets.Ponero };
private static double VolShare(string a) =>
a == Assets.PSC ? Tuning.ChartVolPsc : a == Assets.CRC ? Tuning.ChartVolCrc : a == Assets.CRT ? Tuning.ChartVolCrt :
a == Assets.CRA ? Tuning.ChartVolCra : a == Assets.SPARK ? Tuning.ChartVolSpark : Tuning.ChartVolPonero;
/// <summary>开门:每个币开一根蜡烛(开盘接昨天收盘,夜里的价格变化算进来)。</summary>
public static void Morning(SaveData d, int day)
{
foreach (var a in ChartAssets) d.Candles.Open(a, day, Session.Price(a));
}
/// <summary>白天价格变了(不计成交)。</summary>
public static void Touch(SaveData d, int day, params string[] assets)
{
foreach (var a in assets) d.Candles.Tick(a, day, Session.Price(a));
}
/// <summary>玩家的一笔成交:按信用点计进这个币当天的成交量。</summary>
public static void Trade(string asset, double credits)
{
var d = Session.Data;
if (d == null || Array.IndexOf(ChartAssets, asset) < 0 || credits <= 0) return;
d.Candles.Tick(asset, StoreStation.GetDayCounter(), Session.Price(asset), credits, true);
}
/// <summary>夜间结算:补上当天 NPC 的成交量,收盘。在价格的夜间更新之前调用。</summary>
public static void Close(SaveData d, int day)
{
double depth = Tuning.PoolDepth(day);
foreach (var a in ChartAssets)
{
double p = Session.Price(a);
var c = d.Candles.Today(a, day, p);
double move = c.O > 0 ? Math.Abs(p / c.O - 1) + (c.H - c.L) / c.O : 0;
d.Candles.Tick(a, day, p, CandleBook.NpcVolume(depth, VolShare(a), move, d.Rng));
d.Candles.Close(a, day, p);
}
}
private static readonly Dictionary<string, string> Picked = new Dictionary<string, string>();
public const int Days = 30, Step = 6, Body = 5, PriceH = 70, Gap = 3, VolH = 16, Pad = 2, AxisW = 22, DateH = 7;
public const int W = Days * Step + Pad * 2 + AxisW, H = PriceH + Gap + VolH + Pad * 2 + DateH;
public static float Scale => TerminalWindow.ChartScale;
private static readonly Color32 Bg = new Color32(30, 31, 34, 255), Grid = new Color32(48, 50, 54, 255);
private static readonly Color32 Up = new Color32(108, 190, 112, 255), Down = new Color32(214, 94, 88, 255);
private static readonly Color32 Ma = new Color32(226, 196, 92, 255), Thr = new Color32(150, 170, 230, 255);
private static readonly Color32 VolNpc = new Color32(92, 96, 104, 255), VolMe = new Color32(96, 200, 210, 255);
private static readonly Color32 Txt = new Color32(150, 154, 160, 255), TagBg = new Color32(226, 196, 92, 255), TagTxt = new Color32(30, 31, 34, 255);
/// <summary>3×5 像素数字字体(每行 3 位,自上而下)。</summary>
private static readonly Dictionary<char, string[]> Glyph = new Dictionary<char, string[]>
{
['0'] = new[] { "111", "101", "101", "101", "111" }, ['1'] = new[] { "010", "110", "010", "010", "111" },
['2'] = new[] { "111", "001", "111", "100", "111" }, ['3'] = new[] { "111", "001", "011", "001", "111" },
['4'] = new[] { "101", "101", "111", "001", "001" }, ['5'] = new[] { "111", "100", "111", "001", "111" },
['6'] = new[] { "111", "100", "111", "101", "111" }, ['7'] = new[] { "111", "001", "010", "010", "010" },
['8'] = new[] { "111", "101", "111", "101", "111" }, ['9'] = new[] { "111", "101", "111", "001", "111" },
['.'] = new[] { "0", "0", "0", "0", "1" }, ['-'] = new[] { "000", "000", "111", "000", "000" },
['k'] = new[] { "100", "101", "110", "101", "101" },
};
private static int TextW(string s) => s.Sum(ch => Glyph.TryGetValue(ch, out var g) ? g[0].Length + 1 : 0) - 1;
/// <summary>在 (x, y) 处写字,y 是字的底边。</summary>
private static void Text(Color32[] px, int x, int y, string s, Color32 c)
{
foreach (char ch in s)
{
if (!Glyph.TryGetValue(ch, out var g)) continue;
for (int r = 0; r < 5; r++)
for (int k = 0; k < g[r].Length; k++)
if (g[r][k] == '1') Set(px, x + k, y + 4 - r, c);
x += g[0].Length + 1;
}
}
/// <summary>刻度上的价格:大于 100 不带小数,越小小数越多,最多 5 个字符左右。</summary>
private static string Num(double p)
{
if (p >= 10000) return (p / 1000).ToString("0", CultureInfo.InvariantCulture) + "k";
if (p >= 100) return p.ToString("0", CultureInfo.InvariantCulture);
if (p >= 10) return p.ToString("0.0", CultureInfo.InvariantCulture);
if (p >= 1) return p.ToString("0.00", CultureInfo.InvariantCulture);
return p.ToString("0.000", CultureInfo.InvariantCulture).Substring(1);
}
private sealed class Slot { public Texture2D Tex; public Sprite Sprite; public Color32[] Px; }
private static readonly Dictionary<string, Slot> Slots = new Dictionary<string, Slot>();
/// <summary>画一张图,返回可以交给 AddImage 的 Sprite。threshold &gt; 0 时画一条水平虚线(预测盘的阈值)。</summary>
public static Sprite Render(string key, IList<Candle> all, double threshold = 0)
{
if (!Slots.TryGetValue(key, out var s) || s.Tex == null)
{
var tex = new Texture2D(W, H, TextureFormat.RGBA32, false) { filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp };
s = Slots[key] = new Slot { Tex = tex, Px = new Color32[W * H], Sprite = Sprite.Create(tex, new Rect(0, 0, W, H), new Vector2(0.5f, 0.5f), 100f) };
}
var px = s.Px;
for (int i = 0; i < px.Length; i++) px[i] = Bg;
var c = all.Skip(Math.Max(0, all.Count - Days)).ToList();
int volY0 = Pad + DateH, priceY0 = volY0 + VolH + Gap, plotR = Pad + Days * Step, axisX = plotR + 2;
// 网格:价格区四等分
for (int g = 1; g < 4; g++) HLine(px, Pad, plotR - 1, priceY0 + PriceH * g / 4, Grid);
VLine(px, plotR, volY0, priceY0 + PriceH - 1, Grid);
if (c.Count > 0)
{
var ma = CandleBook.Ma(all, 7).Skip(Math.Max(0, all.Count - Days)).ToList();
double lo = Math.Min(c.Min(x => x.L), ma.Min()), hi = Math.Max(c.Max(x => x.H), ma.Max());
if (threshold > 0) { lo = Math.Min(lo, threshold); hi = Math.Max(hi, threshold); }
double span = Math.Max(hi - lo, hi * 0.02);
lo -= span * 0.05; hi += span * 0.05; span = hi - lo;
Func<double, int> y = p => priceY0 + (int)Math.Round((p - lo) / span * (PriceH - 1));
double vmax = Math.Max(1, c.Max(x => x.VolPlayer + x.VolNpc));
int x0 = Pad + (Days - c.Count) * Step; // 不足 30 根时靠右
// 均线先画,蜡烛盖在上面(贴着锚的稳定币不会被均线遮住)
for (int i = 1; i < ma.Count; i++) Line(px, x0 + (i - 1) * Step + Body / 2, y(ma[i - 1]), x0 + i * Step + Body / 2, y(ma[i]), Ma);
for (int i = 0; i < c.Count; i++)
{
var k = c[i];
int left = x0 + i * Step, mid = left + Body / 2;
var col = k.C >= k.O ? Up : Down;
VLine(px, mid, y(k.L), y(k.H), col);
int b0 = y(Math.Min(k.O, k.C)), b1 = Math.Max(b0, y(Math.Max(k.O, k.C)));
for (int xx = left; xx < left + Body; xx++)
{
// 当天还没收盘的蜡烛画成空心
if (!k.Closed && xx > left && xx < left + Body - 1 && b1 - b0 > 1) { Set(px, xx, b0, col); Set(px, xx, b1, col); continue; }
VLine(px, xx, b0, b1, col);
}
int hn = (int)Math.Round(k.VolNpc / vmax * VolH), hm = (int)Math.Round(k.VolPlayer / vmax * VolH);
for (int xx = left; xx < left + Body; xx++)
{
if (hn > 0) VLine(px, xx, volY0, volY0 + hn - 1, VolNpc);
if (hm > 0) VLine(px, xx, volY0 + hn, volY0 + hn + hm - 1, VolMe);
}
}
if (threshold > 0) { int ty = y(threshold); for (int xx = Pad; xx < plotR; xx++) if ((xx / 3) % 2 == 0) Set(px, xx, ty, Thr); }
// 右侧刻度:三条网格线的价格;最新收盘价标成黄底的价签,和网格刻度挤在一起时让开
int lastY = y(c[c.Count - 1].C);
for (int g = 1; g < 4; g++)
{
int gy = priceY0 + PriceH * g / 4;
if (Math.Abs(gy - lastY) < 7) continue;
double gp = lo + (gy - priceY0) / (double)(PriceH - 1) * span;
Text(px, axisX, gy - 2, Num(gp), Txt);
}
if (threshold > 0) { int ty = y(threshold); if (Math.Abs(ty - lastY) >= 7) Text(px, axisX, ty - 2, Num(threshold), Thr); }
string tag = Num(c[c.Count - 1].C);
int ty0 = Math.Max(priceY0, Math.Min(priceY0 + PriceH - 7, lastY - 3));
for (int yy = ty0; yy < ty0 + 7; yy++) HLine(px, axisX - 1, Math.Min(W - 1, axisX + TextW(tag)), yy, TagBg);
Text(px, axisX, ty0 + 1, tag, TagTxt);
// 底部日期:天数是 5 的倍数的蜡烛下面写天数
for (int i = 0; i < c.Count; i++)
{
if (c[i].Day % 5 != 0) continue;
string ds = c[i].Day.ToString(CultureInfo.InvariantCulture);
int cx = x0 + i * Step + Body / 2;
VLine(px, cx, volY0 - 1, volY0 - 1, Txt);
Text(px, Math.Max(0, Math.Min(plotR - TextW(ds), cx - TextW(ds) / 2)), Pad - 1, ds, Txt);
}
// 成交量区右上:这一屏最大的量
Text(px, axisX, volY0 + VolH - 5, Num(vmax), VolNpc);
}
s.Tex.SetPixels32(px);
s.Tex.Apply(false);
return s.Sprite;
}
/// <summary>图上方的一行说明:最高、最低、最新收盘,涨跌。</summary>
public static string Caption(string asset, IList<Candle> all)
{
var c = all.Skip(Math.Max(0, all.Count - Days)).ToList();
if (c.Count == 0) return L.T("chart.empty", asset);
var last = c[c.Count - 1];
double prev = c.Count > 1 ? c[c.Count - 2].C : last.O, chg = prev > 0 ? last.C / prev - 1 : 0;
return L.T("chart.caption", asset, c.Count, Fmt.Num(c.Max(x => x.H)), Fmt.Num(c.Min(x => x.L)), Fmt.Num(last.C), (chg * 100).ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture));
}
private static void Set(Color32[] px, int x, int y, Color32 c) { if (x >= 0 && x < W && y >= 0 && y < H) px[y * W + x] = c; }
private static void HLine(Color32[] px, int x0, int x1, int y, Color32 c) { for (int x = x0; x <= x1; x++) Set(px, x, y, c); }
private static void VLine(Color32[] px, int x, int y0, int y1, Color32 c) { if (y1 < y0) (y0, y1) = (y1, y0); for (int y = y0; y <= y1; y++) Set(px, x, y, c); }
private static void Line(Color32[] px, int x0, int y0, int x1, int y1, Color32 c)
{
int dx = Math.Abs(x1 - x0), dy = -Math.Abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1, err = dx + dy;
while (true)
{
Set(px, x0, y0, c);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
/// <summary>把一个币的图放进界面:说明一行、黑框包裹的图、图例一行。</summary>
public static void Add(CustomUIBuilder b, string key, string asset, double threshold = 0)
{
var all = Session.Data.Candles.Of(asset);
b.AddLabel(Caption(asset, all), "chart_caption_dim");
b.BeginPanel(W * Scale + 4f, H * Scale + 4f, TextAnchor.MiddleCenter, 2f, 0f, "chart_bezel");
PledgerSkin.Slice(b.Last.image, "ui_hw_bezel", 5);
b.AddImage(Render(key, all, threshold), W * Scale, H * Scale);
b.End();
b.AddLabel(L.T(threshold > 0 ? "chart.legend.thr" : "chart.legend"), "chart_legend_dim");
}
/// <summary>某张图当前选中的币(没选过或不在列表里时是 fallback)。</summary>
public static string Selected(string key, string fallback, IList<string> assets = null)
{
var list = assets ?? ChartAssets.Where(a => a != Assets.Ponero || Unlock.BlackUnlocked).ToList();
return Picked.TryGetValue(key, out var cur) && list.Contains(cur) ? cur : fallback;
}
/// <summary>带币种切换的图:上面一排币种按钮(选中项带准星角标),切换后重画。assets 为空时是全部币种。</summary>
public static void AddPicker(CustomUIBuilder b, string key, string fallback, Action refresh, ICollection<GameObject> picked = null, IList<string> assets = null)
{
var list = (assets ?? ChartAssets.Where(a => a != Assets.Ponero || Unlock.BlackUnlocked)).ToList();
string cur = Selected(key, fallback, list);
b.BeginRow(4f);
foreach (var a in list)
{
string asset = a;
PledgerSkin.Pick(b, asset, asset == cur, () => { Picked[key] = asset; refresh(); }, picked);
}
b.End();
Add(b, key, cur);
}
}
}
+276
View File
@@ -0,0 +1,276 @@
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 用币付款的顾客:每天排队的买家里,按派系(上层 CRC、黑市 CRT、革命派 CRA、下层 PSC)有一小部分改用币结算。
/// 成交时在店里扣回刚入账的信用点,换成币进热钱包;赃物的款项不记销赃,改成币带污点(货值 × 赃物热度%)。
/// </summary>
public static class CoinCustomers
{
private static readonly HashSet<StoreClient> Marked = new HashSet<StoreClient>();
private static int markDay = -1, supportDay = -1;
public static void Subscribe()
{
ModHook.OnGenerateCustomerLate += OnGenerate;
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Run night (test)", () => Log.Guard("过夜(测试)", () =>
{
// 走游戏自己的夜间流程(EndNight 会触发本 mod 的夜间结算),再进下一天并开门前的日程
// 游戏过夜会清空前柜台,先把自己的东西收进物品栏
foreach (var it in EmporiumEntry.Instance.frontInvinvElement.childItems.ToList())
if (GeneralHelper.IsItemOwned(it)) GraphUtils.TryAcceptAll(EmporiumEntry.Instance.invElement, it);
PlayerStore.Instance.EndNight();
StoreStation.Instance.storeEventManager.OnDayEnd(); // 让事件按天数过期
StoreStation.Instance.StartDay();
Log.Info($"过夜完成,现在是第 {StoreStation.GetDayCounter()} 天");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Morning report (test)", () => Log.Guard("晨间日志(测试)", () => StartOfDayUIManager.Instance.ShowMorningReport()));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Newspaper (test)", () => Log.Guard("报纸(测试)", () => NewsUIManager.Instance.ToggleUI()));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Start crackdown (test)", () => Log.Guard("镇压(测试)", () =>
{
StoreStation.Instance.storeEventManager.ActivateEvent(OperationListSec.Crackdown());
Log.Info("镇压事件已激活");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Frozen CRT (test)", () => Log.Guard("冻结 CRT(测试)", () =>
{
var w = Session.Data.Hot;
w.Add(Assets.CRT, 1000);
double f = w.Freeze(Assets.CRT, Tuning.CrtFreezeShare, StoreStation.GetDayCounter() + Tuning.CrtUnfreezeAfter);
Log.Info($"CRT {w.Get(Assets.CRT):0},冻结 {f:0},可动用 {w.Avail(Assets.CRT):0}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tainted wallet (test)", () => Log.Guard("带污点的钱包(测试)", () =>
{
var w = Session.Data.Hot;
w.AddDirty(Assets.PSC, 100, 30); w.AddDirty(Assets.CRT, 1000, 1000); w.Basis += 2000;
Log.Info($"钱包:PSC {w.Get(Assets.PSC):0.##}(脏 {w.DirtyOf(Assets.PSC):0.##}),CRT {w.Get(Assets.CRT):0}(脏 {w.DirtyOf(Assets.CRT):0})");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Terminal (test)", () => Log.Guard("终端(测试)", TerminalWindow.Open));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market +50 rep, restock (test)", () => Log.Guard("黑市(测试)", () =>
{
StoreReputation.ModReputation("FACTION_BLACK_MARKET", 50);
if (!Unlock.IsUnlocked) ModHelper.GetNetworkUpgrade(Unlock.Id)?.Unlock();
if (!Unlock.BlackUnlocked) ModHelper.GetNetworkUpgrade(Unlock.BlackId)?.Unlock();
if (!Unlock.MixerUnlocked) ModHelper.GetNetworkUpgrade(Unlock.MixerId)?.Unlock();
var d = Session.Data;
d.Hot.Add(Assets.PSC, 20); d.Hot.AddDirty(Assets.CRT, 3000, 2000); d.Hot.Basis += 1000;
BlackDelivery.Restock(d, StoreStation.GetDayCounter(), true);
Log.Info($"黑市:声望 {StoreReputation.GetBMReputation()},上架 {d.Black.Offers.Count} 种:{string.Join(", ", d.Black.Offers.Select(o => o.Item + "×" + o.Qty + "@" + o.Price))},解锁 {Unlock.BlackUnlocked}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market sample (test)", () => Log.Guard("黑市抽样(测试)", () =>
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
var vals = new System.Collections.Generic.List<double>();
var tech = new System.Collections.Generic.List<double>();
for (int i = 0; i < 60; i++)
{
BlackDelivery.Restock(d, day, true);
vals.Add(d.Black.CatalogValue);
}
var missing = Merchants.Table.SelectMany(kv => kv.Value.Select(e => (kv.Key, e.Id))).Where(x => BlackDelivery.Value(x.Item2) <= 0).Select(x => x.Item1 + ":" + x.Item2).ToList();
var costs = Merchants.Table.SelectMany(kv => kv.Value.Select(e => (kv.Key, e.Id))).Select(x => BlackDelivery.Value(x.Item2)).Where(v => v > 0).OrderBy(v => v).ToList();
Log.Info("黑市抽样:基础价 >2500 的货:" + string.Join(", ", Merchants.Table.SelectMany(kv => kv.Value.Select(e => (kv.Key, e.Id))).Select(x => (id: x.Item2, v: BlackDelivery.Value(x.Item2))).Where(x => x.v > 2500).Select(x => x.id + "=" + x.v)));
Log.Info($"黑市抽样:无法造出或价格为 0 的货 {missing.Count} 个:{string.Join(", ", missing)};基础价中位 {costs[costs.Count / 2]},最高 {costs.Last()}");
Log.Info($"黑市抽样:60 周货架总值 平均 {vals.Average():0},最小 {vals.Min():0},最大 {vals.Max():0}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market all merchants (test)", () => Log.Guard("黑市全部商家(测试)", () =>
{
var d = Session.Data;
d.Black.RefreshOrders(StoreStation.GetDayCounter(), StoreReputation.GetBMReputation(), d.Rng, BlackDelivery.Value, true);
Log.Info($"黑市全部商家(测试):{d.Black.Sellers.Count} 家,货架 {d.Black.Catalog.Count} 种,{string.Join("/", d.Black.Sellers.Select(x => x.Type))}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market negative cases (test)", () => Log.Guard("黑市负面场景(测试)", () =>
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
d.Black.RefreshOrders(day, StoreReputation.GetBMReputation(), d.Rng, BlackDelivery.Value, true);
var w = d.Hot;
w.Add(Assets.CRT, 20000);
BmListing First(MerchantType t) => d.Black.Catalog.First(l => d.Black.Sellers[l.Seller].Type == t);
// 骗子(老号,接受提前放款):二手矿卡;诚实:全新矿卡;诚实二手卡里夹假卡
var scamUsed = First(MerchantType.GpuUsed);
var sUsed = d.Black.Sellers[scamUsed.Seller]; sUsed.Scam = true; sUsed.Trades = 120; sUsed.Rating = 97;
var honest = First(MerchantType.GpuNew);
d.Black.Sellers[honest.Seller].Scam = false; d.Black.Sellers[honest.Seller].Trades = 150;
foreach (var s in d.Black.Sellers.Where(x => x.Type == MerchantType.Personal)) s.Scam = true;
var fakeL = First(MerchantType.Modules);
var fakeCard = new BmListing { Seller = d.Black.Sellers.FindIndex(x => x.Type == MerchantType.Modules), Item = "gpu:RTX70", Kind = GoodKind.Gpu, Level = 2, Model = "RTX70", FakeOf = "RTX60", Health = 55, Price = 500, Qty = 5 };
d.Black.Sellers[fakeCard.Seller].Scam = false; d.Black.Sellers[fakeCard.Seller].Trades = 150;
d.Black.Catalog.Add(fakeCard);
var persL = d.Black.Catalog.First(l => d.Black.Sellers[l.Seller].Type == MerchantType.Personal);
d.Black.HoneypotUntil = day + 6;
int before = d.Black.Orders.Count;
var o1 = d.Black.Place(scamUsed, 1, PayMode.Escrow, w, Assets.CRT, day, Session.Price); // 骗子 + 托管 → 退款
var o2 = d.Black.Place(scamUsed, 1, PayMode.Advance, w, Assets.CRT, day, Session.Price); // 骗子 + 提前放款 → 钱没了
var o3 = d.Black.Place(fakeCard, 2, PayMode.Escrow, w, Assets.CRT, day, Session.Price); // 假卡
var o4 = d.Black.Place(persL, 1, PayMode.Advance, w, Assets.CRT, day, Session.Price); // 个人卖家不接受提前放款 → null
var o5 = d.Black.Place(persL, 1, PayMode.Escrow, w, Assets.CRT, day, Session.Price); // 个人卖家(骗子)+ 托管
Log.Info($"负面场景:下单 骗子托管={o1 != null} 骗子提前={o2 != null} 假卡={o3 != null} 个人提前={o4 != null}(应为 False)个人托管={o5 != null},蜜罐至第 {d.Black.HoneypotUntil} 天,追查 {d.Black.Traps.Count} 笔,钱包 CRT {w.Get(Assets.CRT):0} 污点 {w.Taint:0}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Black market events (test)", () => Log.Guard("黑市事件(测试)", () =>
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
d.Black.Traps.Add(new BmTrap { Day = day, Amount = 300, Why = "honeypot", Item = "c4" });
var w = d.Hot;
for (int i = 0; i < 8; i++) d.Black.List("gpu:RTX70", "RTX70", "RTX60", 60, 500, 0.6, w, Assets.CRT, day - 1, Session.Price);
var l = d.Black.Catalog.FirstOrDefault();
if (l != null)
{
var o = d.Black.Place(l, 1, PayMode.Escrow, w, Assets.CRT, day, Session.Price);
if (o != null) { o.State = OrderState.Ready; o.Controlled = true; }
}
Log.Info($"黑市事件(测试):追查 300 今晚到期,假卡上架 8 件,受控订单 {d.Black.Orders.Count(x => x.Controlled)} 个");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Courier (test)", () => Log.Guard("黑市快递(测试)", () =>
{
foreach (var o in Session.Data.Black.Orders.Where(x => x.State == OrderState.Pending)) { o.State = OrderState.Ready; }
PlayerStore.Instance.currentClientInstance = null;
PlayerStore.Instance.storeClientManager.clientStack.Insert(0, BlackDelivery.CreateCourier());
PlayerStore.Instance.TryCallNextClient();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Courier trap (test)", () => Log.Guard("受控交付(测试)", () =>
{
foreach (var o in Session.Data.Black.Orders.Where(x => x.State == OrderState.Pending)) { o.State = OrderState.Ready; o.Controlled = true; }
PlayerStore.Instance.currentClientInstance = null;
PlayerStore.Instance.storeClientManager.clientStack.Insert(0, BlackDelivery.CreateCourier());
PlayerStore.Instance.TryCallNextClient();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer forged (test)", () => { ForceFake = true; CoinBuyerTest(); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Coin buyer (test)", () => { ForceFake = false; CoinBuyerTest(); });
}
private static bool ForceFake;
private static void CoinBuyerTest() => Log.Guard("用币付款顾客(测试)", () =>
{
var c = new StoreClient
{
identifier = "pd_coin_test", displayName = "Coin buyer", spriteName = SpriteDict.GetRandomScavSpriteName(),
clientFaction = "FACTION_BLACK_MARKET", useClientBudget = true, clientIntent = StoreClient.ClientIntent.BUY,
canClientExposeFeature = ClientCanExposeFunc.BasicClientExposeCapacity,
};
c.SetBudget(5000);
c.clientBuyingTagList.Add("SEED");
c.mainDialogue.SetText(c.displayName, "CRT.").SetEndAction(() => { });
c.AddBasicDialog();
c.CompleteClientCreation();
Mark(c, Assets.CRT, ForceFake);
Log.Info($"用币付款顾客(测试)已设置,钱包污点 {Session.Data.Hot.Taint:0.##},当前顾客 {PlayerStore.Instance.currentClientInstance?.GetClientBlueprint()?.identifier},队列 {PlayerStore.Instance.storeClientManager.clientStack.Count} 人");
PlayerStore.Instance.currentClientInstance = null; // 测试:丢掉在场的顾客(开局的高利贷者不可遣散)
PlayerStore.Instance.storeClientManager.clientStack.Insert(0, c);
PlayerStore.Instance.TryCallNextClient();
// 把后柜里的种子都标成赃物(热度 50%),买走后钱包的污点应为成交额的一半
foreach (var it in EmporiumEntry.Instance.backInvinvElement.childItems)
if (it.IsTag("SEED")) it.ModifyTag("STOLEN_VALUE_INT", t => t.Enable().SetInt(50));
});
public static void Unsubscribe() => ModHook.OnGenerateCustomerLate -= OnGenerate;
private static void OnGenerate(StoreClientManager manager)
{
if (Session.Data == null || !Unlock.IsUnlocked || !Session.Data.Connected) return; // 没有连接钱包就没有用币付款的顾客
Log.Guard("用币付款", () =>
{
int day = StoreStation.GetDayCounter();
if (day != markDay) { Marked.Clear(); markDay = day; }
// 假客服:一天最多一次
if (day >= Tuning.ScamMinDay && supportDay != day)
{
supportDay = day;
if (Session.Data.Rng.Chance(Tuning.FakeSupportChance)) manager.AddClient(FakeSupport());
}
foreach (var c in manager.clientStack)
{
if (c == null || c.isSecurity || Marked.Contains(c) || c.clientIntent != StoreClient.ClientIntent.BUY || c.isNoContributeToEvidence) continue;
Marked.Add(c);
string asset = CoinPayment.AssetFor(c.clientFaction);
if (asset == null || !Session.Data.Rng.Chance(CoinPayment.Chance(asset))) continue;
Mark(c, asset, day >= Tuning.ScamMinDay && Session.Data.Rng.Chance(Tuning.FakeTransferShare));
}
});
}
/// <summary>
/// 标成用币付款的顾客。开口先说用什么币付:正常的说"链上确认要一会儿";骗子(伪造转账)说"已经转了,给你看截图,快给货"——
/// 成交后信用点照样被扣回,币却不会到账。玩家能做的就是听清这句话,不卖给他。
/// </summary>
private static void Mark(StoreClient c, string asset, bool fake)
{
var before = c.onItemBought;
double total = 0; // 这位顾客这一单一共付了多少币
c.isNoContributeToEvidence = true;
var intro = new Dialogue().SetText(c.displayName, L.T(fake ? "coinpay.intro.fake" : "coinpay.intro", asset));
intro.SetNextDialogue(c.mainDialogue);
c.mainDialogue = intro;
c.onItemBought = (item, cost) =>
{
before?.Invoke(item, cost);
Log.Guard("用币付款成交", () =>
{
if (fake) { FakePay(c, asset, item, cost); total += cost / Session.Price(asset); }
else total += Pay(c, asset, item, cost);
Say(c, asset, total);
});
};
}
/// <summary>伪造转账:信用点扣回(顾客没真付),币不会到账,晨间日志说明。</summary>
private static void FakePay(StoreClient client, string asset, GameItem item, int cost)
{
if (cost <= 0) return;
ModHelper.AddCredit(-cost, false);
Session.Data.FakePaidToday.Add(L.T("coinpay.fake.entry", client.displayName, item.name, cost));
Log.Info($"伪造转账:{client.displayName} 拿走 {item.name}({cost} 信用点),{asset} 不会到账");
}
/// <summary>假客服:自称 Poinbase 客服,说账户被风控,要把 CRC 转到"安全地址"。Poinbase 从不上门,也从不要求转账;转过去的币就没了。</summary>
public static StoreClient FakeSupportForTest() => FakeSupport();
private static StoreClient FakeSupport()
{
var d = Session.Data;
string addr = ChainAddress.Make(d.Rng.NextUInt());
d.Scams.ScamAddrs.Add(addr);
int amount = 100 * d.Rng.Range(3, 15);
var c = new StoreClient
{
identifier = "pd_fake_support", displayName = L.T("scam.support.name"), spriteName = SpriteDict.GetRandomUpperSpriteName(),
clientIntent = StoreClient.ClientIntent.DIALOGUE,
};
c.mainDialogue.SetText(c.displayName, L.T("scam.support.1")).NextDialogue()
.SetText(c.displayName, L.T("scam.support.2", amount, addr)).NextDialogue()
.SetText(c.displayName, L.T("scam.support.3"));
ModHelper.SetClientSource(c, ModInfo.Id);
c.AddBasicDialog();
c.CompleteClientCreation();
Log.Info($"假客服上门:要 {amount} CRC 转到 {addr}");
return c;
}
/// <summary>成交后顾客说的话:告诉玩家这单是币到账,不是信用点。</summary>
private static void Say(StoreClient client, string asset, double total)
{
client.acceptDealDialogue = new Dialogue().SetText(client.displayName, L.T("coinpay.line", Fmt.Num(total), asset));
client.acceptDealDialogue.priority = 4;
client.acceptDealDialoguePlayed = false;
}
private static double Pay(StoreClient client, string asset, GameItem item, int cost)
{
var d = Session.Data;
double price = Session.Price(asset);
if (cost <= 0 || price <= 0) return 0;
int heat = item.IsTag("STOLEN_VALUE_INT") ? item.GetTagReadonly("STOLEN_VALUE_INT").GetInt() : 0;
ModHelper.AddCredit(-cost, false);
double qty = CoinPayment.Receive(d.Hot, asset, cost, price, heat);
d.Chain(ChainAddress.Make(d.Rng.NextUInt()), d.Hot.Address, asset, qty);
d.CoinSalesToday += cost;
Log.Info($"用币付款:{item.name} {cost} 信用点 → {qty:0.####} {asset},污点 +{cost * heat / 100.0:0}");
return qty;
}
}
}
+14 -1
View File
@@ -7,13 +7,19 @@ namespace Decentralized
{
public const string Id = "probably_decentralized";
public const string Name = "Probably Decentralized";
public const string Version = "0.1.0";
public const string Version = "2.0.0";
}
/// <summary>本地化:游戏的 mod 本地化表(ModFiles/Localization/*.csv),当前语言缺失时回退英文。</summary>
public static class L
{
public static string T(string key, params object[] args) => ModHelper.GetLocalized(ModInfo.Id, key, args);
/// <summary>当前游戏语言代码(en zh de fr es pl ru ja)。</summary>
public static string Code => LocHelper.GetCurrentLocaleCode() ?? "en";
/// <summary>译文普遍比中英文长的语言:表格列宽、操作键区按此放宽。</summary>
public static bool Wide { get { var c = Code; return c == "de" || c == "fr" || c == "es" || c == "pl" || c == "ru"; } }
}
public static class Log
@@ -29,6 +35,13 @@ namespace Decentralized
}
}
/// <summary>系统剪贴板(游戏里的输入框也能 Ctrl+V)。</summary>
public static class Clipboard
{
public static void Copy(string s) => GUIUtility.systemCopyBuffer = s ?? "";
public static string Paste() => (GUIUtility.systemCopyBuffer ?? "").Trim();
}
public static class Fmt
{
public static string Num(double v) => Math.Abs(v) >= 100 ? v.ToString("N0") : Math.Abs(v) >= 1 ? v.ToString("0.##") : v.ToString("0.####");
+260
View File
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Decentralized.Domain;
namespace Decentralized
@@ -15,6 +16,258 @@ namespace Decentralized
ModHook.OnPlaceInventorInventoryItemLate += OnPlace;
// 开发者模式(F9)里的测试入口:像到了杰克逊店里一样,清空并扩大柜台后上货
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give rig x3 (test)", () => { for (int i = 0; i < 3; i++) Items.Give(ModHelper.SpawnItem(Items.Rig)); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used cards x4 (test)", () =>
{
var rng = Session.Data.Rng;
Items.Give(Items.NewCard("RTX80", 40, null, rng));
Items.Give(Items.NewCard("RTX80", 70, "RTX60", rng));
Items.Give(Items.NewCard("RX800", 30, "RX70", rng));
Items.Give(Items.NewCard("RX900", 90, "RX70", rng));
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dump inspection UI (test)", () => Log.Guard("检视界面结构", () =>
{
var sb = new System.Text.StringBuilder("\n");
void Walk(Transform t, int depth)
{
var rt = t as RectTransform;
var img = t.GetComponent<UnityEngine.UI.Image>();
sb.AppendLine($"{new string(' ', depth * 2)}{t.name} active={t.gameObject.activeSelf} pos={t.localPosition} size={(rt != null ? rt.sizeDelta.ToString() : "-")} scale={t.localScale}{(img != null ? " img=" + (img.sprite != null ? img.sprite.name : "null") : "")}");
if (depth < 5) foreach (Transform c in t) Walk(c, depth + 1);
}
var m = InspectionUIManager.Instance;
var ci = PlayerStore.Instance.currentClientInstance;
sb.AppendLine("当前顾客:" + (ci == null ? "无" : ci.GetClientBlueprint().identifier + " 意图 " + ci.GetClientBlueprint().clientIntent));
sb.AppendLine("检视物品:" + (m.currentInspectingItem?.identifier ?? "无") + " APPRAISAL=" + (m.currentInspectingItem?.IsTag("APPRAISAL_ITEM") ?? false));
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("RTX70", 55, null, rng);
Items.Give(item);
var u = Items.ToUnit(item);
var c = Claim.Truth(u); c.Model = "RTX90"; 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("RTX90", 60, "RTX60", 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, "Poinbase +5000 CRC, unlock (test)", () => Log.Guard("永续测试", () =>
{
var d = Session.Data;
d.Poinbase.Add(Assets.CRC, 5000); d.Poinbase.Basis += 5000;
if (!Unlock.IsUnlocked) ModHelper.GetNetworkUpgrade(Unlock.Id)?.Unlock();
Log.Info($"测试:Poinbase CRC {d.Poinbase.Get(Assets.CRC):0},解锁 {Unlock.IsUnlocked}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give ASIC M1/M2/M3 (test)", () => { foreach (var m in new[] { "M1", "M2", "M3" }) Items.Give(AsicItems.New(m, 100)); for (int i = 0; i < 4; i++) Items.Give(ModHelper.SpawnItem("rare_electronic")); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Era in 2 days (test)", () => { var n = Session.Data.Network; int day = StoreStation.GetDayCounter(); n.EraDay = day + 2; n.EraWarnDay = day; n.EraWarned = false; n.EraDone = false; Log.Info($"测试:矿机时代第 {n.EraDay} 天,NPC 耗电 {n.NpcKj}"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Predict: refill all kinds (test)", () => Log.Guard("预测测试", () =>
{
var d = Session.Data; int day = StoreStation.GetDayCounter();
d.Predict.Markets.Clear();
PredictGame.Morning(d, day); var made = new List<PredictMarket>(); for (int i = 0; i < 4; i++) made.AddRange(d.Predict.Refill(day, d.Rng, PredictGame.Candidates(day)));
Log.Info($"测试:新开 {made.Count} 个预测盘口(不看解锁日)");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Predict: add era and CRA (test)", () =>
{
var d = Session.Data; int day = StoreStation.GetDayCounter(); var bk = d.Predict;
bk.Markets.Add(PredictMarket.Create(++bk.Seq, PredictKind.Era, "era", L.T("predict.era"), 0, day, day + 1, day + 3, PredictMath.EraProb(day, day + 1, day + 3, -1) + 0.05));
double h = 1 - System.Math.Pow(1 - Tuning.CraSpiralWeekly, 1.0 / 7);
bk.Markets.Add(PredictMarket.Create(++bk.Seq, PredictKind.Cra, "cra", "CRA", 0, day, day + 1, day + 3, PredictMath.CraWarnProb(d.Stable.Cushion, Stablecoins.CushionDrain(Tuning.CraNpcStake + d.Staking.Earning, d.Stable.CraRate), day, day + 1, day + 3, h)));
Log.Info($"测试:矿机时代 {d.Network.EraDay},CRA 储备 {d.Stable.Reserve:0}(R/S {d.Stable.Ratio:0.00})");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Informant: queue power shortage in 3 days (test)", () => Log.Guard("线人测试", () =>
{
StoreStation.Instance.storeEventManager.QueueFuturEvent(EventListNormal.CreatePowerShortage(), 3, true);
Log.Info("测试:停电 3 天后(已知)");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Scams: airdrops + pools + wick (test)", () => Log.Guard("骗局测试", () =>
{
var d = Session.Data; int day = StoreStation.GetDayCounter();
var assets = new[] { Assets.CRT, Assets.PSC, Assets.CRC, Assets.CRA };
for (int i = 0; i < 4; i++) d.Scams.NewAirdrop(day, d.Rng, assets);
while (d.Scams.Pools.Count(p => !p.Closed) < Tuning.YieldPoolMax) d.Scams.NewPool(day, d.Rng);
d.Market.Wick = 0; double w = 0; for (int i = 0; i < 200 && w == 0; i++) w = d.Market.TryWick(d.Rng, day);
Log.Info($"测试:空投 {string.Join(",", d.Scams.Airdrops.Select(a => a.Name + (a.Scam ? "*" : "")))};池子 {string.Join(",", d.Scams.Pools.Select(p => p.Name + (p.Scam ? "*" : "")))};插针 {w:0.000} 回去 {d.Market.WickBack:0.00}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Scams: fake support now (test)", () => Log.Guard("假客服测试", () =>
{
var m = PlayerStore.Instance.storeClientManager;
var c = CoinCustomers.FakeSupportForTest();
PlayerStore.Instance.currentClientInstance = null;
m.clientStack.Insert(0, c);
PlayerStore.Instance.TryCallNextClient();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Chart: run market 30 days (test)", () => Log.Guard("K 线测试", () =>
{
// 用真实的行情过程往前跑 30 天,记录蜡烛(会改动这一局的 PSC 价格,只在测试存档用)
var d = Session.Data; int day0 = StoreStation.GetDayCounter() - 30; var none = new List<MarketEvent>();
d.Candles.Series.Clear();
for (int k = day0; k < day0 + 30; k++)
{
d.Market.Open(none, d.Rng, k); CandleChart.Morning(d, k);
double wh = d.Market.Whale(d.Rng); d.Candles.Tick(Assets.PSC, k, d.Market.Price, wh);
if (d.Market.TryWick(d.Rng, k) != 0) CandleChart.Touch(d, k, Assets.PSC, Assets.Ponero);
if (d.Rng.Chance(0.3)) { double v = d.Rng.Range(500.0, 4000.0); bool buy = d.Rng.Chance(0.5); if (buy) d.Market.Main.BuyBase(v); else d.Market.Main.SellBase(v / d.Market.Price); d.Candles.Tick(Assets.PSC, k, d.Market.Price, v, true); }
d.Market.UndoWick(k); CandleChart.Close(d, k);
d.Stable.Night(k, d.Rng, d.Staking.Earning);
d.Market.Night(none, d.Rng, k, Session.Price); d.Market.UpdateSupply(k);
}
CandleChart.Morning(d, day0 + 30);
Log.Info($"测试:K 线 {d.Candles.Of(Assets.PSC).Count} 根,PSC {d.Market.Price:0.00}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Story: Serpico to Revolution, Eleanor leaks, Lun arrested (test)", () =>
{
var cd = PlayerStore.GetStoreClientData();
cd.serpicoDocumentChoice = SerpicoStoryline.CHOICE_REVOLUTION; cd.eleanorDriveChoice = 2; cd.officerLunArrested = true;
Log.Info("测试:剧情选择已设置(革命军 / 泄露 / 伦警官被捕),今晚生效");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: next visit now (test)", () => Log.Guard("Tumbler 测试", () =>
{
// 把 Tumbler 的下一次上门挪到今天,并立刻叫进来
var t = Session.Data.Story.Tumbler; int day = StoreStation.GetDayCounter();
if (t.StartDay < 0) t.StartDay = day;
t.StartDay = day - Tumbler.Offsets[System.Math.Min(t.Stage, Tumbler.Offsets.Length - 1)]; t.VisitedDay = -1;
var af = Session.Data.Story.After;
if (af.AgentDay >= day) af.AgentDay = day - 1;
if (af.SearchDay >= day) af.SearchDay = day - 1;
PlayerStore.Instance.currentClientInstance = null;
var m = PlayerStore.Instance.storeClientManager;
TumblerStory.ForceVisit(m);
PlayerStore.Instance.TryCallNextClient();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: restart story (test)", () =>
{
var st = Session.Data.Story;
Log.Info($"测试:Tumbler 剧情重来(原进度 {st.Tumbler.Stage},选择 {st.Tumbler.Choice},结束 {st.Tumbler.Done})");
st.Tumbler = new Tumbler(); st.After = new TumblerAfter();
st.Applied.RemoveAll(k => k.StartsWith("tumbler_"));
st.MixerClosed = st.MixerRebranded = st.MixerHoneypot = false; st.MixerFee = st.MixerVolume = st.MixerRug = 1;
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Hot wallet +2000 CRC (test)", () =>
{
var w = Session.Data.Hot; w.Add(Assets.CRC, 2000); w.Basis += 2000;
Log.Info($"测试:钱包 CRC {w.Get(Assets.CRC):0},污点 {w.Taint:0}");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "My mixer: max heat (test)", () =>
{
var m = Session.Data.OwnMix; m.Heat = 1e4;
Log.Info($"测试:自营混币器风声拉满,今晚查封概率 {m.SeizeChance(1, false):P0}");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Mixer: honeypot now (test)", () =>
{
var d = Session.Data;
d.Story.Rebrand(true); d.Story.After.Rebranded = true;
d.Addresses["mixer_old"] = d.Address("mixer"); d.Addresses["mixer"] = ChainAddress.Make(d.Rng.NextUInt());
Log.Info($"测试:混币器换成钓鱼混币器,费率 {Mixer.Fee:P1}");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: contact, crackdown tomorrow (test)", () => Log.Guard("联系人测试", () =>
{
Session.Data.Story.After.Contact = true;
StoreStation.Instance.storeEventManager.QueueFuturEvent(OperationListSec.Crackdown(), 1, false);
Log.Info("测试:Tumbler 成为联系人,镇压排到明天(玩家不知道)");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: leak tonight (test)", () =>
{
var a = Session.Data.Story.After; a.Leaked = false; a.LeakDay = StoreStation.GetDayCounter();
Log.Info($"测试:今晚泄露,出款记录 {Session.Data.Mixer.Returns.Count} 条");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: trial market now (test)", () => Log.Guard("审判盘测试", () =>
{
var d = Session.Data; var a = d.Story.After; int day = StoreStation.GetDayCounter();
a.Verdict = -1; a.TrialMarket = a.TrialDone = false; a.Hints.Clear(); a.HintDay = -1;
a.StartTrial(day - Tuning.TumblerTrialOpen, d.Rng);
PredictGame.OpenTrial(d, day);
Log.Info($"测试:审判盘开盘,第 {a.TrialDay} 天宣判,判决 {a.Verdict}");
Session.Save();
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: revenge tonight (test)", () =>
{
var a = Session.Data.Story.After; a.Revenged = false; a.RevengeDay = StoreStation.GetDayCounter();
Log.Info("测试:今晚报复");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Tumbler: BM rug tonight (test)", () =>
{
var a = Session.Data.Story.After; a.BmRugged = false; a.BmRugDay = StoreStation.GetDayCounter();
Log.Info($"测试:今晚黑市跑路,在途 {Session.Data.Mixer.Pending.Count} 笔");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Mixer: rebrand tonight (test)", () =>
{
var a = Session.Data.Story.After; a.Rebranded = false; a.ClosedDay = StoreStation.GetDayCounter() - Tuning.TumblerRebrandDays;
Log.Info("测试:今晚换皮重开");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Lower-level rep +30 (test)", () => StoreReputation.ModReputation(StoreReputation.FACTION_LOWER_LEVEL, 30));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dump crimes and reputation (test)", () => Log.Guard("罪证与声望", () =>
{
var sd = PlayerStore.Instance.secData;
var crimes = string.Join(",", sd.crimeTypes.Select((c, i) => c + " " + sd.crimeAmount[i]));
string Rep(string f) => f + " " + (StoreReputation.GetStoreReputation(f)?.GetReputation().ToString() ?? "-");
Log.Info($"测试:罪证 [{crimes}],证据 {sd.evidenceLevel};声望 {Rep(StoreReputation.FACTION_LOWER_LEVEL)},{Rep(StoreReputation.FACTION_SECURITY)},{Rep(StoreReputation.FACTION_BLACK_MARKET)};现金 {PlayerStore.Instance.playerCash}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Scams: phishing approval + scam pool (test)", () => Log.Guard("骗局补测", () =>
{
// 当前钱包:一个钓鱼授权(明晚转走 CRT);一个必然是骗局的池子(存 500)和一个正规的时间锁池子(存 500 并申请取出)
var d = Session.Data; int day = StoreStation.GetDayCounter(); var w = d.Hot;
d.Scams.Pools.Clear(); d.Scams.Approvals.Clear();
d.Scams.Approvals.Add(new Approval { Id = ++d.Scams.Seq, Wallet = w.Address, Asset = Assets.CRT, Spender = "P0inbase", Limit = double.PositiveInfinity, Scam = true, DrainDay = day + 1 });
var bad = new YieldPool { Id = ++d.Scams.Seq, Name = "Test Scam", OpenDay = day - 1, Rate = 0.03, Reserve = 9999, Cap = 1e6, Scam = true };
var ok = new YieldPool { Id = ++d.Scams.Seq, Name = "Test Lock", OpenDay = day - 1, Rate = 0.01, Reserve = 9999, Cap = 1e6, Timelock = true };
d.Scams.Pools.Add(bad); d.Scams.Pools.Add(ok);
d.Scams.Deposit(bad, w, 500, Session.Price); d.Scams.Deposit(ok, w, 500, Session.Price); d.Scams.Withdraw(ok, w, day);
Log.Info($"测试:钓鱼授权 #{d.Scams.Approvals[0].Id}(明晚转走 CRT {w.Get(Assets.CRT):0}),骗局池存 500,时间锁池取款第 {day + Tuning.YieldLockDays} 天到账;CRC {w.Get(Assets.CRC):0}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Perp: PSC long 1000 x2 (test)", () => Log.Guard("永续测试", () =>
{
var d = Session.Data; d.Poinbase.Add(Assets.CRC, 2000);
// 先让价格偏离 7 日均线,资金费才不为零:补几个历史标记价
d.Perps.Marks["PSC"] = new System.Collections.Generic.List<double> { d.Market.Price * 0.8, d.Market.Price * 0.8, d.Market.Price * 0.8 };
var res = d.Perps.Open(d.Poinbase, "PSC", true, 1000, 2, d.Market.Price, StoreStation.GetDayCounter(), Session.Price);
Log.Info($"测试:永续开多 {res},现价 {d.Market.Price:0.00}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give key drive (test)", () => Items.Give(ModHelper.SpawnItem(Items.KeyDrive)));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give GPU guide (test)", () => Items.Give(ModHelper.SpawnItem(Items.Guide)));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Predict: window = tomorrow (test)", () => { foreach (var m in Session.Data.Predict.Markets) { m.FromDay = m.ToDay = StoreStation.GetDayCounter() + 1; } Log.Info("测试:所有盘口窗口改成明天"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Perp fund 0 (test)", () => { Session.Data.Perps.InsuranceFund = 0; Log.Info("测试:保险基金清零"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "SPARK x0.3 (test)", () => { var st = Session.Data.Stable; st.Spark *= 0.3; Log.Info($"测试:SPARK 价格 {st.Spark:0.00}"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Wallet gift: CRC, CRA, PSC (test)", () => { var w = Session.Data.Hot; w.Add(Assets.CRA, 1000); w.Add(Assets.CRC, 1000); w.Add(Assets.CRT, 3000); w.Add(Assets.PSC, 20); w.Add(Assets.SPARK, 300); w.Basis += 5000; Log.Info($"测试:{WalletWindow.NameOf(w)} CRA {w.Get(Assets.CRA):0}"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Poinbase gift: CRC, PSC (test)", () => { var pb = Session.Data.Poinbase; pb.Add(Assets.CRC, 5000); pb.Add(Assets.PSC, 30); pb.Basis += 8000; Log.Info($"测试:Poinbase CRC {pb.Get(Assets.CRC):0}"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "CRA spiral warning (test)", () =>
{
var st = Session.Data.Stable; int day = StoreStation.GetDayCounter();
st.CraWarnDay = day; st.CraEventDay = day + 3;
Log.Info($"测试:CRA 预兆今天,挪用日第 {st.CraEventDay} 天");
});
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give terminal (test)", () => { Items.Give(ModHelper.SpawnItem(Items.Terminal)); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Hardware wallets (test)", () => { Items.Give(Items.NewHwWallet(false)); Items.Give(Items.NewHwWallet(true)); Log.Info("测试:给了一个正品、一个被投毒的硬件钱包"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Chain drama (test)", () => Log.Guard("链上事件测试", () =>
{
// 今天拥堵;当前钱包最近转出过的地址立刻收到相似地址的 0 币转入;报纸和论坛各来一条钓鱼
var d = Session.Data;
int day = StoreStation.GetDayCounter();
d.CongestedFrom = day; d.CongestedUntil = day + 1;
var last = d.Hot.History.FirstOrDefault(r => !r.In);
if (last != null) d.Chain(ChainAddress.Lookalike(last.Addr, d.Rng), d.Hot.Address, Assets.PSC, 0, zero: true);
string fake = ChainAddress.Make(d.Rng.NextUInt());
News.Show("pd_news_phish_test_" + day, L.T("news.phish.title"), L.T("news.phish.desc", fake), 2);
d.Black.Forum.Add("phish:" + ChainAddress.Make(d.Rng.NextUInt()));
Log.Info($"测试:拥堵到第 {d.CongestedUntil} 天;投毒 {(last != null ? last.Addr : "无转出记录")};钓鱼 {fake}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{
EmporiumEntry.Instance.ClearFrontInv();
@@ -43,6 +296,13 @@ namespace Decentralized
bool ok = PlayerStore.Instance.TryAddDirectSellingItemToTable(item);
placed.Add(ok ? $"{kind}#{item.uniqueId}" : kind + "(柜台已满)");
}
// 硬件钱包(正品):钱包和手上的硬件钱包加起来还没到上限时,有一定概率上一个
int wallets = Session.Data.Wallets.Count + (owned?.Count(i => i.identifier == Items.HwWallet) ?? 0);
if (wallets < Tuning.WalletMax && Session.Data.Rng.Chance(Tuning.WalletJacksonChance))
{
var hw = Items.NewHwWallet(false);
placed.Add(PlayerStore.Instance.TryAddDirectSellingItemToTable(hw) ? $"硬件钱包#{hw.uniqueId}" : "硬件钱包(柜台已满)");
}
Log.Info("杰克逊博士上货:" + string.Join(",", placed));
});
}
+111
View File
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
using UnityEngine;
namespace Decentralized
{
/// <summary>
/// Poinbase 的出金:转到钱包地址。托管账户发起,几秒钟的确认动画(模拟交易所触发合约执行)走完才落地;
/// 手续费固定中间档,从转出的币里扣。动画走完前币还在账户里,中途存档、退出都不会丢币。
/// 地址格式或校验位不对拒绝;自己的钱包到账;别的格式正确的地址按黑洞处理(币就没了)。
/// </summary>
internal static class Funds
{
internal sealed class Tx
{
public int Id, Confs;
public string Asset, To;
public double Amount, Fee;
public float Start, Total;
public bool Finished;
}
internal static readonly List<Tx> Active = new List<Tx>();
private static int seq;
/// <summary>地址检查:返回 null 表示可以转,否则是错误文案的键。</summary>
internal static string Check(string to)
{
var d = Session.Data;
to = ChainAddress.Normalize(to ?? "");
if (!ChainAddress.Valid(to)) return "funds.err.addr";
if (ChainAddress.Same(to, d.Address("poinbase"))) return "funds.err.self";
if (d.Own(to) == null && WalletWindow.Known(to)) return "funds.err.known";
return null;
}
private static bool Congested
{
get { var d = Session.Data; int day = StoreStation.GetDayCounter(); return day >= d.CongestedFrom && day <= d.CongestedUntil; }
}
/// <summary>开始一笔出金。余额不足或手续费吃掉全部数额返回错误键,成功返回 null。</summary>
internal static string Start(string asset, double amount, string to)
{
var d = Session.Data;
var err = Check(to);
if (err != null) return err;
var pb = d.Poinbase;
if (pb.Avail(asset) + 1e-9 < amount) return pb.Frozen.ContainsKey(asset) ? "msg.frozen" : "msg.balance";
double price = Session.Price(asset);
double fee = price > 0 ? TxRules.FeeCredits(FeeTier.Normal, Congested, d.Gas) / price : 0;
if (fee >= amount) return "funds.err.fee";
double value = amount * price;
int confs = TxRules.Confirmations(value);
Active.Add(new Tx
{
Id = ++seq, Asset = asset, To = ChainAddress.Normalize(to), Amount = amount, Fee = fee, Confs = confs,
Start = Time.unscaledTime, Total = (float)(Tuning.FundsBaseSecs + confs * Tuning.FundsConfSecs),
});
WalletDriver.Ensure();
return null;
}
internal static float Progress(Tx t) => t.Total <= 0 ? 1 : Mathf.Clamp01((Time.unscaledTime - t.Start) / t.Total);
internal static int Conf(Tx t) => Math.Max(1, Math.Min(t.Confs, 1 + (int)((Time.unscaledTime - t.Start) / Math.Max(0.01f, t.Total) * t.Confs)));
internal static string StatusText(Tx t) => L.T("funds.confirm", Conf(t), t.Confs);
/// <summary>每帧:更新开着的窗口里的进度条,走完的落地。</summary>
internal static void Tick()
{
if (Active.Count == 0) return;
var m = CustomUIManager.Instance;
foreach (var t in Active.ToList())
{
if (Time.unscaledTime - t.Start >= t.Total) { Finish(t); continue; }
m?.GetElement(TerminalWindow.Id, "funds_status_" + t.Id)?.SetText(StatusText(t));
m?.GetElement(TerminalWindow.Id, "funds_prog_" + t.Id)?.SetProgress(Progress(t));
}
}
private static void Finish(Tx t)
{
if (t.Finished) return;
t.Finished = true;
Active.Remove(t);
Log.Guard("出金落地", () =>
{
var d = Session.Data;
var pb = d.Poinbase;
double amt = Math.Min(t.Amount, pb.Avail(t.Asset));
double fee = Math.Min(t.Fee, amt);
if (fee > 0) Wallet.Transfer(pb, new Wallet(), t.Asset, fee, Session.Price);
double net = amt - fee;
var own = d.Own(t.To);
Wallet.Transfer(pb, own ?? new Wallet(), t.Asset, net, Session.Price);
d.Block += 1;
d.Chain(d.Address("poinbase"), t.To, t.Asset, net, fee);
if (own != null) WalletWindow.RefreshDevice(own, true);
else Log.Info($"出金转进没人用的地址 {t.To}:{net} {t.Asset}");
Session.Save();
TerminalWindow.Refresh(own != null
? L.T("funds.done", Fmt.Num(net), t.Asset, WalletWindow.NameOf(own))
: L.T("funds.done.void", Fmt.Num(net), t.Asset));
});
}
}
}
+417
View File
@@ -0,0 +1,417 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
using UnityEngine;
namespace Decentralized
{
/// <summary>
/// 显卡鉴定指南:游戏的纸质指南(PaperUI),版式和原版鉴定指南一致——笔记本背景、委员会页眉、居中标题、
/// 插图加图注、“价格不变 / -n%”的折价标注、末页的鉴定示例。第一次打开时按当前语言生成并登记,语言变了重新生成。
/// 坐标是设计单位(314×402,屏幕上放大 2 倍)。
/// </summary>
public static class GpuGuide
{
public const string BookId = "gpu_guide";
private const float X = 35f, W = 253f, Top = 80f, Bottom = 368f;
private const string Ink = "#221C1A", Good = "#3F7D4B", Bad = "#9E1F1A";
private static string _locale;
private static LoadedMod _mod;
public static void OnAssetsLoaded(LoadedMod mod)
{
if (mod?.Manifest?.ID == ModInfo.Id) _mod = mod;
}
/// <summary>原版背景、按钮和页眉图按名字找不到时(纸张素材目录没打包),把内存里同名的原版精灵登记给本 mod。</summary>
private static void ProvideMissingPaperSprites()
{
if (_mod == null) return;
foreach (var sprite in Resources.FindObjectsOfTypeAll<Sprite>())
{
string n = sprite.name;
if (n != "notebook_manual" && n != "guide_logo" && !n.StartsWith("paper_buttons")) continue;
if (_mod.PaperSprites.ContainsKey(n) || PaperAssets.FindInCatalog(n) != null) continue;
_mod.PaperSprites[n] = sprite;
}
}
public static void Open(int page = 0)
{
try
{
if (_locale != L.Code || !ModHelper.GetPaperBookIds(ModInfo.Id).Contains(BookId)) Register();
var ui = ModHelper.OpenPaper(ModInfo.Id, BookId);
if (ui == null) { Log.Warn("[GpuGuide] 打不开鉴定指南"); return; }
ui.FlipTo(page);
RaiseAboveModWindows(ui);
}
catch (Exception ex) { Log.Error("[GpuGuide] " + ex); }
}
/// <summary>纸质画布在 mod 窗口所在的层下面:从终端里打开时会被盖住,打开期间调到上面,关上时恢复。</summary>
private static void RaiseAboveModWindows(PaperUI ui)
{
var canvas = ui.Root != null ? ui.Root.GetComponentInParent<Canvas>()?.rootCanvas : null;
if (canvas == null || canvas.sortingOrder >= 950) return;
int old = canvas.sortingOrder;
canvas.sortingOrder = 950;
void Restore(PaperUI u) { u.Closed -= Restore; if (canvas != null) canvas.sortingOrder = old; }
ui.Closed += Restore;
}
// ================= 生成 =================
private static void Register()
{
ProvideMissingPaperSprites();
ModHelper.RemovePaperBooks(ModInfo.Id);
var b = ModHelper.CreatePaper(ModInfo.Id, BookId, "notebook_manual");
b.SetSize(628f, 804f);
b.SetPosition(-450f, 0f);
b.SetChrome();
Page1(b); Page2(b); Page3(b); Page4(b); Page5(b); Page6(b); Page7(b); Page8(b); Page9(b);
ModHelper.RegisterPaperBook(ModInfo.Id, b.Data);
_locale = L.Code;
Log.Info($"[GpuGuide] 已登记显卡鉴定指南({b.Data.pages.Count} 页,{_locale})");
}
private static string T(string key, params object[] a) => TuningText.Render(L.T(key, a));
/// <summary>新的一页:原版的页眉(委员会印章字)和标题。</summary>
private static float NewPage(PaperUI b)
{
b.AddPage(null, "guide_page");
b.SetSlot("header", L.T("gg.header"));
b.SetSlot("title", L.T("item.guide.name"));
b.SetSlot("body", " ");
return Top;
}
private static string Img(string name) => ModInfo.Id + ":" + name;
private static TMPro.TextMeshProUGUI _measurer;
private static float Measure(string text, string style, float size, float width)
{
if (_measurer == null)
{
var go = new GameObject("pd_gpu_guide_measure", typeof(RectTransform));
UnityEngine.Object.DontDestroyOnLoad(go);
_measurer = go.AddComponent<TMPro.TextMeshProUGUI>();
_measurer.enableWordWrapping = true;
_measurer.richText = true;
}
var st = PaperTextStyle.Get(style);
var font = st.Font();
if (font != null) _measurer.font = font;
_measurer.fontSize = size > 0 ? size : st.size;
float h = _measurer.GetPreferredValues(text, width, 0).y;
return h > 1 ? h + 1.5f : (size > 0 ? size : st.size) * 1.5f * (1 + text.Split('\n').Length);
}
/// <summary>一段文字,返回下一块的 y。</summary>
private static float Text(PaperUI b, float y, string text, string style = "body", string color = Ink, string align = "left", float gap = 5f, float x = X, float w = W)
{
float h = Measure(text, style, 0, w);
b.AddText(x, y, w, h, text, style, align: align);
b.Last.color = color;
if (y + h > Bottom) Log.Warn($"[GpuGuide] 内容超出纸面 overflow:{y + h:0} > {Bottom}({text.Substring(0, Math.Min(18, text.Length))})");
return y + h + gap;
}
/// <summary>小节标题(原版的蓝灰色粗体小标题);标题太长折行时按折行后的高度往下推。</summary>
private static float Section(PaperUI b, float y, string text)
{
float h = Mathf.Max(13f, Measure(text, "label", 8f, W));
b.AddText(X, y, W, h, text, "label", fontSize: 8f);
b.Last.color = "#2E5A78";
return y + h + 2f;
}
/// <summary>一张图(底图加叠加层,同一个位置),居中,宽高是显示尺寸;返回图的底边。</summary>
private static float Art(PaperUI b, float x, float y, float scale, params string[] layers)
{
float w = 192f * scale, h = 128f * scale;
foreach (var l in layers)
{
b.AddImage(x, y, w, h, Img(l), "stretch");
}
return y + h;
}
/// <summary>正面底图:供电口类别(0 = 6 针,1 = 8 针,2 = 双 8 针)与风扇数(1 到 3)各自独立。</summary>
private static string Front(string brand, int pin, int count) => $"gpu_big_{brand}_f_p{pin}n{count}";
private static string Front(string brand, int tier) => Front(brand, Hardware.PinClass[tier], Hardware.FanCount[tier]);
private static int Fans(int tier) => Hardware.FanCount[tier];
private static void Caption(PaperUI b, float x, float y, float w, string text, string color = null)
{
b.AddText(x, y, w, 10, text, "caption", align: "center");
if (color != null) b.Last.color = color;
}
/// <summary>背面贴纸上的序列号:贴纸在大图 (142, 91) 附近,随图的缩放摆放。</summary>
private static void SerialOnSticker(PaperUI b, float ax, float ay, float scale, string serial)
{
float cx = ax + 142f * scale, cy = ay + 91f * scale;
b.AddText(cx - 26f * scale, cy - 4.5f, 52f * scale, 9f, serial, "caption", align: "center", fontSize: 3.2f * scale / 0.78f);
b.Last.color = "#1B1B1B";
}
/// <summary>剩余高度能放下的图缩放(图高 128 设计单位):长语言文字多,图自动缩小,不超出纸面。</summary>
private static float FitScale(float usedHeight, int count, float max, float min = 0.3f)
{
float avail = Bottom - Top - usedHeight;
return Mathf.Clamp(avail / (128f * count), min, max);
}
private static string SampleSerial(string model, string chip = null, int kind = 0)
{
var u = new Unit { Model = model, FakeOf = chip, Health = 100, Look = new CardLook { Seed = 31337, Serial = kind } };
return CardLabels.SerialText(u);
}
// ---- 1 概述 ----
private static void Page1(PaperUI b)
{
float y = NewPage(b);
string body = L.T("gg.p1.body"), makers = L.T("gg.p1.makers"), note = T("gg.p1.note");
float used = Measure(body, "body", 0, W) + Measure(makers, "body", 0, W) + Measure(note, "body", 7f, W) + 6f + 14f + 8f + 14f;
float scale = FitScale(used, 1, 0.9f);
y = Text(b, y, body, gap: 6f);
float x = X + (W - 192f * scale) / 2;
float bottom = Art(b, x, y, scale, Front("probidia", 1));
Caption(b, X, bottom + 1, W, L.T("gg.p1.cap"));
y = bottom + 14f;
y = Text(b, y, makers, gap: 8f);
Text(b, y, note, "body", Good);
b.Last.fontSize = 7f;
}
// ---- 2 显卡型号 ----
private static void Page2(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.p2.h"));
y = Text(b, y, L.T("gg.p2.body"), gap: 6f);
float cw = W / 2;
for (int row = 0; row < 2; row++)
{
float rowBottom = y;
for (int col = 0; col < 2; col++)
{
int tier = row * 2 + col;
float ax = X + col * cw + (cw - 120f) / 2;
float bottom = Art(b, ax, y, 0.625f, Front("probidia", tier));
Caption(b, X + col * cw, bottom, cw, L.T("gg.tier." + tier));
rowBottom = bottom + 12f;
}
y = rowBottom + 4f;
}
}
// ---- 3 风扇数 ----
private static void Page3(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.count.h"));
string cb = L.T("gg.count.body");
float cs = FitScale(15f + Measure(cb, "body", 0, W) + 6f + 3 * 15f, 3, 0.5f, 0.3f);
y = Text(b, y, cb, gap: 6f);
for (int n = 1; n <= 3; n++)
{
float bottom = Art(b, X + (W - 192f * cs) / 2, y, cs, Front("probidia", 1, n));
Caption(b, X, bottom, W, L.T("lbl.count." + n));
y = bottom + 15f;
}
}
// ---- 4 供电口 ----
private static void Page4(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.pin.h"));
string pbd = L.T("gg.pin.body");
float ps = FitScale(15f + Measure(pbd, "body", 0, W) + 6f + 3 * 15f, 3, 0.5f, 0.3f);
y = Text(b, y, pbd, gap: 6f);
for (int p = 0; p < 3; p++)
{
float bottom = Art(b, X + (W - 192f * ps) / 2, y, ps, Front("probidia", p, 2));
Caption(b, X, bottom, W, L.T("lbl.pin." + p));
y = bottom + 15f;
}
}
// ---- 5 序列号 ----
private static void Page5(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.serial.h"));
string body = L.T("gg.serial.body"), pb = L.T("gg.serial.pb"), md = L.T("gg.serial.md"), rule = L.T("gg.serial.rule");
float used = 15f + Measure(body, "body", 0, W) + Measure(pb, "body", 0, W) + Measure(md, "body", 0, W) + Measure(rule, "body", 0, W) + 6f + 14f + 4f + 8f;
float scale = FitScale(used, 1, 0.78f);
y = Text(b, y, body, gap: 6f);
float ax = X + (W - 192f * scale) / 2;
float bottom = Art(b, ax, y, scale, "gpu_big_probidia_b", "gpu_big_ov_print_normal_rtx80");
SerialOnSticker(b, ax, y, scale, SampleSerial("RTX80"));
Caption(b, X, bottom + 1, W, L.T("gg.serial.cap"));
y = bottom + 14f;
y = Text(b, y, pb, gap: 4f);
y = Text(b, y, md, gap: 8f);
Text(b, y, rule, "body");
}
// ---- 6 背面丝印 ----
private static void Page6(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.print.h"));
string note = L.T("gg.print.note");
string[] kinds = { "normal", "stamp", "skew" };
float scale = FitScale(15f + Measure(note, "body", 0, W) + 3 * 14f, 3, 0.5f, 0.3f);
for (int s = 0; s < 3; s++)
{
string model = s == 1 ? "rtx60" : "rtx80";
float ax = X + (W - 192f * scale) / 2;
float bottom = Art(b, ax, y, scale, "gpu_big_probidia_b", $"gpu_big_ov_print_{kinds[s]}_{model}");
SerialOnSticker(b, ax, y, scale, SampleSerial("RTX80"));
Caption(b, X, bottom, W, L.T("gg.print." + s), s == 0 ? null : Bad);
y = bottom + 14f;
}
Text(b, y, note, "body");
}
// ---- 7 叶片与金手指 ----
private static void Page7(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.fan.h"));
float cw = W / 3;
float bottom = y;
for (int s = 0; s < 3; s++)
{
var layers = new List<string> { Front("probidia", 1) };
if (s == 1) layers.Add("gpu_big_ov_fan_dust2_" + Fans(1));
if (s == 2) { layers.Add("gpu_big_ov_fan_dust1_" + Fans(1)); layers.Add("gpu_big_ov_fan_chip_" + Fans(1)); }
float bx = Art(b, X + s * cw + (cw - 78f) / 2, y, 0.4f, layers.ToArray());
Caption(b, X + s * cw, bx, cw, T("gg.fan." + s), s == 0 ? null : Bad);
bottom = bx + 24f;
}
y = bottom + 10f;
y = Section(b, y, L.T("gg.fin.h"));
for (int s = 0; s < 3; s++)
{
var layers = new List<string> { Front("probidia", 1) };
if (s == 1) layers.Add("gpu_big_ov_fin_dim");
if (s == 2) layers.Add("gpu_big_ov_fin_oxide");
float bx = Art(b, X + s * cw + (cw - 78f) / 2, y, 0.4f, layers.ToArray());
Caption(b, X + s * cw, bx, cw, T("gg.fin." + s), s == 0 ? null : Bad);
bottom = bx + 24f;
}
Text(b, bottom + 6f, L.T("gg.wear.note"), "body");
}
// ---- 8 挡板与封条 ----
private static void Page8(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.brk.h"));
float cw = W / 2;
float bottom = y;
for (int s = 0; s < 2; s++)
{
var layers = new List<string> { Front("probidia", 1) };
if (s == 1) layers.Add("gpu_big_ov_brk_scratch");
float bx = Art(b, X + s * cw + (cw - 120f) / 2, y, 0.625f, layers.ToArray());
Caption(b, X + s * cw, bx, cw, T("gg.brk." + s), s == 0 ? null : Bad);
bottom = bx + 12f;
}
y = bottom + 10f;
y = Section(b, y, L.T("gg.seal.h"));
bottom = y;
for (int s = 0; s < 2; s++)
{
var layers = new List<string> { "gpu_big_probidia_b", "gpu_big_ov_print_normal_rtx70" };
if (s == 1) layers.Add("gpu_big_ov_seal_missing");
float ax = X + s * cw + (cw - 120f) / 2;
float bx = Art(b, ax, y, 0.625f, layers.ToArray());
SerialOnSticker(b, ax, y, 0.625f, SampleSerial("RTX70"));
Caption(b, X + s * cw, bx, cw, T(s == 0 ? "gg.seal.0" : "gg.seal.2"), s == 0 ? null : Bad);
bottom = bx + 12f;
}
}
// ---- 9 指出 ----
private static void Page9(PaperUI b)
{
float y = NewPage(b);
y = Section(b, y, L.T("gg.point.h"));
Text(b, y, T("gg.point.body"), "body");
foreach (var e in Examples) ExamplePage(b, e);
}
// ---- 9 起:鉴定示例 ----
private sealed class Ex { public string Title, Label, Chip; public int Health, Fan, Fingers, Bracket, Seal, CountBad, PinBad, SerialBad; }
private static readonly Ex[] Examples =
{
new Ex { Title = "gg.ex.h", Label = "RTX80", Health = 60, Fan = 2, Fingers = 1, Seal = 2 },
new Ex { Title = "gg.ex.h3", Label = "RX800", Health = 35, Fan = 2, Fingers = 2, Bracket = 1, Seal = 2 },
new Ex { Title = "gg.ex.h2", Label = "RTX90", Chip = "RTX60", Health = 90, Fan = 1, CountBad = 1, SerialBad = 2 },
new Ex { Title = "gg.ex.h4", Label = "RTX70", Health = 85, Fan = 1 },
new Ex { Title = "gg.ex.h5", Label = "RX70", Health = 100 },
};
private static string Brand(string model) => Hardware.Specs[model].Brand.ToString();
private static void ExamplePage(PaperUI b, Ex e)
{
float y = NewPage(b);
y = Section(b, y, L.T(e.Title));
string real = e.Chip ?? e.Label;
var u = new Unit { Model = e.Label, FakeOf = e.Chip, Health = e.Health, Look = new CardLook { Seed = 31337, Fan = e.Fan, Fingers = e.Fingers, Bracket = e.Bracket, Sticker = e.Seal, CountBad = e.CountBad, PinBad = e.PinBad, Serial = e.SerialBad } };
long baseV = CardValue.Of(real, e.Health);
string brandKey = Hardware.Specs[real].Brand == "PMD" ? "pmd" : "probidia";
var layers = new List<string> { Front(brandKey, u.ObservedPin, u.ObservedCount) };
int fans = u.ObservedCount;
if (e.Fan == 1) layers.Add("gpu_big_ov_fan_dust2_" + fans);
if (e.Fan == 2) { layers.Add("gpu_big_ov_fan_dust1_" + fans); layers.Add("gpu_big_ov_fan_chip_" + fans); }
if (e.Fingers == 1) layers.Add("gpu_big_ov_fin_dim");
if (e.Fingers == 2) layers.Add("gpu_big_ov_fin_oxide");
if (e.Bracket == 1) layers.Add("gpu_big_ov_brk_scratch");
float bottom = Art(b, X + (W - 140f) / 2, y, 0.73f, layers.ToArray());
y = bottom + 6f;
string name = Hardware.Specs[e.Label].Brand + " " + Items.DisplayModel(e.Label);
y = Text(b, y, L.T("gg.ex.card", name, e.Health), "body", align: "center", gap: 3f);
y = Text(b, y, L.T("gg.ex.serial", CardLabels.SerialText(u)), "caption", Ink, "center", 8f);
const float lx = X + 20f, lw = W - 40f;
if (e.Chip != null)
{
y = Text(b, y, L.T("gg.ex.fake", name, Hardware.Specs[e.Chip].Brand + " " + Items.DisplayModel(e.Chip)), "caption", Bad, "center", 6f);
y = Text(b, y, L.T("gg.ex.claimed", CardValue.Of(e.Label, e.Health)), "caption", Ink, "right", 3f, lx, lw);
}
y = Text(b, y, L.T("gg.ex.base", baseV), "caption", Ink, "right", 3f, lx, lw);
int n = 0; long discSum = 0;
long Disc(double p) { n++; long d = (long)Math.Round(baseV * p); discSum += d; return d; }
if (e.Fan == 1) y = Text(b, y, L.T("gg.ex.fan1", Disc(Tuning.CardFanDust)), "caption", Bad, "right", 3f, lx, lw);
if (e.Fan == 2) y = Text(b, y, L.T("gg.ex.fan", Disc(Tuning.CardFanChipped)), "caption", Bad, "right", 3f, lx, lw);
if (e.Fingers == 1) y = Text(b, y, L.T("gg.ex.fin", Disc(Tuning.CardFingersDark)), "caption", Bad, "right", 3f, lx, lw);
if (e.Fingers == 2) y = Text(b, y, L.T("gg.ex.fin2", Disc(Tuning.CardFingersOxidized)), "caption", Bad, "right", 3f, lx, lw);
if (e.Bracket == 1) y = Text(b, y, L.T("gg.ex.brk", Disc(Tuning.CardBracketScratch)), "caption", Bad, "right", 3f, lx, lw);
if (e.Seal == 2) y = Text(b, y, L.T("gg.ex.seal", Disc(Tuning.CardSealMissing)), "caption", Bad, "right", 3f, lx, lw);
if (n == 0) y = Text(b, y, L.T("gg.ex.nodisc"), "caption", Good, "right", 3f, lx, lw);
y += 3f;
b.AddLine(lx, y, lx + lw, y, 1f, "#6A6458");
y += 6f;
// 外观写死(种子 0)才不会被健康度重算覆盖,价值按它算;序列号要种子,用上面带种子的那个
long total = baseV - discSum; // 页面上各项相加,和游戏的价值函数差不过 1(四舍五入)
y = Text(b, y, L.T("gg.ex.total", total), "label", Ink, "right", 8f, lx, lw);
if (e.Chip != null) y = Text(b, y, L.T("gg.ex.gap", CardValue.Of(e.Label, e.Health) - total), "caption", Bad, "right", 8f, lx, lw);
Text(b, y, L.T("gg.ex.note"), "caption", Ink, "center");
}
}
}
+320
View File
@@ -0,0 +1,320 @@
using System.Collections.Generic;
using System.Linq;
using Decentralized.Domain;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Decentralized
{
/// <summary>
/// 放大镜检视显卡:和香烟一样把物品放大摆在检视台上,正反面各一张,下面是标签栏。
/// 标签栏每行一个标签(型号、风扇、金手指、挡板、丝印、贴纸):左边是卖家的说法,右边的按钮"指出"——
/// 说法属实就没事,隐瞒了问题价格回到真实情况,指错了掉信誉。归自己的卡,有标签机时按钮改成"改",随便改说法。
/// 游戏没有给 mod 开放检视界面,这里不打补丁,只在检视台(InspectionUIManager.panel)下面挂一个自己的界面,
/// 用一个常驻的 MonoBehaviour 每帧看一眼是否正在检视显卡。
/// </summary>
public sealed class GpuInspectView : MonoBehaviour
{
private const float RowH = 26f, CardScale = 3f, PanelTop = -112f, DetailH = 26f, GroupGap = 7f;
private static readonly int RowCount = CardLabels.Rows.Length;
private TextMeshProUGUI priceText;
private static GpuInspectView instance;
private RectTransform root, cardRoot;
private Button flip;
private TextMeshProUGUI flipText;
private GameItem shown;
private string shownSig;
private bool hidGlass;
private Transform buttonTemplate;
private TMP_FontAsset font;
private readonly TextMeshProUGUI[] rowName = new TextMeshProUGUI[16], rowText = new TextMeshProUGUI[16];
private readonly Button[] rowButton = new Button[16];
private readonly TextMeshProUGUI[] rowButtonText = new TextMeshProUGUI[16];
private TextMeshProUGUI serialText;
public static void Ensure()
{
if (instance != null) return;
var go = new GameObject("pd_gpu_inspect_view");
DontDestroyOnLoad(go);
instance = go.AddComponent<GpuInspectView>();
}
public static void Remove()
{
if (instance != null) Destroy(instance.gameObject);
instance = null;
}
private void Update()
{
var m = InspectionUIManager.Instance;
var item = m != null && m.panel != null && m.panel.activeSelf ? m.currentInspectingItem : null;
if (item == null || !Items.IsGpu(item))
{
if (root != null && root.gameObject.activeSelf) root.gameObject.SetActive(false);
shown = null;
if (hidGlass && m != null && m.magnifyingGlass != null) { m.magnifyingGlass.SetActive(true); hidGlass = false; }
return;
}
Log.Guard("显卡检视大图", () =>
{
if (root == null) Build(m);
if (!root.gameObject.activeSelf) root.gameObject.SetActive(true);
// 检视台自带的放大镜图标会压在大图下面,显卡用不着
if (m.magnifyingGlass != null && m.magnifyingGlass.activeSelf) { m.magnifyingGlass.SetActive(false); hidGlass = true; }
var keys = new List<string>();
string sig = Items.LookKeys(item, keys) + "|" + string.Join(",", keys) + "|" + item.unitValue + "|" + Items.Exposed(item) + "|" + (Items.GetClaim(item)?.Encode() ?? "") + "|" + GeneralHelper.IsItemOwned(item) + "|" + HasLabeler();
if (item != shown || sig != shownSig)
{
shown = item;
shownSig = sig;
Redraw(item);
UpdateRows(item);
}
});
}
/// <summary>检视台上的叠加层:每个标签的每个取值对应一种细节,一一对应。</summary>
public static List<string> BigOverlays(GameItem item)
{
var o = new List<string>();
var u = Items.ToUnit(item);
if (u.Dead) return o;
var l = u.Appearance;
string P = "gpu_big_ov_";
if (!Items.IsBack(item))
{
int n = Items.FansOf(u);
if (l.Fan == 1) o.Add(P + "fan_dust2_" + n);
if (l.Fan == 2) { o.Add(P + "fan_dust1_" + n); o.Add(P + "fan_chip_" + n); }
if (l.Fingers == 1) o.Add(P + "fin_dim");
if (l.Fingers == 2) o.Add(P + "fin_oxide");
if (l.Bracket == 1) o.Add(P + "brk_scratch");
}
else
{
string model = (l.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model).ToLowerInvariant();
o.Add(P + "print_" + (l.Print == 2 ? "skew_" : l.Print == 1 ? "stamp_" : "normal_") + model);
if (l.Sticker == 2) o.Add(P + "seal_missing");
}
return o;
}
private static bool HasLabeler() => EmporiumEntry.Instance != null && EmporiumEntry.Instance.GetAllOwnedItemsByTag("LABELER_TAG").Count > 0;
private static void MakeRect(Transform parent, string name, Vector2 pos, Vector2 size, Color color)
{
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(parent, false);
rt.sizeDelta = size;
rt.localPosition = pos;
var img = go.GetComponent<Image>();
img.color = color;
img.raycastTarget = false;
}
private TextMeshProUGUI MakeText(Transform parent, string name, Vector2 pos, Vector2 size, TextAlignmentOptions align, float fontSize)
{
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(parent, false);
rt.sizeDelta = size;
rt.localPosition = pos;
var t = go.GetComponent<TextMeshProUGUI>();
if (font != null) t.font = font;
t.fontSize = fontSize;
t.alignment = align;
t.enableWordWrapping = false;
t.overflowMode = TextOverflowModes.Ellipsis;
t.raycastTarget = false;
return t;
}
private Button MakeButton(Transform parent, string name, Vector2 pos, Vector2 size, out TextMeshProUGUI label)
{
var go = Instantiate(buttonTemplate.gameObject, parent, false);
go.name = name;
var rt = (RectTransform)go.transform;
rt.sizeDelta = size;
rt.localPosition = pos;
var b = go.GetComponent<Button>();
b.onClick = new Button.ButtonClickedEvent();
label = go.GetComponentInChildren<TextMeshProUGUI>();
label.enableAutoSizing = true; label.fontSizeMin = 12; label.fontSizeMax = 24;
// 去掉复制来的按钮上驱动文字的本地化组件,文字由这里设置
foreach (var mb in go.GetComponentsInChildren<MonoBehaviour>(true))
{
string n = mb.GetType().Name;
if (mb is Button || mb is Image || mb is TextMeshProUGUI) continue;
if (n.Contains("Local") || n.Contains("Appraisal") || n.Contains("Send")) Destroy(mb);
}
return b;
}
private void Build(InspectionUIManager m)
{
root = new GameObject("_PdGpuInspection", typeof(RectTransform)).GetComponent<RectTransform>();
root.SetParent(m.panel.transform, false);
root.sizeDelta = new Vector2(1920, 1080);
root.localPosition = Vector3.zero;
cardRoot = new GameObject("Card", typeof(RectTransform)).GetComponent<RectTransform>();
cardRoot.SetParent(root, false);
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);
serialText.color = new Color(0.1f, 0.1f, 0.13f);
serialText.fontStyle = FontStyles.Bold;
serialText.overflowMode = TextOverflowModes.Overflow;
serialText.rectTransform.sizeDelta = new Vector2(48 * CardScale, 14 * CardScale);
if (buttonTemplate == null) return;
font = buttonTemplate.GetComponentInChildren<TextMeshProUGUI>()?.font;
if (font != null) serialText.font = font;
// 标签区:深色底加浅色边框,和上面的大图分开。三组:四项真伪依据、型号、四项成色,最下面一行是标签价
float panelH = RowCount * RowH + 10f + DetailH + GroupGap * 2, 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, "LabelDivider1", new Vector2(0, GroupTop(CardLabels.AuthRows) + GroupGap / 2 + 1), new Vector2(620, 2), new Color(0.78f, 0.68f, 0.45f, 0.8f));
MakeRect(root, "LabelDivider2", new Vector2(0, GroupTop(CardLabels.AuthRows + 1) + GroupGap / 2 + 1), new Vector2(620, 2), new Color(0.78f, 0.68f, 0.45f, 0.5f));
MakeRect(root, "LabelDivider3", new Vector2(0, PanelTop - 5 - RowH * RowCount - GroupGap * 2 - 1), new Vector2(620, 2), new Color(0.78f, 0.68f, 0.45f, 0.8f));
priceText = MakeText(root, "LabelPrice", new Vector2(165, PanelTop - 5 - RowH * RowCount - GroupGap * 2 - DetailH / 2 - 2), new Vector2(300, DetailH), TextAlignmentOptions.Right, 21);
priceText.color = new Color(1f, 0.85f, 0.45f);
PledgerSkin.Fit(priceText);
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 = GroupTop(i) - RowH / 2;
rowName[i] = MakeText(root, "RowName" + i, new Vector2(-232, y), new Vector2(156, RowH), TextAlignmentOptions.Left, 20);
rowText[i] = MakeText(root, "RowText" + i, new Vector2(24, y), new Vector2(300, RowH), TextAlignmentOptions.Left, 20);
rowButton[i] = MakeButton(root, "RowButton" + i, new Vector2(245, y), new Vector2(110, RowH - 3), out rowButtonText[i]);
rowButton[i].onClick.AddListener(() => Log.Guard("标签栏", () => OnRowButton(row)));
}
}
/// <summary>第 i 行的上沿(三组之间各留一道间隔)。</summary>
private static float GroupTop(int i) => PanelTop - 5 - RowH * i - (i >= CardLabels.AuthRows ? GroupGap : 0f) - (i >= CardLabels.AuthRows + 1 ? GroupGap : 0f);
private static string RowKey(int row) => row == CardLabels.RowSpec ? "spec" : row == CardLabels.RowCount ? "count" : row == CardLabels.RowPin ? "pin" : row == CardLabels.RowFan ? "fan" : row == CardLabels.RowFingers ? "fingers"
: row == CardLabels.RowBracket ? "bracket" : row == CardLabels.RowPrint ? "print" : row == CardLabels.RowSerial ? "serial" : "seal";
private static string ValueText(int row, int value) => row == CardLabels.RowSpec ? "" : L.T("lbl." + RowKey(row) + "." + (row == CardLabels.RowSeal && value == 1 ? 0 : value));
private static string RowName(int row) => L.T("lbl.row." + RowKey(row));
/// <summary>现场有没有卖家 / 鉴定客户在:决定"指出"按钮是否出现。</summary>
private static bool CanPoint(GameItem item)
{
var client = PlayerStore.Instance.currentClientInstance?.GetClientBlueprint();
if (client == null || GeneralHelper.IsItemOwned(item)) return false;
if (client.clientIntent == StoreClient.ClientIntent.APPRAISAL_SERVICE) return true;
return client.clientIntent == StoreClient.ClientIntent.SELL && !InspectionUIManager.IsConfrontLocked();
}
private void UpdateRows(GameItem item)
{
if (rowName[0] == null) return;
var u = Items.ToUnit(item);
var real = Claim.Truth(u);
var claim = Items.GetClaim(item);
int exposed = Items.Exposed(item);
bool owned = GeneralHelper.IsItemOwned(item);
bool canPoint = CanPoint(item), canEdit = owned && HasLabeler();
for (int i = 0; i < RowCount; i++)
{
int row = CardLabels.Rows[i];
rowName[i].text = RowName(row);
rowName[i].color = i < CardLabels.AuthRows ? new Color(1f, 0.85f, 0.45f) : new Color(0.6f, 0.85f, 1f); // 真伪行金黄,成色行浅蓝
rowName[i].fontSize = 20; PledgerSkin.Fit(rowName[i]); PledgerSkin.Fit(rowText[i]);
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));
if (claim == null) text = Show(real);
else if ((exposed & row) != 0)
{
text = Show(real);
bool lied = row == CardLabels.RowSpec ? claim.Value.Model != real.Model : claim.Value.Get(row) != real.Get(row);
if (lied) color = new Color(1f, 0.45f, 0.4f); // 卖家的说法只在机制里起作用,面板上不写
else color = new Color(0.5f, 1f, 0.55f);
}
else text = Show(claim.Value);
rowText[i].text = text;
rowText[i].color = color;
bool editable = row == CardLabels.RowSpec || !CardLabels.IsAuth(row);
bool hasButton = row == CardLabels.RowSpec || !CardLabels.IsAuth(row);
rowButton[i].gameObject.SetActive(hasButton && (canEdit ? editable : canPoint && (exposed & row) == 0));
rowButtonText[i].text = L.T(canEdit ? "lbl.edit" : row == CardLabels.RowSpec ? "lbl.point.model" : "lbl.point");
}
if (priceText != null) priceText.text = L.T("lbl.price", item.unitValue);
if (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
// 贴纸上的序列号:只在背面显示
if (serialText != null)
{
serialText.gameObject.SetActive(Items.IsBack(item) && !u.Dead);
serialText.text = CardLabels.SerialText(u);
// 贴纸空白处中心在大图 (142, 91):卡片坐标以图中心为原点,y 向上
serialText.rectTransform.localPosition = new Vector3((142 - 96) * CardScale, 85 + (64 - 91) * CardScale, 0);
}
}
private static string DisplayModel(string model) => Items.DisplayModel(model);
private void OnRowButton(int row)
{
if (shown == null) return;
var item = shown;
if (GeneralHelper.IsItemOwned(item))
{
// 标签机:改说法,每按一次换下一个取值(型号是在八个型号里轮换)
var u = Items.ToUnit(item);
var cur = Items.GetClaim(item) ?? Claim.Truth(u);
if (row == CardLabels.RowSpec)
{
var models = Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap).Select(s => s.Model).ToList();
Items.EditClaim(item, row, 0, models[(models.IndexOf(cur.Model) + 1) % models.Count]);
}
else Items.EditClaim(item, row, row == CardLabels.RowSeal ? (cur.Get(row) == 0 ? 2 : 0) : row == CardLabels.RowBracket ? (cur.Get(row) + 1) % 2 : (cur.Get(row) + 1) % 3);
EmporiumAudioPlayer.Instance?.audioLabeler?.Play();
return;
}
int kind = Items.Point(item, row);
Log.Info($"指出 {row},结果 {kind},报价 {item.unitValue}");
if (kind == 0)
{
InspectionUIManager.Instance.OnConfrontFailed();
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T("point.none." + Random.Range(1, 3))));
}
else
{
InspectionUIManager.Instance.OnConfrontSuccess();
DialogUIManager.Instance.StartDialogue(new Dialogue().SetText("CLIENT", L.T(kind == 2 ? "point.fake." + Random.Range(1, 3) : "point.worn." + Random.Range(1, 3))));
}
}
private void Redraw(GameItem item)
{
foreach (Transform c in cardRoot) Destroy(c.gameObject);
var keys = new List<string>();
string baseKey = Items.LookKeys(item, keys); // 小图键名只用来取底图(品牌与正反面)
var layers = new List<string> { "gpu_big_" + baseKey.Substring("gpu_".Length) };
layers.AddRange(BigOverlays(item));
foreach (var k in layers)
{
var sprite = Items.ModSprite(k);
if (sprite == null) continue;
var go = new GameObject(k, typeof(RectTransform), typeof(Image));
var rt = go.GetComponent<RectTransform>();
rt.SetParent(cardRoot, false);
rt.sizeDelta = new Vector2(sprite.rect.width, sprite.rect.height);
rt.localScale = Vector3.one * CardScale;
var img = go.GetComponent<Image>();
img.sprite = sprite;
img.raycastTarget = false;
}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>Web3 手册阅读窗口:左边章节列表,右边章节全文。章节在本地化文本里用 § 分隔,章节名取第一个冒号或括号之前。</summary>
public static class HandbookWindow
{
public const string Id = "pd_handbook";
private const float WinW = 828f, WinH = 680f, ScreenW = 768f, ScreenH = 578f, Bezel = 6f;
private static float ListW => L.Wide ? 276f : 190f;
private static float RowW => L.Wide ? ListW - 52f : ListW - 12f;
private static float TextW => L.Wide ? 456f : 540f;
private static int _chapter;
private static readonly List<GameObject> _picked = new List<GameObject>();
private static readonly List<Image> _clear = new List<Image>();
private static readonly List<PledgerSkin.RowDef> _rows = new List<PledgerSkin.RowDef>();
private static string Title(string chapter)
{
int end = chapter.Length;
foreach (var m in new[] { ":", ": ", "(", " (" })
{
int i = chapter.IndexOf(m, StringComparison.Ordinal);
if (i > 0 && i < end) end = i;
}
return chapter.Substring(0, end).TrimEnd();
}
private static string Body(string chapter)
{
foreach (var m in new[] { ":", ": " })
{
int i = chapter.IndexOf(m, StringComparison.Ordinal);
int p = chapter.IndexOf("(", StringComparison.Ordinal);
if (i > 0 && (p < 0 || i < p || p > 40)) return chapter.Substring(i + m.Length);
}
return chapter;
}
private static string _key = "guide.web3.text", _nameKey = "guide.web3.title";
private static readonly Dictionary<string, int> _chapterOf = new Dictionary<string, int>();
public static void Open() => Show("guide.web3.text", "guide.web3.title");
private static void Show(string key, string nameKey)
{
_key = key; _nameKey = nameKey;
_chapter = _chapterOf.TryGetValue(key, out var ch0) ? ch0 : 0;
Reopen();
}
private static void Reopen()
{
_chapterOf[_key] = _chapter;
var chapters = TuningText.Render(L.T(_key)).Replace("¶", "\n\n").Split('§');
if (_chapter >= chapters.Length) _chapter = 0;
var b = CustomUIManager.Window(Id, L.T(_nameKey));
if (b == null) return;
_picked.Clear(); _clear.Clear(); _rows.Clear();
b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
b.BeginPanel(ScreenW + Bezel * 2, ScreenH + Bezel * 2, TextAnchor.MiddleCenter, Bezel + 3f, 0f);
var bezel = b.Last.image;
b.BeginPanel(ScreenW, ScreenH, TextAnchor.UpperLeft, 12f, 4f);
var screen = b.CurrentContainer;
var paper = b.Last.image;
b.BeginRow(10f);
b.BeginPanel(ListW, -1f, TextAnchor.UpperLeft, 0f, 0f);
_clear.Add(b.Last.image);
b.BeginScroll(ScreenH - 30f);
_clear.Add(b.Last.image);
for (int i = 0; i < chapters.Length; i++)
{
int k = i;
PledgerSkin.TableRow(b, RowW, 30f, new[] { RowW }, new[] { TextAnchor.MiddleLeft },
new[] { Title(chapters[i]) }, i == _chapter, () => { _chapter = k; Reopen(); }, _rows, _clear);
}
b.End();
b.End();
b.BeginPanel(TextW, -1f, TextAnchor.UpperLeft, 6f, 6f);
_clear.Add(b.Last.image);
b.BeginScroll(ScreenH - 30f);
_clear.Add(b.Last.image);
b.AddHeader(Title(chapters[_chapter]));
b.AddSeparator();
b.AddLabel(Body(chapters[_chapter]));
b.End();
b.End();
b.End();
b.End();
b.End();
var win = b.Show();
Log.Guard("手册外观", () => PledgerSkin.Apply(win, screen, bezel, paper, _picked, _clear, _rows));
}
}
}
+483 -27
View File
@@ -15,17 +15,38 @@ namespace Decentralized
{
public const string Terminal = "pd_terminal";
public const string Rig = "pd_rig";
public const string Guide = "pd_gpu_guide";
public const string GpuPrefix = "pd_gpu_";
public const string HwWallet = "pd_hw_wallet";
public const string KeyDrive = "pd_key_drive";
/// <summary>硬件钱包被动过手脚(供应链投毒)。外观、说明都不显示。</summary>
public const string PoisonTag = "PD_POISONED";
public const string HealthTag = "PD_HEALTH";
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", EditedTag = "PD_EDITED";
public static Action OpenTerminal;
public static Action OpenGpuHandbook;
/// <summary>硬件钱包拖到交易终端上:添加一个钱包(参数:是否被投毒)。</summary>
public static Action<bool> AddWallet;
public static Func<bool> CanAddWallet;
public static void Subscribe() => ModHook.OnModItemDirectoryInit += Register;
public static void Unsubscribe() => ModHook.OnModItemDirectoryInit -= Register;
public static void Subscribe()
{
ModHook.OnModItemDirectoryInit += Register;
ModHook.OnLeavingStoreEarly += SweepFront;
ModHook.OnGoingSleepEarly += SweepFront;
}
public static void Unsubscribe()
{
ModHook.OnModItemDirectoryInit -= Register;
ModHook.OnLeavingStoreEarly -= SweepFront;
ModHook.OnGoingSleepEarly -= SweepFront;
}
public static string GpuId(string model) => GpuPrefix + model.ToLowerInvariant();
@@ -35,7 +56,14 @@ namespace Decentralized
{
dir.Add(Terminal, CreateTerminal);
dir.Add(Rig, CreateRig);
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic))
WrapElectronics(dir);
WrapTurbos(dir);
OtcLamp.Wrap(dir);
dir.Add(Guide, CreateGuide);
dir.Add(HwWallet, CreateHwWallet);
dir.Add(KeyDrive, CreateKeyDrive);
AsicItems.Register(dir);
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap))
{
var s = spec;
dir.Add(GpuId(s.Model), () => CreateGpu(s));
@@ -60,31 +88,278 @@ namespace Decentralized
return item;
}
/// <summary>显卡鉴定手册:型号对照、序列号规则、外观折扣。做法同游戏里的鉴定手册(海明威鉴定指南):纸张图、文档类型、拖放音效。</summary>
private static GameItem CreateGuide()
{
var item = ItemDirectory.CreateEmptyItem(Guide);
item.SetSpriteAndShape("Items/items_misc", "guide1");
item.EnableTag("DOCUMENT");
item.SetGameItemType("DOCUMENT");
item.EnableTag("GUIDE_PAPER_TAG");
BookHelper.InitPaperGuide(item);
item.name = L.T("item.guide.name");
item.shortDescription = L.T("item.guide.desc");
item.mayActivateSlotItemFunc = (_, _, _) => true;
item.canActivateSlotItemFunc = (_, _, _) => true;
item.onActivateSlotItemFunc = (_, _, _) => Log.Guard("打开鉴定手册", () => OpenGpuHandbook?.Invoke());
item.unitValue = 50;
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
/// <summary>
/// PLEDGER 硬件钱包:和游戏里的升级材料一样,拖到交易终端上用掉,钱包 +1。杰克逊博士卖的是正品;
/// Web3 商城卖的便宜一些,外观和说明完全一样,其中一部分被动过手脚(<see cref="PoisonTag"/>)。
/// </summary>
private static GameItem CreateHwWallet()
{
var item = ItemDirectory.CreateEmptyItem(HwWallet);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "hw_wallet");
item.name = L.T("item.hwwallet.name");
item.shortDescription = L.T("item.hwwallet.desc");
item.unitValue = Tuning.WalletJacksonPrice;
item.SetGameItemType("MATERIAL");
item.maySelectSlotItemFunc = (_, _, _) => true;
item.canSelectSlotItemFunc = (_, _, _) => true;
item.mayThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(item.mayThisTargetItemFunc, (Func<GameItem, GameItem, bool>)((s, t) => CanTarget(t)));
item.canThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(item.canThisTargetItemFunc, (Func<GameItem, GameItem, bool>)((s, t) => CanTarget(t)));
item.onThisTargetItemFunc = (Action<GameItem, GameItem>)Delegate.Combine(item.onThisTargetItemFunc, (Action<GameItem, GameItem>)((s, t) => Log.Guard("添加硬件钱包", () =>
{
if (!CanTarget(t)) return;
bool poisoned = s.IsTag(PoisonTag);
s.Destroy();
AddWallet?.Invoke(poisoned);
})));
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
/// <summary>Tumbler 的密钥盘:违禁品(高),搜查时被翻出来按价值记罪证。</summary>
private static GameItem CreateKeyDrive()
{
var item = ItemDirectory.CreateEmptyItem(KeyDrive);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "key_drive");
item.name = L.T("item.keydrive.name");
item.shortDescription = L.T("item.keydrive.desc");
item.unitValue = Tuning.TumblerDriveValue;
ContrabandHelper.InitContrabandItem(item, 3);
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
return item;
}
private static bool CanTarget(GameItem t) => t != null && t.identifier == Terminal && (CanAddWallet?.Invoke() ?? false);
public static GameItem NewHwWallet(bool poisoned)
{
var item = ModHelper.SpawnItem(HwWallet);
if (item != null && poisoned) item.EnableTag(PoisonTag);
return item;
}
// ---------- 显卡 ----------
/// <summary>RZX70 → "RZX 70",RZX80Ti → "RZX 80 Ti",RZ6800 → "RZ 6800"。</summary>
/// <summary>RTX70 → "RTX 70",RX800 → "RX 800"。</summary>
public static string DisplayModel(string model) => Regex.Replace(Regex.Replace(model, @"(?<=[A-Za-z])(?=\d)", " "), @"(?<=\d)(?=[A-Z])", " ");
private static GameItem CreateGpu(HardwareSpec spec)
{
var item = ItemDirectory.CreateEmptyItem(GpuId(spec.Model));
item.SetSpriteAndShapeFromMod(ModInfo.Id, spec.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd");
item.SetGameItemType("ELECTRONIC");
item.SetGameItemType("MATERIAL");
InspectableHelper.InitInspectableItem(item);
item.onLoaded = Refresh;
// 右键"切换"翻面;外观差异用叠加图画在同一张底图上
item.EnableTag(ToggleAppearanceHelper.TOGGLE_APPEARANCE_TAG);
item.mayToggleSlotItemFunc = (_, _, _) => true;
item.canToggleSlotItemFunc = (_, _, _) => true;
item.onToggleSlotItemFunc = (toggled, _, _) => Log.Guard("显卡翻面", () =>
{
Flip(toggled);
});
item.getItemSpriteModifierForItemFunc = Overlays;
// 摆给顾客的时候,顾客可能看出说法与实物对不上
item.onSlotAddItemFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Combine(item.onSlotAddItemFunc,
(Action<GameItem, GameInventory, SlotMarker>)((it, _, __) => Log.Guard("顾客验货", () => OnOffered(it))));
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
Refresh(item);
return item;
}
/// <summary>按给定的状态造一张卡(二手卡、假卡):写入标签并生成外观。</summary>
public static GameItem NewCard(string model, double health, string fakeOf, Rng rng, DealerKind? seller = null, int markupPct = 0)
{
var item = ModHelper.SpawnItem(GpuId(model));
var u = new Unit { Model = model, FakeOf = fakeOf, Health = health, Look = CardLook.Generate(rng, health, fakeOf != null, Hardware.Specs[model].Tier, fakeOf != null ? Hardware.Specs[fakeOf].Tier : Hardware.Specs[model].Tier) };
if (health >= 100 && fakeOf == null) u.Look.Sticker = 0; // 全新卡:封条完好
Store(item, u);
if (seller != null)
{
GeneralHelper.SetItemOwned(item, false); // 卖家的货:买下之前不属于玩家,说法才有效
SetClaim(item, CardLabels.DealerClaim(u, seller.Value, rng), 0, markupPct);
}
return item;
}
// ---------- 标签栏:卖家的说法与指出 ----------
private static object checkedClient;
private static readonly HashSet<int> checkedItems = new HashSet<int>();
/// <summary>被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。</summary>
public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(Tuning.RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
/// <summary>
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来(型号档次不符的必看出来,供电口和风扇数一眼可见);看出来就拆穿这一项(价格回到真实情况),
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
/// </summary>
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) || (!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;
var claim = GetClaim(item);
if (claim == null) return;
if (!ReferenceEquals(checkedClient, ci)) { checkedClient = ci; checkedItems.Clear(); }
if (!checkedItems.Add(item.uniqueId)) return;
var u = ToUnit(item);
int exposed = Exposed(item);
int lies = CardLabels.Lies(u, claim.Value, exposed);
if (lies == 0) return;
var rng = Session.Data.Rng;
int found = 0;
foreach (int r in CardLabels.Rows)
if ((lies & r) != 0 && rng.Chance((r == CardLabels.RowCount || r == CardLabels.RowPin) ? Tuning.CustomerSpotTierMismatch : Tuning.CustomerSpotChance))
{
found |= r;
if (r != CardLabels.RowSpec && CardLabels.IsEvidence(r, claim.Value, Claim.Truth(u))) found |= CardLabels.RowSpec | CardLabels.RowCount | CardLabels.RowPin | CardLabels.RowPrint | CardLabels.RowSerial;
}
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, -rep);
EmporiumAudioPlayer.Instance?.audioFail?.Play();
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))));
}
/// <summary>标签上的说法;没有说法的卡(自己挖出来、正规渠道的新卡)一律按真实情况算。</summary>
public static Claim? GetClaim(GameItem item)
{
if (!item.IsTag(ClaimTag)) return null;
var c = Claim.Decode(item.GetTagReadonly(ClaimTag).GetString());
return string.IsNullOrEmpty(c.Model) ? (Claim?)null : c;
}
/// <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.RowCount | CardLabels.RowPin | CardLabels.RowSerial | CardLabels.RowPrint;
return e;
}
public static void SetClaim(GameItem item, Claim claim, int exposed, int markupPct = 0)
{
item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (markupPct != 0) item.ModifyTag(MarkupTag, t => t.Enable().SetInt(markupPct));
Refresh(item);
}
/// <summary>现在标签上写的型号:说法里的型号,被拆穿之后是真实芯片。</summary>
public static string BelievedModel(GameItem item, Unit u)
{
var c = GetClaim(item);
if (c == null) return u.Model;
return (Exposed(item) & CardLabels.RowSpec) != 0 ? (u.FakeOf ?? u.Model) : c.Value.Model;
}
/// <summary>玩家指出一个标签:记下来,说法改回真实情况;返回 0 说法属实、1 隐瞒了不利的情况、2 拆穿假货。</summary>
public static int Point(GameItem item, int row)
{
var u = ToUnit(item);
var claim = GetClaim(item) ?? Claim.Truth(u);
int exposed = Exposed(item);
int kind = CardLabels.Point(u, claim, row, ref exposed);
item.ModifyTag(ExposedTag, t => t.Enable().SetInt(exposed));
if (!item.IsTag(ClaimTag)) item.ModifyTag(ClaimTag, t => t.Enable().SetString(claim.Encode()));
Refresh(item);
if (item.IsTag("APPRAISAL_ITEM")) AppraisalHelper.SetDefaultAppraisalValue(item, (int)item.unitValue);
return kind;
}
/// <summary>用标签机改说法(随便改):这一项重新算作"没人指出过"。</summary>
public static void EditClaim(GameItem item, int row, int value, string model = null)
{
var u = ToUnit(item);
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);
}
/// <summary>说明:算力和耗电按标称写;成色在归玩家之前只给粗略等级(要看外观自己判断),归玩家之后给精确健康度。</summary>
private static string Desc(GameItem item, Unit u)
{
var label = Hardware.Specs[BelievedModel(item, u)];
string cond = GeneralHelper.IsItemOwned(item)
? Mathf.RoundToInt((float)u.Health) + "%"
: L.T(u.Health >= 85 ? "item.gpu.grade.1" : u.Health >= 60 ? "item.gpu.grade.2" : u.Health >= 35 ? "item.gpu.grade.3" : "item.gpu.grade.4");
return L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), cond);
}
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;
string name = L.T("item.gpu.name", Hardware.Specs[BelievedModel(item, u)].Brand, DisplayModel(BelievedModel(item, u)));
if (item.name != name) item.name = name;
long value = CardLabels.Value(u, GetClaim(item), Exposed(item));
if (!GeneralHelper.IsItemOwned(item) && item.IsTag(MarkupTag)) value = Math.Max(1, (long)Math.Round(value * (1 + item.GetTagReadonly(MarkupTag).GetInt() / 100.0)));
if (item.unitValue != value) item.unitValue = value;
}
public static Unit ToUnit(GameItem item)
{
string model = item.identifier.Substring(GpuPrefix.Length);
model = Hardware.Specs.Keys.First(k => k.ToLowerInvariant() == model);
bool asic = AsicItems.Is(item);
string model = item.identifier.Substring(asic ? AsicItems.Prefix.Length : GpuPrefix.Length);
model = Hardware.Specs.Keys.FirstOrDefault(k => k.ToLowerInvariant() == model) ?? (asic ? "M1" : "RTX60");
var u = new Unit { Model = model, Health = 100 };
if (asic)
{
// 矿机没有外观、没有真假:不记外观种子,算力、损耗、暴毙都按规格表
if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
u.Dead = item.IsTag(DeadTag);
return u;
}
if (item.IsTag(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString();
u.Refurbished = item.IsTag(RefurbTag);
u.Dead = item.IsTag(DeadTag);
if (item.IsTag(LookTag))
{
var enc = item.GetTagReadonly(LookTag).GetString();
u.Look = CardLook.Legacy(enc, CardLook.Decode(enc), u.Model, u.FakeOf); // 旧存档的假卡补上风扇数、供电口破绽
}
// 外观种子:没有记录的卡(正规渠道的新卡)用物品编号当种子,之后同样随损耗变旧
u.Look.Seed = item.IsTag(SeedTag) ? (uint)item.GetTagReadonly(SeedTag).GetInt() : (uint)(item.uniqueId * 2654435761u) | 1u;
return u;
}
@@ -94,12 +369,18 @@ namespace Decentralized
if (u.FakeOf != null) item.ModifyTag(FakeTag, s => s.Enable().SetString(u.FakeOf));
if (u.Refurbished) item.EnableTag(RefurbTag);
if (u.Dead) item.EnableTag(DeadTag);
if (!AsicItems.Is(item))
{
item.ModifyTag(LookTag, s => s.Enable().SetString(u.Look.Encode()));
item.ModifyTag(SeedTag, s => s.Enable().SetInt((int)u.Look.Seed));
}
Refresh(item);
}
/// <summary>按标签更新名字、说明、价值和图:显示的是标签上的型号(假卡也显示被冒充的型号)。</summary>
public static void Refresh(GameItem item)
{
if (AsicItems.Is(item)) { AsicItems.Refresh(item); return; }
var u = ToUnit(item);
var label = u.Label;
if (u.Dead)
@@ -107,12 +388,69 @@ namespace Decentralized
item.name = L.T("item.gpu.burnt.name", label.Brand, DisplayModel(label.Model));
item.shortDescription = L.T("item.gpu.burnt.desc");
item.unitValue = Math.Max(1, label.Price / 20);
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_burnt");
item.SetSpriteAndShapeFromMod(ModInfo.Id, item.IsTag(BackTag) ? "gpu_burnt_b" : "gpu_burnt_f");
return;
}
item.name = L.T("item.gpu.name", label.Brand, DisplayModel(label.Model));
item.shortDescription = L.T("item.gpu.desc", Fmt.Num(label.DisplayHash), Fmt.Num(label.Kj), Mathf.RoundToInt((float)u.Health));
item.unitValue = Math.Max(1, (long)Math.Round(label.Price * u.Health / 100));
item.SetSpriteAndShapeFromMod(ModInfo.Id, LookKeys(item, new List<string>()));
SyncValue(item);
}
public static bool IsBack(GameItem item) => item.IsTag(BackTag);
public static void Flip(GameItem item)
{
if (IsBack(item)) item.ModifyTag(BackTag, t => t.Disable()); else item.EnableTag(BackTag);
Refresh(item);
}
/// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
/// <summary>实物上的风扇数:由真实芯片的档次决定(低档次的板子套高档次的壳,风扇数露馅)。</summary>
public static int FansOf(Unit u) => u.ObservedCount;
public static string LookKeys(GameItem item, List<string> overlays)
{
var u = ToUnit(item);
var l = u.Appearance;
bool back = IsBack(item);
string brand = u.Label.Brand == Hardware.Probidia ? "gpu_probidia" : "gpu_pmd";
if (u.Dead) return back ? "gpu_burnt_b" : "gpu_burnt_f";
if (!back)
{
if (l.Fan > 0) overlays.Add($"gpu_ov_fan{l.Fan}_{FansOf(u)}");
if (l.Fingers > 0) overlays.Add("gpu_ov_fin" + l.Fingers);
if (l.Bracket > 0) overlays.Add("gpu_ov_brk1");
}
else
{
// 丝印:0 标称型号;1 印的是真实芯片型号;2 标称型号但字歪(重印过)
string model = l.Print == 1 && u.FakeOf != null ? u.FakeOf : u.Model;
overlays.Add((l.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
if (l.Sticker == 2) overlays.Add("gpu_ov_stk2");
}
return brand + (back ? "_b" : $"_f_p{u.ObservedPin}n{u.ObservedCount}");
}
public static Sprite ModSprite(string key)
{
var mod = ModLoader.Mods.Find(m => m.Manifest.ID.Equals(ModInfo.Id));
return mod != null && mod.Sprites.TryGetValue(key, out var sprite) ? sprite : null;
}
/// <summary>物品栏里的外观叠加图。</summary>
private static List<ItemSpriteModifier> Overlays(GameItem item)
{
var list = new List<ItemSpriteModifier>();
SyncValue(item); // 归属变化(买下)后价格随之变回真实价值
if (item.IsTag(DeadTag)) return list;
var keys = new List<string>();
LookKeys(item, keys);
int priority = 0;
foreach (var k in keys)
{
var sprite = ModSprite(k);
if (sprite != null) list.Add(new ItemSpriteModifier(sprite, Vector2.zero, 0, false, priority++, false, Color.white));
}
return list;
}
// ---------- 矿架 ----------
@@ -124,17 +462,26 @@ namespace Decentralized
private static GameItem CreateRig()
{
var battery = new GameSlotInventory();
var upgrade = new GameSlotInventory();
var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7);
// 显卡每张 2×2 格:一列 2 格宽的格子,每个槽两行;未解锁的行不可放
// 显卡每张 3×2 格:3 格宽的格子,每张卡 3×2 格,每个槽两行;未解锁的行不可放
var cards = new GameGridInventory(RigSlots.Shape(RigSlots.Base), RigSlots.CellWidth);
// 状态面板:显卡数、耗电、算力(只读)
var stCount = new MachineryText("", 6, TMPro.HorizontalAlignmentOptions.Left, false);
var stPower = new MachineryText("", 6, TMPro.HorizontalAlignmentOptions.Left, false);
var stHash = new MachineryText("", 6, TMPro.HorizontalAlignmentOptions.Left, false);
GameItem rigItem = null;
var status = new MachineryStack(true, 6).Add(stCount).Add(stPower).Add(stHash).Add(Payout.Build(() => rigItem, out var stAddr));
var window = MachineHelper.CreateMachineryWindow(true,
MachineHelper.PowerPanel(battery),
MachineHelper.Panel(L.T("rig.label.modules"), modules, "module"),
MachineHelper.Panel(L.T("rig.label.cards"), cards, "cards"),
MachineHelper.Panel(L.T("rig.label.upgrade"), upgrade, "upgrade"));
MachineHelper.Panel(L.T("rig.label.status"), status, "status", false));
var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window);
rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig");
rigItem = rig;
RigStatus.Add(rig, new[] { stCount, stPower, stHash });
Payout.Register(rig, stAddr);
RigInv.Remove(rig); RigInv.Add(rig, Tuple.Create(cards, (GameInventory)modules));
rig.name = L.T("item.rig.name");
rig.unitValue = Tuning.RigPrice;
MachineHelper.SetupBatterySlot(battery, rig);
@@ -146,13 +493,6 @@ namespace Decentralized
cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag);
cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards);
cards.onSlotRemoveItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards);
// 升级槽:只收下一级需要的电子元件,放进去立刻投入(计入进度),攒够解锁一个显卡槽
upgrade.mayInventoryAddItemFunc = (item, _) =>
{
var need = RigSlots.Next(SlotsOf(rig));
return GeneralHelper.IsItemOwned(item) && need != null && item.identifier == need.Value.Item;
};
upgrade.onSlotAddItemEndFunc = (item, _, _) => Log.Guard("矿架升级槽位", () => TakeUpgrade(rig, upgrade, cards, modules, item));
rig.onLoaded = _ => { ApplySlots(rig, cards); UpdateRig(rig, modules, cards); };
rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, modules, cards));
ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name);
@@ -168,18 +508,86 @@ namespace Decentralized
rig.contentWindow?.Validate();
}
private static void TakeUpgrade(GameItem rig, GameSlotInventory upgradeSlot, GameGridInventory cards, GameInventory modules, GameItem item)
/// <summary>
/// 电子元件作用在矿架上:游戏按“材料作用于目标”判断,元件自己的判断函数对矿架一律为否(三方判断取与),
/// 所以要在元件上加一个对矿架为真的判断。用原版目录里的工厂造元件,再补上这三个函数。
/// </summary>
private static void WrapElectronics(ModItemDirectory dir)
{
try
{
foreach (var id in new[] { "common_electronic", "rare_electronic", "hypertech_electronic" })
{
var vanilla = DirectoryMaster.GetFactory<GameItem>(id);
if (vanilla == null) { Log.Warn($"[Rig] 找不到原版 {id}"); continue; }
var make = vanilla;
dir.Set(id, () =>
{
var it = make();
Func<GameItem, GameItem, bool> can = (src, t) => t != null && ((t.identifier == Rig && CanUpgradeWith(t, src)) || AsicItems.CanRepairWith(t, src));
it.mayThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(it.mayThisTargetItemFunc, can);
it.canThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(it.canThisTargetItemFunc, can);
it.onThisTargetItemFunc = (Action<GameItem, GameItem>)Delegate.Combine(it.onThisTargetItemFunc, (Action<GameItem, GameItem>)((src, t) =>
{
if (t != null && t.identifier == Rig && CanUpgradeWith(t, src)) Log.Guard("矿架升级槽位", () => TakeUpgrade(t, src));
else if (AsicItems.CanRepairWith(t, src)) Log.Guard("矿机修理", () => AsicItems.TakeRepair(t, src));
}));
return it;
});
Log.Info($"[Rig] 已给 {id} 加上升级矿架的作用");
}
}
catch (Exception ex) { Log.Error("[Rig] 包装电子元件失败:" + ex); }
}
/// <summary>
/// 涡轮增压器作用于矿架和 ASIC 矿机:照原版机器的规则多跑一轮,这一轮是立刻挖一晚(见 <see cref="TurboMine"/>)。
/// 原版增压器调用机器的夜间周期回调,所以在它的作用函数外包一层,作用于矿机时打上标记,回调据此改为立刻结算;判断、消耗、充电都照原版。
/// </summary>
private static void WrapTurbos(ModItemDirectory dir)
{
foreach (var id in new[] { "turbo_booster", "turbo_booster_adv", "turbo_booster_hypertech" })
{
var make = DirectoryMaster.GetFactory<GameItem>(id);
if (make == null) { Log.Warn($"[Rig] 找不到原版 {id}"); continue; }
dir.Set(id, () =>
{
var it = make();
var on = it.onThisTargetItemFunc;
it.onThisTargetItemFunc = (s, t) =>
{
if (!IsMiner(t)) { on?.Invoke(s, t); return; }
TurboMine.Boosting = true;
try { on?.Invoke(s, t); } finally { TurboMine.Boosting = false; }
};
return it;
});
}
}
private static bool IsMiner(GameItem t) => t != null && (t.identifier == Rig || AsicItems.Is(t));
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<GameItem, Tuple<GameGridInventory, GameInventory>> RigInv = new System.Runtime.CompilerServices.ConditionalWeakTable<GameItem, Tuple<GameGridInventory, GameInventory>>();
private static bool CanUpgradeWith(GameItem rig, GameItem src)
{
var need = RigSlots.Next(SlotsOf(rig));
return src != null && rig.identifier == Rig && need != null && src.identifier == need.Value.Item && GeneralHelper.IsItemOwned(src);
}
private static void TakeUpgrade(GameItem rig, GameItem item)
{
if (!RigInv.TryGetValue(rig, out var inv)) return;
var cards = inv.Item1; var modules = inv.Item2;
int slots = SlotsOf(rig);
int progress = rig.IsTag(ProgressTag) ? rig.GetTagReadonly(ProgressTag).GetInt() : 0;
int before = slots;
int took = RigSlots.Insert(ref slots, ref progress, item.identifier, item.unitCount);
int took = RigSlots.Insert(ref slots, ref progress, item.identifier, 1);
if (took <= 0) return;
rig.ModifyTag(SlotsTag, s => s.Enable().SetInt(slots));
rig.ModifyTag(ProgressTag, s => s.Enable().SetInt(progress));
// 消耗收下的件数;放不下的部分留在升级槽里(玩家取回)
if (took >= item.unitCount) { upgradeSlot.Expel(item); item.Destroy(); }
else item.GetSplitItem(took).Destroy();
if (item.unitCount <= 1) item.Destroy(); else item.GetSplitItem(1).Destroy();
if (slots != before)
{
ApplySlots(rig, cards);
@@ -215,6 +623,8 @@ namespace Decentralized
double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(b));
int slots = SlotsOf(rig);
rig.shortDescription = L.T("item.rig.desc", slots, Fmt.Num(hash * 10), kj);
RigStatus.Show(rig, Cards(cards).Count, slots, kj, hash * 10);
Payout.Refresh(rig);
var next = RigSlots.Next(slots);
if (next != null)
{
@@ -234,6 +644,8 @@ namespace Decentralized
/// <summary>夜间早段:按矿架上的显卡总耗电(含模块效果)从电池扣电;扣电成功,登记今晚参与挖矿的显卡和模块效果。</summary>
private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory modules, GameInventory cards)
{
if (TurboMine.Boosting) { Boost(rig, battery, modules, cards); return; }
if (!Session.TonightDrawn.Add(rig.uniqueId)) return;
var list = Cards(cards).Where(c => !c.IsTag(DeadTag)).ToList();
Session.TonightRigs++;
if (list.Count == 0) return;
@@ -242,7 +654,19 @@ namespace Decentralized
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return;
Session.TonightPoweredRigs++;
Session.TonightKj += kj;
foreach (var c in list) Session.TonightCards.Add(new KeyValuePair<GameItem, ModuleBonus>(c, b));
foreach (var c in list) { Session.TonightCards.Add(new KeyValuePair<GameItem, ModuleBonus>(c, b)); Session.TonightMachine[c] = rig; }
UpdateRig(rig, modules, cards);
}
/// <summary>涡轮增压:扣一晚的电,立刻挖一轮(不影响今晚照常挖矿)。</summary>
private static void Boost(GameItem rig, GameSlotInventory battery, GameInventory modules, GameInventory cards)
{
var list = Cards(cards).Where(c => !c.IsTag(DeadTag)).ToList();
if (list.Count == 0) return;
var b = Bonus(rig, modules);
int kj = RigKj(cards, b);
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) { Log.Info("涡轮增压:矿架电池电量不够,这一轮没挖"); return; }
TurboMine.Round(rig, list.Select(c => new KeyValuePair<GameItem, ModuleBonus>(c, b)).ToList());
UpdateRig(rig, modules, cards);
}
@@ -250,6 +674,38 @@ namespace Decentralized
/// 放进储藏区(游戏 EmporiumTryAdd 的默认目标,放不下时游戏自己找位置)。
/// 不放柜台:测试中柜台上的物品过夜后会消失(未在正常流程中确认);ModHelper.MoveItemTo* 放满时会静默失败。
/// </summary>
/// <summary>优先放进机器区(machine_bay*)的格子里,没有位置时退回 <see cref="Give"/>。矿架这类机器该待在机器区。</summary>
public static void GiveToMachineBay(GameItem item)
{
GeneralHelper.SetItemOwned(item, true);
var e = EmporiumEntry.Instance;
if (e?.invElement != null)
foreach (var box in e.invElement.childItems.Where(i => i.identifier != null && i.identifier.StartsWith("machine_bay")).ToList())
if (GraphUtils.CanAccept(box, item) && GraphUtils.TryAcceptAll(box, item) == 0) return;
Give(item);
}
/// <summary>放到前台(解锁送的东西第一眼能看到);前台放不下时退回储藏区。游戏离店和过夜会清空前台,<see cref="SweepFront"/> 在那之前把 mod 的物品收进储藏区。</summary>
public static void GiveToFront(GameItem item)
{
GeneralHelper.SetItemOwned(item, true);
var front = EmporiumEntry.Instance?.frontInvinvElement;
if (front != null && GraphUtils.CanAccept(front, item) && GraphUtils.TryAcceptAll(front, item) == 0) return;
Give(item);
}
/// <summary>离店、睡觉前:前台上属于 mod 的物品收进储藏区(游戏随后会清空前台,没拿走的终端、钱包会丢)。</summary>
private static void SweepFront()
{
Log.Guard("收起前台的 mod 物品", () =>
{
var e = EmporiumEntry.Instance;
if (e?.frontInvinvElement == null || e.invElement == null) return;
foreach (var it in e.frontInvinvElement.childItems.ToList())
if (it.identifier != null && it.identifier.StartsWith("pd_") && GeneralHelper.IsItemOwned(it)) GraphUtils.TryAcceptAll(e.invElement, it);
});
}
public static void Give(GameItem item)
{
GeneralHelper.SetItemOwned(item, true);
+19
View File
@@ -12,9 +12,20 @@ namespace Decentralized
{
Items.Subscribe();
Items.OpenTerminal = TerminalWindow.Open;
Items.OpenGpuHandbook = () => GpuGuide.Open();
ModHook.OnModAssetsLoaded += GpuGuide.OnAssetsLoaded;
Items.AddWallet = TerminalWindow.AddWallet;
Items.CanAddWallet = TerminalWindow.CanAddWallet;
Settings.DefineWhenReady(ModHelper.GetConfig(ModInfo.Id));
Unlock.Register();
Night.Subscribe();
DealerStock.Subscribe();
UsedDealerClient.Subscribe();
CoinCustomers.Subscribe();
OtcClient.Subscribe();
BlackDelivery.Subscribe();
TumblerStory.Subscribe();
GpuInspectView.Ensure();
ModHook.OnGameLoadedLate += Session.Load;
ModHook.OnNewGameStarted += OnNewGame;
Log.Info($"{ModInfo.Name} {ModInfo.Version} 已启用");
@@ -25,6 +36,14 @@ namespace Decentralized
Items.Unsubscribe();
Night.Unsubscribe();
DealerStock.Unsubscribe();
UsedDealerClient.Unsubscribe();
CoinCustomers.Unsubscribe();
OtcClient.Unsubscribe();
BlackDelivery.Unsubscribe();
TumblerStory.Unsubscribe();
GpuInspectView.Remove();
WalletDriver.Remove();
ModHook.OnModAssetsLoaded -= GpuGuide.OnAssetsLoaded;
ModHook.OnGameLoadedLate -= Session.Load;
ModHook.OnNewGameStarted -= OnNewGame;
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff. Show more