620 lines
28 KiB
C#
620 lines
28 KiB
C#
|
|
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<int, GiftState> Gifts = new Dictionary<int, GiftState>();
|
|||
|
|
public int[] GiftSlot;
|
|||
|
|
float[] slotIdle;
|
|||
|
|
readonly List<int> giftQueue = new List<int>();
|
|||
|
|
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<SaveQuest> 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); }
|
|||
|
|
}
|
|||
|
|
}
|