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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:29:49 +08:00

113 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Decentralized
{
/// <summary>
/// 借用原版小偷灯招揽换币客户:在原版小偷灯的工厂外包一层,切换顺序末尾加一个目标"换币客户"(解锁换币台后才有)。
/// 原版按灯的目标去公开的顾客表(<see cref="StoreClientListDict.storeClientDict"/>)里造顾客,所以顾客面板、灯的提示、概率都走原版逻辑。
/// 霓虹色:底图换成原版图集里熄灭的暗灯(存档里记的仍是原版的图,卸载后不缺图),上面用原版的图片叠加层
/// (getItemSpriteModifierForItemFunc,运行时委托,不进存档)画黄绿色光晕。原版遇到不认识的目标会点亮成第一种颜色,我们排在它后面改回熄灭的底图。
/// 切换只接管进出我们目标的那一步,其余交给原来的切换函数,别的 mod 往灯上加的目标也照样能切到。
/// </summary>
public static class OtcLamp
{
/// <summary>灯的目标 = 顾客表里的顾客标识。</summary>
public const string Target = "pd_otc";
private const string OverlaySprite = "otc_lamp_on";
/// <summary>原版暗灯所在的图集与熄灭时的图(BlackLampHelper 里是私有常量)。</summary>
private const string LampAtlas = "Items/items_sign", LampOff = "black_lamp";
private static readonly Action<GameItem, GameInventory, SlotMarker> OnShowcaseChanged = (item, _, __) => Log.Guard("展示柜刷新换币灯", () => Refresh(item));
public static bool IsLamp(GameItem item) => item != null && item.identifier == BlackLampHelper.BLACK_LAMP_ID;
public static bool IsOurs(GameItem item) => IsLamp(item) && BlackLampHelper.GetTarget(item) == Target;
/// <summary>在原版小偷灯的工厂外包一层(物品目录初始化时)。</summary>
public static void Wrap(ModItemDirectory dir)
{
try
{
var make = DirectoryMaster.GetFactory<GameItem>(BlackLampHelper.BLACK_LAMP_ID);
if (make == null) { Log.Warn("[OTC] 找不到原版小偷灯"); return; }
dir.Set(BlackLampHelper.BLACK_LAMP_ID, () =>
{
var lamp = make();
var prev = lamp.onActivateSlotItemFunc;
lamp.onActivateSlotItemFunc = (it, inv, slot) => Log.Guard("小偷灯切换", () => Cycle(it, inv, slot, prev));
var prevSprites = lamp.getItemSpriteModifierForItemFunc;
lamp.getItemSpriteModifierForItemFunc = it => Overlay(it, prevSprites);
return lamp;
});
Log.Info("[OTC] 已包装原版小偷灯");
}
catch (Exception ex) { Log.Error("[OTC] 包装小偷灯失败:" + ex); }
}
/// <summary>
/// 切换:当前是"换币客户"时回到原版第一个目标;否则先走原来的切换,原来的切换绕回第一个目标、而且换币台已解锁时,改成"换币客户"。
/// </summary>
private static void Cycle(GameItem lamp, GameInventory inv, SlotMarker slot, Action<GameItem, GameInventory, SlotMarker> prev)
{
string first = BlackLampHelper.TARGETS[0];
if (IsOurs(lamp))
{
SetTarget(lamp, first);
BlackLampHelper.RefreshLampSprite(lamp);
AudioManager.Instance.Play2("click_vintage");
}
else
{
string before = BlackLampHelper.GetTarget(lamp);
prev?.Invoke(lamp, inv, slot);
if (Unlock.OtcUnlocked && before != first && BlackLampHelper.GetTarget(lamp) == first) SetTarget(lamp, Target);
}
Refresh(lamp);
}
public static void SetTarget(GameItem lamp, string target) => lamp.ModifyTag(BlackLampHelper.LAMP_TARGET_TAG, s => { s.Enable().SetString(target); });
/// <summary>目标是"换币客户"、而且灯在展示柜里时,叠一层霓虹灯图(盖住原版退回的点亮图)。</summary>
private static List<ItemSpriteModifier> Overlay(GameItem lamp, Func<GameItem, List<ItemSpriteModifier>> prev)
{
var list = prev?.Invoke(lamp) ?? new List<ItemSpriteModifier>();
if (IsOurs(lamp) && InShowcase(lamp))
{
var sprite = Items.ModSprite(OverlaySprite);
if (sprite != null) list.Add(new ItemSpriteModifier(sprite, Vector2.zero, 0, isFlipped: false, 10, haveMask: false, Color.white));
}
return list;
}
private static bool InShowcase(GameItem item)
{
var sc = EmporiumEntry.Instance?.showcaseElement;
return sc != null && sc.childItems.Contains(item);
}
/// <summary>换币灯在展示柜里:底图改成熄灭的暗灯(光晕由叠加层画);然后重画。</summary>
private static void Refresh(GameItem item)
{
if (!IsLamp(item)) return;
if (IsOurs(item) && InShowcase(item)) item.SetSpriteQuick(LampAtlas, LampOff);
item.Validate();
}
/// <summary>
/// 读档、开新游戏后:在展示柜的放入、取出回调后面挂上刷新(原版在这两个回调里重设灯的底图,我们排在它后面重画叠加层),
/// 先解绑再挂,避免读档几次挂几次;再把展示柜里的灯刷新一遍。
/// </summary>
public static void Attach()
{
var sc = EmporiumEntry.Instance?.showcaseElement;
if (sc == null) return;
sc.onSlotAddItemLateFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Remove(sc.onSlotAddItemLateFunc, OnShowcaseChanged);
sc.onSlotAddItemLateFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Combine(sc.onSlotAddItemLateFunc, OnShowcaseChanged);
sc.onSlotRemoveItemEndFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Remove(sc.onSlotRemoveItemEndFunc, OnShowcaseChanged);
sc.onSlotRemoveItemEndFunc = (Action<GameItem, GameInventory, SlotMarker>)Delegate.Combine(sc.onSlotRemoveItemEndFunc, OnShowcaseChanged);
foreach (var item in sc.childItems) Refresh(item);
}
}
}