> For the complete documentation index, see [llms.txt](/llms.txt)

# GS2-Grade

그레이드·희귀도 기능



캐릭터나 장비의 육성에는 단기 성장 목표인 레벨업과는 별도로 그레이드업을 마련하는 것이 일반적인 사양입니다.
그레이드업을 하면 레벨 캡을 끌어올릴 수 있어 캐릭터나 장비를 더욱 강력하게 육성할 수 있게 됩니다.

그레이드업 방법에는 여러 가지가 있을 수 있으며, 육성 소재를 소비함으로써 그레이드업할 수 있는 것이나, 같은 종류의 캐릭터나 장비를 합성함으로써 그레이드업할 수 있는 것도 있습니다.
GS2-Grade는 방법을 따지지 않고, 그레이드별로 레벨 캡을 설정함으로써 GS2-Experience의 레벨 캡 운용을 더 간단하게 구현할 수 있도록 하는 것을 목적으로 합니다.

```mermaid
graph LR
  Player["플레이어의 소지물"] -- 그레이드업 --> Grade["GS2-Grade<br/>(그레이드 값 상승)"]
  Grade -- ApplyRankCap --> Experience["GS2-Experience<br/>(랭크 캡이 반영)"]
  Experience -- 경험치 획득 --> LevelUp["레벨업"]
```

## 그레이드

그레이드별로 GS2-Experience의 랭크 캡에 설정할 값을 정의할 수 있습니다.
그레이드는 캐릭터나 장비 같은 대상을 고유하게 가리키는 `propertyId`별로 관리되며, `Status`로 개별 보관됩니다.

| 필드 | 설명 |
| --- | --- |
| `gradeName` | 그레이드 모델 이름 |
| `propertyId` | 그레이드를 적용할 리소스를 나타내는 GRN(예: GS2-Inventory의 ItemSet) |
| `gradeValue` | 현재 그레이드 값(0부터 시작하는 정수) |

### 그레이드의 초기값

프로퍼티 ID가 정규 표현식에 일치하는지에 따라 초기 그레이드 값을 설정할 수 있습니다.
예를 들어, GS2-Inventory의 ItemModel 이름이 SSR로 시작하는 경우에는 그레이드 3, SR로 시작하는 경우에는 그레이드 2로 시작하도록 설정함으로써
ItemModel의 종류에 따라 초기 랭크 캡을 다르게 설정할 수 있습니다.

마스터 데이터의 `defaultGrades`에 여러 개의 `propertyIdRegex`와 `defaultGradeValue` 쌍을 등록함으로써 초기 그레이드를 선언합니다.

### 그레이드업 소재 판정

그레이드업 방법 중 "같은 종류의 캐릭터나 장비를 합성하는" 경우에, 소재로 사용하려는 리소스가 "같은 종류의 캐릭터나 장비"인지를 판정하는 처리를 지원하는 기능이 마련되어 있습니다.
그레이드의 프로퍼티 ID에서 정규 표현식으로 파라미터를 추출해 정규 표현식을 구성하고, 소재로 사용하려는 리소스의 프로퍼티 ID가 이에 일치하는지를 판정함으로써 구현합니다.

예를 들어, GS2-Inventory의 ItemSet이 같은 종류의 ItemModel에 의한 것인지를 판정하는 경우에는 다음과 같이 정규 표현식을 설정합니다.
```
propertyIdRegex: grn:gs2:{region}:{ownerId}:inventory:namespace-0001:user:(.*):inventory:character:item:(.*):.*
gradeUpPropertyIdRegex: grn:gs2:{region}:{ownerId}:inventory:namespace-0001:user:$1:inventory:character:item:$2:.*
```

이를 통해 propertyIdRegex를 사용해 "사용자ID"와 "ItemModel의 이름"을 추출합니다.
사용자ID가 "user-0001", ItemModel의 이름이 "item-0001", ItemSet의 이름이 "item-set-0001"이라면, ItemSet의 GRN(프로퍼티 ID)은
```
grn:gs2:{region}:{ownerId}:inventory:namespace-0001:user:user-0001:inventory:character:item:item-0001:item-set-0001
```
가 되며, gradeUpPropertyIdRegex 치환 후의 값은
```
gradeUpPropertyIdRegex: grn:gs2:{region}:{ownerId}:inventory:namespace-0001:user:user-0001:inventory:character:item:item-0001:.*
```
가 됩니다.
이를 통해 같은 종류의 아이템을 소재로 지정할 수 있는 표현이 가능해집니다.

