Mod: mining rig, graphics cards, trading terminal, nightly settlement

First playable slice, tested in game on the test save:
- Network upgrade "Mining Access" (300) delivers a trading terminal, a
  mining rig and an RZX 50.
- The rig is a machine with a battery slot and four card slots; each night
  it draws its total power from the battery and its cards mine PSCoin.
  Cards show hashrate, power draw and condition, wear every night and can
  burn out. All state lives on item tags or in the mod save data.
- Trading terminal with wallet, trade (four pairs, deposit credits as CRC,
  withdraw CRC through the compliance desk) and report pages.
- Nightly log lines; exchange opening and halving news on the paper.
- English and Chinese text; sprites generated by art/sprites.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-03 09:11:46 +08:00
1 parent efc58888f3
commit 750b26e4b3
21 files changed
+1040

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
using System;
using System.Globalization;
using System.Linq;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>交易终端窗口:钱包、交易、报告三页。每次操作后按当前页重建窗口。</summary>
public static class TerminalWindow
{
public const string Id = "pd_terminal";
private enum Page { Wallet, Trade, Report }
private static Page _page = Page.Wallet;
private static string _amount = "";
private static string _message;
public static void Open()
{
if (Session.Data == null) return;
var b = CustomUIManager.Window(Id, L.T("term.title"));
if (b == null) return;
b.SetSize(620, 560).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
b.BeginRow();
foreach (Page p in Enum.GetValues(typeof(Page)))
{
var page = p;
string name = L.T("tab." + p.ToString().ToLowerInvariant());
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; Open(); });
}
b.End();
b.AddSeparator();
b.BeginScroll(440);
switch (_page)
{
case Page.Wallet: WalletPage(b); break;
case Page.Trade: TradePage(b); break;
default: ReportPage(b); break;
}
b.End();
if (!string.IsNullOrEmpty(_message)) b.AddLabel(_message);
b.Show();
}
private static void WalletPage(CustomUIBuilder b)
{
var w = Session.Data.Hot;
if (w.Balances.Count == 0) b.AddLabel(L.T("wallet.empty"));
foreach (var kv in w.Balances.OrderBy(k => k.Key))
b.AddLabel(L.T("wallet.line", kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))));
b.AddSeparator();
b.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
}
private static void TradePage(CustomUIBuilder b)
{
b.AddLabel(L.T("trade.hint"));
b.BeginRow();
b.AddLabel(L.T("trade.amount"));
b.AddInput("0", _amount, s => _amount = s, "amount", s => _amount = s);
b.End();
b.BeginRow();
b.AddButton(L.T("trade.deposit"), Deposit);
b.AddButton(L.T("trade.withdraw"), Withdraw);
b.End();
b.AddSeparator();
foreach (var pool in Session.Data.Market.Pools)
{
var p = pool;
b.AddLabel(L.T("trade.pair", p.Base, p.Quote, L.T("venue." + p.Venue)) + " " + L.T("trade.price", Fmt.Num(p.Price)));
b.BeginRow();
b.AddButton(L.T("trade.buy"), () => Trade(p, buy: true));
b.AddButton(L.T("trade.sell"), () => Trade(p, buy: false));
b.End();
}
}
private static void ReportPage(CustomUIBuilder b)
{
var r = Session.Data.LastNight;
if (r == null) { b.AddLabel(L.T("report.none")); return; }
b.AddHeader(L.T("report.day", r.Day));
b.AddLabel(L.T("report.rigs", r.PoweredRigs, r.Rigs));
b.AddLabel(L.T("report.hash", Fmt.Num(r.Hash * 10), Fmt.Num(r.NetworkHash * 10)));
b.AddLabel(L.T("report.mined", Fmt.Num(r.Mined), Fmt.Credits(r.Mined * r.Price)));
b.AddLabel(L.T("report.energy", Fmt.Num(r.Kj)));
b.AddLabel(L.T("report.burnt", r.Burnt.Count));
foreach (var n in r.Burnt) b.AddLabel(" " + n);
b.AddLabel(L.T("report.price", Fmt.Num(r.Price)));
}
private static bool ReadAmount(out double v)
{
if (!double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) || v <= 0)
{
_message = L.T("msg.amount");
Open();
return false;
}
return true;
}
private static void Deposit()
{
if (!ReadAmount(out double v)) return;
int credits = (int)Math.Floor(v);
if (credits <= 0 || PlayerStore.Instance.playerCash < credits) { _message = L.T("msg.balance"); Open(); return; }
ModHelper.AddCredit(-credits, false);
var w = Session.Data.Hot;
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
w.Basis += credits;
_message = L.T("msg.deposit", credits);
Session.Save();
Open();
}
private static void Withdraw()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
if (d.Hot.Get(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var r = d.Exchange.Cashout(d.Hot, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), false, d.Rng);
int got = (int)Math.Floor(r.Received - r.Tax);
ModHelper.AddCredit(got, true);
_message = L.T("msg.cashout", got, Fmt.Credits(r.Tax));
if (r.Outcome == AmlOutcome.Light) _message += "\n" + L.T("msg.aml_light", Fmt.Credits(r.Confiscated));
if (r.Outcome == AmlOutcome.Heavy)
{
_message += "\n" + L.T("msg.aml_heavy", Fmt.Credits(r.Confiscated));
PlayerStore.Instance.secData.CommitCrime("FENCING", (int)Math.Round(r.Crime));
}
Session.Save();
Open();
}
private static void Trade(Pool p, bool buy)
{
if (!ReadAmount(out double v)) return;
var w = Session.Data.Hot;
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
if (w.Get(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
double got = buy ? p.BuyBase(v) : p.SellBase(v);
w.Add(pay, -v);
w.Add(get, got);
_message = L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
Session.Save();
Open();
}
}
}