# 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 : | N : `로 표시한다. - 사망 통계, 관전 판정, 밀집 구역 표적 선정은 계속 `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 : | N : ` 형식으로 표시한다. - 예를 들어 `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 : | N : ` 형식으로 표시되는지 확인한다. - 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 양쪽 흐름을 수동 확인한다.