## 보상 가산 테이블

그레이드 모델에는 `acquireActionRates`를 설정할 수 있으며, 그레이드에 따라 입수 액션의 보상량을 조정할 수 있습니다.
이를 활용하면 "고그레이드 캐릭터는 퀘스트 보상 획득량이 늘어난다", "그레이드별로 경험치 배율이 달라진다"와 같은 성장 요소를 구현할 수 있습니다.

| `mode` | 설명 |
| --- | --- |
| `double` | `rates`에 설정한 배율을 부동소수점으로 곱합니다. 일반적인 아이템 획득량 보정에 사용합니다. |
| `big` | `bigRates`에 설정한 배율을 문자열 형식으로 처리하며, int64를 초과하는 수치를 다루는 GS2-Inventory의 BigItem 등 대량 보상을 취급하는 경우에 대응합니다. |

트랜잭션의 입수 액션에 `Gs2Grade:MultiplyAcquireActionsByUserId`를 포함시키면, 지정한 그레이드의 배율을 적용한 입수 액션 목록으로 변환할 수 있습니다.

## 스크립트 트리거

네임스페이스에 `changeGradeScript`를 설정하면, 그레이드 변화 전후에 커스텀 스크립트를 실행할 수 있습니다.
트리거는 동기·비동기 실행 방식을 선택할 수 있으며, 비동기 처리에서는 GS2-Script나 Amazon EventBridge를 활용한 외부 연동도 가능합니다.

설정할 수 있는 주요 이벤트 트리거와 스크립트 설정 이름은 다음과 같습니다.

- `changeGradeScript`(완료 알림: `changeGradeDone`): 그레이드 변화 전후

## 트랜잭션 액션

GS2-Grade에서는 다음과 같은 트랜잭션 액션을 제공합니다.

| 종류 | 액션 | 설명 |
| --- | --- | --- |
| 검증 | `Gs2Grade:VerifyGradeByUserId` | 지정한 그레이드 값인지 검증 |
| 검증 | `Gs2Grade:VerifyGradeUpMaterialByUserId` | 그레이드업 소재로 유효한지(`gradeUpPropertyIdRegex`에 일치하는지)를 검증 |
| 소비 | `Gs2Grade:SubGradeByUserId` | 그레이드 감산 |
| 입수 | `Gs2Grade:AddGradeByUserId` | 그레이드 가산 |
| 입수 | `Gs2Grade:AddRankCapByUserId` | 그레이드에 대응하는 랭크 캡을 GS2-Experience에 반영 |
| 입수 | `Gs2Grade:MultiplyAcquireActionsByUserId` | 그레이드에 따른 `acquireActionRates`의 배율을 적용한 입수 액션으로 변환 |

"그레이드업 소재 검증"을 검증 액션으로 활용하면, 합성에 사용하려는 소재(다른 아이템이나 캐릭터)를 실제로 보유하고 있는지와 같은 검증을 트랜잭션 내에서 수행할 수 있습니다. 이를 통해 부정한 소재 지정에 의한 그레이드업을 방지할 수 있습니다.

## 마스터 데이터 운용
마스터 데이터를 등록함으로써 마이크로서비스에서 사용 가능한 데이터와 동작을 설정할 수 있습니다.

마스터 데이터의 종류에는 다음이 있습니다.

- `GradeModel`: 그레이드별 랭크 캡이나 보상량

이하는 마스터 데이터의 JSON 예시입니다.

```json
{
  "version": "2022-06-01",
  "gradeModels": [
    {
      "name": "grade-0001",
      "metadata": "Character Grade",
      "experienceModelId": "grn:gs2:{region}:{ownerId}:experience:namespace-0001:model:experienceModel-0001",
      "defaultGrades": [
        {
          "propertyIdRegex": ".*:item:SSR.*",
          "defaultGradeValue": 3
        },
        {
          "propertyIdRegex": ".*:item:SR.*",
          "defaultGradeValue": 2
        }
      ],
      "gradeEntries": [
        { "metadata": "Grade 0", "rankCapValue": 30 },
        { "metadata": "Grade 1", "rankCapValue": 40 },
        { "metadata": "Grade 2", "rankCapValue": 50 },
        { "metadata": "Grade 3", "rankCapValue": 60 }
      ],
      "acquireActionRates": [
        {
          "name": "experience",
          "mode": "double",
          "rates": [1.0, 1.2, 1.5, 2.0]
        }
      ]
    }
  ]
}
```

