Sprint 01 W2 — Transcendent Drop / Milestone / Affix Importer + Runtime Lookup 기술 설계
작성: 2026-04-21 (code-lead) · 근거:transcendent_drop_table_README.mdv1.0 APPROVED,transcendent_reward_draft.mdv1.0 APPROVED 문서 상태: APPROVED v1.0 (PD 승인 2026-04-21) 범위: Sprint 01 W2 기술 설계 전용 — 실구현 코드 제외 CP 추적: CP1(§3) · CP2(§4) · CP3(§4·§8) · CP4(§5) · CP5(§6) · CP6 = CP5 이월 원칙 내장 · CP7(§7) --- ## PD 승인 결정 (2026-04-21) | # | 질문 | 확정 | |---|------|------| | DQ1 | SO 구조 | 옵션 A — 개별 SO 3개 (DropRateTableSO/MilestoneRewardCatalogSO/AffixPoolSO) | | DQ2 | 메뉴 진입점 | 개별 4개 + ALL 메뉴 1개 | | DQ3 |TsvReader공통화 | Sprint 02 이월 — W2에선 Stage Importer 재건드림 금지 (Sprint 00 회귀 방지) | | DQ4 | 테스트 asmdef | 신설GodBlade.Tests.Editor| 블로커 해소:com.unity.test-framework: 1.4.5는Unity6/Packages/manifest.json:10에 이미 존재 (packages-lock 자동 1.4.6 해석). §9 컴파일러 시뮬레이션 체크리스트 11개(CP9-1~CP9-11) 필수 — PR 머지 전 Console 새 에러 0 확인 원칙.
0. 한 줄 요약
TSV 3종을 Sprint 00 메뉴 방식 Importer 패턴 그대로 복제·확장하여 ScriptableObject 3개(옵션 A) 로 변환, 런타임은 Dictionary<(floor_bucket_id, mobKind, grade), float> 단일 해시 O(1) 조회. Grade 리매핑은 Data 어셈블리의 GradeRemap 정적 클래스가 단일 진실. DOTS BlobAssetReference는 Sprint 02 이후 판단(지금 도입하면 마이그레이션 2회).
1. 데이터 모델 결정
1.1 3 옵션 비교
| 옵션 | 구조 | 장점 | 단점 | W2 적합성 |
|---|---|---|---|---|
| A. 개별 SO 3개 | DropRateTableSO + MilestoneRewardCatalogSO + AffixPoolSO | Sprint 00 TranscendentStageCatalogSO 패턴 1:1 재활용 / 각 TSV 변경 시 영향 분리 / Addressables 주소 단순 | SO 3개 로딩 필요 (방치형 부팅 1회) | 권장 |
| B. 통합 SO 1개 | RewardDatabaseSO 단일체 | 한 번 로드 / FK 조합 쉬움 | 60+22+6 = 88행 단일 덩어리 → diff noise / TSV별 빌드 타임 격리 불가 | 과결합 |
| C. DOTS BlobAssetReference | BlobArray<DropRow> 3종 | 수천 아이템 드롭 롤 시 캐시 친화 / GC 0 | W2 범위 초과 / 에디터 도구·마이그레이션 비용 큼 / 측정 없이 도입 원칙 위반 | 기각 (증거 없음) |
1.2 권장안 = 옵션 A + DOTS 스키마 사전 호환
- A 채택 이유: Sprint 00
TranscendentStageCatalogSO→ Importer → Addressables 경로가 이미 검증됨(2건 회귀 포함). 같은 패턴 3회 복제가 가장 싼 길. - DOTS 호환 배려:
DropRateRow같은 row 타입을[System.Serializable] struct로 잡고 모든 필드blittable(int/enum/float)로 유지 → Sprint 02+BlobArray<DropRateRow>로 이사 가능 (SO → Blob 전환 시 필드 매핑 1:1). - 무엇을 하지 않는가: Grade 리매핑 결과까지 저장 (런타임 enum은 신규 6등급만, 원본 4등급은 Importer가 변환 후 버림).
1.3 SO 세 개 필드 초안 (요약)
DropRateTableSO
List<DropRateRow> rows // 60행
struct DropRateRow { int floorMin; int floorMax; MobKind mob; Grade grade; float rate; }
[NonSerialized] Dictionary<(int,MobKind,Grade),float> _lookup // OnEnable lazy build
[NonSerialized] List<(int min, int max)> _bucketIndex // 경계 검색용
MilestoneRewardCatalogSO
List<MilestoneRewardRow> rows // 22행
struct MilestoneRewardRow {
int floor; string milestoneName;
RewardType type; string rewardCode; // "TBD_" 허용
int count; Grade? gradeFallback; string notes;
}
IReadOnlyList<MilestoneRewardRow> GetRowsAtFloor(int floor)
AffixPoolSO
List<AffixSkeletonRow> rows // 6행
struct AffixSkeletonRow {
Grade grade; int affixSlotCount;
int prefixSlotCount; int suffixSlotCount;
float valueRollMinPct; float valueRollMaxPct;
string legacyGradeMap; // read-only 메모 (Sprint 02 주입 힌트)
int rerollCostGold;
}
모두 GodBlade.Data 어셈블리. Sprint 00 SO와 같은 [CreateAssetMenu] 부착, Addressables 주소는 §2.4.
2. Importer 클래스 설계
2.1 파일 레이아웃 (전부 Editor 어셈블리)
Assets/Editor/
GodBlade.Editor.asmdef (기존)
Importers/
TranscendentStageImporter.cs (Sprint 00 — 손대지 않음)
DropRateTableImporter.cs (신규, 60행)
MilestoneRewardImporter.cs (신규, 22행)
AffixPoolImporter.cs (신규, 6행)
RewardImportAll.cs (신규, 4개 메뉴 순차 실행)
Shared/
TsvReader.cs (신규, 3개 Importer 공통)
GradeParser.cs (신규, "Rare"→Grade.Rare(신규 Epic 의미 NOT 원본!))
2.2 ScriptedImporter vs MenuItem — MenuItem 채택
- Sprint 00
TranscendentStageImporter가[MenuItem]방식 검증 완료. 단일 패턴 유지. - ScriptedImporter는 TSV가 Assets/ 내부에 놓여야 함. 본 프로젝트 TSV는
designs/폴더(저장소 루트, Unity 프로젝트 외부). 이식 비용 없음. - 대신 메뉴 4개로 엔지니어 선택권 제공:
GodBlade/Data/Import Transcendent Stage TSV(Sprint 00 기존)GodBlade/Data/Import Drop Rate Table TSV(신규)GodBlade/Data/Import Milestone Reward TSV(신규)GodBlade/Data/Import Affix Pool TSV(신규)GodBlade/Data/Import ALL (Stage → Drop → Milestone → Affix)(신규, 의존 순서 보장)
2.3 의존 순서 = Import ALL 한 진입점이 보장
[1] TranscendentStageImporter.Import() (Sprint 00) — Stage SO 200개 + Catalog
[2] DropRateTableImporter.Import() — Stage에 의존하지 않음(독립)
[3] MilestoneRewardImporter.Import() — floor→Stage FK 없음(Grant 시 lookup)
[4] AffixPoolImporter.Import() — 독립
- 실제로 Stage SO에 대한 컴파일 타임 의존은 없음(보상 TSV는 floor만 들고 있음). 런타임 Granter가 catalog.GetByFloor()로 조회.
- 따라서 순서 강제는 "안전 마진"일 뿐. 메뉴로 개별 실행도 무방.
2.4 공통 Const 규약 (Sprint 00과 동일 룩&필)
OUT_DIR = "Assets/_Project/Data/Rewards"
ADDR_GROUP = "Rewards"
ADDR_LABEL = "reward-table"
Addresses:
"Rewards/DropRateTable"
"Rewards/MilestoneRewardCatalog"
"Rewards/AffixPool"
2.5 TsvReader 공통화 (Sprint 00 private Row 승격)
- Sprint 00
TranscendentStageImporter내부Row/ReadTsv/ValidateHeader3개를Shared/TsvReader.cs로 추출. - Sprint 00 Importer는 동작 불변 — 새 공통 모듈을 호출하도록 최소 편집 (Sprint 00 검증 자산 파괴 금지).
- 공통 API 시그니처:
public static class TsvReader {
public struct Row { string Get(string k); int I(string k); float F(string k); double D(string k); }
public static List<Row> Read(string path, string[] expectedHeaders); // BOM trim + ValidateHeader 포함
}
- 주의 (§9 재발 방지): TsvReader.cs는
Editor/Importers/Shared/에 두므로 Editor 어셈블리 포함. Data 어셈블리에서 참조 금지.
3. Grade 리매핑 (CP1 대응)
3.1 위치 = GodBlade.Data 어셈블리 (Assets/_Project/Scripts/Data/GradeRemap.cs)
- 이유:
Gradeenum은 런타임 전역 표준. Runtime·Editor·Data 모두 참조. GodBlade.Data.asmdef는references: []빈 배열 유지 가능(의존 없음).
3.2 Enum 구조 초안
namespace GodBlade.Data {
public enum Grade {
Common = 0, // 원본 Normal
Magic = 1, // 신규 (원본 없음, Sprint 02 Normal 복제)
Rare = 2, // 원본 Superior ← ★ 의미 변경 주의
Epic = 3, // 원본 Rare ← ★ 절대 혼동 금지
Legendary = 4, // 원본 Unique
Mythic = 5, // 신규 (원본 없음)
}
public enum MobKind { Normal = 0, Boss = 1 }
public enum RewardType {
CurrencyGold,
EquipmentRandom,
AffixRerollToken,
GuardianExp,
GuardianStone,
CosmeticTitle,
CosmeticAura,
Unknown,
}
public static class GradeRemap {
// 원본 XML Grade 리터럴 4종 → 신규 enum
// Sprint 01 W2: TSV 내부 리터럴(신규 6종) 직접 파싱 + 이 맵은 Sprint 02 XML 주입 때 쓰임
public static bool TryFromLegacy(string legacyLiteral, out Grade grade) { ... }
// TSV 내부 신규 리터럴 파싱
public static bool TryParseNew(string literal, out Grade grade) { ... }
}
public sealed class UnknownGradeException : System.Exception {
public string Literal { get; }
public string SourceTsv { get; }
public int RowNumber { get; }
}
}
3.3 리매핑 테이블 (하드코딩, Dictionary)
| 원본 리터럴(XML, Sprint 02 주입용) | 신규 Grade | 주의 |
|---|---|---|
"Normal" | Grade.Common | 1:1 |
"Superior" | Grade.Rare | 상향 1단계 |
"Rare" | Grade.Epic | ★ 원본 Rare ≠ 신규 Rare. 코멘트 3줄 필수 |
"Unique" | Grade.Legendary | 1:1 |
그 외("Magic"/"Mythic"/대소문자 변형/오타) | UnknownGradeException 발생 |
3.4 Unknown 정책
- Importer는 Row 단위로 try/catch 하지 않음. 첫 Unknown 만나면 즉시
UnknownGradeExceptionthrow → Importer 중단 +EditorUtility.DisplayDialog+ 콘솔 Error. - 이유: 조용한 0 채움(Sprint 00에서 BOM으로 한 번 당했음)보다 "큰 소리로 실패"가 낫다. PD 일관 방침.
Literal,SourceTsv,RowNumber세 필드 포함 → 엔지니어가 10초 안에 수정 위치 파악.
4. 드롭 테이블 Lookup (CP2·CP3 대응)
4.1 자료구조 = Dictionary<Key, float>
readonly struct DropRateKey : System.IEquatable<DropRateKey> {
public readonly byte BucketId; // 0~4, 5개 버킷 (1-30, 31-80, 81-130, 131-180, 181-200)
public readonly byte MobKind; // Normal=0 / Boss=1
public readonly byte Grade; // 0~5
// GetHashCode = BucketId | (MobKind<<4) | (Grade<<5) → 컴팩트
public bool Equals(DropRateKey o) => ...
}
Dictionary<DropRateKey, float> _lookup // 60 엔트리 (5×2×6)
4.2 왜 Dictionary인가 (vs 다차원 배열)
| 접근 | Dictionary | float[5,2,6] |
|---|---|---|
| 조회 O(1) | 해시 + EqualityComparer | 직접 인덱싱 (더 빠름) |
| 누락 검증 | TryGetValue false → 0 감지 쉬움 | 0.0 가 "미정"인지 "진짜 0"인지 모호 |
| 직렬화 | SO에는 List<DropRateRow> 저장, OnEnable에서 Dictionary 빌드 | 다차원 배열은 Unity 직렬화 불가 → 래핑 필요 |
| 확장 | mobKind 추가 쉬움 | 상수 5/2/6 고정 — 추가 시 리팩터 |
선택: Dictionary + OnEnable lazy build (Sprint 00 TranscendentStageCatalogSO._byCode 패턴 그대로).
4.3 Floor → BucketId 변환 (CP3 경계값)
static byte FloorToBucket(int floor) {
if (floor <= 30) return 0;
if (floor <= 80) return 1;
if (floor <= 130) return 2;
if (floor <= 180) return 3;
return 4; // 181~200
}
- 이분 탐색 불필요 — 버킷 5개. 상수 분기가 캐시·분기예측에 최선.
- 경계 규칙:
min <= floor <= max(양끝 포함) — Sprint 01 W1 README §6-CP3 과 일치. DropRateRow.floorMin/floorMax는 검증용으로만 저장 (런타임 조회는 BucketId만 씀). 불변성 자기검증 로그로 활용.
4.4 런타임 API 초안 (DropRateTableSO 메서드)
public float GetDropRate(int floor, MobKind mob, Grade grade);
// floor 1~200 → BucketId → key → dict. miss면 0.0 + Debug.LogWarning(한 번만).
public Grade RollGrade(int floor, MobKind mob, System.Random rng);
// 6등급 weighted pick. 내부에서 GetDropRate 6회 + 누적분포 생성 (캐시 고려는 Sprint 02 측정 후).
4.5 드롭률 합 자기검증 (Importer)
- Import 끝에서 5개 버킷 × 2개 mobKind = 10개 합을 계산.
|sum - 1.000| > 0.001→Debug.LogError+Dialog(런타임 Crash 금지, Editor 전용 Alert).- 로그 포맷:
[DropRateTableImporter] SUM ERROR bucket=81-130 mob=Normal sum=0.9997 expected=1.000
5. TBD_ 플레이스홀더 처리 (CP4)
5.1 감지
MilestoneRewardRow.rewardCode가string타입.reward_code컬럼이1(정수 문자열),TBD_EQUIP_POOL,TBD_REROLL,TBD_TITLE_100등 혼재.- Importer가 prefix 검사:
bool isPlaceholder = rewardCode.StartsWith("TBD_", StringComparison.Ordinal);
- 현재 TSV 22행 중
TBD_*= 14행 (필수 Skip 아님, 표시만 함).
5.2 정책 = 로드는 하되 런타임 Grant는 Skip + Warn
- Import 단계: 모든 row를 SO에 저장 (placeholder 포함). 데이터 손실 없음.
- 런타임
MilestoneGranter.Award(floor)내부에서 row.IsPlaceholder = true면:
Debug.LogWarning($"[MilestoneGranter] TBD reward skipped: floor={floor} code={rewardCode}");
continue; // 다음 row 진행
- 이유: Sprint 02에서
ItemInfo.xml매핑 완료되면 TSV만 갱신 + Importer 재실행하면 자동 활성. 코드 변경 0.
5.3 TODO / Issue 스텁
MilestoneRewardRow구조체에public bool IsPlaceholder계산 프로퍼티 부착.- 코드 주석 고정 형식:
// TODO(Sprint 02): TBD_EQUIP_POOL → RewardItemInfo.xml StageGeneralCode=5001 계열 매핑
// TBD_REROLL → 신규 아이템 등록 (game-designer 협업)
// TBD_TITLE_* → 칭호 시스템 설계 후 (Sprint 04+)
- PM에게 Sprint 02 킥오프 때 "TBD 14건 치환 과제" 별도 티켓 등록 요청.
6. 어픽스 뼈대만 W2 처리 (CP5)
6.1 W2 범위 = AffixPoolSO 6행만
AffixSkeletonRow6개(Common ~ Mythic) 필드 전부 채워짐.- 실수치 풀 없음:
OptionItemRandom211행은 W2에서 건드리지 않음. XML 파서 구현 금지(PD 명시 제약).
6.2 Sprint 02 확장 시 마이그레이션 0 설계
public sealed class AffixPoolSO : ScriptableObject {
public List<AffixSkeletonRow> rows; // W2: 6행 (grade → slot 뼈대)
public List<AffixValueEntry> valueEntries; // W2: 빈 리스트. Sprint 02가 211행 채움.
}
[System.Serializable]
public struct AffixValueEntry {
public Grade grade;
public int optionRandomGroupId; // 원본 OptionItemRandom.OptionRandomGroupID
public int optionDivision;
public int optionRank;
public float valueMin;
public float valueMax;
public int rate; // 10000 분율
// ... 6컬럼 나머지
}
- 핵심:
AffixValueEntry구조체를 W2에 미리 선언 → Sprint 02에선 필드 추가 없이valueEntriesList에 push만. - SO 파일
.asset직렬화 포맷 안정 → PR diff 깨끗.
6.3 Sprint 02에서 필요한 것 (인지만, W2는 구현 안 함)
OptionInfo.xmlXLS-XML 파서 (SpreadsheetML 2003, Sprint 00extract_transcendent_stage.py와 같은 포맷).OptionRandomGroupID와AffixSkeletonRow.grade매핑 규칙 (game-designer와 재협의 — 원본은 직업별 풀, 신규는 Grade별 풀로 축소).
7. 마일스톤 독립 경로 (CP7)
7.1 MilestoneGranter 위치·API
- 위치:
Assets/_Project/Scripts/Runtime/Rewards/MilestoneGranter.cs, 어셈블리GodBlade.Runtime. - 네임스페이스:
GodBlade.Runtime.Rewards(신규 서브 네임스페이스 — Runtime rootNamespace와 충돌 없음, §9 체크 포인트).
7.2 API 초안
namespace GodBlade.Runtime.Rewards {
public sealed class MilestoneGranter {
private readonly MilestoneRewardCatalogSO _catalog;
public MilestoneGranter(MilestoneRewardCatalogSO catalog) { ... }
// 드롭 Roll과 독립. FloorReached 이벤트 채널 한 번만 호출.
public IReadOnlyList<GrantResult> Award(int floor);
public readonly struct GrantResult {
public readonly MilestoneRewardRow Row;
public readonly bool Skipped; // TBD placeholder면 true
public readonly string SkipReason;
}
}
}
7.3 로드 시점
GodBlade.Runtime내RewardBootstrap(미존재, Sprint 01 W2 추가) 이 Addressables에서 3개 SO를 병렬 로드.- Sprint 00
SmokeTester의AsyncOperationHandle<T>패턴 재사용. 3개 핸들WhenAll유사 동시 대기.
7.4 드롭 Roll과 중복 지급 방지 (이벤트 채널 분리)
이벤트 채널 A: OnMonsterKilled(floor, mobKind, monsterCode)
→ LootRoller (드롭 Roll: DropRateTableSO + AffixPoolSO)
이벤트 채널 B: OnFloorReached(floor) ← 층 클리어 1회만 발화
→ MilestoneGranter.Award(floor) ← 10/50/100/150/200에만 실제 지급
- 두 채널이 서로 호출하지 않음(결합 없음).
MilestoneRewardCatalogSO는 floor 키를 Set으로 미리 캐시(_milestoneFloors) →OnFloorReached가 매번 전체 스캔 안 하도록. 22행이라 성능 이슈 없지만 방어적.
8. 테스트 전략 (CP3 보강)
8.1 Edit 모드 = 본 스프린트 타겟
com.unity.test-framework1.4.5 (scaffold manifest 이미 포함 가정) 활용.- Play 모드는 Addressables 로드 필요 → W2 범위 초과. Sprint 02에서 Play-mode smoke.
8.2 파일 배치
Assets/_Project/Tests/Editor/
GodBlade.Tests.Editor.asmdef (신규, testFramework 참조)
DropRateTableLookupTests.cs
GradeRemapTests.cs
MilestoneCatalogTests.cs
§9 재발 경고: asmdef.references에 UnityEngine.TestRunner, UnityEditor.TestRunner, GodBlade.Data, GodBlade.Runtime, GodBlade.Editor 명시. defineConstraints: ["UNITY_INCLUDE_TESTS"].
8.3 Floor 경계값 테스트 8개 (CP3)
[TestCase(30, 0)] [TestCase(31, 1)]
[TestCase(80, 1)] [TestCase(81, 2)]
[TestCase(130, 2)] [TestCase(131, 3)]
[TestCase(180, 3)] [TestCase(181, 4)]
public void FloorToBucket_BoundariesAreInclusiveOnLower(int floor, int expectedBucket);
추가 테스트:
DropRate_SumIsOnePerBucket_PerMobKind(10개 합계)GradeRemap_RareLegacy_MapsToEpic_NotRare(★ 의미 충돌 가드)GradeRemap_UnknownLiteral_ThrowsMilestoneRow_TbdPrefix_IsPlaceholderTrueMilestoneCatalog_Floor200_HasSixRows
9. 정적 분석 한계 + 컴파일러 시뮬레이션 체크리스트
PD 명시 요청 섹션. 2026-04-21 Sprint 00 회귀 2건(CS0246 ResourceManager 네임스페이스 누락 / CS1061 멤버 접근 실패)이 정적 리뷰를 통과했지만 실컴파일에서 드러났다. W2 PR 제출 전 다음 11개 체크 포인트 를 코드 작성자가 직접 확인한다.
9.1 체크 포인트 (11개)
- [CP9-1] asmdef references 배열 완결성 — 신규 테스트 asmdef
GodBlade.Tests.Editor.asmdef에 다음 누락 재발 금지:
UnityEngine.TestRunnerUnityEditor.TestRunnerGodBlade.Data/GodBlade.Runtime/GodBlade.Editor- (Sprint 00 재발 사례)
Unity.ResourceManager— AddressablesAsyncOperationHandle<T>건드리면 필수
- [CP9-2] Editor asmdef의
Unity.Addressables.Editor유지 — 기존GodBlade.Editor.asmdef은 이미 포함. 신규 Importer들이AddressableAssetSettingsDefaultObject를 건드리면 이 참조 없으면 CS0246. - [CP9-3]
internal멤버 +[InternalsVisibleTo]— 새 SO 중 Editor만 쓸 세터가 있으면TranscendentStageSO.cs상단의[assembly: InternalsVisibleTo("GodBlade.Editor")]패턴 복제. 새 SO 파일마다 중복 선언은 금지 (중복 정의 CS0579). 한 파일에만. - [CP9-4]
#if UNITY_EDITOR가드 경계 — Data asmdef는 Editor 전용 심볼 참조 금지.UnityEditor.*사용 블록은 반드시#if UNITY_EDITOR로 감싸거나 Editor 어셈블리로 이동. - [CP9-5] 신규 네임스페이스 충돌 —
GodBlade.Runtime.Rewards/GodBlade.Data.Rewards(X, 하위 안 만듦) /GodBlade.EditorTools.Rewards(X, 평탄 유지) 중 Runtime만 하위 네임스페이스 도입. rootNamespace가GodBlade.Runtime이므로 파일 최상단namespace GodBlade.Runtime.Rewards로 명시. 자동 삽입 방지 위해 asmdefrootNamespace수정 금지.
NOTE (2026-04-21 scaffold sync audit 반영): Editor asmdef의 파일명은GodBlade.Editor이나 rootNamespace / 파일 내 네임스페이스는GodBlade.EditorTools(Sprint 00 scaffold 컨벤션 유지). 본 문서에서GodBlade.Editor.*표기는 asmdef 이름 기준이며, 실제 코드 작성 시namespace GodBlade.EditorTools/GodBlade.EditorTools.Rewards사용. asmdefrootNamespace수정 금지.
- [CP9-6] TSV
Split('\t')CRLF/LF 처리 —File.ReadAllLines는 플랫폼 개행을 자동 분리하나, Windows TSV를 LF만 남기는 편집기에서 복사된 경우 마지막 줄에\r잔여. Row 파싱 후Trim()1회:
for (int c = 0; c < max; c++) row.Fields[header[c]] = cells[c].Trim();
Sprint 00 TranscendentStageImporter.ReadTsv 에 없음 — 공통 TsvReader 추출 시 함께 보강.
- [CP9-7] BOM 트림 중복 적용 — 공통
TsvReader한 곳만 BOM trim. Sprint 00 기존 함수에서 제거(중복 작업 방지 — 단 테스트 돌려 회귀 없음 확인 후 제거). - [CP9-8] Dictionary 직렬화 금지 — Unity는
Dictionary<K,V>직렬화 안 함. SO는 반드시List<Row>로 저장,[NonSerialized]Dictionary는OnEnable에서 빌드. Sprint 00 Catalog 패턴 유지. - [CP9-9] Enum underlying type 고정 —
Grade : int기본이지만, DOTS 전환 고려하면byte로 선언 가능. W2에서는 int 유지 (Unity Inspector 표시, Serialize compatibility 안정). Sprint 02+ DOTS 전환 시 한 번에 byte로. - [CP9-10]
System.Random는 런타임 전용 / Editor Import 단계는 rng 금지 — Importer는 결정론적(input TSV만). rng 사용 zero.LootRoller(Sprint 02)에서 도입. - [CP9-11] Addressables 그룹 중복 생성 방지 — 신규
Rewards그룹을 Stage 그룹과 별도로 만들 것.settings.FindGroup("Rewards")로 기존 확인 후 없으면 생성. Sprint 00TranscendentStageImporter.RegisterAddressables패턴 복제.
9.2 Sprint 00 회귀 2건 재발 경고 (반드시 읽기)
- 회귀 A (CS0246): Runtime asmdef가
Unity.ResourceManager미참조 →AsyncOperationHandle<T>타입 해석 실패. → CP9-1, CP9-2 확인 필수. - 회귀 B (CS1061): Editor 메뉴에서 Data asmdef의 private 세터 호출 →
InternalsVisibleTo선언 파일 오타 / 어셈블리 이름 불일치 → CP9-3 확인 필수. - 두 건 모두 정적 리뷰 통과, 실컴파일에서 검출. 본 설계 W2 코드 작성자는 Unity 에디터 연 상태로 파일 저장 → Console 새 에러 0 확인 후 PR 올림.
10. 리스크 · 미해결
10.1 W2 내 발견 가능 추가 회귀 후보 3건
- R-W2-1: TSV 마지막 행 개행 누락 시
File.ReadAllLines가 마지막 row 누락 없이 반환하나, 에디터에 따라\r미포함 라인이null로 해석되는 예외.IsNullOrWhiteSpace필터는 Sprint 00에 있음 — 회귀 방지 유지. - R-W2-2:
TsvReader공통화 시 Sprint 00TranscendentStageImporter가 깨질 가능성. 대응: 1차 PR은 공통화 없이 새 3개 Importer 단독 구현, 2차 PR에서 공통화 리팩터(독립 테스트). PD 결정 DQ3 참조. - R-W2-3:
System.ValueTuple((int,MobKind,Grade)) 를 딕셔너리 키로 쓰면 기본 비교자가 box 유발. →readonly struct DropRateKey : IEquatable<>로 대체(§4.1 초안 반영). Sprint 00 딕셔너리는<int, SO>단일키라 해당 없음.
10.2 Sprint 02 이월 경계선 (W2 하지 않는 것)
| 항목 | W2 범위 | Sprint 02 |
|---|---|---|
TranscendentStageReward 3459행 주입 | ❌ | ✅ |
OptionItemRandom 211행 주입 | ❌ | ✅ |
| XML 파서 구현 | ❌ | ✅ |
Grade 리매핑 코드(GradeRemap.TryFromLegacy) | ✅ 작성만 | Sprint 02에서 호출 시작 |
| TBD_ 치환 | ❌ (Skip+Warn만) | ✅ |
LootRoller (드롭 Roll 실행) | ❌ | ✅ |
AffixRoller (어픽스 실수치 추첨) | ❌ | ✅ |
| Addressables 플레이모드 로드 테스트 | ❌ | ✅ |
| 프레스티지 PP 적립 훅 | ❌ | prestige_system_spec.md v1.0 Sprint 02 |
10.3 Unity.ResourceManager asmdef 참조 재확인
GodBlade.Runtime.asmdef에 이미 포함(확인 완료, 파일 라인 5"Unity.ResourceManager").GodBlade.Editor.asmdef에도 포함(파일 라인 9"Unity.ResourceManager").- 신규
GodBlade.Tests.Editor.asmdef에도 복사 → CP9-1 핵심 항목.
10.4 시즌 리셋 / 프레스티지 호환성 (스키마 수준만 인지)
prestige_system_spec.mdv1.0 의 PPpermanent/seasonal2채널은 W2 구현 제외.- 단,
MilestoneRewardRow.notes에floor=200의"GodBlade" 칭호 → 시즌 프레스티지 트리거기록 보존.MilestoneGranter.Award(200)구현 시 별도 이벤트 미발화, TODO(Sprint 02) 주석만.
11. PD 결정 대기 열린 질문 (4건)
| # | 질문 | 옵션 | code-lead 추천 |
|---|---|---|---|
| DQ1 | SO 구조는 옵션 A(개별 3개) / B(통합) / C(DOTS Blob) 중? | A / B / C | A. DOTS는 Sprint 02 이후 측정 기반 재검토. |
| DQ2 | Importer 진입점은 개별 메뉴 4개 + ALL 메뉴 1개 / 개별만 / ALL만? | 개별+ALL / 개별만 / ALL만 | 개별+ALL. 엔지니어 선택권 + 원자 실행 모두 제공. |
| DQ3 | TsvReader 공통화를 W2에 포함 / Sprint 02로 이월? | 포함 / 이월 | 이월. R-W2-2 회귀 위험 — W2 1차 PR은 Sprint 00 건드리지 않음. W2 말 별도 리팩터 PR. |
| DQ4 | 테스트 asmdef 신설 / 기존 Editor asmdef 재활용? | 신설 / 재활용 | 신설 (GodBlade.Tests.Editor). testFramework / defineConstraints 격리. |
12. 분량 · CP 추적
- 본 문서 약 7쪽 (마크다운 렌더 기준).
- CP1(§3 GradeRemap) / CP2(§4.1-4.4 Dict Lookup) / CP3(§4.3, §8.3 경계값) / CP4(§5 TBD) / CP5(§6 어픽스 뼈대) / CP6(§6.2 W2 범위 제한 = CP5 이월 원칙) / CP7(§7 독립 채널) 전부 커버.
- §9 정적 분석 체크 포인트 11개.
- 실구현 코드 줄 수: 0 (설계 전용 — PD 제약 준수).