CatGunner CookbookBardCat 1.1.59 · L1 ANALYSIS · tài liệu học
Trang chủ › Cookbook › 02 — Kinh tế & số lớn: InfVal, chuỗi nhân, và bảng bake bằng lặp cắt cụt
Nguồn: 02-kinh-te-so-lon.md · Cập nhật lần cuối: 2026-09-11 21:12 +07 · sha256 nguồn 3cf5df592672

02 — Kinh tế & số lớn: InfVal, chuỗi nhân, và bảng bake bằng lặp cắt cụt

Nguồn framework: docs/framework/01-economy-money.md §2–§6, docs/framework/09-prestige-rebirth.md §3.2 (bảng cân bằng thật), [phân tích nội bộ] (hai nhà phân tích hội tụ), gap list (nội bộ) G138a / G142 / G143. Cấp bằng chứng: L1 ANALYSIS — dựng lại clean-room từ disasm + metadata + quan sát runtime; không phải source gốc. Mỗi dòng bên dưới mang nhãn bằng chứng riêng.

Đính chính quan trọng: doc 01 §2 (viết trước) có hai dòng đã bị doc 09 §3.2 + CONVERGENCE + G138a thay thế: (a) ToPrecision(n) CẮT về phía 0, không làm tròn; (b) Balance_Reload là dead code — bảng thật là mảng bake sẵn trong Main.unity, sinh bằng lặp có cắt cụt. Trang này dùng phiên bản mới. Mô hình tham chiếu economy_reference_sim.py tái tạo mảng scene 0/360 lệch với mô hình cắt (mục [M]).

Mục tiêu

Muốn một game idle có tiền tăng vượt double, nhân qua nhiều nguồn multiplier mà không sửa công thức mỗi lần thêm hệ thống, hiển thị gọn trên điện thoại (134.58b), và — quan trọng nhất — bảng cân bằng bạn ship phải đúng bằng bảng designer nghĩ. CatGunner giải quyết trọn bộ này, và cũng để lại một bài học đắt: công thức trong code và bảng trong game không giống nhau.

Game gốc làm gì

