using System; using System.Collections.Generic; using System.Globalization; using System.Numerics; using UnityEngine; // 원작 표(JSON) 구조 그대로 — 값은 Resources/Data 의 SOT 에서만 읽는다 (C45). namespace VS.Sim { [Serializable] public class LevelRow { public int Level; public string Price; public string Follower; public int TapRate; public int PassRate; public float UpgradeTime; public int Condition; } [Serializable] public class ItemRow { public int Index; public string Name; public int IndexDependency; public List Levels; } [Serializable] public class ItemTable { public List Items; } [Serializable] public class ClothesRow { public int Level; public string Price; public int StatRate; public int RankDependency; public float UpgradeTime; } [Serializable] public class ClothesTable { public List Levels; } [Serializable] public class RankRow { public int Index; public string Rank; public string Follower; public int RankBoostRate; public string Reward; } [Serializable] public class RankTable { public List Items; } [Serializable] public class QuestRow { public int id; public int preconditionId; public int conditionType; public string param1; public string param2; public string rewardNum; } [Serializable] public class QuestTable { public List items; } [Serializable] public class ChatNameTable { public List Names; } [Serializable] public class ChatCommentRow { public int Index; public string CommentText; public List EmojiIndexes; } [Serializable] public class ChatCommentTable { public List Items; } // ---- 원작 직렬화 기본값 (config.json · level0 RemoteConfig 등) [Serializable] public class RemoteCfg { public float AFKRate; public int AFKTimeMax, AFKTimeMin, AOACapTime, SessionShowAOA, BannerOffFirstSessionTime; public bool BannerOffFirstSessionValue; public float BaseIncome; public int BigMoneyGiftCooldown; public float BigMoneyMultiplierX, BigMoneyMultiplierY; public int GiftAppearTime, GiftCooldownNormal, GiftDisappearTime; public float GrowthIncome, HypeMulti; public int InterAdCapTime, InterAdFirstShowTime, InterAdShowTime, SessionShowInter, RemoveAdsPopupCooldown, GiftUnlockTime, BannerUnlockTime; public float KeyLevelMulti; public int NoADsGiftTime; public bool OnOffAOA, OnOffGift, OnOffPK, OnOffSplashAds, OnOffSwitchAppAds, OnOffTut, OnOffRate, OnOffStory, OnOffQuest; public int SaleOffGiftTime; public float SaleOffGiftValue; public int SmallMoneyGiftCooldown; public float SmallMoneyMultiplierX, SmallMoneyMultiplierY; public int X10IncomeTime, X4PassiveIncomeGiftTime, X4TapIncomeGiftTime; public float X2CoinRewardTime, AutoTapRewardTime; public int AutoClickIntroPopupCount, X2CoinIntroPopupCount, X4TapIntroPopupCount, X4PassiveIntroPopupCount, X10IncomeIntroPopupCount, ReducePriceIntroPopupCount; public int MaxTapCoinCount, MaxTapHypeCount; public float HypePercentage, TapMultiplier, X10IncomeGiftCooldown; public int FreeGiftTicketAmount; public bool OnOffActiveHold; public float HoldActivateSeconds; public int HoldTapsPerSecond; public string AbTestGroup, NativeAOATier, ShowNativeAfterInters; public int TimeDelayInterAfterReward; public int[] TimeDailyTicket; } [Serializable] public class InputCfg { public float _tapTimeWindow; public int _autoTapVisualPerSecond; public float _tapVisualWorldZ, _autoTapVisualXMin, _autoTapVisualXMax, _autoTapVisualYMin, _autoTapVisualYMax; public float _punchScaleIntensity, _punchTapForce, _punchSpringStiffness, _punchSpringDamping, _returnIdleDelay; public int _hypeTapPerPercentage; public float _timeStartResetHypeState; } [Serializable] public class IdolCfg { public float _timeBetweenIdleBlink, _idleBlinkDuration, _hypeAnimDuration, _timeBetweenHypeFrame; } [Serializable] public class GameCfg { public bool IsNewPkMode; public float ConvertRate; public bool IsPkEnabled; } [Serializable] public class GiftDefRow { public int Type, Category, UnlockRankIndex; public float EffectDurationSeconds, CooldownSeconds; public int RequiredRewardedVideos; public bool RequireAdToClaim; public string Title, PopupDescription; } [Serializable] public class GiftCfg { public List giftDefinitions; public int giftTimeSource, maxVisibleSlots; public float idleTimeoutSeconds, refillDelaySeconds, unlockDelayAfterTutorial2Seconds, introStepSeconds, day2IdleTimeoutSeconds, day2RefillDelaySeconds; public float dismissHoldDay1And2Seconds, dismissHoldDay3PlusSeconds, blinkRemainingRatio, reducePriceFactor; public int smallMoneyPackPassiveMultiplier, smallMoneyPackTapMultiplier, bigMoneyPackPassiveMultiplier, bigMoneyPackTapMultiplier; } [Serializable] public class OutfitRow { public int Type, UnlockType, VisibilityRank, UnlockLevel, TicketPrice; public string ProductId; } [Serializable] public class ClosetCfg { public List outfits; } [Serializable] public class EnemyCfg { public List EnemyName; } [Serializable] public class ConfigFile { public RemoteCfg remote; public InputCfg input; public IdolCfg idol; public GameCfg game; public GiftCfg gift; public ClosetCfg closet; public EnemyCfg enemy; } [Serializable] public class ChatRule { public int state; public float CommentsPerSecond, IntervalReducePercentPerRank, DonateReplaceChance; public int MaxConsecutiveDonate; } [Serializable] public class DailyReward { public int kind; public int amount; } [Serializable] public class ExtraCfg { public int[] giftDay1Types, giftDay2Types, giftSeedOrder; public int giftSmallPackAdMultiplier, giftNoAdsVideos; public List chatRules; public int chatDonateMin, chatDonateMax; public float chatShowInterval, chatShowIntervalHype; public int chatQueueCap; public float chatMaxQueueAge; public int chatVisibleLines; public int[] afkMultipliers; public float afkArrowMoveDuration; public int questSlots, closetFirstPurchasePercent; public float x2BoostMultiplier; public int[] questTemplateIndex; public List dailyRewards; public int onlineTicketPerStep; public int[] ticketPacks; public int viewRandomMin, viewRandomMax, viewSmallFollower, viewSmallMin; public float viewRerollMin, viewRerollMax; } public static class Big { public static BigInteger P(string s) => string.IsNullOrEmpty(s) ? BigInteger.Zero : BigInteger.Parse(s, CultureInfo.InvariantCulture); public static decimal D(string s) => string.IsNullOrEmpty(s) ? 0m : decimal.Parse(s, NumberStyles.Float, CultureInfo.InvariantCulture); public static string S(decimal d) => d.ToString(CultureInfo.InvariantCulture); public static decimal ToDec(BigInteger b) => b > (BigInteger)decimal.MaxValue ? decimal.MaxValue : (decimal)b; public static BigInteger FloorBig(decimal d) => new BigInteger(decimal.Floor(d)); } /// 로컬라이즈 CSV (Key,English,Russian,Vietnamese,French,German,Japanese,Chinese,Korean) public class Loc { readonly Dictionary rows = new Dictionary(); public int Column = 8; // Korean public void AddCsv(string text) { if (string.IsNullOrEmpty(text)) return; foreach (var f in ParseCsv(text)) if (f.Count > 1 && f[0] != "Key") rows[f[0]] = f.ToArray(); } public string Get(string key, string fallback = null) { if (key != null && rows.TryGetValue(key, out var r)) { if (Column < r.Length && !string.IsNullOrEmpty(r[Column])) return r[Column]; if (r.Length > 1 && !string.IsNullOrEmpty(r[1])) return r[1]; } return fallback ?? key; } public bool Has(string key) => key != null && rows.ContainsKey(key); static List> ParseCsv(string t) { var all = new List>(); var row = new List(); var sb = new System.Text.StringBuilder(); bool q = false; for (int i = 0; i < t.Length; i++) { char c = t[i]; if (q) { if (c == '"') { if (i + 1 < t.Length && t[i + 1] == '"') { sb.Append('"'); i++; } else q = false; } else sb.Append(c); } else if (c == '"') q = true; else if (c == ',') { row.Add(sb.ToString()); sb.Clear(); } else if (c == '\n' || c == '\r') { if (c == '\r' && i + 1 < t.Length && t[i + 1] == '\n') i++; row.Add(sb.ToString()); sb.Clear(); if (row.Count > 1 || row[0].Length > 0) all.Add(row); row = new List(); } else sb.Append(c); } if (sb.Length > 0 || row.Count > 0) { row.Add(sb.ToString()); all.Add(row); } return all; } } public class Tables { public const int HouseCount = 6; public ItemTable[] Rooms = new ItemTable[HouseCount]; public ItemTable Skills; public ClothesTable Clothes; public RankTable Ranks; public QuestTable Quests; public ChatNameTable ChatNames; public ChatCommentTable Comments; public ConfigFile Cfg; public ExtraCfg X; public BattleCfg Battle; public Loc Loc = new Loc(); public string StoryLogicJson, StoryLocaleJson, SubStoryJson, SubStoryLocaleJson; // 파싱 캐시 public BigInteger[][][] RoomPrice, RoomFollower; public BigInteger[][] SkillPrice, SkillFollower; public BigInteger[] ClothesPrice, RankFollower, RankReward; public static readonly LevelRow EmptyLevel = new LevelRow { Level = -1, Price = "0", Follower = "0" }; /// 파일 이름(확장자 제외) → 텍스트. 없으면 null. public static Tables Load(Func read) { var t = new Tables(); for (int h = 0; h < HouseCount; h++) t.Rooms[h] = JsonUtility.FromJson(read("Room" + (h + 1))); t.Skills = JsonUtility.FromJson(read("Skills")); t.Clothes = JsonUtility.FromJson(read("Clothes")); t.Ranks = JsonUtility.FromJson(read("RankFollower")); t.Quests = JsonUtility.FromJson(read("QuestNormal")); t.ChatNames = JsonUtility.FromJson(read("ChatNames")); t.Comments = JsonUtility.FromJson(read("ChatComments_ko") ?? read("ChatComments")); t.Cfg = JsonUtility.FromJson(read("config")); t.X = JsonUtility.FromJson(read("config_extra")); t.Battle = JsonUtility.FromJson(read("battle")); foreach (var n in new[] { "Common", "Gameplay", "Content", "Task", "UI" }) t.Loc.AddCsv(read("loc_" + n)); t.StoryLogicJson = read("StoryLogic"); t.StoryLocaleJson = read("StoryLocale_ko"); t.SubStoryJson = read("SubStory"); t.SubStoryLocaleJson = read("SubStoryLocale_ko"); t.Build(); return t; } void Build() { RoomPrice = new BigInteger[HouseCount][][]; RoomFollower = new BigInteger[HouseCount][][]; for (int h = 0; h < HouseCount; h++) { var items = Rooms[h].Items; RoomPrice[h] = new BigInteger[items.Count][]; RoomFollower[h] = new BigInteger[items.Count][]; for (int i = 0; i < items.Count; i++) { var lv = items[i].Levels; RoomPrice[h][i] = new BigInteger[lv.Count]; RoomFollower[h][i] = new BigInteger[lv.Count]; for (int k = 0; k < lv.Count; k++) { RoomPrice[h][i][k] = Big.P(lv[k].Price); RoomFollower[h][i][k] = Big.P(lv[k].Follower); } } } SkillPrice = new BigInteger[Skills.Items.Count][]; SkillFollower = new BigInteger[Skills.Items.Count][]; for (int i = 0; i < Skills.Items.Count; i++) { var lv = Skills.Items[i].Levels; SkillPrice[i] = new BigInteger[lv.Count]; SkillFollower[i] = new BigInteger[lv.Count]; for (int k = 0; k < lv.Count; k++) { SkillPrice[i][k] = Big.P(lv[k].Price); SkillFollower[i][k] = Big.P(lv[k].Follower); } } ClothesPrice = new BigInteger[Clothes.Levels.Count]; for (int k = 0; k < Clothes.Levels.Count; k++) ClothesPrice[k] = Big.P(Clothes.Levels[k].Price); RankFollower = new BigInteger[Ranks.Items.Count]; RankReward = new BigInteger[Ranks.Items.Count]; for (int k = 0; k < Ranks.Items.Count; k++) { RankFollower[k] = Big.P(Ranks.Items[k].Follower); RankReward[k] = Big.P(Ranks.Items[k].Reward); } } public int RoomItemCount(int house) => Rooms[house].Items.Count; public int ItemRowIndex(int house, int type) => Rooms[house].Items.FindIndex(x => x.Index == type); public int SkillRowIndex(int type) => Skills.Items.FindIndex(x => x.Index == type); public int RankRowIndex(int index) => Ranks.Items.FindIndex(x => x.Index == index); public int ClothesRowIndex(int level) => Clothes.Levels.FindIndex(x => x.Level == level); public GiftDefRow Gift(int type) => Cfg.gift.giftDefinitions.Find(g => g.Type == type); } }