From 36fd25731a35c5c7ea0fbde1636bb56f1df8409c Mon Sep 17 00:00:00 2001 From: Horoli Date: Sun, 24 May 2026 19:15:31 +0900 Subject: [PATCH] fix: mobile team card width and modularize CSS with documentation --- agent.md | 15 +- context/match-ui.md | 7 +- context/style.md | 25 + src/styles.css | 1975 +------------------------------------ src/styles/animations.css | 158 +++ src/styles/base.css | 129 +++ src/styles/game-ui.css | 601 +++++++++++ src/styles/intro.css | 225 +++++ src/styles/mobile.css | 308 ++++++ src/styles/overlay.css | 566 +++++++++++ todo.md | 86 +- 11 files changed, 2117 insertions(+), 1978 deletions(-) create mode 100644 context/style.md create mode 100644 src/styles/animations.css create mode 100644 src/styles/base.css create mode 100644 src/styles/game-ui.css create mode 100644 src/styles/intro.css create mode 100644 src/styles/mobile.css create mode 100644 src/styles/overlay.css diff --git a/agent.md b/agent.md index dd8aa4b..ec98458 100644 --- a/agent.md +++ b/agent.md @@ -29,6 +29,10 @@ │ └── visitors.js # 유니크 방문자 체크 및 통계 API ├── public/ # 정적 리소스 (게임 에셋) │ └── assets/ +│ ├── effects/ # 공통 전투/월드 이펙트 스프라이트시트 +│ │ ├── heal/ # 처치 회복 연출 +│ │ ├── world_Effect.png # 화염 메테오 7프레임 이미지 +│ │ └── world_Effect_2.png # 냉기 메테오 7프레임 이미지 │ └── characters/ # 20종 이상의 캐릭터 스킨 및 투사체 에셋 │ ├── archer/, armored-axeman/, armored-orc/, ... (중략) │ └── wizard/ # 각 폴더 내 애니메이션 시트 및 이펙트 포함 @@ -39,19 +43,21 @@ ├── game/ # 게임 로직 모듈 (역할별 하위 폴더 구성) │ ├── arena/ # 아레나 및 씬 관리 │ │ ├── ArenaScene.js # 메인 게임 씬 (Orchestrator, 생명주기 및 모듈 조율) - │ │ ├── arenaRenderer.js# 경기장 바닥 및 격자 렌더링 + │ │ ├── arenaRenderer.js# 경기장 바닥, 격자 및 팀 시작 영역 렌더링 │ │ └── arenaSpectatorCamera.js # 지능형 관전 카메라 및 줌 로직 │ ├── combat/ # 전투 시스템 │ │ ├── combat.js # 전투 AI, 투사체 및 피격 판정 핵심 엔진 │ │ ├── combatSettings.js # 전투 속도 및 이동 배율 관리 - │ │ └── arenaFinalCombatEffects.js # 최종 교전 슬로우 모션 등 연출 효과 + │ │ ├── arenaFinalCombatEffects.js # 최종 교전 슬로우 모션 등 연출 효과 + │ │ └── worldEffects.js # 주기적 메테오/냉각지대 및 냉기 동결 효과 │ ├── fighter/ # 캐릭터 및 에셋 │ │ ├── fighterAssets.js # 스프라이트 로드 및 팀 실루엣 동적 생성 │ │ ├── fighterFactory.js # 캐릭터 인스턴스화 및 HUD 동기화 │ │ ├── fighterManifest.js # 20종 캐릭터 스탯/특성 상세 정의 + │ │ ├── fighterStats.js # 근접/원거리/마법 프로필 판별 및 스탯 해석 │ │ └── fighterSelection.js # 캐릭터 스킨 무작위 선택 로직 │ └── match/ # 매치 및 진행 - │ ├── matchSetup.js # 팀 구성 및 스폰 좌표 계산 (구역/랜덤) + │ ├── matchSetup.js # 팀 구성 및 스폰 좌표 계산 (스타팅 영역/랜덤) │ └── arenaMatchRuntime.js # 매치 진행 중 헬퍼 (스폰 클러스터, 팀 크기 동기화) └── ui/ # UI 컴포넌트 및 API 연동 ├── arenaKillLog.js # [New] 독립된 킬로그 DOM 조작 모듈 @@ -71,9 +77,10 @@ - **[인프라 및 전역 설정] [context/core.md](./context/core.md)**: `main.js`, `constants.js`, 개발/유지보수 공통 규칙. - **[서버 및 API] [context/server.md](./context/server.md)**: Fastify 서버, MongoDB 연동, 방문자 및 사망 통계 API 상세. - **[아레나 및 카메라] [context/arena.md](./context/arena.md)**: `ArenaScene` 오케스트레이션, 지능형 카메라 추적, 미니맵 가이드라인. -- **[전투 엔진] [context/combat.md](./context/combat.md)**: 전투 AI, 투사체 판정, 처치 보상 성장, 슬로우모션 연출. +- **[전투 엔진] [context/combat.md](./context/combat.md)**: 전투 AI, 투사체 판정, 처치 보상 성장, 슬로우모션 및 월드 이펙트 연출. - **[캐릭터 및 에셋] [context/fighter.md](./context/fighter.md)**: 캐릭터 공장, 동적 실루엣 생성, 종족 및 특성(Slime 등) 정의. - **[매치 로직 및 UI] [context/match-ui.md](./context/match-ui.md)**: 팀 구성 및 스폰 알고리즘, HUD 레이아웃, 킬로그, 승리 연출 UI. +- **[스타일 및 디자인] [context/style.md](./context/style.md)**: CSS 모듈 구조, 디자인 변수, 반응형 및 애니메이션 가이드. ## 4. 기술 사양 diff --git a/context/match-ui.md b/context/match-ui.md index 9cc9878..9d6b80d 100644 --- a/context/match-ui.md +++ b/context/match-ui.md @@ -18,14 +18,15 @@ ### 매치 설정 및 스폰 배치 - **완전 랜덤 배치**: 전장 전체 스폰 슬롯을 무작위로 섞어 배치합니다. -- **스타팅 지점 배치**: 참가자 수에 맞춰 전장을 구역으로 나눈 뒤, 참가자별 구역 배정을 매치마다 섞고 구역 내 무작위 위치에 스폰합니다. +- **스타팅 지점 배치**: 팀마다 전장 스폰 가능 그리드에서 중심 셀을 무작위로 고르고, 중심 주변 2칸(`5 x 5`)을 해당 팀의 스타팅 영역으로 사용합니다. 겹치지 않는 후보가 남아 있는 동안에는 해당 후보를 우선 선택하며, 영역은 매치 시작 후 5초 동안만 팀 색상으로 매우 옅게 표시되고 팀 전투원은 이 안에서만 스폰합니다. - **설정 유지**: 닉네임, 인원, 배치 모드는 `localStorage`에 저장되어 재접속 시 복원됩니다. ### 전투 화면 레이아웃 (HUD) - **팀 Badge**: 좌측 HUD 레일에 배치되며, 클릭 시 해당 팀의 생존 유닛 중 무작위 1명으로 시점을 고정합니다. -- **킬로그**: 처치자와 피처치자를 좌우로 배치하고, 피처치자 아이콘에 빨간 X를 겹쳐 사망 관계를 명확히 표시합니다. +- **팀 Badge 갱신 안정성**: 사망으로 생존 수가 바뀔 때 기존 badge 버튼 DOM을 유지한 채 숫자, 비활성 상태, 선택 강조만 갱신하여 사망 프레임에 겹친 클릭도 시점 고정으로 전달되도록 합니다. +- **킬로그**: 처치자와 피처치자를 좌우로 배치하고, 피처치자 아이콘에 빨간 X를 겹쳐 사망 관계를 명확히 표시합니다. 캐릭터 idle 시트의 `100x100` 프레임 내 투명 여백을 제외한 중앙 하단 영역을 확대 표시해 작은 아이콘 박스에서도 실루엣이 충분히 보이도록 합니다. - **하단 메타 정보**: 전투 화면 우측 하단(`arena-meta` 컨테이너)에 방문자 카운터와 About 버튼이 Pill(알약) 형태로 디자인이 통일되어 나란히 고정 배치됩니다. 드로어가 열려도 동일한 위치를 유지합니다. -- **모바일 레이아웃**: 실제 전투 시작 시 모바일에서는 옵션 drawer를 자동으로 접고, 상단 팀 HUD는 옵션 버튼 폭을 제외한 영역에 두 줄 4열로 맞춰 4개 이후 팀도 잘리지 않게 합니다. 모바일 팀 카드 선택 표시는 내부 테두리로 처리해 외곽선이 잘려 보이지 않게 합니다. 킬로그는 전투 캔버스 바로 아래에 배치하되 하단 메타 정보(방문자 카운터/About)와 겹치지 않게 안전 여백을 확보합니다. +- **모바일 레이아웃**: 실제 전투 시작 시 모바일에서는 옵션 drawer를 자동으로 접고, 상단 팀 HUD는 옵션 버튼 폭을 제외한 영역에 두 줄로 배치됩니다. 이때 데스크톱의 고정 가로폭 상속을 방지(`grid-template-columns: none`)하여 모든 팀 카드가 균일한 가로폭을 유지하도록 하며, 4개 이후 팀도 스크롤을 통해 확인할 수 있습니다. 모바일 팀 카드 선택 표시는 내부 테두리로 처리해 외곽선이 잘려 보이지 않게 합니다. 킬로그는 전투 캔버스 바로 아래에 배치하되 하단 메타 정보(방문자 카운터/About)와 겹치지 않게 안전 여백을 확보합니다. - **모바일 옵션 drawer**: 전투 중 펼친 옵션 drawer는 닉네임 입력 높이와 컨트롤 간격을 줄여 전투 시작/재시작/일시정지 버튼이 작은 화면에서도 한 번에 보이도록 합니다. - **승리 연출**: 승리 시 Web Audio 기반 팡파르와 CSS 애니메이션(광선, 컨페티)을 결합해 화려하게 연출합니다. 전투 종료 시 옵션 drawer를 접어 결과 배너가 설정 폼과 충돌하지 않게 하며, 결과 배너는 일정 시간 후 자동으로 사라지거나 클릭 시 즉시 닫힙니다. 무승부는 더 차분한 톤을 사용합니다. diff --git a/context/style.md b/context/style.md new file mode 100644 index 0000000..daa549c --- /dev/null +++ b/context/style.md @@ -0,0 +1,25 @@ +# Context: Style & Design + +## 1. CSS 모듈 구조 (src/styles/) + +이 프로젝트는 거대한 단일 CSS 파일을 지양하고, 기능별로 분리된 모듈형 CSS 구조를 채택하고 있습니다. `src/styles.css`는 각 모듈을 통합하는 엔트리 포인트 역할을 합니다. + +- **`base.css`**: 전역 변수(`:root`), 리셋 스타일, 레이아웃의 뼈대(`#app`, `#game`, `.arena-shell`)를 정의합니다. +- **`intro.css`**: 대기 화면, 로고 애니메이션, 전투 프리뷰 연출 스타일을 담당합니다. +- **`game-ui.css`**: 스코어보드(팀 badge), 킬로그, 상단 전투 안내바, 승리/무승부 축하 레이어 등 실제 게임 진행 중 노출되는 모든 HUD 요소를 관리합니다. +- **`overlay.css`**: 설정 드로어(전투 옵션 폼), About 다이얼로그 및 공통 폼 컨트롤 스타일을 정의합니다. +- **`animations.css`**: 프로젝트 전역에서 재사용되는 `@keyframes`와 애니메이션 관련 유틸리티 클래스를 포함합니다. +- **`mobile.css`**: `960px` 이하 해상도를 위한 미디어 쿼리 오버라이드 스타일을 통합 관리합니다. 모바일 전용 레이아웃 조정 및 터치 최적화 스타일이 포함됩니다. + +## 2. 디자인 시스템 및 변수 + +- **색상 체계**: 어두운 배경(`#080a07`)과 금색/주황색 계열의 포인트 컬러(`rgb(238 185 73)`)를 사용하여 판타지 아레나 분위기를 연출합니다. +- **반응형 대응**: `clamp()`, `min()`, `calc()` 등 현대적인 CSS 함수를 적극 활용하여 다양한 화면 크기에서도 유연하게 대응합니다. +- **가독성**: 텍스트 섀도우와 반투명 배경(`backdrop-filter`)을 활용해 복잡한 전투 화면 위에서도 UI 요소의 시인성을 확보합니다. + +## 3. 스타일 수정 가이드 + +- **전역 상수 변경**: 색상이나 기본 여백 등은 `base.css`의 `:root` 변수를 먼저 확인하십시오. +- **컴포넌트 스타일 수정**: 수정하려는 UI 요소가 속한 카테고리에 맞는 파일을 열어 작업하십시오. (예: 킬로그 수정 -> `game-ui.css`) +- **모바일 레이아웃 수정**: 데스크톱 스타일을 수정한 후에는 `mobile.css`에서 해당 요소가 모바일에서 어떻게 보이는지 반드시 확인하고 필요한 경우 오버라이드하십시오. +- **애니메이션 추가**: 새로운 키프레임은 `animations.css`에 추가하여 중앙 집중식으로 관리합니다. diff --git a/src/styles.css b/src/styles.css index 070f61e..4c8b79a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,1969 +1,6 @@ -:root { - color-scheme: dark; - font-family: - Inter, Pretendard, "Noto Sans KR", system-ui, -apple-system, BlinkMacSystemFont, - "Segoe UI", sans-serif; - background: #080a07; - color: #fff5db; -} - -* { - box-sizing: border-box; -} - -html { - min-width: 320px; - min-height: 100%; - background: #080a07; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; - overflow: hidden; -} - -button, -input, -textarea { - font: inherit; -} - -button { - border: 0; - cursor: pointer; -} - -button:focus-visible, -input:focus-visible, -textarea:focus-visible { - outline: 3px solid rgb(238 185 73 / 0.46); - outline-offset: 3px; -} - -#app { - --arena-gap: 18px; - --score-band-height: 134px; - --score-panel-left: 14px; - --score-panel-width: 224px; - --score-rail-width: calc(var(--score-panel-left) + var(--score-panel-width)); - --drawer-width: min(430px, 100vw); - --drawer-live-width: min(340px, calc(100vw - 48px)); - position: relative; - min-height: 100vh; - overflow: hidden; - background: - linear-gradient(180deg, rgb(8 10 7 / 0.18), rgb(3 5 4 / 0.84)), - #080a07; -} - -#app.match-live { - --drawer-width: var(--drawer-live-width); -} - -.arena-shell { - position: fixed; - inset: 0; - display: grid; - place-items: center; - overflow: hidden; - background: #090b08; -} - -.arena-shell::before { - content: ""; - position: absolute; - inset: 0; - z-index: 1; - background: - radial-gradient(circle at 50% 50%, rgb(255 211 122 / 0.06), transparent 42%), - linear-gradient(90deg, rgb(3 5 4 / 0.48), rgb(3 5 4 / 0.08) 45%, rgb(3 5 4 / 0.48)), - linear-gradient(180deg, rgb(3 5 4 / 0.08), rgb(3 5 4 / 0.5)); - pointer-events: none; - transition: - background 520ms ease, - opacity 520ms ease; -} - -#app.match-live .arena-shell::before { - opacity: 0.24; -} - -#app.match-live .arena-shell { - place-items: center; -} - -#game { - position: relative; - z-index: 0; - width: max(100vw, 100vh); - height: max(100vw, 100vh); - overflow: hidden; - opacity: 0.68; - filter: saturate(1) contrast(1.08) brightness(1.08); - transform: scale(1.04); - transform-origin: center; - transition: - width 620ms cubic-bezier(0.2, 0.8, 0.2, 1), - height 620ms cubic-bezier(0.2, 0.8, 0.2, 1), - opacity 520ms ease, - filter 520ms ease, - transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -#app.match-live #game { - width: min(100vw, 100vh); - height: min(100vw, 100vh); - margin-left: 0; - opacity: 1; - filter: none; - transform: scale(1); -} - -#game canvas { - display: block; - width: 100% !important; - height: 100% !important; - image-rendering: pixelated; -} - -.battle-preview { - position: fixed; - inset: 0; - z-index: 2; - overflow: hidden; - opacity: 0.84; - pointer-events: none; - transition: - opacity 420ms ease, - transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -#app.match-live .battle-preview { - opacity: 0; - transform: scale(1.08); -} - -.preview-fighter { - position: absolute; - width: 100px; - height: 100px; - background-repeat: no-repeat; - background-size: auto 100px; - image-rendering: pixelated; - transform-origin: center; - filter: - drop-shadow(0 18px 22px rgb(0 0 0 / 0.68)) - saturate(1.14) - brightness(1.12); - animation: - preview-attack var(--sprite-speed) steps(var(--sprite-steps)) infinite, - preview-breathe 1800ms ease-in-out infinite; -} - -.preview-knight { - --sprite-end: -600px; - --sprite-scale: 5.2; - --sprite-speed: 840ms; - --sprite-steps: 6; - left: 10vw; - top: 48vh; - background-image: url("/assets/characters/knight/Knight-Attack01.png"); - transform: scale(var(--sprite-scale)); -} - -.preview-orc { - --sprite-end: -500px; - --sprite-scale: 5.35; - --sprite-speed: 760ms; - --sprite-steps: 5; - right: 9vw; - top: 46vh; - background-image: url("/assets/characters/orc/Orc-Attack01.png"); - transform: scaleX(-1) scale(var(--sprite-scale)); -} - -.preview-wizard { - --sprite-end: -500px; - --sprite-scale: 4.35; - --sprite-speed: 980ms; - --sprite-steps: 5; - left: 56vw; - top: 24vh; - background-image: url("/assets/characters/wizard/Wizard-Attack01.png"); - opacity: 0.58; - transform: scaleX(-1) scale(var(--sprite-scale)); -} - -.preview-strike { - position: absolute; - width: 160px; - height: 5px; - border-radius: 999px; - background: linear-gradient(90deg, transparent, rgb(255 229 156 / 0.86), transparent); - box-shadow: 0 0 24px rgb(227 89 59 / 0.5); - opacity: 0; - transform-origin: center; - animation: preview-strike 980ms ease-in-out infinite; -} - -.preview-strike-a { - left: 38vw; - top: 54vh; - transform: rotate(-18deg); -} - -.preview-strike-b { - right: 31vw; - top: 42vh; - transform: rotate(22deg); - animation-delay: 260ms; -} - -.intro-stage { - position: fixed; - inset: 0; - z-index: 5; - display: grid; - place-items: center; - padding: clamp(24px, 5vw, 56px); - pointer-events: none; - transition: - opacity 420ms ease, - transform 620ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -#app.match-live .intro-stage { - opacity: 0; - transform: scale(0.96); -} - -#app.match-live .intro-content { - pointer-events: none; -} - -.intro-content { - display: grid; - justify-items: center; - gap: 22px; - text-align: center; - pointer-events: auto; - animation: intro-rise 760ms cubic-bezier(0.2, 0.8, 0.2, 1) both; - transition: - transform 560ms cubic-bezier(0.2, 0.8, 0.2, 1), - opacity 360ms ease; -} - -#app.options-open:not(.match-live) .intro-content { - opacity: 0.72; -} - -.arena-logo { - margin: 0; - color: #fff4d1; - font-size: clamp(4rem, 16vw, 11rem); - font-weight: 950; - letter-spacing: 0; - line-height: 0.9; - text-shadow: - 0 2px 0 #ad4d37, - 0 14px 42px rgb(0 0 0 / 0.72), - 0 0 40px rgb(230 173 71 / 0.28); - text-transform: uppercase; -} - -.arena-logo .small-text { - font-size: 0.7em; /* 원래 크기의 85%로 축소 (원하는 만큼 숫자를 조절해 보세요) */ - margin-top: 0.05em; /* 글자가 줄어들면서 생긴 간격을 미세하게 조정하고 싶다면 사용 */ -} - -.arena-logo span { - display: block; -} - -.arena-meta { - position: fixed; - right: clamp(10px, 2vw, 18px); - bottom: clamp(10px, 2vw, 18px); - z-index: 10; - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - pointer-events: none; - transition: opacity 220ms ease; -} - -.visitor-count, -.about-button { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 28px; - margin: 0; - border: 1px solid rgb(238 185 73 / 0.22); - border-radius: 999px; - padding: 5px 12px; - background: rgb(8 10 7 / 0.68); - color: #e7c879; - font-size: 0.72rem; - font-weight: 800; - line-height: 1; - text-decoration: none; - backdrop-filter: blur(10px); - pointer-events: auto; - transition: - background 180ms ease, - border-color 180ms ease, - transform 180ms ease, - opacity 220ms ease; -} - -.visitor-count { - opacity: 0; - transform: translateY(8px); -} - -#app.match-live .visitor-count { - opacity: 0.86; - transform: translateY(0); -} - -.about-button { - min-width: 72px; - color: #ffe8b4; - font-weight: 900; - box-shadow: 0 4px 12px rgb(0 0 0 / 0.22); -} - -.about-button:hover { - border-color: rgb(238 185 73 / 0.42); - background: rgb(255 246 216 / 0.14); - transform: translateY(-1px); -} - -.start-button, -form button[type="submit"], -.pause-button, -.restart-button { - min-height: 52px; - border-radius: 8px; - background: linear-gradient(180deg, #e56443, #b93c2f); - color: #fff7df; - font-weight: 900; - box-shadow: - 0 18px 44px rgb(0 0 0 / 0.36), - inset 0 1px 0 rgb(255 255 255 / 0.2); - transition: - background 180ms ease, - transform 180ms ease, - box-shadow 180ms ease; -} - -.start-button { - min-width: 180px; - padding: 0 30px; - text-transform: uppercase; -} - -#app.options-open:not(.match-live) .start-button { - pointer-events: none; - visibility: hidden; -} - -.start-button:hover, -form button[type="submit"]:hover, -.pause-button:hover, -.restart-button:hover { - background: linear-gradient(180deg, #f0754f, #c84636); - transform: translateY(-1px); - box-shadow: - 0 22px 52px rgb(0 0 0 / 0.42), - inset 0 1px 0 rgb(255 255 255 / 0.24); -} - -.pause-button, -.restart-button { - display: none; - border: 1px solid rgb(238 185 73 / 0.3); - background: rgb(255 246 216 / 0.08); - color: #ffe8b4; -} - -.pause-button:hover, -.restart-button:hover { - background: rgb(255 246 216 / 0.14); -} - -#app.match-live .pause-button, -#app.match-live .restart-button { - display: block; -} - -#app.match-live .match-actions { - grid-template-columns: 1fr 1fr; -} - -#app.match-live .match-actions button[type="submit"] { - grid-column: 1 / -1; -} - -#app.match-paused .pause-button { - background: linear-gradient(180deg, #e3b24f, #9a6c24); - color: #120f08; -} - -#app.match-ended .pause-button { - display: none; -} - -.drawer-scrim { - position: fixed; - inset: 0; - z-index: 6; - background: rgb(4 5 4 / 0.42); - opacity: 0; - pointer-events: none; - transition: opacity 320ms ease; -} - -#app.options-open .drawer-scrim { - opacity: 1; - pointer-events: auto; -} - -#app.match-live .drawer-scrim { - opacity: 0; - pointer-events: none; -} - -.drawer-toggle { - display: none; - min-height: 40px; - border: 1px solid rgb(238 185 73 / 0.28); - border-radius: 8px; - padding: 0 12px; - background: rgb(12 15 11 / 0.84); - color: #ffe8b4; - font-size: 0.82rem; - font-weight: 900; - box-shadow: 0 16px 38px rgb(0 0 0 / 0.36); - transition: - background 180ms ease, - transform 180ms ease; - backdrop-filter: blur(10px); -} - -.drawer-toggle:hover { - background: rgb(255 246 216 / 0.14); - transform: translateY(-1px); -} - -#app.match-live .drawer-toggle { - display: block; -} - -.fighter-entry { - position: fixed; - top: 0; - right: 0; - z-index: 7; - display: grid; - align-content: start; - gap: 24px; - width: var(--drawer-width); - height: 100vh; - overflow-y: auto; - border-left: 1px solid rgb(239 199 103 / 0.22); - padding: clamp(22px, 4vw, 34px); - background: - linear-gradient(180deg, rgb(29 33 22 / 0.94), rgb(13 16 12 / 0.96)), - #11140f; - box-shadow: -28px 0 80px rgb(0 0 0 / 0.52); - transform: translateX(104%); - transition: - opacity 260ms ease, - transform 520ms cubic-bezier(0.2, 0.8, 0.2, 1); - backdrop-filter: blur(16px); -} - -#app.options-open .fighter-entry { - transform: translateX(0); -} - -#app.match-live .fighter-entry { - top: 24px; - right: 24px; - height: auto; - max-height: calc(100vh - 48px); - gap: 16px; - border: 1px solid rgb(239 199 103 / 0.22); - border-radius: 8px; - padding: 20px; - transform: translateX(0); -} - -#app.match-live.drawer-collapsed .fighter-entry { - width: auto; - min-width: 0; - overflow: visible; - border-color: transparent; - padding: 0; - background: transparent; - box-shadow: none; - opacity: 1; - pointer-events: auto; - transform: translateX(0); -} - -#app.match-live .drawer-close { - display: none; -} - -#app.match-live .fighter-entry h2 { - font-size: 2rem; -} - -#app.match-live .fighter-entry textarea { - min-height: 190px; -} - -#app.match-live .fighter-entry fieldset { - padding: 12px; -} - -#app.match-live.drawer-collapsed .entry-copy, -#app.match-live.drawer-collapsed .fighter-entry form { - display: none; -} - -#app.match-live.drawer-collapsed .drawer-header { - justify-content: end; -} - -.drawer-header { - display: flex; - align-items: start; - justify-content: space-between; - gap: 16px; -} - -.drawer-header-controls { - display: flex; - align-items: center; - gap: 8px; -} - -.entry-copy { - display: grid; - gap: 8px; -} - -.eyebrow { - margin: 0; - color: #e3b24f; - font-size: 0.78rem; - font-weight: 900; - letter-spacing: 0; - text-transform: uppercase; -} - -h2 { - margin: 0; - color: #fff3d2; - font-size: clamp(1.7rem, 4vw, 2.5rem); - line-height: 1.05; - letter-spacing: 0; -} - -.drawer-close { - display: grid; - place-items: center; - width: 40px; - height: 40px; - flex: 0 0 auto; - border: 1px solid rgb(238 185 73 / 0.22); - border-radius: 8px; - background: rgb(255 246 216 / 0.08); - color: #f8deb0; - font-weight: 900; -} - -.drawer-close:hover { - background: rgb(255 246 216 / 0.14); -} - -.about-backdrop { - position: fixed; - inset: 0; - z-index: 20; - display: grid; - place-items: center; - padding: clamp(16px, 4vw, 34px); - background: rgb(3 5 4 / 0.66); - backdrop-filter: blur(8px); -} - -.about-backdrop[hidden] { - display: none; -} - -.about-dialog { - display: grid; - grid-template-rows: auto auto minmax(0, 1fr); - width: min(560px, calc(100vw - 32px)); - max-height: min(760px, calc(100svh - 32px)); - overflow: hidden; - border: 1px solid rgb(239 199 103 / 0.28); - border-radius: 8px; - background: - linear-gradient(180deg, rgb(29 33 22 / 0.98), rgb(10 13 9 / 0.98)), - #11140f; - box-shadow: - 0 24px 100px rgb(0 0 0 / 0.62), - inset 0 1px 0 rgb(255 255 255 / 0.06); - outline: none; -} - -.about-header { - display: flex; - align-items: start; - justify-content: space-between; - gap: 16px; - padding: clamp(20px, 4vw, 28px) clamp(20px, 4vw, 30px) 14px; -} - -.about-close { - display: grid; - place-items: center; - width: 40px; - height: 40px; - flex: 0 0 auto; - border: 1px solid rgb(238 185 73 / 0.22); - border-radius: 8px; - background: rgb(255 246 216 / 0.08); - color: #f8deb0; - font-weight: 900; -} - -.about-close:hover { - background: rgb(255 246 216 / 0.14); -} - -.about-tabs { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 4px; - padding: 0 clamp(20px, 4vw, 30px) 14px; - border-bottom: 1px solid rgb(238 185 73 / 0.16); -} - -.about-tab { - min-width: 0; - min-height: 42px; - border: 1px solid transparent; - border-radius: 6px; - padding: 8px 10px; - background: rgb(255 246 216 / 0.06); - color: #ead8ad; - font-size: 0.86rem; - font-weight: 900; - line-height: 1.2; -} - -.about-tab[aria-selected="true"] { - border-color: rgb(238 185 73 / 0.36); - background: #323822; - color: #fff7df; -} - -.about-panel { - min-height: 0; - overflow: auto; - padding: clamp(18px, 4vw, 26px) clamp(20px, 4vw, 30px) clamp(22px, 5vw, 34px); -} - -.about-fields { - display: grid; - gap: 0; - margin: 0; -} - -.about-field-row { - display: grid; - grid-template-columns: 96px minmax(0, 1fr); - align-items: center; - gap: 14px; - min-height: 50px; - border-bottom: 1px solid rgb(238 185 73 / 0.14); -} - -.about-field-row:first-child { - border-top: 1px solid rgb(238 185 73 / 0.14); -} - -.about-field-row dt { - color: #e3b24f; - font-size: 0.78rem; - font-weight: 950; - text-transform: uppercase; -} - -.about-field-row dd { - min-width: 0; - margin: 0; - color: #fff7df; - font-weight: 800; - overflow-wrap: anywhere; -} - -.about-field-row a, -.about-markdown a { - color: #85dcc7; - text-decoration-color: rgb(133 220 199 / 0.42); - text-underline-offset: 3px; -} - -.about-markdown { - display: grid; - gap: 12px; - color: #ead8ad; - font-size: 0.92rem; - font-weight: 600; - line-height: 1.56; -} - -.about-markdown :is(h3, h4, h5, h6, p, ul, blockquote) { - margin: 0; -} - -.about-markdown h3, -.about-markdown h4, -.about-markdown h5, -.about-markdown h6 { - color: #fff3d2; - font-size: 1rem; - line-height: 1.3; -} - -.about-markdown blockquote { - border-left: 3px solid rgb(238 185 73 / 0.36); - padding: 4px 0 4px 16px; - color: #c4b693; - font-style: italic; -} - -.about-markdown hr { - margin: 8px 0; - border: 0; - border-top: 1px solid rgb(238 185 73 / 0.16); -} - -.about-markdown code { - border: 1px solid rgb(238 185 73 / 0.14); - border-radius: 4px; - padding: 2px 5px; - background: rgb(255 246 216 / 0.08); - color: #f1c761; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 0.88em; -} - -.about-markdown li { - line-height: 1.5; -} - -.about-markdown li strong { - color: #fff3d2; -} - -.about-markdown ul { - display: grid; - gap: 8px; - padding-left: 1.2rem; -} - -.about-empty { - color: #bfae83; -} - -form { - display: grid; - gap: 16px; -} - -.match-actions { - display: grid; - gap: 10px; -} - -fieldset { - display: grid; - gap: 10px; - min-width: 0; - margin: 0; - border: 1px solid rgb(238 185 73 / 0.22); - border-radius: 8px; - padding: 14px; - background: rgb(5 7 5 / 0.26); -} - -legend { - padding: 0 6px; - color: #e3b24f; - font-size: 0.78rem; - font-weight: 900; - letter-spacing: 0; - text-transform: uppercase; -} - -.team-size-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.team-size-number { - width: 88px; - min-width: 88px; - padding-inline: 10px; - text-align: center; - font-weight: 900; -} - -label { - color: #ead8ad; - font-size: 0.92rem; -} - -input:not([type="range"]):not([type="radio"]), -textarea { - min-height: 48px; - border: 1px solid rgb(238 185 73 / 0.28); - border-radius: 8px; - padding: 0 14px; - background: #232719; - color: #fff7df; - outline: none; -} - -textarea { - min-height: 258px; - resize: vertical; - padding-block: 12px; - line-height: 1.45; -} - -input[type="range"] { - width: 100%; - accent-color: #e3b24f; -} - -.spawn-placement-field { - display: grid; - gap: 8px; -} - -.spawn-placement-label { - color: #ead8ad; - font-size: 0.92rem; -} - -.spawn-placement-options { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 4px; - min-width: 0; - border: 1px solid rgb(238 185 73 / 0.2); - border-radius: 8px; - padding: 4px; - background: #1d2116; -} - -.spawn-placement-option { - position: relative; - min-width: 0; -} - -.spawn-placement-option input { - position: absolute; - width: 1px; - height: 1px; - opacity: 0; - pointer-events: none; -} - -.spawn-placement-option span { - display: grid; - min-height: 44px; - place-items: center; - border: 1px solid transparent; - border-radius: 6px; - padding: 8px; - color: #ead8ad; - text-align: center; - font-size: 0.86rem; - font-weight: 900; - line-height: 1.25; - cursor: pointer; -} - -.spawn-placement-option input:checked + span { - border-color: rgb(238 185 73 / 0.36); - background: #323822; - color: #fff7df; -} - -.spawn-placement-option input:focus-visible + span { - outline: 2px solid #f1c761; - outline-offset: -2px; -} - -.scoreboard { - position: fixed; - top: clamp(14px, 3vw, 28px); - left: var(--score-panel-left); - z-index: 3; - display: flex; - justify-content: flex-start; - width: var(--score-panel-width); - max-height: calc(100vh - 96px); - min-height: 64px; - overflow: hidden; - padding: 8px; - border: 1px solid rgb(238 185 73 / 0.18); - border-radius: 8px; - background: rgb(4 6 4 / 0.5); - opacity: 0; - pointer-events: none; - transform: translateY(-18px); - transition: - opacity 420ms ease, - transform 420ms ease; - backdrop-filter: blur(10px); -} - -#app.match-live .scoreboard { - opacity: 1; - pointer-events: auto; - transform: translateY(0); -} - -.score-side { - display: flex; - flex-wrap: wrap; - gap: 6px; - width: max-content; -} - -.score-side.right { - display: none; -} - -.team-score { - display: grid; - grid-template-rows: auto 1px auto; - gap: 5px; - width: 100px; - min-height: 54px; - overflow: hidden; - border-radius: 6px; - padding: 7px 9px 6px; - color: #fff; - font-size: 0.8rem; - font-weight: 900; - text-align: left; - text-shadow: 1px 1px 2px #000; - transition: - filter 160ms ease, - transform 160ms ease; -} - -.team-score:hover { - filter: brightness(1.16); - transform: translateY(-1px); -} - -.team-score.is-focused { - box-shadow: - inset 0 0 0 2px rgb(255 244 209 / 0.92), - 0 0 18px rgb(227 178 79 / 0.26); -} - -.team-score:disabled { - cursor: default; - filter: grayscale(0.6) brightness(0.68); -} - -.team-score:disabled:hover { - transform: none; -} - -.team-score-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.team-score-rule { - width: 100%; - background: var(--team-color); - opacity: 0.9; -} - -.team-score-count { - justify-self: end; - color: #fff2c8; - font-size: 0.86rem; -} - -.battle-notice { - position: fixed; - top: clamp(12px, 2vw, 20px); - left: 50%; - z-index: 5; - display: flex; - align-items: center; - justify-content: center; - width: min(420px, 72vmin, calc(100vw - 64px)); - min-height: 38px; - border: 1px solid rgb(238 185 73 / 0.26); - border-radius: 8px; - padding: 8px 14px; - background: rgb(8 10 7 / 0.68); - color: #ffe8b4; - font-size: 0.8rem; - font-weight: 900; - line-height: 1.35; - text-align: center; - text-shadow: 1px 1px 2px #000; - opacity: 0; - pointer-events: none; - transform: translate(-50%, -10px); - transition: - opacity 260ms ease, - transform 260ms ease; - backdrop-filter: blur(10px); -} - -#app.match-live .battle-notice.is-visible { - opacity: 1; - transform: translate(-50%, 0); -} - -@media (min-width: 961px) { - #app.match-live .battle-notice { - right: auto; - left: 50%; - width: min(420px, 72vmin, calc(100vw - var(--drawer-width) - var(--score-rail-width) - 56px)); - transform: translate(-50%, -10px); - } - - #app.match-live .battle-notice.is-visible { - transform: translate(-50%, 0); - } - - #app.match-live.drawer-collapsed .battle-notice { - right: auto; - width: min(420px, 72vmin, calc(100vw - 64px)); - } -} - -.kill-log { - position: fixed; - bottom: clamp(14px, 3vw, 26px); - left: var(--score-panel-left); - z-index: 4; - width: min(370px, calc(100vw - 32px)); - max-height: min(34vh, 292px); - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: rgb(238 185 73 / 0.3) transparent; - border: 1px solid rgb(238 185 73 / 0.2); - border-radius: 8px; - padding: 10px; - background: rgb(4 6 4 / 0.58); - opacity: 0; - pointer-events: none; - transform: translateY(16px); - transition: - opacity 260ms ease, - transform 260ms ease; - backdrop-filter: blur(10px); -} - -.kill-log::-webkit-scrollbar { - width: 4px; -} - -.kill-log::-webkit-scrollbar-track { - background: transparent; -} - -.kill-log::-webkit-scrollbar-thumb { - border-radius: 10px; - background: rgb(238 185 73 / 0.3); -} - -#app.match-live .kill-log.has-entries { - opacity: 1; - pointer-events: auto; - transform: translateY(0); -} - -.kill-log-list { - display: flex; - flex-direction: column; - gap: 6px; - min-height: 0; - margin: 0; - padding: 0; - list-style: none; -} - -.kill-log-item { - display: grid; - grid-template-columns: minmax(0, 1fr) 54px minmax(0, 1fr); - align-items: center; - gap: 8px; - min-height: 54px; - border: 1px solid rgb(255 244 209 / 0.12); - border-radius: 6px; - padding: 7px 9px; - background: rgb(8 10 7 / 0.74); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06); - animation: kill-log-entry 180ms ease both; -} - -.kill-log-fighter { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; -} - -.kill-log-fighter.killer { - border-left: 3px solid var(--killer-color); - padding-left: 6px; -} - -.kill-log-fighter.victim { - flex-direction: row-reverse; - border-right: 3px solid var(--victim-color); - padding-right: 6px; - justify-content: end; - text-align: right; -} - -.kill-log-avatar { - position: relative; - flex: 0 0 auto; - width: 36px; - height: 36px; - border: 1px solid rgb(255 244 209 / 0.16); - border-radius: 6px; - background-color: rgb(255 246 216 / 0.08); - background-position: left center; - background-repeat: no-repeat; - background-size: auto 36px; - image-rendering: pixelated; - box-shadow: inset 0 -10px 18px rgb(0 0 0 / 0.22); -} - -.kill-log-fighter.victim .kill-log-avatar::before, -.kill-log-fighter.victim .kill-log-avatar::after { - content: ""; - position: absolute; - top: 7px; - right: 1px; - width: 14px; - height: 2px; - border: 1px solid rgb(255 216 212 / 0.22); - border-radius: 999px; - background: #f24a42; - box-shadow: - 0 0 0 1px rgb(48 4 3 / 0.7), - 0 0 5px rgb(227 54 46 / 0.6); - transform-origin: center; -} - -.kill-log-fighter.victim .kill-log-avatar::before { - transform: rotate(45deg); -} - -.kill-log-fighter.victim .kill-log-avatar::after { - transform: rotate(-45deg); -} - -.kill-log-copy { - display: grid; - gap: 2px; - min-width: 0; -} - -.kill-log-team, -.kill-log-member { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.kill-log-team { - min-width: 0; - color: #fff7df; - font-size: 0.78rem; - font-weight: 900; - text-shadow: 1px 1px 2px #000; -} - -.kill-log-member { - flex: 0 0 auto; - color: #ead8ad; - font-size: 0.72rem; - font-weight: 800; -} - -.kill-log-action { - display: grid; - justify-items: center; - gap: 2px; - min-width: 0; -} - -.kill-log-action-text { - color: #ffdc93; - font-size: 0.68rem; - font-weight: 950; - line-height: 1; -} - -.kill-log-weapon { - position: relative; - display: block; - width: 28px; - height: 28px; - place-self: center; - border: 1px solid rgb(238 185 73 / 0.28); - border-radius: 999px; - background: rgb(255 246 216 / 0.08); - box-shadow: 0 0 16px rgb(227 89 59 / 0.16); -} - -.kill-log-weapon::before, -.kill-log-weapon::after { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: 18px; - height: 3px; - border-radius: 999px; - background: linear-gradient(90deg, #ffe8b4 0 70%, #b93c2f 70% 100%); - box-shadow: 0 0 8px rgb(255 226 166 / 0.3); - transform-origin: center; -} - -.kill-log-weapon::before { - transform: translate(-50%, -50%) rotate(42deg); -} - -.kill-log-weapon::after { - transform: translate(-50%, -50%) rotate(-42deg); -} - -.victory-celebration { - position: fixed; - z-index: 9; - display: grid; - overflow: hidden; - place-items: center; - inset: 0; - background: rgb(4 6 4 / 0.2); - isolation: isolate; - opacity: 1; - pointer-events: auto; - transform: scale(1); - transition: - opacity 220ms ease, - transform 220ms ease; -} - -.victory-celebration.is-leaving { - opacity: 0; - transform: scale(0.98); -} - -.victory-celebration::before { - content: ""; - position: absolute; - z-index: -1; - width: min(122vmin, 1240px); - aspect-ratio: 1; - border-radius: 50%; - background: - radial-gradient(circle, rgb(255 233 166 / 0.18) 0 18%, rgb(227 178 79 / 0.12) 31%, transparent 66%); - animation: victory-glow 1.8s ease-out both; -} - -.victory-celebration.is-draw::before { - background: - radial-gradient(circle, rgb(255 247 223 / 0.16) 0 18%, rgb(227 178 79 / 0.1) 31%, transparent 62%); -} - -.victory-rays { - position: absolute; - z-index: 0; - width: min(112vmin, 1120px); - aspect-ratio: 1; - border-radius: 50%; - background: repeating-conic-gradient( - from -4deg, - rgb(255 233 166 / 0.18) 0 8deg, - transparent 8deg 18deg - ); - opacity: 0.54; - mask-image: radial-gradient(circle, #000 0 18%, transparent 66%); - animation: victory-rays-in 1.1s ease-out both, victory-rays-turn 11s linear infinite; -} - -.victory-celebration.is-draw .victory-rays { - opacity: 0.22; -} - -.victory-confetti { - position: absolute; - z-index: 1; - inset: 0; -} - -.victory-confetti-piece { - position: absolute; - top: 50%; - left: 50%; - display: block; - width: clamp(6px, 0.8vw, 11px); - height: clamp(10px, 1.2vw, 18px); - border-radius: 8px; - background: var(--confetti-color); - box-shadow: 0 0 12px rgb(255 230 166 / 0.22); - opacity: 0; - transform: translate(-50%, -50%) rotate(var(--confetti-tilt)) scale(0.3); - animation: victory-confetti-burst var(--confetti-duration) cubic-bezier(0.15, 0.84, 0.35, 1) var(--confetti-delay) both; -} - -.victory-confetti-piece:nth-child(3n) { - width: clamp(10px, 1vw, 15px); - height: clamp(6px, 0.72vw, 10px); - border-radius: 2px; -} - -.victory-banner { - position: relative; - z-index: 2; - display: grid; - width: min(calc(100vw - 36px), 760px); - min-height: clamp(108px, 18vw, 170px); - overflow: hidden; - place-items: center; - border: 2px solid #f1c45d; - border-radius: 8px; - padding: clamp(1.25rem, 3.8vw, 2rem) clamp(1.3rem, 5.4vw, 3.4rem); - background: - linear-gradient(135deg, rgb(18 21 13 / 0.98), rgb(3 5 4 / 0.92)), - rgb(4 6 4 / 0.9); - color: #fff7df; - font-size: clamp(1.65rem, 5vw, 3rem); - font-weight: 950; - letter-spacing: 0; - line-height: 1.12; - text-align: center; - text-wrap: balance; - text-shadow: - 0 2px 0 rgb(55 36 8 / 0.56), - 0 0 24px rgb(255 226 153 / 0.28); - box-shadow: - 0 0 0 1px rgb(255 237 187 / 0.2) inset, - 0 0 42px rgb(227 178 79 / 0.44), - 0 24px 90px rgb(0 0 0 / 0.58); - animation: banner-in 0.64s cubic-bezier(0.16, 0.9, 0.25, 1.2); - backdrop-filter: blur(6px); -} - -.victory-banner::before { - content: ""; - position: absolute; - inset: -40% auto -40% -36%; - width: 28%; - background: linear-gradient(90deg, transparent, rgb(255 248 223 / 0.6), transparent); - transform: skewX(-18deg); - animation: victory-banner-sheen 1s 0.28s ease-out both; -} - -.victory-banner::after { - content: ""; - position: absolute; - inset: 10px; - border: 1px solid rgb(255 225 151 / 0.24); - border-radius: 5px; -} - -.victory-banner-message { - position: relative; - z-index: 1; - display: block; - max-width: 100%; - overflow-wrap: anywhere; - animation: victory-message-pulse 720ms 80ms ease-out both; -} - -.victory-celebration.is-draw .victory-banner { - border-color: #d8c28d; - box-shadow: - 0 0 0 1px rgb(255 237 187 / 0.14) inset, - 0 0 28px rgb(227 178 79 / 0.24), - 0 24px 90px rgb(0 0 0 / 0.52); -} - -#app.match-paused .arena-shell::after { - content: "일시정지"; - position: fixed; - top: 50%; - left: 50%; - z-index: 6; - border: 1px solid rgb(238 185 73 / 0.34); - border-radius: 8px; - padding: 14px 26px; - background: rgb(5 7 5 / 0.76); - color: #ffe8b4; - font-size: clamp(1.3rem, 4vw, 2rem); - font-weight: 950; - transform: translate(-50%, -50%); - box-shadow: 0 18px 60px rgb(0 0 0 / 0.46); - backdrop-filter: blur(8px); -} - -.match-status { - position: fixed; - bottom: clamp(14px, 3vw, 26px); - left: 50%; - z-index: 4; - width: min(980px, calc(100vw - 32px)); - min-height: 48px; - overflow: hidden; - border: 1px solid rgb(238 185 73 / 0.28); - border-radius: 8px; - padding: 13px 0; - background: rgb(8 10 7 / 0.74); - color: #ffe2a6; - font-weight: 900; - opacity: 0; - pointer-events: none; - transform: translate(-50%, calc(100% + 28px)); - transition: - opacity 420ms ease, - transform 420ms ease; - backdrop-filter: blur(10px); -} - -#app.status-active:not(.match-live) .match-status { - opacity: 1; - transform: translate(-50%, 0); -} - -#app.match-live .match-status { - display: none; -} - -@media (min-width: 961px) { - #app.match-live .match-status { - left: calc((100vw - var(--drawer-width)) / 2); - width: min(760px, calc(100vw - var(--drawer-width) - 32px)); - } - - #app.match-live.drawer-collapsed .match-status { - left: 50%; - width: min(980px, calc(100vw - 32px)); - } -} - -.status-track { - display: flex; - width: max-content; - min-width: 200%; - gap: 64px; - animation: status-marquee 22s linear infinite; -} - -.status-track span { - flex: 0 0 auto; - min-width: calc(50vw - 32px); - padding-left: 28px; - white-space: nowrap; -} - -@keyframes intro-rise { - from { - opacity: 0; - transform: translateY(22px) scale(0.96); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes preview-attack { - to { - background-position-x: var(--sprite-end); - } -} - -@keyframes preview-breathe { - 0%, - 100% { - margin-top: 0; - } - 50% { - margin-top: -8px; - } -} - -@keyframes preview-strike { - 0%, - 58%, - 100% { - opacity: 0; - } - 64%, - 76% { - opacity: 0.86; - } -} - -@keyframes status-marquee { - from { - transform: translateX(0); - } - to { - transform: translateX(-50%); - } -} - -@keyframes kill-log-entry { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes banner-in { - from { - opacity: 0; - transform: translateY(18px) scale(0.78); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes victory-banner-sheen { - from { - opacity: 0; - transform: translateX(0) skewX(-18deg); - } - 18% { - opacity: 1; - } - to { - opacity: 0; - transform: translateX(560%) skewX(-18deg); - } -} - -@keyframes victory-confetti-burst { - 0% { - opacity: 0; - transform: translate(-50%, -50%) rotate(var(--confetti-tilt)) scale(0.3); - } - 12% { - opacity: 1; - } - 74% { - opacity: 1; - } - 100% { - opacity: 0; - transform: - translate(calc(-50% + var(--confetti-x)), calc(-50% + var(--confetti-y))) - rotate(calc(var(--confetti-tilt) + var(--confetti-spin))) - scale(1); - } -} - -@keyframes victory-glow { - from { - opacity: 0; - transform: scale(0.58); - } - 35% { - opacity: 1; - } - to { - opacity: 0.8; - transform: scale(1); - } -} - -@keyframes victory-rays-in { - from { - transform: scale(0.56); - } - to { - transform: scale(1); - } -} - -@keyframes victory-rays-turn { - to { - rotate: 360deg; - } -} - -@keyframes victory-message-pulse { - from { - opacity: 0.72; - transform: scale(0.88); - } - 58% { - transform: scale(1.05); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@media (prefers-reduced-motion: reduce) { - .victory-banner, - .victory-banner::before, - .victory-banner-message, - .victory-celebration::before, - .victory-confetti-piece, - .victory-rays { - animation-duration: 1ms; - animation-iteration-count: 1; - } -} - -@media (max-width: 960px) { - body { - overflow: hidden; - } - - #app { - --arena-gap: 0px; - --mobile-game-size: min(100vw, calc(100svh - var(--score-band-height))); - --mobile-kill-log-top: calc(var(--score-band-height) + var(--mobile-game-size) + 10px); - --mobile-options-button-width: 54px; - --mobile-options-gap: 8px; - --mobile-team-card-width: clamp(56px, calc((100vw - 120px) / 4), 72px); - --mobile-visitor-space: calc(104px + env(safe-area-inset-bottom)); - --score-band-height: 132px; - --score-panel-left: 10px; - --score-panel-width: calc(100vw - 20px); - --score-rail-width: 0px; - } - - #app.match-live .arena-shell { - place-items: start center; - } - - #app.match-live #game { - width: var(--mobile-game-size); - height: var(--mobile-game-size); - margin-top: var(--score-band-height); - margin-left: 0; - } - - .intro-stage { - padding: 20px; - } - - .arena-logo { - font-size: clamp(3.8rem, 22vw, 7rem); - } - - .fighter-entry { - width: 100vw; - padding: 22px; - } - - #app.match-live .fighter-entry { - top: calc(10px + env(safe-area-inset-top)); - right: 10px; - left: 10px; - width: auto; - max-height: calc(100svh - 20px - env(safe-area-inset-top) - env(safe-area-inset-bottom)); - gap: 10px; - padding: 12px; - } - - #app.match-live.drawer-collapsed .fighter-entry { - top: calc(22px + env(safe-area-inset-top)); - right: 10px; - left: auto; - width: auto; - } - - #app.match-live .fighter-entry h2 { - font-size: clamp(1.45rem, 7vw, 1.8rem); - } - - #app.match-live .fighter-entry textarea { - height: 112px; - min-height: 112px; - resize: none; - } - - #app.match-live .fighter-entry fieldset { - gap: 7px; - padding: 9px; - } - - #app.match-live .fighter-entry form { - gap: 10px; - } - - #app.match-live .entry-copy { - gap: 4px; - } - - #app.match-live .eyebrow { - font-size: 0.68rem; - } - - #app.match-live label, - #app.match-live .spawn-placement-label { - font-size: 0.82rem; - } - - #app.match-live input:not([type="range"]):not([type="radio"]), - #app.match-live textarea { - min-height: 40px; - padding-inline: 10px; - } - - #app.match-live textarea { - padding-block: 9px; - } - - #app.match-live .team-size-number { - width: 64px; - min-width: 64px; - } - - #app.match-live .spawn-placement-option span { - min-height: 36px; - padding: 6px; - font-size: 0.76rem; - } - - #app.match-live .match-actions { - gap: 8px; - } - - #app.match-live .match-actions button { - min-height: 44px; - } - - #app.match-live .drawer-toggle { - min-width: 116px; - } - - #app.match-live.drawer-collapsed .drawer-toggle { - width: var(--mobile-options-button-width); - min-width: var(--mobile-options-button-width); - padding-inline: 6px; - font-size: 0; - } - - #app.match-live.drawer-collapsed .drawer-toggle::before { - content: "옵션"; - font-size: 0.78rem; - line-height: 1; - } - - .battle-preview { - opacity: 0.62; - } - - .preview-knight { - left: -8vw; - top: 52vh; - } - - .preview-orc { - right: -9vw; - top: 49vh; - } - - .preview-wizard { - left: 48vw; - top: 21vh; - } - - .scoreboard { - align-items: flex-start; - top: 10px; - left: var(--score-panel-left); - width: var(--score-panel-width); - max-height: calc(var(--score-band-height) - 12px); - overflow-x: auto; - overflow-y: hidden; - padding: 9px min(148px, 38vw) 9px 9px; - scrollbar-color: rgb(238 185 73 / 0.38) transparent; - scrollbar-width: thin; - touch-action: pan-x; - } - - #app.match-live.drawer-collapsed .scoreboard { - width: calc( - 100vw - 20px - var(--mobile-options-button-width) - var(--mobile-options-gap) - ); - padding-right: 9px; - } - - .score-side { - display: grid; - grid-auto-columns: var(--mobile-team-card-width); - grid-auto-flow: column; - grid-template-rows: repeat(2, 48px); - gap: 5px 5px; - } - - .scoreboard::-webkit-scrollbar { - height: 4px; - } - - .scoreboard::-webkit-scrollbar-track { - background: transparent; - } - - .scoreboard::-webkit-scrollbar-thumb { - border-radius: 999px; - background: rgb(238 185 73 / 0.38); - } - - .team-score { - width: auto; - min-height: 0; - height: 48px; - gap: 3px; - padding: 5px 6px; - font-size: 0.66rem; - } - - .team-score-count { - font-size: 0.74rem; - } - - .team-score.is-focused { - box-shadow: inset 0 0 0 2px rgb(255 244 209 / 0.92); - } - - .battle-notice { - top: calc(var(--score-band-height) + 8px); - right: 24px; - left: 24px; - width: auto; - padding-inline: 12px; - font-size: 0.76rem; - transform: translateY(-10px); - } - - #app.match-live .battle-notice.is-visible { - transform: translateY(0); - } - - .kill-log { - top: var(--mobile-kill-log-top); - bottom: auto; - left: 10px; - width: calc(100vw - 20px); - max-height: calc(100svh - var(--mobile-kill-log-top) - var(--mobile-visitor-space)); - padding: 8px; - } - - #app.match-live .victory-celebration { - padding: - var(--score-band-height) - 14px - min(30svh, 230px); - } - - .victory-banner { - width: min(calc(100vw - 48px), 520px); - min-height: 92px; - padding: 1rem 1.1rem; - font-size: clamp(1.35rem, 7vw, 2rem); - } - - .match-status { - bottom: 10px; - width: calc(100vw - 20px); - } - - .arena-meta { - right: 10px; - bottom: calc(10px + env(safe-area-inset-bottom)); - z-index: 10; - gap: 8px; - opacity: 1; - pointer-events: auto; - } - - .visitor-count { - font-size: 0.68rem; - } - - .about-button { - min-width: 68px; - min-height: 26px; - font-size: 0.68rem; - } - - .about-backdrop { - align-items: end; - padding: 12px; - } - - .about-dialog { - width: 100%; - max-height: calc(100svh - 24px); - } - - .about-header { - padding: 18px 18px 12px; - } - - .about-tabs { - padding: 0 18px 12px; - } - - .about-panel { - padding: 16px 18px 22px; - } - - .about-field-row { - grid-template-columns: 78px minmax(0, 1fr); - min-height: 48px; - gap: 10px; - } -} +@import "./styles/base.css"; +@import "./styles/intro.css"; +@import "./styles/game-ui.css"; +@import "./styles/overlay.css"; +@import "./styles/animations.css"; +@import "./styles/mobile.css"; diff --git a/src/styles/animations.css b/src/styles/animations.css new file mode 100644 index 0000000..d916d07 --- /dev/null +++ b/src/styles/animations.css @@ -0,0 +1,158 @@ +@keyframes intro-rise { + from { + opacity: 0; + transform: translateY(22px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes preview-attack { + to { + background-position-x: var(--sprite-end); + } +} + +@keyframes preview-breathe { + 0%, + 100% { + margin-top: 0; + } + 50% { + margin-top: -8px; + } +} + +@keyframes preview-strike { + 0%, + 58%, + 100% { + opacity: 0; + } + 64%, + 76% { + opacity: 0.86; + } +} + +@keyframes status-marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + +@keyframes kill-log-entry { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes banner-in { + from { + opacity: 0; + transform: translateY(18px) scale(0.78); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes victory-banner-sheen { + from { + opacity: 0; + transform: translateX(0) skewX(-18deg); + } + 18% { + opacity: 1; + } + to { + opacity: 0; + transform: translateX(560%) skewX(-18deg); + } +} + +@keyframes victory-confetti-burst { + 0% { + opacity: 0; + transform: translate(-50%, -50%) rotate(var(--confetti-tilt)) scale(0.3); + } + 12% { + opacity: 1; + } + 74% { + opacity: 1; + } + 100% { + opacity: 0; + transform: + translate(calc(-50% + var(--confetti-x)), calc(-50% + var(--confetti-y))) + rotate(calc(var(--confetti-tilt) + var(--confetti-spin))) + scale(1); + } +} + +@keyframes victory-glow { + from { + opacity: 0; + transform: scale(0.58); + } + 35% { + opacity: 1; + } + to { + opacity: 0.8; + transform: scale(1); + } +} + +@keyframes victory-rays-in { + from { + transform: scale(0.56); + } + to { + transform: scale(1); + } +} + +@keyframes victory-rays-turn { + to { + rotate: 360deg; + } +} + +@keyframes victory-message-pulse { + from { + opacity: 0.72; + transform: scale(0.88); + } + 58% { + transform: scale(1.05); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@media (prefers-reduced-motion: reduce) { + .victory-banner, + .victory-banner::before, + .victory-banner-message, + .victory-celebration::before, + .victory-confetti-piece, + .victory-rays { + animation-duration: 1ms; + animation-iteration-count: 1; + } +} diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..976faab --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,129 @@ +:root { + color-scheme: dark; + font-family: + Inter, Pretendard, "Noto Sans KR", system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", sans-serif; + background: #080a07; + color: #fff5db; +} + +* { + box-sizing: border-box; +} + +html { + min-width: 320px; + min-height: 100%; + background: #080a07; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + overflow: hidden; +} + +button, +input, +textarea { + font: inherit; +} + +button { + border: 0; + cursor: pointer; +} + +button:focus-visible, +input:focus-visible, +textarea:focus-visible { + outline: 3px solid rgb(238 185 73 / 0.46); + outline-offset: 3px; +} + +#app { + --arena-gap: 18px; + --score-band-height: 134px; + --score-panel-left: 14px; + --score-panel-width: 260px; + --score-rail-width: calc(var(--score-panel-left) + var(--score-panel-width)); + --drawer-width: min(430px, 100vw); + --drawer-live-width: min(340px, calc(100vw - 48px)); + position: relative; + min-height: 100vh; + overflow: hidden; + background: + linear-gradient(180deg, rgb(8 10 7 / 0.18), rgb(3 5 4 / 0.84)), + #080a07; +} + +#app.match-live { + --drawer-width: var(--drawer-live-width); +} + +.arena-shell { + position: fixed; + inset: 0; + display: grid; + place-items: center; + overflow: hidden; + background: #090b08; +} + +.arena-shell::before { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + background: + radial-gradient(circle at 50% 50%, rgb(255 211 122 / 0.06), transparent 42%), + linear-gradient(90deg, rgb(3 5 4 / 0.48), rgb(3 5 4 / 0.08) 45%, rgb(3 5 4 / 0.48)), + linear-gradient(180deg, rgb(3 5 4 / 0.08), rgb(3 5 4 / 0.5)); + pointer-events: none; + transition: + background 520ms ease, + opacity 520ms ease; +} + +#app.match-live .arena-shell::before { + opacity: 0.24; +} + +#app.match-live .arena-shell { + place-items: center; +} + +#game { + position: relative; + z-index: 0; + width: max(100vw, 100vh); + height: max(100vw, 100vh); + overflow: hidden; + opacity: 0.68; + filter: saturate(1) contrast(1.08) brightness(1.08); + transform: scale(1.04); + transform-origin: center; + transition: + width 620ms cubic-bezier(0.2, 0.8, 0.2, 1), + height 620ms cubic-bezier(0.2, 0.8, 0.2, 1), + opacity 520ms ease, + filter 520ms ease, + transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +#app.match-live #game { + width: min(100vw, 100vh); + height: min(100vw, 100vh); + margin-left: 0; + opacity: 1; + filter: none; + transform: scale(1); +} + +#game canvas { + display: block; + width: 100% !important; + height: 100% !important; + image-rendering: pixelated; +} diff --git a/src/styles/game-ui.css b/src/styles/game-ui.css new file mode 100644 index 0000000..e489c49 --- /dev/null +++ b/src/styles/game-ui.css @@ -0,0 +1,601 @@ +.scoreboard { + position: fixed; + top: clamp(14px, 3vw, 28px); + left: var(--score-panel-left); + z-index: 3; + display: flex; + justify-content: flex-start; + width: var(--score-panel-width); + max-height: calc(100vh - 420px); + min-height: 64px; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: rgb(238 185 73 / 0.3) transparent; + padding: 8px; + border: 1px solid rgb(238 185 73 / 0.18); + border-radius: 8px; + background: rgb(4 6 4 / 0.5); + opacity: 0; + pointer-events: none; + transform: translateY(-18px); + transition: + opacity 420ms ease, + transform 420ms ease; + backdrop-filter: blur(10px); +} + +.scoreboard::-webkit-scrollbar { + width: 4px; +} + +.scoreboard::-webkit-scrollbar-track { + background: transparent; +} + +.scoreboard::-webkit-scrollbar-thumb { + border-radius: 10px; + background: rgb(238 185 73 / 0.3); +} + +#app.match-live .scoreboard { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.score-side { + display: grid; + grid-template-columns: repeat(2, 114px); + gap: 6px; + width: 100%; +} + +.score-side.right { + display: none; +} + +.team-score { + display: grid; + grid-template-rows: 1fr 1px auto; + gap: 6px; + width: 114px; + min-height: 72px; + overflow: hidden; + border-radius: 6px; + padding: 8px 9px; + color: #fff; + font-size: 0.8rem; + font-weight: 900; + text-align: left; + text-shadow: 1px 1px 2px #000; + transition: + filter 160ms ease, + transform 160ms ease; +} + +.team-score:hover { + filter: brightness(1.16); + transform: translateY(-1px); +} + +.team-score.is-focused { + box-shadow: + inset 0 0 0 2px rgb(255 244 209 / 0.92), + 0 0 18px rgb(227 178 79 / 0.26); +} + +.team-score:disabled { + cursor: default; + filter: grayscale(0.6) brightness(0.68); +} + +.team-score:disabled:hover { + transform: none; +} + +.team-score-name { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + line-height: 1.15; +} + +.team-score-rule { + width: 100%; + background: var(--team-color); + opacity: 0.9; +} + +.team-score-count { + justify-self: end; + color: #fff2c8; + font-size: 0.86rem; +} + +.battle-notice { + position: fixed; + top: clamp(12px, 2vw, 20px); + left: 50%; + z-index: 5; + display: flex; + align-items: center; + justify-content: center; + width: min(420px, 72vmin, calc(100vw - 64px)); + min-height: 38px; + border: 1px solid rgb(238 185 73 / 0.26); + border-radius: 8px; + padding: 8px 14px; + background: rgb(8 10 7 / 0.68); + color: #ffe8b4; + font-size: 0.8rem; + font-weight: 900; + line-height: 1.35; + text-align: center; + text-shadow: 1px 1px 2px #000; + opacity: 0; + pointer-events: none; + transform: translate(-50%, -10px); + transition: + opacity 260ms ease, + transform 260ms ease; + backdrop-filter: blur(10px); +} + +#app.match-live .battle-notice.is-visible { + opacity: 1; + transform: translate(-50%, 0); +} + +@media (min-width: 961px) { + #app.match-live .battle-notice { + right: auto; + left: 50%; + width: min(420px, 72vmin, calc(100vw - var(--drawer-width) - var(--score-rail-width) - 56px)); + transform: translate(-50%, -10px); + } + + #app.match-live .battle-notice.is-visible { + transform: translate(-50%, 0); + } + + #app.match-live.drawer-collapsed .battle-notice { + right: auto; + width: min(420px, 72vmin, calc(100vw - 64px)); + } +} + +.kill-log { + position: fixed; + bottom: clamp(14px, 3vw, 26px); + left: var(--score-panel-left); + z-index: 4; + width: min(370px, calc(100vw - 32px)); + max-height: min(34vh, 292px); + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: rgb(238 185 73 / 0.3) transparent; + border: 1px solid rgb(238 185 73 / 0.2); + border-radius: 8px; + padding: 10px; + background: rgb(4 6 4 / 0.58); + opacity: 0; + pointer-events: none; + transform: translateY(16px); + transition: + opacity 260ms ease, + transform 260ms ease; + backdrop-filter: blur(10px); +} + +.kill-log::-webkit-scrollbar { + width: 4px; +} + +.kill-log::-webkit-scrollbar-track { + background: transparent; +} + +.kill-log::-webkit-scrollbar-thumb { + border-radius: 10px; + background: rgb(238 185 73 / 0.3); +} + +#app.match-live .kill-log.has-entries { + opacity: 1; + pointer-events: auto; + transform: translateY(0); +} + +.kill-log-list { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 0; + margin: 0; + padding: 0; + list-style: none; +} + +.kill-log-item { + display: grid; + grid-template-columns: minmax(0, 1fr) 54px minmax(0, 1fr); + align-items: center; + gap: 8px; + min-height: 54px; + border: 1px solid rgb(255 244 209 / 0.12); + border-radius: 6px; + padding: 7px 9px; + background: rgb(8 10 7 / 0.74); + box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06); + animation: kill-log-entry 180ms ease both; +} + +.kill-log-fighter { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.kill-log-fighter.killer { + border-left: 3px solid var(--killer-color); + padding-left: 6px; +} + +.kill-log-fighter.victim { + flex-direction: row-reverse; + border-right: 3px solid var(--victim-color); + padding-right: 6px; + justify-content: end; + text-align: right; +} + +.kill-log-avatar { + position: relative; + flex: 0 0 auto; + width: 36px; + height: 36px; + border: 1px solid rgb(255 244 209 / 0.16); + border-radius: 6px; + background-color: rgb(255 246 216 / 0.08); + background-position: -24px -16px; + background-repeat: no-repeat; + background-size: auto 86px; + image-rendering: pixelated; + box-shadow: inset 0 -10px 18px rgb(0 0 0 / 0.22); +} + +.kill-log-fighter.victim .kill-log-avatar::before, +.kill-log-fighter.victim .kill-log-avatar::after { + content: ""; + position: absolute; + top: 7px; + right: 1px; + width: 14px; + height: 2px; + border: 1px solid rgb(255 216 212 / 0.22); + border-radius: 999px; + background: #f24a42; + box-shadow: + 0 0 0 1px rgb(48 4 3 / 0.7), + 0 0 5px rgb(227 54 46 / 0.6); + transform-origin: center; +} + +.kill-log-fighter.victim .kill-log-avatar::before { + transform: rotate(45deg); +} + +.kill-log-fighter.victim .kill-log-avatar::after { + transform: rotate(-45deg); +} + +.kill-log-copy { + display: grid; + gap: 2px; + min-width: 0; +} + +.kill-log-team, +.kill-log-member { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kill-log-team { + min-width: 0; + color: #fff7df; + font-size: 0.78rem; + font-weight: 900; + text-shadow: 1px 1px 2px #000; +} + +.kill-log-member { + flex: 0 0 auto; + color: #ead8ad; + font-size: 0.72rem; + font-weight: 800; +} + +.kill-log-action { + display: grid; + justify-items: center; + gap: 2px; + min-width: 0; +} + +.kill-log-action-text { + color: #ffdc93; + font-size: 0.68rem; + font-weight: 950; + line-height: 1; +} + +.kill-log-weapon { + position: relative; + display: block; + width: 28px; + height: 28px; + place-self: center; + border: 1px solid rgb(238 185 73 / 0.28); + border-radius: 999px; + background: rgb(255 246 216 / 0.08); + box-shadow: 0 0 16px rgb(227 89 59 / 0.16); +} + +.kill-log-weapon::before, +.kill-log-weapon::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 18px; + height: 3px; + border-radius: 999px; + background: linear-gradient(90deg, #ffe8b4 0 70%, #b93c2f 70% 100%); + box-shadow: 0 0 8px rgb(255 226 166 / 0.3); + transform-origin: center; +} + +.kill-log-weapon::before { + transform: translate(-50%, -50%) rotate(42deg); +} + +.kill-log-weapon::after { + transform: translate(-50%, -50%) rotate(-42deg); +} + +.victory-celebration { + position: fixed; + z-index: 9; + display: grid; + overflow: hidden; + place-items: center; + inset: 0; + background: rgb(4 6 4 / 0.2); + isolation: isolate; + opacity: 1; + pointer-events: auto; + transform: scale(1); + transition: + opacity 220ms ease, + transform 220ms ease; +} + +.victory-celebration.is-leaving { + opacity: 0; + transform: scale(0.98); +} + +.victory-celebration::before { + content: ""; + position: absolute; + z-index: -1; + width: min(122vmin, 1240px); + aspect-ratio: 1; + border-radius: 50%; + background: + radial-gradient(circle, rgb(255 233 166 / 0.18) 0 18%, rgb(227 178 79 / 0.12) 31%, transparent 66%); + animation: victory-glow 1.8s ease-out both; +} + +.victory-celebration.is-draw::before { + background: + radial-gradient(circle, rgb(255 247 223 / 0.16) 0 18%, rgb(227 178 79 / 0.1) 31%, transparent 62%); +} + +.victory-rays { + position: absolute; + z-index: 0; + width: min(112vmin, 1120px); + aspect-ratio: 1; + border-radius: 50%; + background: repeating-conic-gradient( + from -4deg, + rgb(255 233 166 / 0.18) 0 8deg, + transparent 8deg 18deg + ); + opacity: 0.54; + mask-image: radial-gradient(circle, #000 0 18%, transparent 66%); + animation: victory-rays-in 1.1s ease-out both, victory-rays-turn 11s linear infinite; +} + +.victory-celebration.is-draw .victory-rays { + opacity: 0.22; +} + +.victory-confetti { + position: absolute; + z-index: 1; + inset: 0; +} + +.victory-confetti-piece { + position: absolute; + top: 50%; + left: 50%; + display: block; + width: clamp(6px, 0.8vw, 11px); + height: clamp(10px, 1.2vw, 18px); + border-radius: 8px; + background: var(--confetti-color); + box-shadow: 0 0 12px rgb(255 230 166 / 0.22); + opacity: 0; + transform: translate(-50%, -50%) rotate(var(--confetti-tilt)) scale(0.3); + animation: victory-confetti-burst var(--confetti-duration) cubic-bezier(0.15, 0.84, 0.35, 1) var(--confetti-delay) both; +} + +.victory-confetti-piece:nth-child(3n) { + width: clamp(10px, 1vw, 15px); + height: clamp(6px, 0.72vw, 10px); + border-radius: 2px; +} + +.victory-banner { + position: relative; + z-index: 2; + display: grid; + width: min(calc(100vw - 36px), 760px); + min-height: clamp(108px, 18vw, 170px); + overflow: hidden; + place-items: center; + border: 2px solid #f1c45d; + border-radius: 8px; + padding: clamp(1.25rem, 3.8vw, 2rem) clamp(1.3rem, 5.4vw, 3.4rem); + background: + linear-gradient(135deg, rgb(18 21 13 / 0.98), rgb(3 5 4 / 0.92)), + rgb(4 6 4 / 0.9); + color: #fff7df; + font-size: clamp(1.65rem, 5vw, 3rem); + font-weight: 950; + letter-spacing: 0; + line-height: 1.12; + text-align: center; + text-wrap: balance; + text-shadow: + 0 2px 0 rgb(55 36 8 / 0.56), + 0 0 24px rgb(255 226 153 / 0.28); + box-shadow: + 0 0 0 1px rgb(255 237 187 / 0.2) inset, + 0 0 42px rgb(227 178 79 / 0.44), + 0 24px 90px rgb(0 0 0 / 0.58); + animation: banner-in 0.64s cubic-bezier(0.16, 0.9, 0.25, 1.2); + backdrop-filter: blur(6px); +} + +.victory-banner::before { + content: ""; + position: absolute; + inset: -40% auto -40% -36%; + width: 28%; + background: linear-gradient(90deg, transparent, rgb(255 248 223 / 0.6), transparent); + transform: skewX(-18deg); + animation: victory-banner-sheen 1s 0.28s ease-out both; +} + +.victory-banner::after { + content: ""; + position: absolute; + inset: 10px; + border: 1px solid rgb(255 225 151 / 0.24); + border-radius: 5px; +} + +.victory-banner-message { + position: relative; + z-index: 1; + display: block; + max-width: 100%; + overflow-wrap: anywhere; + animation: victory-message-pulse 720ms 80ms ease-out both; +} + +.victory-celebration.is-draw .victory-banner { + border-color: #d8c28d; + box-shadow: + 0 0 0 1px rgb(255 237 187 / 0.14) inset, + 0 0 28px rgb(227 178 79 / 0.24), + 0 24px 90px rgb(0 0 0 / 0.52); +} + +#app.match-paused .arena-shell::after { + content: "일시정지"; + position: fixed; + top: 50%; + left: 50%; + z-index: 6; + border: 1px solid rgb(238 185 73 / 0.34); + border-radius: 8px; + padding: 14px 26px; + background: rgb(5 7 5 / 0.76); + color: #ffe8b4; + font-size: clamp(1.3rem, 4vw, 2rem); + font-weight: 950; + transform: translate(-50%, -50%); + box-shadow: 0 18px 60px rgb(0 0 0 / 0.46); + backdrop-filter: blur(8px); +} + +.match-status { + position: fixed; + bottom: clamp(14px, 3vw, 26px); + left: 50%; + z-index: 4; + width: min(980px, calc(100vw - 32px)); + min-height: 48px; + overflow: hidden; + border: 1px solid rgb(238 185 73 / 0.28); + border-radius: 8px; + padding: 13px 0; + background: rgb(8 10 7 / 0.74); + color: #ffe2a6; + font-weight: 900; + opacity: 0; + pointer-events: none; + transform: translate(-50%, calc(100% + 28px)); + transition: + opacity 420ms ease, + transform 420ms ease; + backdrop-filter: blur(10px); +} + +#app.status-active:not(.match-live) .match-status { + opacity: 1; + transform: translate(-50%, 0); +} + +#app.match-live .match-status { + display: none; +} + +@media (min-width: 961px) { + #app.match-live .match-status { + left: calc((100vw - var(--drawer-width)) / 2); + width: min(760px, calc(100vw - var(--drawer-width) - 32px)); + } + + #app.match-live.drawer-collapsed .match-status { + left: 50%; + width: min(980px, calc(100vw - 32px)); + } +} + +.status-track { + display: flex; + width: max-content; + min-width: 200%; + gap: 64px; + animation: status-marquee 22s linear infinite; +} + +.status-track span { + flex: 0 0 auto; + min-width: calc(50vw - 32px); + padding-left: 28px; + white-space: nowrap; +} diff --git a/src/styles/intro.css b/src/styles/intro.css new file mode 100644 index 0000000..91716bb --- /dev/null +++ b/src/styles/intro.css @@ -0,0 +1,225 @@ +.battle-preview { + position: fixed; + inset: 0; + z-index: 2; + overflow: hidden; + opacity: 0.84; + pointer-events: none; + transition: + opacity 420ms ease, + transform 700ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +#app.match-live .battle-preview { + opacity: 0; + transform: scale(1.08); +} + +.preview-fighter { + position: absolute; + width: 100px; + height: 100px; + background-repeat: no-repeat; + background-size: auto 100px; + image-rendering: pixelated; + transform-origin: center; + filter: + drop-shadow(0 18px 22px rgb(0 0 0 / 0.68)) + saturate(1.14) + brightness(1.12); + animation: + preview-attack var(--sprite-speed) steps(var(--sprite-steps)) infinite, + preview-breathe 1800ms ease-in-out infinite; +} + +.preview-knight { + --sprite-end: -600px; + --sprite-scale: 5.2; + --sprite-speed: 840ms; + --sprite-steps: 6; + left: 10vw; + top: 48vh; + background-image: url("/assets/characters/knight/Knight-Attack01.png"); + transform: scale(var(--sprite-scale)); +} + +.preview-orc { + --sprite-end: -500px; + --sprite-scale: 5.35; + --sprite-speed: 760ms; + --sprite-steps: 5; + right: 9vw; + top: 46vh; + background-image: url("/assets/characters/orc/Orc-Attack01.png"); + transform: scaleX(-1) scale(var(--sprite-scale)); +} + +.preview-wizard { + --sprite-end: -500px; + --sprite-scale: 4.35; + --sprite-speed: 980ms; + --sprite-steps: 5; + left: 56vw; + top: 24vh; + background-image: url("/assets/characters/wizard/Wizard-Attack01.png"); + opacity: 0.58; + transform: scaleX(-1) scale(var(--sprite-scale)); +} + +.preview-strike { + position: absolute; + width: 160px; + height: 5px; + border-radius: 999px; + background: linear-gradient(90deg, transparent, rgb(255 229 156 / 0.86), transparent); + box-shadow: 0 0 24px rgb(227 89 59 / 0.5); + opacity: 0; + transform-origin: center; + animation: preview-strike 980ms ease-in-out infinite; +} + +.preview-strike-a { + left: 38vw; + top: 54vh; + transform: rotate(-18deg); +} + +.preview-strike-b { + right: 31vw; + top: 42vh; + transform: rotate(22deg); + animation-delay: 260ms; +} + +.intro-stage { + position: fixed; + inset: 0; + z-index: 5; + display: grid; + place-items: center; + padding: clamp(24px, 5vw, 56px); + pointer-events: none; + transition: + opacity 420ms ease, + transform 620ms cubic-bezier(0.2, 0.8, 0.2, 1); +} + +#app.match-live .intro-stage { + opacity: 0; + transform: scale(0.96); +} + +#app.match-live .intro-content { + pointer-events: none; +} + +.intro-content { + display: grid; + justify-items: center; + gap: 22px; + text-align: center; + pointer-events: auto; + animation: intro-rise 760ms cubic-bezier(0.2, 0.8, 0.2, 1) both; + transition: + transform 560ms cubic-bezier(0.2, 0.8, 0.2, 1), + opacity 360ms ease; +} + +#app.options-open:not(.match-live) .intro-content { + opacity: 0.72; +} + +.arena-logo { + margin: 0; + color: #fff4d1; + font-size: clamp(4rem, 16vw, 11rem); + font-weight: 950; + letter-spacing: 0; + line-height: 0.9; + text-shadow: + 0 2px 0 #ad4d37, + 0 14px 42px rgb(0 0 0 / 0.72), + 0 0 40px rgb(230 173 71 / 0.28); + text-transform: uppercase; +} + +.arena-logo .small-text { + font-size: 0.7em; + margin-top: 0.05em; +} + +.arena-logo span { + display: block; +} + +.arena-meta { + position: fixed; + right: clamp(10px, 2vw, 18px); + bottom: clamp(10px, 2vw, 18px); + z-index: 10; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + pointer-events: none; + transition: opacity 220ms ease; +} + +.visitor-count, +.about-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 28px; + margin: 0; + border: 1px solid rgb(238 185 73 / 0.22); + border-radius: 999px; + padding: 5px 12px; + background: rgb(8 10 7 / 0.68); + color: #e7c879; + font-size: 0.72rem; + font-weight: 800; + line-height: 1; + text-decoration: none; + backdrop-filter: blur(10px); + pointer-events: auto; + transition: + background 180ms ease, + border-color 180ms ease, + transform 180ms ease, + opacity 220ms ease; +} + +.visitor-count { + opacity: 0; + transform: translateY(8px); +} + +#app.match-live .visitor-count { + opacity: 0.86; + transform: translateY(0); +} + +.about-button { + min-width: 72px; + color: #ffe8b4; + font-weight: 900; + box-shadow: 0 4px 12px rgb(0 0 0 / 0.22); +} + +.about-button:hover { + border-color: rgb(238 185 73 / 0.42); + background: rgb(255 246 216 / 0.14); + transform: translateY(-1px); +} + +.start-button { + min-width: 180px; + padding: 0 30px; + text-transform: uppercase; +} + +#app.options-open:not(.match-live) .start-button { + pointer-events: none; + visibility: hidden; +} diff --git a/src/styles/mobile.css b/src/styles/mobile.css new file mode 100644 index 0000000..5adf976 --- /dev/null +++ b/src/styles/mobile.css @@ -0,0 +1,308 @@ +@media (max-width: 960px) { + body { + overflow: hidden; + } + + #app { + --arena-gap: 0px; + --mobile-game-size: min(100vw, calc(100svh - var(--score-band-height))); + --mobile-kill-log-top: calc(var(--score-band-height) + var(--mobile-game-size) + 10px); + --mobile-options-button-width: 54px; + --mobile-options-gap: 8px; + --mobile-team-card-width: clamp(56px, calc((100vw - 120px) / 4), 72px); + --mobile-visitor-space: calc(104px + env(safe-area-inset-bottom)); + --score-band-height: 132px; + --score-panel-left: 10px; + --score-panel-width: calc(100vw - 20px); + --score-rail-width: 0px; + } + + #app.match-live .arena-shell { + place-items: start center; + } + + #app.match-live #game { + width: var(--mobile-game-size); + height: var(--mobile-game-size); + margin-top: var(--score-band-height); + margin-left: 0; + } + + .intro-stage { + padding: 20px; + } + + .arena-logo { + font-size: clamp(3.8rem, 22vw, 7rem); + } + + .fighter-entry { + width: 100vw; + padding: 22px; + } + + #app.match-live .fighter-entry { + top: calc(10px + env(safe-area-inset-top)); + right: 10px; + left: 10px; + width: auto; + max-height: calc(100svh - 20px - env(safe-area-inset-top) - env(safe-area-inset-bottom)); + gap: 10px; + padding: 12px; + } + + #app.match-live.drawer-collapsed .fighter-entry { + top: calc(22px + env(safe-area-inset-top)); + right: 10px; + left: auto; + width: auto; + } + + #app.match-live .fighter-entry h2 { + font-size: clamp(1.45rem, 7vw, 1.8rem); + } + + #app.match-live .fighter-entry textarea { + height: 112px; + min-height: 112px; + resize: none; + } + + #app.match-live .fighter-entry fieldset { + gap: 7px; + padding: 9px; + } + + #app.match-live .fighter-entry form { + gap: 10px; + } + + #app.match-live .entry-copy { + gap: 4px; + } + + #app.match-live .eyebrow { + font-size: 0.68rem; + } + + #app.match-live label, + #app.match-live .spawn-placement-label { + font-size: 0.82rem; + } + + #app.match-live input:not([type="range"]):not([type="radio"]), + #app.match-live textarea { + min-height: 40px; + padding-inline: 10px; + } + + #app.match-live textarea { + padding-block: 9px; + } + + #app.match-live .team-size-number { + width: 64px; + min-width: 64px; + } + + #app.match-live .spawn-placement-option span { + min-height: 36px; + padding: 6px; + font-size: 0.76rem; + } + + #app.match-live .match-actions { + gap: 8px; + } + + #app.match-live .match-actions button { + min-height: 44px; + } + + #app.match-live .drawer-toggle { + min-width: 116px; + } + + #app.match-live.drawer-collapsed .drawer-toggle { + width: var(--mobile-options-button-width); + min-width: var(--mobile-options-button-width); + padding-inline: 6px; + font-size: 0; + } + + #app.match-live.drawer-collapsed .drawer-toggle::before { + content: "옵션"; + font-size: 0.78rem; + line-height: 1; + } + + .battle-preview { + opacity: 0.62; + } + + .preview-knight { + left: -8vw; + top: 52vh; + } + + .preview-orc { + right: -9vw; + top: 49vh; + } + + .preview-wizard { + left: 48vw; + top: 21vh; + } + + .scoreboard { + align-items: flex-start; + top: 10px; + left: var(--score-panel-left); + width: var(--score-panel-width); + max-height: calc(var(--score-band-height) - 12px); + overflow-x: auto; + overflow-y: hidden; + padding: 9px min(148px, 38vw) 9px 9px; + scrollbar-color: rgb(238 185 73 / 0.38) transparent; + scrollbar-width: thin; + touch-action: pan-x; + } + + #app.match-live.drawer-collapsed .scoreboard { + width: calc( + 100vw - 20px - var(--mobile-options-button-width) - var(--mobile-options-gap) + ); + padding-right: 9px; + } + + .score-side { + display: grid; + grid-template-columns: none; + grid-auto-columns: var(--mobile-team-card-width); + grid-auto-flow: column; + grid-template-rows: repeat(2, 48px); + gap: 5px 5px; + } + + .scoreboard::-webkit-scrollbar { + height: 4px; + } + + .scoreboard::-webkit-scrollbar-track { + background: transparent; + } + + .scoreboard::-webkit-scrollbar-thumb { + border-radius: 999px; + background: rgb(238 185 73 / 0.38); + } + + .team-score { + width: auto; + min-height: 48px; + height: 48px; + gap: 3px; + padding: 5px 6px; + font-size: 0.66rem; + grid-template-rows: 1fr 1px auto; + align-content: center; + } + + .team-score-count { + font-size: 0.74rem; + } + + .team-score.is-focused { + box-shadow: inset 0 0 0 2px rgb(255 244 209 / 0.92); + } + + .battle-notice { + top: calc(var(--score-band-height) + 8px); + right: 24px; + left: 24px; + width: auto; + padding-inline: 12px; + font-size: 0.76rem; + transform: translateY(-10px); + } + + #app.match-live .battle-notice.is-visible { + transform: translateY(0); + } + + .kill-log { + top: var(--mobile-kill-log-top); + bottom: auto; + left: 10px; + width: calc(100vw - 20px); + max-height: calc(100svh - var(--mobile-kill-log-top) - var(--mobile-visitor-space)); + padding: 8px; + } + + #app.match-live .victory-celebration { + padding: + var(--score-band-height) + 14px + min(30svh, 230px); + } + + .victory-banner { + width: min(calc(100vw - 48px), 520px); + min-height: 92px; + padding: 1rem 1.1rem; + font-size: clamp(1.35rem, 7vw, 2rem); + } + + .match-status { + bottom: 10px; + width: calc(100vw - 20px); + } + + .arena-meta { + right: 10px; + bottom: calc(10px + env(safe-area-inset-bottom)); + z-index: 10; + gap: 8px; + opacity: 1; + pointer-events: auto; + } + + .visitor-count { + font-size: 0.68rem; + } + + .about-button { + min-width: 68px; + min-height: 26px; + font-size: 0.68rem; + } + + .about-backdrop { + align-items: end; + padding: 12px; + } + + .about-dialog { + width: 100%; + max-height: calc(100svh - 24px); + } + + .about-header { + padding: 18px 18px 12px; + } + + .about-tabs { + padding: 0 18px 12px; + } + + .about-panel { + padding: 16px 18px 22px; + } + + .about-field-row { + grid-template-columns: 78px minmax(0, 1fr); + min-height: 48px; + gap: 10px; + } +} diff --git a/src/styles/overlay.css b/src/styles/overlay.css new file mode 100644 index 0000000..bdbd4b3 --- /dev/null +++ b/src/styles/overlay.css @@ -0,0 +1,566 @@ +.start-button, +form button[type="submit"], +.pause-button, +.restart-button { + min-height: 52px; + border-radius: 8px; + background: linear-gradient(180deg, #e56443, #b93c2f); + color: #fff7df; + font-weight: 900; + box-shadow: + 0 18px 44px rgb(0 0 0 / 0.36), + inset 0 1px 0 rgb(255 255 255 / 0.2); + transition: + background 180ms ease, + transform 180ms ease, + box-shadow 180ms ease; +} + +.start-button:hover, +form button[type="submit"]:hover, +.pause-button:hover, +.restart-button:hover { + background: linear-gradient(180deg, #f0754f, #c84636); + transform: translateY(-1px); + box-shadow: + 0 22px 52px rgb(0 0 0 / 0.42), + inset 0 1px 0 rgb(255 255 255 / 0.24); +} + +.pause-button, +.restart-button { + display: none; + border: 1px solid rgb(238 185 73 / 0.3); + background: rgb(255 246 216 / 0.08); + color: #ffe8b4; +} + +.pause-button:hover, +.restart-button:hover { + background: rgb(255 246 216 / 0.14); +} + +#app.match-live .pause-button, +#app.match-live .restart-button { + display: block; +} + +#app.match-live .match-actions { + grid-template-columns: 1fr 1fr; +} + +#app.match-live .match-actions button[type="submit"] { + grid-column: 1 / -1; +} + +#app.match-paused .pause-button { + background: linear-gradient(180deg, #e3b24f, #9a6c24); + color: #120f08; +} + +#app.match-ended .pause-button { + display: none; +} + +.drawer-scrim { + position: fixed; + inset: 0; + z-index: 6; + background: rgb(4 5 4 / 0.42); + opacity: 0; + pointer-events: none; + transition: opacity 320ms ease; +} + +#app.options-open .drawer-scrim { + opacity: 1; + pointer-events: auto; +} + +#app.match-live .drawer-scrim { + opacity: 0; + pointer-events: none; +} + +.drawer-toggle { + display: none; + min-height: 40px; + border: 1px solid rgb(238 185 73 / 0.28); + border-radius: 8px; + padding: 0 12px; + background: rgb(12 15 11 / 0.84); + color: #ffe8b4; + font-size: 0.82rem; + font-weight: 900; + box-shadow: 0 16px 38px rgb(0 0 0 / 0.36); + transition: + background 180ms ease, + transform 180ms ease; + backdrop-filter: blur(10px); +} + +.drawer-toggle:hover { + background: rgb(255 246 216 / 0.14); + transform: translateY(-1px); +} + +#app.match-live .drawer-toggle { + display: block; +} + +.fighter-entry { + position: fixed; + top: 0; + right: 0; + z-index: 7; + display: grid; + align-content: start; + gap: 24px; + width: var(--drawer-width); + height: 100vh; + overflow-y: auto; + border-left: 1px solid rgb(239 199 103 / 0.22); + padding: clamp(22px, 4vw, 34px); + background: + linear-gradient(180deg, rgb(29 33 22 / 0.94), rgb(13 16 12 / 0.96)), + #11140f; + box-shadow: -28px 0 80px rgb(0 0 0 / 0.52); + transform: translateX(104%); + transition: + opacity 260ms ease, + transform 520ms cubic-bezier(0.2, 0.8, 0.2, 1); + backdrop-filter: blur(16px); +} + +#app.options-open .fighter-entry { + transform: translateX(0); +} + +#app.match-live .fighter-entry { + top: 24px; + right: 24px; + height: auto; + max-height: calc(100vh - 48px); + gap: 16px; + border: 1px solid rgb(239 199 103 / 0.22); + border-radius: 8px; + padding: 20px; + transform: translateX(0); +} + +#app.match-live.drawer-collapsed .fighter-entry { + width: auto; + min-width: 0; + overflow: visible; + border-color: transparent; + padding: 0; + background: transparent; + box-shadow: none; + opacity: 1; + pointer-events: auto; + transform: translateX(0); +} + +#app.match-live .drawer-close { + display: none; +} + +#app.match-live .fighter-entry h2 { + font-size: 2rem; +} + +#app.match-live .fighter-entry textarea { + min-height: 190px; +} + +#app.match-live .fighter-entry fieldset { + padding: 12px; +} + +#app.match-live.drawer-collapsed .entry-copy, +#app.match-live.drawer-collapsed .fighter-entry form { + display: none; +} + +#app.match-live.drawer-collapsed .drawer-header { + justify-content: end; +} + +.drawer-header { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; +} + +.drawer-header-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.entry-copy { + display: grid; + gap: 8px; +} + +.eyebrow { + margin: 0; + color: #e3b24f; + font-size: 0.78rem; + font-weight: 900; + letter-spacing: 0; + text-transform: uppercase; +} + +h2 { + margin: 0; + color: #fff3d2; + font-size: clamp(1.7rem, 4vw, 2.5rem); + line-height: 1.05; + letter-spacing: 0; +} + +.drawer-close { + display: grid; + place-items: center; + width: 40px; + height: 40px; + flex: 0 0 auto; + border: 1px solid rgb(238 185 73 / 0.22); + border-radius: 8px; + background: rgb(255 246 216 / 0.08); + color: #f8deb0; + font-weight: 900; +} + +.drawer-close:hover { + background: rgb(255 246 216 / 0.14); +} + +.about-backdrop { + position: fixed; + inset: 0; + z-index: 20; + display: grid; + place-items: center; + padding: clamp(16px, 4vw, 34px); + background: rgb(3 5 4 / 0.66); + backdrop-filter: blur(8px); +} + +.about-backdrop[hidden] { + display: none; +} + +.about-dialog { + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + width: min(560px, calc(100vw - 32px)); + max-height: min(760px, calc(100svh - 32px)); + overflow: hidden; + border: 1px solid rgb(239 199 103 / 0.28); + border-radius: 8px; + background: + linear-gradient(180deg, rgb(29 33 22 / 0.98), rgb(10 13 9 / 0.98)), + #11140f; + box-shadow: + 0 24px 100px rgb(0 0 0 / 0.62), + inset 0 1px 0 rgb(255 255 255 / 0.06); + outline: none; +} + +.about-header { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; + padding: clamp(20px, 4vw, 28px) clamp(20px, 4vw, 30px) 14px; +} + +.about-close { + display: grid; + place-items: center; + width: 40px; + height: 40px; + flex: 0 0 auto; + border: 1px solid rgb(238 185 73 / 0.22); + border-radius: 8px; + background: rgb(255 246 216 / 0.08); + color: #f8deb0; + font-weight: 900; +} + +.about-close:hover { + background: rgb(255 246 216 / 0.14); +} + +.about-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px; + padding: 0 clamp(20px, 4vw, 30px) 14px; + border-bottom: 1px solid rgb(238 185 73 / 0.16); +} + +.about-tab { + min-width: 0; + min-height: 42px; + border: 1px solid transparent; + border-radius: 6px; + padding: 8px 10px; + background: rgb(255 246 216 / 0.06); + color: #ead8ad; + font-size: 0.86rem; + font-weight: 900; + line-height: 1.2; +} + +.about-tab[aria-selected="true"] { + border-color: rgb(238 185 73 / 0.36); + background: #323822; + color: #fff7df; +} + +.about-panel { + min-height: 0; + overflow: auto; + padding: clamp(18px, 4vw, 26px) clamp(20px, 4vw, 30px) clamp(22px, 5vw, 34px); +} + +.about-fields { + display: grid; + gap: 0; + margin: 0; +} + +.about-field-row { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); + align-items: center; + gap: 14px; + min-height: 50px; + border-bottom: 1px solid rgb(238 185 73 / 0.14); +} + +.about-field-row:first-child { + border-top: 1px solid rgb(238 185 73 / 0.14); +} + +.about-field-row dt { + color: #e3b24f; + font-size: 0.78rem; + font-weight: 950; + text-transform: uppercase; +} + +.about-field-row dd { + min-width: 0; + margin: 0; + color: #fff7df; + font-weight: 800; + overflow-wrap: anywhere; +} + +.about-field-row a, +.about-markdown a { + color: #85dcc7; + text-decoration-color: rgb(133 220 199 / 0.42); + text-underline-offset: 3px; +} + +.about-markdown { + display: grid; + gap: 12px; + color: #ead8ad; + font-size: 0.92rem; + font-weight: 600; + line-height: 1.56; +} + +.about-markdown :is(h3, h4, h5, h6, p, ul, blockquote) { + margin: 0; +} + +.about-markdown h3, +.about-markdown h4, +.about-markdown h5, +.about-markdown h6 { + color: #fff3d2; + font-size: 1rem; + line-height: 1.3; +} + +.about-markdown blockquote { + border-left: 3px solid rgb(238 185 73 / 0.36); + padding: 4px 0 4px 16px; + color: #c4b693; + font-style: italic; +} + +.about-markdown hr { + margin: 8px 0; + border: 0; + border-top: 1px solid rgb(238 185 73 / 0.16); +} + +.about-markdown code { + border: 1px solid rgb(238 185 73 / 0.14); + border-radius: 4px; + padding: 2px 5px; + background: rgb(255 246 216 / 0.08); + color: #f1c761; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.88em; +} + +.about-markdown li { + line-height: 1.5; +} + +.about-markdown li strong { + color: #fff3d2; +} + +.about-markdown ul { + display: grid; + gap: 8px; + padding-left: 1.2rem; +} + +.about-empty { + color: #bfae83; +} + +form { + display: grid; + gap: 16px; +} + +.match-actions { + display: grid; + gap: 10px; +} + +fieldset { + display: grid; + gap: 10px; + min-width: 0; + margin: 0; + border: 1px solid rgb(238 185 73 / 0.22); + border-radius: 8px; + padding: 14px; + background: rgb(5 7 5 / 0.26); +} + +legend { + padding: 0 6px; + color: #e3b24f; + font-size: 0.78rem; + font-weight: 900; + letter-spacing: 0; + text-transform: uppercase; +} + +.team-size-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.team-size-number { + width: 88px; + min-width: 88px; + padding-inline: 10px; + text-align: center; + font-weight: 900; +} + +label { + color: #ead8ad; + font-size: 0.92rem; +} + +input:not([type="range"]):not([type="radio"]), +textarea { + min-height: 48px; + border: 1px solid rgb(238 185 73 / 0.28); + border-radius: 8px; + padding: 0 14px; + background: #232719; + color: #fff7df; + outline: none; +} + +textarea { + min-height: 258px; + resize: vertical; + padding-block: 12px; + line-height: 1.45; +} + +input[type="range"] { + width: 100%; + accent-color: #e3b24f; +} + +.spawn-placement-field { + display: grid; + gap: 8px; +} + +.spawn-placement-label { + color: #ead8ad; + font-size: 0.92rem; +} + +.spawn-placement-options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 4px; + min-width: 0; + border: 1px solid rgb(238 185 73 / 0.2); + border-radius: 8px; + padding: 4px; + background: #1d2116; +} + +.spawn-placement-option { + position: relative; + min-width: 0; +} + +.spawn-placement-option input { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +.spawn-placement-option span { + display: grid; + min-height: 44px; + place-items: center; + border: 1px solid transparent; + border-radius: 6px; + padding: 8px; + color: #ead8ad; + text-align: center; + font-size: 0.86rem; + font-weight: 900; + line-height: 1.25; + cursor: pointer; +} + +.spawn-placement-option input:checked + span { + border-color: rgb(238 185 73 / 0.36); + background: #323822; + color: #fff7df; +} + +.spawn-placement-option input:focus-visible + span { + outline: 2px solid #f1c761; + outline-offset: -2px; +} diff --git a/todo.md b/todo.md index c491662..ca258b7 100644 --- a/todo.md +++ b/todo.md @@ -109,12 +109,12 @@ 18. 치명타 적중 표기 추가 (완료) - **조치 사항**: - 공격 프로필의 치명타 판정을 실제 적중 처리까지 전달해 전투 타입별 적중 연출이 같은 흐름을 사용하도록 정리. - - 치명타 적중 시 대상 위에 `Critical!` 문구를 띄우고 즉시 처치와 카메라 흔들림이 함께 적용되도록 `applyHit()`를 보강. + - 치명타 적중 시 대상 위에 `Critical!` 문구를 띄우고 즉시 처치가 적용되도록 `applyHit()`를 보강. (카메라 흔들림은 이후 메테오 착탄 연출로 이전) 19. 리스폰 배치 설정 구분 추가 (완료) - **조치 사항**: - 전투 설정 drawer에 `스타팅 지점 배치`와 기존 `완전 랜덤 배치`를 선택하는 리스폰 설정을 추가. - - `스타팅 지점 배치`에서는 참가자 수에 맞춰 전장 구역을 나누고 참가자별 시작 구역 배정과 구역 안 스폰 위치를 매치마다 무작위로 정하도록 구현. + - `스타팅 지점 배치`에서는 참가자별 스타팅 영역과 영역 안 스폰 위치를 매치마다 무작위로 정하도록 구현했으며, 이후 30번 작업에서 영역 선택을 랜덤 중심 셀 기반 `5 x 5` 방식으로 구체화. - 선택한 리스폰 배치 모드를 `localStorage`에 저장해 새로고침과 재시작 이후에도 유지. 20. 팀당 인원 직접 입력 동기화 (완료) @@ -178,4 +178,86 @@ - 유저가 About 다이얼로그를 열 때마다 DB에서 최신 데이터를 가져오도록 서버 메모리 캐시 로직을 제거. - 기본 개인정보처리방침 마크다운의 공고/시행 일자를 최신화. +28. 전투 역할별 기본 스탯 프로필 분리 (완료) +- **조치 사항**: + - `src/constants.js`에 `FIGHTER_TYPE_STATS.melee/ranged/magic` 프로필을 추가해 최대 체력, 이동속도, 사거리, 쿨다운, 피해량, 치명타, 공격 발동 지연을 역할별로 조정할 수 있도록 변경. + - `src/game/fighter/fighterStats.js`를 추가해 투사체 캐릭터는 원거리, 즉발 주문 캐릭터는 마법, 나머지는 근접 프로필로 판별하고 개별 스킨 오버라이드를 병합. + - 캐릭터 생성과 전투 엔진이 해석된 프로필을 사용하도록 연결해 역할별 체력, 이동 및 공격 수치가 실제 전투에 적용되도록 변경. + +29. 킬로그 캐릭터 아이콘 가시성 개선 (완료) +- **조치 사항**: + - `100x100` idle 프레임에 포함된 투명 여백까지 축소되던 킬로그 아이콘 배경 표시 방식을 보정. + - 아이콘 박스 크기와 행 레이아웃은 유지하면서 캐릭터 실루엣이 있는 중앙 하단 영역을 확대 표시하도록 배경 크기와 위치를 조정. + +30. 팀별 스타팅 영역 앵커 및 전장 표시 추가 (완료) +- **조치 사항**: + - `스타팅 지점 배치`에서 전장 스폰 가능 그리드 중 팀별 중심 셀을 무작위로 선택하고, 중심 주변 2칸을 포함하는 `5 x 5` 영역을 팀별 스폰 구역으로 사용하도록 변경. + - 겹치지 않는 후보가 남아 있는 동안에는 선택된 스타팅 영역끼리 중첩되지 않는 랜덤 중심을 우선 사용해 전투 시작 즉시 팀이 섞이는 상황을 줄임. + - 팀별로 무작위 배정된 스타팅 영역 데이터를 실제 스폰 좌표와 공유해 표시 영역 밖에서 시작하지 않도록 구성. + - `arenaRenderer.js`에 팀 색상의 매우 옅은 채움 및 외곽선 오버레이를 추가하고, 랜덤 배치에서는 오버레이가 표시되지 않도록 연결. + +31. 사망 시점 팀 badge 클릭 입력 유실 수정 (완료) +- **조치 사항**: + - 사망 발생 때마다 `arenaScoreboard.js`가 팀 badge 버튼 전체를 재생성해 클릭 중인 DOM이 제거되던 문제를 수정. + - 팀 구성이 바뀌지 않는 전투 중 갱신에서는 기존 버튼 DOM을 유지하고 생존 인원, 선택 강조, 비활성 상태만 업데이트하도록 변경. + - 사망 처리와 팀 badge 클릭이 같은 시점에 겹쳐도 생존 캐릭터 관전 시점 선택이 정상 전달되도록 보강. + +32. 주기적 월드 이펙트 메테오 및 냉각지대 추가 (완료) +- **조치 사항**: + - `public/assets/effects/world_Effect.png`를 7프레임 공용 스프라이트시트로 로드하고, 실제 전투 시작 후 8초마다 무작위 생존자 위치에 메테오 또는 냉각지대를 무작위 발동하도록 `worldEffects.js`를 추가. + - 메테오는 낙하 경고 후 대상 위치 기준 `5 x 5` 영역에 환경 피해를 적용하고, 환경 사망이 처치 보상 없이 사망 통계와 승패 판정에 반영되도록 전투 피해 처리를 확장. + - 냉각지대는 냉기 착탄 연출과 지속 구역을 표시하며, 구역 안에 있는 캐릭터의 공격속도와 이동속도를 함께 감속하도록 연결. + - 발동 간격, 범위, 피해량, 냉각 지속시간과 감속 배율을 `src/constants.js`의 `WORLD_EFFECT_*` 상수로 분리하고, 새 경기/종료/일시정지 생명주기에 맞춰 정리되도록 구성. + +33. 월드 메테오 대각선 낙하 및 냉기 전용 시트 적용 (완료) +- **조치 사항**: + - 대상 위치가 전장 좌측 반면(2, 3사분면)이면 좌상단에서 우하단, 우측 반면(1, 4사분면)이면 우상단에서 좌하단으로 낙하하도록 궤적, 좌우 반전, `45`도 회전을 적용. + - `WORLD_EFFECT_VISUAL_SCALE`과 `WORLD_EFFECT_FALL_TRAVEL_TILES`를 추가해 피해 판정 `5 x 5`는 유지하면서 스프라이트를 전역 마법처럼 크게 보이도록 확장. + - 화염 메테오는 `public/assets/effects/world_Effect.png`, 냉기 메테오는 새 `public/assets/effects/world_Effect_2.png`를 각각 독립된 7프레임 애니메이션으로 로드하도록 변경. + +34. 냉기 메테오 착탄 피해 옵션 추가 (완료) +- **조치 사항**: + - `WORLD_EFFECT_FROST_DAMAGE`를 추가해 냉기 메테오 피해를 화염 메테오와 독립적으로 조절할 수 있도록 변경. + - 냉기 메테오 착탄 시 `5 x 5` 영역 피해를 먼저 처리하고, 전투가 종료되지 않은 경우 기존 냉각지대 감속 효과를 이어서 생성하도록 연결. + +35. 스타팅 영역 표시 시간 제한 추가 (완료) +- **조치 사항**: + - 팀별 스타팅 영역 오버레이가 `스타팅 지점 배치` 매치 시작 후 5초 동안만 표시되고 이후 자동으로 사라지도록 연결. + - 숨김 예약을 Phaser 씬 타이머로 관리하여 일시정지 시간은 표시 지속 시간에 포함되지 않고, 새 매치 시작 시 이전 숨김 타이머가 남지 않도록 정리. + +36. 최종 2팀 자동 관전 및 메테오 착탄 화면 흔들림 전환 (완료) +- **조치 사항**: + - 생존 캐릭터가 30명 미만이거나 최종 2팀만 남으면 후반 자동 줌과 교전 중심 포커싱이 시작되도록 관전 조건을 확장. + - 치명타의 `Critical!` 표기와 즉시 처치는 유지하면서 카메라 흔들림을 제거. + - 화염 메테오가 착탄할 때 화면 흔들림을 적용하고, 냉각지대 착탄과 피해 계산에서는 흔들림을 분리. + +37. 자동 관전 이전 메테오 임시 포커싱 추가 (완료) +- **조치 사항**: + - 후반 자동 관전 조건이 성립하기 전 화염 또는 냉기 메테오가 낙하하면 착탄 위치를 확대 추적하고 착탄 연출 종료 후 기존 카메라 위치와 줌을 복원. + - 캐릭터 수동 선택과 후반/최종 자동 관전은 메테오 임시 시점보다 우선하도록 카메라 상태를 정리. + - `src/constants.js`의 `CAMERA.METEOR_FOCUS_ENABLED` 플래그로 메테오 임시 포커싱을 코드에서 켜고 끌 수 있도록 구성. + +38. 냉기 메테오 동결 기절 및 실루엣 효과 추가 (완료) +- **조치 사항**: + - 냉기 메테오 착탄 피해에 생존한 전투원은 `2초` 동안 이동과 새 공격이 정지되는 `isFrostStunned` 상태가 되도록 연결. + - 동결 중 캐릭터 본체와 팀 실루엣 마커를 함께 얼음색으로 틴트하고, 시간이 끝나거나 매치가 정리되면 본체 원본 색상과 팀 색상으로 복원. + - `WORLD_EFFECT.FROST_STUN_DURATION`과 `WORLD_EFFECT.FROST_STUN_TINT`를 추가해 동결 지속시간과 표시 색상을 조절 가능하게 구성. + +39. 모바일 세로모드 팀 카드 가로폭 불균형 수정 (완료) +- **조치 사항**: + - 모바일 미디어 쿼리에서 `.score-side`가 데스크톱의 `grid-template-columns: repeat(2, 114px)`를 상속받아 1~4번 팀 카드만 길게 표시되던 현상을 수정. + - `grid-template-columns: none`을 추가하여 모든 팀 카드가 `grid-auto-columns`에 설정된 일정한 가로폭을 가지도록 보정. + +40. CSS 파일 기능별 모듈화 (완료) +- **조치 사항**: + - 거대했던 `src/styles.css`(약 2,000라인)를 기능별로 6개의 파일(`base`, `intro`, `game-ui`, `overlay`, `animations`, `mobile`)로 분리. + - `src/styles/` 폴더를 생성하여 모듈화된 CSS 파일들을 관리. + - `src/styles.css`는 이제 `@import`를 통해 각 모듈을 통합하는 엔트리 포인트 역할만 수행. + - 코드 가독성과 유지보수 편의성을 대폭 향상. + +41. 스타일 관련 컨텍스트 문서 추가 및 라우팅 업데이트 (완료) +- **조치 사항**: + - 새로운 CSS 모듈 구조와 디자인 원칙을 설명하는 `context/style.md` 문서를 신규 생성. + - `agent.md`의 상세 기술 가이드(Context Routing) 섹션에 스타일 및 디자인 항목을 추가하여 문서 접근성 개선. +