Điều game làm Con số / hằng số Mức bằng chứng Nguồn
Kiểu số lớn là package thương mại InfiniteValue.InfVal (Asset Store 194963), không tự viết bignum value = _digits × 10^s_exponent; serialize string s_digits rồi int s_exponent package-source + scene-census doc 01 §2 (G007, G122)
Precision mặc định của đường kinh tế 5 chữ số có nghĩa (20 → (20000,-3), 152.2 → (15220,-2)) runtime-verified doc 01 §2 (parity samples)
ToPrecision(n) cắt về n chữ số (không làm tròn) code 0x44E0E5C → 0x44DF888; 1 202 phần tử mảng scene khớp 100 % mô hình cắt, 0 % mô hình tròn VERIFIED doc 09 §2; CONVERGENCE U3
Tiền tệ là static field trên Gamemanager Money @+0x28, Money_Accum @+0x48, Ruby @+0x68 (InfVal); PetCoin @+0xB8 (int); Dia là ObscuredInt disasm-proven doc 01 §2
4 event đổi tiền tệ OnMoneyChanged / OnDiaChanged / OnRubyChanged / OnPetCoinChanged (static Action, field-like event) — HUD số dư làm mới chỉ qua chúng; bản dựng lại từng có 14 event bất động do công cụ decompile render accessor rỗng (G376) disasm-proven doc 01 §2
Get_Money(v) v' = MoneyValue_return(v); Money += v'; Money_Accum += v'; OnMoneyChanged?.Invoke() — 85 lần / 40 s disasm-proven + runtime-verified doc 01 §2 (RVA 0x2A4ECBC)
MoneyValue_return là chuỗi nhân thu nhập Upgrade × Rebirth × Skin × Pet × Relic (mỗi cái /100) rồi × 3 nếu Money_Buff disasm-proven + runtime-verified doc 01 §3.2 (RVA 0x2A4E7B0)
Chuỗi là thuần nhân, không có cộng/offset 20 → 152.2, 60 → 456.6 ⇒ hệ số ×7.61 ổn định; 25 mẫu đều khớp k·x runtime-verified doc 01 §2, §6
Cache LRU cho chuỗi nhân, khoá theo giá trị input Dictionary<string,InfVal> + Queue<string>, trần 50 (MoneyCacheMaxSize const) disasm-proven + metadata-proven doc 01 §2, §3.3
Cache phải bị xoá khi bất kỳ multiplier nào đổi Gamemanager.InvalidateMoneyCache() xoá cả dictionary lẫn queue disasm-proven doc 01 §3.3 (RVA 0x2A4E324)
Lose_Money clamp bằng ((InfVal)0).ToPrecision(5); Lose_Ruby clamp bằng (InfVal)0 trần bất đối xứng thật trong binary disasm-proven doc 01 §2 (RVA 0x2A4F6E4 / 0x2A4FACC)
Money_Accum chỉ tăng (wallet vs lifetime) — nhưng bị reset khi Rebirth Money_Reset() đặt cả hai về 0 disasm-proven doc 01 §3.4, §6 (RVA 0x2A4F1F8)
Dia (tiền premium): Get_Dia cộng v vào ví và v × 3 vào sổ gem gửi máy chủ (_getDia); Lose_Dia phản chiếu vào _useDia, không kẹp ở 0; Lose_PetCoin thì có kẹp ×3 disasm-proven doc 01 §2 (G395)
Save() của game throttle 5 s thời gian thực (nhiều biến đổi tiền tệ liên tiếp không ghi đĩa dồn dập) 5 s disasm-proven doc 01 §2 (G395)
Notation hiển thị: ≤ 3 chữ số có nghĩa + đơn vị chữ cái thường, bước 3 bậc InfVal_change(): mag = exp + len(digits); if mag>=4: mag=(mag-1)%3+3; ToString(mag) — ví dụ 297a, 203.09a disasm-proven + runtime-verified doc 01 §3.5 (RVA 0x44E47D4; 14 cặp parity)
Formatter hiển thị cắt (45660,-2) = 456.6 → "456" (làm tròn sẽ ra 457) runtime-verified (1 mẫu phân biệt) doc 01 §3.5
Đơn vị viết HOA ở text bay trong thế giới, thường ở HUD — cùng giá trị 3.12A (text sát thương) vs 3.12a (panel nâng cấp), 149.75A (tiền bay) vs 134.58b (số dư) observed doc 01 §3.5.1 (ảnh user_screenshot_stage5-4.png)
Throttle floating text theo ô lưới không gian BUCKET_SIZE = 1.5, DAMAGE_TEXT_COOLDOWN = 0.12, MONEY_TEXT_COOLDOWN = 0.2; CanSpawnText 287 lần / 40 s metadata-proven + runtime-observed doc 01 §2 (dump.cs:583820)
Bảng cân bằng lúc chạy = mảng serialize trong scene, không phải hàm sinh Upgrade_manager Price/Power/Money/Speed @Main.unity:20353425/20373428/20393431/20413434; Rebirth_manager Value ×3 @20451299/20491308/20531317, Price @20571320 scene-YAML doc 09 §2, §3.2
Balance_Reload không bao giờ chạy trong 1.1.59 CodeRefsTo = [] cho 0x2B4A02C (Upgrade), 0x2B21CB4 (Rebirth), 0x2B0AB4C (Gun); không string literal, không UnityEvent trong scene VERIFIED doc 09 §2; CONVERGENCE; G138a
Mảng Money/Power của upgrade sinh bằng lặp có cắt cụt [0]=100; [i]=trunc3([i-1]·1.08) — tái tạo 0/60 lệch VERIFIED doc 09 §3.2
Giá upgrade dùng double nhị phân 1.38 acc=50; acc=trunc4(acc·1.38); price=trunc3(int(acc)) ⇒ [1] = 68, không phải 69 (vì 1.38 = 1.3799999999999998…) VERIFIED doc 09 §2
Sai lệch tích luỹ so với dạng đóng 100·1.08^lv lv 10: 210 vs 215.89 (−2.73 %); lv 46: 3 120 vs 3 447 (−9.50 %); lv 50: −10.24 %; lv 1 000: ~6.5 lần VERIFIED doc 09 §3.2
Ảnh chụp save thật khớp mảng scene 4/4 Speed[46] = 468, Power[46] = Money[46] = 3 120 → 3.12a, Price[46] = 1.34×10⁸ → 134b observed + scene-YAML doc 09 §3.2
Mô hình tham chiếu Python thuần tự kiểm 58/58 vector (hạng A oracle-live + hạng B dẫn xuất) + mục [M] mảng scene 0/360 lệch derived doc 01 §5.6; doc 09 §3.2
Liveops không ghi đè kinh tế qua kênh nào ServerConfig 15 field toàn ads; Firebase RC 5 key version/offerwall; PlayFab không TitleData VERIFIED (G144 còn mở) LIVEOPS_SURFACE §0; CONVERGENCE U4

