164 lines
10 KiB
C#
164 lines
10 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using UnityEngine;
|
|
using VS.Sim;
|
|
|
|
namespace VS.EditorTools
|
|
{
|
|
// 부가 시스템 검사 — 근거 = _re/spec/systems.md
|
|
public static partial class HeadlessRun
|
|
{
|
|
static void SystemTests(Tables t)
|
|
{
|
|
var R = t.Cfg.remote;
|
|
|
|
// S1 x2 부스터 (§4)
|
|
var g = NewGame(t, out var c1);
|
|
decimal p0 = g.PassGps();
|
|
g.ActivateX2Boost();
|
|
Check("S1.x2", g.AdsMultiplier == 2f && Near(g.PassGps(), p0 * 2m, 0.000001m));
|
|
g.ActivateX2Boost();
|
|
Check("S1.stack", Math.Abs(g.AdsBoostRemain - R.X2CoinRewardTime * 2) < 0.01f);
|
|
for (int i = 0; i < 37; i++) g.Tick(100f);
|
|
Check("S1.expire", g.AdsMultiplier == 1f && g.AdsBoostRemain == 0f);
|
|
|
|
// S2 자동 탭: 코인 탭 1/s · 하이프 탭 5/s → 약 10 s 에 하이프
|
|
var g2 = NewGame(t, out _);
|
|
g2.ActivateAutoClick();
|
|
double hypeAt = -1;
|
|
for (int i = 0; i < 300 && hypeAt < 0; i++) { g2.Tick(0.05f); if (g2.HypePlaying) hypeAt = g2.Time; }
|
|
Check("S2.auto_hype", hypeAt > 8 && hypeAt < 12, $"t={hypeAt:0.00}");
|
|
Check("S2.donate_rate", g2.CoinDonate > 0m);
|
|
|
|
// S3 선물 배율 (§1.2)
|
|
var g3 = NewGame(t, out _);
|
|
decimal tap0 = g3.TapGps(), pass0 = g3.PassGps();
|
|
g3.Gifts[GiftTypes.X4TapIncome] = new Game.GiftState { Type = GiftTypes.X4TapIncome, Effect = 10f };
|
|
Check("S3.x4tap", Near(g3.TapGps(), tap0 * 4m, 0.000001m) && Near(g3.PassGps(), pass0, 0.000001m));
|
|
g3.Gifts[GiftTypes.X10Income] = new Game.GiftState { Type = GiftTypes.X10Income, Effect = 10f };
|
|
Check("S3.x10", Near(g3.TapGps(), tap0 * 40m, 0.00001m) && Near(g3.PassGps(), pass0 * 10m, 0.00001m));
|
|
g3.Gifts[GiftTypes.ReducePrice] = new Game.GiftState { Type = GiftTypes.ReducePrice, Effect = 10f };
|
|
var price = t.RoomPrice[0][0][30];
|
|
Check("S3.reduce", g3.DiscountedPrice(price) == (price * 85 + 50) / 100, g3.DiscountedPrice(price).ToString());
|
|
|
|
// S4 돈 꾸러미 (GetMoneyPackReward)
|
|
var g4 = NewGame(t, out _);
|
|
var small = new BigInteger(decimal.Round(g4.PassGpsClean() * (decimal)R.SmallMoneyMultiplierX + g4.TapGpsClean() * (decimal)R.SmallMoneyMultiplierY, 0, MidpointRounding.AwayFromZero));
|
|
Check("S4.small", g4.MoneyPackReward(GiftTypes.SmallMoneyPack) == BigInteger.Max(1, small));
|
|
var big = new BigInteger(decimal.Round(g4.PassGpsClean() * (decimal)R.BigMoneyMultiplierX + g4.TapGpsClean() * (decimal)R.BigMoneyMultiplierY, 0, MidpointRounding.AwayFromZero));
|
|
Check("S4.big", g4.MoneyPackReward(GiftTypes.BigMoneyPack) == BigInteger.Max(1, big));
|
|
|
|
// S5 선물 채우기 — 1일차 첫 선물 = Day1Types[0](X10) · 무료 선물 팝업 · 3일차 무작위 + 방치 30 s 사라짐
|
|
var g5 = NewGame(t, out var c5);
|
|
for (int i = 0; i < (int)(R.GiftUnlockTime + 2); i++) g5.Tick(1f);
|
|
Check("S5.day1_first", Array.IndexOf(g5.GiftSlot, t.X.giftDay1Types[0]) >= 0 && g5.PendingFreeGiftPopup, string.Join(",", g5.GiftSlot));
|
|
int before = g5.S.ticket; g5.ClaimFreeGiftPopup();
|
|
Check("S5.free_ticket", g5.S.ticket == before + R.FreeGiftTicketAmount);
|
|
var s5 = new SaveData { giftPlayDayCount = 2, giftLastPlayDate = c5.Now().AddDays(-1).ToString("yyyy-MM-dd"), giftUnlockedLifetime = true };
|
|
var g5b = NewGame(t, out _, s5);
|
|
Check("S5.playday3", g5b.PlayDay == 3);
|
|
for (int i = 0; i < 20; i++) g5b.Tick(1f);
|
|
int filled = 0; foreach (var x in g5b.GiftSlot) if (x >= 0) filled++;
|
|
Check("S5.day3_fill", filled >= 2, $"filled={filled}");
|
|
// 수령: 작은 돈 → 쿨다운 · 슬롯 비움
|
|
int sm = Array.IndexOf(g5b.GiftSlot, GiftTypes.SmallMoneyPack);
|
|
if (sm < 0) { g5b.GiftSlot[0] = GiftTypes.SmallMoneyPack; }
|
|
var coinB = g5b.Coin; var rw = g5b.ClaimGift(GiftTypes.SmallMoneyPack, true);
|
|
Check("S5.claim_small_x2", rw == g5b.MoneyPackReward(GiftTypes.SmallMoneyPack) * 2 && g5b.Coin == coinB + rw && Array.IndexOf(g5b.GiftSlot, GiftTypes.SmallMoneyPack) < 0);
|
|
|
|
// S6 방치 보상 (§3.2)
|
|
var s6 = new SaveData();
|
|
var g6 = NewGame(t, out var c6, s6);
|
|
decimal gps = g6.PassGps();
|
|
g6.S.timeAfk = c6.Now().AddSeconds(-3600).Ticks;
|
|
g6.OfflineCheck();
|
|
var exp6 = (new BigInteger(3600) * new BigInteger(decimal.Ceiling(gps * 100m)) * (int)Math.Round(R.AFKRate * 100.0) / 100) / 100;
|
|
Check("S6.afk", g6.AfkPending == exp6 && Math.Abs(g6.AfkSeconds - 3600) < 1, $"{g6.AfkPending} vs {exp6}");
|
|
Check("S6.claim_x5", g6.ClaimAfk(5) == exp6 * 5 && g6.AfkPending == 0);
|
|
g6.S.timeAfk = c6.Now().AddSeconds(-(R.AFKTimeMin - 10)).Ticks; g6.OfflineCheck();
|
|
Check("S6.min", g6.AfkPending == 0);
|
|
// 방치 선물 = 최소 1시간 이상 지났을 때만 (2026-10-28 PD 지시 · config AFKTimeMin)
|
|
g6.S.timeAfk = c6.Now().AddMinutes(-59).Ticks; g6.OfflineCheck();
|
|
bool none59 = g6.AfkPending == 0;
|
|
g6.S.timeAfk = c6.Now().AddMinutes(-60).Ticks; g6.OfflineCheck();
|
|
Check("S6.min_1h", R.AFKTimeMin >= 3600 && none59 && g6.AfkPending > 0, $"min={R.AFKTimeMin}s");
|
|
g6.ClaimAfk(1);
|
|
g6.S.timeAfk = c6.Now().AddDays(-3).Ticks; g6.OfflineCheck();
|
|
Check("S6.max", Math.Abs(g6.AfkSeconds - R.AFKTimeMax) < 1);
|
|
|
|
// S7 퀘스트 (§6) — 표 위에서부터 3개 · 1 = 코인 3 획득 · 2 = 코인 6 사용 · 3 = 후원 1회
|
|
var g7 = NewGame(t, out _);
|
|
var act = g7.ActiveQuests();
|
|
Check("S7.slots", act.Count == 3 && act[0].id == 1 && act[1].id == 2 && act[2].id == 3, string.Join(",", act.ConvertAll(q => q.id)));
|
|
g7.AddCoin(3);
|
|
Check("S7.increase", act[0].state == 2);
|
|
decimal snap = Big.D(act[0].gps);
|
|
var c7 = g7.Coin;
|
|
g7.ClaimQuest(1);
|
|
var r1 = g7.T.Quests.items.Find(r => r.id == 1);
|
|
Check("S7.reward", g7.Coin - c7 == new BigInteger(decimal.Ceiling(Big.D(r1.rewardNum) * snap)) && g7.ActiveQuests().Count == 3);
|
|
g7.SpendCoin(6);
|
|
Check("S7.consume", g7.S.quests.Find(q => q.id == 2).state == 2);
|
|
g7.CoinDonate = 2m; g7.ClaimDonate(1);
|
|
Check("S7.donate", g7.S.quests.Find(q => q.id == 3).state == 2);
|
|
|
|
// S8 일일 보상 (§7.1)
|
|
var g8 = NewGame(t, out var c8);
|
|
int tk = g8.S.ticket;
|
|
var d1 = g8.ClaimDaily();
|
|
Check("S8.day1", d1 != null && g8.S.ticket == tk + t.X.dailyRewards[0].amount && !g8.DailyClaimable);
|
|
c8.Extra += 86400; g8.ClaimDaily();
|
|
c8.Extra += 86400; var d3 = g8.ClaimDaily();
|
|
Check("S8.day3_outfit", d3 != null && d3.kind == 1 && g8.OutfitUnlocked(d3.amount));
|
|
|
|
// S9 온라인 티켓 (§7.2) — 600 s 도달 시 정지 · 수령 후 재개
|
|
var g9 = NewGame(t, out _);
|
|
for (int i = 0; i < 700; i++) g9.Tick(1f);
|
|
Check("S9.reach", g9.OnlineClaimable && Math.Abs(g9.S.onlineSeconds - 600) < 1.5, g9.S.onlineSeconds.ToString("0.0"));
|
|
int tk9 = g9.S.ticket; g9.ClaimOnline();
|
|
Check("S9.claim", g9.S.ticket == tk9 + 1 && !g9.OnlineClaimable);
|
|
|
|
// S10 새 집 (§10.1) — 가격 = 남은 만렙 비용 · 필요 랭크 = 다음 방 아이템 10 IndexDependency
|
|
var g10 = NewGame(t, out _);
|
|
BigInteger sum = 0;
|
|
foreach (var it in t.Rooms[0].Items) { int row = t.ItemRowIndex(0, it.Index); for (int lv = g10.RoomLevel(it.Index) + 1; lv < it.Levels.Count; lv++) sum += t.RoomPrice[0][row][lv]; }
|
|
Check("S10.price", g10.NextHousePrice() == sum);
|
|
g10.Coin = sum;
|
|
Check("S10.rank_lock", !g10.CanBuyNextHouse() && g10.NextHouseRankReq() == t.Rooms[1].Items.Find(x => x.Index == 10).IndexDependency);
|
|
g10.S.rankIndex = g10.NextHouseRankReq();
|
|
Check("S10.buy", g10.BuyNextHouse() && g10.S.house == 1 && g10.Coin == 0);
|
|
|
|
// S11 의상 (§5)
|
|
var g11 = NewGame(t, out _);
|
|
Check("S11.first_10pct", g11.ClosetNextPrice() == t.ClothesPrice[0] * t.X.closetFirstPurchasePercent / 100, g11.ClosetNextPrice().ToString());
|
|
g11.Coin = 1000000;
|
|
g11.UpgradeCloset();
|
|
Check("S11.free_unlock", g11.S.closetLevel == 1 && g11.OutfitUnlocked(0) && !g11.OutfitUnlocked(1));
|
|
var tix = t.Cfg.closet.outfits.Find(o => o.UnlockType == ClosetUnlock.Ticket);
|
|
g11.S.ticket = 100;
|
|
Check("S11.ticket_rank", tix.VisibilityRank <= 0 || g11.UnlockTicketOutfit(tix.Type) == BuyResult.Locked);
|
|
g11.S.rankIndex = Math.Max(1, tix.VisibilityRank);
|
|
Check("S11.ticket_buy", g11.UnlockTicketOutfit(tix.Type) == BuyResult.Ok && g11.S.ticket == 100 - Math.Max(1, tix.TicketPrice));
|
|
|
|
// S12 스토리 (§9)
|
|
var g12 = NewGame(t, out _);
|
|
var ch = g12.NextStory();
|
|
Check("S12.start", ch != null && ch.id == "1-1");
|
|
g12.MarkStorySeen("1-1");
|
|
Check("S12.wait", g12.NextStory() == null);
|
|
g12.S.rooms.Add(new SaveItem { Type = 0, Level = 24 });
|
|
var ch2 = g12.NextStory();
|
|
Check("S12.item_trigger", ch2 != null && ch2.id == "1-2", ch2?.id);
|
|
|
|
// S13 저장 왕복
|
|
var g13 = NewGame(t, out var c13);
|
|
g13.Coin = BigInteger.Parse("123456789012345"); g13.Follower = 777; g13.CoinDonate = 12.34m; g13.S.closetLevel = 3;
|
|
g13.ActivateX2Boost(); g13.Tick(1f);
|
|
g13.Commit();
|
|
var json = JsonUtility.ToJson(g13.S);
|
|
var g13b = new Game(t, JsonUtility.FromJson<SaveData>(json), () => c13.Now());
|
|
Check("S13.roundtrip", g13b.Coin == g13.Coin && g13b.Follower == g13.Follower && g13b.CoinDonate == g13.CoinDonate && g13b.S.closetLevel == 3 && g13b.AdsMultiplier == 2f && Math.Abs(g13b.AdsBoostRemain - g13.AdsBoostRemain) < 0.01f);
|
|
}
|
|
}
|
|
}
|