diff --git a/CONTEXT.md b/CONTEXT.md index d70f420..3af62d2 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -40,32 +40,25 @@ - `GET /api/visitors/stats`: 전체 유니크 방문자 수를 반환합니다. ### [Game Logic - src/game/] -- **`ArenaScene.js`**: - - `update()`: 매 프레임 생존 팀을 체크하고 좌측 HUD 레일의 팀 badge를 갱신합니다. - - 최초 로드 시 `presentationMode`로 프리뷰 전투를 조용히 실행하고, 실제 전투 시작 전까지 가까운 교전 지점을 배경처럼 보여줍니다. 프리뷰 전투는 로컬 저장 옵션과 분리된 10팀 x 팀당 5명 설정으로 시작합니다. - - `createFighterPlans()`: 실제 매치 시작 시 배정된 스킨의 `traits.spawnMultiplier`를 반영해 전투원 목록을 확장합니다. 프리뷰 전투는 10팀 x 팀당 5명 고정 규모를 지키도록 이 확장을 건너뜁니다. - - `spawnSplitFighters()`: Slime 사망 분열처럼 전투 중 새 전투원을 생성해야 하는 특성을 처리합니다. - - `recordDeath()`, `persistDailyDeathStats()`: 실제 전투에서만 사망 캐릭터의 `skin.species`를 종족별로 집계하고, 전투 종료 시 오늘 일자별 서버 사망 통계에 더합니다. - - `scheduleBattleNotice()`, `showBattleDeathNotice()`: 실제 전투가 5초 이상 지속되면 오늘 누적 사망 통계와 현재 전투 사망 수를 합산해 상단 안내바에 표시합니다. 안내는 2초 표시 후 10초 대기하는 주기로 반복됩니다. - - `setStatus()`, `createVictoryCelebration()`: 종료 상태가 승리면 중앙 금빛 배너, 광선, 컨페티와 Web Audio 기반 짧은 팡파르를 띄웁니다. 실제 매치 시작에서 오디오 컨텍스트를 미리 깨워 자동 재생 제한을 완화하고, 오디오가 막혀도 CSS 축하 레이어는 그대로 표시합니다. 무승부는 팡파르와 컨페티 없이 절제된 종료 배너를 사용합니다. - - `setPaused()`, `togglePause()`: 실제 전투에서 물리, Phaser 타이머, tween, 스프라이트 애니메이션을 함께 정지/재개합니다. 프리뷰 전투(`presentationMode`)와 종료된 전투는 pause 대상에서 제외합니다. - - `observeCombat()`: 캐릭터가 공격할 때 카메라가 주목할 "관전 대상"을 설정합니다. - - `getSpectatorState()`, `getSpectatorCameraTarget()`: 생존 4명 이하에서는 생존 캐릭터를 무작위로 포커싱하고, 잔여 2팀의 생존 합이 8명 이하이면 생존 수가 적은 팀을 포커싱합니다. - - `triggerFinalCombatSlowMotion()`: `FINAL_COMBAT_SLOW_MOTION_ENABLED`가 켜진 최종교전에서 공격 모션이 시작될 때만 전역 time scale을 낮추고 진입/유지/복귀 속도 램프로 슬로우모션을 연출합니다. Arcade Physics는 timeScale 방향이 반대라 물리 이동에는 역수 배율을 적용합니다. - - `selectFighter()`, `focusSelectedFighter()`: 캐릭터 클릭 시 해당 캐릭터의 히트박스 중심으로 카메라를 고정합니다. 팀 색상 마커는 선택 상태와 별개로 상시 표시됩니다. - - `selectRandomTeamFighter()`: 좌측 팀 badge 클릭 시 해당 팀의 생존 전투원 중 무작위 1명을 골라 카메라를 고정합니다. - - `updateMinimapViewportFrame()`: 주 카메라의 이동에 맞춰 미니맵 가이드 사각형을 렌더링합니다. -- **`matchSetup.js`**: - - 입력된 닉네임을 순회하여 `team` 객체를 생성하고, 요청된 인원만큼 캐릭터 데이터를 복제 배치합니다. - - 기본 `완전 랜덤 배치`는 전장 전체 스폰 슬롯을 섞고, `스타팅 지점 배치`는 참가자 수에 맞춰 전장을 구역으로 나눈 뒤 참가자별 구역 배정을 매치마다 섞고 각 구역 안의 슬롯도 무작위로 사용합니다. -- **`combat.js`**: - - `updateFighter()`: 가장 가까운 적을 찾아 이동하거나 공격하는 유닛 AI의 핵심입니다. - - `applyHit()`: 일반 공격 피해량은 `ATTACK_DAMAGE_MIN/MAX` 범위에서 계산하고, 치명타 적중은 `Critical!` 표기와 즉시 처치/카메라 흔들림을 처리합니다. - - `killFighter()`: 사망한 캐릭터를 반투명 처리하고 살아있는 캐릭터보다 낮은 depth로 내려 전투원을 가리지 않게 합니다. - - `applyKillReward()`: 처치한 캐릭터의 체력 회복, 크기 증가, 공격속도/이동속도 배율 증가를 처리합니다. 누적 배율은 `KILL_GROWTH_MAX_MULTIPLIER`로 제한합니다. - - `clampFighterInsideArena()`: 처치 성장 tween 중/완료 시 커진 캐릭터가 arena 밖으로 밀려 히트박스가 전장 바깥에 놓이지 않도록 위치를 보정합니다. - - `maybeSplitFighter()`: 사망한 캐릭터의 `traits.splitOnDeath` 특성을 확인하고 분열 생성을 요청합니다. - - `projectilePathHitsDefender()`: 투사체가 대상을 스쳐 지나가지 않도록 궤적 검사를 수행합니다. +#### arena/ +- **`ArenaScene.js`**: Phaser 씬의 생명주기와 전반적인 오케스트레이션을 담당합니다. `update()` 매 프레임마다 전투원 상태를 체크하고, 카메라 이동 및 UI 모듈 호출을 조율합니다. +- **`arenaRenderer.js`**: 아레나 배경 그래픽 및 타일 렌더링을 담당합니다. +- **`arenaSpectatorCamera.js`**: 관전 모드 시점 계산 및 카메라 포커싱 로직을 담당합니다. 생존 인원에 따른 지능형 카메라 추적 알고리즘이 구현되어 있습니다. + +#### combat/ +- **`combat.js`**: 전투 AI, 피해 계산, 처치 보상 등 핵심 전투 로직을 담당합니다. 유닛의 이동, 공격, 투사체 발사 등을 처리합니다. +- **`combatSettings.js`**: 전투 속도 배율 등 런타임 전투 설정을 관리합니다. +- **`arenaFinalCombatEffects.js`**: 최종 교전 시 슬로우 모션 등 연출 효과를 담당합니다. 수학적인 이징(easing) 함수와 물리 시간 배율 계산을 포함합니다. + +#### fighter/ +- **`fighterAssets.js`**: 캐릭터 스프라이트 로드 및 애니메이션/실루엣 생성을 담당합니다. 원본 이미지로부터 팀 색상 마커용 실루엣을 동적으로 생성합니다. +- **`fighterFactory.js`**: 캐릭터 인스턴스화 및 HUD(이름표, 체력바) 관리를 담당합니다. Phaser Sprite와 DOM UI 사이의 가교 역할을 합니다. +- **`fighterManifest.js`**: 모든 캐릭터 종족 및 스탯 데이터를 정의합니다. 20여 종의 캐릭터 설정이 포함되어 있습니다. +- **`fighterSelection.js`**: 매치 참여 캐릭터를 무작위로 선택하거나 섞는 로직을 담당합니다. + +#### match/ +- **`matchSetup.js`**: 매치 초기화, 팀 구성, 스폰 위치 계산을 담당합니다. 랜덤 배치 및 스타팅 지점 배치 알고리즘이 구현되어 있습니다. +- **`arenaMatchRuntime.js`**: 스폰 클러스터 계산 및 팀 크기 동기화 등 매치 진행 중 필요한 헬퍼 기능을 제공합니다. ### [Assets & UI] - **`fighterAssets.js`**: 원본 캐릭터 스프라이트의 alpha 값을 읽어 흰색 실루엣 spritesheet를 런타임에 생성합니다. 원본 주변 1px은 비워두고 그 바깥 1px만 칠한 뒤, `fighterFactory.js`에서 팀 색상으로 tint 처리합니다. diff --git a/src/game/ArenaScene.js b/src/game/arena/ArenaScene.js similarity index 96% rename from src/game/ArenaScene.js rename to src/game/arena/ArenaScene.js index ace47e2..84bbbcd 100644 --- a/src/game/ArenaScene.js +++ b/src/game/arena/ArenaScene.js @@ -22,23 +22,23 @@ import { SPECTATOR_LATE_FIGHTER_THRESHOLD, SPECTATOR_LATE_FIGHT_ZOOM, SPECTATOR_RANDOM_FOCUS_INTERVAL, -} from "../constants.js"; +} from "../../constants.js"; import { drawArena } from "./arenaRenderer.js"; -import { clearCombatObjects, updateFighter } from "./combat.js"; -import { createFighterAnimations, preloadFighterSheets } from "./fighterAssets.js"; -import { createFighter, syncFighterHud } from "./fighterFactory.js"; -import { fighterManifest } from "./fighterManifest.js"; -import { pickFighters } from "./fighterSelection.js"; -import { createMatchSetup, matchStatusText } from "./matchSetup.js"; -import { addTodayDeathStats, fetchTodayDeathStats } from "../ui/deathStats.js"; -import { createFighterPlans, clusterSpawnPosition, syncTeamSizes } from "./arenaMatchRuntime.js"; +import { clearCombatObjects, updateFighter } from "../combat/combat.js"; +import { createFighterAnimations, preloadFighterSheets } from "../fighter/fighterAssets.js"; +import { createFighter, syncFighterHud } from "../fighter/fighterFactory.js"; +import { fighterManifest } from "../fighter/fighterManifest.js"; +import { pickFighters } from "../fighter/fighterSelection.js"; +import { createMatchSetup, matchStatusText } from "../match/matchSetup.js"; +import { addTodayDeathStats, fetchTodayDeathStats } from "../../ui/deathStats.js"; +import { createFighterPlans, clusterSpawnPosition, syncTeamSizes } from "../match/arenaMatchRuntime.js"; import { createDeathCounts, normalizeDeathCounts, addDeathCounts, normalizeSpecies, createDeathNoticeMessage, -} from "../ui/battleDeathNotice.js"; +} from "../../ui/battleDeathNotice.js"; import { getSpectatorState, averageFighterPosition, @@ -51,21 +51,21 @@ import { easeOutCubic, easeInOutCubic, arcadePhysicsTimeScale, -} from "./arenaFinalCombatEffects.js"; +} from "../combat/arenaFinalCombatEffects.js"; import { createVictoryCelebration, primeVictoryFanfareAudio, removeVictoryCelebration, -} from "../ui/victoryCelebration.js"; -import { updateScoreboard } from "../ui/arenaScoreboard.js"; -import { appendKillLog, resetKillLog } from "../ui/arenaKillLog.js"; +} from "../../ui/victoryCelebration.js"; +import { updateScoreboard } from "../../ui/arenaScoreboard.js"; +import { appendKillLog, resetKillLog } from "../../ui/arenaKillLog.js"; import { BATTLE_NOTICE_DELAY_MS, BATTLE_NOTICE_INTERVAL_MS, BATTLE_NOTICE_VISIBLE_MS, clearBattleNotice, showBattleDeathNotice, -} from "../ui/battleDeathNotice.js"; +} from "../../ui/battleDeathNotice.js"; export class ArenaScene extends Phaser.Scene { constructor({ getInitialMatchConfig, setStatus }) { diff --git a/src/game/arenaRenderer.js b/src/game/arena/arenaRenderer.js similarity index 92% rename from src/game/arenaRenderer.js rename to src/game/arena/arenaRenderer.js index da717da..9b61f79 100644 --- a/src/game/arenaRenderer.js +++ b/src/game/arena/arenaRenderer.js @@ -1,4 +1,4 @@ -import { ARENA_SIZE, GRID_SIZE, TILE_SIZE } from "../constants.js"; +import { ARENA_SIZE, GRID_SIZE, TILE_SIZE } from "../../constants.js"; export function drawArena(scene) { const graphics = scene.add.graphics(); diff --git a/src/game/arenaSpectatorCamera.js b/src/game/arena/arenaSpectatorCamera.js similarity index 99% rename from src/game/arenaSpectatorCamera.js rename to src/game/arena/arenaSpectatorCamera.js index 1ddac03..b55573f 100644 --- a/src/game/arenaSpectatorCamera.js +++ b/src/game/arena/arenaSpectatorCamera.js @@ -6,7 +6,7 @@ import { SPECTATOR_FINAL_TEAM_TOTAL_THRESHOLD, SPECTATOR_LATE_FIGHTER_THRESHOLD, SPECTATOR_LATE_FIGHT_ZOOM, -} from "../constants.js"; +} from "../../constants.js"; export function getSpectatorState(livingFighters) { const livingFighterCount = livingFighters.length; diff --git a/src/game/arenaFinalCombatEffects.js b/src/game/combat/arenaFinalCombatEffects.js similarity index 100% rename from src/game/arenaFinalCombatEffects.js rename to src/game/combat/arenaFinalCombatEffects.js diff --git a/src/game/combat.js b/src/game/combat/combat.js similarity index 99% rename from src/game/combat.js rename to src/game/combat/combat.js index ac0ff10..a141d62 100644 --- a/src/game/combat.js +++ b/src/game/combat/combat.js @@ -27,7 +27,7 @@ import { SPELL_HIT_DELAY, RANGED_CRITICAL_CHANCE, RANGED_ATTACK_RANGE, -} from "../constants.js"; +} from "../../constants.js"; import { getAttackSpeedMultiplier, getMovementSpeedMultiplier, @@ -39,7 +39,7 @@ import { fighterProjectileKey, healEffectAnimationKey, healEffectKey, -} from "./fighterAssets.js"; +} from "../fighter/fighterAssets.js"; export function updateFighter(scene, fighter, time, onWinner) { const enemy = findNearestEnemy(scene.fighters, fighter); diff --git a/src/game/combatSettings.js b/src/game/combat/combatSettings.js similarity index 100% rename from src/game/combatSettings.js rename to src/game/combat/combatSettings.js diff --git a/src/game/fighterAssets.js b/src/game/fighter/fighterAssets.js similarity index 99% rename from src/game/fighterAssets.js rename to src/game/fighter/fighterAssets.js index 45d56fc..0ddee8a 100644 --- a/src/game/fighterAssets.js +++ b/src/game/fighter/fighterAssets.js @@ -7,7 +7,7 @@ import { SELECTED_FIGHTER_OUTLINE_ALPHA, SELECTED_FIGHTER_OUTLINE_GAP, SELECTED_FIGHTER_OUTLINE_WIDTH, -} from "../constants.js"; +} from "../../constants.js"; const SOURCE_ALPHA_THRESHOLD = 8; const HEAL_EFFECT_PATH = "assets/effects/heal/Heal_Effect.png"; diff --git a/src/game/fighterFactory.js b/src/game/fighter/fighterFactory.js similarity index 99% rename from src/game/fighterFactory.js rename to src/game/fighter/fighterFactory.js index 9be05f1..950e145 100644 --- a/src/game/fighterFactory.js +++ b/src/game/fighter/fighterFactory.js @@ -9,7 +9,7 @@ import { FIGHTER_HITBOX_WIDTH, FIGHTER_MAX_HP, FIGHTER_SCALE, -} from "../constants.js"; +} from "../../constants.js"; import { fighterAnimationKey, fighterOutlineSheetKeyFromSheetKey, diff --git a/src/game/fighterManifest.js b/src/game/fighter/fighterManifest.js similarity index 100% rename from src/game/fighterManifest.js rename to src/game/fighter/fighterManifest.js diff --git a/src/game/fighterSelection.js b/src/game/fighter/fighterSelection.js similarity index 100% rename from src/game/fighterSelection.js rename to src/game/fighter/fighterSelection.js diff --git a/src/game/arenaMatchRuntime.js b/src/game/match/arenaMatchRuntime.js similarity index 96% rename from src/game/arenaMatchRuntime.js rename to src/game/match/arenaMatchRuntime.js index 0fde7bd..eb6a389 100644 --- a/src/game/arenaMatchRuntime.js +++ b/src/game/match/arenaMatchRuntime.js @@ -1,5 +1,5 @@ import Phaser from "phaser"; -import { ARENA_SIZE } from "../constants.js"; +import { ARENA_SIZE } from "../../constants.js"; const SPAWN_CLUSTER_MARGIN = 48; const SPAWN_CLUSTER_STEP = 28; diff --git a/src/game/matchSetup.js b/src/game/match/matchSetup.js similarity index 99% rename from src/game/matchSetup.js rename to src/game/match/matchSetup.js index 4b592b5..9742052 100644 --- a/src/game/matchSetup.js +++ b/src/game/match/matchSetup.js @@ -7,7 +7,7 @@ import { MAX_TEAM_SIZE, SPAWN_PLACEMENTS, TILE_SIZE, -} from "../constants.js"; +} from "../../constants.js"; export function createMatchSetup( names, diff --git a/src/main.js b/src/main.js index e9e3de6..7ac485a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,5 @@ import Phaser from "phaser"; -import { ArenaScene } from "./game/ArenaScene.js"; +import { ArenaScene } from "./game/arena/ArenaScene.js"; import { ARENA_SIZE, PRESENTATION_TEAM_COUNT, diff --git a/todo.md b/todo.md index a8744ce..9910598 100644 --- a/todo.md +++ b/todo.md @@ -127,3 +127,10 @@ - 기존 중앙 승리 배너를 금빛 광선과 컨페티가 함께 터지는 `.victory-celebration` 레이어로 확장. - 실제 전투 시작에서 Web Audio 컨텍스트를 준비하고 승리 시 짧은 팡파르를 합성해 재생하도록 추가. - 무승부는 팡파르와 컨페티를 제외한 절제된 결과 배너를 유지하고, 축하 애니메이션은 축소 모션 설정을 따르도록 보강. + +22. ArenaScene.js 모듈화 및 src/game 폴더 구조 정리 (완료) +- **조치 사항**: + - `ArenaScene.js`의 방대한 기능을 7개의 전문 모듈(`arenaKillLog`, `arenaScoreboard`, `battleDeathNotice`, `victoryCelebration`, `arenaMatchRuntime`, `arenaSpectatorCamera`, `arenaFinalCombatEffects`)로 분리. + - `src/game` 폴더 내의 파일들을 역할별 하위 폴더(`arena/`, `combat/`, `fighter/`, `match/`)로 분류하여 재배치. + - 모든 `import` 경로를 새로운 계층 구조에 맞춰 업데이트하고 빌드 안정성을 확보. + - `ArenaScene.js`는 이제 각 모듈을 조율하는 오케스트레이션 역할에 집중하도록 경량화됨.