Công thức / pseudocode

1. Kiểu số (mô hình hoá InfVal) — L1 ANALYSIS, doc 01 §3.1 / §5.2

BigNum { digits: BigInteger, exponent: int }      value = digits × 10^exponent
precision = 5 chữ số có nghĩa                     (bằng chứng: (20000,-3), (15220,-2))
serialize = (string digits, int exponent)          khớp s_digits / s_exponent của SOT
ToPrecision(n)  → CẮT về n chữ số có nghĩa (về phía 0)      [VERIFIED: 0x44E0E5C, doc 09]
ToString(n)     → CẮT, dùng cho hiển thị                     [runtime-verified: 456.6 → "456"]

2. Chuỗi thu nhập — L1 ANALYSIS, RVA 0x2A4E7B0 (runtime-verified)

private static InfVal MoneyValue_return(InfVal value)
{
    string key = GetMoneyCacheKey(value);
    if (_moneyCache != null && _moneyCache.TryGetValue(key, out InfVal cached))
        return cached;                                          // hit — không tính lại

    InfVal r = value * Upgrade_manager.Money_return() / 100;   // Money_balance[Lv]  (mảng scene) — RESET khi rebirth
    r = r * Rebirth_manager.Money_return() / 100;              // Value_balance[2][Lv] + 100     — BỀN qua rebirth
    r = r * Skin_manager.Money_Accum       / 100;              // int %
    r = r * Pet_manager.Pet_Money_Accum    / 100;              // InfVal %
    r = r * Relic_manager.Money_Value      / 100;              // InfVal %
    if (Buff_manager.instance != null &&
        Buff_manager.instance.IsBuffActive(AD_Type.Money_Buff))
        r = r * 3;                                             // buff quảng cáo: ×3 cứng, KHÔNG phải +200 %

    if (_moneyCache != null) {                                 // LRU trần 50
        if (_moneyCache.Count >= 50) _moneyCache.Remove(_moneyCacheKeys.Dequeue());
        _moneyCache[key] = r;  _moneyCacheKeys.Enqueue(key);
    }
    return r;
}
// Gamemanager.InvalidateMoneyCache() [0x2A4E324] xoá cả hai — BẮT BUỘC gọi sau khi mua upgrade / đổi skin / pet / relic / buff.

Gợi ý framework (doc 01 §4, không phải SOT): mỗi nguồn là một IMultiplierSource.PercentFor(kind) trả 100 = trung tính; pipeline sources.Aggregate(base, (acc, s) => acc * s.PercentFor(kind) / 100); buff/event là tầng riêng nhân sau và phải đi qua service fail-closed; source bắn event để cache tự invalidate thay vì trông chờ call-site nhớ.

3. Formatter hiển thị — L1 ANALYSIS, RVA 0x44E47D4

public readonly string InfVal_change()
{
    int mag = exponent + digits.ToString().Length;   // tổng bậc độ lớn
    if (mag >= 4) mag = (mag - 1) % 3 + 3;           // ép về 3..5, canh theo nhóm nghìn
    return ToString(mag);                            // maxDisplayedDigits; đơn vị a, b, c… bước 3 bậc; CẮT
}
// Ground truth: (100,0)→"100"  (500,-1)→"50"  (297,3)→"297a"  (45660,-2)→"456"  (15220,-2)→"152"  (0,-4)→"0"

4. Bảng cân bằng — BAKE BẰNG LẶP CẮT CỤT, không phải pow() — VERIFIED, doc 09 §3.2

