VSClone/Assets/Scripts/Sim/Tables.cs

225 lines
14 KiB
C#

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<LevelRow> Levels; }
[Serializable] public class ItemTable { public List<ItemRow> 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<ClothesRow> 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<RankRow> 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<QuestRow> items; }
[Serializable] public class ChatNameTable { public List<string> Names; }
[Serializable] public class ChatCommentRow { public int Index; public string CommentText; public List<int> EmojiIndexes; }
[Serializable] public class ChatCommentTable { public List<ChatCommentRow> 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<GiftDefRow> 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<OutfitRow> outfits; }
[Serializable] public class EnemyCfg { public List<string> 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<ChatRule> 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<DailyReward> 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));
}
/// <summary>로컬라이즈 CSV (Key,English,Russian,Vietnamese,French,German,Japanese,Chinese,Korean)</summary>
public class Loc
{
readonly Dictionary<string, string[]> rows = new Dictionary<string, string[]>();
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<List<string>> ParseCsv(string t)
{
var all = new List<List<string>>();
var row = new List<string>();
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<string>();
}
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" };
/// <param name="read">파일 이름(확장자 제외) → 텍스트. 없으면 null.</param>
public static Tables Load(Func<string, string> read)
{
var t = new Tables();
for (int h = 0; h < HouseCount; h++) t.Rooms[h] = JsonUtility.FromJson<ItemTable>(read("Room" + (h + 1)));
t.Skills = JsonUtility.FromJson<ItemTable>(read("Skills"));
t.Clothes = JsonUtility.FromJson<ClothesTable>(read("Clothes"));
t.Ranks = JsonUtility.FromJson<RankTable>(read("RankFollower"));
t.Quests = JsonUtility.FromJson<QuestTable>(read("QuestNormal"));
t.ChatNames = JsonUtility.FromJson<ChatNameTable>(read("ChatNames"));
t.Comments = JsonUtility.FromJson<ChatCommentTable>(read("ChatComments_ko") ?? read("ChatComments"));
t.Cfg = JsonUtility.FromJson<ConfigFile>(read("config"));
t.X = JsonUtility.FromJson<ExtraCfg>(read("config_extra"));
t.Battle = JsonUtility.FromJson<BattleCfg>(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);
}
}