543 lines
22 KiB
C#
543 lines
22 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
|
||
// 원작 경제 재구현 — 근거 = _re/spec/core.md (RVA 표기). 수치는 Tables(SOT) 에서만 읽는다.
|
||
namespace VS.Sim
|
||
{
|
||
public enum BuyResult { Ok, NoMoney, Locked, Max, Busy }
|
||
|
||
public partial class Game
|
||
{
|
||
public readonly Tables T;
|
||
public SaveData S;
|
||
public Func<DateTime> Now;
|
||
public double Time; // 진행 시간(초) — 탭 창·하이프·방치 수입용 (Time.unscaledTime 대응)
|
||
|
||
public BigInteger Coin, Follower;
|
||
public decimal CoinDonate, TotalCompletedIncome;
|
||
public float AdsMultiplier = 1f, AutoMultiplier = 1f;
|
||
|
||
decimal idleRemainder;
|
||
double idleAcc;
|
||
readonly Queue<double> coinTaps = new Queue<double>(), hypeTaps = new Queue<double>();
|
||
readonly Random rng;
|
||
|
||
public event Action<string> Log;
|
||
public event Action<decimal> TapEarned; // OnTapEarnView(11)
|
||
public event Action<int> RoomItemUpgraded; // OnUpdateItemRoom(4)
|
||
public event Action<int> SkillUpgraded; // OnUpdateItemSkill(5)
|
||
public event Action RankChanged, HouseChanged, HypeStarted, HypeEnded;
|
||
|
||
RemoteCfg R => T.Cfg.remote;
|
||
InputCfg In => T.Cfg.input;
|
||
|
||
public Game(Tables t, SaveData s, Func<DateTime> now, int seed = 12345)
|
||
{
|
||
T = t; S = s ?? new SaveData(); Now = now ?? (() => DateTime.Now);
|
||
rng = new Random(seed);
|
||
Coin = Big.P(S.coin); Follower = Big.P(S.follower);
|
||
CoinDonate = Big.D(S.coinDonate); TotalCompletedIncome = Big.D(S.totalCompletedIncome);
|
||
if (S.firstPlay == 0) S.firstPlay = Now().Ticks;
|
||
NewRoom();
|
||
InitSystems();
|
||
}
|
||
|
||
public void Commit()
|
||
{
|
||
S.coin = Coin.ToString(); S.follower = Follower.ToString();
|
||
S.coinDonate = Big.S(CoinDonate); S.totalCompletedIncome = Big.S(TotalCompletedIncome);
|
||
S.lastActive = Now().Ticks;
|
||
CommitSystems();
|
||
}
|
||
|
||
void Say(string m) => Log?.Invoke(m);
|
||
long NowT => Now().Ticks;
|
||
|
||
// ================= 방 아이템 =================
|
||
public ItemTable Room => T.Rooms[Math.Min(S.house, Tables.HouseCount - 1)];
|
||
public ItemRow RoomItem(int type) => Room.Items.Find(x => x.Index == type);
|
||
SaveItem FindRoom(int type) => S.rooms.Find(r => r.Type == type);
|
||
public int RoomLevel(int type) { var r = FindRoom(type); return r == null ? -1 : r.Level; }
|
||
public bool RoomUpgrading(int type) { var r = FindRoom(type); return r != null && r.UpgradeEnd > NowT; }
|
||
public double RoomUpgradeRemain(int type) { var r = FindRoom(type); return r == null ? 0 : Math.Max(0, (r.UpgradeEnd - NowT) / 1e7); }
|
||
|
||
/// RVA 0x255C658 — 타이머 진행 중이면 한 단계 낮게 친다(미구매 = −1)
|
||
public int EffRoomLevel(int type)
|
||
{
|
||
int lv = RoomLevel(type);
|
||
if (RoomUpgrading(type)) lv = Math.Max(lv - 1, -1);
|
||
return lv;
|
||
}
|
||
|
||
public LevelRow RoomLevelData(int type, int lv)
|
||
{
|
||
var it = RoomItem(type);
|
||
if (it == null || lv < 0 || lv >= it.Levels.Count) return Tables.EmptyLevel;
|
||
return it.Levels[lv];
|
||
}
|
||
|
||
/// 새 방 첫 상태 — RVA 0x251CF00: 방 자체(아이템 Index 10) 를 House1 = 레벨 9 · 그 외 0 으로 추가 + 그 방 레벨 0 Follower 지급
|
||
void NewRoom()
|
||
{
|
||
if (S.rooms.Count > 0) return;
|
||
S.rooms.Add(new SaveItem { Type = 10, Level = S.house == 0 ? 9 : 0, UpgradeEnd = 0 });
|
||
int row = T.ItemRowIndex(S.house, 10);
|
||
if (row >= 0) Follower += T.RoomFollower[S.house][row][0];
|
||
}
|
||
|
||
/// RVA 0x255E1B8
|
||
public bool RoomUnlocked(int type)
|
||
{
|
||
var it = RoomItem(type);
|
||
if (it == null) return false;
|
||
return it.IndexDependency == -1 || RoomLevel(it.IndexDependency) >= 0;
|
||
}
|
||
|
||
public bool RoomMaxed(int type) { var it = RoomItem(type); return it != null && RoomLevel(type) >= it.Levels.Count - 1; }
|
||
|
||
public BigInteger RoomNextPrice(int type)
|
||
{
|
||
int row = T.ItemRowIndex(S.house, type);
|
||
int next = RoomLevel(type) + 1;
|
||
if (row < 0 || next >= T.RoomPrice[S.house][row].Length) return BigInteger.Zero;
|
||
return DiscountedPrice(T.RoomPrice[S.house][row][next]);
|
||
}
|
||
|
||
/// ItemHouse._OnButtonBuyInternal RVA 0x2431148 (1단계 구매)
|
||
public BuyResult BuyRoomItem(int type)
|
||
{
|
||
var it = RoomItem(type);
|
||
if (it == null || !RoomUnlocked(type)) return BuyResult.Locked;
|
||
int lv = RoomLevel(type);
|
||
if (lv >= it.Levels.Count - 1) return BuyResult.Max;
|
||
if (RoomUpgrading(type)) return BuyResult.Busy; // 원작은 진행 중에도 버튼 상태로 막음(UpdateInfo) — 클론 = 진행 중 구매 불가
|
||
var next = it.Levels[lv + 1];
|
||
int row = T.ItemRowIndex(S.house, type);
|
||
BigInteger price = DiscountedPrice(T.RoomPrice[S.house][row][lv + 1]);
|
||
if (price > Coin) return BuyResult.NoMoney;
|
||
var r = FindRoom(type);
|
||
if (r == null) { r = new SaveItem { Type = type, Level = -1 }; S.rooms.Add(r); }
|
||
if (next.UpgradeTime > 0f) r.UpgradeEnd = Now().AddSeconds(next.UpgradeTime).Ticks;
|
||
Follower += T.RoomFollower[S.house][row][lv + 1];
|
||
r.Level = lv + 1;
|
||
SpendCoin(price);
|
||
OnRoomBought(type, lv, r.Level);
|
||
RoomItemUpgraded?.Invoke(type);
|
||
return BuyResult.Ok;
|
||
}
|
||
|
||
/// 업그레이드 시간 건너뛰기(원작 = 광고/티켓 · 클론 = 시험 지급)
|
||
public void SkipRoomUpgrade(int type) { var r = FindRoom(type); if (r != null) r.UpgradeEnd = 0; }
|
||
|
||
// ================= 경제 공식 =================
|
||
/// RVA 0x255C74C
|
||
public float ItemProductIncome(int n) => R.BaseIncome + R.GrowthIncome * (n - 1) * (float)Math.Pow(R.KeyLevelMulti, n / 25f);
|
||
|
||
public decimal CurrentRoomProductIncome()
|
||
{
|
||
decimal s = 0m;
|
||
foreach (var r in S.rooms) s += (decimal)ItemProductIncome(EffRoomLevel(r.Type) + 1);
|
||
return s;
|
||
}
|
||
|
||
public decimal TotalProductIncome() => TotalCompletedIncome + CurrentRoomProductIncome();
|
||
|
||
public float TotalSkillPassRate()
|
||
{
|
||
float s = 0f;
|
||
foreach (var it in T.Skills.Items) { int e = EffSkillIndex(it.Index); if (e >= 0 && e < it.Levels.Count) s += it.Levels[e].PassRate; }
|
||
return s;
|
||
}
|
||
|
||
public float TotalSkillTapRate()
|
||
{
|
||
float s = 0f;
|
||
foreach (var it in T.Skills.Items) { int e = EffSkillIndex(it.Index); if (e >= 0 && e < it.Levels.Count) s += it.Levels[e].TapRate; }
|
||
return s;
|
||
}
|
||
|
||
/// RVA 0x255B5A4
|
||
public float TotalPassRate()
|
||
{
|
||
float s = 0f;
|
||
foreach (var r in S.rooms) s += RoomLevelData(r.Type, EffRoomLevel(r.Type)).PassRate;
|
||
return (s + S.sumPassRoom + TotalSkillPassRate()) / 10000f + 1f;
|
||
}
|
||
|
||
/// RVA 0x255BE70
|
||
public float TotalTapRate()
|
||
{
|
||
float s = 0f;
|
||
foreach (var r in S.rooms) s += RoomLevelData(r.Type, EffRoomLevel(r.Type)).TapRate;
|
||
return (s + S.sumTapRoom + TotalSkillTapRate()) / 10000f + 1f;
|
||
}
|
||
|
||
/// RVA 0x255B7CC
|
||
public decimal GlobalIncomeBoost(bool includeTemp)
|
||
{
|
||
decimal b = 1m;
|
||
if (includeTemp) b = 1m * ((decimal)AdsMultiplier * (decimal)AutoMultiplier);
|
||
var rk = CurrentRank();
|
||
if (rk != null) b *= rk.RankBoostRate;
|
||
b *= (decimal)ClosetStatRate(ClosetEffLevel()) / 10000m;
|
||
return b;
|
||
}
|
||
|
||
static decimal ClampGps(decimal v) => v < 0.0001m ? 0.0001m : v;
|
||
|
||
/// RVA 0x255B344
|
||
public decimal PassGps() => ClampGps(TotalProductIncome() * (decimal)TotalPassRate() * GlobalIncomeBoost(true) * GiftPassiveMultiplier());
|
||
/// RVA 0x255BC8C
|
||
public decimal TapGps() => ClampGps(TotalProductIncome() * (decimal)TotalTapRate() * (decimal)R.TapMultiplier * GlobalIncomeBoost(true) * GiftTapMultiplier());
|
||
public decimal PassGpsClean() => ClampGps(TotalProductIncome() * (decimal)TotalPassRate() * GlobalIncomeBoost(false));
|
||
public decimal TapGpsClean() => ClampGps(TotalProductIncome() * (decimal)TotalTapRate() * GlobalIncomeBoost(false));
|
||
|
||
/// RVA 0x255CC30 — 가격 인하 선물 효과 중이면 (price × round(f×100) + 50) / 100
|
||
public BigInteger DiscountedPrice(BigInteger price)
|
||
{
|
||
if (!IsGiftEffectActive(GiftTypes.ReducePrice)) return price;
|
||
decimal f = ReducePriceFactor;
|
||
if (f >= 1m) return price;
|
||
var pct = new BigInteger(decimal.Round(f * 100m, 0, MidpointRounding.AwayFromZero));
|
||
return (price * pct + 50) / 100;
|
||
}
|
||
|
||
// ================= 스킬 =================
|
||
SaveItem FindSkill(int type) => S.skills.Find(r => r.Type == type);
|
||
public ItemRow SkillItem(int type) => T.Skills.Items.Find(x => x.Index == type);
|
||
public int SkillLevel(int type) { var r = FindSkill(type); return r == null ? 0 : r.Level; }
|
||
public bool SkillUpgrading(int type) { var r = FindSkill(type); return r != null && r.UpgradeEnd > NowT; }
|
||
public double SkillUpgradeRemain(int type) { var r = FindSkill(type); return r == null ? 0 : Math.Max(0, (r.UpgradeEnd - NowT) / 1e7); }
|
||
|
||
/// RVA 0x255D25C — 레벨 = 구매 횟수 · 데이터 인덱스 = 레벨−1 · 진행 중이면 한 단계 낮게
|
||
public int EffSkillIndex(int type)
|
||
{
|
||
int lv = SkillLevel(type);
|
||
if (SkillUpgrading(type)) lv = Math.Max(lv - 1, 0);
|
||
return lv >= 1 ? lv - 1 : -1;
|
||
}
|
||
|
||
/// RVA 0x255DB98 + 0x255DCCC
|
||
public bool CanBuySkill(int type)
|
||
{
|
||
var it = SkillItem(type);
|
||
if (it == null || it.Levels.Count == 0) return false;
|
||
int lv = SkillLevel(type);
|
||
if (lv < 0 || lv >= it.Levels.Count) return false;
|
||
var d = it.Levels[lv];
|
||
return d.Condition < 1 || d.Condition <= S.house;
|
||
}
|
||
|
||
public bool SkillMaxed(int type) { var it = SkillItem(type); return it != null && SkillLevel(type) >= it.Levels.Count; }
|
||
|
||
public BigInteger SkillNextPrice(int type)
|
||
{
|
||
int row = T.SkillRowIndex(type);
|
||
int lv = SkillLevel(type);
|
||
if (row < 0 || lv >= T.SkillPrice[row].Length) return BigInteger.Zero;
|
||
return DiscountedPrice(T.SkillPrice[row][lv]);
|
||
}
|
||
|
||
/// ItemSkill._OnButtonBuyInternal RVA 0x241AE98 (1단계)
|
||
public BuyResult BuySkill(int type)
|
||
{
|
||
var it = SkillItem(type);
|
||
if (it == null) return BuyResult.Locked;
|
||
if (SkillMaxed(type)) return BuyResult.Max;
|
||
if (!CanBuySkill(type)) return BuyResult.Locked;
|
||
if (SkillUpgrading(type)) return BuyResult.Busy;
|
||
int row = T.SkillRowIndex(type);
|
||
int lv = SkillLevel(type);
|
||
var d = it.Levels[lv];
|
||
BigInteger price = DiscountedPrice(T.SkillPrice[row][lv]);
|
||
if (price > Coin) return BuyResult.NoMoney;
|
||
BigInteger gain = T.SkillFollower[row][lv];
|
||
BigInteger pending = BigInteger.Zero;
|
||
var r = FindSkill(type);
|
||
if (r == null) { r = new SaveItem { Type = type, Level = 0 }; S.skills.Add(r); }
|
||
if (d.UpgradeTime > 0f)
|
||
{
|
||
pending = gain;
|
||
r.UpgradeEnd = Now().AddSeconds(d.UpgradeTime).Ticks;
|
||
r.Pending = (Big.P(r.Pending) + pending).ToString();
|
||
}
|
||
var immediate = gain - pending;
|
||
if (immediate > 0) Follower += immediate;
|
||
r.Level = lv + 1;
|
||
SpendCoin(price);
|
||
OnSkillBought(type, r.Level);
|
||
SkillUpgraded?.Invoke(type);
|
||
return BuyResult.Ok;
|
||
}
|
||
|
||
public void SkipSkillUpgrade(int type) { var r = FindSkill(type); if (r != null) r.UpgradeEnd = 0; }
|
||
|
||
/// 타이머가 끝난 스킬의 보류 팔로워 지급 (RVA 0x251E848)
|
||
void GrantFinishedSkills()
|
||
{
|
||
long now = NowT;
|
||
foreach (var r in S.skills)
|
||
{
|
||
if (r.UpgradeEnd > now) continue;
|
||
var p = Big.P(r.Pending);
|
||
if (p > 0) { Follower += p; r.Pending = "0"; }
|
||
}
|
||
}
|
||
|
||
// ================= 랭크 =================
|
||
public RankRow CurrentRank()
|
||
{
|
||
int i = T.RankRowIndex(S.rankIndex);
|
||
if (i >= 0) return T.Ranks.Items[i];
|
||
return T.Ranks.Items.Count > 0 ? T.Ranks.Items[Math.Max(0, Math.Min(T.Ranks.Items.Count - 1, S.rankIndex - 1))] : null;
|
||
}
|
||
|
||
/// GetIndexRankFollower RVA 0x255F7F4 — 팔로워가 넘은 가장 높은 랭크 Index
|
||
public int RankIndexFor(BigInteger follower)
|
||
{
|
||
int best = T.Ranks.Items.Count > 0 ? T.Ranks.Items[0].Index : 1;
|
||
for (int k = 0; k < T.Ranks.Items.Count; k++)
|
||
if (follower >= T.RankFollower[k] && T.Ranks.Items[k].Index > best) best = T.Ranks.Items[k].Index;
|
||
return best;
|
||
}
|
||
|
||
public bool RankUpPending => RankIndexFor(Follower) > S.rankIndex;
|
||
|
||
public RankRow NextRank()
|
||
{
|
||
int i = T.RankRowIndex(S.rankIndex + 1);
|
||
return i >= 0 ? T.Ranks.Items[i] : null;
|
||
}
|
||
|
||
/// RankUpPopup.ApplyRankUp RVA 0x25E3450 — 사이 랭크 보상 합산 → 코인 지급 · 반환 = 지급액
|
||
public BigInteger ApplyRankUp()
|
||
{
|
||
int target = RankIndexFor(Follower);
|
||
if (target <= S.rankIndex) return BigInteger.Zero;
|
||
BigInteger total = BigInteger.Zero;
|
||
for (int idx = S.rankIndex + 1; idx <= target; idx++)
|
||
{
|
||
int k = T.RankRowIndex(idx);
|
||
if (k < 0) continue;
|
||
total += T.RankReward[k];
|
||
S.rankIndex = idx;
|
||
}
|
||
if (total > 0) AddCoin(total);
|
||
RankChanged?.Invoke();
|
||
return total;
|
||
}
|
||
|
||
// ================= 의상(Closet) 레벨 =================
|
||
public bool ClosetUpgrading => S.closetUpgradeEnd > NowT;
|
||
public double ClosetUpgradeRemain => Math.Max(0, (S.closetUpgradeEnd - NowT) / 1e7);
|
||
/// ClosetService.GetEffectiveLevel RVA 0x23EAC70
|
||
public int ClosetEffLevel() { int lv = S.closetLevel; if (lv > 0 && ClosetUpgrading) lv -= 1; return lv; }
|
||
public int ClosetMaxLevel => T.Clothes.Levels.Count;
|
||
|
||
/// RVA 0x255CA14
|
||
public int ClosetStatRate(int lv)
|
||
{
|
||
int max = ClosetMaxLevel;
|
||
if (lv > 0 && max > 0)
|
||
{
|
||
lv = Math.Min(lv, max);
|
||
int i = T.ClothesRowIndex(lv);
|
||
if (i >= 0) return T.Clothes.Levels[i].StatRate > 0 ? T.Clothes.Levels[i].StatRate : 10000;
|
||
}
|
||
return 10000;
|
||
}
|
||
|
||
// ================= 탭 · 하이프 · 후원 =================
|
||
public float Hype; // 0..1
|
||
public bool HypePlaying;
|
||
int hypeTapCount;
|
||
double lastHypeTapTime, autoHypeEnd;
|
||
bool hasHypeTap, resettingHype;
|
||
double autoHypeNext;
|
||
|
||
bool RegisterTap(Queue<double> q, int max)
|
||
{
|
||
double win = Math.Max(0.01, In._tapTimeWindow);
|
||
while (q.Count > 0 && Time - q.Peek() >= win) q.Dequeue();
|
||
if (q.Count < max) { q.Enqueue(Time); return true; }
|
||
return false;
|
||
}
|
||
|
||
/// TryProcessAcceptedTap RVA 0x240588C — 반환 = 코인 탭 수용 여부
|
||
public bool Tap(bool canIncreaseHype = true, bool bypassRateLimit = false)
|
||
{
|
||
bool coin = bypassRateLimit || RegisterTap(coinTaps, Math.Max(1, R.MaxTapCoinCount));
|
||
if (coin)
|
||
{
|
||
decimal amt = TapGps();
|
||
decimal rounded = AddCoinDonateFromTap(amt);
|
||
if (rounded > 0m) TapEarned?.Invoke(rounded);
|
||
OnTapCounted();
|
||
}
|
||
if (canIncreaseHype && (bypassRateLimit || RegisterTap(hypeTaps, Math.Max(1, R.MaxTapHypeCount)))) HandleHypeTap();
|
||
return coin;
|
||
}
|
||
|
||
/// RVA 0x23FECD0 — 탭 수익은 coinDonate 로 쌓인다(받기 전에는 코인 아님)
|
||
public decimal AddCoinDonateFromTap(decimal amount)
|
||
{
|
||
decimal rounded = amount > 0m ? decimal.Ceiling(amount * 100m) / 100m : 0m;
|
||
if (rounded > 0m) CoinDonate += rounded;
|
||
return rounded;
|
||
}
|
||
|
||
public BigInteger DonateClaimable => CoinDonate > 0m ? Big.FloorBig(CoinDonate) : BigInteger.Zero;
|
||
|
||
/// ClaimDonatePopup.ClaimDonate RVA 0x2436500
|
||
public BigInteger ClaimDonate(int multiplier = 1)
|
||
{
|
||
var amount = DonateClaimable;
|
||
if (amount <= 0) return BigInteger.Zero;
|
||
var got = amount * Math.Max(1, multiplier);
|
||
AddCoin(got);
|
||
CoinDonate = Math.Max(0m, CoinDonate - (decimal)amount);
|
||
OnDonateClaimed(got);
|
||
return got;
|
||
}
|
||
|
||
/// HandleHypeTap RVA 0x2405E94
|
||
void HandleHypeTap()
|
||
{
|
||
hypeTapCount = resettingHype ? 1 : hypeTapCount + 1;
|
||
resettingHype = false;
|
||
hasHypeTap = true;
|
||
lastHypeTapTime = Time;
|
||
if (hypeTapCount >= Math.Max(1, In._hypeTapPerPercentage))
|
||
{
|
||
hypeTapCount = 0;
|
||
SetHype(Hype + 0.02f);
|
||
}
|
||
}
|
||
|
||
void SetHype(float v)
|
||
{
|
||
float prev = Hype;
|
||
float c = v < 0f ? 0f : (v > 1f ? 1f : v);
|
||
if (c > prev && c >= 0.9999f) c = 1f;
|
||
Hype = c;
|
||
if (prev < 1f && Hype >= 1f) StartHype();
|
||
}
|
||
|
||
/// StartHypeState RVA 0x2407440 + SetAutoHypeActive RVA 0x23FF884
|
||
void StartHype()
|
||
{
|
||
hasHypeTap = false; resettingHype = false; hypeTapCount = 0;
|
||
HypePlaying = true;
|
||
AutoMultiplier = Math.Max(0f, R.HypeMulti);
|
||
autoHypeEnd = Time + T.Cfg.idol._hypeAnimDuration; // TimeResetAutoHype = WaitForSeconds(_hypeAnimDuration)
|
||
double interval = Math.Max(0.01, In._tapTimeWindow) / Math.Max(1, R.MaxTapCoinCount);
|
||
autoHypeNext = Time + interval * 0.5;
|
||
OnHypeCount();
|
||
HypeStarted?.Invoke();
|
||
}
|
||
|
||
void EndHype()
|
||
{
|
||
Hype = 0f; HypePlaying = false; hasHypeTap = false; resettingHype = false; hypeTapCount = 0;
|
||
AutoMultiplier = 1f;
|
||
HypeEnded?.Invoke();
|
||
}
|
||
|
||
float HypeResetDuration => Math.Max(0.01f, T.Cfg.idol._hypeAnimDuration);
|
||
|
||
/// UpdateHypeState RVA 0x2404B70 · DrainHypeValue RVA 0x24072E8 · PlayAutoHypeTap MoveNext RVA 0x2407CC4
|
||
void UpdateHype(float dt)
|
||
{
|
||
if (HypePlaying)
|
||
{
|
||
if (Time >= autoHypeEnd && AutoMultiplier != 1f) AutoMultiplier = 1f;
|
||
if (Time >= autoHypeNext)
|
||
{
|
||
Tap(false, false);
|
||
autoHypeNext = Time + Math.Max(0.01, In._tapTimeWindow) / Math.Max(1, R.MaxTapCoinCount);
|
||
}
|
||
if (Hype > 0f) { Hype = Math.Max(0f, Hype - dt / HypeResetDuration); if (Hype > 0f) return; }
|
||
EndHype();
|
||
return;
|
||
}
|
||
if (!hasHypeTap) return;
|
||
if (!resettingHype)
|
||
{
|
||
if (Time - lastHypeTapTime < In._timeStartResetHypeState) return;
|
||
resettingHype = true; hypeTapCount = 0;
|
||
}
|
||
if (Hype > 0f) { Hype = Math.Max(0f, Hype - dt / HypeResetDuration); if (Hype > 0f) return; }
|
||
hasHypeTap = false; resettingHype = false; hypeTapCount = 0; Hype = 0f;
|
||
}
|
||
|
||
// ================= 진행 =================
|
||
public void Tick(float dt)
|
||
{
|
||
if (dt <= 0f) return;
|
||
Time += dt;
|
||
UpdateHype(dt);
|
||
idleAcc += dt;
|
||
while (idleAcc >= 1.0)
|
||
{
|
||
idleAcc -= 1.0;
|
||
IdleIncomeStep();
|
||
}
|
||
GrantFinishedSkills();
|
||
TickSystems(dt);
|
||
}
|
||
|
||
/// GenerateIdleIncome MoveNext RVA 0x240092C — 1 초마다 PassGps 누적, 정수 부분만 코인
|
||
void IdleIncomeStep()
|
||
{
|
||
decimal g = PassGps();
|
||
if (g <= 0m) return;
|
||
idleRemainder += g;
|
||
decimal whole = decimal.Truncate(idleRemainder);
|
||
idleRemainder -= whole;
|
||
if (whole > 0m) AddCoin(new BigInteger(whole));
|
||
}
|
||
|
||
// ================= 새 집 =================
|
||
/// TryCalculateRoomCompletionEconomy RVA 0x255E530 — 방 각 아이템 최대 레벨 기준
|
||
public bool RoomCompletionEconomy(int house, out decimal income, out float pass, out float tap)
|
||
{
|
||
income = 0m; pass = 0f; tap = 0f;
|
||
if (house < 0 || house >= Tables.HouseCount) return false;
|
||
foreach (var it in T.Rooms[house].Items)
|
||
{
|
||
if (it.Levels == null || it.Levels.Count == 0) continue;
|
||
LevelRow best = null;
|
||
foreach (var l in it.Levels) if (best == null || l.Level > best.Level) best = l;
|
||
income += (decimal)ItemProductIncome(best.Level + 1);
|
||
pass += best.PassRate;
|
||
tap += best.TapRate;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
public bool AllRoomItemsMaxed()
|
||
{
|
||
foreach (var it in Room.Items) if (!RoomMaxed(it.Index) || RoomUpgrading(it.Index)) return false;
|
||
return true;
|
||
}
|
||
|
||
public bool HasNextHouse => S.house + 1 < Tables.HouseCount;
|
||
|
||
/// CompleteRoomTransition RVA 0x251C780
|
||
public bool MoveToNextHouse()
|
||
{
|
||
if (!HasNextHouse) return false;
|
||
if (!RoomCompletionEconomy(S.house, out var inc, out var pass, out var tap)) return false;
|
||
TotalCompletedIncome += inc;
|
||
S.sumPassRoom += pass;
|
||
S.sumTapRoom += tap;
|
||
S.house += 1;
|
||
S.rooms = new List<SaveItem>();
|
||
NewRoom();
|
||
HouseChanged?.Invoke();
|
||
return true;
|
||
}
|
||
}
|
||
}
|