# Tái tạo đúng mảng trong Main.unity (0/360 lệch, lv 0..59 đã đối chiếu) — economy_reference_sim.py mục [M]
def trunc(x, n): ...   # cắt về n chữ số có nghĩa, về phía 0 (ToPrecision của InfVal)

# Upgrade Money_balance / Power_balance (Main.unity:20393431 / :20373428)
v = 100
for i in range(1, N): v = trunc(v * 1.08, 3)          # KHÔNG PHẢI 100 * 1.08**i

# Upgrade Speed_balance (:20413434) — tuyến tính, khớp chính xác
speed[lv] = 100 + 8 * lv

# Upgrade Price_balance (:20353425) — 1.38 là DOUBLE NHỊ PHÂN 1.3799999999999998…
acc = 50
for i in range(1, N):
    acc = trunc(acc * 1.38, 4)
    price[i] = trunc(int(acc), 3)                     # price[1] = 68, không phải 69

# Rebirth Value_balance[0] (Damage) và [2] (Money) (:20451299 / :20531317)
value[lv] = trunc(int(trunc(1.05**lv * 100 - 100, 3)), 3)   # ≈ 1.05^lv nhưng LUÔN thấp hơn
# Rebirth Value_balance[1] (Speed) (:20491308)
value[lv] = 5 * lv
# Rebirth Price_balance (Ruby, :20571320)
price[lv] = trunc(int(trunc(3 * (lv + 1) ** 1.6, 4)), 3)

Bảng lệch (doc 09 §3.2, đọc thẳng từ mảng scene):

lv Money_balance mảng thật 100·1.08^lv lệch
1 108 108.00 0 %
2 116 116.64 −0.55 %
10 210 215.89 −2.73 %
46 3 120 3 447.41 −9.50 %
50 4 210 4 690.16 −10.24 %
1 000 4.06e34 2.65e35 ~6.5 lần

Sai số luôn một chiều (thấp hơn) vì cắt không bao giờ làm tăng.

4b. Bảng chỉ số theo HẠNG — hàm mũ hai cơ số + làm tròn theo chữ số có nghĩa (doc 01 §4 mục 8–9)

Bảng ở §4 là bảng cấp nâng cấp (hàng trăm mức, bake bằng lặp cắt cụt). Còn khi bạn cần một bảng hạng vật phẩm ngắn (10–13 hạng: pet, cá, súng), khuôn đã chạy thật đơn giản hơn nhiều — và mỗi trục chỉ số được một cơ số riêng:

# 13 hạng cá — công thức thật, đã đối chiếu với mảng bake bằng hai tổng đọc trên máy
for i in range(13):
    Damage = round_sig(4  * 3   ** i, 2)          # trục CHÍNH,  cơ số 3
    CriPer = clamp(round_sig(     2.5 ** i, 2), 0, 100)   # trục PHỤ, cơ số 2.5, có TRẦN
    CriVal = round_sig(8  * 2.5 ** i, 2)

def round_sig(x, digits=2):                       # 2 chữ số có nghĩa
    if x == 0: return 0
    exp   = floor(log10(abs(x)))
    scale = 10 ** (digits - exp - 1)
    return int(round(round(x * scale) / scale))   # LÀM TRÒN ToEven: 125 -> 120, không phải 130
hạng 0 1 2 3 4 5 6 7 8 9 10 11 12
Damage % 4 12 36 110 320 970 2900 8700 26000 79000 240000 710000 2100000
CriPer % 1 2 6 16 39 98 100 100 100 100 100 100 100
CriVal % 8 20 50 120 310 780 2000 4900 12000 31000 76000 190000 480000

