ThreeKingdomsSlingshot/Assets/Scripts/OutGame/OutGameTables.cs

502 lines
26 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;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
namespace TKS.OutGame
{
/// <summary>아웃게임 데이터 9 자산 묶음(Resources/OutGame/&lt;형식 이름&gt;.asset · 임포트 = TKS/Import OutGame Data).</summary>
public class OutGameData
{
public const string ResourcesFolder = "OutGame/";
public HeroDb Heroes;
public GearDb Gear;
public GachaConfig Gacha;
public EconomyConfig Economy;
public RewardTable Rewards;
public LevelTable Levels;
public PromotionTable Promotion;
public EvolveTable Evolve;
public SkillInfoDb Skills;
public bool Complete
{
get { return Heroes != null && Gear != null && Gacha != null && Economy != null && Rewards != null && Levels != null && Promotion != null && Evolve != null && Skills != null; }
}
public ScriptableObject[] All
{
get { return new ScriptableObject[] { Heroes, Gear, Gacha, Economy, Rewards, Levels, Promotion, Evolve, Skills }; }
}
static OutGameData cached;
/// <summary>Resources 의 임포트 자산(한 번 읽고 캐시).</summary>
public static OutGameData Default
{
get
{
if (cached == null || !cached.Complete) cached = LoadResources();
return cached;
}
}
public static OutGameData LoadResources()
{
var d = new OutGameData
{
Heroes = Resources.Load<HeroDb>(ResourcesFolder + "HeroDb"),
Gear = Resources.Load<GearDb>(ResourcesFolder + "GearDb"),
Gacha = Resources.Load<GachaConfig>(ResourcesFolder + "GachaConfig"),
Economy = Resources.Load<EconomyConfig>(ResourcesFolder + "EconomyConfig"),
Rewards = Resources.Load<RewardTable>(ResourcesFolder + "RewardTable"),
Levels = Resources.Load<LevelTable>(ResourcesFolder + "LevelTable"),
Promotion = Resources.Load<PromotionTable>(ResourcesFolder + "PromotionTable"),
Evolve = Resources.Load<EvolveTable>(ResourcesFolder + "EvolveTable"),
Skills = Resources.Load<SkillInfoDb>(ResourcesFolder + "SkillInfoDb"),
};
if (!d.Complete) Debug.LogError("[TKS] 아웃게임 데이터 자산 없음(Resources/" + ResourcesFolder + ") — 메뉴 TKS/Import OutGame Data 를 실행하세요");
return d;
}
}
/// <summary>
/// 아웃게임 표(Assets/Data/OutGame/*.csv · utf-8-sig · 기획 tables/ 사본 12장) 해석·검증 → 9 자산. 형식 오류·참조 오류가 하나라도 있으면
/// errors 에 모으고 에디터 임포터는 자산을 바꾸지 않는다(기획서 §12 임포터 검증: 행 수·필수 열·id 유일·참조 무결 hero↔skill↔gear).
/// </summary>
public static class OutGameTables
{
public const string Folder = "Data/OutGame"; // Assets/ 아래
public static readonly string[] Files =
{
"heroes.csv", "skills.csv", "hero_levels.csv", "promotion.csv", "evolve.csv", "gear_items.csv",
"hero_gear.csv", "skill_levels.csv", "gacha.csv", "gacha_pool.csv", "stage_rewards.csv", "economy.csv",
};
public class Result
{
public readonly List<string> errors = new List<string>();
public readonly Dictionary<string, int> rowCounts = new Dictionary<string, int>();
public readonly OutGameData data = new OutGameData();
}
/// <summary>texts = Files 순서의 CSV 내용.</summary>
public static Result Parse(string[] texts)
{
var r = new Result();
var e = r.errors;
var rows = new Dictionary<string, List<Dictionary<string, string>>>();
for (int i = 0; i < Files.Length; i++)
{
var text = texts != null && i < texts.Length ? texts[i] : null;
if (text == null)
{
e.Add(Files[i] + ": 파일 없음");
rows[Files[i]] = new List<Dictionary<string, string>>();
continue;
}
rows[Files[i]] = StageTable.ReadCsv(text, Files[i], e);
r.rowCounts[Files[i]] = rows[Files[i]].Count;
}
var d = r.data;
d.Promotion = ParsePromotion(rows["promotion.csv"], e);
d.Evolve = ParseEvolve(rows["evolve.csv"], e);
d.Levels = ParseLevels(rows["hero_levels.csv"], e);
d.Gear = ParseGear(rows["gear_items.csv"], e);
d.Skills = ParseSkills(rows["skills.csv"], rows["skill_levels.csv"], e);
d.Heroes = ParseHeroes(rows["heroes.csv"], rows["hero_gear.csv"], d, e);
d.Gacha = ParseGacha(rows["gacha.csv"], rows["gacha_pool.csv"], d, e);
d.Rewards = ParseRewards(rows["stage_rewards.csv"], d, e);
d.Economy = ParseEconomy(rows["economy.csv"], d, e);
CheckSkillOwners(d, e);
return r;
}
// ---------- 표별 ----------
static PromotionTable ParsePromotion(List<Dictionary<string, string>> rows, List<string> e)
{
var t = ScriptableObject.CreateInstance<PromotionTable>();
const string f = "promotion.csv";
if (!Cols(rows, f, e, "grade", "order", "promo_pct", "next_grade")) return t;
var seen = new HashSet<string>();
foreach (var r in rows)
{
var row = new PromotionTable.Row { grade = Get(r, "grade"), order = Int(r, "order", f, e), promoPct = Float(r, "promo_pct", f, e), nextGrade = Get(r, "next_grade") };
if (!Unique(seen, row.grade, f, r, e)) continue;
t.rows.Add(row);
}
int tops = 0;
foreach (var row in t.rows)
{
if (row.nextGrade.Length == 0) tops++;
else if (t.Get(row.nextGrade) == null) e.Add(f + ": " + row.grade + " next_grade 「" + row.nextGrade + "」 없음");
}
if (t.rows.Count > 0 && tops != 1) e.Add(f + ": 최고 등급(next_grade 빈 행)이 1개가 아님(" + tops + ")");
return t;
}
static EvolveTable ParseEvolve(List<Dictionary<string, string>> rows, List<string> e)
{
var t = ScriptableObject.CreateInstance<EvolveTable>();
const string f = "evolve.csv";
if (!Cols(rows, f, e, "star", "shards_needed", "star_pct", "skill_unlock")) return t;
foreach (var r in rows)
t.rows.Add(new EvolveTable.Row { star = Int(r, "star", f, e), shardsNeeded = Int(r, "shards_needed", f, e), starPct = Float(r, "star_pct", f, e), skillUnlock = Get(r, "skill_unlock") });
for (int i = 0; i < t.rows.Count; i++)
if (t.rows[i].star != i + 1) e.Add(f + ": star 가 1부터 연속이 아님(" + (i + 1) + "번째 = " + t.rows[i].star + ")");
return t;
}
static LevelTable ParseLevels(List<Dictionary<string, string>> rows, List<string> e)
{
var t = ScriptableObject.CreateInstance<LevelTable>();
const string f = "hero_levels.csv";
if (!Cols(rows, f, e, "lv", "exp_to_next", "gold_cost", "stat_pct", "cap_chapter")) return t;
foreach (var r in rows)
t.rows.Add(new LevelTable.Row { lv = Int(r, "lv", f, e), expToNext = Int(r, "exp_to_next", f, e), goldCost = Int(r, "gold_cost", f, e), statPct = Float(r, "stat_pct", f, e), capChapter = Int(r, "cap_chapter", f, e) });
for (int i = 0; i < t.rows.Count; i++)
if (t.rows[i].lv != i + 1) e.Add(f + ": lv 가 1부터 연속이 아님(" + (i + 1) + "번째 = " + t.rows[i].lv + ")");
return t;
}
static GearDb ParseGear(List<Dictionary<string, string>> rows, List<string> e)
{
var t = ScriptableObject.CreateInstance<GearDb>();
const string f = "gear_items.csv";
if (!Cols(rows, f, e, "id", "name_ko", "kind", "grade", "sell_gold", "drop_from_chapter", "craft_from", "craft_count", "craft_blueprint")) return t;
var seen = new HashSet<string>();
foreach (var r in rows)
{
var it = new GearDb.Item
{
id = Get(r, "id"), nameKo = Get(r, "name_ko"), kind = Get(r, "kind"), grade = Get(r, "grade"),
sellGold = Int(r, "sell_gold", f, e), dropFromChapter = Int(r, "drop_from_chapter", f, e), craftFrom = Get(r, "craft_from"),
craftCount = Int(r, "craft_count", f, e), craftBlueprint = Int(r, "craft_blueprint", f, e),
};
if (!Unique(seen, it.id, f, r, e)) continue;
t.items.Add(it);
}
// 등급 순서 = 합성 사슬 깊이(흰 = craft_from 없음 = 1)
foreach (var it in t.items)
{
int depth = 1;
var cur = it;
while (cur != null && cur.craftFrom.Length > 0 && depth <= t.items.Count)
{
var prev = t.Get(cur.craftFrom);
if (prev == null)
{
e.Add(f + ": " + cur.id + " craft_from 「" + cur.craftFrom + "」 없음");
break;
}
cur = prev;
depth++;
}
it.order = depth;
}
var orderOf = new Dictionary<string, int>();
foreach (var it in t.items)
{
int o;
if (orderOf.TryGetValue(it.grade, out o) && o != it.order) e.Add(f + ": 등급 " + it.grade + " 의 합성 깊이가 행마다 다름(" + it.id + ")");
orderOf[it.grade] = it.order;
}
return t;
}
static SkillInfoDb ParseSkills(List<Dictionary<string, string>> rows, List<Dictionary<string, string>> lvRows, List<string> e)
{
var t = ScriptableObject.CreateInstance<SkillInfoDb>();
const string f = "skills.csv";
스킬 시스템(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
if (Cols(rows, f, e, "id", "hero_id", "kind", "name_ko", "hanja", "trigger", "effect_key", "p1", "p2", "p3", "cooldown", "lv_max", "lv_scale", "desc_ko", "needs_impl"))
아웃게임 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
{
var seen = new HashSet<string>();
foreach (var r in rows)
{
var s = new SkillInfoDb.Skill
{
id = Get(r, "id"), heroId = Get(r, "hero_id"), kind = Get(r, "kind"), nameKo = Get(r, "name_ko"), hanja = Get(r, "hanja"),
trigger = Get(r, "trigger"), cooldown = Int(r, "cooldown", f, e), lvMax = Int(r, "lv_max", f, e), descKo = Get(r, "desc_ko"),
스킬 시스템(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
effectKey = Get(r, "effect_key"), lvScale = Float(r, "lv_scale", f, e),
아웃게임 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
};
if (!Unique(seen, s.id, f, r, e)) continue;
스킬 시스템(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
string at = f + " 행 " + Get(r, "#") + " " + s.id;
if (s.kind != "active" && s.kind != "passive") e.Add(at + ": kind 「" + s.kind + "」 ≠ active|passive");
// 스킬 처리기 열(B 단계) — trigger · effect_key 51종 · p 칸 · needs_impl
bool active = s.kind == "active";
if (active ? (s.trigger != "next_action" && s.trigger != "instant") : s.trigger != "always")
e.Add(at + ": trigger 「" + s.trigger + "」 (active = next_action|instant · passive = always)");
var ni = Get(r, "needs_impl");
if (ni != "0" && ni != "1") e.Add(at + ": needs_impl ≠ 0|1");
s.needsImpl = ni == "1";
var cols = new[] { "p1", "p2", "p3" };
var ps = new float[3];
for (int i = 0; i < 3; i++)
{
var v = Get(r, cols[i]);
if (v.Length == 0) continue;
if (!float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out ps[i])) e.Add(at + ": " + cols[i] + " 숫자 아님(「" + v + "」)");
else s.pMask |= 1 << i;
}
s.p1 = ps[0];
s.p2 = ps[1];
s.p3 = ps[2];
var keys = s.EffectKeys;
if (keys.Length == 0 || keys.Length > 3) e.Add(at + ": effect_key 수 " + keys.Length + " (1~3)");
for (int i = 0; i < keys.Length; i++)
{
if (!SkillKeys.Known(keys[i])) e.Add(at + ": effect_key 「" + keys[i] + "」 처리기 없음");
if (i < 3 && !s.HasP(i) && (keys.Length > 1 || i == 0)) e.Add(at + ": effect_key 「" + keys[i] + "」 의 p" + (i + 1) + " 비었음");
}
if (active && s.cooldown < 1) e.Add(at + ": 액티브 cooldown < 1");
if (s.lvMax < 1) e.Add(at + ": lv_max < 1");
아웃게임 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
t.skills.Add(s);
}
}
const string g = "skill_levels.csv";
if (Cols(lvRows, g, e, "lv", "books", "gold"))
foreach (var r in lvRows)
t.levels.Add(new SkillInfoDb.LevelCost { lv = Int(r, "lv", g, e), books = Int(r, "books", g, e), gold = Int(r, "gold", g, e) });
스킬 시스템(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
for (int i = 0; i < t.levels.Count; i++)
if (t.levels[i].lv != i + 1) e.Add(g + ": lv 가 1부터 연속이 아님(" + (i + 1) + "번째 = " + t.levels[i].lv + ")");
foreach (var s in t.skills)
if (s.lvMax > t.levels.Count) e.Add(f + ": " + s.id + " lv_max " + s.lvMax + " > skill_levels.csv 행 " + t.levels.Count);
아웃게임 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
return t;
}
static HeroDb ParseHeroes(List<Dictionary<string, string>> rows, List<Dictionary<string, string>> gearRows, OutGameData d, List<string> e)
{
var t = ScriptableObject.CreateInstance<HeroDb>();
const string f = "heroes.csv";
if (!Cols(rows, f, e, "id", "name_ko", "hanja", "faction", "unit_type", "rarity", "hp_mult", "atk_mult", "def_mult", "art", "skill_active", "skill_passive", "is_soldier", "first_stage")) return t;
var seen = new HashSet<string>();
foreach (var r in rows)
{
UnitType type;
if (!UnitTypeInfo.TryParseKey(Get(r, "unit_type"), out type)) e.Add(f + " 행 " + Get(r, "#") + ": unit_type 「" + Get(r, "unit_type") + "」 모름");
var soldier = Get(r, "is_soldier");
if (soldier != "0" && soldier != "1") e.Add(f + " 행 " + Get(r, "#") + ": is_soldier ≠ 0|1");
var h = new HeroDb.Hero
{
id = Get(r, "id"), nameKo = Get(r, "name_ko"), hanja = Get(r, "hanja"), faction = Get(r, "faction"), type = type,
rarity = Int(r, "rarity", f, e), hpMult = Float(r, "hp_mult", f, e), atkMult = Float(r, "atk_mult", f, e), defMult = Float(r, "def_mult", f, e),
art = Get(r, "art"), skillActive = Get(r, "skill_active"), skillPassive = Get(r, "skill_passive"), isSoldier = soldier == "1", firstStage = Get(r, "first_stage"),
};
if (!Unique(seen, h.id, f, r, e)) continue;
if (h.rarity < 1 || h.rarity > 3) e.Add(f + ": " + h.id + " rarity " + h.rarity + " ≠ 1~3");
foreach (var sid in new[] { h.skillActive, h.skillPassive })
{
if (sid.Length == 0) continue;
var s = d.Skills != null ? d.Skills.Get(sid) : null;
if (s == null) e.Add(f + ": " + h.id + " 스킬 「" + sid + "」 이 skills.csv 에 없음");
else if (s.heroId != h.id) e.Add(f + ": " + h.id + " 스킬 「" + sid + "」 의 hero_id = " + s.heroId);
}
if (!h.isSoldier && (h.skillActive.Length == 0 || h.skillPassive.Length == 0)) e.Add(f + ": 영웅 " + h.id + " 액티브·패시브 스킬 칸이 비었음");
t.heroes.Add(h);
}
const string g = "hero_gear.csv";
if (!Cols(gearRows, g, e, "hero_id", "grade", "slot", "gear_id")) return t;
int slotCount = 0;
foreach (var r in gearRows) slotCount = Mathf.Max(slotCount, Int(r, "slot", g, e));
foreach (var r in gearRows)
{
var h = t.Get(Get(r, "hero_id"));
var grade = Get(r, "grade");
int slot = Int(r, "slot", g, e);
var gearId = Get(r, "gear_id");
string at = g + " 행 " + Get(r, "#");
if (h == null) { e.Add(at + ": hero_id 「" + Get(r, "hero_id") + "」 없음"); continue; }
if (h.isSoldier) { e.Add(at + ": 병사 유닛 " + h.id + " 은 승급 없음"); continue; }
if (d.Promotion == null || d.Promotion.Get(grade) == null) { e.Add(at + ": grade 「" + grade + "」 가 promotion.csv 에 없음"); continue; }
if (d.Gear == null || d.Gear.Get(gearId) == null) { e.Add(at + ": gear_id 「" + gearId + "」 가 gear_items.csv 에 없음"); continue; }
if (slot < 1) { e.Add(at + ": slot " + slot); continue; }
HeroDb.GearSet set = null;
foreach (var s in h.gear)
if (s.grade == grade) set = s;
if (set == null)
{
set = new HeroDb.GearSet { grade = grade };
for (int i = 0; i < slotCount; i++) set.slots.Add("");
h.gear.Add(set);
}
if (set.slots[slot - 1].Length > 0) e.Add(at + ": " + h.id + " " + grade + " slot " + slot + " 중복");
set.slots[slot - 1] = gearId;
}
// 영웅마다 승급 가능한 등급(next_grade 있음) 전부 · 칸 전부 채움
if (d.Promotion != null)
foreach (var h in t.heroes)
{
if (h.isSoldier) continue;
foreach (var p in d.Promotion.rows)
{
if (p.nextGrade.Length == 0) continue;
var slots = t.GearFor(h, p.grade);
if (slots == null) { e.Add(g + ": " + h.id + " " + p.grade + " 요구 장비 없음"); continue; }
for (int i = 0; i < slots.Count; i++)
if (slots[i].Length == 0) e.Add(g + ": " + h.id + " " + p.grade + " slot " + (i + 1) + " 비었음");
}
}
return t;
}
static GachaConfig ParseGacha(List<Dictionary<string, string>> rows, List<Dictionary<string, string>> poolRows, OutGameData d, List<string> e)
{
var t = ScriptableObject.CreateInstance<GachaConfig>();
KeyValues(rows, "gacha.csv", GachaConfig.RequiredKeys, t.values, e);
if (AllKeys(t.values, GachaConfig.RequiredKeys))
{
float sum = t.P(1) + t.P(2) + t.P(3);
if (Mathf.Abs(sum - 1f) > 1e-4f) e.Add("gacha.csv: p_r1 + p_r2 + p_r3 = " + sum.ToString("0.####", CultureInfo.InvariantCulture) + " ≠ 1");
}
const string f = "gacha_pool.csv";
if (!Cols(poolRows, f, e, "hero_id", "in_pool", "pickup")) return t;
var seen = new HashSet<string>();
foreach (var r in poolRows)
{
var p = new GachaConfig.PoolEntry { heroId = Get(r, "hero_id"), inPool = Get(r, "in_pool") == "1", pickup = Get(r, "pickup") == "1" };
if (!Unique(seen, p.heroId, f, r, e)) continue;
if (d.Heroes == null || d.Heroes.Get(p.heroId) == null) e.Add(f + " 행 " + Get(r, "#") + ": hero_id 「" + p.heroId + "」 가 heroes.csv 에 없음");
t.pool.Add(p);
}
return t;
}
static RewardTable ParseRewards(List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
{
var t = ScriptableObject.CreateInstance<RewardTable>();
const string f = "stage_rewards.csv";
if (!Cols(rows, f, e, "chapter", "gold_base", "exp_base", "gem_first", "gem_boss", "gear_drop_pct", "gear_grade_max", "star_turns")) return t;
var seen = new HashSet<string>();
foreach (var r in rows)
{
var row = new RewardTable.Row
{
chapter = Int(r, "chapter", f, e), goldBase = Int(r, "gold_base", f, e), expBase = Int(r, "exp_base", f, e), gemFirst = Int(r, "gem_first", f, e),
gemBoss = Int(r, "gem_boss", f, e), gearDropPct = Float(r, "gear_drop_pct", f, e), gearGradeMax = Get(r, "gear_grade_max"), starTurns = Int(r, "star_turns", f, e),
};
if (!Unique(seen, row.chapter.ToString(CultureInfo.InvariantCulture), f, r, e)) continue;
if (d.Gear == null || d.Gear.OrderOf(row.gearGradeMax) == 0) e.Add(f + ": 챕터 " + row.chapter + " gear_grade_max 「" + row.gearGradeMax + "」 가 gear_items.csv 등급에 없음");
t.rows.Add(row);
}
return t;
}
static EconomyConfig ParseEconomy(List<Dictionary<string, string>> rows, OutGameData d, List<string> e)
{
var t = ScriptableObject.CreateInstance<EconomyConfig>();
KeyValues(rows, "economy.csv", EconomyConfig.RequiredKeys, t.values, e);
if (!AllKeys(t.values, EconomyConfig.RequiredKeys)) return t;
var starts = t.StartHeroes;
if (starts.Length == 0) e.Add("economy.csv: start_heroes 비었음");
foreach (var id in starts)
if (d.Heroes == null || d.Heroes.Get(id) == null) e.Add("economy.csv: start_heroes 「" + id + "」 가 heroes.csv 에 없음");
foreach (var g in t.ShopGrades)
if (d.Gear == null || d.Gear.OrderOf(g) == 0) e.Add("economy.csv: shop_grades 「" + g + "」 가 gear_items.csv 등급에 없음");
if (t.StaminaRegenSec <= 0) e.Add("economy.csv: stamina_regen_sec ≤ 0");
if (t.PresetCount < 1) e.Add("economy.csv: preset_count < 1");
return t;
}
static void CheckSkillOwners(OutGameData d, List<string> e)
{
if (d.Skills == null || d.Heroes == null) return;
foreach (var s in d.Skills.skills)
if (d.Heroes.Get(s.heroId) == null) e.Add("skills.csv: " + s.id + " hero_id 「" + s.heroId + "」 가 heroes.csv 에 없음");
}
// ---------- 도우미 ----------
static void KeyValues(List<Dictionary<string, string>> rows, string f, string[] required, KeyValueList into, List<string> e)
{
if (!Cols(rows, f, e, "key", "value")) return;
var seen = new HashSet<string>();
foreach (var r in rows)
{
var k = Get(r, "key");
if (!Unique(seen, k, f, r, e)) continue;
into.entries.Add(new KeyValueList.Entry { key = k, value = Get(r, "value") });
}
foreach (var k in required)
if (!into.Has(k)) e.Add(f + ": 필수 키 「" + k + "」 없음");
}
static bool AllKeys(KeyValueList kv, string[] keys)
{
foreach (var k in keys)
if (!kv.Has(k)) return false;
return true;
}
static bool Cols(List<Dictionary<string, string>> rows, string f, List<string> e, params string[] cols)
{
if (rows.Count == 0)
{
e.Add(f + ": 행 없음");
return false;
}
bool ok = true;
foreach (var c in cols)
{
if (rows[0].ContainsKey(c)) continue;
e.Add(f + ": 필수 열 「" + c + "」 없음");
ok = false;
}
return ok;
}
static bool Unique(HashSet<string> seen, string id, string f, Dictionary<string, string> r, List<string> e)
{
if (string.IsNullOrEmpty(id))
{
e.Add(f + " 행 " + Get(r, "#") + ": id 비었음");
return false;
}
if (!seen.Add(id))
{
e.Add(f + " 행 " + Get(r, "#") + ": id 「" + id + "」 중복");
return false;
}
return true;
}
static string Get(Dictionary<string, string> r, string key)
{
string v;
return r.TryGetValue(key, out v) ? v : "";
}
static int Int(Dictionary<string, string> r, string key, string f, List<string> e)
{
int v;
if (int.TryParse(Get(r, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out v)) return v;
e.Add(f + " 행 " + Get(r, "#") + ": " + key + " 정수 아님(「" + Get(r, key) + "」)");
return 0;
}
static float Float(Dictionary<string, string> r, string key, string f, List<string> e)
{
float v;
if (float.TryParse(Get(r, key), NumberStyles.Float, CultureInfo.InvariantCulture, out v)) return v;
e.Add(f + " 행 " + Get(r, "#") + ": " + key + " 숫자 아님(「" + Get(r, key) + "」)");
return 0f;
}
/// <summary>파싱 중 만든 자산 인스턴스 정리(임포트 실패·테스트).</summary>
public static void Destroy(Result r)
{
if (r == null) return;
foreach (var so in r.data.All)
{
if (so == null) continue;
if (Application.isPlaying) UnityEngine.Object.Destroy(so);
else UnityEngine.Object.DestroyImmediate(so);
}
}
}
}