Compare commits
No files matched your search
@@ -1,23 +1,456 @@
|
|||||||
# Changelog
|
# 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 RZX 60 / 70 / 80 / 90,PMD RZ 70 / 80 / 800 / 900;型号只是显示名,性能由牌子和档次定;档次对应供电口(6 / 8 / 8 / 8+8 针)和风扇数(1 / 2 / 3 / 3)。显卡 3×2 格,正反面各一张图;价格约为最初的 1.8 倍,出块奖励 90。
|
- 显卡改成每个牌子四档(低端、中端、高端、旗舰):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 天上报纸)。
|
- 减半抬高价格锚(传导 0.6,减半前一周有预期行情,减半前 3 天上报纸)。
|
||||||
- 交易终端重做:首页三个入口(银行、Poinbase、Web3)加报告;左右分栏表格;买入均价;Web3 页的稳定币价格与状态;银行提取前的审查提示;金额输入框加宽。
|
- 交易终端重做:首页三个入口(银行、Poinbase、Web3)加报告;左右分栏表格;买入均价;Web3 页的稳定币价格与状态;银行提取前的审查提示;金额输入框加宽。
|
||||||
- 入口升级价 400,开局送交易终端、矿架、RZX 60 和鉴定手册。
|
- 入口升级价 400,开局送交易终端、矿架、RTX 60 和鉴定手册。
|
||||||
- 经济模拟 v7(运维库 research/sim/out_v7.md):电网电价下 RZX 70 挖矿第 30 天占总收入 18%、第 60 天 20%。
|
- 经济模拟 v7(运维库 research/sim/out_v7.md):电网电价下 RTX 70 挖矿第 30 天占总收入 18%、第 60 天 20%。
|
||||||
|
|
||||||
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
|
- 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。
|
||||||
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。
|
- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、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、烧毁)、矿架、交易终端。
|
- 物品图:art/sprites.py 生成显卡(Probidia、PMD、烧毁)、矿架、交易终端。
|
||||||
- 矿架加入游戏原有的通用模块槽:性能、效率、品质三项百分比折算 1/4 后作用于算力、耗电和显卡损耗,算力最多 +20%、省电最多 25%、损耗最多 −30%;性能加成同时提高损耗与暴毙率。取代原计划的 mod 自带超频模块。
|
- 矿架加入游戏原有的通用模块槽:性能、效率、品质三项百分比折算 1/4 后作用于算力、耗电和显卡损耗,算力最多 +20%、省电最多 25%、损耗最多 −30%;性能加成同时提高损耗与暴毙率。取代原计划的 mod 自带超频模块。
|
||||||
- 杰克逊博士上货(游戏给 mod 的进货钩子,夜间去他店里或他每周来本店摆摊时):没有矿架时必有一个,有了之后 30% 再上;基础显卡每次两张;进阶显卡第 10 天起 60%(再 30% 多一张);旗舰第 30 天起 25%,特供货事件时第 20 天起必有。全新真货,价格为物品价值加他的加价。加入 Web3 治理社区后才上货。开发者模式 F9 有测试入口。
|
- 杰克逊博士上货(游戏给 mod 的进货钩子,夜间去他店里或他每周来本店摆摊时):没有矿架时必有一个,有了之后 30% 再上;基础显卡每次两张;进阶显卡第 10 天起 60%(再 30% 多一张);旗舰第 30 天起 25%,特供货事件时第 20 天起必有。全新真货,价格为物品价值加他的加价。加入 Web3 治理社区后才上货。开发者模式 F9 有测试入口。
|
||||||
@@ -29,4 +462,4 @@
|
|||||||
- 矿架默认 2 个显卡槽,投入电子元件升级,最多 5 个:第 3 槽 4 个普通电子元件,第 4 槽 1 个稀有,第 5 槽 2 个稀有。窗口里新增"升级槽位",只收下一级需要的元件,放进去立即消耗并累计进度;显卡格子的形状随槽数变化,槽数与进度存在矿架标签上。
|
- 矿架默认 2 个显卡槽,投入电子元件升级,最多 5 个:第 3 槽 4 个普通电子元件,第 4 槽 1 个稀有,第 5 槽 2 个稀有。窗口里新增"升级槽位",只收下一级需要的元件,放进去立即消耗并累计进度;显卡格子的形状随槽数变化,槽数与进度存在矿架标签上。
|
||||||
|
|
||||||
- 矿架的"基础耗电"标签随显卡变化(机身 + 各卡耗电,不含模块),游戏装卸模块时据此重算,不会把耗电改回只有机身的 2 kJ。
|
- 矿架的"基础耗电"标签随显卡变化(机身 + 各卡耗电,不含模块),游戏装卸模块时据此重算,不会把耗电改回只有机身的 2 kJ。
|
||||||
- Web3 治理社区改为同时送一个矿架:交易终端、矿架、一张 RZX 50,送的货值 520;解锁价定为 400(见运维库定价说明)。
|
- Web3 治理社区改为同时送一个矿架:交易终端、矿架、一张 RTX 50,送的货值 520;解锁价定为 400(见运维库定价说明)。
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 项目规则
|
||||||
|
|
||||||
|
## 数值
|
||||||
|
|
||||||
|
- 数值全部放在同一个配置类里(`src/Decentralized.Domain/Tuning.cs`,可以用嵌套类按主题划分),代码、测试里不写魔数。
|
||||||
|
- 手册、说明文字里的数值不手写:用占位符 `{Name}`、`{Name:%}`、`{Name[2]:%}`(嵌套类用 `{Section.Name}`),由 `TuningText.Render` 在显示时从配置类取值。数值一改,文字跟着变。
|
||||||
|
- 新增数值:先加进 `Tuning`,再登记到运营仓库的 `docs/tuning-index.md`,说明文字里用占位符引用。
|
||||||
|
- 单元测试(`TuningText` 那一条)检查中英文手册的占位符都取得到值。
|
||||||
|
|
||||||
|
## 文案
|
||||||
|
|
||||||
|
- 界面标签用名词,提示用陈述句,不口语化;NPC 对话和报纸正文除外。界面不放说明文字,规则说明写进手册。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Probably Decentralized
|
# 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.
|
All coins, exchanges and events are fictional and have no real-world value.
|
||||||
|
|
||||||
@@ -9,14 +9,29 @@ All coins, exchanges and events are fictional and have no real-world value.
|
|||||||
| Path | Content |
|
| Path | Content |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `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.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), news. Builds into `<game>/Mods/ProbablyDecentralized` (`dotnet build src/Decentralized -p:GameDir=...`). |
|
| `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` |
|
| `tests/Decentralized.Domain.Tests` | Console test runner: `dotnet run --project tests/Decentralized.Domain.Tests` |
|
||||||
| `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`. |
|
| `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
|
## License
|
||||||
|
|
||||||
MIT, see `LICENSE`.
|
MIT, see `LICENSE`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
《Probably Stolen》的 mod:挖矿、虚构币市场、稳定币、洗钱及其风险,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。
|
《Probably Stolen》的 mod:挖矿、虚构币市场、稳定币、洗钱及其风险、Web3 黑市,接入游戏原有的电力、事件和执法系统。开发中,尚未发布。所有币种、交易所和事件均为虚构,不具有任何现实价值。
|
||||||
@@ -136,9 +136,10 @@ def fan(c, cx, cy, accent, accent_dark, R=None):
|
|||||||
c.put(sx, sy, 'g')
|
c.put(sx, sy, 'g')
|
||||||
|
|
||||||
|
|
||||||
def gpu_front(accent, accent_dark, brand_text, tier=1, burnt=False):
|
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 针)。"""
|
"""tier 决定实物特征:风扇数(1/2/3/3)与供电口(6 针 / 8 针 / 8 针 / 双 8 针)。"""
|
||||||
count = (1, 2, 3, 3)[tier]
|
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]
|
fans, R = FAN_LAYOUT[count]
|
||||||
c = Canvas(BW, BH)
|
c = Canvas(BW, BH)
|
||||||
bracket(c)
|
bracket(c)
|
||||||
@@ -180,7 +181,7 @@ def gpu_front(accent, accent_dark, brand_text, tier=1, burnt=False):
|
|||||||
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, 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')
|
c.rect(x + 5, 7, x + 6, y0 - 1, 'q'); c.rect(x, 7, x + 6, 7, 'q')
|
||||||
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针
|
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针
|
||||||
for left, pins in ((((118, 6),) if tier == 0 else ((112, 8),) if tier < 3 else ((78, 8), (126, 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
|
right = left + pins * 5 + 2
|
||||||
c.rect(left, 5, right, y0 - 1, 'n'); c.rect(left, 5, right, 6, 'z')
|
c.rect(left, 5, right, y0 - 1, 'n'); c.rect(left, 5, right, 6, 'z')
|
||||||
for i in range(pins):
|
for i in range(pins):
|
||||||
@@ -416,6 +417,9 @@ BIG = {}
|
|||||||
for _b, (_a, _ad, _t) in {'probidia': ('G', 'g2', 'PROBIDIA'), 'pmd': ('R', 'r2', 'PMD')}.items():
|
for _b, (_a, _ad, _t) in {'probidia': ('G', 'g2', 'PROBIDIA'), 'pmd': ('R', 'r2', 'PMD')}.items():
|
||||||
for _tier in range(4):
|
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))
|
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_%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_f'] = lambda: gpu_front('e', 'c', 'PROBIDIA', 1, burnt=True)
|
||||||
BIG['gpu_big_burnt_b'] = lambda: gpu_back('e', 'c', burnt=True)
|
BIG['gpu_big_burnt_b'] = lambda: gpu_back('e', 'c', burnt=True)
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ def print_overlay(model_text, style="normal"):
|
|||||||
|
|
||||||
elif style == "stamp":
|
elif style == "stamp":
|
||||||
# 1) 底层被磨掉/发暗的原型号痕迹(原先低配型号残影)
|
# 1) 底层被磨掉/发暗的原型号痕迹(原先低配型号残影)
|
||||||
orig_text = "RZX 50" if "80" in model_text else "PMD 60"
|
orig_text = "RTX 50" if "80" in model_text else "PMD 60"
|
||||||
text57(c, x_start + 4, y_start + 1, orig_text, "z", scale=scale, gap=1)
|
text57(c, x_start + 4, y_start + 1, orig_text, "z", scale=scale, gap=1)
|
||||||
text57(c, x_start + 5, y_start + 2, orig_text, "k", scale=scale, gap=1)
|
text57(c, x_start + 5, y_start + 2, orig_text, "k", scale=scale, gap=1)
|
||||||
|
|
||||||
@@ -683,15 +683,15 @@ def print_overlay(model_text, style="normal"):
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
def print_skew(model_text="RZX 80Ti"):
|
def print_skew(model_text="RTX 80Ti"):
|
||||||
return print_overlay(model_text, "skew")
|
return print_overlay(model_text, "skew")
|
||||||
|
|
||||||
|
|
||||||
def print_stamp(model_text="RZX 80Ti"):
|
def print_stamp(model_text="RTX 80Ti"):
|
||||||
return print_overlay(model_text, "stamp")
|
return print_overlay(model_text, "stamp")
|
||||||
|
|
||||||
|
|
||||||
def print_normal(model_text="RZX 80Ti"):
|
def print_normal(model_text="RTX 80Ti"):
|
||||||
return print_overlay(model_text, "normal")
|
return print_overlay(model_text, "normal")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ C = {
|
|||||||
'n': (0x1E, 0x1B, 0x25), # 近黑
|
'n': (0x1E, 0x1B, 0x25), # 近黑
|
||||||
'x': (0x4A, 0x4E, 0x58), # 石墨亮边
|
'x': (0x4A, 0x4E, 0x58), # 石墨亮边
|
||||||
'z': (0x2D, 0x2F, 0x38), # 石墨暗
|
'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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ class Canvas:
|
|||||||
for row in self.px:
|
for row in self.px:
|
||||||
line = bytearray()
|
line = bytearray()
|
||||||
for c in row:
|
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:
|
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))
|
r, g, b = (round(v * a / 255 + w * (255 - a) / 255) for v, w in zip((r, g, b), bg))
|
||||||
a = 255
|
a = 255
|
||||||
@@ -173,7 +179,7 @@ def fan(c, cx, cy, accent, burnt=False, R=7):
|
|||||||
FAN_LAYOUT_SMALL = {1: ([(26, 13)], 9), 2: ([(17, 13), (34, 13)], 7), 3: ([(14, 13), (26, 13), (38, 13)], 5)}
|
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):
|
def gpu_front(accent, accent_dark, burnt=False, tier=1, count=None, pin=None):
|
||||||
"""显卡正面(48×32,3×2 格),略微俯视:石墨色散热罩带斜切角与品牌色斜条,两个七叶风扇,
|
"""显卡正面(48×32,3×2 格),略微俯视:石墨色散热罩带斜切角与品牌色斜条,两个七叶风扇,
|
||||||
右端露出鳍片,顶沿露出铜热管和 8 针供电口,前沿是带元件的 PCB 和逐根画出的金手指。每格中心都不透明。"""
|
右端露出鳍片,顶沿露出铜热管和 8 针供电口,前沿是带元件的 PCB 和逐根画出的金手指。每格中心都不透明。"""
|
||||||
c = Canvas(W, H)
|
c = Canvas(W, H)
|
||||||
@@ -191,7 +197,8 @@ def gpu_front(accent, accent_dark, burnt=False, tier=1):
|
|||||||
for y in range(TOP0 + 2, TOP1):
|
for y in range(TOP0 + 2, TOP1):
|
||||||
c.put(x, y, 'g' if (x - (R - 4)) % 2 == 0 else 'k')
|
c.put(x, y, 'g' if (x - (R - 4)) % 2 == 0 else 'k')
|
||||||
c.rect(R - 5, TOP0 + 2, R - 5, TOP1 - 1, 'x')
|
c.rect(R - 5, TOP0 + 2, R - 5, TOP1 - 1, 'x')
|
||||||
count = (1, 2, 3, 3)[tier]
|
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]
|
fans, fr = FAN_LAYOUT_SMALL[count]
|
||||||
# 风扇之间的筋条
|
# 风扇之间的筋条
|
||||||
for a, b in zip(fans, fans[1:]):
|
for a, b in zip(fans, fans[1:]):
|
||||||
@@ -211,7 +218,7 @@ def gpu_front(accent, accent_dark, burnt=False, tier=1):
|
|||||||
for x0 in (L + 6, L + 8, L + 10, L + 12):
|
for x0 in (L + 6, L + 8, L + 10, L + 12):
|
||||||
c.put(x0, TOP0 - 1, 'p'); c.put(x0 + 1, TOP0 - 1, 'q')
|
c.put(x0, TOP0 - 1, 'p'); c.put(x0 + 1, TOP0 - 1, 'q')
|
||||||
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针(每针占两像素)
|
# 供电口:低端 6 针;中端、高端 8 针;旗舰两个 8 针(每针占两像素)
|
||||||
for left, pins in (((L + 22, 6),) if tier == 0 else ((L + 18, 8),) if tier < 3 else ((L + 8, 8), (L + 25, 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):
|
for x in range(left, left + pins * 2):
|
||||||
c.put(x, TOP0 - 1, 'n')
|
c.put(x, TOP0 - 1, 'n')
|
||||||
for x in range(left, left + pins * 2, 2):
|
for x in range(left, left + pins * 2, 2):
|
||||||
@@ -379,16 +386,16 @@ def ov_sticker(kind):
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
MODELS = ['RZX 60', 'RZX 70', 'RZX 80', 'RZX 90', 'RZ 70', 'RZ 80', 'RZ 800', 'RZ 900']
|
MODELS = ['RTX 60', 'RTX 70', 'RTX 80', 'RTX 90', 'RX 70', 'RX 80', 'RX 800', 'RX 900']
|
||||||
|
|
||||||
|
|
||||||
def rig():
|
def rig():
|
||||||
"""矿架(48×48,3×3 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
|
"""矿架(64×64,4×4 格):和游戏里其他机器一样的略微俯视(3/4 视角)——顶面是上窄下宽的梯形,
|
||||||
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
|
下面是正面,正面里四层显卡位,左下角电源;光从左上来,左亮右暗;轮廓统一深紫、削掉尖角、底下两只脚和一圈阴影。"""
|
||||||
c = Canvas(48, 48)
|
c = Canvas(64, 64)
|
||||||
FL, FR, FY, DEP = 3, 44, 24, 19 # 正面左右边、顶面前沿所在行、顶面深度(顶面后沿在 y=6,中心行 y=8 落在顶面上)
|
FL, FR, FY, DEP = 3, 60, 36, 31 # 正面左右边、顶面前沿所在行、顶面深度(顶面后沿在 y=6,中心行 y=8 落在顶面上)
|
||||||
BOT = 44 # 正面底边
|
BOT = 60 # 正面底边
|
||||||
inset = lambda v: v // 6 # 顶面后部只略微收窄:原版机器的顶面也近似矩形,并且要让左右两格的中心落在顶面内
|
inset = lambda v: v // 8 # 顶面后部只略微收窄:原版机器的顶面也近似矩形,并且要让左右两格的中心落在顶面内
|
||||||
|
|
||||||
# 顶面:后部受光、前部稍暗;左缘亮、右缘暗
|
# 顶面:后部受光、前部稍暗;左缘亮、右缘暗
|
||||||
for v in range(DEP):
|
for v in range(DEP):
|
||||||
@@ -401,8 +408,8 @@ def rig():
|
|||||||
for x in range(FL + inset(DEP - 1), FR - inset(DEP - 1) + 1):
|
for x in range(FL + inset(DEP - 1), FR - inset(DEP - 1) + 1):
|
||||||
c.put(x, FY - DEP + 1, 'l')
|
c.put(x, FY - DEP + 1, 'l')
|
||||||
# 顶面上的散热格栅和一个小指示灯
|
# 顶面上的散热格栅和一个小指示灯
|
||||||
for v in range(4, 16, 3):
|
for v in range(4, 26, 3):
|
||||||
for u in range(6, 24):
|
for u in range(6, 34):
|
||||||
c.put(FL + inset(v) + u, FY - v, 'g')
|
c.put(FL + inset(v) + u, FY - v, 'g')
|
||||||
c.put(FR - 9, FY - 8, 'G'); c.put(FR - 8, FY - 8, 'G')
|
c.put(FR - 9, FY - 8, 'G'); c.put(FR - 8, FY - 8, 'G')
|
||||||
# 正面:中间色,左端受光、右端暗;顶部一条暗折线
|
# 正面:中间色,左端受光、右端暗;顶部一条暗折线
|
||||||
@@ -422,7 +429,7 @@ def rig():
|
|||||||
c.put(x, BOT - 1, 'd')
|
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 x in range(FL + 3, FR - 4):
|
||||||
for dy in range(3):
|
for dy in range(3):
|
||||||
c.put(x, y + dy, 'k')
|
c.put(x, y + dy, 'k')
|
||||||
@@ -604,26 +611,201 @@ def guide():
|
|||||||
return 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 = {
|
SPRITES = {
|
||||||
'gpu_guide': guide,
|
|
||||||
'gpu_burnt_f': lambda: gpu_front('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_burnt_b': lambda: gpu_back('e', 'c', burnt=True),
|
||||||
'gpu_probidia_b': lambda: gpu_back('G', 'g2'),
|
'gpu_probidia_b': lambda: gpu_back('G', 'g2'),
|
||||||
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
|
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
|
||||||
'rig': rig,
|
'rig': rig,
|
||||||
'terminal': terminal,
|
'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):
|
for _tier in range(4):
|
||||||
SPRITES['gpu_probidia_f_t%d' % _tier] = (lambda t=_tier: gpu_front('G', 'g2', tier=t))
|
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))
|
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_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) 对齐,只画变化的像素,不参与判格
|
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
|
||||||
OVERLAYS = {
|
OVERLAYS = {
|
||||||
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
|
'gpu_ov_fin1': lambda: ov_fingers(1), 'gpu_ov_fin2': lambda: ov_fingers(2),
|
||||||
'gpu_ov_brk1': lambda: ov_bracket(1),
|
'gpu_ov_brk1': lambda: ov_bracket(1),
|
||||||
'gpu_ov_stk2': lambda: ov_sticker(2),
|
'gpu_ov_stk2': lambda: ov_sticker(2),
|
||||||
|
'otc_lamp_on': otc_lamp_on,
|
||||||
}
|
}
|
||||||
for _n in (1, 2, 3):
|
for _n in (1, 2, 3):
|
||||||
OVERLAYS['gpu_ov_fan1_%d' % _n] = (lambda n=_n: ov_fan(1, n))
|
OVERLAYS['gpu_ov_fan1_%d' % _n] = (lambda n=_n: ov_fan(1, n))
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ namespace Decentralized.Domain
|
|||||||
public enum Venue { Exchange, Dex, Darknet }
|
public enum Venue { Exchange, Dex, Darknet }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 一个交易对 = 一个恒定乘积池(x·y = k)。Base 是被交易的币,Quote 是计价币;价格 = Quote 储备 / Base 储备。
|
/// 一个交易对。DEX 是恒定乘积池(x·y = k):Base 是被交易的币,Quote 是计价币,价格 = Quote 储备 / Base 储备。
|
||||||
/// 每个交易对各有深度;夜间由 NPC 套利者把价格按比例拉向目标价。
|
/// Poinbase 主池是订单簿(<see cref="Book"/>):价格只取决于当天的累计净买入量,冲击随单子变大而增长得比线性慢
|
||||||
|
/// (价格 = 参考价 × exp(κ · sign(x) · |x|^α),x = 净买入的名义价值 / 深度),拆单不占便宜,同量买进再卖出回到原价。
|
||||||
|
/// 稳定币对(<see cref="Stable"/>)是 StableSwap 池(Curve 的曲线,放大系数 A):挂钩附近几乎没有滑点,偏离越大越接近恒定乘积;
|
||||||
|
/// 储备按"价值单位"记(Base 数量 × 挂钩价),挂钩价夜里随参考价重设。
|
||||||
|
/// 每个交易对各有深度;夜间由 NPC 套利者把价格按比例拉向目标价,订单簿的累计量清零。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Pool
|
public sealed class Pool
|
||||||
{
|
{
|
||||||
@@ -14,10 +18,87 @@ namespace Decentralized.Domain
|
|||||||
public Venue Venue;
|
public Venue Venue;
|
||||||
public double BaseReserve, QuoteReserve;
|
public double BaseReserve, QuoteReserve;
|
||||||
public double Fee;
|
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 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 < 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) =>
|
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 };
|
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>
|
/// <summary>预估:卖出 baseIn 个 Base 能得到多少 Quote(不改池子)。</summary>
|
||||||
public double QuoteOut(double baseIn)
|
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);
|
double x = BaseReserve + baseIn * (1 - Fee);
|
||||||
return QuoteReserve - BaseReserve * QuoteReserve / x;
|
return QuoteReserve - BaseReserve * QuoteReserve / x;
|
||||||
}
|
}
|
||||||
@@ -32,15 +115,33 @@ namespace Decentralized.Domain
|
|||||||
/// <summary>预估:花 quoteIn 个 Quote 能买到多少 Base(不改池子)。</summary>
|
/// <summary>预估:花 quoteIn 个 Quote 能买到多少 Base(不改池子)。</summary>
|
||||||
public double BaseOut(double quoteIn)
|
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);
|
double y = QuoteReserve + quoteIn * (1 - Fee);
|
||||||
return BaseReserve - BaseReserve * QuoteReserve / y;
|
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>
|
/// <summary>卖出 Base,返回得到的 Quote。手续费留在池里。</summary>
|
||||||
public double SellBase(double baseIn)
|
public double SellBase(double baseIn)
|
||||||
{
|
{
|
||||||
if (baseIn <= 0) return 0;
|
if (baseIn <= 0) return 0;
|
||||||
double out_ = QuoteOut(baseIn);
|
double out_ = QuoteOut(baseIn);
|
||||||
|
if (Book) { Net -= baseIn; return out_; }
|
||||||
|
if (Stable) { BaseReserve += baseIn * Peg; QuoteReserve -= out_; return out_; }
|
||||||
BaseReserve += baseIn;
|
BaseReserve += baseIn;
|
||||||
QuoteReserve -= out_;
|
QuoteReserve -= out_;
|
||||||
return out_;
|
return out_;
|
||||||
@@ -51,6 +152,8 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
if (quoteIn <= 0) return 0;
|
if (quoteIn <= 0) return 0;
|
||||||
double out_ = BaseOut(quoteIn);
|
double out_ = BaseOut(quoteIn);
|
||||||
|
if (Book) { Net += out_; return out_; }
|
||||||
|
if (Stable) { QuoteReserve += quoteIn; BaseReserve -= out_ * Peg; return out_; }
|
||||||
QuoteReserve += quoteIn;
|
QuoteReserve += quoteIn;
|
||||||
BaseReserve -= out_;
|
BaseReserve -= out_;
|
||||||
return out_;
|
return out_;
|
||||||
@@ -60,6 +163,13 @@ namespace Decentralized.Domain
|
|||||||
public double ImpactOfBuy(double quoteIn)
|
public double ImpactOfBuy(double quoteIn)
|
||||||
{
|
{
|
||||||
double before = Price;
|
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 y = QuoteReserve + quoteIn * (1 - Fee);
|
||||||
double x = BaseReserve * QuoteReserve / y;
|
double x = BaseReserve * QuoteReserve / y;
|
||||||
return (y / x) / before - 1;
|
return (y / x) / before - 1;
|
||||||
@@ -77,6 +187,8 @@ namespace Decentralized.Domain
|
|||||||
|
|
||||||
public void SetPrice(double price, double quoteDepth)
|
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;
|
QuoteReserve = quoteDepth;
|
||||||
BaseReserve = quoteDepth / price;
|
BaseReserve = quoteDepth / price;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,14 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
public string Model;
|
public string Model;
|
||||||
public int Fan, Fingers, Bracket, Print, Sticker, Serial;
|
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 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.RowBuild ? Tier : row == CardLabels.RowFan ? Fan : row == CardLabels.RowFingers ? Fingers
|
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;
|
: row == CardLabels.RowBracket ? Bracket : row == CardLabels.RowPrint ? Print : row == CardLabels.RowSerial ? Serial : Sticker;
|
||||||
|
|
||||||
public Claim With(int row, int value)
|
public Claim With(int row, int value)
|
||||||
@@ -20,24 +24,26 @@ namespace Decentralized.Domain
|
|||||||
if (row == CardLabels.RowFan) c.Fan = value; else if (row == CardLabels.RowFingers) c.Fingers = value;
|
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.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.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;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Encode() => $"{Model}:{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}";
|
public string Encode() => $"{Model}:{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}{CountSet}{PinSet}";
|
||||||
|
|
||||||
public static Claim Decode(string s)
|
public static Claim Decode(string s)
|
||||||
{
|
{
|
||||||
var p = (s ?? "").Split(':');
|
var p = (s ?? "").Split(':');
|
||||||
if (p.Length != 2 || p[1].Length != 6 || !Hardware.Specs.ContainsKey(p[0])) return default;
|
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 D(int i) => Math.Max(0, Math.Min(2, p[1][i] - '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) };
|
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>
|
/// <summary>按真实情况写出的说法(没有隐瞒)。</summary>
|
||||||
public static Claim Truth(Unit u)
|
public static Claim Truth(Unit u)
|
||||||
{
|
{
|
||||||
var l = u.Appearance;
|
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 };
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +55,12 @@ namespace Decentralized.Domain
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CardLabels
|
public static class CardLabels
|
||||||
{
|
{
|
||||||
public const int RowSpec = 1, RowBuild = 2, RowFan = 4, RowFingers = 8, RowBracket = 16, RowPrint = 32, RowSerial = 64, RowSeal = 128, AllRows = 255;
|
public const int RowSpec = 1, RowCount = 2, RowFan = 4, RowFingers = 8, RowBracket = 16, RowPrint = 32, RowSerial = 64, RowSeal = 128, RowPin = 256, AllRows = 511;
|
||||||
/// <summary>标签栏的顺序:第一行鉴定真伪(型号;依据是供电口与风扇数、序列号、背面丝印,玩家对着大图自己看),后四行鉴定成色。</summary>
|
/// <summary>标签栏的顺序:风扇数、供电口、背面丝印、序列号(真伪依据),型号,叶片、金手指、挡板、封条(成色)。</summary>
|
||||||
public static readonly int[] Rows = { RowSpec, RowFan, RowFingers, RowBracket, RowSeal };
|
public static readonly int[] Rows = { RowSpec, RowCount, RowPin, RowPrint, RowSerial, RowFan, RowFingers, RowBracket, RowSeal };
|
||||||
public const int AuthRows = 1;
|
/// <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>各标签各取值对价格的影响(百分比,相加):风扇叶片积灰 −4%、缺角 −12%;金手指发暗 −4%、氧化 −12%;挡板划痕 −2%;封条缺失 −6%。
|
||||||
/// 型号、供电与风扇、丝印、序列号本身不扣价,它们决定的是这块卡是什么芯片。</summary>
|
/// 型号、供电与风扇、丝印、序列号本身不扣价,它们决定的是这块卡是什么芯片。</summary>
|
||||||
@@ -60,10 +68,10 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
switch (row)
|
switch (row)
|
||||||
{
|
{
|
||||||
case RowFan: return value == 1 ? -4 : value == 2 ? -12 : 0;
|
case RowFan: return value == 1 ? -Tuning.CardFanDust * 100 : value == 2 ? -Tuning.CardFanChipped * 100 : 0;
|
||||||
case RowFingers: return value == 1 ? -4 : value == 2 ? -12 : 0;
|
case RowFingers: return value == 1 ? -Tuning.CardFingersDark * 100 : value == 2 ? -Tuning.CardFingersOxidized * 100 : 0;
|
||||||
case RowBracket: return value == 1 ? -2 : 0;
|
case RowBracket: return value == 1 ? -Tuning.CardBracketScratch * 100 : 0;
|
||||||
case RowSeal: return value == 2 ? -6 : 0;
|
case RowSeal: return value == 2 ? -Tuning.CardSealMissing * 100 : 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -73,7 +81,8 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
switch (row)
|
switch (row)
|
||||||
{
|
{
|
||||||
case RowBuild: return claim.Tier != real.Tier;
|
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 RowPrint: return real.Print != 0 && claim.Print != real.Print;
|
||||||
case RowSerial: return real.Serial != 0 && claim.Serial != real.Serial;
|
case RowSerial: return real.Serial != 0 && claim.Serial != real.Serial;
|
||||||
}
|
}
|
||||||
@@ -108,7 +117,7 @@ namespace Decentralized.Domain
|
|||||||
if (row == RowSpec) return claim.Model != real.Model ? 2 : 0;
|
if (row == RowSpec) return claim.Model != real.Model ? 2 : 0;
|
||||||
if (IsEvidence(row, claim, real))
|
if (IsEvidence(row, claim, real))
|
||||||
{
|
{
|
||||||
exposed |= RowSpec | RowBuild;
|
exposed |= RowSpec | RowCount | RowPin | RowPrint | RowSerial;
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (wasExposed) return 0;
|
if (wasExposed) return 0;
|
||||||
@@ -124,6 +133,7 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
if ((exposed & r) != 0) continue;
|
if ((exposed & r) != 0) continue;
|
||||||
if (r == RowSpec) { if (claim.Model != real.Model) lies |= r; }
|
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;
|
else if (claim.Get(r) != real.Get(r) && Pct(r, claim.Get(r)) > Pct(r, real.Get(r))) lies |= r;
|
||||||
}
|
}
|
||||||
return lies;
|
return lies;
|
||||||
@@ -135,7 +145,8 @@ namespace Decentralized.Domain
|
|||||||
var c = Claim.Truth(u);
|
var c = Claim.Truth(u);
|
||||||
if (kind == DealerKind.Honest) return c;
|
if (kind == DealerKind.Honest) return c;
|
||||||
c.Model = u.Model;
|
c.Model = u.Model;
|
||||||
double hide = kind == DealerKind.Scammer ? 1.0 : 0.7;
|
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 })
|
foreach (int r in new[] { RowFan, RowFingers, RowBracket, RowPrint, RowSerial, RowSeal })
|
||||||
if (rng.Chance(hide)) c = c.With(r, 0);
|
if (rng.Chance(hide)) c = c.With(r, 0);
|
||||||
return c;
|
return c;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ namespace Decentralized.Domain
|
|||||||
/// 2 档次码与标称型号不符(贴的是原芯片的序列号)。后两种只出现在假卡上。</summary>
|
/// 2 档次码与标称型号不符(贴的是原芯片的序列号)。后两种只出现在假卡上。</summary>
|
||||||
public int Serial;
|
public int Serial;
|
||||||
|
|
||||||
|
/// <summary>风扇数对不上标称型号(实物是真实芯片的风扇数):0 对得上,1 对不上。只出现在假卡上。</summary>
|
||||||
|
public int CountBad;
|
||||||
|
/// <summary>供电口对不上标称型号(实物是真实芯片的供电口):0 对得上,1 对不上。只出现在假卡上。</summary>
|
||||||
|
public int PinBad;
|
||||||
|
|
||||||
public const int Variants = 3;
|
public const int Variants = 3;
|
||||||
|
|
||||||
/// <summary>每张卡自己的随机种子:老化类维度(风扇、金手指、挡板划痕)由它和当前健康度决定,每晚损耗后重新算,
|
/// <summary>每张卡自己的随机种子:老化类维度(风扇、金手指、挡板划痕)由它和当前健康度决定,每晚损耗后重新算,
|
||||||
@@ -56,13 +61,27 @@ namespace Decentralized.Domain
|
|||||||
public double HashFactor => FanHash[Fan] * FingerHash[Fingers];
|
public double HashFactor => FanHash[Fan] * FingerHash[Fingers];
|
||||||
|
|
||||||
/// <summary>六位数字,存在物品标签上:风扇、金手指、挡板、丝印、封条、序列号。</summary>
|
/// <summary>六位数字,存在物品标签上:风扇、金手指、挡板、丝印、封条、序列号。</summary>
|
||||||
public string Encode() => $"{Fan}{Fingers}{Bracket}{Print}{Sticker}{Serial}";
|
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)
|
public static CardLook Decode(string s)
|
||||||
{
|
{
|
||||||
if (s == null || (s.Length != 5 && s.Length != 6)) return default;
|
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 D(int i) => Math.Max(0, Math.Min(Variants - 1, s[i] - '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 };
|
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)
|
// 外观维度对应卡的真实状态:损耗、暴毙概率随之变化,价值走标签(见 CardLabels)
|
||||||
@@ -76,20 +95,28 @@ namespace Decentralized.Domain
|
|||||||
public double DeathFactor => FanDeath[Fan] * FingerDeath[Fingers];
|
public double DeathFactor => FanDeath[Fan] * FingerDeath[Fingers];
|
||||||
|
|
||||||
/// <summary>只在假卡上出现的取值:玩家一旦看到,就能肯定是假的。</summary>
|
/// <summary>只在假卡上出现的取值:玩家一旦看到,就能肯定是假的。</summary>
|
||||||
public bool HasFakeSign => Print != 0 || Serial != 0;
|
public bool HasFakeSign => Print != 0 || Serial != 0 || CountBad != 0 || PinBad != 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成外观。真卡:老化维度按健康度出现,封条缺失在二手卡上很常见,不会出现破绽类取值。
|
/// 生成外观。真卡:老化维度按健康度出现,封条缺失在二手卡上很常见,不会出现破绽类取值。
|
||||||
/// 假卡:在三类破绽(接口数、丝印、手写序列号)里随机挑 1–3 处,再叠加老化与封条缺失。
|
/// 假卡:在三类破绽(接口数、丝印、手写序列号)里随机挑 1–3 处,再叠加老化与封条缺失。
|
||||||
/// </summary>
|
/// </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)
|
||||||
{
|
{
|
||||||
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 };
|
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)
|
if (fake)
|
||||||
{
|
{
|
||||||
// 假卡本身就带着实物上的破绽(低档次的板子:供电口和风扇数对不上),此外骗子还可能重印丝印、补贴序列号
|
// 四项真伪依据(风扇数、供电口、背面丝印、序列号)各自独立地露出破绽,至少一项;平均每张假卡约 1.5 项
|
||||||
if (rng.Chance(0.55)) look.Print = rng.Chance(0.6) ? 1 : 2;
|
bool countCan = Hardware.FanCount[chipTier] != Hardware.FanCount[labelTier], pinCan = Hardware.PinClass[chipTier] != Hardware.PinClass[labelTier];
|
||||||
if (rng.Chance(0.65)) look.Serial = rng.Chance(0.55) ? 2 : 1;
|
double p = Tuning.FakeSignChance;
|
||||||
|
for (int tries = 0; tries < 30 && !look.HasFakeSign; tries++)
|
||||||
|
{
|
||||||
|
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.Appearance(health);
|
return look.Appearance(health);
|
||||||
}
|
}
|
||||||
@@ -105,7 +132,7 @@ namespace Decentralized.Domain
|
|||||||
public static long Of(string model, double health)
|
public static long Of(string model, double health)
|
||||||
{
|
{
|
||||||
var spec = Hardware.Specs[model];
|
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));
|
return Math.Max(1, (long)Math.Round(spec.Price * f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,12 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
public const string Rig = "RIG";
|
public const string Rig = "RIG";
|
||||||
|
|
||||||
public static readonly string[] Basic = { "RZX60", "RZ70" };
|
public static readonly string[] Basic = { "RTX60", "RX70" };
|
||||||
public static readonly string[] Advanced = { "RZX70", "RZ80", "RZX80", "RZ800" };
|
public static readonly string[] Advanced = { "RTX70", "RX80", "RTX80", "RX800" };
|
||||||
public static readonly string[] Flagship = { "RZX90", "RZ900" };
|
public static readonly string[] Flagship = { "RTX90", "RX900" };
|
||||||
|
|
||||||
public const int AdvancedFromDay = 10, FlagshipFromDay = 30, SpecialFlagshipFromDay = 20;
|
public const int AdvancedFromDay = Tuning.DealerAdvancedFromDay, FlagshipFromDay = Tuning.DealerFlagshipFromDay, SpecialFlagshipFromDay = Tuning.DealerSpecialFlagshipFromDay;
|
||||||
public const double RigRestockChance = 0.30, AdvancedChance = 0.60, AdvancedSecondChance = 0.30, FlagshipChance = 0.25;
|
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)
|
public static List<string> Stock(int day, bool ownsRig, bool specialStock, Rng rng)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,73 +1,49 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Decentralized.Domain
|
namespace Decentralized.Domain
|
||||||
{
|
{
|
||||||
public enum AmlOutcome { Clear, Light, Heavy }
|
/// <summary>一次出金到银行的结果。</summary>
|
||||||
|
|
||||||
/// <summary>一次合规出金的结果。罪证由接入层记进游戏的罪证账本(销赃)。</summary>
|
|
||||||
public sealed class CashoutResult
|
public sealed class CashoutResult
|
||||||
{
|
{
|
||||||
public double Requested;
|
public double Requested;
|
||||||
public double Received;
|
public double Received;
|
||||||
public double Confiscated;
|
|
||||||
public double Crime;
|
|
||||||
public double Tax;
|
public double Tax;
|
||||||
public double HitChance;
|
|
||||||
public AmlOutcome Outcome;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合规交易所出金:CRC 兑回信用点,过 AML。审查按单笔金额、7 天累计金额、污点比例、是否来自混币器或被标记地址判定。
|
/// 合规交易所 Poinbase:只收干净的币(钱包往充值地址最多转“价值 − 污点”,见 <see cref="Wallet.TransferClean"/>),账户没有污点。
|
||||||
/// 轻度命中(污点 < 30%)没收污点部分、不记罪证;重度命中没收污点部分,并把污点额记为销赃罪证。
|
/// 出金到银行把 CRC 兑回信用点,扣手续费,只对超出成本的部分计税,不做审查。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Exchange
|
public sealed class Exchange
|
||||||
{
|
{
|
||||||
/// <summary>最近的出金记录:(天, 金额)。</summary>
|
/// <summary>Poinbase 上架的币:只有这些能充值、能在现货交易。黑市的 CRT、革命军的 CRA 合规交易所不收。</summary>
|
||||||
public List<KeyValuePair<int, double>> History = new List<KeyValuePair<int, double>>();
|
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 t = account.LegacyTaint;
|
||||||
double ratio = taint / amount;
|
account.LegacyTaint = 0;
|
||||||
double big = Tuning.AmlBigRents * rent, cum = Tuning.AmlCumRents * rent;
|
foreach (var p in positions) { t += p.LegacyTaint; p.LegacyTaint = 0; }
|
||||||
double p = 0;
|
return t;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从地址里取出 amount 个 CRC 兑成信用点(按 CRC 当前价格,平时为 1;脱锚期间按市价),扣手续费。污点与成本按比例随取出部分计算;
|
/// 从账户里取出 amount 个 CRC 兑成信用点(按 CRC 当前价格,平时为 1;脱锚期间按市价),扣手续费。成本按取出部分的价值比例带走,
|
||||||
/// 只对超出成本的部分计税。
|
/// 只对超出成本的部分计税。
|
||||||
/// </summary>
|
/// </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 };
|
var r = new CashoutResult { Requested = amount };
|
||||||
if (amount <= 0) return r;
|
if (amount <= 0) return r;
|
||||||
double total = wallet.Value(price);
|
double total = account.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 value = amount * price(Assets.CRC);
|
double value = amount * price(Assets.CRC);
|
||||||
r.HitChance = HitChance(value, taint, recent, rent, fromMixer || wallet.Flagged);
|
double share = total > 0 ? Math.Min(1, value / total) : 0;
|
||||||
wallet.Add(Assets.CRC, -amount);
|
double basis = account.Basis * share;
|
||||||
wallet.Taint -= taint;
|
account.Add(Assets.CRC, -amount);
|
||||||
wallet.Basis -= basis;
|
account.Basis -= basis;
|
||||||
History.Add(new KeyValuePair<int, double>(day, value));
|
|
||||||
double got = value * (1 - Tuning.ExchangeFee);
|
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.Tax = Math.Max(0, got - basis) * Tuning.TaxRate;
|
||||||
r.Received = got;
|
r.Received = got;
|
||||||
return r;
|
return r;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,14 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace Decentralized.Domain
|
namespace Decentralized.Domain
|
||||||
{
|
{
|
||||||
|
/// <summary>一次互换的报价:得到数量、经过几个池、相对中间价的损失(手续费加滑点)。</summary>
|
||||||
|
public sealed class SwapQuote
|
||||||
|
{
|
||||||
|
public string From, To;
|
||||||
|
public double In, Out, Impact;
|
||||||
|
public int Hops;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 影响行情的游戏事件种类。接入层把游戏的新闻事件与派系行动映射到这里(例如全站停电、电力短缺 → Blackout)。
|
/// 影响行情的游戏事件种类。接入层把游戏的新闻事件与派系行动映射到这里(例如全站停电、电力短缺 → Blackout)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -21,39 +29,133 @@ namespace Decentralized.Domain
|
|||||||
public List<Pool> Pools = new List<Pool>();
|
public List<Pool> Pools = new List<Pool>();
|
||||||
/// <summary>开门时的参考价,夜间用它和收盘价算当日均价(合约的标记价)。</summary>
|
/// <summary>开门时的参考价,夜间用它和收盘价算当日均价(合约的标记价)。</summary>
|
||||||
public double OpenPrice = Tuning.PscStartPrice;
|
public double OpenPrice = Tuning.PscStartPrice;
|
||||||
|
/// <summary>玩家自己的现货大单把价格推高(正)或压低(负)的对数幅度,每晚随价格回归一起衰减。操纵检出和预测盘用它估"没有玩家时的价格"。</summary>
|
||||||
|
public double PlayerPush;
|
||||||
|
/// <summary>今天的插针(对数幅度,0 = 没有)和当晚回去的比例(0 = 真跌真涨)。</summary>
|
||||||
|
public double Wick, WickBack;
|
||||||
|
|
||||||
public const double ArbRate = 0.5; // 非主交易对每晚被拉回参考价的比例
|
/// <summary>夜间结算开始时:插针按比例回去(结算、预测盘读的是回去后的价格)。</summary>
|
||||||
public const double SideDepthShare = 0.3; // 非主交易对的深度占主交易对的比例
|
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)
|
public static Market CreateDefault(int day)
|
||||||
{
|
{
|
||||||
var m = new Market();
|
var m = new Market();
|
||||||
double depth = Tuning.PoolDepth(day), p = Tuning.PscStartPrice;
|
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.CreateBook(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.CRC, 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.CreateStable(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.StableFee, Tuning.StableAmp));
|
||||||
m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee));
|
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;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pool Main => Pools[0];
|
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) =>
|
public Pool Find(Venue venue, string baseAsset, string quote) =>
|
||||||
Pools.FirstOrDefault(x => x.Venue == venue && x.Base == baseAsset && x.Quote == quote);
|
Pools.FirstOrDefault(x => x.Venue == venue && x.Base == baseAsset && x.Quote == quote);
|
||||||
|
|
||||||
/// <summary>PSC 的参考价(信用点)。</summary>
|
/// <summary>PSC 的参考价(信用点)。</summary>
|
||||||
public double Price => Main.Price;
|
public double Price => Main.Price;
|
||||||
|
|
||||||
/// <summary>供给因子的目标:奖励比起始奖励每少一半,锚上移 2^传导指数;减半前 7 天内再逐日加预期。</summary>
|
/// <summary>历次减半抽到的传导指数之和:价格锚的供给因子目标为 2^此值。</summary>
|
||||||
public static double SupplyTarget(int day, double reward)
|
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(Tuning.BlockReward / Math.Max(1e-9, reward), Tuning.HalvingPassThrough);
|
double t = Math.Pow(2, HalvingLog);
|
||||||
int toHalving = Tuning.HalvingDays - day % Tuning.HalvingDays; // 减半当晚 day % HalvingDays == 0,那天之后又是一个完整周期
|
int toHalving = Tuning.HalvingDays - day % Tuning.HalvingDays; // 减半当晚 day % HalvingDays == 0,那天之后又是一个完整周期
|
||||||
if (toHalving <= Tuning.HalvingRampDays) t *= 1 + Tuning.HalvingRamp * (Tuning.HalvingRampDays - toHalving + 1) / Tuning.HalvingRampDays;
|
if (toHalving <= Tuning.HalvingRampDays) t *= 1 + Tuning.HalvingRamp * (Tuning.HalvingRampDays - toHalving + 1) / Tuning.HalvingRampDays;
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSupply(int day, double reward) => Supply += (SupplyTarget(day, reward) - Supply) * Tuning.SupplyAdjust;
|
public void UpdateSupply(int day) => Supply += (SupplyTarget(day) - Supply) * Tuning.SupplyAdjust;
|
||||||
|
|
||||||
public double Anchor(ICollection<MarketEvent> active)
|
public double Anchor(ICollection<MarketEvent> active)
|
||||||
{
|
{
|
||||||
@@ -79,11 +181,11 @@ namespace Decentralized.Domain
|
|||||||
OpenPrice = p;
|
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)
|
public double Whale(Rng rng)
|
||||||
{
|
{
|
||||||
if (!rng.Chance(2.0 / 7)) return 0;
|
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; }
|
if (rng.Chance(0.5)) { Main.BuyBase(size); return size; }
|
||||||
Main.SellBase(size / Price);
|
Main.SellBase(size / Price);
|
||||||
return -size;
|
return -size;
|
||||||
@@ -98,6 +200,8 @@ namespace Decentralized.Domain
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Night(ICollection<MarketEvent> active, Rng rng, int day, Func<string, double> stablePrice)
|
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);
|
Sentiment *= Math.Exp(Tuning.SentimentKappa * -Math.Log(Sentiment) + rng.Gauss() * Tuning.SentimentVol);
|
||||||
double anchor = Anchor(active);
|
double anchor = Anchor(active);
|
||||||
double lp = Math.Log(Price);
|
double lp = Math.Log(Price);
|
||||||
@@ -110,7 +214,15 @@ namespace Decentralized.Domain
|
|||||||
double baseCredits = pool.Base == Assets.PSC ? p : stablePrice(pool.Base);
|
double baseCredits = pool.Base == Assets.PSC ? p : stablePrice(pool.Base);
|
||||||
double quoteCredits = pool.Quote == Assets.PSC ? p : stablePrice(pool.Quote);
|
double quoteCredits = pool.Quote == Assets.PSC ? p : stablePrice(pool.Quote);
|
||||||
if (quoteCredits <= 0) continue;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace Decentralized.Domain
|
|||||||
/// <summary>暴毙概率的品牌系数。</summary>
|
/// <summary>暴毙概率的品牌系数。</summary>
|
||||||
/// <summary>档次:0 低端、1 中端、2 高端、3 旗舰(显卡)。实物特征(供电口、风扇数)只看档次。</summary>
|
/// <summary>档次:0 低端、1 中端、2 高端、3 旗舰(显卡)。实物特征(供电口、风扇数)只看档次。</summary>
|
||||||
public int Tier;
|
public int Tier;
|
||||||
|
/// <summary>废片:只会作为假卡背后的真实芯片出现,不上架、不单独出现。低档次的卡也能被冒充靠它。</summary>
|
||||||
|
public bool Scrap;
|
||||||
public double DeathFactor;
|
public double DeathFactor;
|
||||||
/// <summary>每天损耗的系数。</summary>
|
/// <summary>每天损耗的系数。</summary>
|
||||||
public double WearFactor;
|
public double WearFactor;
|
||||||
@@ -30,16 +32,19 @@ namespace Decentralized.Domain
|
|||||||
/// <summary>各档次的实物特征:供电口针数(旗舰是两个 8 针)与风扇数量。</summary>
|
/// <summary>各档次的实物特征:供电口针数(旗舰是两个 8 针)与风扇数量。</summary>
|
||||||
public static readonly string[] PowerPins = { "6", "8", "8", "8+8" };
|
public static readonly string[] PowerPins = { "6", "8", "8", "8+8" };
|
||||||
public static readonly int[] FanCount = { 1, 2, 3, 3 };
|
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 const string Probidia = "Probidia", Pmd = "PMD", RatDigg = "RatDigg";
|
||||||
|
|
||||||
public static readonly IReadOnlyDictionary<string, HardwareSpec> Specs = new[]
|
public static readonly IReadOnlyDictionary<string, HardwareSpec> Specs = new[]
|
||||||
{
|
{
|
||||||
// 四个档次(低端、中端、高端、旗舰),两个牌子通用:型号只是显示名,性能和价格由「牌子 + 档次」决定
|
// 四个档次(低端、中端、高端、旗舰),两个牌子通用:型号只是显示名,性能和价格由「牌子 + 档次」决定
|
||||||
Gpu("RZX60", Probidia, 0, 1.0, 4, 220), Gpu("RZX70", Probidia, 1, 5.0, 12, 800),
|
Gpu("RTX60", Probidia, 0, 1.8, 4, 220), Gpu("RTX70", Probidia, 1, 5.0, 12, 800),
|
||||||
Gpu("RZX80", Probidia, 2, 8.0, 18, 1300), Gpu("RZX90", Probidia, 3, 20.0, 30, 5000),
|
Gpu("RTX80", Probidia, 2, 8.0, 18, 1300), Gpu("RTX90", Probidia, 3, 20.0, 30, 5000),
|
||||||
Gpu("RZ70", Pmd, 0, 0.8, 2.5, 170), Gpu("RZ80", Pmd, 1, 4.0, 8, 680),
|
Gpu("RX70", Pmd, 0, 1.4, 2.5, 170), Gpu("RX80", Pmd, 1, 4.0, 8, 680),
|
||||||
Gpu("RZ800", Pmd, 2, 8.0, 12, 1500), Gpu("RZ900", Pmd, 3, 15.0, 20, 4000),
|
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),
|
Asic("M1", 50, 60, 11000), Asic("M2", 120, 110, 27000), Asic("M3", 300, 220, 72000),
|
||||||
}.ToDictionary(s => s.Model);
|
}.ToDictionary(s => s.Model);
|
||||||
|
|
||||||
@@ -49,6 +54,13 @@ namespace Decentralized.Domain
|
|||||||
DeathFactor = brand == Probidia ? 0.7 : 1.0, WearFactor = brand == Probidia ? 1.0 : 1.2,
|
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
|
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,
|
Model = model, Brand = RatDigg, Hash = h, Kj = kj, Price = price, DeathFactor = 0.5, WearFactor = 0.8, Asic = true,
|
||||||
@@ -56,9 +68,8 @@ namespace Decentralized.Domain
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 <see cref="Tuning.ModuleRatio"/> 后生效,
|
/// 矿架上游戏模块的效果。机器汇总的三项百分比(性能、效率、品质)各除以 <see cref="Tuning.ModuleRatio"/> 后生效,不设上限(强力模块是玩家自己拿到的)。
|
||||||
/// 并有上限,避免模块把挖矿收益放大到破坏经济:算力最多 +20%,省电最多 25%,损耗最多降 30%。
|
/// 性能加成同时按比例提高损耗与暴毙率。三个乘数各有防出错的下限:算力不低于 <see cref="Tuning.ModuleHashFloor"/>,耗电不低于 <see cref="Tuning.ModuleKjFloor"/>,损耗不低于 <see cref="Tuning.ModuleWearFloor"/>。
|
||||||
/// 性能加成同时按比例提高损耗与暴毙率。负数(例如节能模块的负性能、超频模块的负效率)照常生效,耗电增加不设上限。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct ModuleBonus
|
public struct ModuleBonus
|
||||||
{
|
{
|
||||||
@@ -72,14 +83,17 @@ namespace Decentralized.Domain
|
|||||||
public static ModuleBonus From(int performance, int efficiency, int quality)
|
public static ModuleBonus From(int performance, int efficiency, int quality)
|
||||||
{
|
{
|
||||||
double r = Tuning.ModuleRatio * 100.0;
|
double r = Tuning.ModuleRatio * 100.0;
|
||||||
double hash = Clamp(performance / r, -0.5, Tuning.ModuleHashCap);
|
double hash = performance / r, save = efficiency / r, wearCut = quality / r;
|
||||||
double save = Clamp(efficiency / r, -1.0, Tuning.ModuleKjCap);
|
|
||||||
double wearCut = Clamp(quality / r, -1.0, Tuning.ModuleWearCap);
|
|
||||||
double heat = 1 + Math.Max(0, hash);
|
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>
|
/// <summary>一张显卡或一台矿机的实例状态(存进物品标签)。</summary>
|
||||||
@@ -99,6 +113,11 @@ namespace Decentralized.Domain
|
|||||||
public HardwareSpec Label => Hardware.Specs[Model];
|
public HardwareSpec Label => Hardware.Specs[Model];
|
||||||
public HardwareSpec Chip => Hardware.Specs[FakeOf ?? 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>
|
/// <summary>当前算力:健康 ≥ 50 时满算力,低于 50 线性降到健康 0 时的 60%。</summary>
|
||||||
public double Hash(ModuleBonus b)
|
public double Hash(ModuleBonus b)
|
||||||
{
|
{
|
||||||
@@ -142,6 +161,10 @@ namespace Decentralized.Domain
|
|||||||
public sealed class Network
|
public sealed class Network
|
||||||
{
|
{
|
||||||
public double NpcHash = Tuning.NpcHash0;
|
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;
|
public double Reward = Tuning.BlockReward;
|
||||||
/// <summary>停电、动荡等事件对 NPC 算力的可用比例。</summary>
|
/// <summary>停电、动荡等事件对 NPC 算力的可用比例。</summary>
|
||||||
public double Availability = 1.0;
|
public double Availability = 1.0;
|
||||||
@@ -151,7 +174,26 @@ namespace Decentralized.Domain
|
|||||||
{
|
{
|
||||||
if (playerHash <= 0) return 0;
|
if (playerHash <= 0) return 0;
|
||||||
double total = NpcHash * Availability + playerHash;
|
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>
|
/// <summary>
|
||||||
@@ -162,7 +204,7 @@ namespace Decentralized.Domain
|
|||||||
if (day > 0 && day % Tuning.HalvingDays == 0) Reward /= 2;
|
if (day > 0 && day % Tuning.HalvingDays == 0) Reward /= 2;
|
||||||
double total = NpcHash * Availability + playerHash;
|
double total = NpcHash * Availability + playerHash;
|
||||||
double perH = Reward * price / total;
|
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)));
|
double adj = Math.Max(-Tuning.NpcAdj, Math.Min(Tuning.NpcAdj, Tuning.NpcAdj * (ratio / Tuning.NpcTargetRatio - 1)));
|
||||||
NpcHash *= 1 + adj;
|
NpcHash *= 1 + adj;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,48 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Decentralized.Domain
|
namespace Decentralized.Domain
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 三种稳定币的价格与状态:CRC(上层,托管银行出事时脱锚、可能减记),CRT(黑市,镇压或火并时脱锚,
|
/// 三种稳定币的价格与状态:CRC(上层,托管银行出事时脱锚、可能减记),CRT(黑市,镇压或火并时脱锚,
|
||||||
/// 镇压期间按黑市声望出卖客户),CRA(革命军,质押付息,补贴池告急或外生触发时进入三天死亡螺旋)。
|
/// 镇压期间按黑市声望出卖客户),CRA(革命军,部分抵押:流通量 S、储备 R(CRC)、抵押率 c;质押利息以无抵押的新 CRA 付出,
|
||||||
|
/// 稀释储备覆盖率 R/S;革命军挪用储备时 R/S 低于崩盘线进入三天死亡螺旋,劫掠上层补回储备)。
|
||||||
/// </summary>
|
/// </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 sealed class Stablecoins
|
||||||
{
|
{
|
||||||
public double Crc = 1, Crt = 1, Cra = 1;
|
public double Crc = 1, Crt = 1, Cra = 1;
|
||||||
public int CrcDepegEnd = -1, CrtDepegEnd = -1;
|
public int CrcDepegEnd = -1, CrtDepegEnd = -1;
|
||||||
public bool CrcHaircutPending;
|
public bool CrcHaircutPending;
|
||||||
public double CraSubsidy = Tuning.CraSubsidy0;
|
/// <summary>CRA 流通量与储备(CRC);c 是现在铸造要求的 CRC 份额(0~1)。</summary>
|
||||||
public int CraWarnDay = -1, CraSpiralDay = -1;
|
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;
|
public double RevPower = 50;
|
||||||
|
/// <summary>平衡代币 SPARK 的价格(信用点);SparkPre 是螺旋开始前一刻的价格,螺旋三天按它的倍数走。</summary>
|
||||||
|
public double Spark = Tuning.SparkStart, SparkPre = -1;
|
||||||
public List<double> RevHistory = new List<double>();
|
public List<double> RevHistory = new List<double>();
|
||||||
|
|
||||||
public double Price(string asset)
|
public double Price(string asset)
|
||||||
@@ -24,6 +52,7 @@ namespace Decentralized.Domain
|
|||||||
case Assets.CRC: return Crc;
|
case Assets.CRC: return Crc;
|
||||||
case Assets.CRT: return Crt;
|
case Assets.CRT: return Crt;
|
||||||
case Assets.CRA: return Cra;
|
case Assets.CRA: return Cra;
|
||||||
|
case Assets.SPARK: return Spark;
|
||||||
default: return 1;
|
default: return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,34 +60,94 @@ namespace Decentralized.Domain
|
|||||||
/// <summary>减记发生时返回 CRC 持有量要乘的系数(其余情况为 1)。</summary>
|
/// <summary>减记发生时返回 CRC 持有量要乘的系数(其余情况为 1)。</summary>
|
||||||
public double OnEventStart(MarketEvent ev, int day, Rng rng)
|
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);
|
Crc = rng.Range(Tuning.CrcDepegMin, Tuning.CrcDepegMax);
|
||||||
CrcDepegEnd = day + rng.Range(Tuning.CrcDepegDaysMin, Tuning.CrcDepegDaysMax);
|
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);
|
Crt = rng.Range(Tuning.CrtDepegMin, Tuning.CrtDepegMax);
|
||||||
CrtDepegEnd = day + rng.Range(Tuning.CrtDepegDaysMin, Tuning.CrtDepegDaysMax);
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>镇压开始时,玩家持有的 CRT 被冻结一半的概率:黑市声望 0 时 40%,50 以上为 0。</summary>
|
/// <summary>镇压开始时,玩家持有的 CRT 被冻结一半的概率:黑市声望 0 时 40%,50 以上为 0。</summary>
|
||||||
public static double CrtFreezeChance(double blackMarketRep) =>
|
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>
|
/// <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;
|
public double CraRate => CraSubsidyLow ? Tuning.CraRateLow : Tuning.CraRate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 夜间结算。返回 CRC 持有量要乘的系数(减记兑现时为 0.95,否则为 1)。
|
/// 夜间结算。返回 CRC 持有量要乘的系数(减记兑现时为 0.95,否则为 1)。
|
||||||
/// playerStaked:玩家的 CRA 质押额,用来算补贴支出(玩家的利息由调用方按 <see cref="CraRate"/> 发放)。
|
/// playerStaked:玩家的 CRA 质押额,用来算利息带来的稀释(玩家的利息由调用方按 <see cref="CraRate"/> 发放)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double Night(int day, Rng rng, double playerStaked)
|
public double Night(int day, Rng rng, double playerStaked)
|
||||||
{
|
{
|
||||||
@@ -79,21 +168,87 @@ namespace Decentralized.Domain
|
|||||||
if (RevHistory.Count > 7) RevHistory.RemoveAt(0);
|
if (RevHistory.Count > 7) RevHistory.RemoveAt(0);
|
||||||
RevPower = Math.Max(0, Math.Min(100, RevPower + rng.Gauss() * 3));
|
RevPower = Math.Max(0, Math.Min(100, RevPower + rng.Gauss() * 3));
|
||||||
|
|
||||||
if (CraSpiralDay < 0)
|
if (CraCrashDay < 0)
|
||||||
{
|
{
|
||||||
CraSubsidy -= (Tuning.CraNpcStake + playerStaked) * CraRate;
|
double interest = (Tuning.CraNpcStake + playerStaked) * CraRate;
|
||||||
bool revFalling = RevHistory.Count == 7 && RevPower < RevHistory[0] - 5;
|
Supply += interest;
|
||||||
double weekly = Tuning.CraSpiralWeekly * (revFalling ? 3 : 1);
|
// 抵押率调整:价格低于 1 上调,回到 1 下调;兑付的 CRC 份额不超过储备均值(见 <see cref="Redeem"/>)
|
||||||
double daily = 1 - Math.Pow(1 - weekly, 1.0 / 7);
|
if (Cra < 0.995) C = Math.Min(1, C + Tuning.CraCrStep);
|
||||||
if (CraWarnDay < 0 && (rng.Chance(daily) || CraSubsidy <= 0))
|
else if (Cra >= 0.999) C = Math.Max(Tuning.CraCrMin, C - Tuning.CraCrStep);
|
||||||
|
if (CraEventDay < 0)
|
||||||
{
|
{
|
||||||
CraWarnDay = day;
|
bool revFalling = RevHistory.Count == 7 && RevPower < RevHistory[0] - 5;
|
||||||
CraSpiralDay = day + rng.Range(Tuning.CraWarnLeadMin, Tuning.CraWarnLeadMax);
|
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)
|
if (CraCrashDay >= 0)
|
||||||
Cra = Tuning.CraSpiralPath[Math.Min(day - CraSpiralDay, Tuning.CraSpiralPath.Length - 1)];
|
{
|
||||||
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,32 +5,47 @@ namespace Decentralized.Domain
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Tuning
|
public static class Tuning
|
||||||
{
|
{
|
||||||
|
/// <summary>人脉网络升级价格:Web3 治理社区(入口)、混币器。</summary>
|
||||||
|
public const int UnlockPrice = 400, MixerUnlockPrice = 300;
|
||||||
|
|
||||||
// 挖矿
|
// 挖矿
|
||||||
public const double PscStartPrice = 100;
|
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 int HalvingDays = 90;
|
||||||
/// <summary>减半对币价的传导:奖励减半时,价格锚上移 2^传导指数 倍(0.6 即约 +52%),市场已部分预期到,所以不是翻倍。</summary>
|
/// <summary>减半对币价的传导:每次减半在区间内随机一个指数,价格锚上移 2^指数 倍(0.15–0.85 即约 +11% 到 +80%)。
|
||||||
public const double HalvingPassThrough = 0.6;
|
/// 参考比特币:每次减半后的涨幅差别很大,市场也已部分预期到,所以既不固定也不翻倍。</summary>
|
||||||
|
public const double HalvingPassMin = 0.15, HalvingPassMax = 0.85;
|
||||||
/// <summary>减半前一周的预期行情:锚逐日抬高,到减半当天为 +8%。</summary>
|
/// <summary>减半前一周的预期行情:锚逐日抬高,到减半当天为 +8%。</summary>
|
||||||
public const int HalvingRampDays = 7;
|
public const int HalvingRampDays = 7;
|
||||||
public const double HalvingRamp = 0.08;
|
public const double HalvingRamp = 0.08;
|
||||||
/// <summary>价格锚向目标的逐晚靠拢比例(约 7 天完成大半)。</summary>
|
/// <summary>价格锚向目标的逐晚靠拢比例(约 7 天完成大半)。</summary>
|
||||||
public const double SupplyAdjust = 0.15;
|
public const double SupplyAdjust = 0.15;
|
||||||
public const double NpcHash0 = 360;
|
public const double NpcHash0 = 3600;
|
||||||
public const double NpcKjPerH = 2.4;
|
public const double NpcKjPerH = 2.4;
|
||||||
public const double ElecPrice = 1.5;
|
public const double ElecPrice = 1.5;
|
||||||
public const double NpcTargetRatio = 3.5;
|
public const double NpcTargetRatio = 3.5;
|
||||||
public const double NpcAdj = 0.05;
|
public const double NpcAdj = 0.05;
|
||||||
public const double PoolFee = 0.03;
|
public const double PoolFee = 0.03;
|
||||||
public const int RigPrice = 400, RigKj = 2;
|
public const int RigPrice = 400, RigKj = 2;
|
||||||
/// <summary>矿架上模块百分比的折算系数(原版机器用 2–4),以及各项上限。</summary>
|
/// <summary>矿架上模块百分比的折算系数(原版机器用 2–4)。模块效果不设上限,只有防出错的下限:算力、耗电、每日损耗三个乘数各自不低于下面的值。</summary>
|
||||||
public const double ModuleRatio = 4, ModuleHashCap = 0.20, ModuleKjCap = 0.25, ModuleWearCap = 0.30;
|
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 DeathBase = 0.001, DeathWorn = 0.03, FakeDeathFactor = 2;
|
||||||
public const double RefurbishHealth = 30;
|
public const double RefurbishHealth = 30;
|
||||||
|
|
||||||
// 市场
|
// 市场
|
||||||
public const double PoolDepth0 = 50000;
|
/// <summary>交易对信用点一侧的基础深度(DEX 侧池按比例取)。</summary>
|
||||||
public const double DexFee = 0.003, ExchangeFee = 0.002;
|
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 Revert = 0.15;
|
||||||
public const double PscVol = 0.04;
|
public const double PscVol = 0.04;
|
||||||
public const double SentimentKappa = 0.02, SentimentVol = 0.02;
|
public const double SentimentKappa = 0.02, SentimentVol = 0.02;
|
||||||
@@ -39,7 +54,7 @@ namespace Decentralized.Domain
|
|||||||
// 稳定币
|
// 稳定币
|
||||||
public const double CrcDepegChance = 0.30, CrcDepegMin = 0.88, CrcDepegMax = 0.93;
|
public const double CrcDepegChance = 0.30, CrcDepegMin = 0.88, CrcDepegMax = 0.93;
|
||||||
public const int CrcDepegDaysMin = 2, CrcDepegDaysMax = 3;
|
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 double CrtDepegChance = 0.40, CrtDepegMin = 0.92, CrtDepegMax = 0.96;
|
||||||
public const int CrtDepegDaysMin = 1, CrtDepegDaysMax = 2;
|
public const int CrtDepegDaysMin = 1, CrtDepegDaysMax = 2;
|
||||||
public const double CrtFreezeBase = 0.40, CrtFreezePerRep = 0.008, CrtFreezeShare = 0.5;
|
public const double CrtFreezeBase = 0.40, CrtFreezePerRep = 0.008, CrtFreezeShare = 0.5;
|
||||||
@@ -47,20 +62,307 @@ namespace Decentralized.Domain
|
|||||||
public const double CrtSwapFee = 0.02;
|
public const double CrtSwapFee = 0.02;
|
||||||
public const double CraRate = 0.005, CraRateLow = 0.002;
|
public const double CraRate = 0.005, CraRateLow = 0.002;
|
||||||
public const int CraUnlockDays = 4;
|
public const int CraUnlockDays = 4;
|
||||||
public const double CraSubsidy0 = 30000, CraSubsidyWarn = 0.2, CraNpcStake = 20000;
|
public const double CraSubsidyWarn = 0.2, CraNpcStake = 20000;
|
||||||
public const double CraSpiralWeekly = 0.02;
|
/// <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 const int CraWarnLeadMin = 4, CraWarnLeadMax = 7;
|
||||||
public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 };
|
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(阈值以当前房租为单位)
|
/// <summary>出金到银行:超出成本的部分征税。</summary>
|
||||||
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;
|
|
||||||
public const double TaxRate = 0.08;
|
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>
|
/// <summary>单个金融违规事件记入的罪证上限。</summary>
|
||||||
public const double CrimeEventCap = 400;
|
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>
|
/// <summary>主交易对(信用点一侧)的深度随天数增长。</summary>
|
||||||
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
|
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
|
||||||
|
|
||||||
|
/// <summary>永续的成交深度,同样随天数增长。</summary>
|
||||||
|
public static double PerpDepth(int day) => PerpDepth0 * (1 + day / 60.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,15 +18,15 @@ namespace Decentralized.Domain
|
|||||||
|
|
||||||
public static class UsedDealers
|
public static class UsedDealers
|
||||||
{
|
{
|
||||||
public const double MiddlemanMarkup = 0.08;
|
public const double MiddlemanMarkup = Tuning.UsedMiddlemanMarkup;
|
||||||
public const double FakeShareMiddleman = 0.10, FakeShareScammer = 0.45;
|
public const double FakeShareMiddleman = Tuning.UsedFakeMiddleman, FakeShareScammer = Tuning.UsedFakeScammer;
|
||||||
|
|
||||||
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
|
/// <summary>今天来的贩子的类型:骗子越往后越多。</summary>
|
||||||
public static DealerKind Kind(int day, Rng rng)
|
public static DealerKind Kind(int day, Rng rng)
|
||||||
{
|
{
|
||||||
double scam = Math.Min(0.30, 0.10 + day * 0.004);
|
double scam = Scales.R(Math.Min(Tuning.UsedScamMax, Tuning.UsedScamBase + day * Tuning.UsedScamPerDay));
|
||||||
double r = rng.NextDouble();
|
double r = rng.NextDouble();
|
||||||
return r < scam ? DealerKind.Scammer : r < scam + 0.30 ? DealerKind.Middleman : DealerKind.Honest;
|
return r < scam ? DealerKind.Scammer : r < scam + Tuning.UsedMiddlemanShare ? DealerKind.Middleman : DealerKind.Honest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>能出现在二手市场上的型号:按天数放开,旗舰第 30 天起。</summary>
|
/// <summary>能出现在二手市场上的型号:按天数放开,旗舰第 30 天起。</summary>
|
||||||
@@ -38,6 +38,16 @@ namespace Decentralized.Domain
|
|||||||
return list.ToArray();
|
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)
|
public static List<UsedCard> Cards(DealerKind kind, int day, Rng rng)
|
||||||
{
|
{
|
||||||
var pool = Pool(day);
|
var pool = Pool(day);
|
||||||
@@ -46,12 +56,7 @@ namespace Decentralized.Domain
|
|||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
double share = kind == DealerKind.Scammer ? FakeShareScammer : kind == DealerKind.Middleman ? FakeShareMiddleman : 0;
|
double share = kind == DealerKind.Scammer ? FakeShareScammer : kind == DealerKind.Middleman ? FakeShareMiddleman : 0;
|
||||||
var label = pool[rng.Range(0, pool.Length - 1)];
|
cards.Add(Card(pool, share, kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0, rng, Tuning.UsedHealthMin[(int)kind], Tuning.UsedHealthMax[(int)kind]));
|
||||||
var card = new UsedCard { Model = label, Health = 25 + rng.NextDouble() * 60 };
|
|
||||||
var cheaper = Hardware.Specs.Values.Where(s => !s.Asic && s.Price * 2 <= Hardware.Specs[label].Price).Select(s => s.Model).ToArray();
|
|
||||||
if (cheaper.Length > 0 && rng.Chance(share)) card.FakeOf = cheaper[rng.Range(0, cheaper.Length - 1)];
|
|
||||||
card.MarkupPct = kind == DealerKind.Middleman ? (int)Math.Round(MiddlemanMarkup * 100) : 0;
|
|
||||||
cards.Add(card);
|
|
||||||
}
|
}
|
||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,53 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Decentralized.Domain
|
namespace Decentralized.Domain
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 一个链上地址:各币种余额,加上整个地址的污点额与成本(信用点等值)。
|
/// 一个链上地址:各币种余额,每种币里有几枚是脏的(<see cref="Dirty"/>),加上整个地址的成本(信用点)。
|
||||||
/// 污点和成本按转出的价值比例随币走。
|
/// 污点随具体的币进出:收到带污点的币时记下其中几枚脏;花掉、转出这种币时按它的脏比例带走;兑换成别的币时脏比例跟过去。
|
||||||
|
/// 成本按转出的价值比例随币走。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Wallet
|
public sealed class Wallet
|
||||||
{
|
{
|
||||||
public string Address;
|
public string Address;
|
||||||
public Dictionary<string, double> Balances = new Dictionary<string, double>();
|
public Dictionary<string, double> Balances = new Dictionary<string, double>();
|
||||||
/// <summary>污点额(信用点等值)。</summary>
|
/// <summary>每种币里脏的枚数(0 ≤ 脏 ≤ 余额)。</summary>
|
||||||
public double Taint;
|
public Dictionary<string, double> Dirty = new Dictionary<string, double>();
|
||||||
|
/// <summary>旧存档(数据版本 5 之前)整个钱包的污点额(信用点):只在读档迁移时读一次,之后为 0。</summary>
|
||||||
|
[JsonProperty("Taint")] public double LegacyTaint;
|
||||||
/// <summary>成本(信用点):出金时只对超出成本的部分征税。</summary>
|
/// <summary>成本(信用点):出金时只对超出成本的部分征税。</summary>
|
||||||
public double Basis;
|
public double Basis;
|
||||||
/// <summary>被执法标记。</summary>
|
/// <summary>钱包编号("钱包 n"),添加时定下,销毁别的钱包也不变。</summary>
|
||||||
public bool Flagged;
|
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>
|
/// <summary>买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。</summary>
|
||||||
public Dictionary<string, double> BuyQty = new Dictionary<string, double>(), BuyCost = new Dictionary<string, double>();
|
public Dictionary<string, double> BuyQty = new Dictionary<string, double>(), BuyCost = new Dictionary<string, double>();
|
||||||
@@ -32,6 +62,36 @@ namespace Decentralized.Domain
|
|||||||
/// <summary>买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。</summary>
|
/// <summary>买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。</summary>
|
||||||
public double? AvgBuy(string asset) => BuyQty.TryGetValue(asset, out var q) && q > 1e-9 ? BuyCost[asset] / q : (double?)null;
|
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 double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0;
|
||||||
|
|
||||||
public void Add(string asset, double amount)
|
public void Add(string asset, double amount)
|
||||||
@@ -43,31 +103,131 @@ namespace Decentralized.Domain
|
|||||||
BuyQty[asset] = bq * keep; BuyCost[asset] *= keep;
|
BuyQty[asset] = bq * keep; BuyCost[asset] *= keep;
|
||||||
if (BuyQty[asset] < 1e-9) { BuyQty.Remove(asset); BuyCost.Remove(asset); }
|
if (BuyQty[asset] < 1e-9) { BuyQty.Remove(asset); BuyCost.Remove(asset); }
|
||||||
}
|
}
|
||||||
double v = Get(asset) + amount;
|
double had = Get(asset), v = had + amount;
|
||||||
if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足");
|
if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足");
|
||||||
if (Math.Abs(v) < 1e-12) Balances.Remove(asset);
|
if (Math.Abs(v) < 1e-12) Balances.Remove(asset);
|
||||||
else Balances[asset] = v;
|
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>
|
/// <summary>按价格表估值(信用点)。</summary>
|
||||||
|
|
||||||
public double Value(Func<string, double> price) => Balances.Sum(kv => kv.Value * price(kv.Key));
|
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>
|
/// <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>
|
/// </summary>
|
||||||
public static void Transfer(Wallet from, Wallet to, string asset, double amount, Func<string, double> price)
|
public static void Transfer(Wallet from, Wallet to, string asset, double amount, Func<string, double> price)
|
||||||
{
|
{
|
||||||
if (amount <= 0) return;
|
if (amount <= 0) return;
|
||||||
double total = from.Value(price);
|
double total = from.Value(price);
|
||||||
double share = total > 0 ? Math.Min(1.0, amount * price(asset) / total) : 0;
|
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.Add(asset, -amount);
|
||||||
from.Taint -= taint;
|
|
||||||
from.Basis -= basis;
|
from.Basis -= basis;
|
||||||
to.Add(asset, amount);
|
|
||||||
var spec = Assets.Get(asset);
|
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;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 > 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,19 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
public const string Id = "probably_decentralized";
|
public const string Id = "probably_decentralized";
|
||||||
public const string Name = "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>
|
/// <summary>本地化:游戏的 mod 本地化表(ModFiles/Localization/*.csv),当前语言缺失时回退英文。</summary>
|
||||||
public static class L
|
public static class L
|
||||||
{
|
{
|
||||||
public static string T(string key, params object[] args) => ModHelper.GetLocalized(ModInfo.Id, key, args);
|
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
|
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 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.####");
|
public static string Num(double v) => Math.Abs(v) >= 100 ? v.ToString("N0") : Math.Abs(v) >= 1 ? v.ToString("0.##") : v.ToString("0.####");
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ namespace Decentralized
|
|||||||
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used cards x4 (test)", () =>
|
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Used cards x4 (test)", () =>
|
||||||
{
|
{
|
||||||
var rng = Session.Data.Rng;
|
var rng = Session.Data.Rng;
|
||||||
Items.Give(Items.NewCard("RZX80", 40, null, rng));
|
Items.Give(Items.NewCard("RTX80", 40, null, rng));
|
||||||
Items.Give(Items.NewCard("RZX80", 70, "RZX60", rng));
|
Items.Give(Items.NewCard("RTX80", 70, "RTX60", rng));
|
||||||
Items.Give(Items.NewCard("RZ800", 30, "RZ70", rng));
|
Items.Give(Items.NewCard("RX800", 30, "RX70", rng));
|
||||||
Items.Give(Items.NewCard("RZ900", 90, "RZ70", rng));
|
Items.Give(Items.NewCard("RX900", 90, "RX70", rng));
|
||||||
});
|
});
|
||||||
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dump inspection UI (test)", () => Log.Guard("检视界面结构", () =>
|
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dump inspection UI (test)", () => Log.Guard("检视界面结构", () =>
|
||||||
{
|
{
|
||||||
@@ -45,10 +45,10 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
var rng = Session.Data.Rng;
|
var rng = Session.Data.Rng;
|
||||||
// 自己的一张真卡:标签机改成旗舰型号并把成色说成没问题,再摆给一位买家
|
// 自己的一张真卡:标签机改成旗舰型号并把成色说成没问题,再摆给一位买家
|
||||||
var item = Items.NewCard("RZX70", 55, null, rng);
|
var item = Items.NewCard("RTX70", 55, null, rng);
|
||||||
Items.Give(item);
|
Items.Give(item);
|
||||||
var u = Items.ToUnit(item);
|
var u = Items.ToUnit(item);
|
||||||
var c = Claim.Truth(u); c.Model = "RZX90"; c.Fan = 0; c.Fingers = 0; c.Bracket = 0; c.Sticker = 0;
|
var c = Claim.Truth(u); c.Model = "RTX90"; c.Fan = 0; c.Fingers = 0; c.Bracket = 0; c.Sticker = 0;
|
||||||
item.EnableTag(Items.EditedTag);
|
item.EnableTag(Items.EditedTag);
|
||||||
Items.SetClaim(item, c, 0);
|
Items.SetClaim(item, c, 0);
|
||||||
var client = new StoreClient { identifier = "pd_test_buyer", displayName = "买家", spriteName = SpriteDict.GetRandomScavSpriteName(), clientIntent = StoreClient.ClientIntent.BUY };
|
var client = new StoreClient { identifier = "pd_test_buyer", displayName = "买家", spriteName = SpriteDict.GetRandomScavSpriteName(), clientIntent = StoreClient.ClientIntent.BUY };
|
||||||
@@ -61,13 +61,213 @@ namespace Decentralized
|
|||||||
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Buy scam card (test)", () => Log.Guard("买入侧测试", () =>
|
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Buy scam card (test)", () => Log.Guard("买入侧测试", () =>
|
||||||
{
|
{
|
||||||
var rng = Session.Data.Rng;
|
var rng = Session.Data.Rng;
|
||||||
var item = Items.NewCard("RZX90", 60, "RZX60", rng, DealerKind.Scammer, 0);
|
var item = Items.NewCard("RTX90", 60, "RTX60", rng, DealerKind.Scammer, 0);
|
||||||
long before = item.unitValue;
|
long before = item.unitValue;
|
||||||
GeneralHelper.SetItemOwned(item, true);
|
GeneralHelper.SetItemOwned(item, true);
|
||||||
Items.Refresh(item);
|
Items.Refresh(item);
|
||||||
Items.Give(item);
|
Items.Give(item);
|
||||||
Log.Info($"买入测试:卡 #{item.uniqueId} 买前报价 {before},买下后价值 {item.unitValue},真实 {CardValue.True(Items.ToUnit(item))},说法 {(Items.GetClaim(item)?.Encode() ?? "无")}");
|
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)", () =>
|
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
|
||||||
{
|
{
|
||||||
EmporiumEntry.Instance.ClearFrontInv();
|
EmporiumEntry.Instance.ClearFrontInv();
|
||||||
@@ -96,6 +296,13 @@ namespace Decentralized
|
|||||||
bool ok = PlayerStore.Instance.TryAddDirectSellingItemToTable(item);
|
bool ok = PlayerStore.Instance.TryAddDirectSellingItemToTable(item);
|
||||||
placed.Add(ok ? $"{kind}#{item.uniqueId}" : kind + "(柜台已满)");
|
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));
|
Log.Info("杰克逊博士上货:" + string.Join(",", placed));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ namespace Decentralized
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GpuInspectView : MonoBehaviour
|
public sealed class GpuInspectView : MonoBehaviour
|
||||||
{
|
{
|
||||||
private const float RowH = 30f, CardScale = 3f, PanelTop = -112f, DetailH = 26f, GroupGap = 8f;
|
private const float RowH = 26f, CardScale = 3f, PanelTop = -112f, DetailH = 26f, GroupGap = 7f;
|
||||||
private const int RowCount = 5;
|
private static readonly int RowCount = CardLabels.Rows.Length;
|
||||||
private TextMeshProUGUI detailText;
|
private TextMeshProUGUI priceText;
|
||||||
private static GpuInspectView instance;
|
private static GpuInspectView instance;
|
||||||
private RectTransform root, cardRoot;
|
private RectTransform root, cardRoot;
|
||||||
private Button flip;
|
private Button flip;
|
||||||
@@ -28,9 +28,9 @@ namespace Decentralized
|
|||||||
private bool hidGlass;
|
private bool hidGlass;
|
||||||
private Transform buttonTemplate;
|
private Transform buttonTemplate;
|
||||||
private TMP_FontAsset font;
|
private TMP_FontAsset font;
|
||||||
private readonly TextMeshProUGUI[] rowName = new TextMeshProUGUI[RowCount], rowText = new TextMeshProUGUI[RowCount];
|
private readonly TextMeshProUGUI[] rowName = new TextMeshProUGUI[16], rowText = new TextMeshProUGUI[16];
|
||||||
private readonly Button[] rowButton = new Button[RowCount];
|
private readonly Button[] rowButton = new Button[16];
|
||||||
private readonly TextMeshProUGUI[] rowButtonText = new TextMeshProUGUI[RowCount];
|
private readonly TextMeshProUGUI[] rowButtonText = new TextMeshProUGUI[16];
|
||||||
private TextMeshProUGUI serialText;
|
private TextMeshProUGUI serialText;
|
||||||
|
|
||||||
public static void Ensure()
|
public static void Ensure()
|
||||||
@@ -173,26 +173,33 @@ namespace Decentralized
|
|||||||
if (buttonTemplate == null) return;
|
if (buttonTemplate == null) return;
|
||||||
font = buttonTemplate.GetComponentInChildren<TextMeshProUGUI>()?.font;
|
font = buttonTemplate.GetComponentInChildren<TextMeshProUGUI>()?.font;
|
||||||
if (font != null) serialText.font = font;
|
if (font != null) serialText.font = font;
|
||||||
// 标签区:深色底加浅色边框,和上面的大图分开
|
// 标签区:深色底加浅色边框,和上面的大图分开。三组:四项真伪依据、型号、四项成色,最下面一行是标签价
|
||||||
float panelH = RowCount * RowH + 10f + DetailH + GroupGap, panelY = PanelTop - panelH / 2;
|
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, "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, "LabelBack", new Vector2(0, panelY), new Vector2(640, panelH), new Color(0.10f, 0.11f, 0.14f, 0.96f));
|
||||||
MakeRect(root, "LabelDivider", new Vector2(0, PanelTop - 5 - RowH * CardLabels.AuthRows - DetailH - GroupGap / 2), new Vector2(620, 2), new Color(0.78f, 0.68f, 0.45f, 0.8f));
|
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));
|
||||||
detailText = MakeText(root, "Detail", new Vector2(-15, PanelTop - 5 - RowH * CardLabels.AuthRows - DetailH / 2), new Vector2(600, DetailH), TextAlignmentOptions.Left, 17);
|
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 = MakeButton(root, "Flip", new Vector2(0, PanelTop - panelH - 24), new Vector2(200, 30), out flipText);
|
||||||
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
|
flip.onClick.AddListener(() => { if (shown != null) Items.Flip(shown); });
|
||||||
for (int i = 0; i < RowCount; i++)
|
for (int i = 0; i < RowCount; i++)
|
||||||
{
|
{
|
||||||
int row = CardLabels.Rows[i];
|
int row = CardLabels.Rows[i];
|
||||||
float y = PanelTop - 5 - RowH * (i + 0.5f) - (i >= CardLabels.AuthRows ? DetailH + GroupGap : 0f);
|
float y = GroupTop(i) - RowH / 2;
|
||||||
rowName[i] = MakeText(root, "RowName" + i, new Vector2(-255, y), new Vector2(110, RowH), TextAlignmentOptions.Left, 22);
|
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(-30, y), new Vector2(330, RowH), TextAlignmentOptions.Left, 22);
|
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] = MakeButton(root, "RowButton" + i, new Vector2(245, y), new Vector2(110, RowH - 3), out rowButtonText[i]);
|
||||||
rowButton[i].onClick.AddListener(() => Log.Guard("标签栏", () => OnRowButton(row)));
|
rowButton[i].onClick.AddListener(() => Log.Guard("标签栏", () => OnRowButton(row)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RowKey(int row) => row == CardLabels.RowSpec ? "spec" : row == CardLabels.RowBuild ? "build" : row == CardLabels.RowFan ? "fan" : row == CardLabels.RowFingers ? "fingers"
|
/// <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";
|
: 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 ValueText(int row, int value) => row == CardLabels.RowSpec ? "" : L.T("lbl." + RowKey(row) + "." + (row == CardLabels.RowSeal && value == 1 ? 0 : value));
|
||||||
@@ -222,7 +229,7 @@ namespace Decentralized
|
|||||||
int row = CardLabels.Rows[i];
|
int row = CardLabels.Rows[i];
|
||||||
rowName[i].text = RowName(row);
|
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].color = i < CardLabels.AuthRows ? new Color(1f, 0.85f, 0.45f) : new Color(0.6f, 0.85f, 1f); // 真伪行金黄,成色行浅蓝
|
||||||
rowName[i].fontSize = 22;
|
rowName[i].fontSize = 20; PledgerSkin.Fit(rowName[i]); PledgerSkin.Fit(rowText[i]);
|
||||||
string text;
|
string text;
|
||||||
Color color = new Color(0.96f, 0.98f, 1f);
|
Color color = new Color(0.96f, 0.98f, 1f);
|
||||||
string Show(Claim c) => row == CardLabels.RowSpec ? DisplayModel(c.Model) : ValueText(row, c.Get(row));
|
string Show(Claim c) => row == CardLabels.RowSpec ? DisplayModel(c.Model) : ValueText(row, c.Get(row));
|
||||||
@@ -231,24 +238,18 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
text = Show(real);
|
text = Show(real);
|
||||||
bool lied = row == CardLabels.RowSpec ? claim.Value.Model != real.Model : claim.Value.Get(row) != real.Get(row);
|
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); text += L.T("lbl.said", row == CardLabels.RowBuild ? L.T("lbl.tier." + claim.Value.Tier) : Show(claim.Value)); }
|
if (lied) color = new Color(1f, 0.45f, 0.4f); // 卖家的说法只在机制里起作用,面板上不写
|
||||||
else color = new Color(0.5f, 1f, 0.55f);
|
else color = new Color(0.5f, 1f, 0.55f);
|
||||||
}
|
}
|
||||||
else text = Show(claim.Value);
|
else text = Show(claim.Value);
|
||||||
rowText[i].text = text;
|
rowText[i].text = text;
|
||||||
rowText[i].color = color;
|
rowText[i].color = color;
|
||||||
bool editable = row != CardLabels.RowBuild;
|
bool editable = row == CardLabels.RowSpec || !CardLabels.IsAuth(row);
|
||||||
rowButton[i].gameObject.SetActive(canEdit ? editable : canPoint && (exposed & row) == 0);
|
bool hasButton = row == CardLabels.RowSpec || !CardLabels.IsAuth(row);
|
||||||
rowButtonText[i].text = L.T(canEdit ? "lbl.edit" : "lbl.point");
|
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 (detailText != null)
|
|
||||||
{
|
|
||||||
bool spec = (exposed & CardLabels.RowSpec) != 0;
|
|
||||||
string detail = claim == null ? "" : !spec ? L.T("lbl.hint") : Evidence(u, claim.Value, real);
|
|
||||||
detailText.text = detail;
|
|
||||||
detailText.color = spec ? new Color(1f, 0.7f, 0.6f) : new Color(0.75f, 0.78f, 0.85f);
|
|
||||||
}
|
}
|
||||||
|
if (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 (flipText != null) flipText.text = L.T(Items.IsBack(item) ? "inspect.flip.front" : "inspect.flip.back");
|
||||||
// 贴纸上的序列号:只在背面显示
|
// 贴纸上的序列号:只在背面显示
|
||||||
if (serialText != null)
|
if (serialText != null)
|
||||||
@@ -260,16 +261,6 @@ namespace Decentralized
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>型号真伪的依据:实物与卖家说法对不上的地方(供电口与风扇数、序列号、背面丝印)。</summary>
|
|
||||||
private static string Evidence(Unit u, Claim claim, Claim real)
|
|
||||||
{
|
|
||||||
var parts = new List<string>();
|
|
||||||
if (claim.Tier != real.Tier) parts.Add(L.T("lbl.ev.build", L.T("lbl.tier." + real.Tier), Hardware.PowerPins[real.Tier], Hardware.FanCount[real.Tier]));
|
|
||||||
if (real.Serial != 0) parts.Add(L.T("lbl.ev.serial." + real.Serial));
|
|
||||||
if (real.Print != 0) parts.Add(L.T("lbl.ev.print." + real.Print));
|
|
||||||
return parts.Count == 0 ? L.T("lbl.ev.none") : L.T("lbl.ev.prefix") + string.Join(";", parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DisplayModel(string model) => Items.DisplayModel(model);
|
private static string DisplayModel(string model) => Items.DisplayModel(model);
|
||||||
|
|
||||||
private void OnRowButton(int row)
|
private void OnRowButton(int row)
|
||||||
@@ -283,7 +274,7 @@ namespace Decentralized
|
|||||||
var cur = Items.GetClaim(item) ?? Claim.Truth(u);
|
var cur = Items.GetClaim(item) ?? Claim.Truth(u);
|
||||||
if (row == CardLabels.RowSpec)
|
if (row == CardLabels.RowSpec)
|
||||||
{
|
{
|
||||||
var models = Hardware.Specs.Values.Where(s => !s.Asic).Select(s => s.Model).ToList();
|
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]);
|
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);
|
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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ namespace Decentralized
|
|||||||
public const string Rig = "pd_rig";
|
public const string Rig = "pd_rig";
|
||||||
public const string Guide = "pd_gpu_guide";
|
public const string Guide = "pd_gpu_guide";
|
||||||
public const string GpuPrefix = "pd_gpu_";
|
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 HealthTag = "PD_HEALTH";
|
||||||
public const string FakeTag = "PD_FAKE_OF";
|
public const string FakeTag = "PD_FAKE_OF";
|
||||||
@@ -25,9 +29,24 @@ namespace Decentralized
|
|||||||
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 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 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 Subscribe()
|
||||||
public static void Unsubscribe() => ModHook.OnModItemDirectoryInit -= Register;
|
{
|
||||||
|
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();
|
public static string GpuId(string model) => GpuPrefix + model.ToLowerInvariant();
|
||||||
|
|
||||||
@@ -37,8 +56,14 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
dir.Add(Terminal, CreateTerminal);
|
dir.Add(Terminal, CreateTerminal);
|
||||||
dir.Add(Rig, CreateRig);
|
dir.Add(Rig, CreateRig);
|
||||||
|
WrapElectronics(dir);
|
||||||
|
WrapTurbos(dir);
|
||||||
|
OtcLamp.Wrap(dir);
|
||||||
dir.Add(Guide, CreateGuide);
|
dir.Add(Guide, CreateGuide);
|
||||||
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic))
|
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;
|
var s = spec;
|
||||||
dir.Add(GpuId(s.Model), () => CreateGpu(s));
|
dir.Add(GpuId(s.Model), () => CreateGpu(s));
|
||||||
@@ -63,23 +88,79 @@ namespace Decentralized
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>显卡鉴定手册:档次对照、序列号规则、外观折扣。和游戏里的鉴定手册一样,是一件物品,说明里就是内容。</summary>
|
/// <summary>显卡鉴定手册:型号对照、序列号规则、外观折扣。做法同游戏里的鉴定手册(海明威鉴定指南):纸张图、文档类型、拖放音效。</summary>
|
||||||
private static GameItem CreateGuide()
|
private static GameItem CreateGuide()
|
||||||
{
|
{
|
||||||
var item = ItemDirectory.CreateEmptyItem(Guide);
|
var item = ItemDirectory.CreateEmptyItem(Guide);
|
||||||
item.SetSpriteAndShapeFromMod(ModInfo.Id, "gpu_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.name = L.T("item.guide.name");
|
||||||
item.shortDescription = L.T("item.guide.desc");
|
item.shortDescription = L.T("item.guide.desc");
|
||||||
item.unitValue = 0;
|
item.mayActivateSlotItemFunc = (_, _, _) => true;
|
||||||
|
item.canActivateSlotItemFunc = (_, _, _) => true;
|
||||||
|
item.onActivateSlotItemFunc = (_, _, _) => Log.Guard("打开鉴定手册", () => OpenGpuHandbook?.Invoke());
|
||||||
|
item.unitValue = 50;
|
||||||
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
|
item.EnableTag(GeneralHelper.IMPORTANT_TAG);
|
||||||
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
|
item.EnableTag(TypeHelper.NOT_FOR_RESALE);
|
||||||
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
|
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
|
||||||
return item;
|
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",RZ800 → "RZ 800"。</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])", " ");
|
public static string DisplayModel(string model) => Regex.Replace(Regex.Replace(model, @"(?<=[A-Za-z])(?=\d)", " "), @"(?<=\d)(?=[A-Z])", " ");
|
||||||
|
|
||||||
private static GameItem CreateGpu(HardwareSpec spec)
|
private static GameItem CreateGpu(HardwareSpec spec)
|
||||||
@@ -109,7 +190,8 @@ namespace Decentralized
|
|||||||
public static GameItem NewCard(string model, double health, string fakeOf, Rng rng, DealerKind? seller = null, int markupPct = 0)
|
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 item = ModHelper.SpawnItem(GpuId(model));
|
||||||
var u = new Unit { Model = model, FakeOf = fakeOf, Health = health, Look = CardLook.Generate(rng, health, fakeOf != null) };
|
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);
|
Store(item, u);
|
||||||
if (seller != null)
|
if (seller != null)
|
||||||
{
|
{
|
||||||
@@ -123,14 +205,12 @@ namespace Decentralized
|
|||||||
|
|
||||||
private static object checkedClient;
|
private static object checkedClient;
|
||||||
private static readonly HashSet<int> checkedItems = new HashSet<int>();
|
private static readonly HashSet<int> checkedItems = new HashSet<int>();
|
||||||
public const double CustomerSpotChance = 0.50;
|
|
||||||
public const int RepLossMax = 25;
|
|
||||||
|
|
||||||
/// <summary>被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。</summary>
|
/// <summary>被顾客看穿时的信誉损失:骗的价差每满一周房租扣 1 点,最少 1,最多 25。</summary>
|
||||||
public static int RepLoss(long gap) => (int)Math.Max(1, Math.Min(RepLossMax, Math.Round(gap / (double)Math.Max(1, PlayerStore.Instance.rentValue))));
|
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>
|
/// <summary>
|
||||||
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来;看出来就拆穿这一项(价格回到真实情况),
|
/// 自己的卡摆给想买的顾客:说法里隐瞒的每一项,顾客有 50% 的概率看出来(型号档次不符的必看出来,供电口和风扇数一眼可见);看出来就拆穿这一项(价格回到真实情况),
|
||||||
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
|
/// 并且掉一点信誉。每位顾客对每张卡只看一次。说法比实际更差(把好卡说差了)顾客不会去指。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void OfferForTest(GameItem item) { checkedItems.Clear(); OnOffered(item, true); }
|
public static void OfferForTest(GameItem item) { checkedItems.Clear(); OnOffered(item, true); }
|
||||||
@@ -152,10 +232,10 @@ namespace Decentralized
|
|||||||
var rng = Session.Data.Rng;
|
var rng = Session.Data.Rng;
|
||||||
int found = 0;
|
int found = 0;
|
||||||
foreach (int r in CardLabels.Rows)
|
foreach (int r in CardLabels.Rows)
|
||||||
if ((lies & r) != 0 && rng.Chance(CustomerSpotChance))
|
if ((lies & r) != 0 && rng.Chance((r == CardLabels.RowCount || r == CardLabels.RowPin) ? Tuning.CustomerSpotTierMismatch : Tuning.CustomerSpotChance))
|
||||||
{
|
{
|
||||||
found |= r;
|
found |= r;
|
||||||
if (r != CardLabels.RowSpec && CardLabels.IsEvidence(r, claim.Value, Claim.Truth(u))) found |= CardLabels.RowSpec | CardLabels.RowBuild;
|
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;
|
if (found == 0) return;
|
||||||
long before = item.unitValue;
|
long before = item.unitValue;
|
||||||
@@ -181,7 +261,7 @@ namespace Decentralized
|
|||||||
public static int Exposed(GameItem item)
|
public static int Exposed(GameItem item)
|
||||||
{
|
{
|
||||||
int e = item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
|
int e = item.IsTag(ExposedTag) ? item.GetTagReadonly(ExposedTag).GetInt() : 0;
|
||||||
if ((e & CardLabels.RowSpec) != 0) e |= CardLabels.RowSpec | CardLabels.RowBuild | CardLabels.RowSerial | CardLabels.RowPrint;
|
if ((e & CardLabels.RowSpec) != 0) e |= CardLabels.RowSpec | CardLabels.RowCount | CardLabels.RowPin | CardLabels.RowSerial | CardLabels.RowPrint;
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,14 +338,26 @@ namespace Decentralized
|
|||||||
|
|
||||||
public static Unit ToUnit(GameItem item)
|
public static Unit ToUnit(GameItem item)
|
||||||
{
|
{
|
||||||
string model = item.identifier.Substring(GpuPrefix.Length);
|
bool asic = AsicItems.Is(item);
|
||||||
model = Hardware.Specs.Keys.FirstOrDefault(k => k.ToLowerInvariant() == model) ?? "RZX60";
|
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 };
|
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(HealthTag)) u.Health = item.GetTagReadonly(HealthTag).GetDouble();
|
||||||
if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString();
|
if (item.IsTag(FakeTag)) u.FakeOf = item.GetTagReadonly(FakeTag).GetString();
|
||||||
u.Refurbished = item.IsTag(RefurbTag);
|
u.Refurbished = item.IsTag(RefurbTag);
|
||||||
u.Dead = item.IsTag(DeadTag);
|
u.Dead = item.IsTag(DeadTag);
|
||||||
if (item.IsTag(LookTag)) u.Look = CardLook.Decode(item.GetTagReadonly(LookTag).GetString());
|
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;
|
u.Look.Seed = item.IsTag(SeedTag) ? (uint)item.GetTagReadonly(SeedTag).GetInt() : (uint)(item.uniqueId * 2654435761u) | 1u;
|
||||||
return u;
|
return u;
|
||||||
@@ -277,14 +369,18 @@ namespace Decentralized
|
|||||||
if (u.FakeOf != null) item.ModifyTag(FakeTag, s => s.Enable().SetString(u.FakeOf));
|
if (u.FakeOf != null) item.ModifyTag(FakeTag, s => s.Enable().SetString(u.FakeOf));
|
||||||
if (u.Refurbished) item.EnableTag(RefurbTag);
|
if (u.Refurbished) item.EnableTag(RefurbTag);
|
||||||
if (u.Dead) item.EnableTag(DeadTag);
|
if (u.Dead) item.EnableTag(DeadTag);
|
||||||
item.ModifyTag(LookTag, s => s.Enable().SetString(u.Look.Encode()));
|
if (!AsicItems.Is(item))
|
||||||
item.ModifyTag(SeedTag, s => s.Enable().SetInt((int)u.Look.Seed));
|
{
|
||||||
|
item.ModifyTag(LookTag, s => s.Enable().SetString(u.Look.Encode()));
|
||||||
|
item.ModifyTag(SeedTag, s => s.Enable().SetInt((int)u.Look.Seed));
|
||||||
|
}
|
||||||
Refresh(item);
|
Refresh(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>按标签更新名字、说明、价值和图:显示的是标签上的型号(假卡也显示被冒充的型号)。</summary>
|
/// <summary>按标签更新名字、说明、价值和图:显示的是标签上的型号(假卡也显示被冒充的型号)。</summary>
|
||||||
public static void Refresh(GameItem item)
|
public static void Refresh(GameItem item)
|
||||||
{
|
{
|
||||||
|
if (AsicItems.Is(item)) { AsicItems.Refresh(item); return; }
|
||||||
var u = ToUnit(item);
|
var u = ToUnit(item);
|
||||||
var label = u.Label;
|
var label = u.Label;
|
||||||
if (u.Dead)
|
if (u.Dead)
|
||||||
@@ -309,7 +405,7 @@ namespace Decentralized
|
|||||||
|
|
||||||
/// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
|
/// <summary>当前面的底图名与叠加图名:正面三个维度,背面丝印与贴纸;烧毁的卡不叠。</summary>
|
||||||
/// <summary>实物上的风扇数:由真实芯片的档次决定(低档次的板子套高档次的壳,风扇数露馅)。</summary>
|
/// <summary>实物上的风扇数:由真实芯片的档次决定(低档次的板子套高档次的壳,风扇数露馅)。</summary>
|
||||||
public static int FansOf(Unit u) => Hardware.FanCount[Hardware.Specs[u.FakeOf ?? u.Model].Tier];
|
public static int FansOf(Unit u) => u.ObservedCount;
|
||||||
|
|
||||||
public static string LookKeys(GameItem item, List<string> overlays)
|
public static string LookKeys(GameItem item, List<string> overlays)
|
||||||
{
|
{
|
||||||
@@ -331,7 +427,7 @@ namespace Decentralized
|
|||||||
overlays.Add((l.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
|
overlays.Add((l.Print == 2 ? "gpu_ov_txs_" : "gpu_ov_txt_") + model.ToLowerInvariant());
|
||||||
if (l.Sticker == 2) overlays.Add("gpu_ov_stk2");
|
if (l.Sticker == 2) overlays.Add("gpu_ov_stk2");
|
||||||
}
|
}
|
||||||
return brand + (back ? "_b" : "_f_t" + Hardware.Specs[u.FakeOf ?? u.Model].Tier);
|
return brand + (back ? "_b" : $"_f_p{u.ObservedPin}n{u.ObservedCount}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Sprite ModSprite(string key)
|
public static Sprite ModSprite(string key)
|
||||||
@@ -366,17 +462,26 @@ namespace Decentralized
|
|||||||
private static GameItem CreateRig()
|
private static GameItem CreateRig()
|
||||||
{
|
{
|
||||||
var battery = new GameSlotInventory();
|
var battery = new GameSlotInventory();
|
||||||
var upgrade = new GameSlotInventory();
|
|
||||||
var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7);
|
var modules = new GameGridInventory(MachineHelper.DEFAULT_MODULE_BAY_CONFIG, 7);
|
||||||
// 显卡每张 3×2 格:3 格宽的格子,每张卡 3×2 格,每个槽两行;未解锁的行不可放
|
// 显卡每张 3×2 格:3 格宽的格子,每张卡 3×2 格,每个槽两行;未解锁的行不可放
|
||||||
var cards = new GameGridInventory(RigSlots.Shape(RigSlots.Base), RigSlots.CellWidth);
|
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,
|
var window = MachineHelper.CreateMachineryWindow(true,
|
||||||
MachineHelper.PowerPanel(battery),
|
MachineHelper.PowerPanel(battery),
|
||||||
MachineHelper.Panel(L.T("rig.label.modules"), modules, "module"),
|
MachineHelper.Panel(L.T("rig.label.modules"), modules, "module"),
|
||||||
MachineHelper.Panel(L.T("rig.label.cards"), cards, "cards"),
|
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);
|
var rig = ItemDirectory.CreateEmptyItem(Rig).SetContentWindow(window);
|
||||||
rig.SetSpriteAndShapeFromMod(ModInfo.Id, "rig");
|
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.name = L.T("item.rig.name");
|
||||||
rig.unitValue = Tuning.RigPrice;
|
rig.unitValue = Tuning.RigPrice;
|
||||||
MachineHelper.SetupBatterySlot(battery, rig);
|
MachineHelper.SetupBatterySlot(battery, rig);
|
||||||
@@ -388,13 +493,6 @@ namespace Decentralized
|
|||||||
cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag);
|
cards.mayInventoryAddItemFunc = (item, _) => GeneralHelper.IsItemOwned(item) && IsGpu(item) && !item.IsTag(DeadTag);
|
||||||
cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards);
|
cards.onSlotAddItemEndFunc = (_, _, _) => UpdateRig(rig, modules, cards);
|
||||||
cards.onSlotRemoveItemEndFunc = (_, _, _) => 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.onLoaded = _ => { ApplySlots(rig, cards); UpdateRig(rig, modules, cards); };
|
||||||
rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, modules, cards));
|
rig.onCycleEndEarlySlotItemFunc = (_, _, _) => Log.Guard("矿架扣电", () => NightDraw(rig, battery, modules, cards));
|
||||||
ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name);
|
ModHelper.SetSource(rig, ModInfo.Id, ModInfo.Name);
|
||||||
@@ -410,18 +508,86 @@ namespace Decentralized
|
|||||||
rig.contentWindow?.Validate();
|
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 slots = SlotsOf(rig);
|
||||||
int progress = rig.IsTag(ProgressTag) ? rig.GetTagReadonly(ProgressTag).GetInt() : 0;
|
int progress = rig.IsTag(ProgressTag) ? rig.GetTagReadonly(ProgressTag).GetInt() : 0;
|
||||||
int before = slots;
|
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;
|
if (took <= 0) return;
|
||||||
rig.ModifyTag(SlotsTag, s => s.Enable().SetInt(slots));
|
rig.ModifyTag(SlotsTag, s => s.Enable().SetInt(slots));
|
||||||
rig.ModifyTag(ProgressTag, s => s.Enable().SetInt(progress));
|
rig.ModifyTag(ProgressTag, s => s.Enable().SetInt(progress));
|
||||||
// 消耗收下的件数;放不下的部分留在升级槽里(玩家取回)
|
// 消耗收下的件数;放不下的部分留在升级槽里(玩家取回)
|
||||||
if (took >= item.unitCount) { upgradeSlot.Expel(item); item.Destroy(); }
|
if (item.unitCount <= 1) item.Destroy(); else item.GetSplitItem(1).Destroy();
|
||||||
else item.GetSplitItem(took).Destroy();
|
|
||||||
if (slots != before)
|
if (slots != before)
|
||||||
{
|
{
|
||||||
ApplySlots(rig, cards);
|
ApplySlots(rig, cards);
|
||||||
@@ -457,6 +623,8 @@ namespace Decentralized
|
|||||||
double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(b));
|
double hash = Cards(cards).Select(ToUnit).Sum(u => u.Hash(b));
|
||||||
int slots = SlotsOf(rig);
|
int slots = SlotsOf(rig);
|
||||||
rig.shortDescription = L.T("item.rig.desc", slots, Fmt.Num(hash * 10), kj);
|
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);
|
var next = RigSlots.Next(slots);
|
||||||
if (next != null)
|
if (next != null)
|
||||||
{
|
{
|
||||||
@@ -476,6 +644,8 @@ namespace Decentralized
|
|||||||
/// <summary>夜间早段:按矿架上的显卡总耗电(含模块效果)从电池扣电;扣电成功,登记今晚参与挖矿的显卡和模块效果。</summary>
|
/// <summary>夜间早段:按矿架上的显卡总耗电(含模块效果)从电池扣电;扣电成功,登记今晚参与挖矿的显卡和模块效果。</summary>
|
||||||
private static void NightDraw(GameItem rig, GameSlotInventory battery, GameInventory modules, GameInventory cards)
|
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();
|
var list = Cards(cards).Where(c => !c.IsTag(DeadTag)).ToList();
|
||||||
Session.TonightRigs++;
|
Session.TonightRigs++;
|
||||||
if (list.Count == 0) return;
|
if (list.Count == 0) return;
|
||||||
@@ -484,7 +654,19 @@ namespace Decentralized
|
|||||||
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return;
|
if (battery.childItems.Count == 0 || !PowerHelper.DrawPowerSource(battery.childItem, kj)) return;
|
||||||
Session.TonightPoweredRigs++;
|
Session.TonightPoweredRigs++;
|
||||||
Session.TonightKj += kj;
|
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);
|
UpdateRig(rig, modules, cards);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +674,38 @@ namespace Decentralized
|
|||||||
/// 放进储藏区(游戏 EmporiumTryAdd 的默认目标,放不下时游戏自己找位置)。
|
/// 放进储藏区(游戏 EmporiumTryAdd 的默认目标,放不下时游戏自己找位置)。
|
||||||
/// 不放柜台:测试中柜台上的物品过夜后会消失(未在正常流程中确认);ModHelper.MoveItemTo* 放满时会静默失败。
|
/// 不放柜台:测试中柜台上的物品过夜后会消失(未在正常流程中确认);ModHelper.MoveItemTo* 放满时会静默失败。
|
||||||
/// </summary>
|
/// </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)
|
public static void Give(GameItem item)
|
||||||
{
|
{
|
||||||
GeneralHelper.SetItemOwned(item, true);
|
GeneralHelper.SetItemOwned(item, true);
|
||||||
|
|||||||
@@ -12,10 +12,19 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
Items.Subscribe();
|
Items.Subscribe();
|
||||||
Items.OpenTerminal = TerminalWindow.Open;
|
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();
|
Unlock.Register();
|
||||||
Night.Subscribe();
|
Night.Subscribe();
|
||||||
DealerStock.Subscribe();
|
DealerStock.Subscribe();
|
||||||
UsedDealerClient.Subscribe();
|
UsedDealerClient.Subscribe();
|
||||||
|
CoinCustomers.Subscribe();
|
||||||
|
OtcClient.Subscribe();
|
||||||
|
BlackDelivery.Subscribe();
|
||||||
|
TumblerStory.Subscribe();
|
||||||
GpuInspectView.Ensure();
|
GpuInspectView.Ensure();
|
||||||
ModHook.OnGameLoadedLate += Session.Load;
|
ModHook.OnGameLoadedLate += Session.Load;
|
||||||
ModHook.OnNewGameStarted += OnNewGame;
|
ModHook.OnNewGameStarted += OnNewGame;
|
||||||
@@ -28,7 +37,13 @@ namespace Decentralized
|
|||||||
Night.Unsubscribe();
|
Night.Unsubscribe();
|
||||||
DealerStock.Unsubscribe();
|
DealerStock.Unsubscribe();
|
||||||
UsedDealerClient.Unsubscribe();
|
UsedDealerClient.Unsubscribe();
|
||||||
|
CoinCustomers.Unsubscribe();
|
||||||
|
OtcClient.Unsubscribe();
|
||||||
|
BlackDelivery.Unsubscribe();
|
||||||
|
TumblerStory.Unsubscribe();
|
||||||
GpuInspectView.Remove();
|
GpuInspectView.Remove();
|
||||||
|
WalletDriver.Remove();
|
||||||
|
ModHook.OnModAssetsLoaded -= GpuGuide.OnAssetsLoaded;
|
||||||
ModHook.OnGameLoadedLate -= Session.Load;
|
ModHook.OnGameLoadedLate -= Session.Load;
|
||||||
ModHook.OnNewGameStarted -= OnNewGame;
|
ModHook.OnNewGameStarted -= OnNewGame;
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 500 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 420 B |
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 388 B |
|
Before Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 447 B |
|
Before Width: | Height: | Size: 428 B |
|
Before Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 590 B |
|
After Width: | Height: | Size: 577 B |
|
After Width: | Height: | Size: 591 B |
|
After Width: | Height: | Size: 595 B |
|
After Width: | Height: | Size: 513 B |
|
After Width: | Height: | Size: 510 B |
|
After Width: | Height: | Size: 592 B |
|
After Width: | Height: | Size: 596 B |
|
Before Width: | Height: | Size: 502 B |
|
Before Width: | Height: | Size: 507 B |
|
Before Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 593 B |
|
Before Width: | Height: | Size: 596 B |
|
Before Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 592 B |
|
Before Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 849 B |
|
After Width: | Height: | Size: 862 B |