Ba thứ đáng chép:

  1. round_sig(x, 2) chứ không phải số thô. Bảng đọc lên như bảng do người viết (110, 320, 970) chứ không phải 108, 324, 972. Chi phí: sáu dòng. Nhớ khai báo tường minh là làm tròn ToEven, không phải cắt như ToPrecision ở §4 — hai thao tác khác nhau, dùng lẫn là ra bảng khác (xem "Bẫy đã gặp").
  2. Hai cơ số tách hai trục ra theo thời gian. 3^12 ≈ 531 000 so với 2.5^12 ≈ 60 000: sau 12 bậc trục chính bỏ xa trục phụ ~9 lần. Bạn chọn tốc độ tương đối giữa các chỉ số bằng một con số mỗi trục, không phải bằng 13 ô bảng gõ tay.
  3. Chỉ số phụ nên có TRẦN, và trần nên nằm GIỮA bảng. CriPer chạm 100 ngay từ hạng 6 ⇒ một nửa bảng không cộng thêm một điểm tỉ lệ chí mạng nào. Cố ý: nó ngăn con số vô nghĩa ("chí mạng 400%") mà vẫn giữ công thức gọn, và rút gọn không gian quyết định theo thời gian — đầu game người chơi cân ba chỉ số, cuối game chỉ còn một trục. Kỹ thuật: clamp phải nằm trong hàm sinh (bảng bake đã sạch) và lặp lại ở chỗ hiển thị TỔNG — tổng của nhiều vật phẩm vẫn vượt trần dù mỗi vật phẩm đã bị chặn.

Mẹo đối chiếu rẻ nhất (khi không muốn mở file scene hàng trăm MB để đọc mảng bake): chọn một hồ sơ save có các hạng khác nhau rồi so TỔNG hiển thị trên UI, không so từng ô. Với kho [0, 0, 1, 2, 5], bảng trên cho tổng Damage = 1026 và tổng CriVal = 866; màn hình in +1.02a% và 991% (= 866 + hằng nền 125). Hai tổng độc lập khớp là đủ để kết luận mảng bake đúng bằng công thức — mà chỉ tốn một lần chụp màn hình.

5. Ví tiền — hợp đồng hành vi (doc 01 §5.3)

GetMoney(v)        : v' = Chain(v); Money += v'; MoneyAccum += v'; fire(OnMoneyChanged); return v'
GetMoney(v, pos)   : v' = GetMoney(v); SpawnText(v'.ToDisplayString(), pos)     [text bay trong thế giới]
LoseMoney(v)       : Money -= v; if (Money < 0) Money = Zero.ToPrecision(5); fire()
MoneyReset()       : Money = 0; MoneyAccum = 0; fire()                          [gọi bởi Rebirth()]
GetRuby / LoseRuby : Ruby ±= v; if (Ruby < 0) Ruby = 0   [KHÔNG ToPrecision]; fire()
LosePetCoin        : clamp >= 0; có call-site analytics (R1 → IAnalyticsService)

