238 lines
12 KiB
Markdown
238 lines
12 KiB
Markdown
# Elite 캐릭터 구현 문서
|
|
|
|
## 현재 상태
|
|
|
|
이 문서는 `major` 브랜치에서 `30d7be41bef258685bf67219f2fcf77334c191f8`를
|
|
기준으로 구현한 elite 압축 전투 규칙을 설명한다. 이전 WIP에서 누락됐던 월드 이펙트
|
|
비율 상수까지 포함해 구현했으며, `npm run build`로 빌드를 검증했다.
|
|
|
|
## 기능 의도
|
|
|
|
### 1. 인원 압축
|
|
|
|
- 참가자 입력은 기존의 `닉네임*N` 형식을 유지한다.
|
|
- `FIGHTER.ELITE.RANDOMIZED_COMPRESSION.MIN_TEAM_SIZE` 미만에서는 `STACK_SIZE = 100`명마다 elite fighter 1개체를 소환한다.
|
|
- 소규모 팀에서 100명으로 묶이지 않는 나머지는 normal fighter를 1명당 1개체씩 소환한다.
|
|
- 현재 `MIN_TEAM_SIZE = 100` 설정에서는 100명 블록이 있는 입력부터 랜덤 압축 대상이다.
|
|
- 대규모 팀은 100명 블록마다 `ELITE_BLOCK_PROBABILITY = 0.6`으로 elite 압축 여부를 판정한다.
|
|
예: `Alice*4000`은 40개 블록 중 장기 평균 24개가 elite 24개체로 압축되어 2,400명을
|
|
대표하고, 나머지 16개 블록은 normal 1,600개체로 실제 렌더링된다.
|
|
- 팀 카드는 대표 인원 합계 대신 생존 렌더 구성을 `E : <elite 수> | N : <normal 수>`로 표시한다.
|
|
- 사망 통계, 관전 판정, 밀집 구역 표적 선정은 계속 `stackCount` 합계를 사용한다.
|
|
|
|
### 2. Elite 스탯
|
|
|
|
구현된 상수:
|
|
|
|
```js
|
|
export const FIGHTER = {
|
|
// ...
|
|
ELITE: {
|
|
TYPE: "melee",
|
|
STACK_SIZE: 100,
|
|
VISUAL_SCALE_MULTIPLIER: 5,
|
|
HP_BONUS_RATIO: 1,
|
|
ATTACK_RANGE_MULTIPLIER: 1.5,
|
|
ATTACK_DAMAGE_BONUS_MULTIPLIER: 1,
|
|
ATTACK_DAMAGE_STACK_EXPONENT: 0.5,
|
|
ATTACK_SPEED_BONUS_MULTIPLIER: 1,
|
|
ATTACK_SPEED_STACK_EXPONENT: 0,
|
|
MOVE_SPEED_BONUS_MULTIPLIER: 1,
|
|
MOVE_SPEED_STACK_EXPONENT: 0,
|
|
RANDOMIZED_COMPRESSION: {
|
|
MIN_TEAM_SIZE: 100,
|
|
ELITE_BLOCK_PROBABILITY: 0.6,
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
elite 계산 의도:
|
|
|
|
```js
|
|
const attackDamageMultiplier = 1
|
|
+ FIGHTER.ELITE.ATTACK_DAMAGE_BONUS_MULTIPLIER
|
|
* (stackCount ** FIGHTER.ELITE.ATTACK_DAMAGE_STACK_EXPONENT - 1);
|
|
const visualScale = FIGHTER.SCALE * FIGHTER.ELITE.VISUAL_SCALE_MULTIPLIER;
|
|
const rangeBonus =
|
|
(visualScale - FIGHTER.SCALE) * (FIGHTER.HITBOX_WIDTH / 2);
|
|
|
|
damageMin = baseDamageMin * attackDamageMultiplier;
|
|
damageMax = baseDamageMax * attackDamageMultiplier;
|
|
maxHp = baseMaxHp * stackCount * FIGHTER.ELITE.HP_BONUS_RATIO;
|
|
attackRange =
|
|
baseAttackRange * FIGHTER.ELITE.ATTACK_RANGE_MULTIPLIER + rangeBonus;
|
|
attackSpeedMultiplier *= 1 + FIGHTER.ELITE.ATTACK_SPEED_BONUS_MULTIPLIER
|
|
* (stackCount ** FIGHTER.ELITE.ATTACK_SPEED_STACK_EXPONENT - 1);
|
|
moveSpeedMultiplier *= 1 + FIGHTER.ELITE.MOVE_SPEED_BONUS_MULTIPLIER
|
|
* (stackCount ** FIGHTER.ELITE.MOVE_SPEED_STACK_EXPONENT - 1);
|
|
```
|
|
|
|
- 피해량, 공격속도, 이동속도 보너스는 각각 `FIGHTER.ELITE.*_BONUS_MULTIPLIER`와
|
|
`*_STACK_EXPONENT`로 조정한다. multiplier `0`은 추가 보너스 없음이고,
|
|
multiplier `1`은 설정한 stack 곡선을 그대로 적용한다.
|
|
- HP는 압축 인원수와 현재 `FIGHTER.ELITE.HP_BONUS_RATIO = 1` 설정에 따라 선형 비례한다.
|
|
- 이동속도에는 `stackCount` 보정을 적용하지 않는다. 공격 DPS와 생존력만 대표
|
|
인원에 맞춰 증가시키고, 거대 elite의 전장 이동은 일반 이동 규칙을 유지한다.
|
|
|
|
### 3. 처치 보너스 비활성화
|
|
|
|
- elite는 여러 명의 피해량과 체력을 하나의 객체로 대표하므로, 물리 객체 기준의
|
|
처치 1회에 회복/성장 보너스를 주면 실제 대표 인원 기준으로 보상이 과대 적용된다.
|
|
- `COMBAT.KILL_REWARD_ENABLED = false`를 기본 정책으로 두고, 모든 fighter의
|
|
처치 회복, 크기 성장, 공격속도/이동속도 보너스, 회복 이펙트를 비활성화한다.
|
|
- 킬로그, 사망 통계, 승패 판정은 계속 동작한다. 기존 보너스 구현은 별도 모드가
|
|
필요할 경우 명시적으로 재활성화할 수 있도록 코드에 남겨 둔다.
|
|
|
|
### 4. Elite 대상 피해 이원화
|
|
|
|
구현된 치명타 상수:
|
|
|
|
```js
|
|
COMBAT.CRITICAL_DAMAGE_PERCENT = 0.1;
|
|
COMBAT.NORMAL_CRITICAL_DAMAGE_MULTIPLIER = 2;
|
|
```
|
|
|
|
의도한 판정:
|
|
|
|
- normal 대상 치명타: 일반 랜덤 피해의 2배를 적용한다.
|
|
- elite 대상 치명타: `maxHp`의 10% 피해를 적용하되, 일반 타격 피해보다 낮아지지 않게 한다.
|
|
- normal 대상 메테오/냉기: 기존 고정 피해인 `WORLD_EFFECT.METEOR_DAMAGE`,
|
|
`WORLD_EFFECT.FROST_DAMAGE`를 유지한다.
|
|
- elite 대상 메테오: `maxHp`의 40% 피해를 적용한다.
|
|
- elite 대상 냉기: `maxHp`의 20% 피해를 적용한다.
|
|
|
|
이전 WIP에서 누락됐고 이번 구현에서 보완한 상수:
|
|
|
|
```js
|
|
WORLD_EFFECT.METEOR_DAMAGE_PERCENT = 0.4;
|
|
WORLD_EFFECT.FROST_DAMAGE_PERCENT = 0.2;
|
|
```
|
|
|
|
위 두 값은 `src/constants.js`에 정의되어 elite 월드 이펙트 피해 계산에 사용된다.
|
|
|
|
## 구현 변경 지점
|
|
|
|
### `src/constants.js`
|
|
|
|
- `COMBAT.CRITICAL_DAMAGE_PERCENT`, `COMBAT.NORMAL_CRITICAL_DAMAGE_MULTIPLIER`를 추가했다.
|
|
- `COMBAT.KILL_REWARD_ENABLED = false`를 추가해 처치 보너스를 비활성화했다.
|
|
- `WORLD_EFFECT.METEOR_DAMAGE_PERCENT`, `WORLD_EFFECT.FROST_DAMAGE_PERCENT`를 추가했다.
|
|
- fighter 도메인 아래 `FIGHTER.ELITE` 키로 elite 상수를 관리한다.
|
|
- 카메라/렌더/worker 성능 리팩터링 없이 elite 밸런스 상수만 추가했다.
|
|
|
|
### `src/game/match/matchSetup.js`
|
|
|
|
- 기존에는 `team.size`만큼 실제 fighter plan을 만들었다.
|
|
- 임계값 미만 팀은 완전한 100명 블록마다 `stackCount: 100`, `isElite: true`
|
|
plan을 하나 만들고, 나머지는 `stackCount: 1`, `isElite: false` plan으로 유지한다.
|
|
- 임계값 이상 팀은 완전한 100명 블록마다
|
|
`RANDOMIZED_COMPRESSION.ELITE_BLOCK_PROBABILITY`로 elite 여부를 판정한다.
|
|
성공 블록은 `stackCount: 100`인 elite 하나가 되고, 실패 블록은 normal 100개체로 렌더링한다.
|
|
- 스폰 좌표 배열은 요청 인원 기준으로 생성하며, 각 elite는 대표하는 100명 블록의
|
|
첫 스폰 지점을 사용하고 나머지 normal은 대응하는 개별 스폰 지점을 사용한다.
|
|
|
|
### `src/game/match/arenaMatchRuntime.js`
|
|
|
|
- 팀 크기 동기화는 물리 Sprite 수 대신 `stackCount` 합계를 사용한다.
|
|
- elite plan에는 `spawnMultiplier`를 적용하지 않아 대표 스택 전체가 한 번에
|
|
복제되지 않게 한다. normal plan의 기존 trait 동작은 유지한다.
|
|
|
|
### `src/game/fighter/fighterFactory.js`
|
|
|
|
- `stackCount`, `isElite`를 입력으로 받고 HP, 피해량, 사거리, 외형 크기를 계산한다.
|
|
- elite의 `baseScaleX`/`baseScaleY`도 큰 외형 기준으로 저장한다. 현재는 킬 보너스가
|
|
꺼져 있지만, 별도 모드에서 다시 활성화할 경우 elite 기준 크기를 보존한다.
|
|
- 기존 Sprite 기반 `createFighter()`에만 적용했으며, elite는 `splitOnDeath`를
|
|
사용하지 않는다.
|
|
|
|
### `src/game/fighter/fighterSelection.js`
|
|
|
|
- `pickFightersForSetups()`는 elite plan에 `FIGHTER.ELITE.TYPE`과 일치하는 스킨만 배정한다.
|
|
- normal plan은 기존과 동일하게 전체 fighter manifest에서 스킨을 선택한다.
|
|
|
|
### `src/game/combat/combat.js`
|
|
|
|
- 치명타 판정을 normal 즉사에서 normal 2배 피해 / elite 최대 HP 비례 피해로 바꿨다.
|
|
- 월드 이펙트 함수 인자를 고정 `damage` 값에서 `"meteor"`/`"frost"` 타입으로 바꾸고,
|
|
대상이 elite인지에 따라 고정 피해와 비율 피해를 나눈다.
|
|
- 공격력 계산은 `FIGHTER.ELITE.ATTACK_DAMAGE_*`, 공격속도와 이동속도 계산은
|
|
`FIGHTER.ELITE.ATTACK_SPEED_*` 및 `FIGHTER.ELITE.MOVE_SPEED_*` 상수를 사용한다.
|
|
- 처치 흐름은 로그와 사망 처리는 유지하면서 `COMBAT.KILL_REWARD_ENABLED`가
|
|
`false`일 때 `applyKillReward()`를 실행하지 않는다.
|
|
- 기준 커밋에 존재하는 Sprite 전투 경로인 `applyHit()`,
|
|
`applyWorldEffectDamage()`, `fighterAttackSpeedMultiplier()`만 수정했다.
|
|
|
|
### `src/game/combat/worldEffects.js`
|
|
|
|
- 메테오/냉기 낙하 처리에서 effect type을 `applyWorldEffectDamage()`로 전달한다.
|
|
- 밀집 구역 계산은 `stackCount`를 가중치로 사용해 elite가 대표하는 인원을
|
|
월드 이펙트 표적 선정에 반영한다.
|
|
|
|
### `src/game/arena/ArenaScene.js`
|
|
|
|
- 사망 통계 누적 값을 `+ 1` 대신 `+ (fighter.stackCount || 1)`로 바꿨다.
|
|
- elite가 죽으면 압축된 인원 전체가 오늘의 사망 통계에 기록되어야 한다.
|
|
|
|
### `src/game/arena/arenaSpectatorCamera.js`
|
|
|
|
- 관전 진입 임계값, 열세 팀 비교, 평균 포커스 좌표는 `stackCount`를 가중해
|
|
대규모 압축 팀이 시작 즉시 최종 교전으로 오판되지 않게 한다.
|
|
|
|
### `src/ui/arenaScoreboard.js`
|
|
|
|
- 살아 있는 elite 객체 수와 normal 객체 수를 각각 계산해
|
|
`E : <elite> | N : <normal>` 형식으로 표시한다.
|
|
- 예를 들어 `Alice*4000`의 평균 구성은 `E : 24 | N : 1600` 부근으로 표시되어
|
|
실제 렌더링되는 군세 구성을 바로 확인할 수 있다.
|
|
|
|
### `src/game/fighter/fighterModel.js`, `src/game/fighter/fighterAdapter.js`
|
|
|
|
- WIP 당시에는 `stackCount`와 `isElite`를 모델 브리지에도 추가했다.
|
|
- 이 두 모듈은 `30d7be4` 이후 대규모 전투 최적화 커밋에서 추가된 구조이므로,
|
|
이번 롤백 기준에서는 elite 재구현의 선행 조건이 아니다.
|
|
|
|
## 의도적으로 포함하지 않은 변경
|
|
|
|
- `fighterLodWorker.js`, `aggregateCombatWorker.js` 제거 또는 대체
|
|
- 모델 전투/LOD/worker 경로 전면 단순화
|
|
- 렌더 캔버스 크기, 카메라 줌, minimap throttle, HUD 파일 분리
|
|
- `agent.md` 전체를 elite 전용 구조로 축약하는 변경
|
|
|
|
위 항목은 이전 WIP에 섞여 있었지만, elite 캐릭터 기능의 최소 구현과 독립적인
|
|
리팩터링이므로 포함하지 않았다.
|
|
|
|
## 구현 흐름
|
|
|
|
1. `src/constants.js`의 `FIGHTER.ELITE`에 elite 타입, 스탯, 공격력/속도, 랜덤 압축 설정을 묶고, 치명타 비율/배수와 메테오/냉기 elite 비율 상수를 추가한다.
|
|
2. `matchSetup.js`에서 소규모 입력은 100명 단위로 압축하고, 대규모 입력은
|
|
각 100명 블록을 확률적으로 elite 한 개체 또는 normal 100개체로 생성한다.
|
|
3. `fighterFactory.js`의 기존 Sprite 생성 경로에서 elite 외형, HP, 공격력,
|
|
사거리를 계산한다.
|
|
4. `fighterSelection.js`에서 elite plan에는 근거리 스킨만 할당한다.
|
|
5. `combat.js`와 `worldEffects.js`에서 normal/elite 피해 판정을 나눈다.
|
|
6. `COMBAT.KILL_REWARD_ENABLED = false`로 처치 회복/성장 보너스를 차단한다.
|
|
7. `ArenaScene.js`의 사망 통계는 `stackCount` 합산을 유지하고,
|
|
`arenaScoreboard.js` 팀 카드는 생존 elite/normal 객체 수를 분리 표시한다.
|
|
8. `agent.md`, `context/core.md`, `context/combat.md`, `context/fighter.md`,
|
|
`context/match-ui.md`, `context/arena.md`, `todo.md`에 구현 규칙을 기록한다.
|
|
|
|
## 검증 체크리스트
|
|
|
|
- `Alice*1`은 이전과 동일하게 normal 1개체만 생성된다.
|
|
- `Alice*99`는 normal 99개체로 생성된다.
|
|
- 현재 임계값 `100`에서는 `Alice*100` 이상의 완전한 블록이 `ELITE_BLOCK_PROBABILITY`에 따라 elite 또는 normal 100개체가 되는지 확인한다.
|
|
- `Alice*4000` 표본 반복에서 elite 수가 평균 24개, normal 수가 평균 1,600개에 수렴하고,
|
|
모든 plan의 `stackCount` 합계가 매번 4,000인지 확인한다.
|
|
- 팀 카드가 같은 구성에 대해 `E : <elite 수> | N : <normal 수>` 형식으로 표시되는지 확인한다.
|
|
- elite HP/공격력/공격속도/사거리/외형이 상수와 `stackCount` 계산식에 맞는다.
|
|
- normal 치명타가 기존 즉사가 아닌 설정한 고정 배수 피해로 동작하는지 의도와 다시 대조한다.
|
|
- elite 치명타, 메테오, 냉기 피해가 각각 최대 HP 10%, 40%, 20% 기준으로 계산된다.
|
|
- elite 사망 시 사망 통계가 `stackCount`만큼 증가한다.
|
|
- 어떤 fighter도 처치로 회복하거나 커지거나 공격/이동 속도 보너스를 얻지 않는다.
|
|
- elite에는 Slime의 `spawnMultiplier` 및 `splitOnDeath`가 적용되지 않고,
|
|
normal fighter에는 기존 trait 동작이 유지되는지 확인한다.
|
|
- elite plan에 선택된 스킨 타입은 항상 `FIGHTER.ELITE.TYPE`과 일치하고 normal plan은 기존 전체 스킨 풀을 사용하는지 확인한다.
|
|
- 밀집 구역 월드 이펙트 표적 산정은 elite를 `stackCount`만큼 가중한다.
|
|
- `npm run build`를 통과시키고 실제 전투에서 normal/elite 양쪽 흐름을 수동 확인한다.
|