arena/src/constants.js

241 lines
6.1 KiB
JavaScript

// 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,
};