Bẫy đã gặp

  • Dùng pow() / dạng đóng để sinh bảng. Mô hình đầu tiên của chính dự án này dùng công thức + làm tròn: dự đoán panel Lv.46 là 3.5a và giá 136b; ảnh chụp thật là 3.12a và 134b. Sai 2/3 ô — và chính hai ô lệch đó lật lại mô hình (doc 09 §3.2, D008/D010). Bảng là nguồn sự thật; hàm sinh chỉ là công cụ Editor. Với restore: giữ nguyên mảng serialize, đừng gọi Balance_Reload.
  • Nhầm hai thao tác "làm tròn" và "cắt". ToPrecision(n) (số học) và ToString(n) (hiển thị) đều cắt trong SOT; doc 01 §2/§3.5 còn ghi ToPrecision làm tròn — đó là dòng cũ, đã bị doc 09 §2 + CONVERGENCE U3 thay thế. Một InfVal khác version (làm tròn) sẽ sinh ra một nền kinh tế khác SOT (G007a: bản store v2.2.0 có thể làm tròn — phải chạy InfVal.ParityTest.cs để chốt).
  • 1.38 không phải 1.38. Hằng số là double nhị phân; Price_balance[1] = 68 chứ không 69. Nếu bạn bake bằng decimal bạn sẽ ra bảng khác.
  • Quên InvalidateMoneyCache() sau khi mua upgrade ⇒ người chơi trả tiền mà thu nhập không tăng cho tới khi key bị đẩy khỏi LRU — bug im lặng, cực khó truy (doc 01 §3.3).
  • Một formatter cho mọi chỗ. SOT hiển thị 3.12A ở text bay và 3.12a ở HUD. Restore chỉ dùng InfVal_change() sẽ sai ở text bay. Đường code viết hoa chưa biết (UNKNOWN-pending) — đừng giả định .ToUpper().
  • Đổi tên field serialize của kiểu số. Main.unity serialize InfVal inline (s_digits/s_exponent) vào hàng nghìn component; nâng version package mà đổi tên field = âm thầm mất toàn bộ số liệu kinh tế (doc 01 §3.1; G122 đã kiểm layout v2.2.0 khớp).
  • Trộn kinh tế với máy móc toàn vẹn. Một nửa Gamemanager (70 method) là chống gian lận thời gian / autosave; tầng kinh tế phải chạy được không cần Unity, không cần mạng — đó là lý do có simulator (doc 01 §5).
  • Stub quảng cáo trả "đã thưởng". Nhánh ×3 chỉ được bật bởi service thật; NullAdService trả NotAvailable (R1, doc 01 §3.6).
  • Lấy bảng số từ CONSTRUCTOR thay vì từ scene. Với field public/[SerializeField] có mặt trong scene, scene THẮNG lúc chạy; ctor chỉ là mặc định khi AddComponent lên một prefab mới. Dự án này sai hai lần vì bỏ qua điều đó: FormationRadius (ctor 10 / scene 2.5) và bảng tỉ lệ gacha cá (ctor {40,30,20,10} / scene {60,30,10,0} — đã publish bảng sai ra dashboard trước khi bị bắt). Luật đọc: public/[SerializeField] có trong scene ⇒ scene thắng; private không [SerializeField] ⇒ ctor là giá trị thật (scene không cứu được); const/static readonly ⇒ ctor thắng. Với bảng chỉ số cá (§4b) thì hai nguồn khớp nhau — nên bài học không phải "đừng tin ctor" mà là "phải kiểm loại field trước khi tin bất kỳ con số cân bằng nào". Phép thử rẻ khi chưa đọc được dữ liệu gốc: một kiểm tra tần suất — N lượt liên tiếp ra cùng kết quả có xác suất bao nhiêu với mỗi bảng? (0,6⁵ ≈ 7,8% với bảng thật so với 0,4⁵ ≈ 1% với bảng ctor — đủ lệch để đi đọc lại.)

  • Precision của số nguyên khi vào InfVal (G1710, đo cặp cùng save 2026-09-11): bản gốc 1.1.59 tạo new InfVal(int) với precision = số chữ số thật (25 → 2 chữ số), còn gói Infinite Value đời sau đệm lên intSignificantDigits = 10. Khác biệt chỉ lộ ở phép giữ precision: MathInfVal.Pow(n, 2, conservePrecision:true) cắt 25² = 625 về 2 chữ số = 620 ⇒ thưởng Rebirth n=25 là 3.1a (SOT) chứ không phải 3.12a; n=13: 576 vs 609; n=15: 852 vs 871; n=170 (28900, 3 chữ số) không đổi. Bài học: khi mô phỏng số lớn có "precision" đi kèm giá trị, precision của hằng/biến đếm là một phần của công thức — phải đối chiếu từng ctor với binary (decompile 0x44DE55C/0x44DDA20), đừng tin phiên bản gói mới hơn. float/double thì vẫn đệm 9/17 ở cả hai.

Checklist khi tự làm

  • [ ] Tiền là một kiểu bignum theo chữ số có nghĩa, serialize được dạng text; không có double trên đường thu nhập.
  • [ ] Mọi nguồn multiplier trả phần trăm, mặc định 100; thêm hệ thống = đăng ký thêm source, không sửa công thức.
  • [ ] Buff / event / ad là tầng riêng, nhân sau, đi qua service fail-closed.
  • [ ] Cache theo giá trị input, LRU trần nhỏ, và invalidate tự động khi source đổi (source bắn event).
  • [ ] Tách Wallet (tiêu được) khỏi LifetimeEarned (chỉ tăng); quyết định rõ cái nào reset khi prestige.
  • [ ] Formatter là contract có test parity (bộ cặp (giá trị → chuỗi)), quy tắc cắt/làm tròn khai báo tường minh.
  • [ ] Bảng cân bằng: chọn tường minh "đệ quy có lượng tử" hay "dạng đóng", ghi quantizeDigits, và bake ra asset — asset là nguồn sự thật.
  • [ ] Nếu bake, kiểm bằng bộ vector thật (ảnh chụp / oracle) ở ít nhất 3 level cách xa nhau.
  • [ ] Bảng hạng vật phẩm (10–13 hạng): một cơ số mũ cho mỗi trục chỉ số, làm tròn 2 chữ số có nghĩa, và ít nhất một trục phụ có trần chạm giữa bảng (§4b).
  • [ ] clamp của trục phụ phải có ở hai chỗ: trong hàm sinh và ở chỗ cộng tổng để hiển thị.
  • [ ] Đối chiếu bảng bake bằng TỔNG của một hồ sơ nhiều hạng khác nhau, không cần đọc mảng trong scene.
  • [ ] Throttle floating text ở tầng framework (ô lưới + cooldown theo loại text).
  • [ ] Tầng kinh tế chạy độc lập (simulator không đồ hoạ) trước khi cắm vào Unity.

