arena/elite.md

263 lines
14 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 압축 여부를 판정한다.
- 사용자가 입력한 총 fighter 수가 `PERFORMANCE.LARGE_BATTLE_FIGHTER_THRESHOLD`보다 크면
`LARGE_BATTLE_ELITE_BLOCK_PROBABILITY = 0.8`을 사용해 elite 압축 비율을 높인다.
예: `Alice*4000`은 40개 블록 중 장기 평균 32개가 elite 32개체로 압축되어 3,200명을
대표하고, 나머지 8개 블록은 normal 800개체로 실제 렌더링된다.
- 대규모 전투에서는 `PERFORMANCE.LARGE_BATTLE_RENDERED_FIGHTER_LIMIT`도 적용한다.
랜덤 판정 후 실제 렌더링 수가 제한을 넘으면 normal 100명 블록이나 remainder 묶음을 elite 그룹으로 추가 승격한다.
- 팀 카드는 대표 인원 합계 대신 생존 렌더 구성을 `E : <elite 수> | N : <normal 수>`로 표시한다.
- 사망 통계, 관전 판정, 밀집 구역 표적 선정은 계속 `stackCount` 합계를 사용한다.
### 2. Elite 스탯
Update: elite magic attack effects now use `FIGHTER.ELITE.ATTACK_EFFECT_SCALE_MULTIPLIER`
on top of the normal `FIGHTER.ATTACK_EFFECT_SCALE_MULTIPLIER`, so spell visuals can be tuned
separately from the elite fighter body scale.
구현된 상수:
```js
export const FIGHTER = {
// ...
ATTACK_EFFECT_SCALE_MULTIPLIER: 1,
ELITE: {
TYPE: ["melee", "magic"],
STACK_SIZE: 100,
VISUAL_SCALE_MULTIPLIER: 5,
ATTACK_EFFECT_SCALE_MULTIPLIER: 5,
HP_BONUS_RATIO: 1,
ATTACK_RANGE_MULTIPLIER: 1.5,
ATTACK_DAMAGE_BONUS_MULTIPLIER: 1,
ATTACK_DAMAGE_STACK_EXPONENT: 0.1,
ATTACK_SPEED_BONUS_MULTIPLIER: 1,
ATTACK_SPEED_STACK_EXPONENT: 0.1,
MOVE_SPEED_BONUS_MULTIPLIER: 1,
MOVE_SPEED_STACK_EXPONENT: 0,
RANDOMIZED_COMPRESSION: {
MIN_TEAM_SIZE: 100,
ELITE_BLOCK_PROBABILITY: 0.6,
LARGE_BATTLE_ELITE_BLOCK_PROBABILITY: 0.8,
},
},
};
export const PERFORMANCE = {
LARGE_BATTLE_FIGHTER_THRESHOLD: 3000,
LARGE_BATTLE_RENDERED_FIGHTER_LIMIT: 1200,
// ...
};
```
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 상수를 관리한다.
- 일반 마법 공격 이펙트는 `FIGHTER.ATTACK_EFFECT_SCALE_MULTIPLIER`, elite 마법 공격 이펙트는
`FIGHTER.ELITE.ATTACK_EFFECT_SCALE_MULTIPLIER`로 조정한다.
- 카메라/렌더/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개체로 렌더링한다.
- 전체 입력 fighter 수가 `PERFORMANCE.LARGE_BATTLE_FIGHTER_THRESHOLD`보다 크면
`RANDOMIZED_COMPRESSION.LARGE_BATTLE_ELITE_BLOCK_PROBABILITY`를 사용한다.
- 랜덤 압축 결과가 `PERFORMANCE.LARGE_BATTLE_RENDERED_FIGHTER_LIMIT`를 넘으면
실패한 normal 100명 블록이나 normal remainder 묶음을 elite 그룹으로 승격해 실제 렌더링 개체 수를 제한한다.
- 스폰 좌표 배열은 요청 인원 기준으로 생성하며, 각 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_*` 상수를 사용한다.
- instant-spell 공격 이펙트 크기는 상수 기반으로 계산하고, elite magic 스킨이면
`FIGHTER.ELITE.ATTACK_EFFECT_SCALE_MULTIPLIER`를 추가 적용한다.
- 처치 흐름은 로그와 사망 처리는 유지하면서 `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 : 32 | N : 800` 부근으로 표시되어
실제 렌더링되는 군세 구성을 바로 확인할 수 있다.
### `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 수가 평균 32개, normal 수가 평균 800개에 수렴하고,
모든 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 양쪽 흐름을 수동 확인한다.