StatTag 후보 배열 + 인스턴스구조로 확정
Stat배열이란 이 파츠가 가질 수 있는 스텟 후보 배열 (치명타, 공격력, 치명타 데미지 증가 등)
이로인해 FNSPartData에 FGameplayTag StatTag필드 추가
지금 파츠 GE에 들어있는게
UNSRewardHandler::HandleRewardTrigger -> UNSRewardDropResolver::ResolveDropResultsFromTable (DropTable->DropResults) -> UNSRewardDropResolver::SelectDropRow -> UNSRewardDropResolver::ApplyDropRowToResult
UNSRewardHandler::HandleDropResults -> (RewardTypeTag == Part) -> UNSRewardHandler::HandlePartDropResult -> UNSRewardHandler::MakePartDataFromDropResult
이 과정을 통하고 있다.
| Before | After | |
|---|---|---|
FNSPartDefinitionRow | StatTag(단일) | StatTags(배열, 후보 목록) |
FNSPartData | 스탯 없음 (Row에서 파생) | StatTag(신규, 인스턴스 확정 스탯) |
드롭/상점 생성 시 StatTags 후보 중 하나가 균등 확률로 뽑혀 인스턴스에 저장된다. 같은 "암" Definition 하나로 치명타 변형/공격력 변형을 동시에 표현할 수 있게 됐다.
| Before | After | |
|---|---|---|
FNSStatDisplayInfoRow | MaxStatValue(스탯 만점 1개) | ValueRangesByRarity(TMap<ENSPartRarity, FNSPartValueRange>) |
FNSPartUpgradeRow | ValueRange(품질 0~1) + 리롤/상점 필드 | FNSPartShopRerollRow로 개명, 리롤/상점 필드만 (수치 필드 없음) |
| 최종 수치 계산 | 품질 롤(0~1) × MaxStatValue | 등급 범위에서 직접 롤 |
"공격력: 커먼 0~10 / 레어 10~20 / 에픽 20~30 / 레전더리 30~40" 처럼 스탯마다 등급 곡선을 독립적으로 그릴 수 있게 됐다. 카운트 계열(점프/대시)은 등급 범위에 정수를 직접 넣으면 되어, 이전에 floor 오차를 피하려고 쓰던 안전마진 역산(1.5, 2 같은 값)이 더 이상 필요 없다.
파츠 인스턴스 생성 (드롭 / 인런 상점 / 아웃런 구매)
1. 파츠 종류 확정 (전체 후보 균등 랜덤)
2. 등급 확정 (드롭테이블 가중치 / 상점 가중치)
3. StatTags 후보를 현재 등급 기준으로 필터링
└ NSPartUtils::FilterStatTagsByRarity
└ 후보마다 GetStatValueRange(스탯, 등급) 조회, 실패하면 후보에서 제외
4. 남은 후보 중 스탯 하나 확정 (균등 랜덤) → FNSPartData::StatTag
5. 그 스탯의 등급별 범위에서 최종 수치 롤 → FNSPartData::CurrentValue
└ NSPartUtils::GetStatValueRange(확정 스탯, 등급) → FMath::RandRange
리롤 / 등급업
- 스탯(StatTag)은 유지, 3~5단계만 재실행 (RollValueForPart 공용 함수)
공용 GE 적용 (장착 시)
- GetPartStatTag(인스턴스 우선, 구세이브는 후보 첫 번째 폴백)
- StatTag → SetByCaller 태그 매핑(NSCombatStatAttributeMapping) → GE Modifier 적용
증상: MaxJumpCount 스탯을 파츠 시스템에 추가하고 캐릭터 스폰 시 기본값(1)이 적용되지 않고 0으로 보임.
원인: FGameplayAttributeData는 별도 초기값을 안 주면 기본이 0이다. 캐릭터 스폰 시 실제 시작값은 ApplyInitialAttributeEffect가 별도의 Init GE(CharacterBaseStatInitEffectClass)를 적용해서 DT 기본값을 SetByCaller로 박아 넣는 구조인데, 이 Init GE 애셋은 기존에 만들어진 것이라 신규 추가한 MaxJumpCount 스탯용 Modifier가 빠져 있었다. Internal_ApplySharedGE가 쓰는 공용 파츠 GE(GE_SharedPartEffectClass)와는 별개 애셋이라, 파츠 GE에만 Modifier를 추가하고 Init GE 쪽을 빠뜨리기 쉽다.
해결: Init GE에 Attribute=MaxJumpCount, Op=Override, SetByCaller=Effect.SetByCaller.Init.MaxJumpCount Modifier 추가. 기존 MaxHealth, MaxDashCount 등의 Modifier 구성을 그대로 참고.
교훈: 새 Attribute를 추가하면 GE 두 개(공용 파츠 GE + 캐릭터 Init GE)에 각각 Modifier를 넣어야 한다. 하나만 잊어도 컴파일 에러 없이 조용히 0으로 남아서 발견이 늦어진다.
증상: JumpMaxCount가 2 이상이 된 상태에서 스페이스를 떼지 않고 있으면, 착지하지 않았는데도 다음 점프가 즉시 또 나가버림. 아웃런에서는 파츠 없이도 항상 발생, 인런에서는 다단 점프 파츠를 얻은 순간부터 발생.
원인: 언리얼 점프 입력은 엣지(눌리는 순간) 감지가 아니라 홀드 방식이다. bPressedJump가 true인 동안 매 틱 CanJumpInternal()을 재검사하는데, JumpMaxCount=1일 때는 공중에서 JumpCurrentCount >= JumpMaxCount라 자연히 막혔던 것뿐이었다. JumpMaxCount를 2 이상으로 올리는 순간, 홀드 중에도 남은 충전량만큼 계속 재판정되어 발동되는 엔진 기본 동작이 그대로 드러났다. 아웃런은 ApplyInitialAttributeEffect가 실행되지 않아 네이티브 ACharacter::JumpMaxCount 엔진 기본값(2)이 그대로 남아있던 것으로 추정.
해결: ANSPlayerCharacterBase에 "마지막 점프 이후 키를 뗀 적이 있어야만 다음 점프 허용" 게이트 추가.
CanJumpInternal_Implementation(): JumpCurrentCount > 0 && !bHasReleasedJumpKeySinceLastJump면 거부OnJumped_Implementation(): 점프 성공 시 플래그를 false로ClearJumpInput(float DeltaTime): bPressedJump가 false인 프레임에 플래그를 true로 복귀1차 구현의 버그 — 서버 러버밴딩: 처음엔 릴리즈 감지를 StopJumping()(입력 바인딩 함수)에서 했는데, 이 함수는 조종하는 클라이언트에서만 호출된다. 접속자 클라가 2단 점프에 성공해도, 호스트의 서버 파트는 StopJumping()이 호출되지 않아 플래그가 영영 false로 남아 서버가 그 점프를 거부 → 서버 보정으로 캐릭터가 끌어내려짐(러버밴딩). 호스트 본인은 서버 파트가 직접 도니까 이 버그가 재현되지 않아서, 호스트 단독 테스트로는 절대 못 잡는다.
교훈:
StopJumping() 같은 입력 함수는 조종 클라 전용, ClearJumpInput/CanJumpInternal은 이동 검증 경로라 양쪽에서 실행된다.증상: 파츠 리롤/등급업 프리뷰 UI의 수치 범위 표시가 플레이어에게 의미 없는 소수(0.5~1.0 사이)로 보임.
원인: 파츠 품질 롤 시스템으로 전환하면서 DT_PartUpgrade::ValueRange의 의미가 "실제 수치 범위"에서 "품질 배율(0~1)"로 바뀌었는데, NSPartUpgradeWidget의 UI 코드는 이 값을 그대로 찍고 있었다(전환 당시 놓친 부분).
해결: UI에서 표시 직전에 ValueRange × 해당 파츠 StatTag의 MaxStatValue로 환산해서 실제 수치로 변환 후 표시.
교훈: DataTable 필드의 "의미"를 바꾸는 리팩터링(절대값 → 배율 등)을 하면, 그 필드를 직접 읽어 화면에 찍는 모든 지점을 찾아서 같이 고쳐야 한다. 게임 로직(값 계산)과 UI 표시 코드가 같은 원본 필드를 각자 따로 읽는 구조라 한쪽만 고치면 조용히 어긋난다.