Điều chưa biết (UNKNOWN)

Chép từ doc 01 §6 và doc 09 §5, cập nhật theo CONVERGENCE:

  • Đường code viết HOA đơn vị ở text bay (3.12A) vs thường ở HUD (3.12a): UNKNOWN-pending — hiện tượng đã xác nhận bằng ảnh, đường code chưa. Khoá bằng disasm Damage_Txt_Spawn 0x2A50594, Critical_Txt_Spawn 0x2A506AC, Money_Txt_Spawn 0x2A4EF48.
  • ToString(int) cắt: đã chốt CẮT nhưng dựa trên một mẫu phân biệt (456.6 → "456"). Khoá bằng mở rộng parity ở giá trị lớn + InfVal.ParityTest.cs.
  • Thứ tự đơn vị sau z (aa, ab… hay A, B) ở bậc rất lớn: UNKNOWN — mẫu hiện tại dừng ở a.
  • ~~GetMoneyCacheKey sinh key chính xác thế nào~~: ĐÃ ĐÓNG (G400) — chuỗi 7 phần giá trị | Upgrade | Rebirth | Skin | Pet | Relic | buff đang bật, tức key liệt kê đúng mọi nguồn multiplier của chuỗi thu nhập ⇒ đổi bất kỳ mắt nào là key khác, cache tự vô hiệu (bổ sung cho InvalidateMoneyCache). Stub trả null của nó từng làm tra cache ném và huỷ khoản trả offline trên thiết bị (G394).
  • Phân rã hệ số ×7.61 theo từng nguồn: UNKNOWN — chỉ đo được tích, trên một save đã tiến xa không ghi lại state (G142). Lần oracle sau phải log 5 getter Money_return + IsBuffActive cùng lúc.
  • Version InfVal chính xác mà SOT ship: UNKNOWN — chỉ biết "cũ hơn 2.2.0, có API unitsList / DisplayOption / InfVal_change".
  • Kinh tế offline là một mô hình riêng (OfflineProgressSimulator, cap MaxOfflineSeconds 28800 + relic Offline_Value·60): đã dựng trọn — harness độc lập tái tạo khoản trả của game gốc 17 chữ số (G382; 12 luật + ví dụ ở 14). Đính chính: Skin/Pet/Relic có áp offline, chỉ buff quảng cáo ×3 không. Fallback Math.Pow(1.08) không thể chạy trong 1.1.59 (doc 01 §5.8). Mô hình tham chiếu economy_reference_sim.py không có mô hình clear-wave nên không cho ra được con số tiền — gap đã ghi (G1000).
  • Cache LRU (_moneyCache, _moneyCacheKeys) và _textCooldowns được cấp trong static ctor của Gamemanager — công cụ decompile bỏ rơi khởi tạo này; bản dựng lại thiếu nó NRE ở lần đầu chạm cache (G319). Không phải UNKNOWN, là bẫy khôi phục đã đóng.
  • Mảng scene chỉ mới đối chiếu lv 0…59; điểm gãy ×1.002 (lv ≥ 14 999) và ×2.0 (lv ≥ 5 000) chỉ tồn tại trong Balance_Reload đã chết — UNKNOWN trong mảng thật ở tầm level đó.
  • Đã giải quyết (không còn UNKNOWN, ghi để khỏi tìm lại): lệch giá 136b vs 134b và panel 3.12a ≠ 3500 trong doc 01 §6 là do mô hình cũ dùng công thức + làm tròn; mảng scene khớp 4/4 (doc 09 §3.2). Money_Accum có bị reset khi rebirth: CÓ. Liveops ghi đè kinh tế: KHÔNG (CONVERGENCE U4), ngoại trừ rủi ro G144 (Google Sheet súng) còn phải probe.

