ThreeKingdomsSlingshot/Assets/Scripts/OutGame/GachaRules.cs

124 lines
4.9 KiB
C#
Raw Permalink Normal View History

아웃게임 1차(PD #1093 세션 A) — 본영 로비 · 세이브 · 군량 · 영웅(레벨·승급 6칸·진화·스킬 표시) · 편성(전열 3/후열 2 · 프리셋 3) · 거래(뽑기·골드 장비 상점) · 스테이지 보상·별 3·소탕 - 데이터: 기획 tables/ 12장 → Assets/Data/OutGame/*.csv · 메뉴 TKS/Import OutGame Data(Editor/DataImporter) → Resources/OutGame 9 자산(HeroDb·GearDb·GachaConfig·EconomyConfig·RewardTable·LevelTable·PromotionTable·EvolveTable·SkillInfoDb) · 검증(행 수·필수 열·id 유일·hero↔skill↔gear·확률 합) 오류 시 자산 불변 · BalanceConfig.heroes[]/HeroDef 삭제 → HeroDb(기존 5인 id 유지) · economy.csv 키 17 + gacha.csv 키 2 추가(기획서 본문 값 · C45) - 세이브: persistentDataPath/save.json · ver 1 · 즉시 저장 · 오프라인 군량 회복 · 일일 초기화 · 신규 유저 = 유비·관우·장비·조운·황충 Lv1 녹 ★=희귀도 · 골드 1000 · 보옥 300 · 군량 30 · 테스트·캡처 = 메모리 전용 - 화면(uGUI 와이어프레임 · img03·06·08·11): 시작 = 본영 로비(출정 → 월드맵 · 편성 · 연무장/보물/내정 준비 중 · 동맹 추후) · 재화 바(월드맵 포함 · 군량 타이머) · 하단 탭 5 · ◀/Esc 일관 · 영웅 목록·상세·상세정보·도감 · 편성(탭·드래그) · 뽑기(천장 n/80 · 확률 공개 · 첫 10연 무료 · 10연 ★★ 보장 · 카드 순차 공개) · 장비 상점(흰/녹 6종 일일 · sell×5 · 새로고침 보옥 20) - 전투 연결: BattleManager.LineupProvider/Deployed(LineupEntry · §3-2 스탯 · 방어 배율 생성 뒤) · playerSpawnPoints = 전열 (−4/0/4, −12) · 후열 (−2/2, −14) · WorldMapScreen 훅(팝업 군량·보상·별 · 입장 군량 · 노드 별 star_on/off · 소탕 · ◀ 본영 · TopInset) · 결과 패널(별 3 조건 · 골드·경험·보옥·장비) · GameBootstrap/AutoPlayDriver 로비 시작 - B 세션 파일(ActionResolver·Combat·UnitView·BattleHUD·SlingshotInput·EnemyAI) 미변경 - 테스트 56/56(기존 41 유지 · 신규 OutGameTests 15) · console error 0 · 사본 GUI 캡처 11장(DataImporter.CaptureOutGame) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:11:10 +00:00
using System.Collections.Generic;
using UnityEngine;
namespace TKS.OutGame
{
/// <summary>뽑기 1장 결과.</summary>
public class GachaPull
{
public string heroId;
/// <summary>뽑힌 희귀도 등급(1~3 · 확률 표 기준).</summary>
public int tier;
public bool isNew;
public bool pickup;
public bool pity;
public int shards;
public int gold;
}
/// <summary>
/// 영웅·유닛 뽑기(기획서 §4 · gacha.csv · gacha_pool.csv): 등급 확률 p_r3/p_r2/p_r1 · 천장 = pity 번째 ★★★ 확정(획득 시 0) ·
/// ★★★ 중 pickup_share 는 픽업 영웅 · 10연 = ten_guarantee_rarity 이상 1 보장 · 병사 유닛 = ★ 풀 · 중복 = 조각(shard_dup_r*) / 병사 = 골드(soldier_dup_gold) · 첫 10연 무료.
/// </summary>
public static class GachaRules
{
public static bool FirstTenFree(OutGameData db, SaveData d)
{
return db.Gacha.FreeFirstTen && !d.gacha.firstTenUsed;
}
/// <summary>count 회 비용(1 = cost_single · ten_count = cost_ten · 첫 10연 무료).</summary>
public static int Cost(OutGameData db, SaveData d, int count)
{
var g = db.Gacha;
if (count == g.TenCount) return FirstTenFree(db, d) ? 0 : g.CostTen;
return g.CostSingle * count;
}
public static string CanDraw(OutGameData db, SaveData d, int count)
{
int cost = Cost(db, d, count);
if (d.gem < cost) return "보옥 부족 " + d.gem + " / " + cost;
if (Candidates(db, 1).Count == 0) return "뽑기 풀 비었음";
return null;
}
/// <summary>풀에서 등급 tier 에 나오는 유닛(영웅 = 희귀도 = tier · 병사 = ★ 풀).</summary>
public static List<GachaConfig.PoolEntry> Candidates(OutGameData db, int tier)
{
var res = new List<GachaConfig.PoolEntry>();
foreach (var p in db.Gacha.pool)
{
if (!p.inPool) continue;
var h = db.Heroes.Get(p.heroId);
if (h == null) continue;
int t = h.isSoldier ? 1 : h.rarity;
if (t == tier) res.Add(p);
}
return res;
}
public static List<GachaPull> Draw(OutGameData db, SaveData d, int count, System.Random rng)
{
var res = new List<GachaPull>();
if (CanDraw(db, d, count) != null) return res;
var g = db.Gacha;
if (count == g.TenCount && FirstTenFree(db, d)) d.gacha.firstTenUsed = true;
else d.gem -= Cost(db, d, count);
int gearSlots = HeroRules.GearSlotCount(db);
bool guaranteed = false;
for (int i = 0; i < count; i++)
{
int n = d.gacha.pity + 1;
int tier;
bool pity = n >= g.Pity;
if (pity) tier = 3;
else
{
double r = rng.NextDouble();
tier = r < g.P(3) ? 3 : r < g.P(3) + g.P(2) ? 2 : 1;
}
if (tier >= g.TenGuaranteeRarity) guaranteed = true;
if (count == g.TenCount && i == count - 1 && !guaranteed) tier = g.TenGuaranteeRarity;
d.gacha.pity = tier == 3 ? 0 : n;
d.gacha.total++;
var pull = Pick(db, tier, rng);
if (pull == null) continue;
pull.pity = pity;
var h = db.Heroes.Get(pull.heroId);
if (HeroRules.Grant(db, d, h, gearSlots)) pull.isNew = true;
else if (h.isSoldier)
{
pull.gold = db.Economy.SoldierDupGold;
d.gold += pull.gold;
}
else
{
pull.shards = db.Economy.ShardDup(h.rarity);
d.Hero(h.id).shards += pull.shards;
}
res.Add(pull);
}
return res;
}
static GachaPull Pick(OutGameData db, int tier, System.Random rng)
{
var list = Candidates(db, tier);
for (int t = tier; list.Count == 0 && t > 1; t--) list = Candidates(db, t - 1); // 그 등급 풀이 비면 아래 등급
if (list.Count == 0) return null;
if (tier == 3)
{
var pick = new List<GachaConfig.PoolEntry>();
var rest = new List<GachaConfig.PoolEntry>();
foreach (var p in list) (p.pickup ? pick : rest).Add(p);
if (pick.Count > 0 && (rest.Count == 0 || rng.NextDouble() < db.Gacha.PickupShare)) list = pick;
else if (rest.Count > 0) list = rest;
}
var e = list[rng.Next(list.Count)];
return new GachaPull { heroId = e.heroId, tier = tier, pickup = e.pickup && tier == 3 };
}
}
}