diff --git a/README.md b/README.md index ba496d5..e9c2297 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,146 @@ -# Holodream Tools +# 홀로도리 계산기 -## Card master data +홀로라이브 드림즈의 카드와 멤버 성장 상태를 관리하고, 악곡별 최적 배치와 카드 조합을 분석하는 비공식 도구입니다. -Collect the current card-related master data and Korean, English, and Japanese text from the public HolodoriDB diff repositories: +[최신 버전 다운로드](https://github.com/DD4SN/HolodoriCalc/releases/latest) · 한국어 전용 · PC 환경 권장 -```bash -npm run data:sync -``` +![홀로도리 계산기 홈 화면](docs/images/home.png) -The generated manifest, raw relational bundle, and card-by-card bundle are written to `data/holodoridb`. Card artwork, movies, and voices are intentionally excluded. +## 주요 기능 -### 카드 표시명 수동 수정 +- 5성 카드의 타입, 주스탯, 기수와 스킬 정보 조회 +- 보유 카드와 개화 단계, 메모리, 멤버 강화 수치 저장 +- 멤버별 보드 노드와 연결 카드 설정 +- 현재 편성에서 초록 노드의 자원 대비 효율 분석 +- 특정 악곡과 5인 편성의 최적 배치 및 빈도 감소 조합 분석 +- 고정 멤버를 기준으로 남은 자리에 들어갈 카드 조합 탐색 +- 액티브·스페셜 발동 구간과 노트 분포 시각화 +- 사용자 데이터 백업과 복원 +- 인터넷 연결 없이 실행 가능한 오프라인 버전 제공 -특별 버전명이나 번역 이름은 `data/card-overrides.csv`에서 관리합니다. +## 다운로드 및 실행 -```csv -cardId,versionName,displayName -card-06002-5-uniq-0066-00,수영복, -``` +1. [최신 Release](https://github.com/DD4SN/HolodoriCalc/releases/latest)를 엽니다. +2. `Assets`에서 `HolodoriCalc-offline-v버전.zip`을 내려받습니다. +3. ZIP 파일을 원하는 폴더에 완전히 압축 해제합니다. +4. 압축을 해제한 폴더의 `index.html`을 실행합니다. -- `cardId`: 수정할 카드의 고유 ID -- `versionName`: 기본 이름 옆에 표시할 짧은 버전명 -- `displayName`: 기본 멤버 이름을 완전히 바꿀 때만 입력하며, 평소에는 비워 둡니다. +> GitHub가 자동으로 제공하는 `Source code (zip)`은 실행용 배포 파일이 아닙니다. 반드시 `HolodoriCalc-offline-...zip`을 받아 주세요. -수정 후 `npm run data:sync`를 실행하면 카드 목록에 반영됩니다. 이 파일은 원본 데이터를 다시 수집해도 덮어쓰지 않습니다. +Chrome 또는 Edge의 최신 버전을 권장합니다. 일부 브라우저는 로컬 HTML의 저장소나 네트워크 접근을 제한할 수 있습니다. -## 브라우저 데이터 +## 처음 사용하는 순서 -사용자 데이터는 서버로 전송하지 않고 현재 브라우저의 `localStorage`에 자동 저장합니다. 별도의 `사용자 데이터` 페이지에서 JSON 백업을 내보내거나 가져오고, 전체 데이터를 초기화할 수 있습니다. +### 1. 사용자 데이터를 설정합니다 -- 현재 카드 목록의 출력 조건을 자동으로 복원합니다. -- `백업 내보내기`로 버전이 포함된 JSON 파일을 내려받을 수 있습니다. -- `백업 가져오기`로 다른 브라우저에서 데이터를 복원할 수 있습니다. -- `초기화`는 현재 브라우저에 저장된 수집 내역, 저장 덱, 설정을 모두 기본값으로 되돌립니다. +상단 메뉴에서 **사용자 데이터 → 카드**로 이동해 보유 카드와 개화 단계를 입력합니다. -향후 보유 카드와 저장 덱 기능도 `src/lib/models/user-data.ts`의 동일한 버전 데이터에 저장합니다. +- 5성 카드는 `0~5` 개화 단계를 설정합니다. +- 4성 카드는 보유 여부와 5개화 여부를 설정합니다. +- 보유하지 않은 카드는 개화 단계를 변경할 수 없습니다. +- 행을 누르면 보유 여부를 빠르게 전환할 수 있습니다. -홀로드림스 덱 최적화와 스킬 로스 분석을 통합할 웹사이트의 개발 기반입니다. 현재 단계에는 게임 기능을 포함하지 않습니다. +![카드 보유 및 개화 설정](docs/images/user-cards.png) -## 구성 +**사용자 데이터 → 일반**에서는 메모리 보유 수와 멤버 강화 수치를 설정하고, 전체 사용자 데이터를 백업하거나 복원할 수 있습니다. + +### 2. 멤버 보드를 설정합니다 + +**사용자 데이터 → 보드**에서 멤버를 선택한 뒤 실제 게임에서 활성화한 노드를 같은 형태로 입력합니다. + +- 빈 공간을 드래그해 보드를 이동합니다. +- 마우스 휠로 확대하거나 축소합니다. +- 인접하지 않은 노드를 누르면 중심에서 이어지는 최단 경로가 함께 활성화됩니다. +- 보드 슬롯에는 보유 카드를 한 장씩 배치할 수 있습니다. +- `모든 멤버 초록 3칸 활성화`와 `중앙 연결 노드 자동 배치`로 일반적인 설정을 빠르게 적용할 수 있습니다. + +![멤버 보드 설정](docs/images/member-board.png) + +같은 페이지의 **초록 노드 효율 분석**에서는 현재 편성을 기준으로 다음 항목을 비교할 수 있습니다. + +- 아직 활성화하지 않은 노드 중 우선 활성화할 노드 +- 이미 활성화한 노드 중 초기화를 검토할 노드 +- 종합력 변화, 필요한 연결 노드 수, 자원 소모량과 그린 큐브 효율 + +목록의 항목을 누르면 해당 멤버와 노드로 바로 이동합니다. + +### 3. 카드 정보를 확인합니다 + +**카드 목록**에서는 타입, 주스탯, 기수, 액티브 확률, 조건부 스킬과 이격 여부로 카드를 필터링할 수 있습니다. 카드 행을 누르면 패시브·액티브·스페셜·의상 효과가 펼쳐집니다. + +![카드 목록과 상세 스킬](docs/images/cards.png) + +### 4. 5인 편성의 최적 배치를 분석합니다 + +**배치 분석**은 선택한 악곡과 5인 조합을 유지하면서 가장 높은 점수를 내는 멤버 순서와 빈도 감소 조합을 찾습니다. + +1. 악곡과 난이도를 선택합니다. +2. 5장의 카드와 개화 단계를 설정합니다. +3. 필요한 경우 카드별 빈도 감소를 특정 값으로 고정합니다. +4. 리더와 리더 의상을 선택합니다. +5. `최적 배치 분석`을 누릅니다. + +![배치 분석 조건 설정](docs/images/analysis.png) + +분석 결과에서는 최고·평균 점수, 최적 순서, 카드별 빈도 감소율과 함께 다음 시각화 자료를 확인할 수 있습니다. + +- 초당 노트 분포 +- 멤버별 액티브 발동 구간 +- 중첩을 반영한 최종 액티브 적용 구간 +- 멤버별 스페셜 발동 구간 + +분석 조건과 마지막 결과는 브라우저에 저장되며, 같은 악곡과 난이도의 조합 탐색에서 **기준 조합**으로 사용할 수 있습니다. + +### 5. 더 좋은 카드 조합을 탐색합니다 + +**조합 탐색**은 악곡, 리더, 리더 의상과 최대 4명의 고정 카드를 기준으로 남은 자리에 들어갈 후보를 비교합니다. + +- 고정하지 않은 자리에는 전체 5성 카드가 후보로 들어갑니다. +- 자동 후보의 개화 단계와 기수 보너스 덮어쓰기 값을 지정할 수 있습니다. +- 보유 카드의 실제 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다. +- 고정 멤버별 개화, 빈도 감소, 파란 노드 전체 적용 여부와 기수 보너스를 따로 설정할 수 있습니다. +- 한 편성에는 같은 인물의 일반 카드와 이격 카드가 동시에 들어가지 않습니다. +- 결과는 최고 점수 기준으로 정렬되며 상위 조합의 상세 분석을 펼쳐볼 수 있습니다. + +![조합 탐색 조건과 기준 조합](docs/images/combination.png) + +`최대 컴퓨팅 자원 활용`을 켜면 탐색 속도가 빨라질 수 있지만, 분석 중 브라우저와 다른 프로그램의 반응이 느려질 수 있습니다. + +## 데이터 저장과 백업 + +사용자 데이터와 분석 조건은 서버로 전송하지 않고 현재 브라우저의 `localStorage`에 자동 저장합니다. + +- 브라우저나 사용자 프로필이 달라지면 별도의 데이터로 취급될 수 있습니다. +- 브라우저 데이터 삭제 시 저장 내용도 사라질 수 있습니다. +- 새 버전으로 이동하기 전 **사용자 데이터 → 일반 → 백업 내보내기**를 권장합니다. +- 백업 JSON은 다른 브라우저나 PC의 **백업 가져오기**에서 복원할 수 있습니다. + +## 업데이트 + +홈 화면에서 현재 버전과 GitHub의 최신 Release를 비교합니다. 새 버전이 있으면 오프라인 ZIP 다운로드 버튼이 표시됩니다. + +업데이트는 자동으로 파일을 교체하지 않습니다. 새 ZIP을 내려받아 별도 폴더에 압축을 해제한 뒤 `index.html`을 실행해 주세요. 업데이트 전에 사용자 데이터 백업을 권장합니다. + +## 참고 + +- 이 프로젝트는 홀로라이브 드림즈의 비공식 팬 제작 도구입니다. +- 게임 업데이트와 확보된 데이터 범위에 따라 실제 게임 결과와 차이가 날 수 있습니다. +- 분석 결과는 입력한 카드·개화·보드·메모리·멤버 강화 데이터가 정확하다는 전제에서 계산됩니다. +- 카드와 악곡의 원본 게임 데이터는 프로젝트가 직접 소유하거나 공식 제공하는 데이터가 아닙니다. + +
+개발 환경과 실행 방법 + +### 기술 구성 - SvelteKit + TypeScript + `@sveltejs/adapter-node` - SQLite + Drizzle ORM -- Web Worker를 위한 독립 모듈 경계 -- Docker Compose 개발 환경(HMR) -- 멀티 스테이지 production 이미지 -- Docker named volume 기반 SQLite 영속 저장소 +- Web Worker 기반 클라이언트 계산 +- Docker / Docker Compose -## Docker 개발 실행 +계산 엔진은 UI와 분리하며 `UI → Engine → Models` 의존성 방향을 유지합니다. + +### Docker 개발 실행 ```bash docker compose up --build @@ -56,32 +149,9 @@ docker compose up --build - 웹: - 상태 확인: -소스 파일을 수정하면 Windows의 Docker Desktop에서도 polling 기반 HMR이 동작합니다. +종료할 때 `docker compose down`은 SQLite 볼륨을 보존합니다. `docker compose down --volumes`는 저장된 볼륨까지 제거하므로 주의하세요. -종료: - -```bash -docker compose down -``` - -`docker compose down`은 SQLite 볼륨을 보존합니다. 데이터까지 지우려는 경우에만 `docker compose down --volumes`를 사용하세요. - -## production 실행 - -```bash -docker compose -f compose.prod.yaml up --build -d -``` - -- 웹: -- 상태 확인: - -종료: - -```bash -docker compose -f compose.prod.yaml down -``` - -## 호스트에서 실행 +### 호스트에서 실행 Node.js 22.12 이상이 필요합니다. @@ -91,33 +161,53 @@ npm ci npm run dev ``` -기본 로컬 DB 경로는 `./data/holodream.db`입니다. - -## 검증 명령 +### 검증과 빌드 ```bash npm run check npm run build +npm run build:offline ``` -스키마를 변경한 뒤에는 migration을 생성합니다. +릴리즈용 오프라인 ZIP 생성: ```bash -npm run db:generate +npm run package:offline ``` -생성된 migration은 서버가 시작될 때 자동 적용됩니다. 호스트에서 수동 적용하려면 `npm run db:migrate`를 사용합니다. +### Production 실행 -## 디렉터리 경계 - -```text -src/ -├─ lib/ -│ ├─ engine/ # UI 프레임워크에 의존하지 않는 순수 계산 로직 -│ ├─ models/ # UI·엔진·서버가 공유하는 도메인 타입 -│ ├─ server/db/ # SQLite/Drizzle 서버 전용 모듈 -│ └─ workers/ # 무거운 최적화 작업의 Web Worker 진입점 -└─ routes/ # SvelteKit 화면과 API +```bash +docker compose -f compose.prod.yaml up --build -d ``` -의존성 방향은 `UI → Engine → Models`를 유지합니다. 계산 엔진은 Svelte나 서버 모듈을 import하지 않습니다. +- 웹: +- 상태 확인: + +
+ +
+게임 데이터 갱신과 카드 표시명 수정 + +공개 HolodoriDB diff 저장소에서 카드 관련 마스터 데이터와 한국어·영어·일본어 텍스트를 수집합니다. + +```bash +npm run data:sync +``` + +생성된 데이터는 `data/holodoridb`에 저장합니다. 카드 일러스트, 영상과 음성은 수집 대상에서 제외합니다. + +특별 버전명이나 번역 이름은 `data/card-overrides.csv`에서 관리합니다. + +```csv +cardId,versionName,displayName +card-06002-5-uniq-0066-00,수영복, +``` + +- `cardId`: 수정할 카드의 고유 ID +- `versionName`: 이름 옆에 표시할 짧은 버전명 +- `displayName`: 기본 멤버 이름을 완전히 바꿀 때만 입력 + +수정 후 `npm run data:sync`를 실행하면 반영됩니다. 이 파일은 원본 데이터를 다시 수집해도 덮어쓰지 않습니다. + +
diff --git a/docs/images/analysis.png b/docs/images/analysis.png new file mode 100644 index 0000000..417b722 Binary files /dev/null and b/docs/images/analysis.png differ diff --git a/docs/images/cards.png b/docs/images/cards.png new file mode 100644 index 0000000..3f4a4c7 Binary files /dev/null and b/docs/images/cards.png differ diff --git a/docs/images/combination.png b/docs/images/combination.png new file mode 100644 index 0000000..0b91c08 Binary files /dev/null and b/docs/images/combination.png differ diff --git a/docs/images/home.png b/docs/images/home.png new file mode 100644 index 0000000..33d083a Binary files /dev/null and b/docs/images/home.png differ diff --git a/docs/images/member-board.png b/docs/images/member-board.png new file mode 100644 index 0000000..02f510b Binary files /dev/null and b/docs/images/member-board.png differ diff --git a/docs/images/user-cards.png b/docs/images/user-cards.png new file mode 100644 index 0000000..6b70e67 Binary files /dev/null and b/docs/images/user-cards.png differ diff --git a/package-lock.json b/package-lock.json index ee81560..cc3f748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "holodream-tools", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "holodream-tools", - "version": "1.0.1", + "version": "1.0.2", "engines": { "node": ">=22.12.0" }, diff --git a/package.json b/package.json index 3052d34..25d0ed6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "holodream-tools", - "version": "1.0.1", + "version": "1.0.2", "private": true, "type": "module", "engines": { diff --git a/release-notes/1.0.2.md b/release-notes/1.0.2.md new file mode 100644 index 0000000..3f46aab --- /dev/null +++ b/release-notes/1.0.2.md @@ -0,0 +1,25 @@ +# 홀로도리 계산기 1.0.2 + +조합 탐색의 후보 누락을 줄이고, 패시브 적용 대상과 분석 결과 표시를 개선한 업데이트입니다. + +## 주요 변경 사항 + +- 빈 편성 슬롯이 많을수록 조합 탐색 후보와 정밀 분석 대상을 확대했습니다. +- 여러 선별 기준과 후보별 교체 조합 재검증을 추가해 강한 카드가 조기에 제외될 가능성을 줄였습니다. +- 보유 카드의 실제 개화가 자동 후보 설정값보다 높으면 실제 개화를 우선 적용합니다. +- 편성 카드 목록과 추천 스페셜 발동 순서를 구분해 표시합니다. +- 제한 인원 패시브와 스코어 서포트의 적용 대상을 파라미터 합계 기준으로 판정하도록 수정했습니다. +- 배치 분석과 조합 탐색에 최고점, 최고점의 99%, 95% 이상 달성 확률을 추가했습니다. +- 중앙 연결 노드 자동 배치 결과에서 어떤 카드가 어느 멤버 보드에 배치됐는지 확인할 수 있습니다. +- 초록 노드 효율 분석에 `코어 큐브 미소모만` 검색 옵션을 추가했습니다. +- 스크린샷을 포함한 사용자용 README를 추가했습니다. + +## 알려진 문제 + +> 달성 확률과 이론상 최고점이 실제 인게임 결과보다 다소 높게 평가되는 현상이 확인되었습니다. 현재 결과는 절대적인 예상 점수보다 조합 간 상대 비교와 참고값으로 활용해 주세요. + +달성 확률은 모든 노트를 PERFECT로 처리한다는 가정 아래 액티브 발동 확률만 반영합니다. 실제 플레이의 판정과 아직 확인되지 않은 게임 내부 계산 방식에 따라 차이가 발생할 수 있습니다. + +## 다운로드 + +Release의 Assets에서 `HolodoriCalc-offline-v1.0.2.zip`을 내려받아 완전히 압축 해제한 뒤 `index.html`을 실행해 주세요. GitHub가 자동으로 제공하는 Source code 압축파일은 실행용 배포본이 아닙니다. diff --git a/src/lib/components/AnalysisResultView.svelte b/src/lib/components/AnalysisResultView.svelte index 9514920..bd0745b 100644 --- a/src/lib/components/AnalysisResultView.svelte +++ b/src/lib/components/AnalysisResultView.svelte @@ -97,11 +97,16 @@ } -
    - {#each result.order as cardId, slot} -
  1. {slot + 1}{cardName(cardId)}쿨타임 {result.cooldownReductions[cardId]}% 감소
  2. - {/each} -
+
+
+

추천 스페셜 발동 순서

+
    + {#each result.order as cardId, slot} +
  1. {slot + 1}{cardName(cardId)}쿨타임 {result.cooldownReductions[cardId]}% 감소
  2. + {/each} +
+
+
{#if showScoreSummary}
예상 점수{result.expectedScore.toLocaleString('ko-KR')}{#if baseline}기준 대비 {delta(result.expectedScore, baseline.expectedScore)}{/if}
@@ -164,7 +169,10 @@ diff --git a/src/routes/data/+page.svelte b/src/routes/data/+page.svelte index 43c6688..3528a58 100644 --- a/src/routes/data/+page.svelte +++ b/src/routes/data/+page.svelte @@ -36,6 +36,7 @@ awakenings: number[]; leaderCharacterId: string; leaderOutfitCardId: string; + coreCubeFreeOnly: boolean; }; type GreenNodeCost = { points: number; greenCubes: number; greenCoreCubes: number }; type GreenNodeEfficiency = { @@ -54,6 +55,13 @@ upgrades: GreenNodeEfficiency[]; removals: GreenNodeEfficiency[]; }; + type AutomaticPlacementResult = { + memberId: string; + memberName: string; + cardId: string; + cardName: string; + rarity: number; + }; const ANALYSIS_STORAGE_KEY = 'holodori.analysis.v2'; const EFFICIENCY_FORMATION_STORAGE_KEY = 'holodori.green-efficiency-formation.v1'; const GREEN_CUBE_ID = 'item-skill_tree_allmember-1'; @@ -100,12 +108,14 @@ let selectedSocketGroupId = ''; let boardCardSearch = ''; let automaticPlacementSnapshot: BoardCardPlacements | null = null; + let automaticPlacementResult: AutomaticPlacementResult[] = []; let savedAnalysis: SavedAnalysisState | null = null; let efficiencyFormation: EfficiencyFormationState = { cardIds: ['', '', '', '', ''], awakenings: [0, 0, 0, 0, 0], leaderCharacterId: '', - leaderOutfitCardId: '' + leaderOutfitCardId: '', + coreCubeFreeOnly: false }; let greenEfficiencyResult: GreenEfficiencyResult | null = null; let greenEfficiencyRunning = false; @@ -163,7 +173,8 @@ cardIds: stored.cardIds.map((id) => cardById.has(id) ? id : ''), awakenings: stored.awakenings.map(normalizeEfficiencyAwakening), leaderCharacterId: leaderMembers.some((member) => member.characterId === stored.leaderCharacterId) ? stored.leaderCharacterId : '', - leaderOutfitCardId: cardById.get(stored.leaderOutfitCardId)?.details.costume ? stored.leaderOutfitCardId : '' + leaderOutfitCardId: cardById.get(stored.leaderOutfitCardId)?.details.costume ? stored.leaderOutfitCardId : '', + coreCubeFreeOnly: stored.coreCubeFreeOnly === true }; } } catch { @@ -222,13 +233,18 @@ saveEfficiencyFormation({ ...efficiencyFormation, leaderOutfitCardId: cardId }); } + function updateCoreCubeFreeOnly(coreCubeFreeOnly: boolean) { + saveEfficiencyFormation({ ...efficiencyFormation, coreCubeFreeOnly }); + } + function importAnalysisFormation() { if (!savedAnalysis) return; saveEfficiencyFormation({ cardIds: [...savedAnalysis.cardIds], awakenings: savedAnalysis.awakenings.map(normalizeEfficiencyAwakening), leaderCharacterId: savedAnalysis.leaderCharacterId, - leaderOutfitCardId: savedAnalysis.leaderOutfitCardId + leaderOutfitCardId: savedAnalysis.leaderOutfitCardId, + coreCubeFreeOnly: efficiencyFormation.coreCubeFreeOnly }); } @@ -625,6 +641,7 @@ const path = shortestPathToSet(position.groupId, active, positions); const added = path.filter((groupId) => groupId !== center && !active.has(groupId)); if (added.length === 0) continue; + if (efficiencyFormation.coreCubeFreeOnly && greenNodeCost(member.id, added).greenCoreCubes > 0) continue; const nextBoards = { ...userData.memberBoards, [member.id]: [...new Set([...currentNodes, ...added])] }; const nextPower = currentFormationPower(nextBoards); const intermediateNodes = added.filter((groupId) => groupId !== position.groupId); @@ -643,6 +660,7 @@ const reachable = reachableNodesForPositions(remaining, positions, center); const removed = currentNodes.filter((groupId) => !reachable.has(groupId)); if (removed.length === 0) continue; + if (efficiencyFormation.coreCubeFreeOnly && greenNodeCost(member.id, removed).greenCoreCubes > 0) continue; const nextMemberNodes = currentNodes.filter((groupId) => reachable.has(groupId)); const nextBoards = { ...userData.memberBoards }; if (nextMemberNodes.length > 0) nextBoards[member.id] = nextMemberNodes; @@ -788,12 +806,24 @@ automaticPlacementSnapshot = cloneBoardCards(userData.boardCards); const boardCards = cloneBoardCards(userData.boardCards); + const placementResult: AutomaticPlacementResult[] = []; for (const match of matches) { const card = candidateCards[match.rowIndex]; const slot = emptyGreenSlots[match.columnIndex]; if (!card || !slot) continue; boardCards[slot.member.id] = { ...(boardCards[slot.member.id] ?? {}), [slot.socket.groupId]: card.id }; + placementResult.push({ + memberId: slot.member.id, + memberName: slot.member.name, + cardId: card.id, + cardName: cardLabel(card), + rarity: card.rarity + }); } + automaticPlacementResult = placementResult.sort((left, right) => + (boardMembers.find((member) => member.id === left.memberId)?.order ?? Number.MAX_SAFE_INTEGER) + - (boardMembers.find((member) => member.id === right.memberId)?.order ?? Number.MAX_SAFE_INTEGER) + ); persist({ ...userData, boardCards }); showMessage(`기존 배치를 유지하고 초록색 빈 슬롯 ${matches.length}곳에 보유 카드를 최적 배치했습니다.`); } @@ -802,6 +832,7 @@ if (!automaticPlacementSnapshot) return; const boardCards = cloneBoardCards(automaticPlacementSnapshot); automaticPlacementSnapshot = null; + automaticPlacementResult = []; persist({ ...userData, boardCards }); showMessage('방금 실행한 초록색 카드 자동 배치를 취소했습니다.'); } @@ -1111,6 +1142,19 @@ {#if automaticPlacementSnapshot}{/if}
+ {#if automaticPlacementResult.length > 0} +
+ 자동 배치 결과 {automaticPlacementResult.length}건 +
+ {#each automaticPlacementResult as placement (placement.memberId)} +
+ {placement.memberName} 보드 + {placement.rarity}★{placement.cardName} +
+ {/each} +
+
+ {/if}
@@ -1173,6 +1217,15 @@
리더 의상 효과

{plainSkillText(cardById.get(efficiencyFormation.leaderOutfitCardId)?.details.costume?.description ?? '') || '적용하지 않음'}

+ {#if greenEfficiencyError}{/if} {#if greenEfficiencyResult} @@ -1201,7 +1254,7 @@ {/each} - {:else}

종합력을 올리는 비활성 초록 노드가 없습니다.

{/if} + {:else}

{efficiencyFormation.coreCubeFreeOnly ? '코어 큐브를 소모하지 않고 종합력을 올리는 비활성 초록 노드가 없습니다.' : '종합력을 올리는 비활성 초록 노드가 없습니다.'}

{/if}
@@ -1221,7 +1274,7 @@ {/each}
- {:else}

초기화를 검토할 활성 초록 노드가 없습니다.

{/if} + {:else}

{efficiencyFormation.coreCubeFreeOnly ? '코어 큐브를 환급하지 않는 활성 초록 노드가 없습니다.' : '초기화를 검토할 활성 초록 노드가 없습니다.'}

{/if}
{/if} @@ -1457,7 +1510,7 @@ .board-toolbar p strong { color: #f0a1c2; font-size: 16px; } .board-toolbar div { display: flex; gap: 6px; } .board-toolbar button { padding: 6px 9px; border: 1px solid #394252; border-radius: 7px; color: #aeb7c5; background: #202630; font-size: 12px; font-weight: 750; cursor: pointer; } - .automatic-placement { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 18px; padding: 12px 14px; border: 1px solid #355044; border-radius: 10px; background: rgb(58 113 83 / 10%); } + .automatic-placement { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 18px; padding: 12px 14px; border: 1px solid #355044; border-radius: 10px; background: rgb(58 113 83 / 10%); } .automatic-placement > div:first-child { display: grid; gap: 4px; min-width: 0; } .automatic-placement strong { color: #b7dec7; font-size: 14px; } .automatic-placement small { color: #82988c; font-size: 12px; line-height: 1.45; } @@ -1467,6 +1520,14 @@ .automatic-placement button.undo { color: #c4ccd5; background: transparent; } .automatic-placement button:hover { filter: brightness(1.1); } .automatic-placement button:focus-visible { outline: 3px solid rgb(120 196 147 / 25%); outline-offset: 2px; } + .automatic-placement-result { display: grid; flex: 1 0 100%; gap: 9px; padding-top: 12px; border-top: 1px solid rgb(120 196 147 / 18%); } + .automatic-placement-result > strong { display: flex; align-items: center; gap: 7px; color: #d8e9df; font-size: 14px; } + .automatic-placement-result > strong span { color: #78c493; font-size: 12px; } + .automatic-placement-result-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 6px 10px; max-height: 280px; overflow-y: auto; padding-right: 4px; } + .automatic-placement-result-item { display: grid; grid-template-columns: minmax(105px, 0.8fr) minmax(150px, 1.2fr); align-items: center; gap: 10px; min-height: 38px; padding: 7px 10px; border: 1px solid rgb(120 196 147 / 13%); border-radius: 7px; background: rgb(9 18 13 / 34%); } + .automatic-placement-result-item span { color: #91a59a; font-size: 12px; } + .automatic-placement-result-item b { display: flex; align-items: center; gap: 7px; min-width: 0; color: #dbe8df; font-size: 13px; } + .automatic-placement-result-item i { flex: 0 0 auto; color: #e7bd70; font-size: 11px; font-style: normal; } .green-efficiency { order: 2; margin: 18px 0 0; padding: 0 16px 16px; border: 1px solid #394453; border-radius: 16px; background: #151b23; } .green-efficiency > summary { display: flex; align-items: center; min-height: 62px; margin: 0 -16px; padding: 12px 16px; color: #dce6e0; cursor: pointer; list-style: none; } .green-efficiency > summary::-webkit-details-marker { display: none; } @@ -1503,6 +1564,12 @@ .efficiency-outfit-effect { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 10px; margin-top: 9px; padding: 9px 10px; border-radius: 8px; background: #171e26; } .efficiency-outfit-effect span { color: #d990af; font-size: 13px; font-weight: 850; } .efficiency-outfit-effect p { margin: 0; color: #a7b0bd; font-size: 13px; line-height: 1.5; } + .efficiency-core-filter { display: flex; align-items: center; gap: 10px; margin-top: 9px; padding: 10px 11px; border: 1px solid #314238; border-radius: 8px; background: #111a15; cursor: pointer; } + .efficiency-core-filter input { width: 18px; height: 18px; margin: 0; accent-color: #78c493; cursor: pointer; } + .efficiency-core-filter input:disabled { cursor: default; } + .efficiency-core-filter > span { display: grid; gap: 2px; } + .efficiency-core-filter strong { color: #dbe5df; font-size: 14px; } + .efficiency-core-filter small { color: #8f9e96; font-size: 12px; line-height: 1.45; } .efficiency-summary { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px 12px; margin-top: 14px; padding: 10px 12px; border-radius: 8px; background: #10151c; } .efficiency-summary span { color: #8b96a5; font-size: 13px; } .efficiency-summary strong { margin-right: 10px; color: #d6dde7; font-size: 16px; }