Đọc thêm

  • docs/framework/01-economy-money.md — đặc tả module kinh tế chạy độc lập + simulator (§5)
  • docs/framework/09-prestige-rebirth.md §3.2 — "công thức của designer vs bảng đã ship"
  • [F001: economy_reference_sim.py] — chạy python3 để tự kiểm 58 vector + mục [M]
  • [phân tích nội bộ] — U3 (ToPrecision cắt), U4 (không remote override), dead code
  • [liveops catalog: LIVEOPS_SURFACE.md] §0, §4 — liveops và rủi ro G144
  • 03 Prestige / Rebirth — mắt Rebirth_manager.Money_return() trong chuỗi
  • gap list (nội bộ) — G007, G007a, G122, G129, G138a, G142, G143

Hiện số lớn ra màn hình — luật riêng, dễ bỏ quên

Tính đúng con số mới là nửa việc; nửa còn lại là in nó ra. Cat Gunner giao toàn bộ khâu này cho một ScriptableObject duy nhất (Resources/Configuration.asset của package Infinite Value): 702 đơn vị a…z, aa…zz, trần 5 chữ số, không phân nhóm, không ép mũ. Ba luật đáng chép sang game khác:

  1. Số chữ số dao động 3→5 theo vị trí trong nhóm nghìn (mag = (mag-1)%3+3), nên bề rộng chuỗi gần như không đổi khi số nhảy — đó là thứ làm HUD idle trông "đứng yên".
  2. Cắt là TRUNCATE, không làm tròn: 456.6 với 3 chữ số ra 456. Người chơi không bao giờ thấy 1.00b khi chưa thật sự có 1.00b.
  3. Suy giảm sang dạng mũ chỉ khi hết đơn vị, và bảng đơn vị dài tới mức thực tế là vô hạn.

Triệu chứng khi khâu này hỏng rất dễ chẩn nhầm thành lỗi tính toán: số in ra thành cặp (digits, exponent) — đó là formatter không nạp được, không phải số sai.

Chi tiết + cách nghiệm thu bằng cách so 21 chuỗi với bản gốc trên cùng một save: ../functions/so-lon-hien-thi.md.

Auto-upgrade của Cat Gunner: "nâng ô thấp nhất mua được", và một biến thể im lặng (lô 11)

Khi gói Auto Upgrade bật, mỗi lần panel nâng cấp vẽ lại, game chạy một vòng: chọn ô có cấp THẤP NHẤT trong số ô chưa max và đủ tiền, nâng, lặp tới khi không còn ô nào mua được. Ba điều đáng chép:

  1. Tiêu chí là "cấp thấp nhất", không phải "rẻ nhất" hay "theo thứ tự". Nhờ vậy Power/Speed/Money được kéo đều; người chơi không bao giờ mở panel thấy một chỉ số bỏ xa hai chỉ số kia — cảm giác "auto biết chơi".
  2. Có một biến thể Upgrade_Silent = hàm nâng thường bỏ hết vẽ lại/SFX/anim/huỷ cache; vòng auto gọi nó hàng chục lần rồi mới làm những thứ đó một lần ở cuối đợt (một tiếng "tách", một lần nháy anim cho cả ba ô). Không có biến thể này, auto sẽ kêu như tràng pháo và vẽ lại UI mỗi bậc.
  3. Huỷ cache tiền chỉ khi ô Money thật sự đổi (moneyChanged |= best == 2) — đúng luật đã thấy ở nút nâng tay và ở nâng cấp Rebirth: cache đắt thì chỉ huỷ khi có lý do.

Chi tiết nhỏ nhưng dễ dựng sai: công tắc On/Off là cờ Setting.is_AutoDisable (true = TẮT); tên method nói về "Auto", tên cờ nói về "Disable" — AutoUpgrade_Able() phải ghi false. Và nút cheat "Max Lv" đặt cấp 5000 (= mốc giá bắt đầu tăng dốc), không phải trần 10000 — đọc từ lệnh, không từ tên nút.