ThreeKingdomsSlingshot/Assets/Scripts/OutGame/UI/TradeScreen.cs

383 lines
21 KiB
C#
Raw 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;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace TKS.OutGame
{
/// <summary>
/// 거래(img11 · 기획 S15) — ① 영웅·유닛 뽑기(픽업 · 천장 n/80 · 확률 공개 · 1회/10연 · 첫 10연 무료 · 결과 = 카드 순차 공개)
/// ② 보물 상자 = 준비 중 ③ 상점·패스 = 골드 승급 장비 상점(흰/녹 · 일일 · 새로고침 보옥) · 패스 = 준비 중.
/// </summary>
public class TradeScreen : OutGameScreen
{
int page;
readonly List<Button> tabs = new List<Button>();
readonly List<RectTransform> pages = new List<RectTransform>();
Text pityText;
RectTransform pityBar;
GameObject freeChip;
GameObject freeChip2;
Button singleBtn;
Button tenBtn;
RectTransform pickupArt;
RectTransform shopGrid;
ScrollRect shopScroll;
Text shopTimer;
Button refreshBtn;
GameObject resultRoot;
readonly List<GameObject> resultCards = new List<GameObject>();
Coroutine revealing;
public List<GachaPull> LastPulls { get; private set; }
public int Page
{
get { return page; }
}
public bool ResultVisible
{
get { return resultRoot != null && resultRoot.activeSelf; }
}
스킬 시스템(PD #1093 세션 B) — skills.csv effect_key 51종 처리기 · 액티브(예약/즉시)·패시브 · HUD 스킬 버튼 · 적 무장 패시브 · 스킬 레벨업·스킬서 - 데이터: SkillInfoDb(= 스킬 DB) 에 effect_key·p1~p3(pMask)·lv_scale·needs_impl 추가 · 임포터 검증(trigger · 키 51종 · p 칸 · skill_levels 연속 · lv_max) · skill_levels = levels(비용 표) - 처리기 Runtime/Skills: SkillKeys(51종 · 레벨 계수 = 값×계수 / 배율은 1 넘는 몫 / 횟수 반올림 / 켜기·치환 고정 · 팀 키) · SkillDef·SkillCatalog · SkillSystem(유닛별 쿨·예약 · 즉시형 시한 효과 이번 아군 턴/이번 적 턴 · 행동 훅 · 호위 분담 · HUD 한 줄) · SkillMods(전투 코드 수치 질의) · SkillRules(상한 = 등급 + ★3 액티브 +1 · ★5 패시브 ×1.5 = evolve.csv · 레벨업 · 스킬서 판매) - 전투 연결: ActionResolver(밀기·튕김·추가 이동 n·추가 이동 공격·충돌 추가 피해·밀며 전진 ×·삼첨도·관통 피해/넉백/반격 없음/빗나가도 복귀/한 번 더 관통·사거리·사선 통과·크리·광역 반경/비율/아군 0·백기습영 이동 후 사격·군령/대장군 공격 횟수) · Combat(상성 하한·공 %·주는/받는 피해·근접 패널티 해제·반격 배율/받는 반격·견제 배율) · BattleManager(편성·적 무장 부착 · 견제 반경/횟수/무제한/회복 · 교전 반응 정지 · 교전 범위 · 즉시형 실행) · EnemyAI·RangeIndicator(슬링·사거리 스킬 반영) - UI: HUD 스킬 버튼(이름·Lv·쿨·사용 가능/불가·패시브) · 탭 = 예약/취소/즉시 · 조준 중 2손가락 탭 · 발동 = 이름 글자 + 플래시 · 스킬 한 줄 · 예약 배지 · 변진 반경 원 · 영웅 상세 스킬 레벨업 팝업 · 거래 상점 스킬서 칸(임시) - 표: economy.csv start_skill_books 10 · skill_book_shop_daily 3 · skill_book_price 300(임시 · PD 확인) - 테스트 73/73(기존 56 + SkillTests 17 · 51종 전부) · console error 0 · 캡처 = DataImporter.CaptureSkills Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:31:27 +00:00
/// <summary>장비 상점 칸 수(스킬서 임시 판매 칸 제외).</summary>
아웃게임 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
public int ShopCardCount
{
스킬 시스템(PD #1093 세션 B) — skills.csv effect_key 51종 처리기 · 액티브(예약/즉시)·패시브 · HUD 스킬 버튼 · 적 무장 패시브 · 스킬 레벨업·스킬서 - 데이터: SkillInfoDb(= 스킬 DB) 에 effect_key·p1~p3(pMask)·lv_scale·needs_impl 추가 · 임포터 검증(trigger · 키 51종 · p 칸 · skill_levels 연속 · lv_max) · skill_levels = levels(비용 표) - 처리기 Runtime/Skills: SkillKeys(51종 · 레벨 계수 = 값×계수 / 배율은 1 넘는 몫 / 횟수 반올림 / 켜기·치환 고정 · 팀 키) · SkillDef·SkillCatalog · SkillSystem(유닛별 쿨·예약 · 즉시형 시한 효과 이번 아군 턴/이번 적 턴 · 행동 훅 · 호위 분담 · HUD 한 줄) · SkillMods(전투 코드 수치 질의) · SkillRules(상한 = 등급 + ★3 액티브 +1 · ★5 패시브 ×1.5 = evolve.csv · 레벨업 · 스킬서 판매) - 전투 연결: ActionResolver(밀기·튕김·추가 이동 n·추가 이동 공격·충돌 추가 피해·밀며 전진 ×·삼첨도·관통 피해/넉백/반격 없음/빗나가도 복귀/한 번 더 관통·사거리·사선 통과·크리·광역 반경/비율/아군 0·백기습영 이동 후 사격·군령/대장군 공격 횟수) · Combat(상성 하한·공 %·주는/받는 피해·근접 패널티 해제·반격 배율/받는 반격·견제 배율) · BattleManager(편성·적 무장 부착 · 견제 반경/횟수/무제한/회복 · 교전 반응 정지 · 교전 범위 · 즉시형 실행) · EnemyAI·RangeIndicator(슬링·사거리 스킬 반영) - UI: HUD 스킬 버튼(이름·Lv·쿨·사용 가능/불가·패시브) · 탭 = 예약/취소/즉시 · 조준 중 2손가락 탭 · 발동 = 이름 글자 + 플래시 · 스킬 한 줄 · 예약 배지 · 변진 반경 원 · 영웅 상세 스킬 레벨업 팝업 · 거래 상점 스킬서 칸(임시) - 표: economy.csv start_skill_books 10 · skill_book_shop_daily 3 · skill_book_price 300(임시 · PD 확인) - 테스트 73/73(기존 56 + SkillTests 17 · 51종 전부) · console error 0 · 캡처 = DataImporter.CaptureSkills Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:31:27 +00:00
get
{
int n = 0;
foreach (Transform c in shopGrid)
if (c.name != SkillBookCellName) n++;
return n;
}
}
public const string SkillBookCellName = "SkillBook";
/// <summary>스킬서 판매 칸이 있는가(PD #1093 B).</summary>
public bool SkillBookCellVisible
{
get { return shopGrid != null && shopGrid.Find(SkillBookCellName) != null; }
아웃게임 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
}
public override void Build()
{
Tab = 4;
Header("거래", "뽑기 · 상자 · 상점 · 패스", true);
var body = Body();
string[] names = { "영웅·유닛 뽑기", "보물 상자", "상점·패스" };
for (int i = 0; i < names.Length; i++)
{
int k = i;
tabs.Add(UiKit.Btn("Tab" + i, body, names[i], 32, UiKit.Paper, UiKit.Ink, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f + i * 348f, -18f), new Vector2(332f, 84f), () => SetPage(k)));
pages.Add(UiKit.Fill("Page" + i, body, 0f, 0f, 118f, 0f));
}
BuildGacha(pages[0]);
var chest = pages[1];
UiKit.Label(chest, "보물 상자\n<size=34>청동 · 은 · 금 · 명장 · 보옥 상자</size>\n\n<size=36>준비 중 (2차 · 시스템 기획 §8)</size>", 56, UiKit.Ink, TextAnchor.MiddleCenter,
new Vector2(0.5f, 0.6f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(900f, 500f));
BuildShop(pages[2]);
}
public void SetPage(int p)
{
page = Mathf.Clamp(p, 0, pages.Count - 1);
Refresh();
}
public void OpenShop()
{
Root.Show(this);
SetPage(2);
}
public override void OnShow()
{
S.EnsureShop();
}
// ---------- 뽑기 ----------
void BuildGacha(RectTransform p)
{
var g = S.Db.Gacha;
var banner = UiKit.TopBand("Banner", p, 12f, 440f, 24f, 24f);
UiKit.Paint(banner, new Color(0.27f, 0.14f, 0.12f));
var pick = UiKit.Box("Pickup", banner, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(30f, -24f), new Vector2(230f, 62f), UiKit.Red);
UiKit.FillLabel(pick.transform, "픽업 (2주)", 30, UiKit.White);
UiKit.Label(banner, "영웅·유닛 뽑기", 60, UiKit.White, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -110f), new Vector2(600f, 90f));
UiKit.Label(banner, "모든 영웅·병사 유닛\n= 뽑기로 획득", 32, new Color(0.9f, 0.82f, 0.78f), TextAnchor.UpperLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(36f, -210f), new Vector2(520f, 110f), false);
var free = UiKit.Box("Free", banner, new Vector2(0f, 0f), new Vector2(0f, 0f), new Vector2(36f, 28f), new Vector2(300f, 78f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(free.transform, "첫 10연 무료", 34, UiKit.Ink);
freeChip = free.gameObject;
pickupArt = UiKit.Rect("PickupArt", banner, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-20f, 10f), new Vector2(440f, 400f));
var pity = UiKit.TopBand("Pity", p, 468f, 130f, 24f, 24f);
var pimg = UiKit.Paint(pity, UiKit.Paper);
UiKit.Border(pimg, UiKit.Line, 3f);
UiKit.Label(pity, "천장 게이지", 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -10f), new Vector2(400f, 56f));
pityText = UiKit.Label(pity, "", 30, UiKit.Red, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -10f), new Vector2(560f, 56f));
pityBar = UiKit.Bar("PityBar", pity, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 18f), new Vector2(980f, 32f), new Color(0.86f, 0.82f, 0.75f), UiKit.Red);
var prob = UiKit.TopBand("Prob", p, 614f, 270f, 24f, 24f);
var primg = UiKit.Paint(prob, UiKit.Paper);
UiKit.Border(primg, UiKit.Line, 3f);
UiKit.Label(prob, "확률 공개", 36, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, -10f), new Vector2(400f, 56f));
UiKit.Label(prob, "확률·천장 전부 공개", 26, UiKit.InkSoft, TextAnchor.MiddleRight, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-24f, -10f), new Vector2(400f, 56f), false);
for (int t = 3; t >= 1; t--)
{
float y = -84f - (3 - t) * 58f;
UiKit.Label(prob, UiKit.Stars(t, t), 38, UiKit.Star, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(24f, y), new Vector2(160f, 50f));
var fill = UiKit.Bar("P" + t, prob, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(200f, y - 12f), new Vector2(380f, 28f), new Color(0.86f, 0.82f, 0.75f), t == 3 ? UiKit.Red : new Color(0.47f, 0.38f, 0.32f));
UiKit.SetBar(fill, g.P(t));
UiKit.Label(prob, (g.P(t) * 100f).ToString("0.#") + " %" + (t == 1 ? " (병사 유닛 포함)" : ""), 32, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 1f), new Vector2(0f, 1f), new Vector2(600f, y), new Vector2(420f, 50f));
}
var btns = UiKit.TopBand("Buttons", p, 904f, 170f, 24f, 24f);
singleBtn = UiKit.Btn("Single", btns, "", 40, UiKit.Dark, UiKit.White, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(500f, 160f), () => DoDraw(1));
tenBtn = UiKit.Btn("Ten", btns, "", 40, UiKit.Red, UiKit.White, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(500f, 160f), () => DoDraw(g.TenCount));
var f2 = UiKit.Box("Free2", tenBtn.transform, new Vector2(1f, 1f), new Vector2(1f, 0.5f), new Vector2(-10f, 0f), new Vector2(240f, 56f), new Color(0.93f, 0.75f, 0.24f));
UiKit.FillLabel(f2.transform, "첫 10연 무료", 26, UiKit.Ink);
freeChip2 = f2.gameObject;
var e = S.Db.Economy;
UiKit.Label(p, "중복 = 영웅 조각(★★★ " + e.ShardDup(3) + " · ★★ " + e.ShardDup(2) + " · ★ " + e.ShardDup(1) + ") · 병사 중복 = 골드 " + e.SoldierDupGold +
" · 10연 = ★★ 이상 1 보장", 26, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -1090f), new Vector2(1020f, 80f), false);
}
void RefreshGacha()
{
var g = S.Db.Gacha;
var d = S.Save;
pityText.text = d.gacha.pity + " / " + g.Pity + " · " + g.Pity + "회 ★★★ 확정";
UiKit.SetBar(pityBar, d.gacha.pity / (float)Mathf.Max(1, g.Pity));
bool free = GachaRules.FirstTenFree(S.Db, d);
freeChip.SetActive(free);
freeChip2.SetActive(free);
UiKit.LabelOf(singleBtn).text = "1회\n<size=32>보옥 " + g.CostSingle.ToString("N0") + "</size>";
UiKit.LabelOf(tenBtn).text = g.TenCount + "연\n<size=32>" + (free ? "무료" : "보옥 " + g.CostTen.ToString("N0")) + "</size>";
UiKit.Clear(pickupArt);
var picks = new List<HeroDb.Hero>();
foreach (var pe in g.pool)
if (pe.pickup && pe.inPool && S.Def(pe.heroId) != null) picks.Add(S.Def(pe.heroId));
for (int i = 0; i < picks.Count && i < 2; i++)
{
UiKit.Portrait("Pick" + i, pickupArt, picks[i], new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 50f), new Vector2(230f, 330f), false, 1f, new Color(1f, 1f, 1f, 0f));
UiKit.Label(pickupArt, picks[i].nameKo + " ★" + picks[i].rarity, 26, UiKit.White, TextAnchor.MiddleCenter, new Vector2(1f, 0f), new Vector2(1f, 0f), new Vector2(-i * 210f, 6f), new Vector2(230f, 44f));
}
}
public void DoDraw(int count)
{
var why = GachaRules.CanDraw(S.Db, S.Save, count);
if (why != null)
{
Root.Toast(why);
return;
}
LastPulls = S.Draw(count);
ShowResult(LastPulls);
}
// ---------- 뽑기 결과(카드 순차 공개) ----------
void ShowResult(List<GachaPull> pulls)
{
CloseResult();
var dim = UiKit.Fill("GachaResult", Root.TopRoot);
var dimg = UiKit.Paint(dim, new Color(0.08f, 0.05f, 0.04f, 0.9f), false, true);
dimg.raycastTarget = true;
var skip = dim.gameObject.AddComponent<Button>();
skip.transition = Selectable.Transition.None;
skip.onClick.AddListener(RevealAll);
resultRoot = dim.gameObject;
UiKit.Label(dim, "뽑기 결과", 64, UiKit.White, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -200f), new Vector2(900f, 100f));
int cols = Mathf.Min(5, Mathf.Max(1, pulls.Count));
int rows = (pulls.Count + cols - 1) / cols;
float cw = 196f, chh = 330f;
for (int i = 0; i < pulls.Count; i++)
{
int r = i / cols, c = i % cols;
int inRow = Mathf.Min(cols, pulls.Count - r * cols);
float x = (c - (inRow - 1) / 2f) * (cw + 12f);
float y = 120f - r * (chh + 20f) + (rows - 1) * (chh + 20f) / 2f;
resultCards.Add(ResultCard(dim, pulls[i], new Vector2(x, y), new Vector2(cw, chh)));
}
UiKit.Btn("Ok", dim, "확인", 40, UiKit.Red, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 180f), new Vector2(420f, 110f), CloseResult);
revealing = Root.StartCoroutine(Reveal());
}
GameObject ResultCard(RectTransform parent, GachaPull p, Vector2 pos, Vector2 size)
{
var h = S.Def(p.heroId);
var back = UiKit.Box("Card", parent, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), pos, size, new Color(0.35f, 0.22f, 0.18f));
UiKit.Border(back, new Color(0.8f, 0.62f, 0.3f), 3f);
UiKit.FillLabel(back.transform, "?", 90, new Color(0.8f, 0.62f, 0.3f));
var front = UiKit.Fill("Front", back.transform);
UiKit.Paint(front, p.tier == 3 ? new Color(1f, 0.93f, 0.75f) : p.tier == 2 ? new Color(0.92f, 0.9f, 0.98f) : UiKit.Paper);
UiKit.Portrait("Face", front, h, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -10f), new Vector2(176f, 176f), false, 1.8f, new Color(0.9f, 0.86f, 0.8f));
UiKit.Label(front, UiKit.Stars(p.tier, p.tier), 30, UiKit.Star, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -188f), new Vector2(190f, 40f));
UiKit.Label(front, h != null ? h.nameKo : p.heroId, 32, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -226f), new Vector2(190f, 44f));
string tag = p.isNew ? "<color=#b0301e>NEW</color>" : p.shards > 0 ? "조각 +" + p.shards : p.gold > 0 ? "골드 +" + p.gold : "";
if (p.pity) tag += " 천장";
if (p.pickup) tag += " 픽업";
UiKit.Label(front, tag, 26, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 10f), new Vector2(190f, 44f));
front.gameObject.SetActive(false);
return front.gameObject;
}
IEnumerator Reveal()
{
foreach (var c in resultCards)
{
yield return new WaitForSecondsRealtime(0.14f);
if (c != null) c.SetActive(true);
}
revealing = null;
}
public void RevealAll()
{
if (revealing != null) Root.StopCoroutine(revealing);
revealing = null;
foreach (var c in resultCards)
if (c != null) c.SetActive(true);
}
public void CloseResult()
{
if (revealing != null) Root.StopCoroutine(revealing);
revealing = null;
resultCards.Clear();
if (resultRoot != null)
{
resultRoot.SetActive(false);
Object.Destroy(resultRoot);
resultRoot = null;
}
Refresh();
}
public int RevealedCount
{
get
{
int n = 0;
foreach (var c in resultCards)
if (c != null && c.activeSelf) n++;
return n;
}
}
// ---------- 장비 상점 ----------
void BuildShop(RectTransform p)
{
var head = UiKit.TopBand("ShopHead", p, 12f, 110f, 24f, 24f);
UiKit.Label(head, "승급 장비 상점", 44, UiKit.Ink, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), Vector2.zero, new Vector2(400f, 70f));
var e = S.Db.Economy;
var grades = new List<string>();
foreach (var g in e.ShopGrades) grades.Add(UiKit.GradeKo(g));
UiKit.Label(head, "골드 · " + string.Join("/", grades.ToArray()) + " · 매일 갱신", 26, UiKit.InkSoft, TextAnchor.MiddleLeft, new Vector2(0f, 0.5f), new Vector2(0f, 0.5f), new Vector2(330f, -4f), new Vector2(400f, 50f), false);
shopTimer = UiKit.Label(head, "", 28, UiKit.Red, TextAnchor.MiddleRight, new Vector2(1f, 0.5f), new Vector2(1f, 0.5f), Vector2.zero, new Vector2(320f, 50f));
shopGrid = UiKit.ScrollGrid("ShopGrid", p, 24f, 24f, 136f, 280f, new Vector2(326f, 400f), new Vector2(17f, 20f), 3, out shopScroll);
refreshBtn = UiKit.Btn("Refresh", p, "", 36, UiKit.Jade, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 150f), new Vector2(640f, 104f), () =>
{
var why = S.RefreshShop();
Root.Toast(why ?? "상점 새로고침");
});
UiKit.Label(p, "청 이상 장비(보옥) · 성장 패스 · 월정액 · 챕터 팩 = 준비 중", 28, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 60f), new Vector2(1000f, 60f), false);
}
void RefreshShop()
{
S.EnsureShop();
var d = S.Save;
var left = EconomyRules.UntilNextDay(S.Now);
shopTimer.text = "갱신까지 " + ((int)left.TotalHours).ToString("00") + ":" + left.Minutes.ToString("00");
UiKit.LabelOf(refreshBtn).text = "새로고침 · 보옥 " + S.Db.Economy.ShopRefreshGem;
UiKit.Clear(shopGrid);
for (int i = 0; i < d.shop.items.Count; i++)
{
var it = d.shop.items[i];
var g = S.Db.Gear.Get(it.gearId);
int k = i;
var cell = UiKit.Cell(shopGrid, "Item" + i, it.sold ? new Color(0.86f, 0.84f, 0.8f) : UiKit.Paper, null);
UiKit.Border(cell, UiKit.Line, 2f);
var strip = UiKit.TopBand("Grade", cell.transform, 0f, 64f);
UiKit.Paint(strip, UiKit.GradeColor(g != null ? g.grade : ""));
UiKit.FillLabel(strip, (g != null ? UiKit.GradeKo(g.grade) : "?") + " 등급", 28, UiKit.White);
UiKit.Label(cell.transform, g != null ? g.nameKo : it.gearId, 40, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -86f), new Vector2(310f, 70f));
UiKit.Label(cell.transform, "보유 " + d.inv.GearCount(it.gearId) + " · 필요 영웅 " + NeedCount(it.gearId), 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -160f), new Vector2(310f, 44f), false);
UiKit.Label(cell.transform, "골드 " + it.price.ToString("N0"), 36, UiKit.Coin, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 120f), new Vector2(310f, 56f));
var b = UiKit.Btn("Buy", cell.transform, it.sold ? "품절" : "구매", 34, it.sold ? UiKit.Disabled : UiKit.Red, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 20f), new Vector2(260f, 84f), () =>
{
var why = S.BuyShop(k);
Root.Toast(why ?? (g != null ? g.nameKo : "") + " 구매 → 가방");
});
b.interactable = !it.sold;
}
스킬 시스템(PD #1093 세션 B) — skills.csv effect_key 51종 처리기 · 액티브(예약/즉시)·패시브 · HUD 스킬 버튼 · 적 무장 패시브 · 스킬 레벨업·스킬서 - 데이터: SkillInfoDb(= 스킬 DB) 에 effect_key·p1~p3(pMask)·lv_scale·needs_impl 추가 · 임포터 검증(trigger · 키 51종 · p 칸 · skill_levels 연속 · lv_max) · skill_levels = levels(비용 표) - 처리기 Runtime/Skills: SkillKeys(51종 · 레벨 계수 = 값×계수 / 배율은 1 넘는 몫 / 횟수 반올림 / 켜기·치환 고정 · 팀 키) · SkillDef·SkillCatalog · SkillSystem(유닛별 쿨·예약 · 즉시형 시한 효과 이번 아군 턴/이번 적 턴 · 행동 훅 · 호위 분담 · HUD 한 줄) · SkillMods(전투 코드 수치 질의) · SkillRules(상한 = 등급 + ★3 액티브 +1 · ★5 패시브 ×1.5 = evolve.csv · 레벨업 · 스킬서 판매) - 전투 연결: ActionResolver(밀기·튕김·추가 이동 n·추가 이동 공격·충돌 추가 피해·밀며 전진 ×·삼첨도·관통 피해/넉백/반격 없음/빗나가도 복귀/한 번 더 관통·사거리·사선 통과·크리·광역 반경/비율/아군 0·백기습영 이동 후 사격·군령/대장군 공격 횟수) · Combat(상성 하한·공 %·주는/받는 피해·근접 패널티 해제·반격 배율/받는 반격·견제 배율) · BattleManager(편성·적 무장 부착 · 견제 반경/횟수/무제한/회복 · 교전 반응 정지 · 교전 범위 · 즉시형 실행) · EnemyAI·RangeIndicator(슬링·사거리 스킬 반영) - UI: HUD 스킬 버튼(이름·Lv·쿨·사용 가능/불가·패시브) · 탭 = 예약/취소/즉시 · 조준 중 2손가락 탭 · 발동 = 이름 글자 + 플래시 · 스킬 한 줄 · 예약 배지 · 변진 반경 원 · 영웅 상세 스킬 레벨업 팝업 · 거래 상점 스킬서 칸(임시) - 표: economy.csv start_skill_books 10 · skill_book_shop_daily 3 · skill_book_price 300(임시 · PD 확인) - 테스트 73/73(기존 56 + SkillTests 17 · 51종 전부) · console error 0 · 캡처 = DataImporter.CaptureSkills Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:31:27 +00:00
AddSkillBookCell(d);
}
/// <summary>스킬서 임시 판매(PD #1093 B · 하루 skill_book_shop_daily 개 · 골드 skill_book_price · 정식 획득처 = 사당 던전(C) · PD 확인 대상).</summary>
void AddSkillBookCell(SaveData d)
{
var e = S.Db.Economy;
int left = Mathf.Max(0, e.SkillBookShopDaily - SkillRules.BooksBoughtToday(d, S.Now));
var cell = UiKit.Cell(shopGrid, SkillBookCellName, left > 0 ? UiKit.Paper : new Color(0.86f, 0.84f, 0.8f), null);
UiKit.Border(cell, UiKit.Line, 2f);
var strip = UiKit.TopBand("Grade", cell.transform, 0f, 64f);
UiKit.Paint(strip, UiKit.Purple);
UiKit.FillLabel(strip, "스킬서 · 임시 판매", 28, UiKit.White);
UiKit.Label(cell.transform, "스킬서", 40, UiKit.Ink, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -86f), new Vector2(310f, 70f));
UiKit.Label(cell.transform, "보유 " + d.inv.books.skill + " · 오늘 " + left + "/" + e.SkillBookShopDaily, 24, UiKit.InkSoft, TextAnchor.MiddleCenter, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0f, -160f), new Vector2(310f, 44f), false);
UiKit.Label(cell.transform, "골드 " + e.SkillBookPrice.ToString("N0"), 36, UiKit.Coin, TextAnchor.MiddleCenter, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 120f), new Vector2(310f, 56f));
var b = UiKit.Btn("BuyBook", cell.transform, left > 0 ? "구매" : "품절", 34, left > 0 ? UiKit.Red : UiKit.Disabled, UiKit.White, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0f, 20f), new Vector2(260f, 84f), () =>
{
var why = SkillRules.BuyBook(S.Db, S.Save, S.Now);
if (why == null) S.Commit();
Root.Toast(why ?? "스킬서 +1 (보유 " + S.Save.inv.books.skill + ")");
Refresh();
});
b.interactable = left > 0;
아웃게임 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
}
/// <summary>그 장비를 지금 등급 칸에서 기다리는(아직 안 채운) 보유 영웅 수.</summary>
int NeedCount(string gearId)
{
int n = 0;
foreach (var hs in S.Save.heroes)
{
var h = S.Def(hs.id);
var list = h != null ? S.Db.Heroes.GearFor(h, hs.grade) : null;
if (list == null) continue;
for (int i = 0; i < list.Count; i++)
if (list[i] == gearId && string.IsNullOrEmpty(hs.gear[i]))
{
n++;
break;
}
}
return n;
}
public override void Refresh()
{
for (int i = 0; i < tabs.Count; i++)
{
bool on = i == page;
tabs[i].GetComponent<Image>().color = on ? UiKit.Red : UiKit.Paper;
UiKit.LabelOf(tabs[i]).color = on ? UiKit.White : UiKit.Ink;
pages[i].gameObject.SetActive(on);
}
if (page == 0) RefreshGacha();
else if (page == 2) RefreshShop();
}
}
}