// 1. ARENA 도메인 const GRID_SIZE = 50; const TILE_SIZE = 64; const ARENA_SIZE = GRID_SIZE * TILE_SIZE; export const ARENA = { GRID_SIZE, TILE_SIZE, SIZE: ARENA_SIZE, }; // 2. FIGHTER 도메인 export const FIGHTER = { SCALE: 3, DEPTH: 2, DEAD_DEPTH: 1, DEAD_ALPHA: 0.42, FRAME_WIDTH: 100, FRAME_HEIGHT: 100, HITBOX_WIDTH: 22, HITBOX_HEIGHT: 20, HITBOX_OFFSET_X: 39, HITBOX_OFFSET_Y: 40, NICKNAME_LENGTH: 18, // 캐릭터 액션별 애니메이션 프레임 속도와 반복 횟수 ANIMATION_OPTIONS: { attack: { frameRate: 15, repeat: 0 }, attack02: { frameRate: 15, repeat: 0 }, attack03: { frameRate: 15, repeat: 0 }, block: { frameRate: 13, repeat: 0 }, death: { frameRate: 11, repeat: 0 }, heal: { frameRate: 13, repeat: 0 }, hurt: { frameRate: 13, repeat: 0 }, idle: { frameRate: 7, repeat: -1 }, walk: { frameRate: 10, repeat: -1 }, walk02: { frameRate: 10, repeat: -1 }, }, // 역할별 기본 스탯 TYPE_STATS: { melee: { maxHp: 100, moveSpeed: 148 * 1.1, attackRange: 84, attackCooldown: 840, damageMin: 14, damageMax: 24, criticalChance: 0.2, windupDelay: 260, }, ranged: { maxHp: 80, moveSpeed: 148, attackRange: TILE_SIZE * 5, attackCooldown: 840 * 1.1, damageMin: 14 * 1.2, damageMax: 24 * 1.2, criticalChance: 0, windupDelay: 360, projectileSpeed: 420, }, magic: { maxHp: 80, moveSpeed: 148, attackRange: TILE_SIZE * 5, attackCooldown: 840 * 1.1, damageMin: 14 * 1.5, damageMax: 24 * 1.5, criticalChance: 0, windupDelay: 340, effectHitDelay: 160, }, }, }; // 3. SPAWN 도메인 export const SPAWN = { DEFAULT_TEAM_SIZE: 5, DEFAULT_PLACEMENT: "random", PLACEMENTS: { RANDOM: "random", STARTING_ZONES: "starting-zones", }, STARTING_ZONE_RADIUS: 2, STARTING_ZONE_FILL_ALPHA: 0.07, STARTING_ZONE_BORDER_ALPHA: 0.14, STARTING_ZONE_VISIBLE_DURATION_MS: 5000, PRESENTATION_TEAM_COUNT: 10, PRESENTATION_TEAM_SIZE: 5, MAX_TEAM_SIZE: 100, }; // 4. COMBAT 도메인 export const COMBAT = { KILL_HEALTH_RECOVERY_RATIO: 0.3, KILL_HEAL_EFFECT_FRAMES: 4, KILL_HEAL_EFFECT_FRAME_RATE: 12, KILL_GROWTH_MULTIPLIER: 1.25, KILL_GROWTH_MAX_MULTIPLIER: 5, KILL_GROWTH_TWEEN_DURATION: 180, // 최종교전 슬로우모션 설정 FINAL_SLOW_MOTION_ENABLED: false, FINAL_SLOW_MOTION_ENTER_DURATION: 14000, FINAL_SLOW_MOTION_HOLD_DURATION: 14000, FINAL_SLOW_MOTION_EXIT_DURATION: 14000, FINAL_SLOW_MOTION_SCALE: 0.28, }; // 5. PROJECTILE 도메인 export const PROJECTILE = { LIFETIME: 1800, BODY_OFFSET: 4, HIT_PADDING: 20, HIT_RADIUS: 12, SPAWN_DISTANCE: 1, }; // 6. WORLD_EFFECT 도메인 export const WORLD_EFFECT = { INTERVAL: 4000, AREA_TILES: 5, FRAMES: 7, FRAME_RATE: 14, FALL_DURATION: 920, FALL_TRAVEL_TILES: 8, VISUAL_SCALE: 12, METEOR_DAMAGE: 80, FROST_DAMAGE: 40, FROST_STUN_DURATION: 2000, FROST_STUN_TINT: 0x82e9ff, FROST_DURATION: 20000, FROST_SPEED_MULTIPLIER: 0.55, }; // 7. CAMERA 도메인 export const CAMERA = { MIN_ZOOM: 1, MAX_ZOOM: 3, ZOOM_STEP: 0.1, // 자동 관전 진입 전 화염/냉기 메테오 낙하 위치를 임시로 확대 추적합니다. METEOR_FOCUS_ENABLED: true, METEOR_FOCUS_ZOOM: 2, SPECTATOR_LERP: 0.1, // 메테오 착탄 후 카메라를 해당 위치에 유지하는 시간(ms)입니다. METEOR_FOCUS_HOLD_DURATION: 1200, SPECTATOR_FINAL_FIGHTER_THRESHOLD: 5, SPECTATOR_FINAL_FIGHT_ZOOM: 3, SPECTATOR_FINAL_TEAM_COUNT: 2, SPECTATOR_FINAL_TEAM_TOTAL_THRESHOLD: 8, SPECTATOR_RANDOM_FOCUS_INTERVAL: 10000, SPECTATOR_LATE_FIGHTER_THRESHOLD: 30, SPECTATOR_LATE_FIGHT_ZOOM: 2, SELECTED_FIGHTER_ZOOM: 2, }; // 8. UI 도메인 export const UI = { MINIMAP_ALPHA: 0.8, MINIMAP_MARGIN: Math.round(ARENA_SIZE * 0.016), MINIMAP_VIEWPORT_SIZE: Math.round(ARENA_SIZE * 0.22), MINIMAP_VIEW_FRAME_STROKE: 10, SELECTED_FIGHTER_OUTLINE_GAP: 1, SELECTED_FIGHTER_OUTLINE_WIDTH: 1, SELECTED_FIGHTER_OUTLINE_RED: 255, SELECTED_FIGHTER_OUTLINE_GREEN: 228, SELECTED_FIGHTER_OUTLINE_BLUE: 64, SELECTED_FIGHTER_OUTLINE_ALPHA: 0.65, }; // 9. TEAM 도메인 const TEAM_COLORS = [ "#da6a48", "#5fb4d9", "#9bd15a", "#d6a94a", "#d477b8", "#7f90e8", "#63c5a6", "#d98755", ]; const TEAM_COLOR_GOLDEN_ANGLE = 137.508; const TEAM_COLOR_HUE_OFFSET = 12; const TEAM_COLOR_SATURATIONS = [72, 62, 78, 68]; const TEAM_COLOR_LIGHTNESSES = [57, 63, 51, 69]; function getTeamColor(index, totalTeams = TEAM_COLORS.length) { const safeIndex = Math.max(0, Math.floor(Number(index) || 0)); const safeTeamCount = Math.max(1, Math.floor(Number(totalTeams) || 1)); if (safeTeamCount <= TEAM_COLORS.length) { return TEAM_COLORS[safeIndex % TEAM_COLORS.length]; } const hue = (TEAM_COLOR_HUE_OFFSET + safeIndex * TEAM_COLOR_GOLDEN_ANGLE) % 360; const saturation = TEAM_COLOR_SATURATIONS[safeIndex % TEAM_COLOR_SATURATIONS.length]; const lightness = TEAM_COLOR_LIGHTNESSES[ Math.floor(safeIndex / TEAM_COLOR_SATURATIONS.length) % TEAM_COLOR_LIGHTNESSES.length ]; return hslToHex(hue, saturation, lightness); } function hslToHex(hue, saturation, lightness) { const normalizedSaturation = saturation / 100; const normalizedLightness = lightness / 100; const chroma = (1 - Math.abs(2 * normalizedLightness - 1)) * normalizedSaturation; const huePrime = hue / 60; const x = chroma * (1 - Math.abs((huePrime % 2) - 1)); const match = normalizedLightness - chroma / 2; const [red, green, blue] = huePrime < 1 ? [chroma, x, 0] : huePrime < 2 ? [x, chroma, 0] : huePrime < 3 ? [0, chroma, x] : huePrime < 4 ? [0, x, chroma] : huePrime < 5 ? [x, 0, chroma] : [chroma, 0, x]; return `#${[red, green, blue] .map((channel) => Math.round((channel + match) * 255) .toString(16) .padStart(2, "0"), ) .join("")}`; } export const TEAM = { COLORS: TEAM_COLORS, getColor: getTeamColor, };