using System; using System.Collections.Generic; using System.Globalization; using System.Numerics; // 부가 시스템 — 근거 = _re/spec/systems.md (절 번호 표기) · 값 = config.json(RemoteConfig·GiftDataSO) + config_extra.json namespace VS.Sim { public static class GiftTypes { public const int NoAds = 0, SmallMoneyPack = 1, BigMoneyPack = 2, ReducePrice = 3, X4TapIncome = 4, X4PassiveIncome = 5, X10Income = 6; public static bool IsInstant(int t) => t == SmallMoneyPack || t == BigMoneyPack; } public static class QuestCond { public const int ConsumeCoin = 1, IncreaseCoin = 2, ConsumeCoinDynamic = 3, IncreaseCoinDynamic = 4, UnlockRoomItem = 5, UpgradeRoomItem = 6, UnlockHouse = 7, ReachFollower = 8, ClaimDonate = 9, ClaimGiftMoney = 10, ClaimDonateOrGift = 11, ActivateX2Boost = 12, ActivateAutoClick = 13, HypeCount = 14, ClaimGiftBuff = 15; } public partial class Game { float sessionPlay, questTimer; string Today => Now().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); ExtraCfg X => T.X; GiftCfg GC => T.Cfg.gift; void InitSystems() { InitStory(); RefreshPlayDay(); RestoreBoosters(); RestoreGifts(); FillQuests(); OnlineDayCheck(); } void CommitSystems() { S.adsBoostRemain = AdsBoostRemain; S.autoClickRemain = AutoClickRemain; SaveGifts(); } void TickSystems(float dt) { sessionPlay += dt; S.playSeconds += dt; TickBoosters(dt); TickGifts(dt); TickOnline(dt); questTimer -= dt; if (questTimer <= 0f) { questTimer = 0.5f; RefreshQuestThresholds(); } } // ================= 코인 출입 (퀘스트 1~4) ================= public void AddCoin(BigInteger v) { if (v <= 0) return; Coin += v; QuestCoin(v); } public void SpendCoin(BigInteger v) { if (v <= 0) return; Coin -= v; QuestCoin(-v); } // ================= 부스터 (§4) ================= public float AdsBoostRemain, AutoClickRemain; double autoClickNext; public bool AutoClickActive => AutoClickRemain > 0f; double AcceptedTapInterval { get { double w = Math.Max(0.01, In._tapTimeWindow); return Math.Min(w / Math.Max(1, R.MaxTapCoinCount), w / Math.Max(1, R.MaxTapHypeCount)); } } void RestoreBoosters() { AdsBoostRemain = Math.Max(0f, S.adsBoostRemain); AutoClickRemain = Math.Max(0f, S.autoClickRemain); if (AdsBoostRemain > 0f) AdsMultiplier = X.x2BoostMultiplier; } /// StartAdsBoostForDuration RVA 0x23FF180 — 남은 시간 + X2CoinRewardTime 누적 · AdsMultiplier 2 public void ActivateX2Boost() { AdsBoostRemain += R.X2CoinRewardTime; AdsMultiplier = X.x2BoostMultiplier; QuestEvent(QuestCond.ActivateX2Boost, 0); } /// StartAutoClickForDuration RVA 0x23FF400 public void ActivateAutoClick() { if (AutoClickRemain <= 0f) autoClickNext = Time + AcceptedTapInterval * 0.5; AutoClickRemain += R.AutoTapRewardTime; QuestEvent(QuestCond.ActivateAutoClick, 0); } /// PlayAutoClick MoveNext RVA 0x2407AD8 — 0.2 s 마다 자동 탭(하이프 연출 중 제외) void TickBoosters(float dt) { if (AdsBoostRemain > 0f) { AdsBoostRemain -= dt; if (AdsBoostRemain <= 0f) { AdsBoostRemain = 0f; AdsMultiplier = 1f; } } if (AutoClickRemain > 0f) { AutoClickRemain -= dt; if (!HypePlaying && Time >= autoClickNext) { Tap(true, false); autoClickNext = Time + AcceptedTapInterval; } if (AutoClickRemain <= 0f) AutoClickRemain = 0f; } } // ================= 선물 (§1) ================= public class GiftState { public int Type; public float Effect; public long CooldownEnd; public int Rv; public float DismissHold; } public readonly Dictionary Gifts = new Dictionary(); public int[] GiftSlot; float[] slotIdle; readonly List giftQueue = new List(); float fillTimer, rotateTimer; bool introDone; int introIndex; bool firstFill = true; public bool PendingFreeGiftPopup; public event Action GiftsChanged; public int PlayDay => Math.Max(1, S.giftPlayDayCount); /// RefreshGiftPlayDay RVA 0x251FAE8 — 서로 다른 날짜 수 void RefreshPlayDay() { if (S.giftLastPlayDate != Today) { S.giftPlayDayCount += 1; S.giftLastPlayDate = Today; } } public bool GiftSystemOn => R.OnOffGift; /// IsGiftFeatureUnlocked RVA 0x24CCB70 (튜토리얼 미구현 → 세션 플레이 시간 기준) public bool GiftFeatureUnlocked { get { if (S.giftUnlockedLifetime) return true; if (sessionPlay >= R.GiftUnlockTime) { S.giftUnlockedLifetime = true; return true; } return false; } } GiftState GS(int t) { if (!Gifts.TryGetValue(t, out var g)) { g = new GiftState { Type = t }; Gifts[t] = g; } return g; } public float GiftEffectDuration(int t) => t switch { GiftTypes.NoAds => R.NoADsGiftTime, GiftTypes.ReducePrice => R.SaleOffGiftTime, GiftTypes.X4TapIncome => R.X4TapIncomeGiftTime, GiftTypes.X4PassiveIncome => R.X4PassiveIncomeGiftTime, GiftTypes.X10Income => R.X10IncomeTime, _ => 0f }; /// GetCooldownSeconds RVA 0x24CABE8 public float GiftCooldown(int t) => t switch { GiftTypes.SmallMoneyPack => R.SmallMoneyGiftCooldown, GiftTypes.BigMoneyPack => R.BigMoneyGiftCooldown, GiftTypes.X10Income => R.X10IncomeGiftCooldown, _ => R.GiftCooldownNormal }; float RefillDelay => PlayDay == 2 ? GC.day2RefillDelaySeconds : R.GiftAppearTime; float IdleTimeout => R.GiftDisappearTime > 0 ? R.GiftDisappearTime : GC.idleTimeoutSeconds; float AppearDelay => Math.Abs(R.GiftAppearTime - R.GiftDisappearTime) < 0.0001f ? GC.refillDelaySeconds : R.GiftAppearTime; float DismissHoldSeconds => PlayDay <= 2 ? GC.dismissHoldDay1And2Seconds : GC.dismissHoldDay3PlusSeconds; /// IsSpawnAllowed RVA 0x24CEF14 bool SpawnAllowed(int t) { if (PlayDay == 1) return Array.IndexOf(X.giftDay1Types, t) >= 0; if (PlayDay == 2) return Array.IndexOf(X.giftDay2Types, t) >= 0; return true; } /// IsAvailableForFill RVA 0x24CDE3C bool AvailableForFill(int t) { var g = GS(t); var def = T.Gift(t); return SpawnAllowed(t) && (def == null || def.UnlockRankIndex <= S.rankIndex) && Array.IndexOf(GiftSlot, t) < 0 && g.DismissHold <= 0f && g.CooldownEnd <= NowT && g.Effect <= 0f; } public bool IsGiftEffectActive(int type) => Gifts.TryGetValue(type, out var g) && g.Effect > 0f; public float GiftEffectRemain(int type) => Gifts.TryGetValue(type, out var g) ? g.Effect : 0f; /// GetTapIncomeMultiplier RVA 0x24D1E30 public decimal GiftTapMultiplier() => !GiftSystemOn ? 1m : (IsGiftEffectActive(GiftTypes.X4TapIncome) ? 4m : 1m) * (IsGiftEffectActive(GiftTypes.X10Income) ? 10m : 1m); /// GetPassiveIncomeMultiplier RVA 0x24D1F64 public decimal GiftPassiveMultiplier() => !GiftSystemOn ? 1m : (IsGiftEffectActive(GiftTypes.X4PassiveIncome) ? 4m : 1m) * (IsGiftEffectActive(GiftTypes.X10Income) ? 10m : 1m); /// ReducePriceFactor RVA 0x24CA908 = 1 − max(SaleOffGiftValue, 0) decimal ReducePriceFactor => 1m - (decimal)Math.Max(0f, R.SaleOffGiftValue); void RestoreGifts() { int n = Math.Max(1, GC.maxVisibleSlots); GiftSlot = new int[n]; slotIdle = new float[n]; for (int i = 0; i < n; i++) GiftSlot[i] = -1; foreach (var sg in S.gifts) { var g = GS(sg.Type); g.Effect = Math.Max(0f, sg.PausedRemain); g.CooldownEnd = sg.CooldownEnd; g.Rv = sg.RvProgress; } // 효과 진행 중인 선물은 슬롯에 남겨 남은 시간을 보여준다 foreach (var g in Gifts.Values) if (g.Effect > 0f) PlaceInSlot(g.Type); introDone = PlayDay >= 3; fillTimer = 0f; } void SaveGifts() { S.gifts.Clear(); foreach (var g in Gifts.Values) S.gifts.Add(new SaveGift { Type = g.Type, PausedRemain = g.Effect, CooldownEnd = g.CooldownEnd, RvProgress = g.Rv }); } bool PlaceInSlot(int t) { for (int i = 0; i < GiftSlot.Length; i++) if (GiftSlot[i] < 0) { GiftSlot[i] = t; slotIdle[i] = 0f; giftQueue.Remove(t); if (t == GiftTypes.X10Income && !S.freeGiftPopupClaimed) PendingFreeGiftPopup = true; // PlaceInSlot RVA 0x24CE94C GiftsChanged?.Invoke(); return true; } return false; } void ReleaseSlot(int t, bool toQueue) { int i = Array.IndexOf(GiftSlot, t); if (i >= 0) { GiftSlot[i] = -1; slotIdle[i] = 0f; } if (toQueue && !giftQueue.Contains(t)) giftQueue.Add(t); GiftsChanged?.Invoke(); } bool HasEmptySlot { get { foreach (var s in GiftSlot) if (s < 0) return true; return false; } } /// GiftManager.Update RVA 0x24CCD38 계열 (일차별 채우기·사라짐) void TickGifts(float dt) { if (!GiftSystemOn || !GiftFeatureUnlocked) return; foreach (var g in Gifts.Values) { if (g.Effect > 0f) { g.Effect -= dt; if (g.Effect <= 0f) { g.Effect = 0f; g.CooldownEnd = Now().AddSeconds(GiftCooldown(g.Type)).Ticks; ReleaseSlot(g.Type, false); } } if (g.DismissHold > 0f) { g.DismissHold -= dt; if (g.DismissHold <= 0f) g.DismissHold = 0f; } } foreach (var t in X.giftSeedOrder) if (!giftQueue.Contains(t) && AvailableForFill(t)) giftQueue.Add(t); if (!introDone) { var types = PlayDay == 1 ? X.giftDay1Types : X.giftDay2Types; fillTimer -= dt; if (firstFill || fillTimer <= 0f) { firstFill = false; fillTimer = GC.introStepSeconds; while (introIndex < types.Length && !AvailableForFill(types[introIndex])) introIndex++; if (introIndex < types.Length && HasEmptySlot) { PlaceInSlot(types[introIndex]); introIndex++; } if (introIndex >= types.Length || !HasEmptySlot) { introDone = true; fillTimer = RefillDelay; } } } else if (HasEmptySlot) { fillTimer -= dt; if (fillTimer <= 0f) { int pick = -1; var cands = giftQueue.FindAll(AvailableForFill); if (cands.Count > 0) pick = PlayDay >= 3 ? cands[rng.Next(cands.Count)] : cands[0]; if (pick >= 0) PlaceInSlot(pick); fillTimer = PlayDay >= 3 ? AppearDelay : RefillDelay; } } else fillTimer = PlayDay >= 3 ? AppearDelay : RefillDelay; // 방치 시 사라짐: 3일차+ 슬롯마다 IdleTimeout · 2일차 120 s 마다 가장 오래된 것 1개 int oldest = -1; for (int i = 0; i < GiftSlot.Length; i++) { int t = GiftSlot[i]; if (t < 0) continue; var g = GS(t); if (g.Effect > 0f || (t == GiftTypes.NoAds && g.Rv > 0)) { slotIdle[i] = 0f; continue; } slotIdle[i] += dt; if (oldest < 0 || slotIdle[i] > slotIdle[oldest]) oldest = i; if (PlayDay >= 3 && slotIdle[i] >= IdleTimeout) ReleaseSlot(t, true); } if (PlayDay == 2) { rotateTimer += dt; if (rotateTimer >= GC.day2IdleTimeoutSeconds) { rotateTimer = 0f; if (oldest >= 0 && GiftSlot[oldest] >= 0) ReleaseSlot(GiftSlot[oldest], true); } } } /// GetMoneyPackReward RVA 0x24D34A4 = Round(PassGPSClean × X + TapGPSClean × Y) (최소 1) public BigInteger MoneyPackReward(int t) { decimal x = t == GiftTypes.SmallMoneyPack ? (decimal)R.SmallMoneyMultiplierX : (decimal)R.BigMoneyMultiplierX; decimal y = t == GiftTypes.SmallMoneyPack ? (decimal)R.SmallMoneyMultiplierY : (decimal)R.BigMoneyMultiplierY; var v = new BigInteger(decimal.Round(PassGpsClean() * x + TapGpsClean() * y, 0, MidpointRounding.AwayFromZero)); return v < 1 ? BigInteger.One : v; } /// GiftPopup.ApplyClaim RVA 0x25EB89C + GiftManager.ClaimGift RVA 0x24D1874 · viaAd = 광고/티켓(작은 돈만 ×2) public BigInteger ClaimGift(int t, bool viaAd) { if (Array.IndexOf(GiftSlot, t) < 0) return BigInteger.Zero; var g = GS(t); if (GiftTypes.IsInstant(t)) { var reward = MoneyPackReward(t) * ((viaAd && t == GiftTypes.SmallMoneyPack) ? X.giftSmallPackAdMultiplier : 1); AddCoin(reward); g.CooldownEnd = Now().AddSeconds(GiftCooldown(t)).Ticks; ReleaseSlot(t, false); QuestEvent(QuestCond.ClaimGiftMoney, t); return reward; } if (t == GiftTypes.NoAds) { g.Rv += 1; int i = Array.IndexOf(GiftSlot, t); if (i >= 0) slotIdle[i] = 0f; if (g.Rv >= Math.Max(1, X.giftNoAdsVideos)) { g.Rv = 0; g.Effect = GiftEffectDuration(t); } GiftsChanged?.Invoke(); return BigInteger.Zero; } g.Effect = GiftEffectDuration(t); QuestEvent(QuestCond.ClaimGiftBuff, t); GiftsChanged?.Invoke(); return BigInteger.Zero; } /// DismissGift RVA 0x24D1D64 public void DismissGift(int t) { if (Array.IndexOf(GiftSlot, t) < 0 || IsGiftEffectActive(t)) return; ReleaseSlot(t, false); GS(t).DismissHold = DismissHoldSeconds; } /// 검사·렌더용: 플레이 일차 바꾸기 public void DebugSetPlayDay(int day) { S.giftPlayDayCount = day; introDone = day >= 3; introIndex = 0; firstFill = true; fillTimer = 0f; } public void ClaimFreeGiftPopup() { if (S.freeGiftPopupClaimed) return; S.freeGiftPopupClaimed = true; PendingFreeGiftPopup = false; S.ticket += R.FreeGiftTicketAmount; } // ================= 방치 보상 (§3) ================= public BigInteger AfkPending => Big.P(S.afkPending); public double AfkSeconds; /// 백그라운드·종료 시 (GamePlayUI.OnApplicationPause(true) RVA 0x2605080) public void MarkAfkStamp() { if (S.timeAfk == 0) S.timeAfk = NowT; } /// CalculateCoinAfk RVA 0x2604068 public void OfflineCheck() { if (AfkPending > 0) { S.timeAfk = 0; return; } long stamp = S.timeAfk != 0 ? S.timeAfk : S.lastActive; S.timeAfk = 0; if (stamp <= 0 || stamp >= NowT) return; double t = (NowT - stamp) / 1e7; double min = Math.Max(0, R.AFKTimeMin), max = Math.Max(min, R.AFKTimeMax); if (t < min) return; if (t > max) t = max; var gps100 = new BigInteger(decimal.Ceiling(PassGps() * 100m)); int rate100 = (int)Math.Round(R.AFKRate * 100.0); var coin = (new BigInteger(t) * gps100 * rate100 / 100) / 100; AfkSeconds = t; if (coin > 0) S.afkPending = coin.ToString(); } /// ClaimAfkPopup._OnClaim RVA 0x2435190 / GrantMultipliedRewardAndComplete RVA 0x24357EC public BigInteger ClaimAfk(int multiplier) { var c = AfkPending; if (c <= 0) return BigInteger.Zero; var got = c * Math.Max(1, multiplier); AddCoin(got); S.afkPending = "0"; return got; } // ================= 퀘스트 (§6) ================= public List ActiveQuests() => S.quests.FindAll(q => q.state == 1 || q.state == 2); public QuestRow QuestRowOf(int id) => T.Quests.items.Find(r => r.id == id); SaveQuest QState(int id) => S.quests.Find(q => q.id == id); /// FillNormalSlots RVA 0x2583958 · FindNextNormalToActivate RVA 0x2583CA4 public void FillQuests() { if (!R.OnOffQuest) return; int guard = 0; while (ActiveQuests().Count < Math.Max(1, X.questSlots) && guard++ < 10) { QuestRow next = null; foreach (var r in T.Quests.items) { var s = QState(r.id); if (s != null && s.state >= 1) continue; if (r.preconditionId > 0 && (QState(r.preconditionId)?.state ?? 0) != 3) continue; next = r; break; } if (next == null) break; ActivateQuest(next); } } static decimal Dec(string s) { decimal.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d); return d; } /// ActivateQuest RVA 0x2583E80 · ComputeTargetSnapshot RVA 0x2584390 void ActivateQuest(QuestRow r) { var q = QState(r.id); if (q == null) { q = new SaveQuest { id = r.id }; S.quests.Add(q); } q.state = 1; q.progress = "0"; decimal gps = PassGps(); q.gps = Big.S(gps); decimal p1 = Dec(r.param1), p2 = Dec(r.param2), target; switch (r.conditionType) { case QuestCond.ConsumeCoin: case QuestCond.IncreaseCoin: target = decimal.Ceiling(p1); break; case QuestCond.ConsumeCoinDynamic: case QuestCond.IncreaseCoinDynamic: target = Math.Max(1m, decimal.Ceiling(gps * p1)); break; case QuestCond.UpgradeRoomItem: target = p2; break; case QuestCond.ReachFollower: target = p1; break; case QuestCond.ClaimDonate: case QuestCond.HypeCount: target = Math.Max(1m, p1); break; default: target = 1m; break; } q.target = Big.S(target); RefreshThreshold(q, r); } void AddProgress(SaveQuest q, decimal v) { if (q.state != 1) return; q.progress = Big.S(Dec(q.progress) + v); CheckQuest(q); } /// IsTargetReached RVA 0x2584BB8 void CheckQuest(SaveQuest q) { if (q.state == 1 && Dec(q.progress) + 0.0001m >= Dec(q.target)) q.state = 2; } void QuestCoin(BigInteger delta) { if (S.quests.Count == 0) return; decimal d = Big.ToDec(BigInteger.Abs(delta)); foreach (var q in S.quests) { if (q.state != 1) continue; var r = QuestRowOf(q.id); if (r == null) continue; int c = r.conditionType; if (delta > 0 && (c == QuestCond.IncreaseCoin || c == QuestCond.IncreaseCoinDynamic)) AddProgress(q, d); if (delta < 0 && (c == QuestCond.ConsumeCoin || c == QuestCond.ConsumeCoinDynamic)) AddProgress(q, d); } } /// 이벤트형 조건 (MatchesGiftFilter RVA 0x2585744) void QuestEvent(int cond, int param) { foreach (var q in S.quests) { if (q.state != 1) continue; var r = QuestRowOf(q.id); if (r == null || r.conditionType != cond) continue; int p1 = (int)Dec(r.param1); if (cond == QuestCond.ClaimGiftMoney && !(p1 <= 0 ? (param == 1 || param == 2) : param == p1)) continue; if (cond == QuestCond.ClaimGiftBuff && !(p1 <= 0 ? (param >= 3 && param <= 6) : param == p1)) continue; AddProgress(q, 1m); } } /// RefreshThresholdProgress RVA 0x2585340 — 5~8 = 현재 상태값 void RefreshQuestThresholds() { foreach (var q in S.quests) if (q.state == 1) { var r = QuestRowOf(q.id); if (r != null) RefreshThreshold(q, r); } } void RefreshThreshold(SaveQuest q, QuestRow r) { decimal v; switch (r.conditionType) { case QuestCond.UnlockRoomItem: v = TargetValue(r.param1, true); break; case QuestCond.UpgradeRoomItem: v = TargetValue(r.param1, false); break; case QuestCond.UnlockHouse: v = S.house >= (int)Dec(r.param1) ? 1m : 0m; break; case QuestCond.ReachFollower: v = Big.ToDec(Follower); break; default: return; } q.progress = Big.S(v); CheckQuest(q); } /// QuestProductionTargetParser.TryParse RVA 0x2586198 · GetUpgradeProgress RVA 0x258697C decimal TargetValue(string target, bool unlockOnly) { var p = (target ?? "").Split('_'); if (p.Length == 0) return 0m; if (p[0] == "0") { if (p.Length >= 2 && int.TryParse(p[1], out var ot)) return OutfitUnlocked(ot) ? 1m : 0m; return unlockOnly ? (S.closetLevel >= 1 ? 1m : 0m) : S.closetLevel; } if (p[0] == "1" && p.Length >= 3 && int.TryParse(p[1], out var h) && int.TryParse(p[2], out var it)) { if (h < S.house) return unlockOnly ? 1m : 9999m; if (h > S.house) return 0m; int lv = RoomLevel(it); return unlockOnly ? (lv >= 0 ? 1m : 0m) : lv + 1; } if (p[0] == "2" && p.Length >= 2 && int.TryParse(p[1], out var sk)) { int lv = SkillLevel(sk); return unlockOnly ? (lv > 0 ? 1m : 0m) : lv; } return 0m; } /// TryClaim RVA 0x25822A0 · QuestRewardGranter RVA 0x25869E4 — 집 이동 = 티켓 · 그 외 = rewardNum × 활성화 시 PassGps 코인 public string ClaimQuest(int id) { var q = QState(id); var r = QuestRowOf(id); if (q == null || r == null || q.state != 2) return null; string got; if (r.conditionType == QuestCond.UnlockHouse) { int n = (int)Math.Round(Dec(r.rewardNum), MidpointRounding.AwayFromZero); S.ticket += n; got = "티켓 " + n; } else { decimal gps = Dec(q.gps); if (gps <= 0m) gps = PassGps(); var c = new BigInteger(decimal.Ceiling(Dec(r.rewardNum) * gps)); AddCoin(c); got = c.ToString(); } q.state = 3; FillQuests(); return got; } public decimal QuestRewardPreview(SaveQuest q) { var r = QuestRowOf(q.id); if (r == null) return 0m; if (r.conditionType == QuestCond.UnlockHouse) return Dec(r.rewardNum); decimal gps = Dec(q.gps); if (gps <= 0m) gps = PassGps(); return decimal.Ceiling(Dec(r.rewardNum) * gps); } public bool AnyQuestClaimable() => S.quests.Exists(q => q.state == 2); // ================= 일일 보상 · 온라인 티켓 (§7) ================= public bool DailyClaimable => X.dailyRewards != null && S.dailyClaimedDays < X.dailyRewards.Count && S.dailyLastDate != Today; /// DailyRewardManager RVA 0x24DB250~ — 받은 일수+1 번째 칸 · 하루 1회 · 7일 끝 public DailyReward ClaimDaily() { if (!DailyClaimable) return null; var d = X.dailyRewards[S.dailyClaimedDays]; if (d.kind == 0) S.ticket += d.amount; else GrantEventOutfit(d.amount); S.dailyClaimedDays++; S.dailyLastDate = Today; return d; } double[] OnlineSteps { get { var a = R.TimeDailyTicket; if (a == null) return new double[0]; var s = new double[a.Length]; double sum = 0; for (int i = 0; i < a.Length; i++) { sum += a[i]; s[i] = sum; } return s; } } public int OnlineReached { get { int n = 0; foreach (var v in OnlineSteps) if (S.onlineSeconds >= v) n++; return n; } } public bool OnlineClaimable => S.onlineClaimed < OnlineReached; public double OnlineNextAt { get { var s = OnlineSteps; return S.onlineClaimed < s.Length ? s[S.onlineClaimed] : 0; } } void OnlineDayCheck() { if (S.onlineDate != Today) { S.onlineDate = Today; S.onlineSeconds = 0; S.onlineClaimed = 0; } } /// OnlineTicketRewardManager RVA 0x256FAF8~ — 받을 단계가 있으면 시간 멈춤 void TickOnline(float dt) { OnlineDayCheck(); if (S.onlineClaimed >= OnlineSteps.Length || OnlineClaimable) return; S.onlineSeconds += dt; } public bool ClaimOnline() { if (!OnlineClaimable) return false; S.ticket += X.onlineTicketPerStep; S.onlineClaimed++; return true; } public bool TrySpendTicket() { if (S.ticket <= 0) return false; S.ticket -= 1; return true; } // ================= 새 집 (§10.1) ================= /// GetTotalPriceToMaxCurrentRoom RVA 0x255F4E4 — 현재 방을 전부 만렙으로 만드는 남은 비용(할인 없음) public BigInteger NextHousePrice() { BigInteger sum = BigInteger.Zero; for (int i = 0; i < Room.Items.Count; i++) { var it = Room.Items[i]; int row = T.ItemRowIndex(S.house, it.Index); for (int lv = RoomLevel(it.Index) + 1; lv < it.Levels.Count; lv++) sum += T.RoomPrice[S.house][row][lv]; } return sum; } /// 다음 방 아이템 10(방 자체)의 IndexDependency = 필요 랭크 public int NextHouseRankReq() { if (!HasNextHouse) return int.MaxValue; var it = T.Rooms[S.house + 1].Items.Find(x => x.Index == 10); return it != null ? it.IndexDependency : 0; } /// CanBuyNextRoom RVA 0x255F350 public bool CanBuyNextHouse() => HasNextHouse && Coin >= NextHousePrice() && NextHouseRankReq() <= S.rankIndex; /// BuyNewHousePopup._OnBuy RVA 0x2438214 — 방 승격 후 가격 차감 public bool BuyNextHouse() { if (!CanBuyNextHouse()) return false; var price = NextHousePrice(); if (!MoveToNextHouse()) return false; SpendCoin(price); RefreshQuestThresholds(); return true; } // ================= 훅 ================= public void OnRankUp(int from, int to) { } public void DebugAdvance(double sec) { S.firstPlay -= (long)(sec * 1e7); foreach (var r in S.rooms) if (r.UpgradeEnd > 0) r.UpgradeEnd -= (long)(sec * 1e7); foreach (var r in S.skills) if (r.UpgradeEnd > 0) r.UpgradeEnd -= (long)(sec * 1e7); foreach (var g in Gifts.Values) if (g.CooldownEnd > 0) g.CooldownEnd -= (long)(sec * 1e7); } void OnClosetUpgraded() { RefreshQuestThresholds(); } void OnRoomBought(int type, int fromLevel, int toLevel) { RefreshQuestThresholds(); } void OnSkillBought(int type, int level) { RefreshQuestThresholds(); } void OnTapCounted() { } void OnDonateClaimed(BigInteger amount) { QuestEvent(QuestCond.ClaimDonate, 0); } void OnHypeCount() { QuestEvent(QuestCond.HypeCount, 0); } } }