마스터 데이터의 등록은 관리 콘솔에서 등록하는 방법 외에도, GitHub에서 데이터를 반영하거나 GS2-Deploy를 사용해 CI에서 등록하는 워크플로우를 구성할 수 있습니다.

## 구현 예제

### 그레이드 가산

그레이드 가산은 게임 엔진용 SDK에서는 처리할 수 없습니다.

GS2-Exchange에서 강화 소재를 소비할 때나, GS2-Enhance의 강화 보상으로 트랜잭션 액션 `Gs2Grade:AddGradeByUserId`를 포함시켜 그레이드를 가산하도록 해 주세요.
이를 통해 부정한 클라이언트 조작에 의한 그레이드업을 방지하면서, 서버 측에서 정합성을 유지한 채 성장 요소를 구현할 수 있습니다.

### 그레이드 목록 조회



**Unity**
```csharp

    var items = await gs2.Grade.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).StatusesAsync(
        gradeName: "grade-0001"
    ).ToListAsync();
```
**Unreal Engine**
```cpp

    const auto It = Gs2->Grade->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Statuses(
        "grade-0001" // gradeName
    );
    TArray<Gs2::UE5::Grade::Model::FEzStatusPtr> Result;
    for (auto Item : *It)
    {
        if (Item.IsError())
        {
            return false;
        }
        Result.Add(Item.Current());
    }
```
**Godot**
```gdscript

var iterator = ez.grade.namespace_(
        "namespace-0001"
    ).me(
        game_session
    ).statuses(
        "grade-0001"
    )

var async_result = await iterator.load()
if async_result.error != null:
    # 에러를 처리
    push_error(str(async_result.error))
    return

var items = async_result.result

```


### 그레이드 조회



**Unity**
```csharp

    var item = await gs2.Grade.Namespace(
        namespaceName: "namespace-0001"
    ).GradeModel(
        gradeName: "grade-0001"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Future = Gs2->Grade->Namespace(
        "namespace-0001" // namespaceName
    )->GradeModel(
        "grade-0001" // gradeName
    )->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }
```
**Godot**
```gdscript

var domain = ez.grade.namespace_(
        "namespace-0001"
    ).grade_model(
        "grade-0001"
    )

var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### 개별 그레이드(Status) 조회

특정 `propertyId`(캐릭터, 장비 등)에 대한 현재 그레이드 값을 조회합니다.



**Unity**
```csharp

    var item = await gs2.Grade.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        gradeName: "grade-0001",
        propertyId: "property-0001"
    ).ModelAsync();
```
**Unreal Engine**
```cpp

    const auto Future = Gs2->Grade->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "grade-0001", // gradeName
        "property-0001" // propertyId
    )->Model();
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }
```
**Godot**
```gdscript

var domain = ez.grade.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "grade-0001",
        "property-0001"
    )

var async_result = await domain.model()
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


### GS2-Experience의 랭크 캡 동기화

그레이드가 변화하면, GS2-Experience의 랭크 캡 반영은 자동으로 이루어집니다.
다만, 그레이드 모델을 갱신해 특정 그레이드의 랭크 캡 값을 갱신한 경우에는 아래의 동기화 API를 명시적으로 호출함으로써 갱신 후의 랭크 캡을 적용할 수 있습니다.



**Unity**
```csharp

    var result = await gs2.Grade.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        gradeName: "grade-0001",
        propertyId: "property-0001"
    ).ApplyRankCapAsync(
    );
    var item = await result.ModelAsync();

```
**Unreal Engine**
```cpp

    const auto Future = Gs2->Grade->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        GameSession
    )->Status(
        "grade-0001", // gradeName
        "property-0001" // propertyId
    )->ApplyRankCap(
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

    // obtain changed values / result values
    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError())
    {
        return Future2->GetTask().Error();
    }
    const auto Result = Future2->GetTask().Result();
```
**Godot**
```gdscript

var domain = ez.grade.namespace_(
        "namespace-0001"
    ).me(game_session).status(
        "grade-0001",
        "property-0001"
    )

var async_result = await domain.apply_rank_cap(
)
if async_result.error != null:
    push_error(str(async_result.error))
    return

var result = async_result.result

```


## 상세 레퍼런스

[GS2-Grade API 레퍼런스](../../api_reference/grade)



