diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fffcc10 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,5 @@ +# GitHub Copilot 项目入口 + +开始任何仓库任务前,必须完整读取并遵守根目录 `AGENTS.md`。 + +`AGENTS.md` 是 Agent 统一加载入口;强制规范正文仍以 `docs/engineering-standards.md` 和仓库可执行配置为准。不得只依赖本文件、聊天上下文或规则摘要。 diff --git a/.github/workflows/pull-request-checks.yml b/.github/workflows/pull-request-checks.yml new file mode 100644 index 0000000..002cf5d --- /dev/null +++ b/.github/workflows/pull-request-checks.yml @@ -0,0 +1,81 @@ +name: Pull Request Checks + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pr-checks-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + legacy-config-upgrade: + name: Legacy configuration upgrade compatibility + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Test old install directory upgrade + run: npm run test:legacy-config-upgrade + + backend-fleet-contract: + name: Backend fleet contract + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout GUI + uses: actions/checkout@v4 + + - name: Checkout AutoWSGR + uses: actions/checkout@v4 + with: + repository: ShiinaKuroko/AutoWSGR + ref: ShiinaKuroko + path: AutoWSGR + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup uv + uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + + - name: Install GUI dependencies + run: npm ci + + - name: Install AutoWSGR dependencies + run: uv sync --project AutoWSGR --no-dev + + - name: Test candidate-only GUI services + run: npm run test:main-services + + - name: Test GUI to AutoWSGR fleet contract + env: + AUTOWSGR_REPO: ${{ github.workspace }}/AutoWSGR + AUTOWSGR_PYTHON: ${{ github.workspace }}/AutoWSGR/.venv/bin/python + run: npm run test:api-contract diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f078a7..9c80073 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,12 +5,6 @@ on: tags: - 'v*' workflow_dispatch: - inputs: - prerelease: - description: '标记为预发布' - required: false - type: boolean - default: false permissions: contents: write @@ -32,8 +26,26 @@ jobs: $pkg = Get-Content package.json | ConvertFrom-Json $ver = $pkg.version } + if ($ver -match '^\d+\.\d+\.\d+$') { + $channel = 'latest' + $stage = 'stable' + $prerelease = 'false' + } elseif ($ver -match '^\d+\.\d+\.\d+-beta\.\d+$') { + $channel = 'beta' + $stage = 'prerelease' + $prerelease = 'true' + } elseif ($ver -match '^\d+\.\d+\.\d+-dev(?:\.\d+)?$') { + $channel = 'dev' + $stage = 'development' + $prerelease = 'true' + } else { + throw "版本 $ver 不符合 X.Y.Z、X.Y.Z-beta.N、X.Y.Z-dev 或 X.Y.Z-dev.N" + } echo "VERSION=$ver" >> $env:GITHUB_OUTPUT - echo "Resolved version: $ver" + echo "CHANNEL=$channel" >> $env:GITHUB_OUTPUT + echo "STAGE=$stage" >> $env:GITHUB_OUTPUT + echo "PRERELEASE=$prerelease" >> $env:GITHUB_OUTPUT + echo "Resolved version: $ver ($stage/$channel)" - name: Setup Node.js uses: actions/setup-node@v4 @@ -49,6 +61,10 @@ jobs: run: | $pkg = Get-Content package.json | ConvertFrom-Json $pkg.version = "${{ steps.version.outputs.VERSION }}" + $pkg.build.publish | Add-Member ` + -NotePropertyName channel ` + -NotePropertyValue "${{ steps.version.outputs.CHANNEL }}" ` + -Force $pkg | ConvertTo-Json -Depth 10 | Set-Content package.json -Encoding UTF8 - name: Build & Package @@ -60,17 +76,34 @@ jobs: shell: pwsh run: Get-ChildItem -Path release -Recurse -File | Select-Object FullName, Length + - name: Verify update channel metadata + shell: pwsh + run: | + $channel = "${{ steps.version.outputs.CHANNEL }}" + $expected = Join-Path release "$channel.yml" + if (-not (Test-Path $expected)) { + throw "缺少 $channel 频道更新清单: $expected" + } + @('latest', 'beta', 'dev') | + Where-Object { $_ -ne $channel } | + ForEach-Object { + $unexpected = Join-Path release "$_.yml" + if (Test-Path $unexpected) { + throw "发布产物混入其他频道清单: $unexpected" + } + } + - name: Create Release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.version.outputs.VERSION }} name: AutoWSGR-GUI v${{ steps.version.outputs.VERSION }} - prerelease: ${{ github.event.inputs.prerelease == 'true' }} + prerelease: ${{ steps.version.outputs.PRERELEASE == 'true' }} generate_release_notes: true files: | release/*-Setup-*.exe release/*.exe.blockmap - release/latest.yml + release/${{ steps.version.outputs.CHANNEL }}.yml body: | ## AutoWSGR-GUI v${{ steps.version.outputs.VERSION }} diff --git a/.gitignore b/.gitignore index 8485763..82a94ca 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ Thumbs.db usersettings.yaml gui_settings.json task_groups.json +!scripts/fixtures/**/task_groups.json templates.json .env_ready plans/ @@ -38,6 +39,7 @@ release/ # 安装临时文件 _setup_tmp/ .tmp/ +.tmp_* node/ python/ redist/ @@ -45,10 +47,14 @@ adb/ # 缓存 .cache/ +.dbg/ +__pycache__/ +*.py[cod] +/debug-backend-pipe-epipe.md # Kilo 工作区 .kilo/ # 测试用地图 resource/maps/99-1.json -plans/99-1.yaml. \ No newline at end of file +plans/99-1.yaml. diff --git a/.tmp_issue360.json b/.tmp_issue360.json deleted file mode 100644 index 9df7d21..0000000 --- a/.tmp_issue360.json +++ /dev/null @@ -1,2 +0,0 @@ -{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360","repository_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360/labels{/name}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360/comments","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360/events","html_url":"https://github.com/OpenWSGR/AutoWSGR/issues/360","id":4052520746,"node_id":"I_kwDOHhoLA87xjI8q","number":360,"title":"[Feature] GUI API ǿֹͣÿֽݡ","user":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[],"state":"open","locked":false,"assignees":[],"milestone":null,"comments":2,"created_at":"2026-03-10T15:21:29Z","updated_at":"2026-04-05T06:37:38Z","closed_at":null,"assignee":null,"author_association":"CONTRIBUTOR","type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"## \n\nAutoWSGR-GUI ʵеȡֹͣ顢ÿսչʾȹܿܣֹ API ݲû޷ issue GUI ĺ API ǿ\n\n---\n\n## 1. ֹͣɿÿֵȼߣ\n\n**ǰ״**\n- ʵ `stopCondition` ƣ`loot_count_ge` / `ship_count_ge`ûáˢ N սƷ/ʱԶֹͣ\n- `/api/game/context` ص `dropped_ship_count` / `dropped_loot_count` ɿ**ֹͣ߼עͽ**\n\n**Ҫģ**\n- ÿս׼ȷۼƱγĵ佢սƷ\n- ͨ `/api/game/context`սشṩ׼ȷۼֵ\n- #355 ĵ佢ʶ𻥲#355 ʶʲôۼƵ˶١\n\n**ǰеĵô루ǰעͣ**\n```typescript\n// Scheduler.ts\nconst resp = await this.api.gameContext();\nif (data.dropped_loot_count >= cond.loot_count_ge) { /* ֹͣ */ }\nif (data.dropped_ship_count >= cond.ship_count_ge) { /* ֹͣ */ }\n```\n\n---\n\n## 2. ÿսشȼͣ\n\n**ǰ״**\n- `RoundResult` ݽṹѶ `mvp``grade``ship_damage` ֶ\n- WebSocket `task_completed` Ϣе `result.details` ĿǰЩֶΪ\n\n**Ҫģ**\n- սʱ `RoundResult` 䣺\n - `ship_damage`: λ̶ȣѡǰչʾ״̬\n- MVP ƺ͵佢 #355 \n\n** gradeѽ**\n> սۣSS/S/A/B/C/Dͨǰ˽ stderr ־ `[Combat] ս: MVP=0 =SS ڵ: A` ʵɫʾ** API ⷵ**δ `grade` ֶΣǰ˿ֱʹãⲻ\n\n---\n\n## 3. ˵㣨ȼͣ\n\n**ǰ״**\n- WebSocket Զ\n- ޷˽̿ OCR סģӦȳ\n\n**Ҫģ**\n- ṩ `GET /api/health` ˵㣬غ˴״̬\n- ѡصǰǷij賬쳣ʱ\n\n---\n\n## \n\n- #355 MVPƻش & 佢ʶشϵ\n- GUI ֿ⣺[yltx/AutoWSGR-GUI](https://github.com/yltx/AutoWSGR-GUI)","closed_by":null,"reactions":{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360/timeline","performed_via_github_app":null,"state_reason":null,"pinned_comment":null} - diff --git a/.tmp_issue360_comments.json b/.tmp_issue360_comments.json deleted file mode 100644 index fe5ae76..0000000 --- a/.tmp_issue360_comments.json +++ /dev/null @@ -1,2 +0,0 @@ -[{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188401106","html_url":"https://github.com/OpenWSGR/AutoWSGR/issues/360#issuecomment-4188401106","issue_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360","id":4188401106,"node_id":"IC_kwDOHhoLA875pe3S","user":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2026-04-05T06:37:17Z","updated_at":"2026-04-05T06:37:17Z","body":"ǰ˷һ£issueǷѾ@github-actions","author_association":"CONTRIBUTOR","pin":null,"reactions":{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188401106/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null},{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188401263","html_url":"https://github.com/OpenWSGR/AutoWSGR/issues/360#issuecomment-4188401263","issue_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/360","id":4188401263,"node_id":"IC_kwDOHhoLA875pe5v","user":{"login":"github-actions[bot]","id":41898282,"node_id":"MDM6Qm90NDE4OTgyODI=","avatar_url":"https://avatars.githubusercontent.com/in/15368?v=4","gravatar_id":"","url":"https://api.github.com/users/github-actions%5Bbot%5D","html_url":"https://github.com/apps/github-actions","followers_url":"https://api.github.com/users/github-actions%5Bbot%5D/followers","following_url":"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/github-actions%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/github-actions%5Bbot%5D/repos","events_url":"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/github-actions%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"created_at":"2026-04-05T06:37:29Z","updated_at":"2026-04-05T06:37:38Z","body":"ִԡ\n\n---\n\n
˴չ\nError: Model \"gpt-5.4\" from --model flag is not available.\n
\n\n?? [GitHub Action м¼](https://github.com/OpenWSGR/AutoWSGR/actions/runs/23996089391)\n","author_association":"CONTRIBUTOR","pin":null,"reactions":{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188401263/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2025-12-02T18:13:15Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]}}] - diff --git a/.tmp_issue372.json b/.tmp_issue372.json deleted file mode 100644 index 32cc256..0000000 --- a/.tmp_issue372.json +++ /dev/null @@ -1,2 +0,0 @@ -{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372","repository_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372/labels{/name}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372/comments","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372/events","html_url":"https://github.com/OpenWSGR/AutoWSGR/issues/372","id":4080457974,"node_id":"I_kwDOHhoLA87zNtj2","number":372,"title":"[Feature Request] ҳ OCR ʶ ֧/ճ״̬ѯ","user":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"labels":[],"state":"open","locked":false,"assignees":[],"milestone":null,"comments":2,"created_at":"2026-03-16T06:12:51Z","updated_at":"2026-04-05T06:10:41Z","closed_at":null,"assignee":null,"author_association":"CONTRIBUTOR","type":null,"active_lock_reason":null,"sub_issues_summary":{"total":0,"completed":0,"percent_completed":0},"issue_dependencies_summary":{"blocked_by":0,"total_blocked_by":0,"blocking":0,"total_blocking":0},"body":"## \n\nϣͨ OCR ʶҳ棨ճ/ܳ/ս۵ȣȡ״̬Ӷ GUI Զűһ\n\n## ʹó\n\n- GUI ǰչʾǰճ/ܳɽ\n- Զűδ̬ѡһִ\n- ÿԶУԽʡʱ\n\n## Ԥʵ\n\n1. ҳ\n2. OCR ʶƺ״̬/δ/У\n3. ؽṹб\n\n## ѵ\n\n- Ҫ OCR ģƥҳ\n- ݿ⣨ Ӧӳ䣩\n- ֱͬµ\n\n## ȼ\n\nе Ŀǰ̶ͨƹ˴˹ܿԴԶܶȡ","closed_by":null,"reactions":{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"timeline_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372/timeline","performed_via_github_app":null,"state_reason":null,"pinned_comment":null} - diff --git a/.tmp_issue372_comments.json b/.tmp_issue372_comments.json deleted file mode 100644 index ba104aa..0000000 --- a/.tmp_issue372_comments.json +++ /dev/null @@ -1,2 +0,0 @@ -[{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188369930","html_url":"https://github.com/OpenWSGR/AutoWSGR/issues/372#issuecomment-4188369930","issue_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372","id":4188369930,"node_id":"IC_kwDOHhoLA875pXQK","user":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"created_at":"2026-04-05T06:10:22Z","updated_at":"2026-04-05T06:10:22Z","body":"@github-actions һ","author_association":"CONTRIBUTOR","pin":null,"reactions":{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188369930/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":null},{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188370118","html_url":"https://github.com/OpenWSGR/AutoWSGR/issues/372#issuecomment-4188370118","issue_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/372","id":4188370118,"node_id":"IC_kwDOHhoLA875pXTG","user":{"login":"github-actions[bot]","id":41898282,"node_id":"MDM6Qm90NDE4OTgyODI=","avatar_url":"https://avatars.githubusercontent.com/in/15368?v=4","gravatar_id":"","url":"https://api.github.com/users/github-actions%5Bbot%5D","html_url":"https://github.com/apps/github-actions","followers_url":"https://api.github.com/users/github-actions%5Bbot%5D/followers","following_url":"https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github-actions%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/github-actions%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/github-actions%5Bbot%5D/repos","events_url":"https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/github-actions%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"created_at":"2026-04-05T06:10:33Z","updated_at":"2026-04-05T06:10:41Z","body":"ִԡ\n\n---\n\n
˴չ\nError: Authentication failed (Request ID: 9401:14A9D1:93F818:D06440:69D1FCDF)\n\nYour GitHub token may be invalid, expired, or lacking the required permissions.\n\nTo resolve this, try the following:\n ? Start 'copilot' and run the '/login' command to re-authenticate\n ? If using a Fine-Grained PAT, ensure it has the 'Copilot Requests' permission enabled\n ? If using COPILOT_GITHUB_TOKEN, GH_TOKEN or GITHUB_TOKEN environment variable, verify the token is valid and not expired\n ? Run 'gh auth status' to check your current authentication status\n
\n\n?? [GitHub Action м¼](https://github.com/OpenWSGR/AutoWSGR/actions/runs/23995683447)\n","author_association":"CONTRIBUTOR","pin":null,"reactions":{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments/4188370118/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"performed_via_github_app":{"id":15368,"client_id":"Iv1.05c79e9ad1f6bdfa","slug":"github-actions","node_id":"MDM6QXBwMTUzNjg=","owner":{"login":"github","id":9919,"node_id":"MDEyOk9yZ2FuaXphdGlvbjk5MTk=","avatar_url":"https://avatars.githubusercontent.com/u/9919?v=4","gravatar_id":"","url":"https://api.github.com/users/github","html_url":"https://github.com/github","followers_url":"https://api.github.com/users/github/followers","following_url":"https://api.github.com/users/github/following{/other_user}","gists_url":"https://api.github.com/users/github/gists{/gist_id}","starred_url":"https://api.github.com/users/github/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/github/subscriptions","organizations_url":"https://api.github.com/users/github/orgs","repos_url":"https://api.github.com/users/github/repos","events_url":"https://api.github.com/users/github/events{/privacy}","received_events_url":"https://api.github.com/users/github/received_events","type":"Organization","user_view_type":"public","site_admin":false},"name":"GitHub Actions","description":"Automate your workflow from idea to production","external_url":"https://help.github.com/en/actions","html_url":"https://github.com/apps/github-actions","created_at":"2018-07-30T09:30:17Z","updated_at":"2025-12-02T18:13:15Z","permissions":{"actions":"write","administration":"read","artifact_metadata":"write","attestations":"write","checks":"write","contents":"write","copilot_requests":"write","deployments":"write","discussions":"write","issues":"write","merge_queues":"write","metadata":"read","models":"read","packages":"write","pages":"write","pull_requests":"write","repository_hooks":"write","repository_projects":"write","security_events":"write","statuses":"write","vulnerability_alerts":"read"},"events":["branch_protection_rule","check_run","check_suite","create","delete","deployment","deployment_status","discussion","discussion_comment","fork","gollum","issues","issue_comment","label","merge_group","milestone","page_build","public","pull_request","pull_request_review","pull_request_review_comment","push","registry_package","release","repository","repository_dispatch","status","watch","workflow_dispatch","workflow_run"]}}] - diff --git a/.tmp_runs.json b/.tmp_runs.json deleted file mode 100644 index 5efad4f..0000000 --- a/.tmp_runs.json +++ /dev/null @@ -1,2 +0,0 @@ -{"total_count":236,"workflow_runs":[{"id":23995683447,"name":"AI Issue Analysis","node_id":"WFR_kwLOHhoLA88AAAAFlkESdw","head_branch":"main","head_sha":"8e568107bd3ccce8bbf4896d30a50c16b793e633","path":".github/workflows/ai-issue-analysis.yml","display_title":"[Feature Request] ҳ OCR ʶ ֧/ճ״̬ѯ","run_number":2,"event":"issue_comment","status":"completed","conclusion":"failure","workflow_id":256224518,"check_suite_id":63358713624,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOwHk_GA","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23995683447","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23995683447","pull_requests":[],"created_at":"2026-04-05T06:10:25Z","updated_at":"2026-04-05T06:10:43Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-05T06:10:25Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23995683447/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23995683447/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63358713624","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23995683447/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23995683447/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23995683447/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/256224518","head_commit":{"id":"8e568107bd3ccce8bbf4896d30a50c16b793e633","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill (#398)","timestamp":"2026-04-04T16:23:59Z","author":{"name":"yltx","email":"37734213+yltx@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23992934314,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFlhcfqg","head_branch":"main","head_sha":"8e568107bd3ccce8bbf4896d30a50c16b793e633","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":104,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63351563072,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOwAwjQA","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23992934314","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23992934314","pull_requests":[],"created_at":"2026-04-05T02:56:47Z","updated_at":"2026-04-05T02:56:48Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-05T02:56:47Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23992934314/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23992934314/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63351563072","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23992934314/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23992934314/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23992934314/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"8e568107bd3ccce8bbf4896d30a50c16b793e633","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill (#398)","timestamp":"2026-04-04T16:23:59Z","author":{"name":"yltx","email":"37734213+yltx@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23989486085,"name":"AI Issue Analysis","node_id":"WFR_kwLOHhoLA88AAAAFleKCBQ","head_branch":"main","head_sha":"8e568107bd3ccce8bbf4896d30a50c16b793e633","path":".github/workflows/ai-issue-analysis.yml","display_title":"űȱȷϻ","run_number":1,"event":"issues","status":"completed","conclusion":"failure","workflow_id":256224518,"check_suite_id":63342341233,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOv39scQ","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23989486085","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23989486085","pull_requests":[],"created_at":"2026-04-04T23:04:58Z","updated_at":"2026-04-04T23:05:14Z","actor":{"login":"syokounya","id":93929783,"node_id":"U_kgDOBZlBNw","avatar_url":"https://avatars.githubusercontent.com/u/93929783?v=4","gravatar_id":"","url":"https://api.github.com/users/syokounya","html_url":"https://github.com/syokounya","followers_url":"https://api.github.com/users/syokounya/followers","following_url":"https://api.github.com/users/syokounya/following{/other_user}","gists_url":"https://api.github.com/users/syokounya/gists{/gist_id}","starred_url":"https://api.github.com/users/syokounya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syokounya/subscriptions","organizations_url":"https://api.github.com/users/syokounya/orgs","repos_url":"https://api.github.com/users/syokounya/repos","events_url":"https://api.github.com/users/syokounya/events{/privacy}","received_events_url":"https://api.github.com/users/syokounya/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T23:04:58Z","triggering_actor":{"login":"syokounya","id":93929783,"node_id":"U_kgDOBZlBNw","avatar_url":"https://avatars.githubusercontent.com/u/93929783?v=4","gravatar_id":"","url":"https://api.github.com/users/syokounya","html_url":"https://github.com/syokounya","followers_url":"https://api.github.com/users/syokounya/followers","following_url":"https://api.github.com/users/syokounya/following{/other_user}","gists_url":"https://api.github.com/users/syokounya/gists{/gist_id}","starred_url":"https://api.github.com/users/syokounya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syokounya/subscriptions","organizations_url":"https://api.github.com/users/syokounya/orgs","repos_url":"https://api.github.com/users/syokounya/repos","events_url":"https://api.github.com/users/syokounya/events{/privacy}","received_events_url":"https://api.github.com/users/syokounya/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23989486085/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23989486085/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63342341233","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23989486085/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23989486085/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23989486085/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/256224518","head_commit":{"id":"8e568107bd3ccce8bbf4896d30a50c16b793e633","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill (#398)","timestamp":"2026-04-04T16:23:59Z","author":{"name":"yltx","email":"37734213+yltx@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23985336227,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFlaMvow","head_branch":"main","head_sha":"8e568107bd3ccce8bbf4896d30a50c16b793e633","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":103,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63331452211,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvtlFMw","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23985336227","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23985336227","pull_requests":[],"created_at":"2026-04-04T18:51:32Z","updated_at":"2026-04-04T18:51:33Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T18:51:32Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23985336227/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23985336227/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63331452211","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23985336227/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23985336227/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23985336227/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"8e568107bd3ccce8bbf4896d30a50c16b793e633","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill (#398)","timestamp":"2026-04-04T16:23:59Z","author":{"name":"yltx","email":"37734213+yltx@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23982824993,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFlXzeIQ","head_branch":"main","head_sha":"8e568107bd3ccce8bbf4896d30a50c16b793e633","path":".github/workflows/lint.yml","display_title":"ci: add AI issue analysis bot workflow and skill (#398)","run_number":16,"event":"push","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":63324589462,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvnCNlg","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982824993","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23982824993","pull_requests":[],"created_at":"2026-04-04T16:24:02Z","updated_at":"2026-04-04T16:24:26Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T16:24:02Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982824993/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982824993/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63324589462","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982824993/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982824993/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982824993/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"8e568107bd3ccce8bbf4896d30a50c16b793e633","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill (#398)","timestamp":"2026-04-04T16:23:59Z","author":{"name":"yltx","email":"37734213+yltx@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23982820830,"name":"Copilot code review","node_id":"WFR_kwLOHhoLA88AAAAFlXzN3g","head_branch":"refs/pull/398/head","head_sha":"e994212b43653113829747305dfa8a0018c352d7","path":"dynamic/copilot-pull-request-reviewer/copilot-pull-request-reviewer","display_title":"Copilot code review","run_number":17,"event":"dynamic","status":"completed","conclusion":"success","workflow_id":220767798,"check_suite_id":63324579249,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvnBlsQ","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820830","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23982820830","pull_requests":[],"created_at":"2026-04-04T16:23:50Z","updated_at":"2026-04-04T16:26:35Z","actor":{"login":"Copilot","id":175728472,"node_id":"BOT_kgDOCnlnWA","avatar_url":"https://avatars.githubusercontent.com/in/946600?v=4","gravatar_id":"","url":"https://api.github.com/users/Copilot","html_url":"https://github.com/apps/copilot-pull-request-reviewer","followers_url":"https://api.github.com/users/Copilot/followers","following_url":"https://api.github.com/users/Copilot/following{/other_user}","gists_url":"https://api.github.com/users/Copilot/gists{/gist_id}","starred_url":"https://api.github.com/users/Copilot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Copilot/subscriptions","organizations_url":"https://api.github.com/users/Copilot/orgs","repos_url":"https://api.github.com/users/Copilot/repos","events_url":"https://api.github.com/users/Copilot/events{/privacy}","received_events_url":"https://api.github.com/users/Copilot/received_events","type":"Bot","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T16:23:50Z","triggering_actor":{"login":"Copilot","id":175728472,"node_id":"BOT_kgDOCnlnWA","avatar_url":"https://avatars.githubusercontent.com/in/946600?v=4","gravatar_id":"","url":"https://api.github.com/users/Copilot","html_url":"https://github.com/apps/copilot-pull-request-reviewer","followers_url":"https://api.github.com/users/Copilot/followers","following_url":"https://api.github.com/users/Copilot/following{/other_user}","gists_url":"https://api.github.com/users/Copilot/gists{/gist_id}","starred_url":"https://api.github.com/users/Copilot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Copilot/subscriptions","organizations_url":"https://api.github.com/users/Copilot/orgs","repos_url":"https://api.github.com/users/Copilot/repos","events_url":"https://api.github.com/users/Copilot/events{/privacy}","received_events_url":"https://api.github.com/users/Copilot/received_events","type":"Bot","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820830/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820830/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63324579249","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820830/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820830/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820830/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/220767798","head_commit":{"id":"e994212b43653113829747305dfa8a0018c352d7","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill","timestamp":"2026-04-04T16:20:48Z","author":{"name":"yltx","email":"2326439151@qq.com"},"committer":{"name":"yltx","email":"2326439151@qq.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23982820779,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFlXzNqw","head_branch":"yltx/ai-issue-analysis-bot","head_sha":"e994212b43653113829747305dfa8a0018c352d7","path":".github/workflows/lint.yml","display_title":"ci: add AI issue analysis bot workflow and skill","run_number":15,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":63324579166,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvnBlXg","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820779","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23982820779","pull_requests":[],"created_at":"2026-04-04T16:23:49Z","updated_at":"2026-04-04T16:24:16Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T16:23:49Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820779/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820779/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63324579166","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820779/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820779/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982820779/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"e994212b43653113829747305dfa8a0018c352d7","tree_id":"9771f8ad31c04a907fed94274b10c059af83a062","message":"ci: add AI issue analysis bot workflow and skill","timestamp":"2026-04-04T16:20:48Z","author":{"name":"yltx","email":"2326439151@qq.com"},"committer":{"name":"yltx","email":"2326439151@qq.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23982415777,"name":"Copilot code review","node_id":"WFR_kwLOHhoLA88AAAAFlXafoQ","head_branch":"refs/pull/397/head","head_sha":"3b5d45ec8f6155ad7059e18996bd9f9388a6d83c","path":"dynamic/copilot-pull-request-reviewer/copilot-pull-request-reviewer","display_title":"Copilot code review","run_number":16,"event":"dynamic","status":"completed","conclusion":"success","workflow_id":220767798,"check_suite_id":63323476980,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvl-T9A","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415777","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23982415777","pull_requests":[],"created_at":"2026-04-04T16:00:31Z","updated_at":"2026-04-04T16:03:43Z","actor":{"login":"Copilot","id":175728472,"node_id":"BOT_kgDOCnlnWA","avatar_url":"https://avatars.githubusercontent.com/in/946600?v=4","gravatar_id":"","url":"https://api.github.com/users/Copilot","html_url":"https://github.com/apps/copilot-pull-request-reviewer","followers_url":"https://api.github.com/users/Copilot/followers","following_url":"https://api.github.com/users/Copilot/following{/other_user}","gists_url":"https://api.github.com/users/Copilot/gists{/gist_id}","starred_url":"https://api.github.com/users/Copilot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Copilot/subscriptions","organizations_url":"https://api.github.com/users/Copilot/orgs","repos_url":"https://api.github.com/users/Copilot/repos","events_url":"https://api.github.com/users/Copilot/events{/privacy}","received_events_url":"https://api.github.com/users/Copilot/received_events","type":"Bot","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T16:00:31Z","triggering_actor":{"login":"Copilot","id":175728472,"node_id":"BOT_kgDOCnlnWA","avatar_url":"https://avatars.githubusercontent.com/in/946600?v=4","gravatar_id":"","url":"https://api.github.com/users/Copilot","html_url":"https://github.com/apps/copilot-pull-request-reviewer","followers_url":"https://api.github.com/users/Copilot/followers","following_url":"https://api.github.com/users/Copilot/following{/other_user}","gists_url":"https://api.github.com/users/Copilot/gists{/gist_id}","starred_url":"https://api.github.com/users/Copilot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Copilot/subscriptions","organizations_url":"https://api.github.com/users/Copilot/orgs","repos_url":"https://api.github.com/users/Copilot/repos","events_url":"https://api.github.com/users/Copilot/events{/privacy}","received_events_url":"https://api.github.com/users/Copilot/received_events","type":"Bot","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415777/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415777/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63323476980","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415777/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415777/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415777/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/220767798","head_commit":{"id":"3b5d45ec8f6155ad7059e18996bd9f9388a6d83c","tree_id":"fffe0658788dd1a57ad15482ec87ce0de6c0564d","message":"fix(server): forward plan fleet overrides for task routes","timestamp":"2026-04-04T15:58:59Z","author":{"name":"yltx","email":"2326439151@qq.com"},"committer":{"name":"yltx","email":"2326439151@qq.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23982415664,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFlXafMA","head_branch":"yltx/web-fleet-override","head_sha":"3b5d45ec8f6155ad7059e18996bd9f9388a6d83c","path":".github/workflows/lint.yml","display_title":"fix(server): forward plan fleet overrides for task routes","run_number":14,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":63323476662,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvl-Stg","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415664","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23982415664","pull_requests":[{"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls/397","id":3489133143,"number":397,"head":{"ref":"yltx/web-fleet-override","sha":"3b5d45ec8f6155ad7059e18996bd9f9388a6d83c","repo":{"id":505023235,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","name":"AutoWSGR"}},"base":{"ref":"main","sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","repo":{"id":505023235,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","name":"AutoWSGR"}}}],"created_at":"2026-04-04T16:00:31Z","updated_at":"2026-04-04T16:00:59Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T16:00:31Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415664/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415664/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63323476662","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415664/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415664/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23982415664/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"3b5d45ec8f6155ad7059e18996bd9f9388a6d83c","tree_id":"fffe0658788dd1a57ad15482ec87ce0de6c0564d","message":"fix(server): forward plan fleet overrides for task routes","timestamp":"2026-04-04T15:58:59Z","author":{"name":"yltx","email":"2326439151@qq.com"},"committer":{"name":"yltx","email":"2326439151@qq.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23979515051,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFlUpcqw","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":102,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63315604070,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvedyZg","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23979515051","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23979515051","pull_requests":[],"created_at":"2026-04-04T13:07:10Z","updated_at":"2026-04-04T13:07:12Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T13:07:10Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23979515051/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23979515051/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63315604070","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23979515051/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23979515051/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23979515051/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23973883346,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFlPRt0g","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":101,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63300865246,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvQaM3g","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23973883346","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23973883346","pull_requests":[],"created_at":"2026-04-04T07:07:54Z","updated_at":"2026-04-04T07:07:55Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T07:07:54Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23973883346/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23973883346/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63300865246","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23973883346/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23973883346/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23973883346/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23969581474,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFlLLJog","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":100,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63289590630,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOvFqDZg","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23969581474","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23969581474","pull_requests":[],"created_at":"2026-04-04T02:39:07Z","updated_at":"2026-04-04T02:39:08Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-04T02:39:07Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23969581474/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23969581474/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63289590630","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23969581474/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23969581474/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23969581474/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23958205726,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFlAU1Hg","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":99,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63256920847,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOumgDDw","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23958205726","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23958205726","pull_requests":[],"created_at":"2026-04-03T18:55:21Z","updated_at":"2026-04-03T18:55:22Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-03T18:55:21Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23958205726/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23958205726/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63256920847","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23958205726/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23958205726/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23958205726/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23947464491,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFk2FPKw","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":98,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63223243938,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOuGYkog","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23947464491","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23947464491","pull_requests":[],"created_at":"2026-04-03T13:15:08Z","updated_at":"2026-04-03T13:15:09Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-03T13:15:08Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23947464491/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23947464491/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63223243938","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23947464491/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23947464491/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23947464491/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23937894897,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFks9J8Q","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":97,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63194817144,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOtrRieA","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23937894897","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23937894897","pull_requests":[],"created_at":"2026-04-03T07:17:28Z","updated_at":"2026-04-03T07:17:29Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-03T07:17:28Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23937894897/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23937894897/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63194817144","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23937894897/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23937894897/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23937894897/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23931542572,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFkm5cLA","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":96,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63177894139,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOtbIo-w","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23931542572","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23931542572","pull_requests":[],"created_at":"2026-04-03T02:48:52Z","updated_at":"2026-04-03T02:48:53Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-03T02:48:52Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23931542572/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23931542572/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63177894139","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23931542572/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23931542572/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23931542572/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23917157522,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFkZLckg","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":95,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63135940764,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOszIAnA","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23917157522","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23917157522","pull_requests":[],"created_at":"2026-04-02T19:03:25Z","updated_at":"2026-04-02T19:03:26Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-02T19:03:25Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23917157522/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23917157522/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63135940764","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23917157522/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23917157522/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23917157522/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23902717157,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFkLaE5Q","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":94,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63089076243,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOsGboEw","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23902717157","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23902717157","pull_requests":[],"created_at":"2026-04-02T13:26:34Z","updated_at":"2026-04-02T13:26:35Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-02T13:26:34Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23902717157/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23902717157/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63089076243","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23902717157/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23902717157/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23902717157/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23892656683,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFkB0CKw","head_branch":"main","head_sha":"77e0a80d72158d1a56c70237a2c12b32b237b383","path":".github/workflows/lint.yml","display_title":"fix: harden page recovery and chapter navigation (#395)","run_number":13,"event":"push","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":63057918939,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOrot72w","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23892656683","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23892656683","pull_requests":[],"created_at":"2026-04-02T08:58:58Z","updated_at":"2026-04-02T08:59:25Z","actor":{"login":"huan-yp","id":85162020,"node_id":"MDQ6VXNlcjg1MTYyMDIw","avatar_url":"https://avatars.githubusercontent.com/u/85162020?v=4","gravatar_id":"","url":"https://api.github.com/users/huan-yp","html_url":"https://github.com/huan-yp","followers_url":"https://api.github.com/users/huan-yp/followers","following_url":"https://api.github.com/users/huan-yp/following{/other_user}","gists_url":"https://api.github.com/users/huan-yp/gists{/gist_id}","starred_url":"https://api.github.com/users/huan-yp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/huan-yp/subscriptions","organizations_url":"https://api.github.com/users/huan-yp/orgs","repos_url":"https://api.github.com/users/huan-yp/repos","events_url":"https://api.github.com/users/huan-yp/events{/privacy}","received_events_url":"https://api.github.com/users/huan-yp/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-02T08:58:58Z","triggering_actor":{"login":"huan-yp","id":85162020,"node_id":"MDQ6VXNlcjg1MTYyMDIw","avatar_url":"https://avatars.githubusercontent.com/u/85162020?v=4","gravatar_id":"","url":"https://api.github.com/users/huan-yp","html_url":"https://github.com/huan-yp","followers_url":"https://api.github.com/users/huan-yp/followers","following_url":"https://api.github.com/users/huan-yp/following{/other_user}","gists_url":"https://api.github.com/users/huan-yp/gists{/gist_id}","starred_url":"https://api.github.com/users/huan-yp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/huan-yp/subscriptions","organizations_url":"https://api.github.com/users/huan-yp/orgs","repos_url":"https://api.github.com/users/huan-yp/repos","events_url":"https://api.github.com/users/huan-yp/events{/privacy}","received_events_url":"https://api.github.com/users/huan-yp/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23892656683/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23892656683/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63057918939","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23892656683/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23892656683/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23892656683/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"77e0a80d72158d1a56c70237a2c12b32b237b383","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation (#395)","timestamp":"2026-04-02T08:58:55Z","author":{"name":"syokounya","email":"93929783+syokounya@users.noreply.github.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23891911883,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFkBGkyw","head_branch":"main","head_sha":"5c276f1f1d9239a577e9dfffb54a5042b844307f","path":".github/workflows/lint.yml","display_title":"fix: ǿҳָ½ڵȶ ","run_number":12,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":63055673298,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOrmk30g","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23891911883","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23891911883","pull_requests":[],"created_at":"2026-04-02T08:39:25Z","updated_at":"2026-04-02T08:39:54Z","actor":{"login":"syokounya","id":93929783,"node_id":"U_kgDOBZlBNw","avatar_url":"https://avatars.githubusercontent.com/u/93929783?v=4","gravatar_id":"","url":"https://api.github.com/users/syokounya","html_url":"https://github.com/syokounya","followers_url":"https://api.github.com/users/syokounya/followers","following_url":"https://api.github.com/users/syokounya/following{/other_user}","gists_url":"https://api.github.com/users/syokounya/gists{/gist_id}","starred_url":"https://api.github.com/users/syokounya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syokounya/subscriptions","organizations_url":"https://api.github.com/users/syokounya/orgs","repos_url":"https://api.github.com/users/syokounya/repos","events_url":"https://api.github.com/users/syokounya/events{/privacy}","received_events_url":"https://api.github.com/users/syokounya/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-02T08:39:25Z","triggering_actor":{"login":"syokounya","id":93929783,"node_id":"U_kgDOBZlBNw","avatar_url":"https://avatars.githubusercontent.com/u/93929783?v=4","gravatar_id":"","url":"https://api.github.com/users/syokounya","html_url":"https://github.com/syokounya","followers_url":"https://api.github.com/users/syokounya/followers","following_url":"https://api.github.com/users/syokounya/following{/other_user}","gists_url":"https://api.github.com/users/syokounya/gists{/gist_id}","starred_url":"https://api.github.com/users/syokounya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syokounya/subscriptions","organizations_url":"https://api.github.com/users/syokounya/orgs","repos_url":"https://api.github.com/users/syokounya/repos","events_url":"https://api.github.com/users/syokounya/events{/privacy}","received_events_url":"https://api.github.com/users/syokounya/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23891911883/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23891911883/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63055673298","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23891911883/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23891911883/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23891911883/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"5c276f1f1d9239a577e9dfffb54a5042b844307f","tree_id":"2998c9e90d00effc80e88e6562fbea42d20f1c7a","message":"fix: harden page recovery and chapter navigation","timestamp":"2026-04-02T03:36:20Z","author":{"name":"syokounya","email":"syokounya0828@gmail.com"},"committer":{"name":"syokounya","email":"syokounya0828@gmail.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":1179848710,"node_id":"R_kgDORlMQBg","name":"AutoWSGR","full_name":"syokounya/AutoWSGR","private":false,"owner":{"login":"syokounya","id":93929783,"node_id":"U_kgDOBZlBNw","avatar_url":"https://avatars.githubusercontent.com/u/93929783?v=4","gravatar_id":"","url":"https://api.github.com/users/syokounya","html_url":"https://github.com/syokounya","followers_url":"https://api.github.com/users/syokounya/followers","following_url":"https://api.github.com/users/syokounya/following{/other_user}","gists_url":"https://api.github.com/users/syokounya/gists{/gist_id}","starred_url":"https://api.github.com/users/syokounya/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syokounya/subscriptions","organizations_url":"https://api.github.com/users/syokounya/orgs","repos_url":"https://api.github.com/users/syokounya/repos","events_url":"https://api.github.com/users/syokounya/events{/privacy}","received_events_url":"https://api.github.com/users/syokounya/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/syokounya/AutoWSGR","description":"սŮRȫͰ","fork":true,"url":"https://api.github.com/repos/syokounya/AutoWSGR","forks_url":"https://api.github.com/repos/syokounya/AutoWSGR/forks","keys_url":"https://api.github.com/repos/syokounya/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/syokounya/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/syokounya/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/syokounya/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/syokounya/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/syokounya/AutoWSGR/events","assignees_url":"https://api.github.com/repos/syokounya/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/syokounya/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/syokounya/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/syokounya/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/syokounya/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/syokounya/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/syokounya/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/syokounya/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/syokounya/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/syokounya/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/syokounya/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/syokounya/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/syokounya/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/syokounya/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/syokounya/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/syokounya/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/syokounya/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/syokounya/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/syokounya/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/syokounya/AutoWSGR/merges","archive_url":"https://api.github.com/repos/syokounya/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/syokounya/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/syokounya/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/syokounya/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/syokounya/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/syokounya/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/syokounya/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/syokounya/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/syokounya/AutoWSGR/deployments"}},{"id":23889006308,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFj-VO5A","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":93,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63047307737,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOremR2Q","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23889006308","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23889006308","pull_requests":[],"created_at":"2026-04-02T07:21:00Z","updated_at":"2026-04-02T07:21:01Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-02T07:21:00Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23889006308/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23889006308/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63047307737","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23889006308/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23889006308/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23889006308/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23881218529,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFj2554Q","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":92,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":63026316384,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOrKlEYA","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23881218529","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23881218529","pull_requests":[],"created_at":"2026-04-02T02:46:08Z","updated_at":"2026-04-02T02:46:09Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-02T02:46:08Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23881218529/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23881218529/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/63026316384","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23881218529/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23881218529/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23881218529/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23866101683,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFjofPsw","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":91,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":62981311854,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOqfqNbg","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23866101683","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23866101683","pull_requests":[],"created_at":"2026-04-01T19:10:33Z","updated_at":"2026-04-01T19:10:34Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-01T19:10:33Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23866101683/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23866101683/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62981311854","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23866101683/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23866101683/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23866101683/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23851946501,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFja_SBQ","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":90,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":62934235612,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOpyw53A","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23851946501","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23851946501","pull_requests":[],"created_at":"2026-04-01T13:47:53Z","updated_at":"2026-04-01T13:47:54Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-01T13:47:53Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23851946501/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23851946501/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62934235612","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23851946501/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23851946501/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23851946501/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23837688033,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFjNZA4Q","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":89,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":62889571829,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOpIK19Q","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23837688033","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23837688033","pull_requests":[],"created_at":"2026-04-01T07:42:07Z","updated_at":"2026-04-01T07:42:08Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-01T07:42:07Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23837688033/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23837688033/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62889571829","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23837688033/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23837688033/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23837688033/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23830299495,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFjGWDZw","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":88,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":62868399833,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOoz-m2Q","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23830299495","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23830299495","pull_requests":[],"created_at":"2026-04-01T03:20:04Z","updated_at":"2026-04-01T03:20:04Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-04-01T03:20:04Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23830299495/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23830299495/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62868399833","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23830299495/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23830299495/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23830299495/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23814811476,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFi3kvVA","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":87,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":62821967453,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOoHsmXQ","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23814811476","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23814811476","pull_requests":[],"created_at":"2026-03-31T19:09:17Z","updated_at":"2026-03-31T19:09:19Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-03-31T19:09:17Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23814811476/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23814811476/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62821967453","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23814811476/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23814811476/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23814811476/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23800635818,"name":"Sync upstream","node_id":"WFR_kwLOHhoLA88AAAAFiqDhqg","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/sync-upstream.yml","display_title":"Sync upstream","run_number":86,"event":"schedule","status":"completed","conclusion":"skipped","workflow_id":244437590,"check_suite_id":62775432391,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOnbUUxw","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23800635818","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23800635818","pull_requests":[],"created_at":"2026-03-31T13:45:28Z","updated_at":"2026-03-31T13:45:29Z","actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-03-31T13:45:28Z","triggering_actor":{"login":"yltx","id":37734213,"node_id":"MDQ6VXNlcjM3NzM0MjEz","avatar_url":"https://avatars.githubusercontent.com/u/37734213?v=4","gravatar_id":"","url":"https://api.github.com/users/yltx","html_url":"https://github.com/yltx","followers_url":"https://api.github.com/users/yltx/followers","following_url":"https://api.github.com/users/yltx/following{/other_user}","gists_url":"https://api.github.com/users/yltx/gists{/gist_id}","starred_url":"https://api.github.com/users/yltx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/yltx/subscriptions","organizations_url":"https://api.github.com/users/yltx/orgs","repos_url":"https://api.github.com/users/yltx/repos","events_url":"https://api.github.com/users/yltx/events{/privacy}","received_events_url":"https://api.github.com/users/yltx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23800635818/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23800635818/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62775432391","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23800635818/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23800635818/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23800635818/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/244437590","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23791041751,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFig581w","head_branch":"main","head_sha":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","path":".github/workflows/lint.yml","display_title":"feat: quick repair retry (#394)","run_number":11,"event":"push","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":62744802505,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOm-G0yQ","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23791041751","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23791041751","pull_requests":[],"created_at":"2026-03-31T09:45:40Z","updated_at":"2026-03-31T09:46:06Z","actor":{"login":"kuainx","id":22209650,"node_id":"MDQ6VXNlcjIyMjA5NjUw","avatar_url":"https://avatars.githubusercontent.com/u/22209650?v=4","gravatar_id":"","url":"https://api.github.com/users/kuainx","html_url":"https://github.com/kuainx","followers_url":"https://api.github.com/users/kuainx/followers","following_url":"https://api.github.com/users/kuainx/following{/other_user}","gists_url":"https://api.github.com/users/kuainx/gists{/gist_id}","starred_url":"https://api.github.com/users/kuainx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kuainx/subscriptions","organizations_url":"https://api.github.com/users/kuainx/orgs","repos_url":"https://api.github.com/users/kuainx/repos","events_url":"https://api.github.com/users/kuainx/events{/privacy}","received_events_url":"https://api.github.com/users/kuainx/received_events","type":"User","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-03-31T09:45:40Z","triggering_actor":{"login":"kuainx","id":22209650,"node_id":"MDQ6VXNlcjIyMjA5NjUw","avatar_url":"https://avatars.githubusercontent.com/u/22209650?v=4","gravatar_id":"","url":"https://api.github.com/users/kuainx","html_url":"https://github.com/kuainx","followers_url":"https://api.github.com/users/kuainx/followers","following_url":"https://api.github.com/users/kuainx/following{/other_user}","gists_url":"https://api.github.com/users/kuainx/gists{/gist_id}","starred_url":"https://api.github.com/users/kuainx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kuainx/subscriptions","organizations_url":"https://api.github.com/users/kuainx/orgs","repos_url":"https://api.github.com/users/kuainx/repos","events_url":"https://api.github.com/users/kuainx/events{/privacy}","received_events_url":"https://api.github.com/users/kuainx/received_events","type":"User","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23791041751/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23791041751/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62744802505","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23791041751/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23791041751/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23791041751/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"1b9fb1ec4ed2236b1f50ca235cb760c761a8bdb4","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"feat: quick repair retry (#394)\n\n* quick repair retry\n\n* [pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci\n\n---------\n\nCo-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>","timestamp":"2026-03-31T09:45:37Z","author":{"name":"KUAI","email":"ekuai@foxmail.com"},"committer":{"name":"GitHub","email":"noreply@github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"}},{"id":23790790019,"name":"Lint","node_id":"WFR_kwLOHhoLA88AAAAFigqlgw","head_branch":"quick-repair","head_sha":"daf3afb0c5dafe933e5512d26e80535219a21509","path":".github/workflows/lint.yml","display_title":"feat: quick repair retry","run_number":10,"event":"pull_request","status":"completed","conclusion":"success","workflow_id":253260226,"check_suite_id":62744035802,"check_suite_node_id":"CS_kwDOHhoLA88AAAAOm9YB2g","url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23790790019","html_url":"https://github.com/OpenWSGR/AutoWSGR/actions/runs/23790790019","pull_requests":[],"created_at":"2026-03-31T09:39:30Z","updated_at":"2026-03-31T09:39:58Z","actor":{"login":"pre-commit-ci[bot]","id":66853113,"node_id":"MDM6Qm90NjY4NTMxMTM=","avatar_url":"https://avatars.githubusercontent.com/in/68672?v=4","gravatar_id":"","url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D","html_url":"https://github.com/apps/pre-commit-ci","followers_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/followers","following_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/repos","events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"run_attempt":1,"referenced_workflows":[],"run_started_at":"2026-03-31T09:39:30Z","triggering_actor":{"login":"pre-commit-ci[bot]","id":66853113,"node_id":"MDM6Qm90NjY4NTMxMTM=","avatar_url":"https://avatars.githubusercontent.com/in/68672?v=4","gravatar_id":"","url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D","html_url":"https://github.com/apps/pre-commit-ci","followers_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/followers","following_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/following{/other_user}","gists_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/gists{/gist_id}","starred_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/subscriptions","organizations_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/orgs","repos_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/repos","events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/events{/privacy}","received_events_url":"https://api.github.com/users/pre-commit-ci%5Bbot%5D/received_events","type":"Bot","user_view_type":"public","site_admin":false},"jobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23790790019/jobs","logs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23790790019/logs","check_suite_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/check-suites/62744035802","artifacts_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23790790019/artifacts","cancel_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23790790019/cancel","rerun_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/runs/23790790019/rerun","previous_attempt_url":null,"workflow_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/actions/workflows/253260226","head_commit":{"id":"daf3afb0c5dafe933e5512d26e80535219a21509","tree_id":"57d2c142267649a540cdb46fd868e39b57bfb5a2","message":"[pre-commit.ci] auto fixes from pre-commit.com hooks\n\nfor more information, see https://pre-commit.ci","timestamp":"2026-03-31T09:39:17Z","author":{"name":"pre-commit-ci[bot]","email":"66853113+pre-commit-ci[bot]@users.noreply.github.com"},"committer":{"name":"pre-commit-ci[bot]","email":"66853113+pre-commit-ci[bot]@users.noreply.github.com"}},"repository":{"id":505023235,"node_id":"R_kgDOHhoLAw","name":"AutoWSGR","full_name":"OpenWSGR/AutoWSGR","private":false,"owner":{"login":"OpenWSGR","id":186071259,"node_id":"O_kgDOCxc42w","avatar_url":"https://avatars.githubusercontent.com/u/186071259?v=4","gravatar_id":"","url":"https://api.github.com/users/OpenWSGR","html_url":"https://github.com/OpenWSGR","followers_url":"https://api.github.com/users/OpenWSGR/followers","following_url":"https://api.github.com/users/OpenWSGR/following{/other_user}","gists_url":"https://api.github.com/users/OpenWSGR/gists{/gist_id}","starred_url":"https://api.github.com/users/OpenWSGR/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OpenWSGR/subscriptions","organizations_url":"https://api.github.com/users/OpenWSGR/orgs","repos_url":"https://api.github.com/users/OpenWSGR/repos","events_url":"https://api.github.com/users/OpenWSGR/events{/privacy}","received_events_url":"https://api.github.com/users/OpenWSGR/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/OpenWSGR/AutoWSGR","description":"սŮRȫͰ","fork":false,"url":"https://api.github.com/repos/OpenWSGR/AutoWSGR","forks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/forks","keys_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/events","assignees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/merges","archive_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/OpenWSGR/AutoWSGR/deployments"},"head_repository":{"id":917691832,"node_id":"R_kgDONrLduA","name":"AutoWSGR","full_name":"kuainx/AutoWSGR","private":false,"owner":{"login":"kuainx","id":22209650,"node_id":"MDQ6VXNlcjIyMjA5NjUw","avatar_url":"https://avatars.githubusercontent.com/u/22209650?v=4","gravatar_id":"","url":"https://api.github.com/users/kuainx","html_url":"https://github.com/kuainx","followers_url":"https://api.github.com/users/kuainx/followers","following_url":"https://api.github.com/users/kuainx/following{/other_user}","gists_url":"https://api.github.com/users/kuainx/gists{/gist_id}","starred_url":"https://api.github.com/users/kuainx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kuainx/subscriptions","organizations_url":"https://api.github.com/users/kuainx/orgs","repos_url":"https://api.github.com/users/kuainx/repos","events_url":"https://api.github.com/users/kuainx/events{/privacy}","received_events_url":"https://api.github.com/users/kuainx/received_events","type":"User","user_view_type":"public","site_admin":false},"html_url":"https://github.com/kuainx/AutoWSGR","description":"սŮRȫͰ","fork":true,"url":"https://api.github.com/repos/kuainx/AutoWSGR","forks_url":"https://api.github.com/repos/kuainx/AutoWSGR/forks","keys_url":"https://api.github.com/repos/kuainx/AutoWSGR/keys{/key_id}","collaborators_url":"https://api.github.com/repos/kuainx/AutoWSGR/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/kuainx/AutoWSGR/teams","hooks_url":"https://api.github.com/repos/kuainx/AutoWSGR/hooks","issue_events_url":"https://api.github.com/repos/kuainx/AutoWSGR/issues/events{/number}","events_url":"https://api.github.com/repos/kuainx/AutoWSGR/events","assignees_url":"https://api.github.com/repos/kuainx/AutoWSGR/assignees{/user}","branches_url":"https://api.github.com/repos/kuainx/AutoWSGR/branches{/branch}","tags_url":"https://api.github.com/repos/kuainx/AutoWSGR/tags","blobs_url":"https://api.github.com/repos/kuainx/AutoWSGR/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/kuainx/AutoWSGR/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/kuainx/AutoWSGR/git/refs{/sha}","trees_url":"https://api.github.com/repos/kuainx/AutoWSGR/git/trees{/sha}","statuses_url":"https://api.github.com/repos/kuainx/AutoWSGR/statuses/{sha}","languages_url":"https://api.github.com/repos/kuainx/AutoWSGR/languages","stargazers_url":"https://api.github.com/repos/kuainx/AutoWSGR/stargazers","contributors_url":"https://api.github.com/repos/kuainx/AutoWSGR/contributors","subscribers_url":"https://api.github.com/repos/kuainx/AutoWSGR/subscribers","subscription_url":"https://api.github.com/repos/kuainx/AutoWSGR/subscription","commits_url":"https://api.github.com/repos/kuainx/AutoWSGR/commits{/sha}","git_commits_url":"https://api.github.com/repos/kuainx/AutoWSGR/git/commits{/sha}","comments_url":"https://api.github.com/repos/kuainx/AutoWSGR/comments{/number}","issue_comment_url":"https://api.github.com/repos/kuainx/AutoWSGR/issues/comments{/number}","contents_url":"https://api.github.com/repos/kuainx/AutoWSGR/contents/{+path}","compare_url":"https://api.github.com/repos/kuainx/AutoWSGR/compare/{base}...{head}","merges_url":"https://api.github.com/repos/kuainx/AutoWSGR/merges","archive_url":"https://api.github.com/repos/kuainx/AutoWSGR/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/kuainx/AutoWSGR/downloads","issues_url":"https://api.github.com/repos/kuainx/AutoWSGR/issues{/number}","pulls_url":"https://api.github.com/repos/kuainx/AutoWSGR/pulls{/number}","milestones_url":"https://api.github.com/repos/kuainx/AutoWSGR/milestones{/number}","notifications_url":"https://api.github.com/repos/kuainx/AutoWSGR/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/kuainx/AutoWSGR/labels{/name}","releases_url":"https://api.github.com/repos/kuainx/AutoWSGR/releases{/id}","deployments_url":"https://api.github.com/repos/kuainx/AutoWSGR/deployments"}}]} - diff --git a/.tmp_weekly_fix.ps1 b/.tmp_weekly_fix.ps1 deleted file mode 100644 index 433231a..0000000 --- a/.tmp_weekly_fix.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -$utf8 = New-Object System.Text.UTF8Encoding($false) -function Get-Indent([string]$s){ $s.Length - $s.TrimStart(' ').Length } -function Is-PureString([string[]]$b){ - if($b.Count -ne 1){return $false} - if($b[0] -notmatch '^(\s*)-\s*(.*)$'){return $false} - $r=$matches[2].Trim(); if(!$r -or $r.StartsWith('#')){return $false} - if($r -match '^\{.*\}$' -or $r -match '^\[.*\]$' -or $r -match '^[^#]+:\s*'){return $false} - return $true -} -function Split-ItemBlocks([string[]]$lines,[int]$start,[int]$shipsIndent){ - $line=$lines[$start] - $line -match '^(\s*)-\s*(.*)$' | Out-Null - $itemIndent=$matches[1].Length - $block=New-Object System.Collections.Generic.List[string]; $block.Add($line) - $j=$start+1 - while($j -lt $lines.Length){ - $nl=$lines[$j]; $nt=$nl.Trim() - if($nt -ne '' -and -not $nl.TrimStart().StartsWith('#')){ - $nind=Get-Indent $nl - if($nind -le $shipsIndent){break} - if($nind -eq $itemIndent -and $nl -match '^\s*-\s*'){break} - } - $block.Add($nl); $j++ - } - return @($block,$j,$itemIndent) -} -$files=Get-ChildItem resource/builtin_plans -Filter '周常*.yaml' -File | Sort-Object Name -$rows=@() -foreach($f in $files){ - $lines=[IO.File]::ReadAllLines($f.FullName,[Text.Encoding]::UTF8) - $out=New-Object System.Collections.Generic.List[string] - $inFleet=$false;$fleetIndent=-1;$inShips=$false;$shipsIndent=-1 - $nameConverted=0;$shipTypeMinAdded=0;$unchanged=0 - for($i=0;$i -lt $lines.Length;){ - $line=$lines[$i];$t=$line.Trim() - if($inShips -and $t -ne '' -and -not $line.TrimStart().StartsWith('#') -and (Get-Indent $line) -le $shipsIndent){$inShips=$false} - if($inFleet -and $t -ne '' -and -not $line.TrimStart().StartsWith('#') -and (Get-Indent $line) -le $fleetIndent -and $line -notmatch '^\s*fleet_presets:\s*$'){$inFleet=$false;$inShips=$false} - if($line -match '^(\s*)fleet_presets:\s*$'){ $inFleet=$true;$fleetIndent=$matches[1].Length; $out.Add($line);$i++; continue } - if($inFleet -and $line -match '^(\s*)ships:\s*$'){ $inShips=$true;$shipsIndent=$matches[1].Length; $out.Add($line);$i++; continue } - if($inShips -and $line -match '^(\s*)-\s*(.*)$' -and $matches[1].Length -gt $shipsIndent){ - $res=Split-ItemBlocks $lines $i $shipsIndent; $block=[string[]]$res[0]; $j=[int]$res[1]; $itemIndent=[int]$res[2] - $changed=$false - if(Is-PureString $block){ - $block[0] -match '^(\s*)-\s*(.*)$' | Out-Null; $ws=$matches[1]; $rest=$matches[2].Trim(); $comment='' - if($rest -match '^(.*?)(\s+#.*)$'){ $rest=$matches[1].TrimEnd(); $comment=$matches[2] } - $out.Add("$ws- { name: $rest, min_level: 100 }$comment"); $nameConverted++; $changed=$true - } else { - $hasType=($block|Where-Object{$_ -match '\bship_type\s*:'}).Count -gt 0 - $hasMin=($block|Where-Object{$_ -match '\bmin_level\s*:'}).Count -gt 0 - if($hasType -and -not $hasMin){ - $first=$block[0] - if($block.Count -eq 1 -and $first -match '^(\s*)-\s*(.*)$'){ - $ws=$matches[1]; $body=$matches[2].Trim(); $comment='' - if($body -match '^(.*?)(\s+#.*)$'){ $body=$matches[1].TrimEnd(); $comment=$matches[2] } - if($body -match '^\{(.*)\}$'){ $inner=$matches[1].Trim(); $newBody = if($inner){"{ $inner, min_level: 100 }"}else{'{ min_level: 100 }'}; $out.Add("$ws- $newBody$comment") } - else { $out.Add($first); $out.Add((' ' * ($itemIndent+2)) + 'min_level: 100') } - } else { foreach($b in $block){$out.Add($b)}; $out.Add((' ' * ($itemIndent+2)) + 'min_level: 100') } - $shipTypeMinAdded++; $changed=$true - } - } - if(-not $changed){ foreach($b in $block){$out.Add($b)}; $unchanged++ } - $i=$j; continue - } - $out.Add($line); $i++ - } - [IO.File]::WriteAllLines($f.FullName,$out,$utf8) - $rows += [pscustomobject]@{File=$f.Name;nameConverted=$nameConverted;shipTypeMinAdded=$shipTypeMinAdded;unchanged=$unchanged} -} -$rows | Format-Table -AutoSize - -$pure=0;$missing=0 -foreach($f in $files){ - $lines=[IO.File]::ReadAllLines($f.FullName,[Text.Encoding]::UTF8) - $inFleet=$false;$fleetIndent=-1;$inShips=$false;$shipsIndent=-1 - for($i=0;$i -lt $lines.Length;){ - $line=$lines[$i];$t=$line.Trim() - if($inShips -and $t -ne '' -and -not $line.TrimStart().StartsWith('#') -and (Get-Indent $line) -le $shipsIndent){$inShips=$false} - if($inFleet -and $t -ne '' -and -not $line.TrimStart().StartsWith('#') -and (Get-Indent $line) -le $fleetIndent -and $line -notmatch '^\s*fleet_presets:\s*$'){$inFleet=$false;$inShips=$false} - if($line -match '^(\s*)fleet_presets:\s*$'){ $inFleet=$true;$fleetIndent=$matches[1].Length; $i++; continue } - if($inFleet -and $line -match '^(\s*)ships:\s*$'){ $inShips=$true;$shipsIndent=$matches[1].Length; $i++; continue } - if($inShips -and $line -match '^(\s*)-\s*(.*)$' -and $matches[1].Length -gt $shipsIndent){ - $res=Split-ItemBlocks $lines $i $shipsIndent; $block=[string[]]$res[0]; $j=[int]$res[1] - if(Is-PureString $block){$pure++} - $hasType=($block|Where-Object{$_ -match '\bship_type\s*:'}).Count -gt 0 - $hasMin=($block|Where-Object{$_ -match '\bmin_level\s*:'}).Count -gt 0 - if($hasType -and -not $hasMin){$missing++} - $i=$j; continue - } - $i++ - } -} -"pureStringRemain=$pure shipTypeNoMinRemain=$missing" -if($pure -eq 0 -and $missing -eq 0){ 'WEEKLY_SHIPS_LEVEL_RULES_OK' } diff --git a/.trae/rules/project-constraints.md b/.trae/rules/project-constraints.md new file mode 100644 index 0000000..1447886 --- /dev/null +++ b/.trae/rules/project-constraints.md @@ -0,0 +1,9 @@ +--- +alwaysApply: true +--- + +# AutoWSGR-GUI 项目入口 + +在分析、修改文件或执行命令前,必须完整读取并遵守根目录 `AGENTS.md`。 + +必须按照 `AGENTS.md` 规定的顺序继续读取 `docs/engineering-standards.md`、可执行配置、`CONTRIBUTING.md` 和任务相关架构文档。任何摘要均不得替代强制规范原文。 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a0ff980 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,397 @@ +# AutoWSGR-GUI Agent 项目约束 + +本文件是所有 AI 编码 Agent 进入本仓库时的统一入口。它负责规定阅读顺序和执行协议,不替代详细规范,也不得削弱详细规范中的任何要求。 + +适用范围包括人工辅助 Agent、自动化 Agent、代码生成工具、代码审查工具和 Issue 分析工具。 + +## 0. 首次接入协议 + +开始分析、修改文件或执行可能改变仓库状态的命令前,Agent 必须: + +1. 执行 `git status --short --branch`,确认当前分支、worktree 和未提交修改。 +2. 完整读取 `docs/engineering-standards.md`,不得只读取摘要、标题或局部章节。 +3. 读取 `.editorconfig`、`.gitattributes`、`tsconfig.json` 和 `package.json`。 +4. 读取 `CONTRIBUTING.md`。 +5. 根据任务影响范围,读取第 2 节列出的相关架构文档和现有测试。 +6. 搜索现有实现、相同领域规则、历史 workaround 和相关数据契约。 +7. 在修改前明确行为目标、非目标、状态所有者、受影响架构层和验证方式。 + +如果无法读取强制规范,或者无法确认当前工作树是否包含他人修改,Agent 必须停止写入并报告阻塞原因。 + +## 1. 强制性规则 + +### 1.1 完整保留原则 + +`docs/engineering-standards.md` 是强制工程规则的唯一正文来源,其中全部规则均原样生效。 + +- `必须 / 不得(MUST / MUST NOT)` 是合并门槛。 +- `应当 / 不应(SHOULD / SHOULD NOT)` 默认必须遵守;偏离时必须在 PR 中说明理由。 +- 本文件中的概述不能替代原文。 +- Agent 不得通过摘要、改写、选择性引用或工具限制忽略原文中的任何规则。 +- 修改强制规范时,必须保留原有规则的完整语义;删除、降级或豁免必须经过维护者书面批准。 + +### 1.2 权威顺序 + +发生冲突时,严格按照以下顺序处理: + +1. `package.json`、`tsconfig.json`、`.editorconfig`、`.gitattributes` 等可执行配置。 +2. `docs/engineering-standards.md`。 +3. `docs/architecture/` 中描述当前系统结构的文档。 +4. `CONTRIBUTING.md`。 +5. `docs/teaching/` 中的历史重构教学。 + +`AGENTS.md` 和各工具入口文件只负责加载上述规则,不形成更高优先级,也不能覆盖它们。 + +### 1.3 强制规则范围 + +Agent 必须完整执行 `docs/engineering-standards.md` 的全部章节,包括但不限于: + +1. 文档权威顺序和现有技术债务处理。 +2. 正确性证据、单一状态所有者、根因修复和可回滚行为单元。 +3. MVC + ViewObject 架构边界,以及 View、Controller、Model、Types、Electron 和 Python 后端职责。 +4. 公共数据契约、版本化迁移、用户目录、原子写入和 Electron 文件安全。 +5. Python 环境一致性、后端进程树和更新生命周期。 +6. Patch 失败计数、止损信号、L0-L4 升级和第三次修复门槛。 +7. 干净基线重写、行为契约、旧代码复用清单和垂直切片。 +8. AI Agent 修改前、修改中、强制暂停和交付记录要求。 +9. PR 范围、提交粒度、Conventional Commits 和规模审查。 +10. 构建、确定性验证、测试原则和文档同步。 +11. 临时 containment 的例外条件、批准和移除要求。 +12. 合并前审查清单。 + +### 1.4 不得弱化的核心约束 + +- 每份可变数据只能有一个权威所有者;不得用标志、影子集合或重复缓存掩盖所有权问题。 +- 标准数据流是 `Model -> Controller -> ViewObject -> View`,用户意图由 View 返回 Controller。 +- View 不得直接调用 Model、ApiClient、文件 IPC 或持久化能力。 +- Controller 不得成为第二个 Model,也不得承载 parser、路径安全和环境发现等基础设施实现。 +- Model 不得依赖具体 View、DOM、Electron 或 Node 文件系统实现。 +- ViewObject、领域模型、API DTO 和 IPC DTO 必须区分;不得使用 `any` 或类型断言逃避契约设计。 +- `electron/main.ts` 只负责生命周期、IPC 注册、模块初始化和依赖注入。 +- GUI 只能依赖后端公开且版本化的接口;跨仓数据结构必须有契约测试或固定 fixture。 +- 已发布的数据格式、目录、模板 ID、任务索引和默认行为属于兼容契约。 +- 安装资源只读,用户数据写入系统用户目录;文件替换失败必须保留旧文件。 +- renderer 不得通过通用 IPC 操作任意绝对路径;路径必须 canonicalize 并检查 containment。 +- Bug 修复必须提供修复前失败、修复后通过的可复现证据。 +- 已失败的 workaround 必须删除,不得在外围继续叠加 guard、retry、delay 或 fallback。 +- 达到止损条件时必须暂停,Agent 不得自行批准 L2 以上继续实施。 + +### 1.5 最低验证 + +- TypeScript 或 SCSS 变更至少执行 `npm run build`。 +- 打包、安装资源或发布流程变更应执行 `npm run pack`,无法执行时必须说明原因。 +- 行为修改必须提供额外的确定性验证,不能只以 build、lint、截图或一次手工运行作为完成证据。 +- Bug 修复测试必须证明旧实现失败、新实现通过。 +- 修改架构、目录、命令、数据格式、后端契约或发布流程时,必须同步更新对应文档。 + +## 2. 约束性规则 + +约束性规则用于保持现有结构和团队协作方式。相关文件必须保留;偏离时必须说明理由并同步文档。 + +### 2.1 贡献和代码风格 + +完整规则见 `CONTRIBUTING.md`: + +- 使用 UTF-8、LF 和文件末尾换行。 +- TypeScript、SCSS、JSON、Markdown 和 YAML 默认使用 2 空格缩进;Python 使用 4 空格。 +- TypeScript 使用 strict 模式,新代码不得利用 `noImplicitAny: false` 引入隐式 `any`。 +- 类文件使用 PascalCase,工具和类型文件使用 camelCase。 +- 样式使用 SCSS,并遵循现有 BEM 命名和目录结构。 +- 分支使用 `feat/`、`fix/`、`chore/` 或 `docs/` 等语义前缀。 +- Commit 使用 Conventional Commits,一个 commit 对应一个逻辑变更。 + +### 2.2 架构文档路由 + +任务开始时至少读取对应领域文档。跨领域任务必须读取所有受影响文档。 + +| 影响范围 | 必读文档 | +|---|---| +| 全局分层、目录、启动流程、Types | `docs/architecture/00-overview.md` | +| Controller、Host、依赖注入、启动编排 | `docs/architecture/01-controller-layer.md` | +| Scheduler、TaskQueue、定时任务、修理 | `docs/architecture/02-task-scheduling.md` | +| 用户配置、主题、设置持久化 | `docs/architecture/03-configuration.md` | +| 计划、地图、节点和舰队预设 | `docs/architecture/04-battle-plan.md` | +| 模板、任务组和队列加载 | `docs/architecture/05-template-and-taskgroup.md` | +| IPC、HTTP、WebSocket 和后端契约 | `docs/architecture/06-backend-communication.md` | +| Python、模拟器、后端和更新生命周期 | `docs/architecture/07-environment-management.md` | +| 环境搭建、构建、打包、SCSS | `docs/architecture/08-dev-setup.md` | + +如果实现与架构文档不一致,必须修正实现或在同一变更中更新文档,不得无说明地选择其中一套。 + +### 2.3 教学文档 + +`docs/teaching/` 用于理解历史设计动机,不是合并规范。可以从中复用设计思路,但: + +- 不得把历史文件数、行数或代码快照当作当前事实。 +- 不得用教学示例覆盖强制规范或当前架构文档。 +- 涉及拆分类、Host 接口、ViewObject、Electron、View、Model 或 Types 时,可以读取对应教学章节辅助理解。 + +### 2.4 变更范围 + +- 优先在现有模块和职责边界内完成最小行为闭环。 +- 不得把无关重构、格式化、生成文件或资源更新混入行为修改。 +- 同一功能自然涉及 Model、Controller、Types 和 View 时可以形成一个垂直切片,但必须保持依赖方向。 +- 发现历史违规时,只处理与当前行为目标和风险相称的范围,并在 PR 中披露剩余技术债务。 + +## 3. 宽泛规则中的高风险约束 + +本节只把现有工具说明中的高风险操作提升为通用项目约束。其他工具规则仍按其原始适用范围执行。 + +### 3.1 Git 和工作区安全 + +- 不得覆盖、回退、删除或暂存不是当前任务产生的修改。 +- 工作树存在无关修改时,使用独立 branch 或 worktree 隔离任务。 +- 未经用户明确要求,不得执行 `git reset --hard`、`git checkout -- `、强制清理或历史重写。 +- 不得使用 `git push --force`、`--no-verify` 或其他绕过保护的参数。 +- 不得使用 `git add .` 混入无关文件;必须按逻辑变更显式暂存。 +- 推送前必须检查待推送 commit 范围和工作树状态。 + +### 3.2 发布和 Tag + +- 只有用户明确要求发布时,才能修改版本、创建 Tag 或触发发布。 +- 发布前必须确认功能修改已独立提交并推送,工作树干净。 +- Release commit 只能包含版本和发布元数据,不得夹带功能代码。 +- Tag、`package.json` 版本和发布产物版本必须一致。 +- 不得修改、删除或覆盖已有远程 Tag 来掩盖发布错误。 + +### 3.3 安全和证据 + +- 不得提交密钥、Token、用户配置、日志、运行时数据或本地环境文件。 +- 不得通过关闭 SSL、路径、类型、Schema、测试或权限校验绕过问题。 +- 证据不足时必须区分已验证事实与推测,不得给出确定性根因结论。 +- Issue 分析必须给出实现位置、行为链路、最小修复方案和验证步骤。 +- 涉及数据迁移、文件 IPC、更新器、发布或跨仓契约时,必须优先验证失败路径和回滚路径。 + +### 3.4 生成文件、资源和依赖 + +- 不得手工修改可由构建生成的输出,除非仓库明确要求提交该输出。 +- Lockfile 变更必须由明确的依赖变更产生,并与依赖修改一起说明。 +- 大型资源、fixture、图片和机械生成内容必须与手写行为代码分别统计和说明。 +- 安装资源和内置资源的兼容更新必须有 manifest、版本规则或迁移策略。 + +### 3.5 专用工具规则的边界 + +- `.github/agents/code-length-audit.agent.md` 仅在代码长度审计任务中生效,不自动成为所有 PR 的行数合并门槛。 +- `.github/skills/commit-and-release/SKILL.md` 在提交、推送和发布任务中生效。 +- `.claude/skills/generic-issue-log-analysis/SKILL.md` 在 Issue 和日志分析任务中生效。 +- `.github/workflows/` 定义实际自动化行为;修改 workflow 前必须说明权限、Secret、触发条件和发布影响。 + +## 4. Agent 执行协议 + +### 4.1 修改前 + +Agent 必须报告或在工作记录中明确: + +- 行为目标与非目标; +- 当前工作树和隔离方式; +- 读取过的强制规范和相关架构文档; +- 状态所有者和数据流; +- 当前 Patch 止损等级; +- 计划执行的验证。 + +### 4.2 修改中 + +- 声明当前修改是在替代旧尝试、删除旧 workaround,还是新增独立行为。 +- 一旦出现新的状态源、跨层补偿或意外扩大范围,立即重新评估止损等级。 +- 不得以“先跑起来”为由保留无法解释的 fallback。 + +### 4.3 交付前 + +- 检查 diff,只保留当前任务文件。 +- 执行强制构建和与行为风险匹配的确定性验证。 +- 更新受影响文档。 +- 记录执行命令、测试结果、未验证路径、失败尝试、状态源变化和回滚方式。 +- 对照 `docs/engineering-standards.md` 第 12 节完成审查清单。 + +## 5. 维护本文件 + +- 强制规范只在 `docs/engineering-standards.md` 维护正文,本文件通过引用完整继承。 +- 架构变化先更新对应架构文档,再更新本文件的路由或摘要。 +- 工具入口文件只能指向本文件,不得复制整套规则。 +- 新增规则时必须明确属于强制性、约束性还是高风险宽泛规则。 +- 如果本文件与权威来源冲突,以第 1.2 节顺序为准,并在同一变更中修复冲突。 + +## 6. ShiinaKuroko Fork 分支管理 + +本仓库的个人 Fork 为 `https://github.com/ShiinaKuroko/AutoWSGR-GUI.git`。后续 Agent +必须遵守以下分支职责: + +### 6.1 本机仓库与 GitHub 网络路径 + +- GUI 仓库固定为 `C:\ShiinaKuroko\01.Project\AutoWSGR-GUI`,后端仓库固定为 `C:\ShiinaKuroko\01.Project\AutoWSGR`。 +- GUI 个人远端为 `https://github.com/ShiinaKuroko/AutoWSGR-GUI.git`,后端个人远端为 `https://github.com/ShiinaKuroko/AutoWSGR.git`,发布分支均为 `ShiinaKuroko`。 +- 本机开启代理时,Git CLI 不会自动继承 Windows 系统代理。执行 GitHub `fetch`、`pull` 或 `push` 前,应读取 `HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings` 的 `ProxyServer`;当前代理为 `127.0.0.1:7897`。 +- `127.0.0.1:7897` 正在监听时,GitHub 命令应优先通过临时参数使用代理,例如 `git -c http.proxy=http://127.0.0.1:7897 -c https.proxy=http://127.0.0.1:7897 push origin ShiinaKuroko`。不得为此修改全局 Git 配置。 +- 代理未启用或端口未监听时,允许使用正常直连路径;连接失败时应根据实际错误区分网络、认证和远端冲突问题。 + +- `main` 只用于同步 `OpenWSGR/AutoWSGR-GUI:main`,禁止在该分支直接开发、提交或推送功能代码。 +- `ShiinaKuroko` 是个人 Fork 的最新开发分支,经过验证的最新 GUI 代码才允许推送到这里。 +- 本地独立分支或 worktree 只能用于编码、测试和审查,禁止直接推送到 Fork 的发布分支。 +- 本地独立分支完成后,必须将已验证提交合并、`cherry-pick` 或 rebase 整理到本地 `ShiinaKuroko` 分支;只有本地 `ShiinaKuroko` 分支允许执行 `git push origin ShiinaKuroko`。 +- 不得执行 `git push origin ` 作为发布流程;远程临时分支如确有协作需要,必须获得维护者明确授权,并不得替代 `ShiinaKuroko` 发布入口。 +- `backup/YYYYMMDD-` 是版本备份分支。每次更新 `ShiinaKuroko` 前创建更新前稳定提交的备份,完成更新后创建新版本备份,最多保留两个。 +- 备份分支一旦创建不得移动、覆盖或追加提交。超过两个备份时只删除最旧备份,不删除当前备份和上一版本备份。 +- 推送前必须确认当前分支为本地 `ShiinaKuroko`,并检查 `git status --short --branch`、`git diff --check`、`git log --oneline -5` 以及 `git log origin/ShiinaKuroko..ShiinaKuroko`。 +- 推送最新代码前必须先创建备份,并使用 `git push --force-with-lease` 更新 `ShiinaKuroko`,禁止无条件 `--force`。 +- 任何删除远程分支的操作都必须先列出分支、提交和原因;禁止删除 `main`、`ShiinaKuroko` 或未明确授权的分支。 + +## 7. GUI 2.0 合并协作边界 + +我这边已经梳理过 GUI 2.0 和当前稳定分支的差异。为了避免我们分别修改 Scheduler、配置模型和舰队语义,后续按下面的边界协作。 + +### 你在合并前需要完成的范围 + +你负责把 GUI 2.0 当前已经实现的功能收敛到“可合并、不会破坏已有数据”的状态,重点是现有功能和基础架构,不需要替我实现自动强化。 + +#### 1. 先解决当前 review 中会造成数据损坏、安全问题或兼容性破坏的阻塞项 + +- 给主进程文件 IPC 加严格的路径边界校验,禁止路径穿越和访问受管目录以外的文件。 +- 修复 AtomicFileStore:写入失败时必须保留原文件,不能出现旧文件先被删除、临时文件又未成功替换的情况。 +- 统一外部 Python 环境的依赖安装目录和实际启动目录。 +- 保持 v1.4.1 及当前稳定版计划格式兼容,不能要求用户手工重写旧计划。 +- 完成旧 path-form 任务组迁移,迁移失败时要保留原数据并给出明确错误。 +- 设置 YAML round-trip 时保留 GUI 尚不认识的字段,尤其是未知嵌套字段。 +- 修复 candidate-only 语义:槽位没有顶层 `name` 时,所有 `candidates` 都是平等备选,不能把第一个 candidate 自动提升成 primary。 + +#### 2. 稳定 GUI 2.0 已经引入的核心契约 + +- 舰队方案与出征计划保持分离。 +- 编辑态计划可以引用独立舰队 YAML。 +- 运行前由 `RuntimePlanService` 展开为后端可执行的完整 YAML。 +- 系统计划与用户计划有明确的读写边界。 +- `SchedulerRepairPolicy`、`SchedulerTaskPolicy`、`RepairManager`、`TaskQueue.switchTaskPreset()` 的责任边界保持清晰。 +- 编队轮换继续使用统一 Scheduler,不再增加另一套并行状态机。 + +#### 3. 处理旧活动计划兼容性 + +当前旧目录中有用户正在使用的活动计划,不能仅通过删除: + +- `resource/builtin_plans/活动20260730-E1炸鱼.yaml` +- `resource/builtin_plans/活动20260730-E5夜战.yaml` +- `resource/builtin_plans/活动20260730-H1炸鱼.yaml` +- `resource/builtin_plans/活动20260730-H5夜战.yaml` + +并在 `resource/system_battle_plans` 放几个相似文件,就认为迁移完成。 + +合并前需要验证: + +- 旧计划仍可读取、执行或显式迁移。 +- 新旧计划行为等价。 +- 用户修改过的旧文件不会被安装、升级或迁移流程静默覆盖。 +- 无法迁移时保留原文件,并显示可理解的错误。 + +#### 4. 合并前最低验收程度 + +- `npm run build` 通过。 +- `git diff --check` 通过。 +- 至少覆盖以下回归测试: + - legacy plan 读取和迁移; + - candidate-only 舰队槽位; + - 设置 YAML 未知字段保留; + - 原子写入失败恢复; + - 文件 IPC 路径越界拒绝; + - 任务组旧格式迁移; + - 四个活动计划的兼容性; + - 舰队引用展开后的运行时 YAML 合法性。 +- 把 PR 中尚未接到 UI 的执行入口明确处理:要么接通,要么删除死入口,要么明确标记为暂不支持,不能保留一个看似可用但实际没有调用方的链路。 +- 最好按领域拆分提交,至少让 Electron 服务、舰队 YAML、运行时计划、Scheduler 规则、UI 和舰船资源可以分别审查。 + +### 可以在合并后交给我处理的内容 + +以下内容不要求你在 GUI 2.0 合并前完成: + +- 自动强化的业务实现。 +- 自动强化后端 API 和设备独占租约。 +- intensify Scheduler 任务类型。 +- 自动强化的保护舰集合计算。 +- 自动强化与轮换、泡澡、战斗任务之间的优先级。 +- 自动强化设置 UI 和最终 Scheduler 接线。 +- 自动强化运行日志、失败重试和任务状态展示。 +- 将后端唯一舰船 ID 引入自动强化保护规则;在后端尚未提供 ID 时先兼容舰名。 + +但请预留正常的扩展边界,不要把 Scheduler task type、API 请求或设置 schema 写成无法扩展的封闭分支。 + +### 我这边会负责什么 + +我负责自动强化以及它与稳定分支公共调度层的最终集成: + +- 后端定义并实现自动强化 API。 +- 所有强化设备操作使用全局 device lease,不能与战斗、泡澡或解装并发。 +- 自动强化只作为统一 Scheduler 的正式任务执行,不启动独立后台定时器。 +- 强化前重新读取保护集合,至少保护: + - 当前游戏编队; + - 所有启用轮换预设; + - 正在泡澡的舰船; + - 收藏或锁定舰; + - 强化目标舰; + - 用户自定义保护名单。 +- 设计素材舰筛选、上限、失败和重试规则。 +- 接入 `ApiClient`、API types、Scheduler types、`ConfigModel`、`ConfigController` 和 `SchedulerBinder`。 +- 最终处理自动强化与 GUI 2.0 合并后的公共文件冲突。 + +### 我在 GUI 2.0 合并前会做到的程度 + +我会优先在后端和独立领域层工作,避免现在修改你的舰队编辑器、计划管理 UI 和 Scheduler 核心: + +- 先稳定自动强化的 HTTP contract。 +- 接入后端全局设备租约。 +- 建立 ops 层和 UI controller 的责任边界。 +- 定义目标舰、允许的素材舰种、素材数量上限和保护名单。 +- 编写无设备单元测试和 route contract 测试。 +- 未完成真实设备坐标与 OCR 标定前,功能必须 fail closed,不能尝试点击或消耗任何舰船。 +- GUI 2.0 合并后,我再基于你的最终 Scheduler 和配置架构完成 GUI 任务接线,避免双方同时修改同一套状态机。 + +总体原则是:你负责让 GUI 2.0 已有功能安全、兼容、可合并;我负责自动强化及合并后的公共调度集成。编队轮换核心继续以你的 GUI 2.0 实现为准,我不会另写一套。 + +## 8. Windows 打包失败记录 + +### 8.1 `winCodeSign` 符号链接权限 + +在未启用 Windows 开发者模式、且终端没有创建符号链接权限时, +Electron Builder 解压旧版 `winCodeSign-2.6.0.7z` 会失败。典型日志为: + +```text +ERROR: Cannot create symbolic link : 客户端没有所需的特权。 +darwin\10.12\lib\libcrypto.dylib +darwin\10.12\lib\libssl.dylib +``` + +这两个文件属于 macOS 工具,但 7-Zip 使用 `-snld` 解压整个工具包,因此 +Windows 打包仍会被阻断。开始打包前必须先检查开发者模式或当前终端的符号链接 +权限,不得在确认缺少权限后反复执行相同命令和重复下载。 + +正式解决方式是启用 Windows 开发者模式,或使用具备创建符号链接权限的终端执行 +`npm run dist`。不要把本地工具路径覆盖当成正式解决方式。 + +启用开发者模式后,应使用 Electron Builder 自带的 `7za.exe` 和 +`winCodeSign-2.6.0.7z` 执行一次带 `-snld` 的解压探针。PowerShell 5 的 +`New-Item -ItemType SymbolicLink` 即使在开发者模式已经开启时,也可能继续 +误报“需要管理员权限”,不能把它作为打包前置检查。确认 7-Zip 能创建两个 +`.dylib` 符号链接后,直接执行原始 `npm run dist`;不要再设置工具路径覆盖。 + +### 8.2 已失败的规避方式 + +- 不得使用 `--config.win.signAndEditExecutable=false` 生成正式交付包。该参数会 + 跳过 EXE 资源、版本信息和 ASAR 完整性写入,只能用于诊断,不能作为成功打包。 +- 只设置 `ELECTRON_BUILDER_RCEDIT_PATH` 不足以完成打包。EXE 资源编辑会通过, + 但处理附带的 ADB、Python 等 EXE 时仍会查找签名工具并下载旧版 + `winCodeSign`。 +- 同时设置 `ELECTRON_BUILDER_RCEDIT_PATH` 和 `SIGNTOOL_PATH` 仍不足以完成 + NSIS 打包。应用 EXE 资源编辑及附带 EXE 处理会通过,但进入 NSIS 阶段前仍会 + 强制获取旧版 `winCodeSign`。不要重复尝试该双路径方案。 +- 打包失败后不得继续使用外围重试、延时或运行时文件写入 fallback 掩盖问题; + 必须先区分打包工具权限错误和应用自身持久化错误。 + +### 8.3 单 EXE 交付检查 + +用户要求“只有 EXE”时,目标是 NSIS 安装程序,不是 `--dir` 目录包: + +1. 关闭所有从 `release/win-unpacked` 启动的 GUI 进程。 +2. 确认 `package.json` 与 `package-lock.json` 版本一致。 +3. 使用具备完整 `winCodeSign` 权限的环境执行 `npm run dist`。 +4. 从 `release/` 中单独取出 + `AutoWSGR-GUI-Setup-.exe`,交付目录不得混入 + `win-unpacked`、更新清单或 blockmap。 +5. 检查安装程序版本、文件哈希和签名状态,并实际启动安装后的 GUI。 + +生成的安装包、`release/` 和本地 Electron Builder 缓存不得提交到 Git。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..137540f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +# Claude Code 项目入口 + +在分析、修改文件或执行命令前,必须完整读取并遵守根目录 `AGENTS.md`。 + +`AGENTS.md` 要求继续完整读取 `docs/engineering-standards.md`、可执行配置、`CONTRIBUTING.md` 和任务相关架构文档。不得用本入口文件或任何摘要替代这些原始规则。 diff --git a/README.md b/README.md index 62348b6..6fc1482 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,241 @@ -# AutoWSGR GUI - -[AutoWSGR](https://github.com/OpenWSGR/AutoWSGR) 的桌面图形界面,基于 Electron + TypeScript 构建。 - -## 当前 GUI 已实现功能 - -- **主页** - - 连接状态与远征倒计时展示 - - 手动运维操作(收远征 / 收奖励 / 收建造 / 食堂烹饪 / 浴室修理) - - 任务队列开始、停止、清空 - - 任务组管理(新建、重命名、删除、导入导出、整组入队) - - 后端日志实时查看与等级过滤 -- **方案预览** - - 导入 YAML 或从地图新建方案 - - 节点路线可视化与节点级参数编辑 - - 方案级参数配置(修理策略、战况、编队、次数、间隔、终止条件) - - 编队预设与泡澡修理阈值配置 - - 一键加入队列 / 加入任务组 / 保存 YAML / 另存为 -- **模板库** - - 支持普通出击、演习、战役、决战四类模板 - - 模板导入、创建、详情配置、加入任务列表 -- **配置页** - - 模拟器类型、路径、ADB 串号检测 - - Python 解释器路径检测与后端端口设置 - - 自动更新模式与手动检查更新 - - 每日自动任务(远征、战役、演习、常规出击、决战、战利品) - - 主题模式、主色调、调试模式 -- **调度与运行** - - 任务优先级调度 - - 条件停止与失败重试 - - 启动流程检查与运行期日志可观测 - -## 安装 +# AutoWSGR-GUI + +[AutoWSGR](https://github.com/OpenWSGR/AutoWSGR) 的 Windows 桌面图形界面,使用 Electron、TypeScript 和 SCSS 构建。 + +GUI 负责配置、编辑和管理 YAML,并通过 HTTP 与 WebSocket 连接 AutoWSGR 后端。后端模型和 YAML 契约是功能的事实来源,GUI 不额外定义后端不支持的字段或规则。 + +## 功能总览 + +当前主导航分为 **作战**、**计划** 和 **设置**。 + +### 作战 + +- 展示当前任务、剩余次数、任务进度、运行状态和远征倒计时。 +- 导入出征 YAML,或从计划管理中选择已有计划。 +- 管理任务列表:新建、保存、加载及整组加入队列。 +- 管理任务队列:开始、停止和清空。 +- 提供收取远征、收取奖励、收取建造、食堂烹饪和浴室修理等快捷操作。 +- 实时显示后端日志,并支持按日志等级筛选。 +- 当前任务携带明确编队时,在舰队预览中展示最多六艘舰船立绘; + 没有任务执行时显示空状态。 + +### 计划 + +计划页用于生成、保存和管理 YAML,不直接执行出征任务。完成编辑后,需要回到作战页将计划加入任务列表和任务队列。 + +#### 舰队规划 + +- 使用本地舰船资料库展示舰船立绘和资料。 +- 支持舰名或编号搜索、舰种和国籍多选、改造过滤及多种排序方式。 +- 编辑六个主选位置,以及每个位置独立的备选队列。 +- 支持主选、备选和舰船图鉴之间拖拽,空位自动整理到右侧。 +- 支持纯备选位置、位置级等级限制及复制备选队列。 +- 支持新建、保存和加载;未保存修改与同名覆盖均会二次确认。 +- 用户舰队保存为 `resource/user_team_plans/team-{预设名称}.yaml`。 + +舰队 YAML 支持主选舰船和位置级 `candidates`。一个位置也可以没有主选 `name`,只保留非空的结构化 `candidates`。 + +#### 出征规划 + +- 编辑地图章节、关卡、执行次数、轮次间隔和舰队编号。 +- 配置战况、维修策略、维修方式、战利品停止条件和掉落停止条件。 +- 选择并预览一个或多个舰队方案。 +- 通过地图编辑节点启用状态、终点、迂回、阵型、战斗动作、最低战果和索敌规则。 +- 支持新建、保存和加载;保存时校验文件名、地图信息及 YAML 内容。 +- 用户计划保存为 `resource/user_battle_plans/bettle-{预设名称}.yaml`。 + +出征规划当前没有直接执行入口。保存后的计划应在作战页加载并加入队列。 + +#### 决战计划(旧) + +保留旧版决战计划配置入口,用于现有决战流程。该页面属于兼容功能,不代表新的统一 YAML 任务设计。 + +#### 计划管理 + +- 汇总系统和用户的出征计划、舰队方案及关联状态。 +- 支持按来源、类型、名称和“仅看需要处理”筛选。 +- 标记舰队缺失、未被引用或 YAML 无法读取等状态。 +- 支持从管理列表跳转到舰队规划或出征规划并加载对应文件。 +- 用户 YAML 可以删除;系统 YAML 在界面中按只读资源处理。 +- 对不需要关联舰队的 YAML,可以忽略未关联提示并随时恢复。 + +### 设置 + +设置页分为 **系统设置** 和 **脚本行为**。 + +系统设置包括: + +- 模拟器类型、路径、账号和 ADB 地址。 +- ADB 主动连接、断开、自动检测及在线状态。 +- 自动远征、战役、演习、出征和胖次任务。 +- 日志等级、日志目录和调试模式。 +- Python 路径、后端地址、后端启动模式及本地 AutoWSGR 仓库路径。 +- 默认窗口大小、记录退出时窗口位置和大小。 +- GUI 更新模式、舰船数据库更新。 +- 亮色、暗色、跟随系统主题及主色调。 + +脚本行为包括: + +- 全局操作延迟。 +- OCR 下载源、加速模式、CUDA 路径和硬件识别。 +- 舰名匹配置信度、系统舰名规则、自定义舰名映射和识别纠错规则。 +- 舰队、船坞、维修、解装及自定义作战方案目录等后端配置。 + +## 推荐使用流程 + +1. 在设置页配置模拟器、ADB、Python 和后端启动模式。 +2. 按需更新舰船数据库。 +3. 在舰队规划中创建并保存舰队方案。 +4. 在出征规划中创建计划、关联舰队并保存 YAML。 +5. 在计划管理中检查计划与舰队的关联状态。 +6. 回到作战页加载计划,加入任务列表和任务队列后执行。 + +## 安装与运行 ### 普通用户 -1. 安装 Android 模拟器(MuMu 12 / 雷电 / 蓝叠) -2. 从 [Releases](https://github.com/yltx/AutoWSGR-GUI/releases) 下载最新安装包 -3. 安装运行,程序自动配置环境(下载便携 Python 3.12、安装 autowsgr 依赖到程序目录,**不影响系统环境**) -4. 确保模拟器已运行,程序自动检测并连接 +1. 安装并启动 MuMu 12、雷电或 BlueStacks。 +2. 从 [Releases](https://github.com/yltx/AutoWSGR-GUI/releases) 下载 Windows x64 安装包。 +3. 安装并启动 AutoWSGR-GUI。 +4. 在设置页确认模拟器、ADB 地址和后端环境。 + +默认的 `managed` 模式由 GUI 管理 Python 和 AutoWSGR 依赖。依赖安装到程序自己的 `python/site-packages/`,不会写入系统 Python 的全局包目录。 + +遇到环境问题时,可以运行 `debug_deps.bat` 生成诊断信息。 -> 如果你已有 Python ≥ 3.12,程序也可以使用它(依赖仍安装到 `python/site-packages/`,不修改全局包)。 -> -> 遇到问题时可运行 `debug_deps.bat` 生成诊断报告。 +### 源码运行 -### 开发者 +前置要求: -```bash +- Windows 10/11 +- Node.js 18 或更高版本 +- npm +- AutoWSGR 后端需要 Python 3.12 或 3.13 + +```powershell git clone https://github.com/yltx/AutoWSGR-GUI.git cd AutoWSGR-GUI npm install -setup.bat # 安装便携 Python + autowsgr 依赖 -npm run dev # 编译 + 启动 Electron +npm run dev ``` -## 用户文档 +项目没有热重载。修改代码后需要退出当前 Electron 实例,再次运行 `npm run dev`。 + +## 后端启动模式 + +### managed -- GUI 操作指南(含“从方案预览页开始的新建任务配置流程”):[docs/user-guide.md](docs/user-guide.md) -- YAML 关键字说明(战斗方案编写):[docs/plan-guide.md](docs/plan-guide.md) +适合普通用户: -## 编写战斗方案 +- GUI 查找或准备可用的 Python。 +- AutoWSGR 及依赖由 GUI 安装到本地 `python/site-packages/`。 +- GUI 启动并管理 uvicorn 后端进程。 +- 自动更新策略由设置页的更新模式控制。 + +### external + +适合同时开发 GUI 和 AutoWSGR 后端: + +- 使用本地 AutoWSGR 仓库源码及其虚拟环境。 +- 本地仓库根目录必须包含 `autowsgr/server/main.py`。 +- GUI 将仓库根目录加入后端 Python 的模块搜索路径。 +- external 模式不会自动安装或更新远端 `autowsgr`;缺少依赖时,本地仓库本身作为安装 requirement。 + +可以在设置页填写,也可以在开发模式的 `gui_settings.json` 中配置: + +```json +{ + "backend_startup_mode": "external", + "backend_repo_path": "C:\\path\\to\\AutoWSGR", + "python_path": "C:\\path\\to\\AutoWSGR\\.venv\\Scripts\\python.exe", + "update_mode": "manual" +} +``` -参见 [docs/plan-guide.md](docs/plan-guide.md),详细说明 YAML 方案的所有关键字和用法。 +默认后端地址为 `http://127.0.0.1:8438`。MuMu 12 常用 ADB 地址为 `127.0.0.1:16384`,实际值应以模拟器实例为准。 -`plans/` 目录提供了多个示例方案可供参考。 +## YAML 与数据目录 + +开发模式下的主要目录如下: + +| 路径 | 内容 | +| --- | --- | +| `resource/system_battle_plans/` | 系统出征计划,界面中只读 | +| `resource/user_battle_plans/` | 用户出征计划,标准文件名为 `bettle-{名称}.yaml` | +| `resource/system_team_plans/` | 系统舰队方案,界面中只读 | +| `resource/user_team_plans/` | 用户舰队方案,标准文件名为 `team-{名称}.yaml` | +| `resource/ship-library/` | 舰船资料库 manifest、中文标签和本地资源 | +| `resource/maps/` | 出征规划使用的地图数据 | +| `usersettings.yaml` | 传递给 AutoWSGR 后端的用户配置 | +| `gui_settings.json` | GUI 环境、窗口、调度及界面状态 | +| `task_groups.json` | 作战页任务列表数据 | + +打包运行时,系统资源从安装包资源目录读取,用户数据由主进程写入应用可写目录。不要直接修改系统方案;需要调整时应另存到用户目录。 + +GUI 保存的 YAML 必须通过当前前端校验,并最终符合 AutoWSGR 后端模型。战斗方案字段说明见 [docs/plan-guide.md](docs/plan-guide.md)。 + +## 开发命令 + +| 命令 | 说明 | +| --- | --- | +| `npm run dev` | 清理、编译、打包并启动 Electron | +| `npm start` | 执行构建后启动 Electron | +| `npm run build` | 编译 TypeScript、SCSS 并打包渲染进程,不启动应用 | +| `npm run build:css` | 单独编译 SCSS | +| `npm run prepare-python` | 准备便携版 Python | +| `npm run prepare-adb` | 准备 ADB 工具 | +| `npm run pack` | 生成未安装的应用目录 | +| `npm run dist` | 准备 Python 和 ADB,并生成 NSIS 安装包 | ## 项目结构 ```text -├── electron/ # Electron 主进程 -│ ├── main.ts # 窗口管理、IPC 注册、应用生命周期 -│ ├── preload.ts # contextBridge 安全桥接 -│ ├── backend.ts # Python 后端进程管理 -│ ├── emulatorDetect.ts # 模拟器注册表检测 -│ └── pythonEnv/ # Python 环境检查、安装、更新(7 个模块) +├── electron/ # Electron 主进程、IPC、后端和环境管理 +│ ├── main.ts # 窗口生命周期、文件管理及 IPC +│ ├── preload.ts # 安全桥接 +│ ├── backend.ts # AutoWSGR 后端进程管理 +│ ├── emulatorDetect.ts # 模拟器检测 +│ └── pythonEnv/ # Python 与依赖环境管理 ├── src/ -│ ├── controller/ # 控制器层 — 业务逻辑与调度 -│ │ ├── app/ # AppController、ConfigController、SchedulerBinder -│ │ ├── plan/ # PlanController(方案编辑) -│ │ ├── startup/ # StartupController(启动流程) -│ │ ├── taskGroup/ # TaskGroupController -│ │ ├── template/ # TemplateController -│ │ └── shared/ # ControllerHost 接口 -│ ├── model/ # 模型层 — 数据、API、调度 -│ │ ├── ApiClient.ts # HTTP + WebSocket 通信 -│ │ ├── ConfigModel.ts # 用户配置读写 -│ │ ├── PlanModel.ts # 方案数据 & YAML 序列化 -│ │ ├── scheduler/ # Scheduler、CronScheduler、TaskQueue -│ │ ├── TaskGroupModel.ts -│ │ └── TemplateModel.ts -│ ├── view/ # 视图层 — 纯 DOM 渲染 -│ │ ├── main/ # 主页面(状态、队列、日志) -│ │ ├── plan/ # 方案预览 & 地图可视化 -│ │ ├── config/ # 配置页 -│ │ ├── template/ # 模板库 -│ │ ├── taskGroup/ # 任务组管理 -│ │ ├── setup/ # 安装向导 -│ │ └── styles/ # SCSS 样式 -│ ├── types/ # TypeScript 类型定义 -│ ├── data/ # 静态数据(舰船信息) -│ └── utils/ # 工具(Logger 等) -├── resource/ # 运行时资源(地图 JSON、内置方案、图片) -├── scripts/ # 构建脚本(esbuild、prepare-python、prepare-adb) -├── plans/ # 示例 YAML 战斗方案 -├── templates/ # 用户模板持久化 -├── docs/ # 文档 & 架构说明 -├── build/ # electron-builder 配置(NSIS 脚本) -├── setup.bat # 环境一键配置 -├── debug_deps.bat # 依赖诊断脚本 +│ ├── controller/ # 页面业务协调与调度 +│ ├── model/ # 配置、计划、API、任务和调度模型 +│ ├── view/ # 作战、计划、设置页面及 SCSS +│ ├── types/ # TypeScript 类型与 bridge 契约 +│ └── utils/ # 日志及通用工具 +├── resource/ +│ ├── maps/ # 地图数据 +│ ├── ship-library/ # 舰船资料库 +│ ├── system_battle_plans/ # 系统出征计划 +│ ├── system_team_plans/ # 系统舰队方案 +│ ├── user_battle_plans/ # 用户出征计划 +│ └── user_team_plans/ # 用户舰队方案 +├── tools/ship_library/ # 舰船资料库更新工具 +├── scripts/ # 构建、Python 和 ADB 准备脚本 +├── docs/ # 使用与架构文档 +├── build/ # electron-builder / NSIS 配置 +├── setup.bat # Windows 环境配置脚本 +├── debug_deps.bat # 环境诊断脚本 ├── package.json └── tsconfig.json ``` -## 开发命令 - -| 命令 | 说明 | -| ------ | ------ | -| `npm run dev` | 编译 TypeScript + 打包 + 启动 Electron | -| `npm run build` | 仅编译 + 打包(不启动) | -| `npm run build:css` | 编译 SCSS → CSS | -| `npm run dist` | 下载 Python/ADB + 编译 + 打包为 NSIS 安装程序 | -| `npm run pack` | 编译 + 打包为目录(不生成安装程序) | - -## 架构说明 +## 文档 -采用 **MVC + ViewObject** 模式,详细文档见 [docs/architecture/](docs/architecture/)。 +- [战斗方案 YAML 说明](docs/plan-guide.md) +- [架构文档索引](docs/architecture/README.md) +- [开发环境搭建](docs/architecture/08-dev-setup.md) +- [环境管理](docs/architecture/07-environment-management.md) +- [贡献指南](CONTRIBUTING.md) -- **Controller** — 从 Model 提取数据,拼装为只读 ViewObject,单向传递给 View -- **View** — 纯 DOM 渲染,不包含业务逻辑 -- **Model** — API 通信、配置解析、任务调度、数据持久化 +## 技术栈 -Python 后端通过 `pip install autowsgr` 安装(`--target` 安装到程序目录),由 Electron 主进程管理 uvicorn 子进程,前端通过 HTTP + WebSocket 与后端通信。 +- Electron 33 +- TypeScript 5.6 +- esbuild +- Sass / SCSS +- electron-builder / NSIS +- js-yaml +- AutoWSGR FastAPI / uvicorn 后端 ## 贡献 diff --git a/docs/TODO.md b/docs/TODO.md index e06101c..3d4ec00 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -77,7 +77,7 @@ 说明:决战 preset 已支持这些字段,并可被调度器执行。 - [x] 默认快修打完一轮 - 说明:决战任务模板已具备基础执行能力,默认执行一轮;更细致的快修策略仍需后端进一步支撑。 + 说明:决战任务模板已具备基础执行能力;自动决战固定执行一轮,更细致的快修策略仍需后端进一步支撑。 #### 日常调度 @@ -356,7 +356,7 @@ ### 战利品调度 - [x] 提供战利品任务模板,用户可选择常见地图并配置编队 - 说明:已通过内置模板 (builtin_farm_loot) 实现,包含 9-2、7-4、8-5、2-1 四个地图方案,支持 loot_count_ge 停止条件。 + 说明:已通过内置模板 (builtin_farm_loot) 实现,包含 9-4、9-2、7-4、8-5、8-2、2-1 六个地图方案,支持 loot_count_ge 停止条件。 - [ ] 自动安排合理的战斗方案和修复方案,以获取特定战利品 说明:当前仅能执行既定方案,不会根据目标掉落自动规划路线、编队和修理。 @@ -366,10 +366,14 @@ 说明:当前已支持基于获取数量的停止条件,在单任务维度可自动停止。 - [x] 对用户展示:是否启用每日自动战利品 - 说明:已在配置页自动化设置中添加「每日自动刷战利品」开关,可选择地图方案(9-2/7-4/8-5/2-1)和停止数量。CronScheduler 每日 0 点后自动触发,使用内置 builtin_farm_loot 模板执行。 + 说明:已在配置页自动化设置中添加「每日自动刷战利品」开关,可选择地图方案(9-4/9-2/7-4/8-5/8-2/2-1)和停止数量。CronScheduler 每日 0 点后自动触发,并按稳定系统计划文件名执行。 ### 决战调度 +- [x] 每日自动生成一轮决战任务 + 说明:配置页可选择决战模板。模板舰队为空时回退到决战计划页当前配置;任务实际结束后当天不再重复触发。 + 说明:当前不探测剩余票数,旧版票数保留字段只做无损归档,不参与执行轮数。 + - [ ] 决战实现中断功能 说明:后端依赖。需要后端支持决战任务安全中断、保存中间状态与恢复。 diff --git a/docs/architecture/00-overview.md b/docs/architecture/00-overview.md index ea9ff62..4e0747c 100644 --- a/docs/architecture/00-overview.md +++ b/docs/architecture/00-overview.md @@ -16,14 +16,16 @@ AutoWSGR-GUI 是一个基于 **Electron** 的桌面应用,为 [AutoWSGR](https graph TB subgraph Renderer["渲染进程 (src/)"] View["View 层
MainView(Facade) · PlanPreviewView(Facade)
ConfigView · TaskGroupView
TemplateLibraryView · SetupWizardView"] - Controller["Controller 层
AppController · StartupController
PlanController · TaskGroupController
TemplateController · SchedulerBinder
── ControllerHost 接口解耦 ──"] - Model["Model 层
Scheduler · CronScheduler · TaskQueue
ApiClient · ConfigModel · PlanModel
TemplateModel · TaskGroupModel
RepairManager · StopConditionChecker"] + Controller["Controller 层
AppController · StartupController
PlanController · FleetPlannerController
TaskGroupController · TemplateController
── 各功能最小 Host 接口 ──"] + Model["Model 层
Scheduler · CronScheduler · TaskQueue
ApiClient · ConfigModel · PlanModel
TemplateModel · TaskGroupModel · DailySortieStats
RepairManager · StopConditionChecker"] end subgraph Main["Electron 主进程 (electron/)"] - IPC["IPC Handlers
main.ts · preload.ts"] - PyEnv["Python 环境管理
pythonEnv/ (7 个模块)"] - Backend["后端进程管理
backend.ts"] + Root["组合根
main.ts"] + IPC["IPC Adapter
ipc/*.ts · preload.ts"] + Service["用例与领域服务
services/*.ts"] + PyEnv["Python 环境管理
pythonEnv/ (12 个模块)"] + Backend["后端进程管理
services/BackendService.ts"] Emulator["模拟器检测
emulatorDetect.ts"] end @@ -36,8 +38,11 @@ graph TB Controller -->|"调用 / 订阅"| Model Model -->|"HTTP / WebSocket"| ASGI Controller -->|"contextBridge (preload.ts)"| IPC - IPC --> PyEnv - IPC --> Backend + Root --> IPC + Root --> Service + IPC --> Service + Service --> PyEnv + Service --> Backend IPC --> Emulator Backend -->|"spawn 子进程"| ASGI ``` @@ -47,10 +52,11 @@ graph TB | 层 | 位置 | 职责 | |----|------|------| | **View** | `src/view/` | 纯 UI 渲染,接收 ViewObject 显示;不含业务逻辑。大型视图采用 Facade 模式内部拆分 | -| **Controller** | `src/controller/` | 从 Model 提取数据 → 拼装 ViewObject → 调用 View 渲染;处理用户事件 → 调用 Model / IPC。通过 ControllerHost 接口解耦 | -| **Model** | `src/model/` | 业务实体 + 领域服务:调度、配置、方案解析、后端通信 | -| **Types** | `src/types/` | 跨层共享的 TypeScript 类型定义,按领域拆分为 5 个文件 | -| **主进程** | `electron/` | 窗口管理、IPC handler、Python 环境发现/安装、后端子进程生命周期、模拟器检测 | +| **Controller** | `src/controller/` | 从 Model 提取数据 → 拼装 ViewObject → 调用 View 渲染;处理用户事件 → 调用 Model / Adapter。各功能声明自己的最小 Host | +| **Model** | `src/model/` | 业务实体 + 领域服务:调度、配置、方案、Fleet 规则、后端通信 | +| **Adapter** | `src/adapter/` | YAML/JSON、IPC、HTTP/WS 和浏览器存储边界 | +| **Types** | `src/types/` | API、IPC、Model、View、Scheduler 和 Statistics 六个领域类型文件 | +| **主进程** | `electron/` | `main.ts` 负责装配;`ipc/` 保持通道契约;`services/` 承担窗口、配置、计划、环境和后端用例 | | **Python 后端** | 外部 | 游戏自动化核心逻辑:模拟器连接、战斗执行、OCR 识别 | --- @@ -60,13 +66,43 @@ graph TB ``` AutoWSGR-GUI/ ├── electron/ # Electron 主进程 -│ ├── main.ts # 入口:窗口创建、IPC handler 注册 +│ ├── main.ts # 组合根:依赖装配、IPC 注册、迁移与生命周期 │ ├── preload.ts # contextBridge 安全 API 暴露 -│ ├── backend.ts # Python 后端启动/停止 │ ├── emulatorDetect.ts # 模拟器注册表检测 +│ ├── ipc/ # IPC Adapter,不实现领域规则 +│ │ ├── FileIpc.ts # 文件、路径和系统对话框 +│ │ ├── DeviceIpc.ts # 模拟器与 ADB +│ │ ├── ConfigurationIpc.ts # 同步 getter 与配置 setter +│ │ ├── TeamPlanIpc.ts # 编队计划 +│ │ ├── CombatPlanIpc.ts # 作战计划 +│ │ ├── DailyPlanIpc.ts # 演习、战役和决战日常计划 +│ │ ├── ShipLibraryIpc.ts # 舰船资料库 +│ │ ├── EnvironmentIpc.ts # Python 环境 +│ │ ├── BackendIpc.ts # 后端进程 +│ │ ├── UpdaterIpc.ts # GUI 自动更新 +│ │ └── IpcRegistrar.ts # 最小注册接口 +│ ├── services/ # 用例、领域和基础设施服务 +│ │ ├── AppPaths.ts · SafePathService.ts · SecureFileService.ts +│ │ ├── AtomicFileStore.ts · GuiSettingsStore.ts +│ │ ├── WindowService.ts · UserDataMigrationService.ts +│ │ ├── TeamPlanCodec.ts · TeamPlanRepository.ts · TeamPlanService.ts +│ │ ├── CombatPlanCodec.ts · CombatPlanRepository.ts +│ │ ├── RuntimePlanService.ts · PlanManagementService.ts +│ │ ├── DailyPlanService.ts · TaskPresetCodec.ts +│ │ ├── ShipLibraryService.ts · ShipLibraryUpdater.ts +│ │ ├── AdbService.ts · CudaEnvironmentService.ts +│ │ ├── GuiConfigurationService.ts · PythonEnvironmentService.ts +│ │ ├── LegacyPlanMigration.ts +│ │ ├── BackendService.ts · BackendShutdownService.ts +│ │ └── GuiUpdatePolicy.ts # 更新版本和频道策略 │ └── pythonEnv/ # Python 环境管理子模块 +│ ├── backendRequirement.ts # 支持的后端版本范围 +│ ├── backendContractProbe.ts # 外部后端能力探测 │ ├── context.ts # 共享上下文与缓存状态 +│ ├── dependencies.ts # Python 依赖声明 │ ├── finder.ts # Python 可执行文件发现 +│ ├── environment.ts # 统一安装、检查和启动环境 +│ ├── cuda.ts # CUDA 路径与环境变量 │ ├── envCheck.ts # 环境验证主流程 │ ├── installer.ts # Python 安装与依赖管理 │ ├── updater.ts # autowsgr 自动更新 @@ -74,14 +110,17 @@ AutoWSGR-GUI/ │ └── index.ts # 聚合导出 ├── src/ # 渲染进程 (MVC) │ ├── controller/ # 控制器(6 个子目录) -│ │ ├── app/ # 主控制器:AppController · ConfigController · SchedulerBinder · rendering · theme · constants +│ │ ├── app/ # 应用控制器:AppController · ConfigController · SchedulerBinder · AutomaticDecisiveTask │ │ ├── startup/ # 启动流程:StartupController · connection · envAndUpdates -│ │ ├── plan/ # 方案控制器:PlanController · importExport · nodeEditor · presetFlow · rendering -│ │ ├── taskGroup/ # 任务组:TaskGroupController · addItems · contextMenu · importExport · metaLoader · queueLoader +│ │ ├── plan/ # 方案控制器:PlanController · BattlePlanLoaderController · FleetPlannerController · DecisivePlanController +│ │ ├── taskGroup/ # 任务组:TaskGroupController · DailyTaskLoaderController · queueLoader │ │ ├── template/ # 模板:TemplateController · crud · selectors · useTemplate · wizard -│ │ └── shared/ # 共享基接口:ControllerHost · DialogHelper +│ │ └── shared/ # 共享 UI 边界:DialogHelper +│ ├── adapter/ # YAML/JSON · IPC · HTTP/WS · Storage │ ├── model/ # 数据模型 + 业务服务 +│ │ ├── fleet/ # FleetDraft · DecisiveFleetDraft · FleetPresetIdentity · FleetRuleMapper │ │ ├── scheduler/ # 调度子模块:Scheduler · CronScheduler · TaskQueue · ExpeditionTimer · StopConditionChecker · RepairManager +│ │ ├── statistics/ # 今日出征统计 │ │ ├── ApiClient.ts # HTTP/WebSocket 后端通信 │ │ ├── ConfigModel.ts # 配置数据模型 │ │ ├── PlanModel.ts # 方案解析/序列化 @@ -90,36 +129,73 @@ AutoWSGR-GUI/ │ │ └── MapDataLoader.ts # 地图数据加载与缓存 │ ├── view/ # UI 视图(8 个子目录) │ │ ├── main/ # 主页面 Facade:MainView · LogView · TaskQueueView · StatusBar -│ │ ├── plan/ # 方案预览 Facade:PlanPreviewView · MapView · NodeEditorView · FleetPresetView · FleetEditDialog +│ │ ├── plan/ # 方案与舰队:PlanPreviewView · FleetPlannerView · FleetEditorView · FleetGalleryView · PlanManagementView │ │ ├── config/ # 配置页:ConfigView │ │ ├── taskGroup/ # 任务组:TaskGroupView │ │ ├── template/ # 模板:TemplateLibraryView · TemplateWizardView · SelectorDialog │ │ ├── setup/ # 初始化向导:SetupWizardView -│ │ ├── shared/ # 共享组件:ShipAutocomplete +│ │ ├── shared/ # 共享组件:ShipAutocomplete · AnimatedSelect │ │ └── styles/ # SCSS 样式(base/ · components/ · pages/) -│ ├── types/ # TypeScript 类型定义 -│ │ ├── api.ts # API / WebSocket 通信类型 -│ │ ├── electronBridge.ts # IPC 桥接口 -│ │ ├── model.ts # 业务实体类型(PlanData 等) +│ ├── types/ # 6 个按完整领域合并的类型文件 +│ │ ├── api.ts # API / WebSocket DTO +│ │ ├── ipc.ts # IPC DTO、ElectronBridge 和 Window 声明 +│ │ ├── model.ts # Plan、Config、Template、Repair 领域类型 +│ │ ├── view.ts # 页面 ViewObject 和表单值 │ │ ├── scheduler.ts # 调度器公共类型 -│ │ └── view.ts # ViewObject 接口 -│ ├── data/ # 静态数据(舰船数据库) +│ │ └── statistics.ts # 今日出征统计类型 +│ ├── shared/ # 决战、胖次和舰种等跨层稳定契约 +│ ├── data/ # 舰船静态 JSON;运行时规则位于 model/fleet │ └── utils/ # 工具类(Logger) ├── resource/ # 只读资源 -│ ├── builtin_plans/ # 内置战斗方案 (.yaml) +│ ├── system_battle_plans/ # 系统作战计划 (.yaml) +│ ├── system_team_plans/ # 系统编队计划 (.yaml) +│ ├── system_daily_plans/ # 演习、战役和决战日常计划 +│ ├── user_battle_plans/ # 兼容旧目录,运行时不写入 +│ ├── user_team_plans/ # 兼容旧目录,运行时不写入 +│ ├── migrations/v6/ # 已下架系统计划的只读迁移快照 +│ ├── ship-library/ # 打包舰船资料库种子 │ ├── builtin_templates.json # 内置模板 │ ├── maps/ # 地图 JSON(节点坐标、连线) │ └── images/ # 图片资源 -├── templates/ # 用户自定义模板 -├── plans/ # 用户战斗方案目录 +├── templates/ # 用户自定义模板(历史兼容来源) ├── scripts/ # 构建脚本 ├── build/ # electron-builder 配置 -├── usersettings.yaml # 用户配置文件 -├── gui_settings.json # GUI 级配置(端口等) -├── task_groups.json # 任务组持久化 +├── usersettings.yaml # 历史兼容来源,运行时写入 userData +├── gui_settings.json # 历史兼容来源,运行时写入 userData +├── task_groups.json # 历史兼容来源,运行时写入 userData └── package.json # 项目配置 ``` +用户可变计划、舰队、日常计划、设置和迁移状态写入 Electron `userData`: +`user_battle_plans/`、`user_team_plans/`、`user_daily_plans/`、`gui_settings.json` 和 +`.migration-state.json`。安装目录和 `resource/` 只读;v5 迁移会合并当前 +安装目录的旧设置、任务组和模板,递归识别有效计划 YAML,并按新规范重命名 +后纳入 GUI 管理。不同内容的同名配置保存为“(旧版)”副本,源文件始终保留。 +当前迁移状态版本为 v6:在 v5 计划迁移完成项之上,继续升级系统预设库存、 +旧系统计划引用和胖次稳定计划标识。只有 v6 操作全部成功才写入版本 6;失败项 +在下次启动重试。本次存在实际迁移项时,主窗口创建后会显示成功、失败数量和 +失败文件说明。 + +### 主进程依赖方向 + +```text +main.ts + ↓ +ipc/*Ipc.ts + ↓ +Service + ↓ +Codec / Repository + ↓ +AppPaths / SafePathService / AtomicFileStore +``` + +`main.ts` 不包含 IPC 业务分支,只创建一次实例并保持原启动顺序。可变状态有 +唯一所有者:后端子进程在 `BackendService`,资料库更新互斥在 +`ShipLibraryUpdater`,运行时计划序号在 `RuntimePlanService`,Python 发现缓存 +在 `pythonEnv/context.ts`。配置服务每次读取唯一的 `gui_settings.json`,不建立 +第二份内存配置。 + --- ## 启动流程 @@ -171,12 +247,12 @@ sequenceDiagram ## 关键架构模式 -### ControllerHost 依赖注入 +### 最小 Host 依赖注入 -子控制器(PlanController / TaskGroupController 等)不直接依赖 AppController,而是通过 `ControllerHost` 接口访问共享能力: +子控制器不直接依赖 `AppController`,而是在所属功能模块中声明最小 Host,例如: ```typescript -interface ControllerHost { +interface PlanHost { readonly scheduler: Scheduler; plansDir: string; renderMain(): void; @@ -184,11 +260,14 @@ interface ControllerHost { } ``` -各子控制器还定义自己的扩展 Host 接口(如 `StartupHost`、`TaskGroupHost`),由 AppController 实现。详见 [Controller 层](01-controller-layer.md)。 +`PlanHost`、`StartupHost`、`TaskGroupHost` 等接口彼此独立,不再存在通用 +`ControllerHost` 基接口。详见 [Controller 层](01-controller-layer.md)。 ### ViewObject 单向数据传递 -Controller 从 Model 提取数据,拼装为 **ViewObject**(定义在 `src/types/view.ts`),单向传递给 View 渲染。View 不直接访问 Model,保证视图层的纯净。 +Controller 从 Model 提取数据,拼装为 **ViewObject**(定义在 +`src/types/view.ts`),单向传递给 View 渲染。View 不访问有状态 Model;类型、 +不可变目录和无状态领域函数可以直接复用,但不能成为第二个状态所有者。 ``` Model → Controller.extractViewObject() → ViewObject → View.render(vo) @@ -196,7 +275,13 @@ Model → Controller.extractViewObject() → ViewObject → View.render(vo) ### View Facade 模式 -大型视图组件采用 Facade 模式:`MainView` 持有 `LogView` / `TaskQueueView` / `StatusBar`,`PlanPreviewView` 持有 `MapView` / `NodeEditorView` / `FleetPresetView`。Controller 只与 Facade 交互,无需感知内部拆分。 +大型视图组件采用 Facade 模式:`MainView` 组合日志、队列和状态栏; +`PlanPreviewView` 组合地图和节点编辑;`FleetPlannerView` 组合舰队编辑、规则、 +图鉴、计划管理和方案选择。普通舰队草稿只由 `FleetPlannerController` 的单个 +`FleetDraft` 持有,决战草稿由 `DecisivePlanController` 的 +`DecisiveFleetDraft` 独立持有。舰船库读取、编队计划 DTO 转换、保存覆盖和 +`file/source` identity 都在 Controller/Model;Fleet View 只接收 ViewObject 和 +不透明计划 ID,并上报用户意图。 ### 优先级任务队列 @@ -208,9 +293,15 @@ Model → Controller.extractViewObject() → ViewObject → View.render(vo) | `USER_TASK` | 10 | 用户手动添加的战斗任务 | | `DAILY` | 20 | 定时触发的日常任务 | +每个队列轮次使用独立 `id`,同一有限或无限任务的后触发轮次共享稳定 +`logicalId`。单轮完成、逻辑任务完成和逻辑取消是三个不同事件,cron/pending +状态只在逻辑任务结束或明确取消时清理。 + ### 本地 Python 隔离 -所有 Python 包安装到 `{appRoot}/python/site-packages/`,不污染全局 Python 环境。通过 `.env_ready` 标记文件缓存环境状态,实现 < 100ms 的后续启动检查。 +managed 模式和内置 Python 使用 `{appRoot}/python/site-packages/`;external +模式使用用户选定解释器时,依赖安装到该解释器自身环境。安装、依赖检查、 +CUDA 检测和后端启动复用同一环境描述。通过 `.env_ready` 标记缓存已验证环境。 ### 双层通信 @@ -223,13 +314,14 @@ Model → Controller.extractViewObject() → ViewObject → View.render(vo) 类型定义从各模块提取为独立的 `src/types/` 层,按领域划分: -| 文件 | 内容 | 被谁引用 | +| 路径 | 内容 | 被谁引用 | |------|------|----------| | `api.ts` | API 响应、TaskRequest、WebSocket 消息类型 | ApiClient、Controller | -| `electronBridge.ts` | IPC 桥接口 `ElectronBridge` | Controller、StartupController | -| `model.ts` | 业务实体:PlanData、NodeArgs、FleetPreset、StopCondition | Model、Controller | +| `ipc.ts` | IPC DTO、`ElectronBridge` 与全局 Window 声明 | Adapter、Controller | +| `model.ts` | Plan、Config、Template、Repair 领域类型 | Model、Controller | | `scheduler.ts` | 调度器:TaskPriority、SchedulerTask、SchedulerCallbacks | Scheduler、SchedulerBinder | -| `view.ts` | ViewObject:MainViewObject、PlanPreviewViewObject 等 | Controller → View | +| `statistics.ts` | 今日出征次数、评级和掉落提示 | DailySortieStats、SchedulerBinder、View | +| `view.ts` | Main、Plan、Config、Template、TaskGroup ViewObject | Controller → View | --- @@ -237,7 +329,7 @@ Model → Controller.extractViewObject() → ViewObject → View.render(vo) | 文档 | 功能域 | |------|--------| -| [Controller 层](01-controller-layer.md) | ControllerHost/DI 模式 · 6 个子目录结构 · StartupController 启动编排 | +| [Controller 层](01-controller-layer.md) | 最小 Host 接口 · 6 个子目录结构 · StartupController 启动编排 | | [任务调度系统](02-task-scheduling.md) | Scheduler · TaskQueue · CronScheduler · ExpeditionTimer · StopCondition · RepairManager | | [配置系统](03-configuration.md) | ConfigModel · ConfigView · usersettings.yaml · gui_settings.json | | [出击计划系统](04-battle-plan.md) | PlanModel · PlanController · PlanPreviewView(Facade) · MapDataLoader | @@ -245,3 +337,5 @@ Model → Controller.extractViewObject() → ViewObject → View.render(vo) | [后端通信](06-backend-communication.md) | IPC Bridge · ApiClient · REST API · WebSocket 事件 | | [环境管理](07-environment-management.md) | Python 发现/安装 (pythonEnv/) · 模拟器检测 · 后端生命周期 | | [开发环境搭建](08-dev-setup.md) | 依赖安装 · 开发/构建/打包命令 · SCSS 架构 · 调试技巧 | +| [`src` TypeScript 模块目录](09-src-typescript-catalog.md) | 当前 107 个 TS 文件和逐文件职责 | +| [当前运行时边界 ADR](10-runtime-boundaries-adr.md) | 存储 · IPC 权限 · 迁移 · 调度身份 · 更新安装决策 | diff --git a/docs/architecture/01-controller-layer.md b/docs/architecture/01-controller-layer.md index f608ee9..1f56355 100644 --- a/docs/architecture/01-controller-layer.md +++ b/docs/architecture/01-controller-layer.md @@ -1,20 +1,21 @@ # Controller 层 -> 涉及文件:`src/controller/` 全部 6 个子目录(33 个文件) +> 涉及文件:`src/controller/` 的 6 个业务子目录 ## 概述 -Controller 层采用 **Host 接口依赖注入** 模式组织。`AppController` 作为唯一的根控制器,实现各子控制器定义的 Host 接口,子控制器通过 Host 访问调度器、页面切换等共享能力,避免子控制器之间的直接依赖。 +Controller 层采用 **最小 Host 接口依赖注入** 模式组织。`AppController` 是唯一 +Renderer 组合根,创建子控制器时传入只包含所需能力的对象。 每个子控制器内部进一步按职责拆分为多个模块文件,主控制器类保持精简("瘦身版"),核心逻辑委托给同目录下的模块。 --- -## ControllerHost — 基础 Host 接口 +## 最小 Host 接口 ```typescript -// src/controller/shared/ControllerHost.ts -interface ControllerHost { +// src/controller/plan/PlanController.ts +interface PlanHost { readonly scheduler: Scheduler; plansDir: string; renderMain(): void; @@ -22,7 +23,9 @@ interface ControllerHost { } ``` -这是所有子控制器的最小依赖接口。各子控制器根据自身需求定义扩展的 Host 接口(如 `StartupHost`、`PlanHost`、`TaskGroupHost`),AppController 统一实现。 +`StartupHost`、`PlanHost`、`TaskGroupHost` 等接口由各功能模块自行声明,不继承 +公共基接口。已删除无实际调用方的 `controller/shared/ControllerHost.ts` 和 +Controller 目录 barrel,内部代码使用明确模块路径。 --- @@ -32,7 +35,6 @@ interface ControllerHost { | 文件 | 职责 | |------|------| -| `ControllerHost.ts` | 基础 Host 接口定义 | | `DialogHelper.ts` | 通用对话框工具(`showPrompt` / `showConfirm` / `showAlert`) | --- @@ -44,12 +46,15 @@ interface ControllerHost { | 文件 | 职责 | |------|------| | `AppController.ts` | 根控制器类:初始化子控制器、实现 Host 接口、协调全局状态 | +| `AutomaticDecisiveTask.ts` | 分别从用户决战计划和系统预设构造自动决战请求 | | `ConfigController.ts` | 配置保存逻辑:从表单收集 → 更新 ConfigModel → 同步 CronScheduler/Scheduler → 写文件 | -| `SchedulerBinder.ts` | 调度器回调绑定:将 Scheduler/CronScheduler 的回调连接到 UI 更新,管理远征/演习/战役等待中任务的 ID 跟踪 | +| `SettingsController.ts` | 设置页操作:环境与 ADB 检测、舰船库、更新检查和主题交互 | +| `NavigationController.ts` | 页面和方案标签导航 | +| `OperationsController.ts` | 远征收取和奖励领取等常用操作 | +| `SchedulerBinder.ts` | 绑定 Scheduler/CronScheduler 回调,按稳定 `logicalId` 管理等待任务并维护今日出征统计 | | `rendering.ts` | 渲染分发:构建 `MainViewObject` → 调用 `MainView.render()` | | `theme.ts` | 主题管理:亮色/暗色/自动切换、强调色应用 | | `constants.ts` | 常量定义 | -| `index.ts` | 聚合导出 | **SchedulerBinder Host 接口**: @@ -59,6 +64,7 @@ interface SchedulerBinderHost { readonly cronScheduler: CronScheduler; readonly api: ApiClient; readonly templateModel: TemplateModel; + readonly configModel: ConfigModel; renderMain(): void; updateOpsAvailability(connected: boolean): void; } @@ -72,10 +78,9 @@ interface SchedulerBinderHost { | 文件 | 职责 | |------|------| -| `StartupController.ts` | 启动流程主编排:路径获取 → 配置加载 → 模拟器检测 → 首次引导 → 环境检查 → 后端连接 | +| `StartupController.ts` | 启动流程主编排,并持有后端心跳和自动重启定时器 | | `envAndUpdates.ts` | 环境检查与更新:调用 IPC `checkEnvironment()` / `installDeps()` / `checkForUpdates()` | | `connection.ts` | 后端连接:`waitForBackendAndConnect()` 轮询等待后端 HTTP 就绪,然后发送系统启动请求 | -| `index.ts` | 聚合导出 | **StartupHost 接口**(由 AppController 实现): @@ -128,16 +133,18 @@ flowchart TD ### controller/plan/ — 方案控制器 -管理方案的导入/导出/编辑和预览渲染。 +管理受管方案的新建、加载、编辑、保存和预览渲染。 | 文件 | 职责 | |------|------| | `PlanController.ts` | 方案子控制器类:持有当前方案状态,协调下属模块 | -| `importExport.ts` | 方案文件的导入/导出/新建流程 | +| `BattlePlanLoaderController.ts` | 独立持有受管方案选择器状态,加载、筛选并返回最终选择 | +| `DecisivePlanController.ts` | 持有决战舰队草稿,协调设置读取、编辑和保存 | +| `FleetPlannerController.ts` | 持有普通舰队草稿,协调舰船库、计划持久化、覆盖冲突和文件 identity | | `presetFlow.ts` | 任务预设的导入/查看/关闭/执行流程 | | `nodeEditor.ts` | 节点编辑器:从 UI 收集节点阵型/夜战/索敌规则并写回 PlanData | | `rendering.ts` | 构建 `PlanPreviewViewObject`,协调地图数据和方案数据的合并 | -| `index.ts` | 聚合导出 | +| `selectedNodes.ts` | 规范化节点选择顺序并生成后端需要的节点列表 | **PlanHost 接口**: @@ -160,11 +167,12 @@ interface PlanHost { |------|------| | `TaskGroupController.ts` | 任务组子控制器类:绑定视图事件,协调下属模块 | | `addItems.ts` | 向任务组添加项目:从当前方案/文件/预设添加 | +| `DailyTaskLoaderController.ts` | 管理日常任务浮窗的分类、参数和加入列表/队列动作 | +| `TaskListLoaderController.ts` | 解析外部任务列表并协调批量载入 | +| `managedPlanReader.ts` | 读取受管作战计划并统一用户/系统来源信息 | | `queueLoader.ts` | 加载任务组到调度队列:逐项构建 TaskRequest → `Scheduler.addTask()` | | `metaLoader.ts` | 加载任务项的元数据(方案标题、模板名称)用于 UI 显示 | | `contextMenu.ts` | 右键上下文菜单:编辑/删除/复制任务项 | -| `importExport.ts` | 任务组的导入/导出 | -| `index.ts` | 聚合导出 | **TaskGroupHost 接口**: @@ -181,6 +189,8 @@ interface TaskGroupHost { closePresetDetail(): void; executePreset(): void; getCurrentPresetInfo(): { preset: TaskPreset; filePath: string } | null; + pickManagedBattlePlan(): Promise; + openManagedPlan(file: string, source: PlanPresetSource): Promise; } ``` @@ -197,7 +207,6 @@ interface TaskGroupHost { | `useTemplate.ts` | "使用模板"流程:展示选项弹窗 → 添加到任务组 / 加入队列 / 直接执行 | | `selectors.ts` | 选择弹窗:方案选择、战役选择、舰队选择、决战章节选择 | | `crud.ts` | 模板的编辑/删除/重命名/批量导入 | -| `index.ts` | 聚合导出 | --- @@ -210,23 +219,21 @@ graph TD Plan["controller/plan/
PlanController"] TG["controller/taskGroup/
TaskGroupController"] Tpl["controller/template/
TemplateController"] - Shared["controller/shared/
ControllerHost"] - AppCtrl -->|"实现"| Shared AppCtrl -->|"创建 & 持有"| Plan AppCtrl -->|"创建 & 持有"| TG AppCtrl -->|"创建 & 持有"| Tpl AppCtrl -->|"创建 & 持有"| Startup - Plan -->|"通过 PlanHost"| AppCtrl - TG -->|"通过 TaskGroupHost"| AppCtrl - Startup -->|"通过 StartupHost"| AppCtrl + AppCtrl -->|"注入 PlanHost"| Plan + AppCtrl -->|"注入 TaskGroupHost"| TG + AppCtrl -->|"注入 StartupHost"| Startup - Plan -.->|"无直接依赖"| TG - Plan -.->|"无直接依赖"| Tpl + Plan -.->|"显式功能依赖"| Tpl ``` -**关键设计**:Plan/TaskGroup/Template 之间没有直接依赖,需要跨子控制器协作时通过 Host 接口回调到 AppController,再由 AppController 分发。 +**关键设计**:共享能力通过最小 Host 注入;确有业务关系的模块使用明确 import 或 +回调,不通过通用根控制器或隐藏 barrel 获取依赖。 --- diff --git a/docs/architecture/02-task-scheduling.md b/docs/architecture/02-task-scheduling.md index 0c9216c..57b43b1 100644 --- a/docs/architecture/02-task-scheduling.md +++ b/docs/architecture/02-task-scheduling.md @@ -34,7 +34,7 @@ enum TaskPriority { EXPEDITION = 0, // 远征检查(最高) USER_TASK = 10, // 用户手动发起的战斗 - DAILY = 20, // 日常自动任务(演习/战役) + DAILY = 20, // 日常自动任务 } ``` @@ -44,13 +44,16 @@ enum TaskPriority { ```typescript interface SchedulerTask { - id: string; // 唯一标识 + id: string; // 当前单轮的唯一标识 + logicalId: string; // 所有后触发轮次共享的稳定逻辑任务标识 name: string; // 显示名称 type: SchedulerTaskType; // normal_fight | campaign | exercise | decisive | expedition priority: TaskPriority; request: TaskRequest; // 发送给后端的 API 请求体 remainingTimes: number; // 剩余执行次数 totalTimes: number; // 总次数(用于显示进度) + unlimited?: boolean; // 无限任务每轮后继续,但 logicalId 不变 + backendTaskId?: string; // 当前轮对应的后端任务 ID stopCondition?: StopCondition; // 可选的提前终止条件 bathRepairConfig?: BathRepairConfig; // 可选的泡澡修理配置 fleetPresets?: FleetPreset[]; // 可轮换的编队预设列表 @@ -62,9 +65,9 @@ interface SchedulerTask { #### 生产者 三类生产者向队列添加任务: -1. **用户手动**:通过 UI 导入方案或从任务组加载(`USER_TASK` 优先级) -2. **定时触发**:`CronScheduler` 在刷新时间点生成演习/战役任务(`DAILY` 优先级) -3. **后触发**:任务完成后,若 `remainingTimes > 1` 则自动追加下一轮(保持原优先级) +1. **用户手动**:通过 UI 选择受管方案或从任务组加载(`USER_TASK` 优先级) +2. **定时触发**:`CronScheduler` 生成演习、战役、出击、决战和胖次任务(`DAILY` 优先级) +3. **后触发**:单轮完成后按剩余次数或无限标记追加下一轮;新轮次生成新 `id`,但继承原 `logicalId` #### 消费流程 @@ -88,9 +91,9 @@ flowchart TD J -->|否| N N --> O[状态=running, 等待后端完成] O --> P{成功?} - P -->|是| Q{remainingTimes > 1?} + P -->|是| Q{还有剩余次数
或无限任务?} Q -->|是| R[后触发: 重新入队] - Q -->|否| S[任务完成, 消费下一个] + Q -->|否| S[逻辑任务完成, 消费下一个] P -->|否| T{retryCount < maxRetries?} T -->|是| U[retryCount++, 5s 后重试] T -->|否| V[任务失败, 消费下一个] @@ -98,6 +101,26 @@ flowchart TD --- +### 任务身份与生命周期 + +调度器严格区分物理轮次和逻辑任务: + +| 事件 | 标识 | 含义 | +|------|------|------| +| `onTaskCompleted` | `id` | 一轮后端任务结束,可继续生成后触发轮次 | +| `onLogicalTaskCompleted` | `logicalId` | 已无后触发、满足停止条件或重试耗尽,整个逻辑任务结束 | +| `onLogicalTaskCanceled` | `logicalId` | 用户删除、清空队列或系统停止;不等同于成功或失败 | + +有限和无限任务的每个后触发轮次都有新的 `id`,但共享创建任务时生成的 +`logicalId`。`SchedulerBinder` 只在逻辑完成时清理 cron/pending 状态;单轮 +完成只重置当前进度。取消时还会根据原因区分行为:用户删除或清空表示主动 +放弃,`system_stopped` 只释放 pending,允许下次启动重新触发。 + +延迟间隔、失败重试和修理等待中的任务也属于同一逻辑任务。删除或清空操作会 +同时检查运行中、就绪队列和等待队列,保证一个 `logicalId` 的停止语义可追踪。 + +--- + ### CronScheduler — 定时触发 `CronScheduler` 每分钟检查一次系统时间,在特定时间点自动向 `Scheduler` 添加日常任务。 @@ -109,21 +132,38 @@ flowchart TD | 演习 | 0:00 / 12:00 / 18:00 后 | `localStorage` 记录**实际完成**时间戳 | | 战役 | 每日 0:00 后 | `localStorage` 记录完成日期 (YYYY-MM-DD) | | 常规出击 | 每日 0:00 后 | 同上 | +| 决战 | 每日 0:00 后 | 实际任务结束后记录完成日期 | | 刷战利品 | 每日 0:00 后 | 同上 | | 定时方案 | YAML 中 `scheduled_time: "HH:MM"` | 当日 `firedToday` 标志 | **关键设计**:记录的是任务**实际完成**的时间戳而非"是否已触发"。这样即使 App 因 ADB 断开等原因重启,只要任务未真正完成,下次启动后仍会补发。 +自动决战固定生成一轮(`decisive_rounds: 1`),不查询或推测剩余票数。来源 +只能是 `user_plan` 或 `system_preset`:前者读取决战计划页保存的 +`decisive_plan`,后者读取只读系统预设 `builtin_decisive_6`。任务入队前读取 +或配置失败会清除 pending,允许后续 tick 重试;逻辑任务实际结束后,无论成功 +或失败,当天都不再重复,以免失败前已消耗票数。 + #### 事件回调 `CronScheduler` 通过回调通知 `AppController`,由 Controller 调用 `Scheduler.addTask()` 入队: ```typescript +import type { LootPlanSource } from '../../src/shared/lootPlans'; +import type { + DecisiveAutomationSource, +} from '../../src/shared/decisiveAutomation'; + interface CronCallbacks { onExerciseDue?: (fleetId: number) => void; onCampaignDue?: (campaignName: string, times: number) => void; onNormalFightDue?: () => void; - onLootDue?: (planIndex: number, stopCount: number) => void; + onDecisiveDue?: (source: DecisiveAutomationSource) => void; + onLootDue?: ( + source: LootPlanSource, + planId: string, + stopCount: number, + ) => void; } ``` @@ -217,7 +257,7 @@ graph LR ## 与其他系统的关系 - **Controller 层**:`SchedulerBinder`(`controller/app/SchedulerBinder.ts`)封装 Scheduler/CronScheduler 的回调绑定,管理待完成任务的 ID 跟踪 -- **配置系统**:`CronScheduler` 的触发规则来自 `usersettings.yaml` 的 `daily_automation` 字段;远征间隔 (`expedition_interval`) 同步到 `ExpeditionTimer` +- **配置系统**:开关类后端设置来自 `usersettings.yaml.daily_automation`;远征间隔、战役次数、自动决战和胖次设置来自 `gui_settings.json.automation` - **模板与任务组**:任务组通过 `loadGroupToQueue()`(`controller/taskGroup/queueLoader.ts`)批量向 `Scheduler` 添加任务 - **出击计划**:方案解析后构建 `TaskRequest`,通过 `Scheduler.addTask()` 入队 - **后端通信**:`Scheduler` 持有 `ApiClient` 引用,通过 REST API 发起任务、通过 WebSocket 接收进度和完成通知 diff --git a/docs/architecture/03-configuration.md b/docs/architecture/03-configuration.md index 485a92e..990e92f 100644 --- a/docs/architecture/03-configuration.md +++ b/docs/architecture/03-configuration.md @@ -1,6 +1,6 @@ # 配置系统 -> 涉及文件:`src/model/ConfigModel.ts` · `src/view/config/ConfigView.ts` · `src/controller/app/ConfigController.ts` · `src/controller/app/theme.ts` · `src/types/model.ts` · `usersettings.yaml` · `gui_settings.json` +> 涉及文件:`src/model/ConfigModel.ts` · `src/view/config/ConfigView.ts` · `src/controller/app/ConfigController.ts` · `src/controller/app/theme.ts` · `src/types/model.ts` · `electron/services/GuiSettingsStore.ts` · `electron/services/GuiConfigurationService.ts` · `electron/ipc/ConfigurationIpc.ts` ## 概述 @@ -8,8 +8,8 @@ | 层级 | 文件 | 内容 | 读写方式 | |------|------|------|----------| -| **GUI 级** | `gui_settings.json` | 后端端口、Python 路径 | Electron 主进程直接读写 | -| **用户级** | `usersettings.yaml` | 模拟器、账号、日常自动化 | 渲染进程通过 IPC 读写 | +| **GUI 级** | `userData/gui_settings.json` | 后端、Python、CUDA、窗口和 GUI 自动化设置 | Electron 配置服务读写 | +| **用户级** | `userData/usersettings.yaml` | 模拟器、账号、日常自动化 | 渲染进程通过 IPC 读写 | 另外,主题/调试模式等纯 UI 偏好存储在浏览器 `localStorage` 中。 @@ -47,20 +47,40 @@ interface UserSettings { | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| -| `auto_expedition` | `boolean` | `true` | 自动远征 | -| `expedition_interval` | `number` | `15` | 远征检查间隔(分钟,1~120) | +| `auto_expedition` | `boolean` | `false` | 自动远征 | +| `auto_gain_bonus` | `boolean` | `false` | 自动领取奖励 | +| `auto_bath_repair` | `boolean` | `false` | 自动浴室修理 | +| `auto_set_support` | `boolean` | `false` | 自动设置支援 | +| `bath_repair_blacklist` | `string[]` | `[]` | 浴室修理黑名单 | | `auto_exercise` | `boolean` | `false` | 自动演习 | -| `exercise_fleet_id` | `number` | `1` | 演习舰队 (1~4) | +| `exercise_fleet_id` | `number?` | `null` | 演习舰队 (1~4) | | `auto_battle` | `boolean` | `false` | 自动战役 | | `battle_type` | `string` | `"困难潜艇"` | 战役类型 | -| `battle_times` | `number` | `3` | 战役次数 | | `auto_normal_fight` | `boolean` | `false` | 自动常规出击 | -| `auto_decisive` | `boolean` | `false` | 自动决战 | -| `decisive_ticket_reserve` | `number` | `0` | 决战票保留数 | -| `decisive_template_id` | `string` | `""` | 决战模板 ID | -| `auto_loot` | `boolean` | `false` | 自动刷战利品 | -| `loot_plan_index` | `number` | `0` | 战利品方案索引 | -| `loot_stop_count` | `number` | `50` | 战利品停止数量 | +| `normal_fight_tasks` | `NormalFightTaskConfig[]` | `[]` | 自动出击任务及顺序 | +| `quick_repair_limit` | `number?` | `null` | 快修限制 | +| `stop_max_ship` | `boolean` | `false` | 船坞满时停止 | +| `stop_max_loot` | `boolean` | `false` | 战利品满时停止 | + +#### GuiAutomationSettings — GUI 自动化 + +这些字段仅属于 GUI 调度器,保存在 `gui_settings.json.automation`,不会写回 +`usersettings.yaml`: + +| 字段 | 类型 | 默认 | 说明 | +|------|------|------|------| +| `expeditionInterval` | `number` | `15` | 远征检查间隔(分钟,1~120) | +| `battleTimes` | `number` | `3` | 每日战役次数 | +| `autoDecisive` | `boolean` | `false` | 每日自动决战 | +| `decisiveTemplateId` | `string` | `"builtin_decisive_6"` | 决战模板 ID | +| `autoLoot` | `boolean` | `false` | 每日自动刷胖次 | +| `lootPlanId` | `string` | 内置稳定 ID | 系统出征计划文件名 | +| `lootStopCount` | `number` | `50` | 胖次停止数量(1~50) | + +旧版 YAML 中的 `expedition_interval`、`battle_times`、胖次字段和决战字段会在 +启动时迁移。`auto_decisive` 与 `decisive_template_id` 升级为正式 automation; +`decisive_ticket_reserve` 只原样归档到 `legacy_decisive_automation`,不参与 +执行轮数。独立的 `decisive_plan` 不会被旧字段覆盖。 --- @@ -84,8 +104,8 @@ interface UserSettings { ```mermaid flowchart LR subgraph Storage["持久化存储"] - YAML["usersettings.yaml"] - GUI["gui_settings.json"] + YAML["userData/usersettings.yaml"] + GUI["userData/gui_settings.json"] LS["localStorage"] end @@ -105,7 +125,7 @@ flowchart LR subgraph Side["副作用"] Cron["CronScheduler"] Sched["Scheduler"] - IPC["Electron IPC"] + IPC["Electron IPC / GuiConfigurationService"] end YAML -->|"bridge.readFile()"| AC @@ -117,6 +137,8 @@ flowchart LR CC -->|"update()"| CM CM -->|"toYaml()"| CC CC -->|"bridge.saveFile()"| YAML + CC -->|"setGuiAutomation()"| IPC + IPC -->|"顶层浅合并"| GUI CC -->|"updateConfig()"| Cron CC -->|"setExpeditionInterval()"| Sched AC -->|"setBackendPort()"| IPC @@ -136,7 +158,8 @@ flowchart LR 3. `ConfigController.saveConfig()` 执行以下操作: - 保存 UI 偏好到 `localStorage`(主题、调试模式) - 调用 `bridge.setBackendPort()` 更新端口(需重启生效) - - 调用 `ConfigModel.update()` 深合并 + - 调用 `ConfigModel.update()` 深合并后端配置 + - 调用 `setGuiAutomation()` 保存 GUI 私有自动化字段 - 同步 `CronScheduler.updateConfig()` 更新定时任务规则 - 同步 `Scheduler.setExpeditionInterval()` 更新远征检查间隔 - 序列化写入 `usersettings.yaml` @@ -177,25 +200,52 @@ flowchart LR ## gui_settings.json -由 Electron 主进程直接管理的配置: +由 Electron 主进程直接管理的配置,位于 Electron `userData`: ```json { "backend_port": 8438, - "python_path": null + "python_path": "", + "update_mode": "auto", + "backend_startup_mode": "managed", + "backend_repo_path": "", + "ocr_gpu_mode": "auto", + "cuda_path": "", + "save_backend_screenshots": false, + "automation": { + "expeditionInterval": 15, + "battleTimes": 3, + "autoDecisive": false, + "decisiveTemplateId": "builtin_decisive_6", + "autoLoot": false, + "lootPlanId": "bettle-old-8-5AI六潜胖次.yaml", + "lootStopCount": 50 + } } ``` - `backend_port`:Python 后端 HTTP 服务端口 -- `python_path`:用户手动指定的 Python 路径(`null` = 自动检测) - -读写在主进程 `main.ts` 中通过 `readGuiSettings()` / `writeGuiSettings()` 完成,渲染进程通过 IPC (`get-backend-port-sync`, `set-backend-port`, `get-python-path`, `set-python-path`) 间接访问。 +- `python_path`:用户手动指定的 Python 路径(空字符串 = 自动检测) +- `backend_startup_mode`:`managed` 使用 GUI 管理的环境,`external` 使用指定仓库 +- `ocr_gpu_mode` / `cuda_path`:OCR GPU 模式与 CUDA 路径 +- `automation`:GUI 私有自动化设置 +- `decisive_plan`:决战计划页单独维护的当前配置 +- `legacy_decisive_automation`:旧决战字段无损归档 +- `window`:窗口位置与尺寸偏好 + +`GuiSettingsStore` 是唯一 JSON 存储入口,每次写入执行顶层浅合并,因此未参与 +本次更新的未知顶层字段不会丢失。`GuiConfigurationService` 负责默认值、边界、 +旧决战字段迁移和 Python 缓存清理;`ConfigurationIpc` 只保持通道契约。 + +渲染进程通过 `get-backend-port-sync`、`get-python-path-sync` 等同步 getter +读取启动配置,通过 `set-backend-port`、`set-python-path` 等异步 setter 写入。 +同步 getter 必须继续使用 `ipcMain.on` / `sendSync`,不能改成 Promise。 --- ## 与其他系统的关系 -- **任务调度**:`daily_automation` 字段直接驱动 `CronScheduler` 的触发规则和 `ExpeditionTimer` 的间隔 +- **任务调度**:`daily_automation` 提供后端自动化开关和出击任务;`automation` 提供远征间隔、战役次数、自动决战和胖次参数 - **环境管理**:`gui_settings.json` 中的 `python_path` 影响 Python 发现优先级 - **后端通信**:`backend_port` 决定 `ApiClient` 的连接地址 - **模拟器检测**:初始化时若 `emulator` 字段为空,自动调用 `detectEmulator()` 填充 diff --git a/docs/architecture/04-battle-plan.md b/docs/architecture/04-battle-plan.md index ddbf6b4..eccb773 100644 --- a/docs/architecture/04-battle-plan.md +++ b/docs/architecture/04-battle-plan.md @@ -1,6 +1,6 @@ # 出击计划系统 -> 涉及文件:`src/model/PlanModel.ts` · `src/controller/plan/`(PlanController · importExport · presetFlow · nodeEditor · rendering)· `src/view/plan/`(PlanPreviewView(Facade) · MapView · NodeEditorView · FleetPresetView · FleetEditDialog)· `src/model/MapDataLoader.ts` · `src/types/model.ts` · `resource/builtin_plans/` · `resource/maps/` +> 涉及文件:`src/model/PlanModel.ts` · `src/controller/plan/` · `src/view/plan/` · `src/model/MapDataLoader.ts` · `electron/services/CombatPlanCodec.ts` · `electron/services/CombatPlanRepository.ts` · `electron/services/PlanManagementService.ts` · `electron/services/RuntimePlanService.ts` · `electron/ipc/CombatPlanIpc.ts` · `resource/system_battle_plans/` · `resource/maps/` ## 概述 @@ -57,17 +57,28 @@ enemy_rules: ```typescript interface FleetPreset { name: string; - ships: (string | ShipFilter)[]; // 6 个舰位:具体舰名或模糊筛选 + ships: ShipSlot[]; // 6 个舰位:具体舰名、结构化规则或空位 } interface ShipFilter { + name?: string; + search_name?: string; nation?: string; // 国籍筛选 - ship_type?: string; // 舰型筛选 + ship_type?: string[]; // 舰型筛选 + candidates?: ShipRule[]; + min_level?: number; + max_level?: number; } + +type ShipSlot = string | ShipFilter | null; ``` 编队预设支持**具体舰名**(如 `"85工程"`)和**模糊筛选**(如 `{nation: "苏联", ship_type: "dd"}`),后者在执行时由 `resolveFleetPreset()` 解析为实际舰船。 +没有顶层 `name` 的槽位可以只包含结构化 `candidates`。此时候选项是平等 +替代项,GUI 不得把第一个候选提升为主选;只有旧版字符串候选列表才按旧 +格式推断第一项为主选。 + --- ## YAML 示例 @@ -150,8 +161,8 @@ interface MapNode { | 文件 | 职责 | |------|------| -| `PlanController.ts` | 主控制器:持有当前方案状态,协调下属模块 | -| `importExport.ts` | 方案文件的导入/导出/新建流程 | +| `PlanController.ts` | 主控制器:持有当前方案状态,协调编辑、保存和执行 | +| `BattlePlanLoaderController.ts` | 持有受管方案选择器状态并协调读取、筛选和选择结果 | | `presetFlow.ts` | 任务预设的导入/查看/关闭/执行流程 | | `nodeEditor.ts` | 从 UI 收集节点阵型/夜战/索敌规则并写回 PlanData | | `rendering.ts` | 构建 `PlanPreviewViewObject`,协调地图数据和方案数据的合并 | @@ -166,6 +177,77 @@ interface MapNode { | `NodeEditorView` | `view/plan/NodeEditorView.ts` | 节点详细编辑器(阵形、夜战、继续条件) | | `FleetPresetView` | `view/plan/FleetPresetView.ts` | 编队预设列表管理(添加、编辑、删除) | | `FleetEditDialog` | `view/plan/FleetEditDialog.ts` | 编队预设编辑弹窗(支持舰船自动补全) | +| `BattlePlanLoaderView` | `view/plan/BattlePlanLoaderView.ts` | 受管方案列表、筛选、预览和舰队选择弹窗 | + +### Fleet Planner — 舰队计划 + +普通舰队草稿的唯一所有者是 `FleetPlannerController` 中的单个 `FleetDraft`。 +`FleetPlannerView` 只组合完整业务子视图并转发用户意图: + +| 子视图 | 职责 | +|--------|------| +| `FleetEditorView` | 舰位、主选/备选和拖拽编辑 | +| `FleetRuleView` | 舰种、国籍、等级等规则输入 | +| `FleetGalleryView` | 图鉴筛选、排序和展示缓存 | +| `PlanManagementView` | 展示管理清单并上报重命名、删除和批量导出意图 | +| `TeamPlanLoaderView` | 系统/用户编队方案选择 | + +`FleetPlannerController` 通过 `FleetPlannerRepository` 读取舰船库和编队计划, +处理保存、同名覆盖确认、系统预设另存为用户副本,以及当前草稿的 +`file/source` identity。Controller 将持久化 `UserTeamPlan` 转换为 +`TeamPlanViewObject`,View 只接收不透明的计划 `id`,不知道真实文件路径。 + +`FleetDraft.fleetDraftToTeamPlan()` 和 `fleetDraftFromTeamPlan()` 是草稿与持久化 +DTO 的唯一双向转换边界。名称、舰种、等级和 candidate-only 规则在这里校验; +View 不拼装 YAML/IPC DTO,也不更新文件 identity。 + +决战舰队由 `DecisivePlanController` 的独立 `DecisiveFleetDraft` 管理,不与普通 +舰队共享草稿。`DecisivePlanController` 负责设置和舰船库 Repository 调用, +`DecisivePlanView` 只通过显式 Host 上报编辑和保存意图。两类 View 都不直接 +访问 Model、IPC 或持久化。 + +Fleet 规则集中在 `src/model/fleet/`:`ShipMatcher` 负责匹配和显示标签, +`FleetRuleMapper` 负责 API rule 映射,`ShipNameNormalizer` 负责后端舰名, +`FleetDraft` 负责编辑草稿和持久化编队 DTO 的转换。candidate-only 槽位不会 +生成顶层 `name`,槽位约束、候选顺序和各候选的舰种/等级规则均保持。 + +--- + +## 主进程计划流水线 + +作战计划在主进程按职责拆分,IPC Adapter 不直接解析或写 YAML: + +| 模块 | 职责 | +|------|------| +| `CombatPlanIpc` | 处理本地 YAML 选择、冲突确认及受管计划 IPC 边界 | +| `PlanManagementService` | 管理页汇总、导入升级、保存、重命名、删除和运行时准备 | +| `LegacyPlanMigration` | 启动时升级旧计划、拆分舰队并迁移任务组引用 | +| `CombatPlanCodec` | YAML 根校验、未知字段保留、编队引用拆分与展开 | +| `CombatPlanRepository` | 系统/用户目录、受管路径和原子文件操作 | +| `RuntimePlanService` | 展开舰队引用并写入当前进程的临时执行目录 | + +系统计划从只读 `resource/system_battle_plans/` 读取,用户计划写入 Electron +`userData/user_battle_plans/`,用户舰队写入 +`userData/user_team_plans/`。两类计划均通过 GUI 统一清单加载和管理。 +执行前生成的临时计划位于 +`/AutoWSGR-GUI/runtime_battle_plans//`,文件序号只由 +`RuntimePlanService` 持有。 + +保存计划时,内嵌 `fleet_presets` 被拆成独立编队文件;运行时再按来源优先级 +展开引用。YAML 根对象、文件开头注释和不认识的字段必须保留。 + +旧计划迁移状态 v5 会扫描旧 `plans/`、`resource/user_*`,并递归识别安装目录中的有效 +计划 YAML。旧 YAML 经当前 Codec 升级并按 `bettle-*`、`team-*` 规范重命名 +后写入用户目录,源文件保留。不同内容的同名计划或舰队保存为“(旧版)” +副本,计划中的舰队引用和旧任务组的受管文件名同步更新。完成项按旧来源路径 +和内容哈希记录,实际输出文件名也写入迁移状态;第二次启动不会重复处理或把 +引用改回默认文件名。若同名目标与升级结果一致,则直接复用目标,并保留用户 +之后修改的编队。 + +计划加载浮窗提供“添加本地 YAML”。用户显式选择的 YAML 会经过同一套 +Codec 升级、拆分舰队并按规范命名后写入用户受管目录,源文件保持不变; +同名目标必须由用户确认后才会覆盖。外部文件路径不会进入任务队列,运行时 +仍只读取受管计划。计划页不再提供独立的手工转换入口。 --- @@ -175,7 +257,7 @@ interface MapNode { flowchart TB subgraph Input["输入"] YAML["方案 YAML 文件"] - Builtin["内置方案
resource/builtin_plans/"] + Builtin["系统方案
resource/system_battle_plans/"] end subgraph Parse["解析"] @@ -209,17 +291,15 @@ flowchart TB --- -## 内置方案 +## 系统方案 -`resource/builtin_plans/` 包含 18 个预制方案: +`resource/system_battle_plans/` 当前包含周常和活动预制方案。活动计划从 +AutoWSGR 主库经 `PlanManagementService` 的现有升级链路导入: | 分类 | 数量 | 示例 | |------|------|------| -| 周常 | 11 | `周常1章-1-2.yaml` ~ `周常9章-9-2.yaml` | -| 捞胖次 | 4 | `捞胖次9-2.yaml`, `捞胖次7-4.yaml` | -| 战役 | 1 | `战役.yaml` | -| 演习 | 1 | `自动演习.yaml` | -| 决战 | 1 | `决战.yaml` | +| 周常 | 10 | `bettle-周常-1-1.yaml` ~ `bettle-周常-10-1.yaml` | +| 20260730 活动 | 4 | `bettle-E1炸鱼.yaml`、`bettle-E5夜战.yaml`、`bettle-H1炸鱼.yaml`、`bettle-H5夜战.yaml` | --- diff --git a/docs/architecture/05-template-and-taskgroup.md b/docs/architecture/05-template-and-taskgroup.md index c5de0d3..8efc36b 100644 --- a/docs/architecture/05-template-and-taskgroup.md +++ b/docs/architecture/05-template-and-taskgroup.md @@ -1,6 +1,6 @@ # 模板与任务组 -> 涉及文件:`src/model/TemplateModel.ts` · `src/controller/template/`(TemplateController · wizard · useTemplate · selectors · crud)· `src/model/TaskGroupModel.ts` · `src/controller/taskGroup/`(TaskGroupController · addItems · queueLoader · metaLoader · contextMenu · importExport)· `src/view/template/`(TemplateLibraryView · TemplateWizardView · SelectorDialog)· `src/view/taskGroup/TaskGroupView.ts` · `src/view/shared/ShipAutocomplete.ts` · `resource/builtin_templates.json` · `templates/templates.json` · `task_groups.json` +> 涉及文件:`src/model/TemplateModel.ts` · `src/controller/template/`(TemplateController · wizard · useTemplate · selectors · crud)· `src/model/TaskGroupModel.ts` · `src/controller/taskGroup/`(TaskGroupController · addItems · queueLoader · metaLoader · contextMenu)· `src/view/template/`(TemplateLibraryView · TemplateWizardView · SelectorDialog)· `src/view/taskGroup/TaskGroupView.ts` · `src/view/shared/ShipAutocomplete.ts` · `resource/builtin_templates.json` · `templates/templates.json` · `task_groups.json` ## 概述 @@ -52,7 +52,7 @@ interface TaskTemplate { | 来源 | 文件 | 可写 | |------|------|------| | **内置** | `resource/builtin_templates.json` | 只读 | -| **用户** | `templates/templates.json` | 可读写 | +| **用户** | `userData/templates/templates.json` | 可读写 | `TemplateModel.init()` 在启动时合并两个来源,内置模板的 `builtin: true` 标识确保不可删除。 @@ -60,8 +60,8 @@ interface TaskTemplate { | ID | 名称 | 说明 | |----|------|------| -| `builtin_farm_loot` | 刷胖次 | 4 个方案路径可选,`stopCondition: {loot_count_ge: 50}` | -| `builtin_weekly` | 周常任务 | 11 个章节方案 | +| `builtin_farm_loot` | 刷胖次 | 6 个方案路径可选,`stopCondition: {loot_count_ge: 50}` | +| `builtin_weekly_v2` | 周常任务 | 10 个章节方案 | | `builtin_exercise` | 自动演习 | 舰队 ID 可配置 | | `builtin_campaign` | 战役 | 战役类型任务 | | `builtin_decisive` | 决战 | 决战模式 | @@ -87,6 +87,20 @@ flowchart LR | `TemplateWizardView` | `TemplateWizardView.ts` | 创建向导多步骤表单(含舰船自动补全) | | `SelectorDialog` | `SelectorDialog.ts` | 通用选择器弹窗(单选/多选方案、战役、舰队等) | +### 当前兼容状态 + +`TemplateModel` 和 `TemplateController` 仍在应用启动时初始化,这是兼容性要求, +不是可删除的闲置代码: + +- 旧任务组的 `kind: "template"` 条目仍通过 `TemplateModel` 解析和执行。 +- 自动决战的 `system_preset` 仍读取内置模板 `builtin_decisive_6`。 +- 用户模板文件仍需加载、保存和迁移,不能在没有数据迁移的情况下删除。 + +当前计划页没有挂载独立模板库导航和 `template-library-*` 容器,因此 +`TemplateLibraryView` 的列表与创建/导入入口不会作为独立页面显示。模板系统的 +新入口和创建流程由后续界面方案承接;在新方案完成数据与执行迁移前,必须保留 +上述 Model、Controller 和 `kind: "template"` 执行链路。 + --- ## 任务组系统 @@ -94,8 +108,9 @@ flowchart LR ### 数据结构 ```typescript -// task_groups.json 结构 +// task_groups.json v3 结构 { + version: 3, activeGroup: string; // 当前激活的组名 groups: TaskGroup[]; } @@ -106,18 +121,31 @@ interface TaskGroup { } interface TaskGroupItem { - kind: 'plan' | 'template'; // 类型 - path?: string; // 方案文件路径(kind=plan) + kind: 'plan' | 'preset' | 'template'; + managedSource?: 'system' | 'user'; // 当前受管计划来源 + managedFile?: string; // 当前受管计划文件名 + path?: string; // 只为旧格式兼容保留 templateId?: string; // 模板 ID(kind=template) times: number; // 执行次数 label: string; // 显示标签 + fleet_id?: number; // 可选舰队覆盖 + forceRetry?: boolean; // 可选强制重试 + allowPolling?: boolean; // 可选同优先级轮询 fleetPresetIndex?: number; // 可选的编队预设覆盖 } ``` ### 持久化 -`TaskGroupModel` 通过 IPC 读写 `task_groups.json`: +`TaskGroupModel` 通过 IPC 读写 `task_groups.json`。加载 v1、v2 或无版本旧 +数据时,会把旧 `path` 推断为 `managedSource + managedFile`,并将 v1.4.1 +的四个 `活动20260730-*.yaml` 引用映射到当前 `bettle-*.yaml` 系统计划。 +迁移保留原始 `path` 和未知条目字段,保存时写出 v3。文件写入仍由唯一 Model +所有者控制,失败时不会删除旧文件。 + +启动时还会把当前安装目录根部的旧 `task_groups.json` 合并到 `userData`。 +目标文件已存在时不会覆盖:新组直接追加,同名但内容不同的组以“(旧版)” +重命名保留。完成记录包含旧文件来源和内容摘要,重复启动不会重复导入。 | 方法 | 说明 | |------|------| @@ -154,7 +182,6 @@ UI 包含: | `queueLoader.ts` | 加载任务组到调度队列:逐项构建 TaskRequest → `Scheduler.addTask()` | | `metaLoader.ts` | 加载任务项元数据(方案标题、模板名称) | | `contextMenu.ts` | 右键上下文菜单:编辑/删除/复制任务项 | -| `importExport.ts` | 任务组的导入/导出 | --- diff --git a/docs/architecture/06-backend-communication.md b/docs/architecture/06-backend-communication.md index d566879..48568dc 100644 --- a/docs/architecture/06-backend-communication.md +++ b/docs/architecture/06-backend-communication.md @@ -1,6 +1,6 @@ # 后端通信 -> 涉及文件:`electron/preload.ts` · `electron/main.ts`(IPC handlers)· `src/model/ApiClient.ts` · `src/types/api.ts` · `src/types/electronBridge.ts` +> 涉及文件:`electron/preload.ts` · `electron/main.ts`(组合根)· `electron/ipc/` · `electron/services/` · `src/model/ApiClient.ts` · `src/types/api.ts` · `src/types/ipc.ts` ## 概述 @@ -14,7 +14,8 @@ graph LR end subgraph Main["Electron 主进程"] - IPC["IPC Handlers"] + IPC["IPC Adapter
electron/ipc/"] + Service["Service
electron/services/"] end subgraph Py["Python 后端"] @@ -23,7 +24,8 @@ graph LR end View -->|"contextBridge"| IPC - IPC -->|"fs / spawn / exec"| Main + IPC --> Service + Service -->|"fs / spawn / exec"| Main Api -->|"HTTP fetch"| REST Api -->|"WebSocket"| WS ``` @@ -37,23 +39,73 @@ graph LR ## IPC 通信层 +external 后端模式使用用户指定的本地 AutoWSGR 仓库。路径不存在、仓库结构 +无效或缺少 `autowsgr/server/main.py` 时启动明确失败,不会静默回退到 managed +后端;检测和实际启动使用同一解释器与环境变量。 + ### 暴露机制 `preload.ts` 通过 Electron 的 `contextBridge.exposeInMainWorld()` 安全地将 IPC 方法暴露到 `window.electronBridge` 对象上。渲染进程只能通过预定义的方法调用主进程,无法直接访问 Node.js API。 +### Adapter 组织 + +`main.ts` 只创建 Service 并调用注册函数。通道按领域位于 +`electron/ipc/`:`FileIpc`、`DeviceIpc`、`ConfigurationIpc`、 +`TeamPlanIpc`、`CombatPlanIpc`、`DailyPlanIpc`、`ShipLibraryIpc`、 +`EnvironmentIpc`、`BackendIpc` 和 `UpdaterIpc`。 + +Adapter 允许处理 Electron 对话框、同步 `event.returnValue` 和边界异常转换, +但不得实现配置默认值、YAML 解析、路径规则或进程状态。同步 getter 使用 +`ipcMain.on`,其余调用使用 `ipcMain.handle`;通道名、参数顺序和返回结构属于 +兼容性契约,由 `scripts/test-main-ipc.js` 自动与 `preload.ts` 对照。 + ### API 分类 #### 文件操作 | 方法 | 参数 | 返回 | 说明 | |------|------|------|------| -| `readFile(path)` | 文件路径 | `string` | 读取文件内容 | -| `saveFile(path, content)` | 路径 + 内容 | `void` | 写入文件 | -| `appendFile(path, content)` | 路径 + 内容 | `void` | 追加内容 | +| `readFile(path)` | 受控路径 | `string` | 只读取 `userData` 或打包后的 `resource/` | +| `saveFile(path, content)` | 受控路径 + 内容 | `void` | 只写入 `userData` | +| `appendFile(path, content)` | 受控路径 + 内容 | `void` | 只追加到 `userData` | | `openFileDialog(filters, defaultDir?)` | 文件过滤器 | `{path, content} \| null` | 打开文件选择对话框 | | `saveFileDialog(name, content, filters)` | 默认名 + 内容 | `string \| null` | 保存文件对话框 | | `openDirectoryDialog(title?)` | 对话框标题 | `string \| null` | 文件夹选择 | +通用文件 IPC 先拒绝 `..`、UNC、盘符相对路径和 NTFS ADS,再使用 +`path.resolve` 与 `realpath` 展开现有祖先中的目录链接,最后验证 canonical +目标仍位于对应允许根目录。安装资源目录只读;renderer 不能通过 +`saveFile` 或 `appendFile` 修改它。文件选择和保存对话框属于用户在当前操作 +中明确确认的外部文件能力,不复用通用文件 IPC 的路径参数。 + +#### 作战计划管理 + +| 方法 | 返回 | 说明 | +|------|------|------| +| `getPlanManagement()` | `PlanManagementResult` | 汇总系统和用户计划清单 | +| `exportUserPlans(selections)` | `UserPlanExportResult` | 将勾选的用户计划按类型打包为 ZIP | +| `importLocalCombatPlan()` | `PlanFileOperationResult` | 选择本地 YAML,升级后写入用户受管目录 | +| `readManagedCombatPlan(source, file)` | `PlanFileOperationResult` | 读取受管计划并生成运行时展开文件 | +| `saveManagedCombatPlan(...)` | `PlanFileOperationResult` | 保存计划并拆分内嵌舰队 | + +`importLocalCombatPlan()` 的文件路径仅在主进程对话框和计划服务之间传递。 +渲染进程不能提交外部绝对路径;冲突覆盖也由主进程在同一次用户操作中确认。 +`exportUserPlans()` 只接收计划类型和文件名,主进程从用户受管目录重新定位并 +校验文件;系统预设不能导出,ZIP 输出路径由保存对话框授权。 + +#### 日常计划管理 + +| 方法 | 说明 | +|------|------| +| `listDailyPlans()` | 合并只读系统计划和用户计划,同名时用户版本优先 | +| `readDailyPlan(source, file)` | 按受管来源和文件名读取日常计划 | +| `getDailyDecisivePlan(chapter)` | 读取用户优先的指定章节决战计划 | +| `getSystemDailyDecisivePlan(chapter)` | 读取只读系统决战计划 | +| `saveDailyDecisivePlan(settings)` | 写入 `userData/user_daily_plans/` 并同步决战设置 | + +`DailyPlanService` 只接受演习、战役和决战三类计划。Renderer 只提交受管来源、 +文件名或结构化设置,不能把任意路径传给日常计划 IPC。 + #### 路径查询 | 方法 | 返回 | 说明 | @@ -62,7 +114,8 @@ graph LR | `getPlansDir()` | `string` | 方案文件目录 | | `getConfigDir()` | `string` | 配置文件目录 | | `listPlanFiles()` | `{name, file}[]` | 列出方案文件 | -| `openFolder(path)` | `void` | 在资源管理器中打开 | +| `resolveAppPath(path)` | `string` | 仅解析 `userData` 或只读 `resource/` 内的路径 | +| `openFolder(path)` | `void` | 仅打开 `userData` 内经过 canonical 校验的目录 | #### 环境管理 @@ -71,8 +124,8 @@ graph LR | `checkEnvironment()` | `{pythonCmd, pythonVersion, missingPackages, allReady}` | 检查 Python 环境 | | `installDeps()` | `{success, output}` | 安装 Python 依赖 | | `installPortablePython()` | `{success}` | 安装便携版 Python | -| `checkUpdates()` | `{gitAvailable, hasUpdates, ...}` | 检测 autowsgr 库更新 | -| `pullUpdates()` | `{success, output}` | 拉取更新 | +| `checkUpdates()` | - | preload 兼容入口,主进程 handler 当前停用 | +| `pullUpdates()` | - | preload 兼容入口,主进程 handler 当前停用 | #### Python 路径配置 @@ -100,6 +153,25 @@ graph LR | `installGuiUpdate()` | 安装更新并重启 | | `onUpdateStatus(callback)` | 监听更新状态变化 | +`checkGuiUpdates()` 返回严格三态,不允许把异常当成“最新版”: + +```typescript +type GuiUpdateCheckResult = + | { status: 'available'; version: string } + | { status: 'up-to-date' } + | { status: 'error'; message: string }; +``` + +`GuiUpdatePolicy` 根据当前应用版本选择并校验频道:稳定版 `X.Y.Z` 使用 +`latest`,预发布版 `X.Y.Z-beta.N` 使用 `beta`,开发版 `X.Y.Z-dev` 或 +`X.Y.Z-dev.N` 使用 `dev`。候选版本不属于当前频道时检查失败,不允许回退 +读取其他频道清单。 + +安装更新前,`UpdaterIpc` 必须等待共享的 `stopBackend()` 完成。关闭流程依次 +调用后端 `/api/system/stop`、终止服务进程树、等待 `close`;超时后才强制 +终止并再次等待。任何阶段无法确认进程树退出都会返回错误并阻止 +`quitAndInstall()`,避免任务运行中安装或 Windows 文件锁损坏升级。 + #### 事件监听 | 方法 | 事件 | 说明 | @@ -145,7 +217,7 @@ interface ApiResponse { | 方法 | 端点 | 超时 | 说明 | |------|------|------|------| | POST | `/api/system/start` | 300s | 连接模拟器 + 启动游戏 | -| POST | `/api/system/stop` | - | 断开连接 | +| POST | `/api/system/stop` | - | 优雅停止当前系统任务并释放后端资源 | | GET | `/api/system/status` | - | 系统状态查询 | | GET | `/api/system/emulator/devices` | 15s | ADB 设备列表 | diff --git a/docs/architecture/07-environment-management.md b/docs/architecture/07-environment-management.md index e41e30e..7e55cbf 100644 --- a/docs/architecture/07-environment-management.md +++ b/docs/architecture/07-environment-management.md @@ -1,6 +1,6 @@ # 环境管理 -> 涉及文件:`electron/pythonEnv/`(context · finder · envCheck · installer · updater · utils)· `electron/emulatorDetect.ts` · `electron/backend.ts` · `electron/main.ts` +> 涉及文件:`electron/pythonEnv/` · `electron/services/PythonEnvironmentService.ts` · `electron/services/CudaEnvironmentService.ts` · `electron/services/AdbService.ts` · `electron/services/BackendService.ts` · `electron/services/BackendShutdownService.ts` · `electron/ipc/EnvironmentIpc.ts` · `electron/ipc/DeviceIpc.ts` · `electron/ipc/BackendIpc.ts` · `electron/emulatorDetect.ts` ## 概述 @@ -18,14 +18,24 @@ Python 环境管理位于 `electron/pythonEnv/` 子目录,采用依赖注入 | 文件 | 职责 | |------|------| +| `backendRequirement.ts` | 固定 managed 模式使用的 AutoWSGR 来源,安装和自动更新共用同一契约 | +| `backendContractProbe.ts` | 在隔离检查进程中验证外部后端是否支持 GUI 所需的正式运行时契约 | | `context.ts` | 共享上下文与缓存状态(`PythonEnvContext` 接口、缓存变量) | +| `dependencies.ts` | 集中声明 GUI 运行时和舰船资料库工具所需的 Python 依赖 | | `finder.ts` | Python 可执行文件发现(用户配置 → 便携版 → 系统全局) | +| `environment.ts` | 统一描述解释器、源码、安装目标和运行路径 | +| `cuda.ts` | 校验 CUDA 路径并在同一个 Python 环境上叠加 CUDA 变量 | | `envCheck.ts` | 环境验证主流程(VC++ Redistributable 检查、标记文件管理、依赖包验证) | | `installer.ts` | Python 安装与依赖管理(pip 设置、autowsgr 安装) | | `updater.ts` | autowsgr 自动更新逻辑(PyPI 版本检查 + 升级) | | `utils.ts` | 工具函数与共享接口(路径工具、环境变量、pip 命令、.pth 文件处理) | | `index.ts` | 聚合导出 | +`PythonEnvironmentService` 是 IPC 使用的无状态用例入口,负责保持 Python +路径校验结果、环境检查和安装返回结构。它不复制 `pythonEnv/context.ts` 的缓存; +Python 发现状态仍只有一个所有者。`CudaEnvironmentService` 负责 Toolkit、 +Runtime DLL 和当前 Python/PyTorch CUDA 能力检测。 + ### 发现优先级 `finder.ts` 中的 `findPython()` 按以下顺序查找可用的 Python: @@ -47,6 +57,20 @@ flowchart TD **缓存**:发现结果缓存在 `context.ts` 的 `PythonEnvContext` 中,用户切换路径时调用 `clearPythonCache()` 清除。 +### 统一环境描述 + +`environment.ts` 生成的同一个环境描述同时用于依赖安装、依赖检查和 +`BackendService` 启动。安装规则如下: + +| 后端模式 | Python 来源 | 依赖安装位置 | 启动路径 | +|---------|-------------|-------------|---------| +| managed | 任意兼容解释器 | `{appRoot}/python/site-packages` | GUI site-packages | +| external | GUI 内置 Python | `{appRoot}/python/site-packages` | GUI site-packages + 本地仓库 | +| external | 用户选择的外部 Python | 该解释器自身环境 | 解释器自身环境 + 本地仓库 | + +external 仓库无效时,环境检查、依赖安装和后端启动都会明确失败,不会回退到 +managed 后端,也不会把 GUI 的依赖目录混入外部解释器。 + ### 便携版 Python 应用打包时内置 Python 3.12.8 embed 发行版,位于 `{appRoot}/python/`。 @@ -78,8 +102,8 @@ flowchart TD B -->|否| H["findPython()"] H --> I{"找到 Python?"} I -->|否| J["返回 {allReady: false, pythonCmd: null}"] - I -->|是| K["ensurePthFile()"] - K --> L["单次 Python 调用:
检查 uvicorn/fastapi/autowsgr"] + I -->|是| K["resolvePythonEnvironment()"] + K --> L["按环境描述检查
uvicorn/fastapi/autowsgr"] L --> M{"所有依赖就绪?"} M -->|否| N["返回 {allReady: false, missingPackages}"] M -->|是| O["自动更新 autowsgr"] @@ -95,25 +119,30 @@ flowchart TD { "pythonCmd": "C:\\path\\to\\python.exe", "pythonVersion": "Python 3.12.8", - "autowsgrVersion": "2.1.9" + "autowsgrVersion": "2.1.9", + "environmentIdentity": "{\"startupMode\":\"managed\",...}" } ``` - **路径**:`{appRoot}/.env_ready` -- **失效时机**:安装依赖后删除、Python 路径配置变更后删除 +- **失效时机**:安装依赖后删除;Python、模式、仓库或安装目标变化后失效 - **验证条件**:Python 文件存在 + autowsgr 版本 ≥ 2.1.0 ### 依赖安装 -`installer.ts` 中的 `installDependencies()`: +`installer.ts` 中的 `installDependencies()`: 1. 删除 `.env_ready` 标记 2. 确保 pip 可用 (`ensurePip()`) -3. 安装到本地目录: +3. 按统一环境描述安装: ``` - pip install --target {appRoot}/python/site-packages --upgrade setuptools autowsgr + managed/内置 Python: + pip install --target {appRoot}/python/site-packages ... + + external + 外部 Python: + -m pip install ... ``` -**所有包安装到 `{appRoot}/python/site-packages/`**,不影响全局 Python 环境。 +外部模式选中的 Python、pip 安装目标、依赖检查解释器和后端启动解释器必须一致。 ### 自动更新 @@ -129,6 +158,9 @@ flowchart TD `detectEmulator()` 通过 Windows 注册表自动识别已安装的模拟器: +`DeviceIpc` 只保持通道和异常边界。ADB 可执行文件选择、设备列表解析以及 +connect/disconnect 的结果由 `AdbService` 统一处理。 + ### 支持的模拟器 | 模拟器 | 检测方式 | 默认 ADB 串口 | @@ -171,7 +203,8 @@ flowchart TD ### 启动流程 -`startBackend()` (`electron/backend.ts`) 负责启动 Python 后端: +`startBackend()` (`electron/services/BackendService.ts`) 负责启动 Python 后端。 +`BackendIpc` 只转换启动结果,后端子进程引用仍只存在于 `BackendService`。 ```mermaid sequenceDiagram @@ -223,7 +256,18 @@ sequenceDiagram ### 停止 -`stopBackend()` 直接 `kill()` 子进程。应用退出时 (`app.on('before-quit')`) 自动调用。 +`stopBackend()` 与更新安装共用 `BackendShutdownService`,关闭步骤固定为: + +1. `POST /api/system/stop`,给运行中的任务最多 35 秒执行正式清理。 +2. 若服务进程仍在,Windows 执行 `taskkill /PID /T` 终止完整进程树; + 其他平台发送 `SIGTERM`。 +3. 等待进程 `close` 最多 5 秒,确认操作系统已释放进程资源和文件锁。 +4. 超时后 Windows 执行 `/T /F`,其他平台发送 `SIGKILL`,再等待 5 秒。 +5. 仍无法确认退出时抛出错误,不清空活动进程引用。 + +后端停止接口失败只会进入进程树终止回退,不会假装关闭成功。应用退出时 +`before-quit` 会阻止立即退出并等待完整流程;失败时应用保持运行并显示错误。 +更新安装也必须等待同一流程,失败时取消 `quitAndInstall()`。 --- @@ -233,8 +277,8 @@ sequenceDiagram sequenceDiagram participant App as AppController participant IPC as IPC Bridge - participant PyEnv as pythonEnv.ts - participant Back as backend.ts + participant PyEnv as pythonEnv/ + participant Back as BackendService participant Py as Python 后端 App->>IPC: checkEnvironment() @@ -254,7 +298,7 @@ sequenceDiagram App->>IPC: startBackend() IPC->>Back: startBackend() - Back->>Back: ensurePthFile() + findPython() + Back->>Back: resolvePythonEnvironment() Back->>Back: ADB connect Back->>Py: spawn 子进程 diff --git a/docs/architecture/08-dev-setup.md b/docs/architecture/08-dev-setup.md index 7eaea6b..cacf0ac 100644 --- a/docs/architecture/08-dev-setup.md +++ b/docs/architecture/08-dev-setup.md @@ -34,6 +34,15 @@ npm run dev |------|------| | `npm run dev` | 编译 TypeScript + esbuild 打包 + 启动 Electron(开发日常使用) | | `npm run build` | 仅编译(`tsc` + `esbuild`),不运行 | +| `npm run test:main-services` | 构建后验证主进程 Service 的路径、配置、计划、环境和资料库行为 | +| `npm run test:main-ipc` | 构建后核对 preload 与 IPC Adapter 的通道及同步/异步契约 | +| `npm run test:legacy-config-upgrade` | 验证旧设置、决战配置和任务组升级 | +| `npm run test:legacy-plan` | 验证旧计划、舰队拆分和受管引用迁移 | +| `npm run test:api-contract` | 构建后验证 GUI 与 AutoWSGR API 契约 fixture | +| `npm run test:fleet-domain` | 验证舰队草稿、候选槽位和持久化 DTO 往返 | +| `npm run test:scheduler-domain` | 验证逻辑任务身份、后触发、取消和排序 | +| `npm run test:python-environment` | 验证 managed/external Python、CUDA 和后端环境一致性 | +| `npm run test:task-group-migration` | 构建后验证任务组迁移和往返兼容 | | `npm start` | 等同于 `build` + `electron .`(含 chcp 65001) | | `npm run dist` | 完整打包:下载 Python + ADB → 编译 → electron-builder NSIS 安装包 | | `npm run pack` | 编译 + `electron-builder --dir`(生成目录,不打安装包) | @@ -82,12 +91,27 @@ flowchart LR "appId": "com.autowsgr.gui", "productName": "AutoWSGR-GUI", "directories": { "output": "release" }, - "files": ["dist/**/*", "src/view/**/*", "scripts/**/*"], + "files": [ + "dist/**/*", + "src/view/index.html", + "src/view/styles/styles.css" + ], "extraResources": [ - { "from": "resource", "to": "resource" }, - { "from": "plans", "to": "plans" }, + { + "from": "resource", + "to": "resource", + "filter": [ + "**/*", + "!user_battle_plans/**/*", + "!user_team_plans/**/*" + ] + }, { "from": "setup.bat", "to": "setup.bat" }, + { "from": "tools/ship_library", "to": "tools/ship_library" } + ], + "extraFiles": [ { "from": "python", "to": "python" }, + { "from": "redist", "to": "redist" }, { "from": "adb", "to": "adb" } ] } @@ -98,7 +122,7 @@ flowchart LR **包含内容**: - `dist/` — 编译后的 JS - `src/view/` — HTML/CSS -- `resource/` — 内置方案 + 模板 + 地图 +- `resource/` — 内置方案、模板、地图、舰船资料库种子和只读迁移快照 - `python/` — 便携版 Python - `adb/` — ADB 工具 @@ -114,13 +138,51 @@ flowchart LR |------|--------------|--------------| | `appRoot()` | 项目根目录 | `%LOCALAPPDATA%/autowsgr-gui/` 或安装目录 | | `resourceRoot()` | 同 appRoot | `resources/` (extraResources) | -| `plans/` | 项目根 `plans/` | extraResources `plans/` | +| `resource/system_battle_plans/` | 项目根 `resource/system_battle_plans/` | extraResources `resource/system_battle_plans/` | +| `resource/system_daily_plans/` | 项目根 `resource/system_daily_plans/` | extraResources `resource/system_daily_plans/` | +| `resource/migrations/v6/` | 项目根 `resource/migrations/v6/`,只读 | extraResources `resource/migrations/v6/`,只读 | +| `resource/ship-library/` | 打包资料库种子 | extraResources `resource/ship-library/` | +| `userData/user_battle_plans/` | Electron userData | Electron userData | +| `userData/user_team_plans/` | Electron userData | Electron userData | +| `userData/user_daily_plans/` | Electron userData | Electron userData | | `python/` | 项目根 `python/` | extraResources `python/` | | `adb/` | 项目根 `adb/` | extraResources `adb/` | -| `usersettings.yaml` | 项目根 | appRoot | -| `gui_settings.json` | 项目根 | appRoot | -| `task_groups.json` | 项目根 | appRoot | -| `templates/` | 项目根 | appRoot | +| `userData/usersettings.yaml` | Electron userData | Electron userData | +| `userData/gui_settings.json` | Electron userData | Electron userData | +| `userData/task_groups.json` | Electron userData | Electron userData | +| `userData/templates/` | Electron userData | Electron userData | + +启动迁移会扫描本次运行所在的旧项目根目录。即使 `userData` 已存在,也会 +深度合并旧设置,并迁移旧任务组、模板和递归扫描发现的有效计划 YAML。 +旧字段覆盖同名当前字段,当前版本独有字段继续保留。不同内容的同名任务组、 +计划、舰队和模板以“(旧版)”保留,不会覆盖现有文件。迁移按旧来源路径和 +内容哈希记录完成状态及实际输出文件名,状态保存在 +`userData/.migration-state.json`。源文件不会删除;本次实际执行迁移后会 +弹窗展示总数、成功数、失败数,失败项在下次启动继续尝试。 + +迁移状态当前为 **v6**。旧计划和舰队的结构迁移由 +`LegacyPlanMigration` 维护 v5 完成项;v6 由 `UserDataMigrationService` +升级系统预设库存、将已删除但仍被引用的系统计划保存为个人副本,并把旧胖次 +数字索引迁移为稳定计划标识。只有 v6 操作全部成功才写入版本 6;失败项不会 +写成完成状态,下一次启动继续重试。安装目录和迁移资源始终只读。 + +GUI 发布严格使用三个版本规范和更新频道: + +- 稳定版 `X.Y.Z` 使用 `latest.yml`。 +- 预发布版 `X.Y.Z-beta.N` 使用 `beta.yml`。 +- 开发版 `X.Y.Z-dev` 或 `X.Y.Z-dev.N` 使用 `dev.yml`。 + +发布工作流会拒绝其他格式,并验证产物只能包含当前频道的更新清单。客户端也会 +校验候选版本的频道,禁止开发版或预发布版回退到稳定频道。 + +完整升级生命周期如下: + +1. release workflow 从 tag 解析版本、阶段和频道,覆盖构建时 publish channel。 +2. 打包后验证只生成当前频道的 YAML 清单,再创建对应 stable/prerelease Release。 +3. 客户端检查返回“有更新 / 已是最新 / 检查失败”三态,网络或频道错误不能显示成最新版。 +4. 下载前再次校验候选版本频道;没有已确认版本时拒绝下载和安装。 +5. 安装前调用后端正式停止接口,终止并等待完整进程树退出。 +6. 无法确认后端退出或文件锁释放时取消安装;成功后才调用 `quitAndInstall()`。 --- diff --git a/docs/architecture/09-src-typescript-catalog.md b/docs/architecture/09-src-typescript-catalog.md new file mode 100644 index 0000000..90d5b85 --- /dev/null +++ b/docs/architecture/09-src-typescript-catalog.md @@ -0,0 +1,160 @@ +# `src` TypeScript 模块目录 + +当前 `src` 共有 107 个 TypeScript 文件。每个模块均有文件头职责说明;本目录由 +这些说明汇总,用于快速定位功能所有者。 + +## 当前结构 + +```text +src/ +├─ adapter/ 6 个:API、IPC、JSON、YAML、Storage 边界 +├─ controller/ 35 个:页面和用例编排 +├─ model/ 24 个:领域状态、Fleet、Scheduler 和统计规则 +├─ view/ 30 个:DOM 渲染、表单输入和 UI 意图 +├─ types/ 6 个:API、IPC、Model、View、Scheduler、统计类型 +├─ shared/ 5 个:跨层无状态业务契约 +├─ data/ 舰船静态 JSON +└─ utils/ 1 个:统一日志 +``` + +## Adapter(6) + +| 文件 | 职责 | +|---|---| +| `src/adapter/ApiAdapter.ts` | 封装后端 HTTP 请求和 WebSocket 连接,向 Model 提供传输能力。 | +| `src/adapter/index.ts` | 集中导出 Renderer 使用的 Adapter 实例和边界类型。 | +| `src/adapter/IpcAdapter.ts` | 封装 Electron IPC 文件与计划操作,提供 Renderer 侧仓储接口。 | +| `src/adapter/JsonAdapter.ts` | 统一 JSON 序列化、解析和对象类型校验。 | +| `src/adapter/StorageAdapter.ts` | 定义键值存储契约,并封装浏览器 localStorage 实现。 | +| `src/adapter/YamlAdapter.ts` | 统一 YAML 编解码及作战方案元数据读取。 | + +## Controller(35) + +| 文件 | 职责 | +|---|---| +| `src/controller/app/AppController.ts` | 作为 Renderer 组合根,初始化并协调页面、模型和功能控制器。 | +| `src/controller/app/AutomaticDecisiveTask.ts` | 分别从用户决战计划和系统预设构造自动决战请求。 | +| `src/controller/app/ConfigController.ts` | 编排设置加载、环境检测、表单保存和配置持久化。 | +| `src/controller/app/constants.ts` | 维护任务优先级、状态文案和修理模式等界面常量。 | +| `src/controller/app/NavigationController.ts` | 处理主页面导航、标签切换和当前页面状态。 | +| `src/controller/app/OperationsController.ts` | 编排远征收取、奖励领取等常用自动化操作。 | +| `src/controller/app/rendering.ts` | 把调度器和游戏状态转换为主页面 ViewObject。 | +| `src/controller/app/SchedulerBinder.ts` | 绑定 Scheduler 回调并同步日志、进度、队列和连接状态。 | +| `src/controller/app/SettingsController.ts` | 编排设置页的环境检测、设备连接、资料库更新和主题交互。 | +| `src/controller/app/theme.ts` | 读取并应用主题、强调色和系统主题变化。 | +| `src/controller/plan/BattlePlanLoaderController.ts` | 管理受管作战方案选择器的加载、筛选、选择和结果返回流程。 | +| `src/controller/plan/DecisivePlanController.ts` | 独立持有决战舰队草稿并协调加载、编辑和保存。 | +| `src/controller/plan/FleetPlannerController.ts` | 持有普通舰队草稿并协调舰船库、规则编辑和计划管理。 | +| `src/controller/plan/nodeEditor.ts` | 编排节点编辑表单与 PlanModel 节点规则更新。 | +| `src/controller/plan/PlanController.ts` | 协调作战方案加载、预览、编辑、保存和任务执行。 | +| `src/controller/plan/presetFlow.ts` | 把任务预设和舰队预设转换为可调度的作战任务。 | +| `src/controller/plan/rendering.ts` | 把作战方案、地图和节点数据转换为预览 ViewObject。 | +| `src/controller/plan/selectedNodes.ts` | 维护地图节点选择顺序并同步方案的选中节点。 | +| `src/controller/shared/DialogHelper.ts` | 封装 Renderer 通用确认、提示和输入对话框。 | +| `src/controller/startup/connection.ts` | 检测后端可用性并管理启动阶段的连接等待。 | +| `src/controller/startup/envAndUpdates.ts` | 编排运行环境准备、依赖安装和 GUI 更新检查。 | +| `src/controller/startup/StartupController.ts` | 统一协调应用启动、后端连接、环境检查和资源释放。 | +| `src/controller/taskGroup/addItems.ts` | 将方案、模板和预设添加为任务组条目。 | +| `src/controller/taskGroup/contextMenu.ts` | 处理任务组条目的右键菜单、编辑、复制和删除。 | +| `src/controller/taskGroup/DailyTaskLoaderController.ts` | 管理日常任务浮窗的加载、分类、参数和提交动作。 | +| `src/controller/taskGroup/managedPlanReader.ts` | 读取受管作战方案并统一返回内容和来源信息。 | +| `src/controller/taskGroup/metaLoader.ts` | 批量读取任务条目元数据并生成界面展示摘要。 | +| `src/controller/taskGroup/queueLoader.ts` | 把任务组条目解析为 Scheduler 可执行任务并加入队列。 | +| `src/controller/taskGroup/TaskGroupController.ts` | 管理任务组选择、增删改和 Model 到 ViewObject 的映射。 | +| `src/controller/taskGroup/TaskListLoaderController.ts` | 协调任务列表文件选择、解析和批量载入。 | +| `src/controller/template/crud.ts` | 实现模板创建、编辑、删除、导入和导出用例。 | +| `src/controller/template/selectors.ts` | 提供方案、舰队和任务参数的模板选择流程。 | +| `src/controller/template/TemplateController.ts` | 协调模板库、模板向导和任务组添加流程。 | +| `src/controller/template/useTemplate.ts` | 把模板参数实例化为可加入任务组的任务条目。 | +| `src/controller/template/wizard.ts` | 维护模板向导步骤、预填数据和表单提交。 | + +## Model(24) + +| 文件 | 职责 | +|---|---| +| `src/model/ApiClient.ts` | 提供后端业务 API、任务控制和 WebSocket 事件客户端。 | +| `src/model/ConfigModel.ts` | 持有用户配置状态并负责默认值、迁移和 YAML 转换。 | +| `src/model/fleet/DecisiveFleetDraft.ts` | 维护决战舰队独立草稿及决战配置转换规则。 | +| `src/model/fleet/FleetDraft.ts` | 维护普通舰队槽位、候选舰和拖拽编辑的唯一草稿。 | +| `src/model/fleet/FleetPresetIdentity.ts` | 统一编队规则格式并生成稳定的预设身份标识。 | +| `src/model/fleet/FleetRuleMapper.ts` | 在舰队槽位规则和后端 fleet_rules 请求之间转换。 | +| `src/model/fleet/index.ts` | 导出舰队草稿、匹配、名称和规则映射等领域能力。 | +| `src/model/fleet/ShipCatalog.ts` | 加载静态舰船资料并提供舰名和国籍只读目录。 | +| `src/model/fleet/ShipMatcher.ts` | 解析舰队预设、匹配舰船规则并生成槽位显示文本。 | +| `src/model/fleet/ShipNameNormalizer.ts` | 统一舰名别名、后端标准名和搜索名转换。 | +| `src/model/MapDataLoader.ts` | 加载、缓存并查询地图节点、连线和位置信息。 | +| `src/model/PlanModel.ts` | 持有作战方案状态并处理节点规则、迁移和 YAML 往返。 | +| `src/model/scheduler/CronScheduler.ts` | 维护定时任务触发器、最后执行时间和 pending 状态。 | +| `src/model/scheduler/ExpeditionTimer.ts` | 根据远征状态计算并发布下一次收取倒计时。 | +| `src/model/scheduler/index.ts` | 导出调度器、任务队列、定时器和修理管理器公共接口。 | +| `src/model/scheduler/RepairManager.ts` | 跟踪泡澡舰船并编排修理、替换和状态恢复。 | +| `src/model/scheduler/Scheduler.ts` | 持有当前任务和运行状态,驱动任务消费、重试与后续任务。 | +| `src/model/scheduler/SchedulerRepairPolicy.ts` | 提供舰船损伤阈值、替换候选和修理时长的纯规则。 | +| `src/model/scheduler/SchedulerTaskPolicy.ts` | 提供任务完成、重试、轮询和后续任务构造的纯规则。 | +| `src/model/scheduler/StopConditionChecker.ts` | 读取游戏统计并判断掉落、舰船上限等停止条件。 | +| `src/model/scheduler/TaskQueue.ts` | 唯一持有就绪与延迟队列,并实现优先级和轮询排序。 | +| `src/model/statistics/DailySortieStats.ts` | 从后端成功日志维护可持久化的今日出征统计。 | +| `src/model/TaskGroupModel.ts` | 持有任务组及条目状态,并负责 JSON 迁移和持久化。 | +| `src/model/TemplateModel.ts` | 管理内置与用户模板,负责校验、CRUD 和持久化。 | + +## View(30) + +| 文件 | 职责 | +|---|---| +| `src/view/config/ConfigView.ts` | 渲染设置页面、收集表单输入并发出保存和检测意图。 | +| `src/view/main/FleetPreviewView.ts` | 渲染当前舰队舰船、等级和损伤状态预览。 | +| `src/view/main/LogView.ts` | 追加、筛选并滚动展示运行日志。 | +| `src/view/main/MainView.ts` | 组合主页面状态栏、舰队、队列和日志子视图。 | +| `src/view/main/StatusBar.ts` | 渲染连接、运行状态、当前任务和远征倒计时。 | +| `src/view/main/TaskQueueView.ts` | 渲染任务队列进度并发出删除和停止操作意图。 | +| `src/view/plan/BattlePlanLoaderView.ts` | 渲染受管作战方案选择器,并收集搜索、筛选和舰队选择操作。 | +| `src/view/plan/DecisivePlanView.ts` | 渲染决战舰队配置并向 Controller 提交编辑意图。 | +| `src/view/plan/FleetEditDialog.ts` | 提供舰队预设名称、舰船和候选规则的编辑对话框。 | +| `src/view/plan/FleetEditorView.ts` | 渲染舰队槽位并处理选择、清空和拖拽排序意图。 | +| `src/view/plan/FleetGalleryView.ts` | 展示舰船图鉴并管理筛选、排序和选择等 UI 状态。 | +| `src/view/plan/FleetPlannerView.ts` | 组合舰队编辑、规则、图鉴、计划管理和编队加载视图。 | +| `src/view/plan/FleetPresetView.ts` | 渲染方案内舰队预设并提供应用、编辑和任务创建入口。 | +| `src/view/plan/FleetRuleView.ts` | 渲染主选、候选、舰种和等级规则编辑区。 | +| `src/view/plan/MapView.ts` | 绘制作战地图节点与连线并发出节点选择意图。 | +| `src/view/plan/NodeEditorView.ts` | 渲染节点属性和敌舰规则编辑对话框。 | +| `src/view/plan/PlanManagementView.ts` | 渲染本地方案列表并发出导入、导出、重命名和删除意图。 | +| `src/view/plan/PlanPreviewView.ts` | 组合地图、节点编辑、舰队预设和方案参数预览。 | +| `src/view/plan/ShipArtwork.ts` | 创建舰船立绘元素并处理资源路径和加载失败回退。 | +| `src/view/plan/TeamPlanListUi.ts` | 渲染编队方案卡片并实现搜索、筛选和排序。 | +| `src/view/plan/TeamPlanLoaderView.ts` | 展示编队方案选择器并发出加载方案意图。 | +| `src/view/setup/SetupWizardView.ts` | 渲染首次启动向导并收集模拟器和 Python 配置。 | +| `src/view/shared/AnimatedSelect.ts` | 在不改变原生表单数据源的前提下提供统一动画下拉面板。 | +| `src/view/shared/scrollPosition.ts` | 保存和恢复可滚动容器的界面位置。 | +| `src/view/shared/ShipAutocomplete.ts` | 为舰名输入框提供搜索建议、键盘选择和补全。 | +| `src/view/taskGroup/DailyTaskLoaderView.ts` | 渲染日常任务浮窗并上报页签、参数和提交意图。 | +| `src/view/taskGroup/TaskGroupView.ts` | 渲染任务组和任务条目,并发出选择、排序和菜单意图。 | +| `src/view/template/SelectorDialog.ts` | 渲染通用选项对话框并返回用户选择。 | +| `src/view/template/TemplateLibraryView.ts` | 渲染模板库卡片并发出使用、编辑和删除意图。 | +| `src/view/template/TemplateWizardView.ts` | 渲染模板创建向导、计划列表和分步表单。 | + +## Types(6) + +| 文件 | 职责 | +|---|---| +| `src/types/api.ts` | 定义 Renderer 与后端 HTTP/WebSocket 通信使用的请求、响应和事件类型。 | +| `src/types/ipc.ts` | 定义 Renderer 与 Electron 主进程之间的桥接方法、文件结果和资源契约。 | +| `src/types/model.ts` | 定义配置、作战方案、舰队规则、模板和修理等领域数据结构。 | +| `src/types/scheduler.ts` | 定义任务队列、调度状态及 Scheduler 对外回调契约。 | +| `src/types/statistics.ts` | 定义今日出征统计、战斗评级和掉落提示结构。 | +| `src/types/view.ts` | 定义 Controller 交给各页面渲染的 ViewObject、表单值和展示状态。 | + +## Shared(5) + +| 文件 | 职责 | +|---|---| +| `src/shared/decisiveAutomation.ts` | 定义自动决战的用户计划、系统预设来源和稳定标识。 | +| `src/shared/fleetShipTypes.ts` | 提供 22 种规范舰种代码、标签、映射和契约校验。 | +| `src/shared/legacyDecisiveAutomation.ts` | 定义旧决战自动化配置的无损迁移结构。 | +| `src/shared/lootPlans.ts` | 定义刷胖次稳定计划标识、来源和旧数字索引映射。 | +| `src/shared/nativeFleetShipTypes.generated.ts` | 保存由 autowsgr_native 生成的舰种代码,供前端漂移检查。 | + +## Utils(1) + +| 文件 | 职责 | +|---|---| +| `src/utils/Logger.ts` | 提供统一日志级别、频道和控制台输出格式。 | diff --git a/docs/architecture/10-runtime-boundaries-adr.md b/docs/architecture/10-runtime-boundaries-adr.md new file mode 100644 index 0000000..69d9806 --- /dev/null +++ b/docs/architecture/10-runtime-boundaries-adr.md @@ -0,0 +1,115 @@ +# ADR-001:当前运行时边界与生命周期 + +- **状态**:已接受 +- **日期**:2026-08-05 +- **适用范围**:当前 AutoWSGR-GUI 主进程、渲染进程、用户数据迁移、任务调度和 GUI 更新 + +## 背景 + +GUI 已完成大规模模块拆分。为了防止实现继续演进而工程文档停留在旧结构,本 +决策记录固定当前版本的所有权和生命周期边界。代码是最终契约;相关变更必须 +同时更新本 ADR 和对应专题文档。 + +## 决策 + +### 1. 渲染进程遵循单向边界 + +标准数据流为: + +```text +Model / Repository → Controller → ViewObject → View +View → 用户意图 → Controller +``` + +View 不读取 Electron bridge,不拼装持久化 DTO,也不拥有文件 identity。 +`FleetPlannerController` 负责舰船库和编队计划读取、保存覆盖、DTO 映射及 +`file/source`;`FleetDraft` 负责草稿与 `UserTeamPlan` 的双向转换和规则校验。 +View 只看到 ViewObject 和不透明计划 ID。`DecisivePlanController` 以同样方式 +拥有设置 Repository 和 `DecisiveFleetDraft`。 + +### 2. `electron/main.ts` 只做组合根 + +`main.ts` 可以创建 Service、注入依赖、注册 IPC、编排启动迁移和处理 Electron +生命周期,不实现文件策略、YAML 规则、计划归一化、资料库升级、环境检测或 +更新策略。业务边界分别由以下模块承担: + +| 边界 | 所有者 | +|------|--------| +| 普通文件能力 | `SafePathService`、`SecureFileService` | +| GUI 设置 | `GuiSettingsStore`、`GuiConfigurationService` | +| 作战计划 | `CombatPlanCodec`、`CombatPlanRepository`、计划 Service | +| 编队计划 | `TeamPlanCodec`、`TeamPlanRepository`、`TeamPlanService` | +| 舰船资料库 | `ShipLibraryService`、`ShipLibraryUpdater` | +| Python/CUDA/ADB | `pythonEnv/*`、环境 Service | +| 后端进程 | `BackendService`、`BackendShutdownService` | +| GUI 更新 | `UpdaterIpc`、`GuiUpdatePolicy` | + +### 3. 安装资源只读,用户数据写入 `userData` + +| 数据 | 运行时位置 | +|------|------------| +| 系统作战/舰队/日常计划、地图、内置模板 | `resource/`,只读 | +| 已下架系统计划的迁移快照 | `resource/migrations/v6/`,只读 | +| 用户作战计划 | `userData/user_battle_plans/` | +| 用户舰队计划 | `userData/user_team_plans/` | +| 用户日常计划 | `userData/user_daily_plans/` | +| 设置、任务组、模板 | `userData/` | +| 舰船资料库工作副本 | `userData/ship-library/` | +| 迁移状态 | `userData/.migration-state.json` | +| 临时展开计划 | 系统 temp 下的进程专属目录 | + +舰船资料库按 manifest 的 schema 版本和生成时间升级,通过临时目录、备份和 +重命名切换,失败时恢复旧目录。 + +通用文件 IPC 只允许读取 `userData` 和 `resource`,只允许写入 `userData`。 +路径验证拒绝 `..`、UNC、盘符跳转和 NTFS ADS,并通过 `realpath` 检查符号链接 +或 junction 的真实目标。系统文件对话框授予的单次外部文件能力不扩展通用 IPC。 + +### 4. 迁移是可重试的版本状态机 + +`LegacyPlanMigration` 维护计划结构迁移 v5 完成项;当前 +`USER_DATA_MIGRATION_VERSION` 为 6。v6 负责系统预设库存、已删除系统计划引用 +和旧胖次数字索引升级。迁移遵循: + +1. 源文件不修改、不删除。 +2. 同名不同内容保存为“(旧版)”副本,并同步受管引用。 +3. 设置按字段深度合并,未知扩展字段保留。 +4. 完成项包含来源和内容摘要,重复启动不重复迁移。 +5. 全部 v6 操作成功后才写入版本 6;失败项下次启动重试。 +6. 实际发生迁移时显示总数、成功数、失败数和失败文件。 + +### 5. 调度器区分轮次与逻辑任务 + +`SchedulerTask.id` 只标识当前物理轮次;`logicalId` 在所有后触发、重试、间隔 +等待和无限轮次之间保持稳定。事件分为: + +- `onTaskCompleted(id)`:单轮完成。 +- `onLogicalTaskCompleted(logicalId)`:整个任务不再有后续轮次。 +- `onLogicalTaskCanceled(logicalId, reason)`:删除、清空或系统停止。 + +`SchedulerBinder` 只根据逻辑事件更新 cron/pending。系统停止允许下次重新触发, +用户删除或清空表示主动放弃。 + +### 6. 模板兼容链路暂时保留 + +`TemplateModel`、`TemplateController`、用户模板文件和 +`kind: "template"` 仍是旧任务组及系统决战预设的执行依赖。当前计划页没有 +独立模板库入口;新入口按后续界面方案实现。在完成数据和执行迁移之前,不得 +删除或停止初始化兼容链路。 + +### 7. 发布和安装使用严格生命周期 + +版本只允许 `X.Y.Z`、`X.Y.Z-beta.N`、`X.Y.Z-dev` 或 `X.Y.Z-dev.N`, +分别对应 `latest`、`beta`、`dev`。发布工作流和客户端使用同一解析规则, +产物不得混入其他频道清单。 + +更新检查必须返回有更新、已是最新或失败三态。安装前调用 +`BackendShutdownService`:后端正式停止接口、进程树终止、等待 `close`、超时 +强制回退。无法确认退出时阻止 `quitAndInstall()`。 + +## 后果 + +- 新功能需要明确状态所有者,不能把 Repository 或 Electron bridge 重新放回 View。 +- 新的用户可变文件必须先定义 `userData` 位置和 IPC 能力,不能写安装目录。 +- 调度回调不能用单轮 `id` 清理逻辑任务状态。 +- 修改迁移、频道或关闭顺序时,必须补对应 Service 测试和旧版本 fixture。 diff --git a/docs/architecture/README.md b/docs/architecture/README.md index baf8867..2ca6b00 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -8,15 +8,17 @@ | # | 文档 | 说明 | |---|------|------| -| 0 | [总架构文档](00-overview.md) | 项目简介、分层架构、目录结构、启动流程、关键模式、Types 层组织 | -| 1 | [Controller 层](01-controller-layer.md) | ControllerHost/DI 模式、6 个子目录结构、StartupController 启动编排 | +| 0 | [总架构文档](00-overview.md) | 项目简介、主进程组合根/IPC/Service 分层、渲染进程架构、启动流程 | +| 1 | [Controller 层](01-controller-layer.md) | 最小 Host 接口、6 个子目录结构、StartupController 启动编排 | | 2 | [任务调度系统](02-task-scheduling.md) | Scheduler、TaskQueue、CronScheduler、远征轮询、停止条件、修理轮换 | | 3 | [配置系统](03-configuration.md) | ConfigModel、ConfigController、ConfigView、usersettings.yaml、gui_settings.json | | 4 | [出击计划系统](04-battle-plan.md) | PlanModel、PlanController(拆分)、PlanPreviewView(Facade)、MapDataLoader | | 5 | [模板与任务组](05-template-and-taskgroup.md) | TemplateController(拆分)、TaskGroupController(拆分)、模板视图、队列加载 | | 6 | [后端通信](06-backend-communication.md) | IPC Bridge、ApiClient、REST API、WebSocket 事件 | -| 7 | [环境管理](07-environment-management.md) | pythonEnv/ 子模块(7 文件)、模拟器检测、后端进程生命周期 | +| 7 | [环境管理](07-environment-management.md) | pythonEnv/ 子模块(12 文件)、Python/CUDA/ADB 服务、后端进程生命周期 | | 8 | [开发环境搭建](08-dev-setup.md) | 依赖安装、开发/构建/打包命令、SCSS 架构、调试技巧 | +| 9 | [`src` TypeScript 模块目录](09-src-typescript-catalog.md) | 当前 107 个 TS 文件及其职责索引 | +| 10 | [当前运行时边界 ADR](10-runtime-boundaries-adr.md) | 存储、IPC 权限、迁移、调度身份和更新安装决策 | ## 阅读建议 diff --git a/docs/engineering-standards.md b/docs/engineering-standards.md index 2080042..bb263ec 100644 --- a/docs/engineering-standards.md +++ b/docs/engineering-standards.md @@ -462,3 +462,111 @@ AI 参与的 PR 必须记录: - [ ] 当前止损等级和失败尝试已披露; - [ ] 架构和使用文档已同步; - [ ] 无关文件、用户数据和本地未提交修改未被包含。 + +## 13. GUI 2.0 合并协作边界 + +我这边已经梳理过 GUI 2.0 和当前稳定分支的差异。为了避免我们分别修改 Scheduler、配置模型和舰队语义,后续按下面的边界协作。 + +### 你在合并前需要完成的范围 + +你负责把 GUI 2.0 当前已经实现的功能收敛到“可合并、不会破坏已有数据”的状态,重点是现有功能和基础架构,不需要替我实现自动强化。 + +#### 1. 先解决当前 review 中会造成数据损坏、安全问题或兼容性破坏的阻塞项 + +- 给主进程文件 IPC 加严格的路径边界校验,禁止路径穿越和访问受管目录以外的文件。 +- 修复 AtomicFileStore:写入失败时必须保留原文件,不能出现旧文件先被删除、临时文件又未成功替换的情况。 +- 统一外部 Python 环境的依赖安装目录和实际启动目录。 +- 保持 v1.4.1 及当前稳定版计划格式兼容,不能要求用户手工重写旧计划。 +- 完成旧 path-form 任务组迁移,迁移失败时要保留原数据并给出明确错误。 +- 设置 YAML round-trip 时保留 GUI 尚不认识的字段,尤其是未知嵌套字段。 +- 修复 candidate-only 语义:槽位没有顶层 `name` 时,所有 `candidates` 都是平等备选,不能把第一个 candidate 自动提升成 primary。 + +#### 2. 稳定 GUI 2.0 已经引入的核心契约 + +- 舰队方案与出征计划保持分离。 +- 编辑态计划可以引用独立舰队 YAML。 +- 运行前由 `RuntimePlanService` 展开为后端可执行的完整 YAML。 +- 系统计划与用户计划有明确的读写边界。 +- `SchedulerRepairPolicy`、`SchedulerTaskPolicy`、`RepairManager`、`TaskQueue.switchTaskPreset()` 的责任边界保持清晰。 +- 编队轮换继续使用统一 Scheduler,不再增加另一套并行状态机。 + +#### 3. 处理旧活动计划兼容性 + +当前旧目录中有用户正在使用的活动计划,不能仅通过删除: + +- `resource/builtin_plans/活动20260730-E1炸鱼.yaml` +- `resource/builtin_plans/活动20260730-E5夜战.yaml` +- `resource/builtin_plans/活动20260730-H1炸鱼.yaml` +- `resource/builtin_plans/活动20260730-H5夜战.yaml` + +并在 `resource/system_battle_plans` 放几个相似文件,就认为迁移完成。 + +合并前需要验证: + +- 旧计划仍可读取、执行或显式迁移。 +- 新旧计划行为等价。 +- 用户修改过的旧文件不会被安装、升级或迁移流程静默覆盖。 +- 无法迁移时保留原文件,并显示可理解的错误。 + +#### 4. 合并前最低验收程度 + +- `npm run build` 通过。 +- `git diff --check` 通过。 +- 至少覆盖以下回归测试: + - legacy plan 读取和迁移; + - candidate-only 舰队槽位; + - 设置 YAML 未知字段保留; + - 原子写入失败恢复; + - 文件 IPC 路径越界拒绝; + - 任务组旧格式迁移; + - 四个活动计划的兼容性; + - 舰队引用展开后的运行时 YAML 合法性。 +- 把 PR 中尚未接到 UI 的执行入口明确处理:要么接通,要么删除死入口,要么明确标记为暂不支持,不能保留一个看似可用但实际没有调用方的链路。 +- 最好按领域拆分提交,至少让 Electron 服务、舰队 YAML、运行时计划、Scheduler 规则、UI 和舰船资源可以分别审查。 + +### 可以在合并后交给我处理的内容 + +以下内容不要求你在 GUI 2.0 合并前完成: + +- 自动强化的业务实现。 +- 自动强化后端 API 和设备独占租约。 +- intensify Scheduler 任务类型。 +- 自动强化的保护舰集合计算。 +- 自动强化与轮换、泡澡、战斗任务之间的优先级。 +- 自动强化设置 UI 和最终 Scheduler 接线。 +- 自动强化运行日志、失败重试和任务状态展示。 +- 将后端唯一舰船 ID 引入自动强化保护规则;在后端尚未提供 ID 时先兼容舰名。 + +但请预留正常的扩展边界,不要把 Scheduler task type、API 请求或设置 schema 写成无法扩展的封闭分支。 + +### 我这边会负责什么 + +我负责自动强化以及它与稳定分支公共调度层的最终集成: + +- 后端定义并实现自动强化 API。 +- 所有强化设备操作使用全局 device lease,不能与战斗、泡澡或解装并发。 +- 自动强化只作为统一 Scheduler 的正式任务执行,不启动独立后台定时器。 +- 强化前重新读取保护集合,至少保护: + - 当前游戏编队; + - 所有启用轮换预设; + - 正在泡澡的舰船; + - 收藏或锁定舰; + - 强化目标舰; + - 用户自定义保护名单。 +- 设计素材舰筛选、上限、失败和重试规则。 +- 接入 `ApiClient`、API types、Scheduler types、`ConfigModel`、`ConfigController` 和 `SchedulerBinder`。 +- 最终处理自动强化与 GUI 2.0 合并后的公共文件冲突。 + +### 我在 GUI 2.0 合并前会做到的程度 + +我会优先在后端和独立领域层工作,避免现在修改你的舰队编辑器、计划管理 UI 和 Scheduler 核心: + +- 先稳定自动强化的 HTTP contract。 +- 接入后端全局设备租约。 +- 建立 ops 层和 UI controller 的责任边界。 +- 定义目标舰、允许的素材舰种、素材数量上限和保护名单。 +- 编写无设备单元测试和 route contract 测试。 +- 未完成真实设备坐标与 OCR 标定前,功能必须 fail closed,不能尝试点击或消耗任何舰船。 +- GUI 2.0 合并后,我再基于你的最终 Scheduler 和配置架构完成 GUI 任务接线,避免双方同时修改同一套状态机。 + +总体原则是:你负责让 GUI 2.0 已有功能安全、兼容、可合并;我负责自动强化及合并后的公共调度集成。编队轮换核心继续以你的 GUI 2.0 实现为准,我不会另写一套。 diff --git a/docs/features/gui-2.0.md b/docs/features/gui-2.0.md new file mode 100644 index 0000000..e58bc33 --- /dev/null +++ b/docs/features/gui-2.0.md @@ -0,0 +1,1228 @@ +# GUI 2.0 功能说明 + +> 文档状态:GUI 2.0 开发版 +> 对应版本:`2.0.2-dev` +> 基线分支:`upstream/main` +> 目标平台:Windows 10/11 x64 +> 前端技术:Electron 33、TypeScript 5.6、SCSS +> 后端接口:AutoWSGR FastAPI、WebSocket 和 YAML 模型 +> 文档用途:功能说明、数据契约、代码审查和发布验收 + +## 1. 改造背景 + +GUI 2.0 不是对旧页面做局部换皮,而是重新明确 AutoWSGR-GUI 的职责: + +1. GUI 是用户配置、舰队方案和出征计划的可视化管理器。 +2. AutoWSGR 后端模型和 YAML 契约是业务规则的唯一事实来源。 +3. GUI 可以在数据进入后端前提供明确校验,但不能发明后端不存在的字段。 +4. 用户编辑的是可持久化 YAML;执行时再将引用的舰队展开为后端可消费的完整计划。 +5. 页面按“作战、计划、设置”组织,减少旧版页面之间重复、割裂的操作。 +6. 系统资源与用户文件分开维护,系统资源用于提供默认能力,用户文件用于保存修改。 +7. 开发模式必须能够直接连接本地 AutoWSGR 源码,避免 GUI 调试被远端包版本阻塞。 + +旧版 GUI 已经能够启动后端、管理任务队列和编辑基础出征方案,但存在以下问题: + +- 配置项平铺且缺少清晰分类,部分设置只有界面没有完整持久化链。 +- 舰队规则只能通过文本或旧弹窗维护,无法直观看到舰船、位置和备选关系。 +- 出征计划与舰队数据耦合在同一份 YAML 中,不便复用和独立维护。 +- 系统方案、用户方案、任务分组和模板入口分散,缺少统一的状态总览。 +- 保存、新建和加载的行为不一致,容易误覆盖或误报未保存。 +- 开发环境可能错误使用远端 `autowsgr`,无法稳定联调本地后端源码。 +- 页面在 720P 或更窄窗口下存在控件挤压、换行失控和内容不可见问题。 +- 成功提示没有统一语义,点击保存并不等于数据已经成功落盘。 + +GUI 2.0 围绕这些问题建立新的页面结构、数据边界和持久化流程。 + +## 2. 核心设计原则 + +### 2.1 后端契约优先 + +- YAML 字段、允许值和业务含义以 AutoWSGR 后端模型为准。 +- GUI 只负责收集、展示、预校验和序列化。 +- 舰种参数使用 `autowsgr_native.VesselType` 提供的 canonical code,例如 + `dd`、`cl`、`asdg`、`aadg`、`kp`、`cg` 和 `bg`;业务组合项另有 + `ss_or_ssg`。 +- GUI 舰种清单由 native 契约生成,不接受旧 Wiki code `ddg`、`ddgaa`、 + `cgaa` 和 `cbg`。 +- `fleet_presets` 保持列表结构。 +- 一份舰队 YAML 只描述一支舰队。 +- 一个舰队位置可以有主选,也可以只有结构化 `candidates`。 +- `candidates` 中的每一项必须是含 `name` 的对象,不接受仅有字符串的旧写法。 + +### 2.2 系统资源与用户数据分离 + +| 数据类型 | 系统目录 | 用户目录 | +| --- | --- | --- | +| 出征计划 | `resource/system_battle_plans/` | `userData/user_battle_plans/` | +| 舰队方案 | `resource/system_team_plans/` | `userData/user_team_plans/` | +| 舰船资料 | `resource/ship-library/` | 打包版更新到用户可写资料目录 | + +系统资源在界面中按只读内容展示。用户修改系统方案时,需要保存为用户方案。 +打包时不把用户目录中的本地内容写入安装包。 + +### 2.3 编辑态与运行态分离 + +- 编辑态出征计划只保存舰队名称引用,避免重复保存大量舰船规则。 +- 运行任务前,主进程读取对应舰队文件并展开 `ships`。 +- 展开后的完整 YAML 写入进程级临时目录。 +- 后端收到的仍然是完整、独立、可解析的出征 YAML。 +- 临时运行文件不污染系统或用户方案目录。 + +### 2.4 明确的保存成功语义 + +所有可见保存入口统一遵守以下规则: + +1. 点击按钮不会直接显示成功。 +2. 输入校验必须先通过。 +3. 文件写入、原子替换或设置 IPC 必须明确返回成功。 +4. 更新当前文件身份和保存快照后,才能显示顶部成功提示。 +5. 失败时只显示错误信息,不能同时出现成功提示。 +6. 连续保存会刷新提示计时,提示约 2.4 秒后自动隐藏。 + +### 2.5 保持开发环境可控 + +- `managed` 模式由 GUI 管理 Python 和 AutoWSGR 依赖。 +- `external` 模式使用指定的本地 AutoWSGR 仓库和虚拟环境。 +- `external` 模式不自动安装或更新远端 `autowsgr`。 +- 环境检查会验证实际导入的 `autowsgr.__file__` 是否来自预期目录。 +- 本地仓库路径属于开发配置,不进行便携路径改写。 + +## 3. 页面信息架构 + +GUI 2.0 主导航固定为: + +```text +作战 +计划 + ├─ 舰队规划 + ├─ 出征规划 + ├─ 决战计划(旧) + └─ 计划管理 +设置 + ├─ 系统设置 + └─ 脚本行为 +``` + +顶部导航固定在窗口上方,集中展示: + +- 当前执行任务。 +- 当前进度。 +- 剩余次数。 +- 远征检查倒计时。 +- 后端运行状态。 +- 主导航入口。 + +页面内容在导航下方独立滚动,避免页面较长时丢失任务状态。 + +## 4. 作战页 + +### 4.1 页面定位 + +作战页负责把已经准备好的配置变成任务,并展示执行状态。计划编辑器不再承担 +主要执行入口,用户应先保存 YAML,再在作战页加入任务列表或任务队列。 + +### 4.2 当前任务状态 + +顶部状态区展示: + +- 当前任务名称。 +- 运行、等待、已停止等状态。 +- 已完成次数和总次数。 +- 动态进度条。 +- 远征检查剩余时间。 +- 后端在线状态。 + +没有任务时不会伪造进度,状态栏回到空闲状态。 + +### 4.3 当前舰队预览 + +作战页增加当前舰队预览: + +- 从正在运行的任务请求中读取明确携带的舰队规则。 +- 支持从 `fleet_rules` 或内联计划中的 `fleet_presets[].ships` 提取舰名。 +- 泡澡维修触发队伍切换后,预览同步使用切换后的舰队。 +- 舰名优先使用 `name`,缺少时使用 `search_name`。 +- 使用舰船资料库解析立绘、背景、边框、舰种和稀有度。 +- 复用舰队规划中的舰娘卡片结构。 +- 主页预览不显示左上角编号,减少小尺寸卡片的信息拥挤。 +- 720P 下最多一行展示六张卡片。 +- 没有任务执行时在预览框中央显示“当前无任务执行”。 +- 任务存在但没有明确舰队规则时不展示虚构卡片。 + +只携带 `fleet_id`、没有携带具体舰名的旧任务无法可靠推断游戏内实际队伍, +因此不会猜测并展示错误舰队。 + +### 4.4 任务列表 + +任务列表以分组方式持久化常用任务: + +- 新建任务分组。 +- 修改分组名称。 +- 保存分组。 +- 删除分组。 +- 导入和导出。 +- 加入快捷动作。 +- 加入系统或用户出征计划。 +- 整组加入任务队列。 +- 记录当前激活分组。 + +任务列表中的出征计划优先保存受管身份: + +```text +managedSource + managedFile +``` + +旧数据如果只有路径,会根据 `system_battle_plans` 或 +`user_battle_plans` 推断来源和文件名。 + +显式保存按钮只有在 `task_groups.json` 写入完成后才显示成功提示。 + +### 4.5 任务队列 + +任务队列继续作为实际调度入口,支持: + +- 查看排队顺序。 +- 开始执行。 +- 停止当前任务。 +- 清空队列。 +- 展示任务次数和完成状态。 +- 将受管出征计划在执行前展开为完整运行时 YAML。 +- 在任务状态变化时同步顶部状态与舰队预览。 + +### 4.6 快捷动作 + +快捷动作与任务列表位于同一区域,包括: + +- 收取远征。 +- 收取奖励。 +- 收取建造。 +- 食堂烹饪。 +- 浴室修理。 + +快捷动作继续通过现有后端 API 执行,不强行转换为不存在的 YAML 契约。 + +### 4.7 日志 + +- 通过 WebSocket 接收后端日志。 +- 支持日志等级过滤。 +- 日志等级由设置页保存。 +- 完整用户配置只在调试模式下使用 `debug` 等级输出。 +- 普通模式不打印敏感的完整配置 YAML。 +- 已移除向本机 `127.0.0.1:7777` 发送调试事件的临时代码。 + +## 5. 舰队规划 + +### 5.1 页面定位 + +舰队规划用于生成独立舰队 YAML。用户通过本地舰船图鉴选择主选和备选, +GUI 将可视化状态转换为后端支持的 `ships` 结构。 + +### 5.2 舰娘卡片 + +舰娘卡片由以下层级组成: + +1. 稀有度背景。 +2. 舰船立绘。 +3. 舰种图标。 +4. 左上角编号。 +5. 底部黑色半透明名称背景。 +6. 根据稀有度显示的舰名颜色。 + +卡片在舰船图鉴、主选舰队、备选队列和出征预览之间复用。 +主页的小型预览复用同一结构,但按页面需求隐藏编号。 + +### 5.3 舰船搜索与筛选 + +舰船图鉴支持: + +- 按舰名模糊搜索。 +- 按编号搜索。 +- 舰种多选。 +- 舰型集合筛选。 +- 国籍多选。 +- 按类型、名称或编号排序。 +- 正序与倒序切换。 +- 过滤改造:同一舰船存在改造前后形态时只保留改造形态, + 不会过滤掉本身没有改造形态的舰船。 +- 恢复默认筛选。 +- 筛选状态和排序状态持久化。 + +集合按钮与成员按钮联动: + +- 选择“大型舰”等集合时,激活集合包含的舰种。 +- 取消任意成员后,集合按钮立即取消激活。 +- 后续重新补齐成员不会自动重新点亮集合,除非用户再次主动选择集合。 + +### 5.4 主选位置 + +- 一支舰队最多六个位置。 +- 空位统一整理到右侧。 +- 删除前方主选后,后续完整位置规则向左移动。 +- 移动位置时,主选、等级限制和对应备选队列作为整体移动。 +- 点击已有位置可以替换舰船。 +- 相同舰船不会因为连续点击而重复加入多个位置。 +- 图鉴中已经被主选使用的形态会按同名规则隐藏。 +- 选择同舰的另一形态时执行替换,而不是新增重复位置。 + +### 5.5 备选队列 + +每个位置拥有独立备选队列: + +- 标题显示为 `【主选舰名】的备选队列`。 +- 没有主选时显示 `【位置 X】的备选队列`。 +- 支持增加备选槽位。 +- 新舰船默认进入最左侧空位。 +- 删除第七个及后续备选时,同时收缩多余空槽。 +- 选择已有备选后从图鉴点击舰船会替换当前项。 +- 只有焦点位于尾部空槽时,图鉴点击才执行追加。 +- 支持复制当前备选队列到其他位置。 +- 与目标备选完全一致时禁用复制。 +- 目标已有备选时要求用户确认覆盖。 +- 复制完成后保留当前主选焦点。 + +### 5.6 纯备选位置 + +GUI 支持“没有主选,只有备选”的位置: + +- 删除主选时可以保留其备选队列。 +- 主选卡片位置显示“使用备选队列”占位状态。 +- 纯备选位置不能被当作无效空位自动删除。 +- 只有主选和备选都为空时,整个位置才参与左移整理。 +- 保存时不要求顶层 `name`,但要求 `candidates` 非空。 + +这种结构用于让后端按候选顺序寻找第一个可用舰船。 + +### 5.7 等级限制 + +- 主选位置和备选队列分别维护等级限制。 +- 等级限制跟随位置规则移动。 +- 当前没有主选且备选也为空时,不能开启等级限制。 +- 开关关闭时不输出 `min_level` 或 `max_level`。 +- 最小等级不能大于最大等级。 +- 值域和后端支持范围保持一致。 +- 舰种规则根据舰船资料自动写入,不要求普通用户手工输入。 + +### 5.8 拖拽 + +支持以下拖拽方向: + +- 图鉴到主选。 +- 图鉴到备选。 +- 主选到主选。 +- 备选到备选。 +- 主选到备选。 +- 备选到主选。 + +拖拽原则: + +- 拖到空白尾部时执行追加和自动整理。 +- 拖到已有卡片时根据区域执行交换或替换。 +- 位置移动必须携带对应备选和等级规则。 +- 从图鉴拖入后,焦点切换到实际落位的位置。 +- 动态渲染后恢复图鉴和备选容器原有滚动位置。 + +### 5.9 新建、保存和加载 + +舰队文件具有明确生命周期: + +- “新建”只在存在真实未保存修改时确认。 +- 新建后清空当前文件身份和来源。 +- “保存”更新当前文件。 +- 已加载方案修改名称后,仍按当前文件身份处理更新或改名。 +- 新建同名方案时询问是否覆盖。 +- “加载”显示系统和用户方案列表。 +- 当前文件按 `source + file` 预选。 +- 加载前检查未保存修改。 +- 保存成功后更新当前文件、来源和保存快照。 +- 系统方案修改后保存为用户方案。 + +### 5.10 舰队 YAML + +标准文件名: + +```text +team-{预设名称}.yaml +``` + +普通主选和备选示例: + +```yaml +name: 潜艇周常队 +ships: + - name: U-47 + ship_type: [ss, ssg] + min_level: 100 + max_level: 110 + candidates: + - {name: U-96, ship_type: [ss], min_level: 100, max_level: 110} + - {name: U-1206, ship_type: [ss], min_level: 100, max_level: 110} +``` + +纯备选位置示例: + +```yaml +name: 纯备选示例 +ships: + - candidates: + - {name: U-47, ship_type: [ss]} + - {name: U-96, ship_type: [ss]} +``` + +序列化约束: + +- 空字段不输出 `null`。 +- `ships` 的顺序对应位置顺序。 +- 主选字段分行显示。 +- 备选对象使用紧凑行内格式,避免大型候选池纵向膨胀。 +- `candidates` 必须是对象列表。 +- `ship_type` 使用后端代码值。 +- `fleet_id` 不在独立舰队文件中维护。 + +## 6. 舰船资料库 + +### 6.1 数据结构 + +本地资料库位于 `resource/ship-library/`,包括: + +- `database/ships.sqlite3`:可增量维护的数据源。 +- `manifest.json`:GUI 快速加载的舰船索引。 +- `labels.zh-CN.json`:中文显示标签。 +- `assets/portraits/`:舰船立绘。 +- `assets/backgrounds/`:稀有度背景。 +- `assets/frames/`:稀有度边框。 +- `assets/type-icons/`:舰种图标。 + +当前资料库包含: + +- 894 条有效舰船记录。 +- 894 张舰船立绘。 +- 6 张共享稀有度背景。 +- 6 张共享稀有度边框。 +- 23 张舰种图标。 +- 0 个缺失资源。 + +数据库字段和内部枚举使用英文,GUI 显示标签使用中文。 + +### 6.2 增量更新工具 + +`tools/ship_library/update_ship_library.py` 提供: + +- 从舰 R 百科 Lua 数据模块读取结构化舰船数据。 +- 以 Lua `type` 字段作为舰种语义来源,页面图标只用于定位对应资源。 +- 自动识别 Wiki legacy/native 舰种 schema;旧 code 只在导入边界转换, + 输出结果必须通过 `autowsgr_native.VesselType` canonical 校验。 +- Wiki `CF` 在导入边界转换为后端 canonical `cav`,不进入本地舰种集合。 +- GUI 读取 schema 2/3 既有资料库时会在内存中转换旧舰种 code, + 保留原有立绘、背景、边框和舰种图标路径,不改写用户数据库。 +- 从舰娘图鉴页读取立绘、背景、边框和舰种图标地址。 +- 使用稳定舰船编号作为主键。 +- 保存数据源版本、哈希和更新时间。 +- 只下载新增、变更或缺失资源。 +- 下载使用临时文件并原子替换。 +- 失效记录在 SQLite 中标记,不直接破坏历史信息。 +- 更新完成后重新生成 GUI manifest。 +- 输出机器可读结果供 Electron IPC 使用。 + +设置页提供“更新舰船数据库”按钮、当前版本状态和更新进度。 +打包配置只携带更新工具本身,不携带无关脚本。 + +## 7. 出征规划 + +### 7.1 页面定位 + +出征规划负责生成地图级出征 YAML,并引用独立舰队方案。页面分为: + +- 左侧出征配置、任务配置和编队管理。 +- 右侧地图节点路线、节点信息和舰队选择/预览。 + +已删除旧引导页,进入后直接编辑。 + +### 7.2 出征配置 + +支持: + +- 章节选择。 +- 地图选择。 +- 战况目标。 +- 维修策略。 +- 维修方式。 +- 泡澡维修相关旧配置。 + +默认维修阈值由现有后端行为处理,不再为每艘船生成重复的默认 50% 阈值。 + +### 7.3 任务配置 + +支持: + +- 执行次数。 +- 轮次间隔。 +- 战利品停止条件。 +- 掉落停止条件。 +- 自定义预设名称。 + +停止条件规则: + +- 开关关闭时保存为禁用值,不展示输入文案。 +- 开关开启后才显示并允许编辑数量。 +- 战利品数量范围为 1 到 50。 +- 掉落数量范围为 1 到 500。 + +### 7.4 编队关联 + +- 从系统和用户舰队方案中选择。 +- 选择卡片默认只预览。 +- 打开卡片右侧关联开关后,才加入当前出征计划。 +- 支持同时关联多个舰队方案。 +- 左侧显示已关联数量和方案名称。 +- 编队预览展示主选、纯备选占位和全部备选。 +- 主选卡片显示独立等级范围。 +- 预览只读,修改必须回到舰队规划。 + +### 7.5 地图与节点 + +- 地图按原始宽高比缩放。 +- 地图尽可能利用容器空间,但不会超出边框。 +- 点击节点打开节点编辑。 +- 未选择节点时显示“点击地图上的节点查看和编辑”。 +- 节点路线和节点信息位于同一大区域。 + +节点编辑支持: + +- 节点启用开关。 +- 终点节点。 +- 阵型单选。 +- 前进。 +- 夜战。 +- 远程打击。 +- 迂回。 +- 迂回失败后 S/L。 +- 最低战果。 +- 索敌规则文本。 + +节点关闭时: + +- 其他编辑控件隐藏。 +- 本次编辑中的值仍保留在前端内存。 +- 保存 YAML 时不输出关闭节点的其他选项。 +- 切换文件或关闭程序后,未保存内存状态自然丢弃。 + +索敌规则输入框提示 `detour`、`retreat` 等现有后端规则用法。 + +### 7.6 新建、保存和加载 + +出征计划与舰队方案使用相同的生命周期模型: + +- 当前文件由 `source + file` 唯一标识。 +- 新建前只在有真实修改时提示。 +- 加载前检查未保存修改。 +- 保存直接更新当前用户文件。 +- 改名保存时创建标准新文件并删除旧用户文件。 +- 同名覆盖需要确认。 +- 系统计划修改后保存为用户计划。 +- 保存成功后更新快照,不再误报未保存。 +- 新建后自动聚焦预设名称输入框。 + +标准文件名: + +```text +bettle-{预设名称}.yaml +``` + +项目继续沿用现有 `bettle-` 拼写,以避免破坏已经持久化的文件身份。 + +### 7.7 编辑态引用与运行态展开 + +编辑态保存: + +```yaml +chapter: 1 +map: 1 +times: 3 +fleet_presets: + - name: 4日驱周常队 +``` + +任务执行前,主进程会: + +1. 根据计划来源和舰队名称查找独立舰队文件。 +2. 优先匹配同来源方案。 +3. 读取并校验舰队 `ships`。 +4. 将舰队完整嵌入 `fleet_presets`。 +5. 保留原计划顶部注释。 +6. 将完整 YAML 写入进程临时目录。 +7. 把临时文件路径传给后端执行。 + +如果引用舰队不存在,执行准备阶段明确失败,不会向后端提交半完整计划。 + +### 7.8 旧计划自动升级 + +旧计划可能在 `fleet_presets` 中直接包含舰队内容。首次启动迁移和计划加载 +浮窗中的“添加本地 YAML”都会复用当前 Codec 执行以下转换;计划页不再要求 +用户手工点击独立的“转换旧计划”: + +- 读取旧计划。 +- 为每个内嵌舰队生成独立 `team-{名称}.yaml`。 +- 将出征计划中的舰队改为名称引用。 +- 将旧字符串舰名转换为结构化对象。 +- 将旧 `priority` 按顺序转换为主选和 `candidates`。 +- 保留结构化纯备选位置。 +- 同名不同内容的计划或舰队保存为“(旧版)”副本。 +- 通过临时文件和原子替换完成多文件写入。 +- 保证再次读取时可以恢复完整运行时 YAML。 + +手动添加时,升级结果按 `bettle-*`、`team-*` 规范写入用户受管目录,源文件 +保持不变;同名目标必须由用户确认后才能覆盖。旧格式不会作为新编辑器的 +输出格式,外部文件路径也不会直接进入任务队列。 + +v5 启动迁移还会递归识别旧安装目录中的有效计划 YAML,并合并旧设置、任务组 +和模板。旧字段覆盖同名字段,当前版本新增字段保留默认值。迁移状态按来源 +路径和内容哈希记录,失败项下次启动重试;本次有实际迁移项时弹窗展示总数、 +成功数、失败数,并说明旧版原始文件均已保留。 + +## 8. 计划管理 + +### 8.1 统一清单 + +计划管理读取: + +- 系统出征计划。 +- 用户出征计划。 +- 系统舰队方案。 +- 用户舰队方案。 +- 任务分组。 +- 忽略未关联状态。 + +表格展示: + +- 计划或舰队名称。 +- 来源。 +- 出征计划与舰队关联。 +- 所属任务分组。 +- 当前状态。 +- 可用操作。 + +### 8.2 状态识别 + +能够识别: + +- 出征计划引用了哪些舰队。 +- 舰队被哪些出征计划引用。 +- 出征计划被哪些任务分组引用。 +- 用户舰队未被引用。 +- 出征计划没有关联舰队。 +- 引用舰队文件缺失。 +- YAML 无法读取。 +- 文件名不符合管理规则。 + +“未加入任务分组”只作为信息展示,不自动计入“需要处理”。 + +### 8.3 筛选和搜索 + +支持: + +- 按系统或用户来源筛选。 +- 按出征计划或舰队方案筛选。 +- 搜索计划名称。 +- 搜索舰队名称。 +- 搜索任务分组名称。 +- 仅查看需要处理项目。 + +### 8.4 操作 + +- 跳转到舰队规划并加载指定舰队。 +- 跳转到出征规划并加载指定计划。 +- 删除用户出征计划。 +- 删除用户舰队方案。 +- 忽略不需要处理的未关联提示。 +- 恢复已经忽略的提示。 + +忽略状态写入: + +```json +{ + "plan_management_ignored_unlinked": [] +} +``` + +文件改名时同步迁移忽略键,避免状态丢失。 + +所有动态列表在重新渲染后恢复原滚动位置,避免点击后跳回顶部。 + +## 9. 决战计划(旧) + +GUI 2.0 暂时保留旧决战配置,明确标注为兼容页面: + +- 章节。 +- 快速修理。 +- 两组等级范围。 +- 现有决战筛选和配置。 +- 重置与保存。 + +保存调用主进程设置 IPC。只有 `gui_settings.json` 写入完成并返回规范化结果后, +才显示“决战配置保存成功”。 + +该页面不代表最终统一 YAML 设计。决战流程后续需要从旧直接配置中独立出来, +与普通出征任务形成清晰边界。 + +## 10. 设置页 + +### 10.1 页面结构 + +设置页改为分组列表,标题和备注优先同一行,空间不足时自然换行。 +内容宽度限制在可读范围内并居中,减少大窗口下无意义的横向拉伸。 + +### 10.2 系统设置 + +#### 模拟器与 ADB + +- 模拟器类型。 +- 模拟器路径和浏览按钮。 +- 游戏账号。 +- ADB 地址。 +- 主动连接。 +- 断开连接。 +- 自动检测。 +- 输入框右侧在线、离线或未检测状态。 +- 完整地址和错误信息通过悬停查看。 + +MuMu 12 常用地址为 `127.0.0.1:16384`,但 GUI 不强制覆盖用户输入。 + +#### 自动化设置 + +- 自动远征。 +- 自动战役和战役类型。 +- 自动演习和舰队编号。 +- 自动出征和有序计划列表。 +- 自动胖次及预设参数。 +- 泡澡维修相关配置。 + +自动出征列表使用受管计划选择器,保存文件来源、名称和执行参数。 + +#### 日志与调试 + +- 日志等级。 +- 日志目录。 +- 调试模式。 +- 后端截图保存。 + +调试模式控制敏感完整配置日志,不在普通模式展示。 + +#### 环境配置 + +- Python 路径。 +- Python 浏览和检测。 +- 后端端口。 +- 后端启动模式。 +- 本地 AutoWSGR 仓库路径。 +- CUDA 路径。 +- 当前环境状态。 + +#### 更新与界面 + +- GUI 更新模式。 +- 默认窗口宽度和高度。 +- 记住退出时窗口大小与位置。 +- 更新舰船数据库。 +- 舰船数据库状态。 + +#### 颜色主题 + +- 亮色。 +- 暗色。 +- 跟随系统。 +- 主题强调色。 + +主题和强调色即时应用并持久化。 + +### 10.3 脚本行为 + +#### 全局延迟 + +- 最小延迟。 +- 最大延迟。 +- 输入与滑块联动。 +- 范围为 0 到 10 秒。 +- 最小值不能大于最大值。 +- 用户值写入 `usersettings.yaml`,优先于后端默认值。 + +#### OCR + +- 模型下载源。 +- 加速模式。 +- CUDA 路径。 +- 硬件识别开关。 +- 舰名匹配置信度。 +- 舰名别名。 +- OCR 纠错映射。 + +用户 OCR 规则由后端配置模型读取。无效目标舰名应由后端按 `SHIPNAMES` +校验并跳过,同时输出警告。 + +#### 舰队与脚本配置 + +- 船坞和维修相关行为。 +- 解装行为。 +- 浴室数量。 +- 自定义方案目录。 +- 后端已经开放给用户的其他低风险配置。 + +GUI 保留未知 YAML 字段,避免保存设置时破坏后端新增但当前 GUI 尚未展示的配置。 + +### 10.4 设置保存事务 + +设置同时涉及: + +- `localStorage` 主题状态。 +- 多个 GUI 设置 IPC。 +- `usersettings.yaml`。 +- 自动化调度器状态。 + +保存前会检查所有必需 bridge 接口是否存在。任一接口缺失或写入失败时: + +- 显示保存失败。 +- 不显示顶部成功提示。 +- 建议完整重启 GUI,避免旧 preload 与新渲染代码混用。 + +配置持久化全部成功后才显示“设置保存成功”。保存后的后端重连属于后续动作, +重连失败不会否定已经成功写入磁盘的配置。 + +## 11. 主题、响应式和交互统一 + +### 11.1 亮色主题 + +新增完整亮色主题,覆盖: + +- 页面背景。 +- 卡片和边框。 +- 输入框。 +- 下拉框。 +- 模态框。 +- 任务列表。 +- 舰船卡片文字。 +- 状态颜色。 +- 滚动条。 + +暗色和亮色共享语义变量,避免页面单独硬编码造成对比度不一致。 + +### 11.2 下拉框与开关 + +- 下拉框使用统一半透明背景和高对比文字。 +- 布尔配置统一使用开关样式。 +- 重要筛选不再依赖浏览器默认下拉控件。 +- 开关关闭时关联输入框禁用或隐藏。 + +### 11.3 滚动条 + +- 统一使用约 5px 主题细滚动条。 +- 舰船图鉴、备选列表、任务列表和计划管理保持相同视觉语言。 +- 动态 DOM 重建前记录 `scrollTop` 和 `scrollLeft`。 +- 重建后恢复位置,不因点击、删除或拖拽跳回顶部。 + +### 11.4 分辨率适配 + +- 以 720P 为主要布局基准。 +- 支持更窄窗口,不限制大窗口扩展。 +- 小窗口下工具栏允许分组换行。 +- 舰队主选在 720P 下保持一行六张卡片。 +- 图鉴卡片保持可识别尺寸,宽窗口增加每行数量。 +- 表格在列数较多时使用容器横向滚动,不挤坏操作按钮。 +- 标题、备注和操作按钮保持同一视觉层级。 + +## 12. Electron IPC 与文件安全 + +### 12.1 preload 契约 + +所有新增主进程能力通过 `contextBridge` 暴露明确方法,并在 +`src/types/electronBridge.ts` 中维护对应类型,包括: + +- ADB 检测、连接和断开。 +- 窗口设置。 +- 后端模式和仓库路径。 +- OCR 与 CUDA 设置。 +- 自动化设置。 +- 舰船资料库状态和更新。 +- 舰队方案列出、读取、保存和删除。 +- 出征计划列出、读取、保存、转换和删除。 +- 计划管理。 +- 忽略提示状态。 +- 运行时出征计划展开。 +- 决战兼容设置。 + +### 12.2 路径校验 + +- 文件参数必须是纯文件名,拒绝目录穿越。 +- 舰队文件必须以 `team-` 或兼容的 `team_` 开头。 +- 出征计划必须是 YAML 文件。 +- 新文件名会清除 Windows 非法字符。 +- 文件名长度限制在可维护范围内。 +- 系统和用户来源由枚举值控制,不接受任意目录。 + +### 12.3 原子写入 + +关键 YAML 保存采用: + +1. 在目标目录创建唯一临时文件。 +2. 完整写入内容。 +3. 将临时文件重命名为目标文件。 +4. 替换失败时清理临时状态并返回错误。 + +多文件旧计划迁移会先分配无冲突的“(旧版)”文件名,再执行写入;计划写入 +失败时清理本次新建的舰队文件,降低只生成一半文件的风险。 + +### 12.4 覆盖和改名 + +- 同名新建要求用户确认。 +- 当前文件原名保存直接覆盖。 +- 用户文件改名后删除旧文件。 +- 系统文件不能因改名流程被删除。 +- 保存结果返回新文件身份,渲染进程据此更新快照。 + +## 13. 后端环境管理 + +### 13.1 managed 模式 + +- 使用 GUI 管理的 Python。 +- 依赖安装到 GUI 自己的 `python/site-packages`。 +- 不写入系统 Python 全局目录。 +- 启动 GUI 管理的 uvicorn。 +- 根据更新模式检查 AutoWSGR 依赖。 +- 验证导入路径位于 GUI 管理目录。 + +### 13.2 external 模式 + +- 使用用户指定 Python,通常是本地 AutoWSGR `.venv`。 +- `backend_repo_path` 必须是包含 `autowsgr/server/main.py` 的仓库根目录。 +- 通过 Python 模块路径加载本地源码。 +- 验证 `autowsgr.__file__` 位于指定仓库。 +- 不混入 GUI 自带 Python 3.12 的二进制依赖。 +- 不检查或升级远端 `autowsgr`。 +- 缺依赖时使用本地仓库本身作为 pip requirement。 + +这保证 Python 3.13 后端虚拟环境不会错误加载 GUI Python 3.12 的二进制包。 + +### 13.3 启动与错误处理 + +- 后端端口可配置。 +- 外部后端模式支持只连接已有服务。 +- 环境检查输出阶段性进度。 +- 错误信息区分 Python、依赖、导入来源和后端连接问题。 +- 环境检查和安装同时覆盖更新器所需的 `requests` 与 `beautifulsoup4`。 +- 启动过程不再发送无关的本地调试 HTTP 请求。 + +## 14. 数据持久化 + +| 文件 | 用途 | +| --- | --- | +| `usersettings.yaml` | AutoWSGR 后端用户配置 | +| `gui_settings.json` | GUI 环境、窗口、主题、调度和页面状态 | +| `task_groups.json` | 作战页任务分组 | +| `userData/user_team_plans/*.yaml` | 用户舰队方案 | +| `userData/user_battle_plans/*.yaml` | 用户出征计划 | + +GUI 2.0 对持久化作出以下约束: + +- 后端配置值优先于代码默认值。 +- GUI 设置和后端 YAML 设置分开保存。 +- 未识别的后端 YAML 字段尽量保留。 +- 用户目录不会被打包为系统资源。 +- 系统资源更新不覆盖用户文件。 +- 当前文件身份不依赖输入框中的显示名称。 +- 未保存判断使用序列化快照,不用单一 `dirty` 点击标志代替真实比较。 + +## 15. 代码结构调整 + +### 15.1 新增主要模块 + +| 模块 | 职责 | +| --- | --- | +| `FleetPlannerView.ts` | 舰队规划、图鉴筛选、拖拽和计划管理 | +| `ShipArtwork.ts` | 舰船资料与卡片资源解析 | +| `TeamPlanListUi.ts` | 舰队加载列表共用逻辑 | +| `FleetPreviewView.ts` | 作战页当前舰队预览 | +| `DecisivePlanView.ts` | 旧决战配置兼容页面 | +| `TaskListLoaderController.ts` | 作战页受管计划选择 | +| `managedPlanReader.ts` | 任务分组中的计划元信息读取 | +| `scrollPosition.ts` | 动态列表滚动位置保存与恢复 | +| `_fleet-planner.scss` | 舰队规划和计划管理样式 | +| `_decisive-plan.scss` | 旧决战页面样式 | +| `_save-notice.scss` | 顶部保存成功提示 | +| `_light.scss` | 亮色主题 | + +### 15.2 主进程扩展 + +Electron 主进程通过组合根装配以下 Service: + +- GUI 设置规范化。 +- 用户数据目录定位。 +- 舰队 YAML 规范化和序列化。 +- 出征计划引用展开。 +- 旧计划迁移。 +- 计划管理聚合。 +- 文件安全校验。 +- 原子写入。 +- ADB 操作。 +- 舰船资料库更新。 +- 窗口状态持久化。 + +`electron/main.ts` 只负责依赖装配、IPC 注册和生命周期;格式、存储、迁移与 +运行时准备分别由 Codec、Repository 和 Service 负责。 + +### 15.3 旧资源清理 + +- 删除旧 `resource/builtin_plans/`,替换为系统出征和系统舰队目录。 +- 删除四个不再自动加载的旧模板示例 JSON。 +- 删除临时 Issue 数据和一次性 PowerShell 脚本。 +- 删除 `.dbg`、Python 缓存和未引用的舰娘卡片原型。 +- `.gitignore` 增加调试目录、Python 缓存和临时调试文档规则。 +- 打包文件列表只包含运行所需 HTML、CSS、编译产物和资源。 + +模板模型和 `templates/templates.json` 路径暂时保留,用于旧任务和用户模板兼容; +删除的是无运行引用的示例文件,不是兼容模型。 + +## 16. 构建与测试 + +### 16.1 构建 + +```powershell +npm run build +``` + +覆盖: + +- 清理旧 `dist`。 +- 编译 SCSS。 +- TypeScript 编译。 +- 渲染进程 esbuild 打包。 + +### 16.2 旧计划迁移测试 + +```powershell +npm run test:legacy-plan +``` + +测试在系统临时目录中: + +- 模拟已有旧迁移状态的用户升级到 v5。 +- 深度合并旧设置并保留当前版本独有字段。 +- 递归扫描旧安装目录中的有效计划 YAML。 +- 迁移旧 `plans/` 和旧版 `resource/user_*` 目录。 +- 升级旧内嵌舰队计划和独立舰队。 +- 校验普通主选、候选和纯备选结构。 +- 校验计划、舰队拆分及同名“(旧版)”副本。 +- 校验标准文件名、任务组受管引用和幂等状态。 +- 校验迁移完成提示的成功、失败数量和失败文件说明。 +- 校验旧源文件仍然保留。 +- 不接触真实用户配置。 + +### 16.3 设置持久化测试 + +```powershell +npm run test:settings +``` + +测试使用隐藏 Electron 窗口和临时目录: + +- 渲染全部设置值。 +- 从表单重新收集。 +- 验证非法延迟区间。 +- 调用真实 `ConfigController` 保存逻辑。 +- 模拟完整 preload bridge。 +- 重新读取 `usersettings.yaml` 和 `gui_settings.json`。 +- 验证主题、强调色和调试模式。 +- 验证未知 YAML 字段保留。 +- 不修改项目真实用户设置。 + +### 16.4 静态检查 + +```powershell +git diff --check +``` + +用于检查行尾空白和补丁格式。 + +严格未使用声明检查: + +```powershell +npx tsc --noEmit --noUnusedLocals --noUnusedParameters --pretty false +``` + +当前已知仍有开发残留提示,详见 +`docs/reviews/2026-08-03-workspace-review-pending.md`。默认构建不启用这两个选项。 + +## 17. 已知边界和后续工作 + +### 17.1 出征规划直接执行链 + +`PlanController` 中仍保留一套没有页面入口的直接执行链。当前推荐流程是保存计划后, +从作战页加入任务列表和队列。待主页执行入口稳定后,需要决定: + +- 恢复明确的“加入任务队列”入口并补测试;或 +- 删除无调用的直接执行链。 + +### 17.2 旧后端源码更新链 + +旧的手动源码检查和拉取实现仍有部分跨层残留。它与以下功能不同: + +- GUI 自身的 `electron-updater`。 +- managed 环境中的 AutoWSGR 依赖更新。 +- external 模式下的本地源码联调。 + +后续需要先确定产品策略,再整体恢复或整体删除,不能只清理其中一层。 + +### 17.3 TypeScript 未使用声明 + +严格检查仍可能报告未使用导入、参数和只写字段。当前正常类型编译通过。 +后续应在页面入口和设置策略稳定后逐项判断是补齐功能还是删除声明。 + +### 17.4 决战流程 + +决战页面仍是兼容入口,尚未完成统一 YAML 化。未来应将决战执行器从旧页面配置中 +独立出来,并与普通出征形成平级、可测试的执行模型。 + +### 17.5 当前舰队预览 + +预览只显示任务请求中能够可靠识别的具体舰船。只有 `fleet_id` 的旧任务不会读取 +游戏屏幕或猜测队伍内容。若后端未来提供当前实际舰队状态接口,可再接入实时状态。 + +### 17.6 视觉回归 + +本轮以用户快速目测为主要视觉验收方式。代码侧负责数据结构、保存、加载、 +YAML 往返、IPC 和构建。不同 Windows 缩放比例仍建议在发布前做一次集中视觉回归。 + +## 18. 兼容与迁移说明 + +### 18.1 旧出征计划 + +- v5 启动迁移自动升级,不再提供手工转换入口。 +- 不覆盖不同内容的同名目标,旧内容保存为“(旧版)”副本。 +- 迁移后使用标准受管目录和 `bettle-*`、`team-*` 文件名。 +- 旧源文件保留,迁移失败会在下次启动重试。 +- 迁移完成后弹窗展示本次总数、成功数和失败数;无实际迁移时不弹窗。 +- 新编辑器不继续生成旧字符串候选格式。 + +### 18.2 旧任务分组 + +- 保留路径形式的计划项。 +- 尝试从目录推断系统或用户来源。 +- 保存后逐步迁移到 `managedSource + managedFile`。 + +### 18.3 旧模板 + +- 内置模板继续从 `resource/builtin_templates.json` 读取。 +- 用户模板模型继续支持 `templates/templates.json`。 +- 旧页面中的可见模板库已经移除。 +- 无引用的四个模板示例文件不再保留。 + +### 18.4 旧舰队字段 + +- 新独立舰队不写 `fleet_id`。 +- 读取时可识别有限旧字段用于转换。 +- 新保存严格输出 `name`、`ships` 和位置规则。 +- 不生成 `priority`。 + +## 19. 审查重点 + +建议审查者按以下顺序检查: + +1. `electron/main.ts` 中路径校验、原子写入和运行时展开。 +2. `PlanModel.ts` 与舰队/出征 YAML 往返。 +3. `FleetPlannerView.ts` 的位置、备选和拖拽状态转换。 +4. `PlanController.ts` 的新建、加载、保存、改名和来源身份。 +5. `ConfigModel.ts` 与 `ConfigController.ts` 的字段保留和保存事务。 +6. `SchedulerBinder.ts`、`TaskQueue.ts` 和运行时计划准备。 +7. preload 与 `electronBridge.ts` 的方法签名一致性。 +8. 系统资源和用户目录在开发版、打包版中的定位。 +9. 舰船资料库更新失败时是否保留现有可用数据。 +10. 720P 布局、亮色主题和动态列表滚动位置。 + +不建议仅根据单个文件评估本次改造。舰队保存、出征引用和任务执行跨越: + +```text +View +→ Controller +→ preload bridge +→ Electron IPC +→ 用户 YAML +→ 运行时展开 +→ Scheduler +→ AutoWSGR API +``` + +任何一层缺失都会造成“界面看起来成功,但任务无法正确执行”的问题。 + +## 20. 验收清单 + +### 作战 + +- [ ] 没有任务时显示“当前无任务执行”。 +- [ ] 有明确舰队的任务显示最多六艘舰船。 +- [ ] 主页预览不显示编号。 +- [ ] 任务分组保存失败时不显示成功提示。 +- [ ] 动态列表操作后滚动位置不跳回顶部。 + +### 舰队规划 + +- [ ] 图鉴搜索、筛选、排序和过滤改造正确。 +- [ ] 主选、备选和图鉴之间拖拽正确。 +- [ ] 删除主选时备选按规则保留。 +- [ ] 纯备选位置可以保存和加载。 +- [ ] 等级限制跟随对应位置。 +- [ ] 同名覆盖需要确认。 +- [ ] 改名保存更新正确文件身份。 + +### 出征规划 + +- [ ] 新建、保存和加载具有未保存确认。 +- [ ] 关联多个舰队后可以再次加载。 +- [ ] 节点关闭时不输出节点选项。 +- [ ] 停止条件开关和值正确往返。 +- [ ] 用户计划使用标准文件名。 +- [ ] 执行前可以展开完整舰队。 +- [ ] 舰队缺失时明确失败。 + +### 计划管理 + +- [ ] 系统和用户计划均能列出。 +- [ ] 双向舰队关联正确。 +- [ ] 任务分组关联正确。 +- [ ] 忽略和恢复提示持久化。 +- [ ] 用户计划删除后列表刷新。 +- [ ] 编辑跳转加载正确文件。 + +### 设置 + +- [ ] ADB 检测、连接和断开可用。 +- [ ] managed 与 external 模式行为隔离。 +- [ ] 亮色、暗色和跟随系统正确。 +- [ ] 窗口默认尺寸和退出位置持久化。 +- [ ] OCR、自动化和脚本行为字段保存后可重新读取。 +- [ ] 保存接口缺失或写入失败时不显示成功。 +- [ ] 舰船资料库状态和更新进度可见。 + +### 工程 + +- [ ] `npm run build` 通过。 +- [ ] `npm run test:legacy-plan` 通过。 +- [ ] `npm run test:settings` 通过。 +- [ ] `git diff --check` 通过。 +- [ ] 安装包不包含用户 YAML。 +- [ ] 调试产物和 Python 缓存未进入版本库。 + +## 21. 用户可感知的最终变化 + +GUI 2.0 将原来分散的功能整理为一条明确工作流: + +```text +设置运行环境 +→ 更新舰船资料 +→ 可视化创建舰队 +→ 创建并关联出征计划 +→ 在计划管理检查状态 +→ 加入任务列表 +→ 加入任务队列并执行 +→ 在作战页查看任务、进度、舰队和日志 +``` + +这次改造解决的核心问题不是“增加更多按钮”,而是让每个按钮对应的数据状态、 +文件身份、保存结果和后端输入都能够被明确追踪。用户可以通过图形界面维护 YAML, +同时保留 AutoWSGR 后端模型作为唯一执行标准。 diff --git a/docs/features/plan-batch-export.md b/docs/features/plan-batch-export.md new file mode 100644 index 0000000..6e680d4 --- /dev/null +++ b/docs/features/plan-batch-export.md @@ -0,0 +1,33 @@ +# 计划批量导出 + +## 功能入口 + +计划管理页的用户来源按钮右侧提供“批量导出”按钮。表格最左侧为选择列: + +- 用户出征计划和用户舰队方案支持单选。 +- 表头复选框选择或取消当前筛选结果中的全部用户配置。 +- 系统预设不显示复选框,也不能通过导出 IPC 读取。 +- 切换来源、类型或搜索条件时,已经勾选的用户配置保持选中。 + +## 导出格式 + +保存文件名默认为本地日期生成的 `YYYY-MM-DD-plans.zip`。ZIP 内固定使用以下目录: + +```text +user_bettle_plans/ +user_team_plans/ +``` + +出征计划保留原文件名写入 `user_bettle_plans/`,舰队方案保留原文件名写入 +`user_team_plans/`。 + +## 安全边界 + +Renderer 仅向主进程提交计划类型和文件名。主进程会重新校验: + +- 类型只能是 `battle` 或 `team`。 +- 文件名必须是不含目录的 YAML 文件名。 +- 文件必须真实存在于对应的用户计划目录中。 +- 文件真实路径不能通过符号链接逃逸用户计划目录。 + +ZIP 只能写入系统保存对话框返回的用户授权路径。 diff --git a/docs/plan-guide.md b/docs/plan-guide.md index 1d3f2ac..ea6a5b1 100644 --- a/docs/plan-guide.md +++ b/docs/plan-guide.md @@ -1,6 +1,10 @@ # 战斗方案 YAML 编写指南 -本文档介绍如何编写 AutoWSGR-GUI 的战斗方案配置文件。方案文件为 YAML 格式,放置在 `plans/` 目录下,可在 GUI 中导入使用。 +本文档介绍 AutoWSGR-GUI 的战斗方案 YAML。系统预设来自只读 +`resource/system_battle_plans/`,GUI 保存的用户方案位于 Electron +`userData/user_battle_plans/`,并通过计划管理页统一加载。旧目录中的 YAML +会在启动时自动升级迁移;其他本地 YAML 可通过计划加载浮窗的“添加本地 +YAML”升级并复制到用户计划目录,原文件不会被修改。 --- diff --git a/docs/reviews/2026-08-03-pr18-migration-ledger.md b/docs/reviews/2026-08-03-pr18-migration-ledger.md new file mode 100644 index 0000000..29a4ad5 --- /dev/null +++ b/docs/reviews/2026-08-03-pr18-migration-ledger.md @@ -0,0 +1,67 @@ +# PR #18 Migration Ledger + +基线:GUI 2.0 `da5fd8d` +参考:下午整改代码 `reference/gui2-afternoon-20260803` -> `a58b1d1` + +## 已重新实现 + +- 用户计划、舰队、配置迁移到 Electron `userData`。 +- 任务组 v1→v2 首次加载写回磁盘,根级/组级/条目级未知字段保留。 +- 用户编队根级/槽位级/候选级未知字段 round-trip,candidate-only 结构保留。 +- 旧计划启动自动迁移,保留源文件、同名冲突另存、状态记录和失败重试。 +- 旧任务组 v1 兼容读取并转换为 v2,保留 `path`、managed identity 和未知条目字段。 +- v5 迁移按旧安装目录记录来源和内容哈希;已有 `userData` 时仍会深度合并 + 旧设置,迁移任务组、模板和有效 YAML,并更新队列引用。实际输出文件名写入 + 迁移状态,重复启动不会重复导入;本次完成后弹窗展示成功和失败数量。 +- Electron 文件 IPC 按读写能力限制到 `userData`/只读 `resource`,并拒绝 + `..`、越权绝对路径、UNC、盘符跳转和符号链接逃逸。 +- atomic write 失败时不删除旧文件。 +- Plan / Config 未知字段 round-trip。 +- candidate-only 编队语义。 +- scheduler logicalId 与逻辑完成事件。 +- external 后端无效时显式失败。 +- updater 结果区分 available / up-to-date / error。 +- 舰队 1 可更换编成,首槽保护保留。 +- 计划添加流程允许不绑定舰队预设。 +- AutoWSGR 编队 OCR info 日志。 +- AutoWSGR 主库的 20260730 E1/E5/H1/H5 系统计划恢复到只读 `resource/system_battle_plans`,并恢复 `builtin_event_20260730` 模板。 +- `main.ts` 已缩减为组合根;主进程按 22 个 Service 和 10 个 IPC 文件拆分, + IPC 通道名、参数顺序和同步/异步方式由契约测试锁定。 +- 舰船资料库使用临时目录、备份目录和失败恢复完成版本切换。 +- managed / external 的检查、安装和启动复用同一 Python 环境描述。 +- 作战计划、编队、任务组、API map / NodeDecision 均有兼容 fixture。 + +## 参考分支处理结论 + +- `electron/fileIpc.ts`:未直接移植;已按当前 SafePath 和 selected-file 能力重写为 `ipc/FileIpc.ts`。 +- `electron/appPaths.ts`:未直接移植;已拆为 `AppPaths`、`SafePathService`、`AtomicFileStore` 和迁移服务。 +- `electron/shipLibrary.ts`:未直接移植;已拆为 `ShipLibraryService` 和 `ShipLibraryUpdater` 并覆盖失败恢复。 +- `src/controller/taskGroup/managedPlanReader.ts`:当前 GUI 2.0 已有同名服务,需补 v1 path identity 和 preset/task group 场景测试。 +- `src/controller/taskGroup/TaskListLoaderController.ts`:参考分支没有该文件,不能直接移植;应保留 GUI 2.0 当前任务列表管理器。 +- Python 环境服务拆分:未使用参考分支的私有 monkey-patch;当前通过统一环境描述和显式依赖完成。 + +## 尚未完成的 Issue #18 项目 + +- Windows 文件锁自动化测试。 +- backend graceful shutdown 和完整进程树终止。 +- updater prerelease/stable channel 隔离。 +- managed / external / CPU / CUDA 真实环境矩阵。 +- 新 PR 的提交拆分。 + +## 验证记录 + +- `npm run test:settings` 曾在与其他 Electron 测试并行执行时因 Windows + `dist` 文件锁出现 `EPERM`;清理残留 Electron 进程后串行执行通过。后续 + Electron 测试必须串行运行,避免测试进程竞争构建目录。 +- 当前没有使用这个失败结果放宽测试;根因是测试资源生命周期竞争,不是设置持久化断言失败。 +- `npm run test:legacy-config-upgrade` 独立覆盖已有 `userData`、不同安装目录、 + 同名任务组合并、旧 YAML 升级、队列引用更新和重复启动幂等。PR 工作流 + `Legacy configuration upgrade compatibility` 会在 Windows runner 自动执行。 +- 使用 `AutoWSGR-GUI-old` 的真实数据在隔离临时目录完成两轮演练:空计划目标 + 下迁移 4 个作战计划和 8 个编队;复制当前完整 `userData` 后再次迁移时, + 当前队列和用户修改过的编队均保留,旧任务组引用升级成功。演练未修改真实 + `%APPDATA%` 和旧目录,临时目录已清理。 +- 主进程拆分完成后已通过: + `test:main-services`、`test:main-ipc`、`test:settings`、 + `test:legacy-plan`、`test:api-contract`、`test:task-group-migration`、 + `test:python-environment` 和 `test:event-resources`。 diff --git a/docs/reviews/2026-08-03-workspace-review-pending.md b/docs/reviews/2026-08-03-workspace-review-pending.md new file mode 100644 index 0000000..2163a73 --- /dev/null +++ b/docs/reviews/2026-08-03-workspace-review-pending.md @@ -0,0 +1,152 @@ +# GUI 工作区代码审查挂起项 + +> 记录日期:2026-08-03 +> 审查范围:AutoWSGR-GUI 当前工作区全部未提交代码 +> 当前状态:3 组问题挂起,等待相关页面和并行开发稳定后复查 +> 复核结果:两个独立审查线程均确认问题存在 + +## 记录目的 + +本轮审查的目标是清理调试代码、无用代码和可安全精简的结构。 +以下问题暂不修改,不代表问题已经解决,而是相关页面仍在开发, +现在删除可能干扰后续功能接入。 + +```mermaid +flowchart LR + A[发现可清理代码] --> B{相关功能是否稳定} + B -->|否| C[挂起并记录用途] + C --> D[页面与并行开发完成] + D --> E[重新检查调用和严格编译] + E --> F{代码是否仍有用途} + F -->|有| G[补齐入口和测试] + F -->|无| H[执行最小删除] + classDef pending fill:#fff3e0,color:#e65100 + classDef review fill:#bbdefb,color:#0d47a1 + classDef done fill:#c8e6c9,color:#1a5e20 + class C pending + class D,E review + class G,H done +``` + +## 挂起清单 + +| 编号 | 挂起问题 | 挂起原因 | 重新审查条件 | +|---|---|---|---| +| P-01 | 出征规划直接执行链没有调用入口 | 主页任务入口仍在开发,暂时不能判断这套执行链是否还会使用 | 主页任务入口和 YAML 执行方式确定后 | +| P-02 | 已停用的后端源码更新链仍有跨层残留 | 设置页面和更新功能仍在调试,暂时不能决定恢复还是删除 | 设置页面更新策略确定后 | +| P-03 | 严格检查仍有 4 条未使用提示 | 三条属于已挂起功能,一条属于待确认参数 | 当前一轮并行开发完成后 | + +## P-01 出征规划直接执行链 + +### 当前用途 + +这套代码原本用于在出征规划页面直接执行当前方案: + +1. 转换节点规则。 +2. 生成内存战斗计划。 +3. 为未保存方案创建临时 YAML。 +4. 组装舰队、维修和停止条件。 +5. 调用 `Scheduler.addTask()` 加入任务队列。 +6. 切回主页展示执行状态。 + +当前 `executePlan()` 没有按钮、事件绑定或其他代码调用,相关辅助方法只在 +这条封闭执行链中互相调用。 + +代码位置: + +- [节点转换、内存方案和临时文件](../../src/controller/plan/PlanController.ts#L1007-L1078) +- [直接执行和加入任务队列](../../src/controller/plan/PlanController.ts#L1080-L1158) + +### 挂起决定 + +- 当前不删除。 +- 主页任务入口完成后,先确认是否继续支持“出征规划直接加入队列”。 +- 如果保留,需要恢复明确入口并补充方案保存、入队参数和失败处理测试。 +- 如果统一改为从已保存 YAML 执行,则删除 `executePlan()` 及其专用辅助链。 + +## P-02 后端源码更新残留 + +### 当前用途 + +这是一套旧的手动后端源码更新流程,原设计为: + +```text +设置页面检查更新 +→ preload bridge +→ Electron IPC +→ checkForUpdates / pullUpdates +→ 拉取本地 AutoWSGR Git 仓库 +``` + +目前页面调用、preload bridge 和主进程 IPC 已通过注释停用,但以下内容仍保留: + +- 主进程中的未使用导入和注释 IPC: + [main.ts:L16-L17](../../electron/main.ts#L16-L17)、 + [main.ts:L2175-L2201](../../electron/main.ts#L2175-L2201) +- 注释掉的 preload bridge: + [preload.ts:L276-L292](../../electron/preload.ts#L276-L292) +- 保留的 bridge 类型: + [electronBridge.ts:L223-L237](../../src/types/electronBridge.ts#L223-L237) +- 仍然存在的检查和拉取实现: + [installer.ts:L131-L166](../../electron/pythonEnv/installer.ts#L131-L166)、 + [installer.ts:L251](../../electron/pythonEnv/installer.ts#L251) +- 设置页和启动流程中的停用调用: + [AppController.ts:L763-L789](../../src/controller/app/AppController.ts#L763-L789)、 + [envAndUpdates.ts:L88-L108](../../src/controller/startup/envAndUpdates.ts#L88-L108) + +这套旧流程不能和以下仍在使用的功能混为一谈: + +- `electron-updater` 提供的 GUI 自身更新。 +- 环境检查中的 AutoWSGR Python 包更新。 + +### 挂起决定 + +- 当前不删除,也不恢复。 +- 如果后续恢复手动源码更新,需要先定义 `external` 和 `managed` 两种后端模式的更新边界。 +- 如果后续确认不再使用,需要一次性删除导入、IPC、bridge、类型、实现和大段注释。 +- 清理时必须保留 GUI 自动更新和仍在使用的环境依赖更新。 + +## P-03 严格未使用检查 + +检查命令: + +```powershell +npx tsc --noEmit --noUnusedLocals --noUnusedParameters --pretty false +``` + +2026-08-03 最新结果为 4 条提示,全部属于未使用声明,没有发现其他类型错误。 +默认 `npm run build` 不启用这两个严格选项,因此正常构建仍能通过。 + +| 来源 | 数量 | 内容 | +|---|---:|---| +| P-01 | 1 | `executePlan` 没有调用 | +| P-02 | 2 | `checkForUpdates`、`pullUpdates` 导入未使用 | +| 其他开发残留 | 1 | `RepairManager` 的 `fleetId` 参数未使用 | + +前三条需要随 P-01 和 P-02 的功能去留一起处理。`fleetId` 需要先确认泡澡维修流程 +是否仍应按舰队区分,再决定补齐用途或删除参数。 + +### 挂起决定 + +- 当前不做批量清理。 +- 等并行开发稳定后重新运行严格检查,以最新结果为准。 +- 只删除最终版本中仍无调用的声明,不根据本次快照机械修改。 +- 对 `fleetId` 这类可能代表未完成业务逻辑的参数,先判断应该补齐功能还是删除。 + +## 复查清单 + +- [ ] 主页任务入口已经稳定。 +- [ ] 设置页面更新策略已经确定。 +- [ ] 当前一轮并行 Agent 修改已经结束。 +- [ ] 重新搜索 `executePlan` 的调用入口。 +- [ ] 区分旧后端源码更新、GUI 自动更新和环境依赖更新。 +- [ ] 重新运行正常构建和严格未使用检查。 +- [ ] 根据最终代码决定保留、接入或删除。 +- [ ] 更新本文档状态和最新检查结果。 + +## 不属于挂起项 + +- 系统 YAML 只读方案已被否决,不作为后续待办。 +- 后端日志中的 `7777` HTTP 调试请求已经删除。 +- 完整配置 YAML 已改为仅在调试模式下输出 `debug` 日志。 +- `.dbg`、Python 缓存和临时舰娘卡片预览页已经清理。 diff --git a/docs/reviews/2026-08-04-src-module-split-agent-runbook.md b/docs/reviews/2026-08-04-src-module-split-agent-runbook.md new file mode 100644 index 0000000..e3dad88 --- /dev/null +++ b/docs/reviews/2026-08-04-src-module-split-agent-runbook.md @@ -0,0 +1,494 @@ +# `src` TypeScript 模块拆分单 Agent 执行任务书 + +## 1. 文档用途 + +本文件把 [`src` TypeScript 模块拆分方案](./2026-08-04-src-module-split-plan.md) +转换为一个长期 Agent 可以分阶段执行的任务书。 + +两份文档的职责不同: + +- 原方案说明为什么拆、最终边界和现有模块的去向。 +- 本任务书规定单个 Agent 每个阶段做什么、允许修改什么、如何验证和何时停止。 + +本任务书不是一次性全目录搬迁,也不是多 Agent 并行队列。实施过程按 S0-S6 +串行推进,并在每个阶段验证行为和架构边界。 + +本任务的目标不是机械复制 `electron/` 的目录名称,而是让 `src/` 形成同样清晰的 +职责边界: + +```text +src/ +├─ adapter/ YAML、JSON、IPC、HTTP、WebSocket、Storage 边界 +├─ controller/ 页面和用例编排 +├─ model/ 领域模型、规则和唯一状态所有者 +├─ view/ DOM/ViewObject 渲染和用户意图回调 +├─ types/ 按领域和通信方向组织的类型 +└─ shared/ 无状态共享工具 +``` + +最终没有为了目录外观增加 `app/` 或 `domain/`:`AppController` 继续是 Renderer +组合根,领域状态继续位于 `model/`。Adapter 按完整边界收敛为 +`ApiAdapter.ts`、`IpcAdapter.ts`、`JsonAdapter.ts`、`StorageAdapter.ts` 和 +`YamlAdapter.ts`,避免微型文件。 + +## 2. 执行启用门槛 + +本轮实际执行记录: + +```text +BASE_SHA=7bd0c65 +WORK_BRANCH=ShiinaKuroko +CURRENT_STAGE=POST_S6_TYPES_CONSOLIDATION_COMPLETED +PATCH_LEVEL=L0 +``` + +维护者已明确要求以当前脏工作树为准并保留全部未提交改动,因此本轮没有创建 +独立 worktree,也没有执行 reset、checkout、stash 或覆盖用户文件。该授权只适用 +于本次连续实施;后续新一轮拆分仍应先记录基线并隔离工作树。 + +## 3. 单 Agent 通用执行协议 + +### 3.1 开始阶段 + +Agent 每次只执行一个阶段,例如 `S2`。阶段完成并经过维护者验收后,才可以开始 +下一个阶段。开始阶段前必须完整读取: + +1. `AGENTS.md` +2. `docs/engineering-standards.md` +3. `.editorconfig` +4. `.gitattributes` +5. `tsconfig.json` +6. `package.json` +7. `CONTRIBUTING.md` +8. 原拆分方案 +9. 本任务书 +10. 当前阶段卡指定的架构文档和现有测试 + +### 3.2 修改前报告 + +Agent 在写代码前必须报告: + +```text +阶段 ID: +基线 SHA: +当前分支/worktree: +行为目标: +非目标: +允许修改的主要文件: +状态所有者: +当前数据流: +预期数据流: +Patch 等级: +计划执行的验证: +``` + +没有完成该报告,不得开始写代码。阶段完成后必须暂停,不得自动进入下一阶段。 + +### 3.3 修改范围 + +- 只修改当前阶段卡的“主要范围”。 +- 可以修改直接 import、barrel export、对应测试和对应架构文档。 +- 需要进入未列出的业务模块时,立即停止并报告范围扩散;不得顺手修改其他阶段 + 的核心文件。 +- 不修改 IPC channel、API 字段、YAML/JSON 公共格式和用户目录。 +- 不修改 UI 样式,除非当前阶段明确允许。 +- 不进行无关格式化、依赖升级、资源更新或命名清理。 +- 不使用 `any`、类型断言、retry、sleep、fallback 或第二状态源绕过问题。 + +### 3.4 兼容迁移 + +拆文件时采用以下顺序: + +1. 增加目标模块和最小接口。 +2. 将现有逻辑原样迁入,先保持行为。 +3. 原公共入口暂时改为 facade 或 re-export。 +4. 修改调用方。 +5. 运行验证。 +6. 只有 S6 可以删除跨阶段兼容出口。 + +禁止在同一个任务中一边迁移结构,一边修改业务语义。 + +### 3.5 每个阶段的通用验证 + +所有代码阶段至少执行: + +```powershell +npm run build +git diff --check +git status --short +``` + +`npm run build` 会生成 CSS/构建输出。没有样式变化时,不得把生成差异作为任务 +成果提交。 + +阶段卡列出的专项测试必须全部执行。无法执行时,阶段不得标记完成,交付记录 +必须写明阻塞原因和未验证风险。 + +### 3.6 强制停止条件 + +出现任一情况,Agent 必须停止,不得继续补代码: + +1. 当前工作树包含无法确认归属的修改。 +2. 阶段需要修改另一个未解锁阶段的核心文件。 +3. 需要新增第二份可写状态、同步标志、延时或 fallback。 +4. 两次实现尝试仍未通过同一验证。 +5. 旧测试与架构文档对当前行为给出冲突结论。 +6. 无法证明 candidate-only、YAML 未知字段或任务生命周期保持不变。 +7. 需要改变 IPC、API 或持久化格式才能完成结构拆分。 + +## 4. 状态所有权不变量 + +所有任务都必须遵守: + +| 状态 | 唯一可写所有者 | 禁止行为 | +|---|---|---| +| 当前任务、运行状态 | `Scheduler` | 子策略保存镜像状态 | +| 就绪队列、延迟重试 | `TaskQueue` | Controller 保存第二份任务队列 | +| Cron 定时器、pending | `CronScheduler` | Store 决定触发行为 | +| 泡澡舰船集合 | `RepairManager` | View/Controller 维护影子集合 | +| 当前作战方案 | `PlanController` | 子 View 持有可独立修改副本 | +| 舰队编辑草稿 | 单个 `FleetDraft` | 多个子 View 各自保存草稿 | +| 决战舰队草稿 | 单个 `DecisiveFleetDraft` | 复用普通草稿后再加补偿字段 | + +以下外部语义不得改变: + +- 纯候选舰船槽位不得自动生成顶层 `name`。 +- `candidates` 中每项仍必须有 `name`。 +- YAML 未知字段、头部注释和旧字段迁移行为保持。 +- 队列优先级、延迟重试、后触发和停止条件时序保持。 +- IPC channel、参数、返回值和错误文本保持。 +- REST/WebSocket 路径、请求和回调顺序保持。 + +## 5. 状态所有权与目标边界 + +### 5.1 Renderer 目标数据流 + +```text +Adapter → Domain Model → Controller → ViewObject → View + ↑ │ + └ 用户意图 ───┘ +``` + +职责要求: + +- `adapter/` 只处理 YAML、JSON、IPC、HTTP、WebSocket 和 Storage 边界。 +- `model/` 只持有领域状态和纯业务规则,不访问 `window`、`document`、Electron 或 + Node 文件系统。 +- `controller/` 只编排用例和转换 ViewObject,不直接依赖 YAML/JSON parser。 +- `view/` 只渲染 DOM、读取用户输入并发出意图,不访问有状态 Model、全局 IPC 或 + Storage;允许使用类型、不可变目录和无状态领域函数。 +- `AppController` 是 Renderer 组合根,负责生命周期和跨域协调。 +- `types/` 区分领域类型、API DTO、IPC DTO 和 ViewObject。 +- `shared/` 只放无状态、无业务所有权的共享工具。 + +### 5.2 单一状态所有者 + +| 状态 | 唯一可写所有者 | 其他模块允许做什么 | +|---|---|---| +| 当前任务、运行状态 | `Scheduler` | 读取、发出用户意图 | +| 就绪队列、延迟重试 | `TaskQueue` | 读取、请求入队 | +| Cron timer、pending | `CronScheduler` | 读取、请求触发 | +| 泡澡舰船集合 | `RepairManager` | 读取快照、请求修理 | +| 当前作战方案 | `PlanController` | View 只能通过意图修改 | +| 普通舰队草稿 | `FleetDraft` | Controller 只协调 mutation | +| 决战舰队草稿 | `DecisiveFleetDraft` | Controller 只协调 mutation | + +Controller 不得保存 Draft、Scheduler 或 RepairManager 的镜像字段。Store、Policy、 +Factory 和 View 都不得成为第二个可写状态源。 + +## 6. 单 Agent 阶段顺序 + +严格串行执行以下阶段。每个阶段完成后暂停,维护者验收通过后才能继续: + +```text +S0 只读审计与行为基线 +→ S1 Types 拆分 +→ S2 Adapter 边界 +→ S3 Domain 拆分 +→ S4 App 与 Controller 收口 +→ S5 View 拆分 +→ S6 兼容层清理与文档同步 +``` + +不得把这些阶段拆给多个 Agent 并行执行。单个 Agent 必须持续掌握同一套状态所有权、 +依赖图和目标边界。 + +## 7. 阶段总表 + +| ID | 阶段 | 前置阶段 | 主要状态 | +|---|---|---|---| +| S0 | 只读审计与行为基线 | 基线 SHA | 已完成 | +| S1 | Types 拆分与兼容出口 | S0 | 已完成 | +| S2 | YAML/JSON/IPC/API/Storage Adapter | S1 | 已完成 | +| S3 | Fleet、Plan、Scheduler Domain | S2 | 已完成 | +| S4 | App 与 Controller 收口 | S3 | 已完成 | +| S5 | View 拆分与纯 View 边界 | S4 | 已完成 | +| S6 | 删除兼容层、死代码并同步文档 | S0-S5 | 已完成 | + +## 8. 阶段卡 + +### S0 只读审计与行为基线 + +**目标**:不改变业务行为,固定可复现基线。 + +**允许范围**:`scripts/`、`docs/reviews/`,以及为基线测试新增的最小 fixture。 + +**必须完成**: + +- 记录 `BASE_SHA`、Node/npm 版本、依赖状态和工作树状态。 +- 生成 `src` import 依赖图和直接越层访问清单。 +- 覆盖 YAML round-trip、未知字段、头部注释、candidate-only、任务队列、Cron、 + Repair、TaskGroup/Template 迁移和 API/IPC 契约。 +- 记录每个测试的通过/失败,不在本阶段修业务。 + +**验收**: + +```powershell +npm run build +npm run test:legacy-config-upgrade +npm run test:legacy-plan +npm run test:task-group-migration +npm run test:api-contract +npm run test:settings +npm run test:main-services +npm run test:main-ipc +git diff --check +``` + +**完成后必须暂停。** 未固定基线不得进入 S1。 + +### S1 Types 拆分与兼容出口 + +**范围**: + +```text +src/types/api.ts +src/types/ipc.ts +src/types/model.ts +src/types/view.ts +src/types/scheduler.ts +``` + +**要求**: + +- API DTO、IPC DTO、领域类型、ViewObject 和调度类型各自保持完整文件。 +- 不保留只做 re-export 的 Types facade 或二级子目录。 +- 不改变类型语义,不修改运行时行为。 +- 不批量迁移所有 import,只需证明新出口可用。 +- 不把默认值、迁移逻辑和 View 逻辑放入 types。 + +**验收**:`npm run build`、`npm run test:api-contract`。 + +### S2 Adapter 边界 + +**范围**: + +```text +src/adapter/ApiAdapter.ts +src/adapter/IpcAdapter.ts +src/adapter/JsonAdapter.ts +src/adapter/StorageAdapter.ts +src/adapter/YamlAdapter.ts +src/model/PlanModel.ts +src/model/ConfigModel.ts +src/model/TaskGroupModel.ts +src/model/TemplateModel.ts +src/model/MapDataLoader.ts +src/model/ApiClient.ts +``` + +**要求**: + +- YAML/JSON 解析、迁移和序列化各只有一个实现位置。 +- Repository/Store 只处理边界,不决定业务行为。 +- `ApiClient` 保留业务 facade,HTTP/WS 传输实现移入 adapter。 +- 保持未知字段、注释、旧格式、storage key、IPC channel、API path、请求体和回调 + 时序。 +- 不增加通用文件 IPC、备用 endpoint 或第二份持久化状态。 + +**验收**: + +```powershell +npm run test:legacy-config-upgrade +npm run test:legacy-plan +npm run test:task-group-migration +npm run test:api-contract +npm run test:main-ipc +``` + +### S3 Domain 拆分 + +**范围**: + +```text +src/model/fleet/ +src/model/scheduler/ +src/data/shipData.ts +``` + +**Fleet 要求**: + +- 集中 `ShipCatalog`、`ShipNameNormalizer`、`ShipMatcher`、`FleetRuleMapper`、 + `FleetDraft`、`DecisiveFleetDraft`。 +- candidate-only 不得自动生成顶层 `name`。 +- candidates 顺序和每项独立规则必须保留。 +- `ship_type`、`search_name`、等级规则不能在 View 中重复解释。 +- `shipData.ts` 在迁移期只作为 facade。 + +**Scheduler 要求**: + +- `Scheduler` 唯一持有当前任务和运行状态。 +- `TaskQueue` 唯一持有 ready/delayed 队列。 +- `CronScheduler` 唯一持有 timer/pending。 +- `RepairManager` 唯一持有 bathingShips。 +- 提取的 Policy/Factory 只能是纯函数或无状态对象。 + +**必须新增或补齐测试**:优先级、延迟、重试、后触发、Cron 恢复、Repair 恢复、舰队 +预设切换、candidate-only、舰种和等级规则。 + +### S4 App 与 Controller 收口 + +**范围**:`src/controller/app/`、`src/controller/startup/`、 +`src/controller/plan/`、`src/controller/taskGroup/`、`src/controller/template/`。 + +**要求**: + +- `AppController` 是唯一 Renderer 组合根。 +- `StartupController` 保留启动顺序和销毁顺序。 +- 子 Controller 只接收最小 Host/Port,不接收整个 AppController。 +- Controller 不直接依赖 YAML/JSON parser,不直接持久化业务状态。 +- Plan、TaskGroup、Template 的请求构造分别收口到明确的 mapper/factory。 +- 保持任务顺序、次数、优先级、重试、停止条件和后端连接时序。 + +**验收**:`npm run test:settings`、`npm run test:main-services`、 +`npm run test:main-ipc`、`npm run test:python-environment`、相关 API/迁移测试。 + +### S5 View 拆分与纯 View 边界 + +**范围**:`src/view/` 以及与 View 直接相关的 Controller/ViewObject mapper。 + +**要求**: + +- View 只渲染 DOM、读取输入和发出用户意图。 +- View 不访问有状态 Model、全局 IPC、localStorage、js-yaml 或持久化。 +- View 可以使用类型、不可变目录和无状态领域函数,但不能取得业务状态所有权。 +- View 不保存可独立修改的业务副本。 +- `FleetPlannerView` 必须先建立唯一 `FleetDraft`,再拆编辑、规则、图鉴、选择和管理子 View。 +- `DecisivePlanView` 使用独立 `DecisiveFleetDraft`,不得复用普通草稿后叠加补偿字段。 +- 保持 candidate-only、舰队规则、保存覆盖、导入导出和预览行为。 + +**验收**:build、API/迁移/服务测试,以及固定步骤的手工验证记录。 + +### S6 兼容层清理与文档同步 + +**前置**:S0-S5 全部完成并通过验收。 + +**要求**: + +- 先用 `rg`、bundle 检查、测试和入口检查证明旧 facade 无内部/外部依赖。 +- 再删除 types facade、`shipData` facade、旧 View re-export、无引用 helper 和 barrel。 +- 不因为目标目录存在就删除仍可能是公共入口的 facade。 +- 同步 `docs/architecture/`、本方案和本任务书,使文档与实现一致。 + +**最终静态检查**: + +```powershell +rg -n "window\.electronBridge|\(window as any\)" src/model src/view +rg -n "localStorage" src/model src/view +rg -n "js-yaml|yaml\.load|yaml\.dump" src/controller src/model src/view +rg -n "\bas any\b" src/controller src/model src/view +rg -n "shipData" src scripts electron +rg -n "types/(api|ipc|model|view)/" src scripts electron +rg -n "controller/shared/ControllerHost|controller/(app|plan|startup|taskGroup|template|shared)/index" src scripts electron +``` + +最终运行全部基线测试和 `git diff --check`。`as any` 只能在明确注释的第三方边界存在, +业务 Controller/Model/View 中不得存在。 + +**实际清理结果**: + +- 原 4 个 Types 根 facade 已由真实定义替代,旧 Types 子目录已删除。 +- 删除 `data/shipData.ts` 和 6 个无引用 Controller barrel。 +- 删除无调用方的 `controller/shared/ControllerHost.ts` 和 + `controller/taskGroup/importExport.ts`。 +- 保留仍有测试或业务调用方的 `model/fleet/index.ts`、 + `model/scheduler/index.ts`、`queueLoader.ts` 和 `managedPlanReader.ts`。 +- 更新 `docs/architecture/`、拆分方案和本任务书。 + +**最终验证结果**: + +- 构建、舰种契约、Fleet/Scheduler Domain、旧配置/旧方案/任务组迁移和 API 契约通过。 +- 设置持久化、主进程服务、主进程 IPC、Python 环境、舰船库更新器和活动资源测试通过。 +- 7 项静态边界/旧入口检查无匹配,删除路径均不存在。 +- `git diff --check` 通过,仅报告资源 JSON 的 CRLF/LF 转换提示。 + +桌面 Electron 启动、舰队拖拽、模拟器连接和实际任务执行未在本轮工具环境中手工 +验证。代码仍处于未提交工作树,没有生成完成 SHA。 + +## 9. 阶段交付格式 + +单 Agent 完成每个阶段后必须按以下格式交付,并在交付后暂停: + +```text +阶段 ID: +基线 SHA: +完成 SHA: + +行为目标: +实际修改: +明确未修改: + +修改文件: +新增文件: +删除文件: + +状态所有者变化: +外部契约变化:无 / 具体说明 +兼容层:新增 / 保留 / 删除 + +执行命令及结果: +1. +2. + +手工验证: +未验证路径: +失败尝试次数: +当前 Patch 等级: +回滚方式: + +git status --short: +下一阶段建议: +``` + +交付中只说“构建通过”不算完成。必须列出专项测试和业务不变量的验证证据。 + +## 10. 可直接派发的提示词 + +维护者可以复制以下内容,替换阶段 ID 和 SHA。不要把多个阶段一次性派给 Agent: + +```text +请执行 C:\ShiinaKuroko\04.Code\AutoWSGR\AutoWSGR-GUI\docs\reviews\2026-08-04-src-module-split-agent-runbook.md 中的阶段 。 + +基线 SHA: +前置阶段完成 SHA: + +你只能执行该阶段,不得开始下一阶段。修改前先按任务书提交预检报告。 +使用独立分支/worktree,保护现有未提交修改。完成后执行通用验证和阶段卡专项验证, +并按“阶段交付格式”报告,然后暂停等待维护者验收。出现强制停止条件时停止写代码 +并报告,不得自行扩大范围。 +``` + +## 11. 阶段验收 + +维护者接受每个阶段前必须确认: + +- [ ] Agent 使用了正确的基线。 +- [ ] 只完成一个阶段 ID。 +- [ ] 没有混入共享工作树的旧改动。 +- [ ] 状态所有者没有复制。 +- [ ] 外部契约没有变化,或已获得单独批准。 +- [ ] 通用验证和专项验证均有结果。 +- [ ] 失败尝试和 Patch 等级已披露。 +- [ ] 文档与实现没有互相矛盾。 +- [ ] 回滚该任务不会要求同时回滚未关联功能。 +- [ ] 下一阶段基于本阶段合并后的新 SHA。 diff --git a/docs/reviews/2026-08-04-src-module-split-plan.md b/docs/reviews/2026-08-04-src-module-split-plan.md new file mode 100644 index 0000000..3c43386 --- /dev/null +++ b/docs/reviews/2026-08-04-src-module-split-plan.md @@ -0,0 +1,332 @@ +# `src` TypeScript 模块拆分方案 + +## 1. 范围和结论 + +本方案最初覆盖 `src/` 下 77 个 TypeScript 文件。S6 完成后的粒度复核将 +`types` 合并为完整领域文件;功能边界复核后当前 `src/` 共有 97 个 TypeScript 文件: + +| 目录 | 文件数 | +|---|---:| +| `controller` | 33 | +| `view` | 28 | +| `model` | 22 | +| `types` | 5 | +| `adapter` | 6 | +| 其他 `src` 目录 | 3 | + +拆分不以行数为唯一标准。只有出现以下情况才拆: + +1. 一个文件包含多个独立变化原因。 +2. View、Controller、Model 之间发生越层调用。 +3. YAML、JSON、IPC、HTTP、WebSocket 或 `localStorage` 没有明确适配边界。 +4. 多个界面重复实现同一套舰船筛选、拖拽或规则转换。 +5. 可以提取纯策略,同时不产生第二份可写状态。 + +本次只调整 Renderer 的模块边界,不改变 IPC channel、HTTP API、YAML 格式、 +任务队列行为、candidate-only 语义和用户数据目录。 + +## 2. 目标边界 + +```text +View + 只持有 DOM 引用、渲染 ViewObject、发出用户操作回调 + 不直接调用全局 electronBridge、localStorage、js-yaml 或有状态 Model + 可以读取类型、不可变目录和无状态领域函数 + +Controller + 持有页面/用例状态,协调 View、Model 和 Repository + 不直接依赖 YAML/JSON parser,不使用 any 绕过 Host/Port 契约 + +Model + 持有领域状态和纯业务规则 + 不直接访问 window、document、electronBridge 或 localStorage + +Adapter + 负责 IPC、REST、WebSocket、YAML、JSON 和浏览器存储 + 对 Controller/Model 暴露最小接口 +``` + +状态所有权保持不变: + +| 状态 | 唯一所有者 | +|---|---| +| 当前任务、调度状态 | `Scheduler` | +| 就绪队列、延迟重试队列 | `TaskQueue` | +| Cron 定时器和 pending 标记 | `CronScheduler` | +| 泡澡舰船集合 | `RepairManager` | +| 当前作战方案 | `PlanController` | +| 舰队编辑草稿 | `FleetPlannerController` + 单个 `FleetDraft` | +| 决战舰队草稿 | `DecisivePlanController` + 单个 `DecisiveFleetDraft` | + +## 3. 最终目录 + +实现采用完整边界和完整业务功能作为文件粒度,没有继续执行早期草案中的微型 +Repository、Codec 或 helper 拆分。下列目录是 S6 收口后的实际结构: + +```text +src/ +├─ adapter/ +│ ├─ ApiAdapter.ts +│ ├─ IpcAdapter.ts +│ ├─ JsonAdapter.ts +│ ├─ StorageAdapter.ts +│ ├─ YamlAdapter.ts +│ └─ index.ts +├─ controller/ +│ ├─ app/ +│ │ ├─ NavigationController.ts +│ │ ├─ OperationsController.ts +│ │ ├─ AppController.ts +│ │ ├─ ConfigController.ts +│ │ ├─ SchedulerBinder.ts +│ │ └─ SettingsController.ts +│ ├─ plan/ +│ │ ├─ BattlePlanLoaderController.ts +│ │ ├─ DecisivePlanController.ts +│ │ ├─ FleetPlannerController.ts +│ │ └─ PlanController.ts +│ ├─ startup/ +│ ├─ taskGroup/ +│ └─ template/ +├─ model/ +│ ├─ fleet/ +│ │ ├─ DecisiveFleetDraft.ts +│ │ ├─ FleetDraft.ts +│ │ ├─ FleetRuleMapper.ts +│ │ ├─ ShipCatalog.ts +│ │ ├─ ShipMatcher.ts +│ │ ├─ ShipNameNormalizer.ts +│ │ └─ index.ts +│ └─ scheduler/ +│ ├─ SchedulerRepairPolicy.ts +│ ├─ SchedulerTaskPolicy.ts +│ └─ index.ts +├─ view/ +│ ├─ plan/ +│ │ ├─ FleetPlannerView.ts +│ │ ├─ FleetEditorView.ts +│ │ ├─ FleetRuleView.ts +│ │ ├─ FleetGalleryView.ts +│ │ ├─ PlanManagementView.ts +│ │ └─ TeamPlanLoaderView.ts +│ ├─ config/ +│ ├─ main/ +│ ├─ setup/ +│ ├─ shared/ +│ ├─ taskGroup/ +│ └─ template/ +└─ types/ + ├─ api.ts + ├─ ipc.ts + ├─ model.ts + ├─ view.ts + └─ scheduler.ts +``` + +无引用的 Controller barrel 已删除。`model/fleet/index.ts` 和 +`model/scheduler/index.ts` 仍有实际调用方,因此继续作为领域公共入口。 + +## 4. Controller 逐文件映射 + +| 现有文件 | 处理 | 目标 | +|---|---|---| +| `controller/app/AppController.ts` | 收口完成 | 保留唯一组合根;设置页交互进入 `SettingsController`,心跳由 `StartupController` 持有 | +| `controller/app/ConfigController.ts` | 收口完成 | 保留完整配置用例协调,通过公开方法更新最小依赖,不再绕过私有 Host | +| `controller/app/SchedulerBinder.ts` | 保留并收口 | 继续统一绑定 Scheduler/CronScheduler 回调,不复制调度状态 | +| `controller/app/SettingsController.ts` | 新增并收口 | 集中设置页环境检测、ADB、舰船库、更新检查和主题交互 | +| `controller/app/constants.ts` | 保留并清理 | 保留优先级和状态文案;删除无引用的 `resolveRepairModeLabel()` | +| `controller/app/index.ts` | 已删除 | 无代码、脚本或打包入口引用 | +| `controller/app/rendering.ts` | 保留 | 继续作为纯 ViewObject 构造模块 | +| `controller/app/theme.ts` | 保留 | 完整管理 Renderer 主题偏好和 DOM 主题应用 | +| `controller/plan/BattlePlanLoaderController.ts` | 新增并收口 | 独立持有受管方案选择器状态并返回最终选择结果 | +| `controller/plan/PlanController.ts` | 收口完成 | 保留当前方案编辑、地图、保存和执行;方案选择委托给 Loader | +| `controller/plan/index.ts` | 已删除 | 无代码、脚本或打包入口引用 | +| `controller/plan/nodeEditor.ts` | 保留 | 节点编辑用例集中,无需再拆 | +| `controller/plan/presetFlow.ts` | 保留并收口 | 集中预设导入、展示和任务请求构造 | +| `controller/plan/rendering.ts` | 保留 | 继续作为纯 ViewObject mapper | +| `controller/plan/selectedNodes.ts` | 保留 | 单一纯规则 | +| `controller/shared/ControllerHost.ts` | 已删除 | 各控制器使用自己的最小 Host/Port | +| `controller/shared/DialogHelper.ts` | 保留 | 集中的对话框适配层 | +| `controller/shared/index.ts` | 已删除 | 无调用方,不再保留无意义 barrel | +| `controller/startup/StartupController.ts` | 保留并清理 | 保留环境检查、更新、后端连接和销毁顺序 | +| `controller/startup/connection.ts` | 保留 | 后端连接启动流程集中 | +| `controller/startup/envAndUpdates.ts` | 保留 | 环境准备和启动更新仍构成一个完整启动业务边界 | +| `controller/startup/index.ts` | 已删除 | 无代码、脚本或打包入口引用 | +| `controller/taskGroup/TaskListLoaderController.ts` | 保留并收口 | 继续负责完整的任务列表加载用例 | +| `controller/taskGroup/TaskGroupController.ts` | 保留并瘦身 | 继续协调任务组;固定 DOM 事件迁到 View 回调 | +| `controller/taskGroup/addItems.ts` | 保留并收口 | 保留添加条目用例;删除无引用的 `addFileToGroup()`,文件/YAML 处理走 Adapter | +| `controller/taskGroup/contextMenu.ts` | 保留并收口 | 集中上下文菜单和任务编辑意图,不再继续拆成微型文件 | +| `controller/taskGroup/importExport.ts` | 已删除 | 两个导出函数均无调用方,且没有对应 UI 入口 | +| `controller/taskGroup/index.ts` | 已删除 | 无代码、脚本或打包入口引用 | +| `controller/taskGroup/managedPlanReader.ts` | 保留 | 被元数据和队列加载流程共同调用 | +| `controller/taskGroup/metaLoader.ts` | 保留并收口 | 保留批量元数据编排;YAML 解析统一走 `yamlCodec` | +| `controller/taskGroup/queueLoader.ts` | 保留并收口 | 集中 managed/group/template 三种来源的入队规则 | +| `controller/template/TemplateController.ts` | 收口完成 | 保留模板页和向导协调,使用稳定的强类型状态引用 | +| `controller/template/crud.ts` | 保留并收口 | 保留 CRUD 用例,JSON 解析统一走 `jsonCodec` | +| `controller/template/index.ts` | 已删除 | 无代码、脚本或打包入口引用 | +| `controller/template/selectors.ts` | 保留并收口 | 保留选择用例,使用强类型和 `yamlCodec`,移除 `any` | +| `controller/template/useTemplate.ts` | 保留 | 用例单一 | +| `controller/template/wizard.ts` | 保留并改契约 | 保留步骤规则;用明确状态接口替代 `as any` ref-wrapper | + +## 5. View 逐文件映射 + +| 现有文件 | 处理 | 目标 | +|---|---|---| +| `view/config/ConfigView.ts` | 保留并收口 | 完整设置页纯渲染组件;不解析 YAML,不访问 IPC | +| `view/main/FleetPreviewView.ts` | 保留并注入 | 由 Controller 传入舰船库 manifest,移除直接 IPC | +| `view/main/LogView.ts` | 保留 | 日志渲染职责集中 | +| `view/main/MainView.ts` | 保留 | 继续作为主页面 facade | +| `view/main/StatusBar.ts` | 保留 | 状态栏职责集中 | +| `view/main/TaskQueueView.ts` | 保留 | 队列渲染和拖拽回调集中 | +| `view/plan/BattlePlanLoaderView.ts` | 新增并收口 | 集中受管方案选择弹窗、搜索筛选、列表和舰队预览 DOM | +| `view/plan/DecisivePlanView.ts` | 保留并收口 | 通过 `DecisivePlanViewHost` 发出意图,草稿由 `DecisivePlanController` 独立持有 | +| `view/plan/FleetEditDialog.ts` | 保留并收口 | 对话框保留,复用只读舰船目录和唯一名称规范化规则 | +| `view/plan/FleetPlannerView.ts` | 拆分完成 | facade + `FleetEditorView`、`FleetRuleView`、`FleetGalleryView`、`PlanManagementView`、`TeamPlanLoaderView` | +| `view/plan/FleetPresetView.ts` | 保留并收口 | 通过最小 Host 获取计划和舰船库数据,不直接访问全局 IPC | +| `view/plan/MapView.ts` | 保留 | 地图渲染职责集中 | +| `view/plan/NodeEditorView.ts` | 保留 | 节点编辑表单职责集中 | +| `view/plan/PlanPreviewView.ts` | 保留 | 继续组合地图、节点和方案表单 | +| `view/plan/ShipArtwork.ts` | 保留 | 集中计划页舰船图片创建和 fallback | +| `view/plan/TeamPlanListUi.ts` | 保留 | 集中编队计划过滤、排序和卡片渲染 | +| `view/setup/SetupWizardView.ts` | 保留 | 向导渲染职责集中 | +| `view/shared/ShipAutocomplete.ts` | 保留 | 通用自动补全组件 | +| `view/shared/scrollPosition.ts` | 保留 | 通用纯 DOM 工具 | +| `view/taskGroup/TaskGroupView.ts` | 保留 | 任务组面板渲染职责集中 | +| `view/template/SelectorDialog.ts` | 保留 | 通用选择弹窗 | +| `view/template/TemplateLibraryView.ts` | 保留 | 模板列表渲染职责集中 | +| `view/template/TemplateWizardView.ts` | 保留并收口 | 继续负责向导 DOM;固定事件通过回调交给 Controller | + +## 6. Model、Types、Data、Utils 逐文件映射 + +| 现有文件 | 处理 | 目标 | +|---|---|---| +| `model/ApiClient.ts` | 收口完成 | 保留业务 API facade;REST 和 WebSocket 传输委托 `ApiAdapter` | +| `model/ConfigModel.ts` | 收口完成 | 保留配置状态、默认值和迁移;YAML 解析统一走 `yamlCodec` | +| `model/MapDataLoader.ts` | 收口完成 | 保留缓存和地图查询;文件读取和 JSON 解析委托 Adapter | +| `model/PlanModel.ts` | 收口完成 | 保留方案状态、未知字段合并和序列化规则;底层 YAML 解析统一走 `yamlCodec` | +| `model/TaskGroupModel.ts` | 收口完成 | 保留任务组权威状态/CRUD;JSON 和文件持久化委托 Adapter | +| `model/TemplateModel.ts` | 收口完成 | 保留模板 CRUD 和校验;JSON 和文件持久化委托 Adapter | +| `model/scheduler/CronScheduler.ts` | 策略/存储提取 | 保留定时器和 pending 状态;时间规则与 `localStorage` 分离 | +| `model/scheduler/ExpeditionTimer.ts` | 保留 | 单一定时职责 | +| `model/scheduler/RepairManager.ts` | 策略/存储提取 | 保留 `bathingShips`;阈值判断与持久化分离 | +| `model/scheduler/Scheduler.ts` | 提取纯策略 | 保留 `currentTask/status`、消费和 API 回调;纯规则进入 `SchedulerTaskPolicy` 和 `SchedulerRepairPolicy` | +| `model/scheduler/StopConditionChecker.ts` | 保留 | 停止条件职责集中 | +| `model/scheduler/TaskQueue.ts` | 保留并收口 | 继续唯一持有就绪和延迟队列 | +| `model/scheduler/index.ts` | 保留 | 继续作为调度系统公共出口 | +| `types/api.ts` | 真实定义 | 后端请求、响应、任务 DTO 和 WebSocket 事件 | +| `types/ipc.ts` | 真实定义 | IPC DTO、`ElectronBridge` 和全局 Window 声明 | +| `types/model.ts` | 真实定义 | 配置、方案、模板、舰队和修理领域类型 | +| `types/scheduler.ts` | 保留 | 调度类型内聚且规模合理 | +| `types/view.ts` | 真实定义 | 页面 ViewObject、表单值和展示状态 | +| `data/shipData.ts` | 已删除 | 舰船目录、名称规范化、匹配和规则映射已归入 `model/fleet/` | +| `utils/Logger.ts` | 保留 | 日志格式、级别和输出职责集中 | + +## 7. 重点模块的实际拆法 + +### 7.1 `FleetPlannerView.ts` + +最终按完整舰队业务功能拆分,而不是按单个 helper 拆分: + +1. `FleetPlannerController` 持有唯一 `FleetDraft`。 +2. `FleetGalleryView` 负责图鉴筛选、排序、加载缓存等纯展示状态。 +3. `FleetEditorView` 负责舰队槽位编辑和拖拽意图。 +4. `FleetRuleView` 负责主选、备选、舰种和等级规则输入。 +5. `TeamPlanLoaderView` 负责编队计划选择。 +6. `PlanManagementView` 负责计划列表和管理意图。 +7. `FleetPlannerView` 只组合上述完整业务 View 并转发回调。 + +`FleetDraft` 必须保留 candidate-only:没有明确 `name` 的槽位不能把第一个 +candidate 提升为主选。 + +### 7.2 `Scheduler.ts` + +不把执行流程拆成多个可写对象。只提取纯策略: + +- `SchedulerTaskPolicy`:任务完成、重试、后触发和队列请求规则。 +- `SchedulerRepairPolicy`:修理和替换相关的无状态判断。 + +`consumeNext()`、重试时序、`currentTask` 和状态切换继续留在 `Scheduler`。 + +### 7.3 `PlanModel.ts` 和 `ConfigModel.ts` + +Model 不再直接依赖 `js-yaml`,公共方法继续保留: + +```typescript +PlanModel.fromYaml(content) +plan.toYaml() +config.loadFromYaml(content) +config.toYaml() +``` + +这些方法统一委托 `yamlCodec`,保持 YAML 未知字段、头部注释和旧字段迁移行为。 + +## 8. 实施状态 + +| 阶段 | 状态 | 结果 | +|---|---|---| +| S0 行为基线 | 已完成 | 建立迁移、API、Fleet 和 Scheduler 特征测试 | +| S1 Types | 已完成并复核粒度 | 四个领域和 Scheduler 各一个真实定义文件,不保留子目录 barrel | +| S2 Adapter | 已完成 | 形成 5 个完整边界 Adapter 和统一入口 | +| S3 Domain | 已完成 | Fleet/Scheduler 规则收口,状态所有权未复制 | +| S4 Controller | 已完成 | `AppController` 保持唯一组合根,子 Controller 使用最小 Host | +| S5 View | 已完成 | Fleet View 按完整业务功能拆分,业务草稿移出 View | +| S6 清理 | 已完成 | facade、无引用 barrel 和死代码已删除,文档与最终回归通过 | + +## 9. 每阶段验收 + +每个提交至少执行: + +```powershell +npm run build +npm run test:api-contract +git diff --check +``` + +按改动范围追加: + +```powershell +npm run test:task-group-migration +npm run test:legacy-plan +npm run test:settings +npm run test:main-services +npm run test:main-ipc +``` + +最终静态边界检查: + +```powershell +rg -n "window\.electronBridge|\(window as any\)" src/model src/view +rg -n "localStorage" src/model src/view +rg -n "js-yaml|yaml\.load|yaml\.dump" src/controller src/model src/view +rg -n "\bas any\b" src/controller src/model src/view +``` + +以上 4 条均应无结果。 + +S6 最终验证已通过: + +- `npm run build`、舰种契约同步检查。 +- Fleet、Scheduler、旧配置、旧方案、任务组迁移和 API 契约测试。 +- 设置持久化、主进程服务、主进程 IPC 和 Python 环境测试。 +- 舰船库更新器、活动资源测试、静态边界检查和 `git diff --check`。 + +桌面 Electron 启动、舰队拖拽、模拟器连接和实际任务执行没有在本轮工具环境中 +进行手工验收,仍需在合并前按下列清单验证。 + +最终手工回归: + +1. 启动、ADB 连接、心跳和后端停止。 +2. 配置加载/保存、外部 Python、CUDA/OCR、更新检查。 +3. 作战方案加载、修改、保存、执行和 candidate-only 请求。 +4. 编队创建、备选拖拽、覆盖确认、计划管理和批量导出。 +5. 任务组保存、加载、排序、单项/整组入队和旧数据迁移。 +6. Cron、重试、停止条件、泡澡轮换和远征任务。 +7. 模板创建、编辑、导入和加入任务组。 + +## 10. 明确不做 + +- 不在拆分提交中修改 IPC channel、接口字段或用户文件格式。 +- 不同时重写 UI 样式。 +- 不把 `Scheduler`、`CronScheduler`、`RepairManager` 的状态复制到新对象。 +- 不以“文件超过多少行”为理由继续细拆单一职责文件。 +- 不在当前脏工作区直接进行全目录搬迁。 diff --git a/docs/user-guide.md b/docs/user-guide.md index ef73332..3a12659 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -184,9 +184,13 @@ DD >= 1 and CL >= 2, 4 - 自动战役(战役类型、次数) - 自动演习(演习舰队) - 自动常规出击 -- 自动决战(保留票数、模板) +- 自动决战(决战模板) - 每日自动刷战利品(地图方案、停止数量) +自动决战每天最多执行一轮。模板中填写的舰队优先;模板舰队留空时,使用 +“决战计划”页当前保存的舰队和快修设置。GUI 不读取剩余票数,也不会根据 +旧版“票数保留”字段推算执行轮数。任务一旦实际结束,当天不会再次自动触发。 + ## 7. 常见建议 - 调整后端端口或 Python 路径后,建议重启 GUI 再执行任务。 diff --git a/electron/backend.ts b/electron/backend.ts deleted file mode 100644 index 91766dc..0000000 --- a/electron/backend.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * 后端服务管理(启动 / 停止 / setup.bat)。 - * 从 main.ts 提取。 - */ -import * as path from 'path'; -import * as fs from 'fs'; -import { execSync, spawn, ChildProcess } from 'child_process'; -import type { BrowserWindow } from 'electron'; -import { ensurePthFile, ensureSslCertForPython, findPython, localSitePackages } from './pythonEnv'; - -// ════════════════════════════════════════ -// Context — 由 main.ts 在启动时注入 -// ════════════════════════════════════════ - -export interface BackendContext { - appRoot: () => string; - resourceRoot: () => string; - BACKEND_PORT: number; - getMainWindow: () => BrowserWindow | null; -} - -let ctx: BackendContext; - -export function initBackend(context: BackendContext): void { - ctx = context; -} - -// ════════════════════════════════════════ -// 内部状态 -// ════════════════════════════════════════ - -let backendProcess: ChildProcess | null = null; - -export function getBackendProcess(): ChildProcess | null { - return backendProcess; -} - -type OcrGpuMode = 'auto' | 'cpu' | 'cuda'; - -function readGuiSettings(): Record { - try { - const settingsPath = path.join(ctx.appRoot(), 'gui_settings.json'); - if (!fs.existsSync(settingsPath)) return {}; - return JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as Record; - } catch { - return {}; - } -} - -function readBackendRepoOverrideFromSettings(): string | null { - const raw = readGuiSettings(); - const value = raw.backend_repo_path; - if (typeof value !== 'string') return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function readOcrGpuModeFromSettings(): OcrGpuMode { - const raw = readGuiSettings(); - const value = raw.ocr_gpu_mode; - if (value === 'cpu' || value === 'cuda') return value; - return 'auto'; -} - -function normalizeCudaRoot(candidate: string): string { - const resolved = path.resolve(candidate.trim()); - if (isCudaRuntimeDirectory(resolved)) return resolved; - return path.basename(resolved).toLowerCase() === 'bin' ? path.dirname(resolved) : resolved; -} - -function isCudaRuntimeDirectory(candidate: string): boolean { - try { - const names = fs.readdirSync(candidate); - return names.some(name => /^cudart64.*\.dll$/i.test(name)) - && names.some(name => /^cublas64.*\.dll$/i.test(name)); - } catch { - return false; - } -} - -function readCudaPathFromSettings(): string | null { - const raw = readGuiSettings(); - const value = raw.cuda_path; - if (typeof value !== 'string' || !value.trim()) return null; - const cudaRoot = normalizeCudaRoot(value); - const binDir = path.join(cudaRoot, 'bin'); - const runtimeDir = isCudaRuntimeDirectory(cudaRoot) - ? cudaRoot - : isCudaRuntimeDirectory(binDir) - ? binDir - : null; - if (!fs.existsSync(path.join(binDir, 'nvcc.exe')) && !runtimeDir) { - console.warn(`[Backend] 忽略 cuda_path(未找到 Toolkit 或 CUDA Runtime DLL): ${cudaRoot}`); - return null; - } - return fs.existsSync(path.join(binDir, 'nvcc.exe')) ? cudaRoot : runtimeDir; -} - -/** 构造后端 CUDA 环境;手动路径优先,留空保留系统自动检测。 */ -export function buildCudaEnvironment( - baseEnv: NodeJS.ProcessEnv, - configuredCudaRoot: string | null, -): NodeJS.ProcessEnv { - if (!configuredCudaRoot) return { ...baseEnv }; - const cudaRoot = normalizeCudaRoot(configuredCudaRoot); - const isToolkit = fs.existsSync(path.join(cudaRoot, 'bin', 'nvcc.exe')); - const cudaBin = isToolkit ? path.join(cudaRoot, 'bin') : cudaRoot; - const existingPath = baseEnv.PATH || baseEnv.Path || ''; - const pathEntries = existingPath.split(path.delimiter).filter(Boolean); - const withoutDuplicate = pathEntries.filter(entry => path.resolve(entry).toLowerCase() !== path.resolve(cudaBin).toLowerCase()); - const env: NodeJS.ProcessEnv = { ...baseEnv }; - for (const key of Object.keys(env)) { - if (key.toLowerCase() === 'path') delete env[key]; - } - if (isToolkit) { - env.CUDA_PATH = cudaRoot; - env.CUDA_HOME = cudaRoot; - } - env.PATH = [cudaBin, ...withoutDuplicate].join(path.delimiter); - - let version: string | null = null; - try { - const versionJson = path.join(cudaRoot, 'version.json'); - if (fs.existsSync(versionJson)) { - const raw = JSON.parse(fs.readFileSync(versionJson, 'utf-8').replace(/^\uFEFF/, '')) as Record; - version = raw.cuda?.version ?? raw.cuda_cudart?.version ?? null; - } - } catch { /* use directory name fallback */ } - version ??= path.basename(cudaRoot).match(/v(\d+(?:\.\d+)?)/i)?.[1] ?? null; - const versionMatch = version?.match(/^(\d+)\.(\d+)/); - if (isToolkit && versionMatch) { - env[`CUDA_PATH_V${versionMatch[1]}_${versionMatch[2]}`] = cudaRoot; - } - return env; -} - -function readSaveBackendScreenshotsFromSettings(): boolean { - const raw = readGuiSettings(); - return raw.save_backend_screenshots === true; -} - -function resolveLocalBackendRepoPath(): string | null { - const fromEnv = process.env.AUTOWSGR_BACKEND_REPO?.trim(); - const fromSettings = readBackendRepoOverrideFromSettings(); - const candidate = fromEnv || fromSettings; - if (!candidate) return null; - - let resolved = path.resolve(candidate); - if (!fs.existsSync(resolved)) { - console.warn(`[Backend] 忽略 backend_repo_path(路径不存在): ${resolved}`); - return null; - } - - // 允许直接填写包目录 .../autowsgr - const looksLikePkgDir = fs.existsSync(path.join(resolved, '__init__.py')) && fs.existsSync(path.join(resolved, 'server', 'main.py')); - if (looksLikePkgDir && path.basename(resolved).toLowerCase() === 'autowsgr') { - resolved = path.dirname(resolved); - } - - if (!fs.existsSync(path.join(resolved, 'autowsgr', 'server', 'main.py'))) { - console.warn(`[Backend] 忽略 backend_repo_path(未找到 autowsgr/server/main.py): ${resolved}`); - return null; - } - - return resolved; -} - -// ════════════════════════════════════════ -// 后端服务 -// ════════════════════════════════════════ - -/** 运行 setup.bat 安装环境 */ -export function runSetupScript(): Promise<{ success: boolean; output: string }> { - return new Promise((resolve) => { - // 打包模式下 setup.bat 在 extraResources 里 - let setupPath = path.join(ctx.resourceRoot(), 'setup.bat'); - if (!fs.existsSync(setupPath)) { - setupPath = path.join(ctx.appRoot(), 'setup.bat'); - } - if (!fs.existsSync(setupPath)) { - resolve({ success: false, output: '找不到 setup.bat' }); - return; - } - - const proc = spawn('cmd.exe', ['/c', setupPath], { - cwd: ctx.appRoot(), - windowsHide: false, - stdio: 'pipe', - }); - - let output = ''; - proc.stdout?.on('data', (data: Buffer) => { - const text = data.toString(); - output += text; - ctx.getMainWindow()?.webContents.send('setup-log', text); - }); - proc.stderr?.on('data', (data: Buffer) => { - const text = data.toString(); - output += text; - ctx.getMainWindow()?.webContents.send('setup-log', text); - }); - proc.on('close', (code) => { - resolve({ success: code === 0, output: output.slice(-1000) }); - }); - proc.on('error', (err) => { - resolve({ success: false, output: err.message }); - }); - }); -} - -export async function startBackend(): Promise { - ensurePthFile(); - const pythonCmd = await findPython(); - if (!pythonCmd) { - console.error('[Backend] 找不到 Python'); - return; - } - - const certFile = await ensureSslCertForPython(pythonCmd); - if (certFile) console.log(`[Backend] TLS cert: ${certFile}`); - else console.warn('[Backend] WARNING 未检测到 TLS 根证书,HTTPS 请求可能失败'); - - const cwd = ctx.appRoot(); - const localSite = localSitePackages(); - const guiSettings = readGuiSettings(); - const backendStartupMode = guiSettings.backend_startup_mode === 'external' ? 'external' : 'managed'; - const localBackendRepo = backendStartupMode === 'external' ? resolveLocalBackendRepoPath() : null; - const ocrGpuMode = readOcrGpuModeFromSettings(); - const configuredCudaRoot = readCudaPathFromSettings(); - const saveBackendScreenshots = readSaveBackendScreenshotsFromSettings(); - - const pyLiteral = (value: string): string => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); - - // 使用 -c 启动而非 -m uvicorn,以便: - // 1. 显式注入 site-packages 到 sys.path - // 2. 激活 setuptools 的 distutils 兼容层 (Python 3.12+ 需要) - // 3. 绕过嵌入式 Python 的 ._pth/PYTHONPATH 限制 - const bootstrapParts = [ - `import sys, os, site`, - `sp = r'${pyLiteral(localSite)}'`, - `sys.path.insert(0, sp)`, - `site.addsitedir(sp)`, // 处理 .pth 文件,激活 _distutils_hack - ...(localBackendRepo ? [`repo = r'${pyLiteral(localBackendRepo)}'`, `sys.path.insert(0, repo)`] : []), - `GUI_OCR_GPU_MODE = '${ocrGpuMode}'`, - `GUI_SAVE_IMAGES = ${saveBackendScreenshots ? 'True' : 'False'}`, - `import autowsgr`, - `print('[Bootstrap] autowsgr=' + getattr(autowsgr, '__file__', 'unknown'))`, - `print('[Bootstrap] repo_override=' + (r'${pyLiteral(localBackendRepo ?? '')}' or ''))`, - `print('[Bootstrap] ocr_gpu_mode=' + GUI_OCR_GPU_MODE)`, - `print('[Bootstrap] save_backend_screenshots=' + ('true' if GUI_SAVE_IMAGES else 'false'))`, - `from pathlib import Path`, - `import autowsgr.infra.logger as _aw_logger`, - `from autowsgr.scheduler import launcher as _aw_launcher`, - `import autowsgr.vision.ocr as _aw_ocr`, - `_orig_load_config = _aw_launcher.Launcher.load_config`, - `_orig_save_image = _aw_logger.save_image`, - `_orig_create = _aw_ocr.OCREngine.create.__func__`, - `_cuda_cache = None`, - `def _detect_cuda():`, - ` global _cuda_cache`, - ` if _cuda_cache is not None:`, - ` return _cuda_cache`, - ` try:`, - ` import torch`, - ` print('[Bootstrap] torch=' + str(getattr(torch, '__version__', 'unknown')))`, - ` print('[Bootstrap] torch_cuda_build=' + str(getattr(getattr(torch, 'version', None), 'cuda', None)))`, - ` _cuda_cache = bool(torch.cuda.is_available())`, - ` except Exception:`, - ` _cuda_cache = False`, - ` return _cuda_cache`, - `def _resolve_gpu_mode():`, - ` if GUI_OCR_GPU_MODE == 'cuda':`, - ` if not _detect_cuda():`, - ` raise RuntimeError('已强制使用 CUDA,但当前 PyTorch/驱动未检测到可用 CUDA;请检查 CUDA 路径、CUDA 版 PyTorch 与 NVIDIA 驱动')`, - ` return True`, - ` if GUI_OCR_GPU_MODE == 'cpu':`, - ` return False`, - ` return _detect_cuda()`, - `if GUI_OCR_GPU_MODE == 'cpu':`, - ` print('[Bootstrap] cuda_available=skipped(cpu mode)')`, - `else:`, - ` print('[Bootstrap] cuda_available=' + ('true' if _detect_cuda() else 'false'))`, - `def _patched_create(cls, engine='easyocr', gpu=False, mirror='tencent'):`, - ` use_gpu = gpu`, - ` if str(engine).lower() == 'easyocr':`, - ` use_gpu = _resolve_gpu_mode()`, - ` return _orig_create(cls, engine=engine, gpu=use_gpu, mirror=mirror)`, - `_aw_ocr.OCREngine.create = classmethod(_patched_create)`, - `def _patched_load_config(self):`, - ` cfg = _orig_load_config(self)`, - ` if GUI_SAVE_IMAGES:`, - ` try:`, - ` log_dir = getattr(cfg.log, 'dir', None)`, - ` if log_dir is not None:`, - ` img_dir = Path(log_dir) / 'images'`, - ` img_dir.mkdir(parents=True, exist_ok=True)`, - ` _aw_logger._image_dir = img_dir`, - ` _aw_logger.logger.info('[GUI] 截图保存目录: {}', img_dir)`, - ` except Exception as _e:`, - ` _aw_logger.logger.warning('[GUI] 截图目录初始化失败: {}', _e)`, - ` else:`, - ` _aw_logger._image_dir = None`, - ` return cfg`, - `_aw_launcher.Launcher.load_config = _patched_load_config`, - `def _patched_save_image(image, tag='screenshot', img_dir=None):`, - ` if not GUI_SAVE_IMAGES:`, - ` return None`, - ` target_dir = img_dir or getattr(_aw_logger, '_image_dir', None)`, - ` if target_dir is None:`, - ` return None`, - ` return _orig_save_image(image, tag=tag, img_dir=target_dir)`, - `_aw_logger.save_image = _patched_save_image`, - `import uvicorn`, - `uvicorn.run('autowsgr.server.main:app', host='127.0.0.1', port=${ctx.BACKEND_PORT})`, - ]; - - const bootstrap = bootstrapParts.join('\n'); - const mainWindow = ctx.getMainWindow(); - if (localBackendRepo) { - console.log(`[Backend] 使用本地后端仓库: ${localBackendRepo}`); - mainWindow?.webContents.send('backend-log', `[GUI] 使用本地后端仓库: ${localBackendRepo}`); - } else { - mainWindow?.webContents.send('backend-log', '[GUI] 未启用本地后端仓库覆盖,使用 site-packages 中的 autowsgr'); - } - mainWindow?.webContents.send('backend-log', `[GUI] OCR 加速模式: ${ocrGpuMode}`); - mainWindow?.webContents.send('backend-log', `[GUI] CUDA 路径: ${configuredCudaRoot ?? '系统自动检测'}`); - mainWindow?.webContents.send('backend-log', `[GUI] 保存识别异常截图: ${saveBackendScreenshots ? '开启' : '关闭'}`); - - // 将内置 ADB 目录加入 PATH,使后端 shutil.which('adb') 能找到 - const adbDir = path.join(ctx.appRoot(), 'adb'); - const cudaEnv = buildCudaEnvironment(process.env, configuredCudaRoot); - const envPath = cudaEnv.PATH || ''; - const pathWithAdb = fs.existsSync(adbDir) ? `${adbDir};${envPath}` : envPath; - - // 预连接 ADB 设备(MuMu 多开实例不会自动被 ADB 发现,需要主动 connect) - try { - const cfgPath = path.join(ctx.appRoot(), 'usersettings.yaml'); - if (fs.existsSync(cfgPath)) { - const cfgText = fs.readFileSync(cfgPath, 'utf-8'); - const serialMatch = cfgText.match(/serial:\s*(\S+)/); - if (serialMatch) { - const serial = serialMatch[1]; - const adbExe = path.join(adbDir, 'adb.exe'); - const adbCmd = fs.existsSync(adbExe) ? adbExe : 'adb'; - execSync(`"${adbCmd}" connect ${serial}`, { windowsHide: true, timeout: 5000, stdio: 'pipe' }); - console.log(`[Backend] ADB connect ${serial} 完成`); - } - } - } catch (e: any) { - console.warn(`[Backend] ADB connect 失败 (非致命): ${e.message}`); - } - - backendProcess = spawn(pythonCmd, [ - '-X', 'utf8', - '-c', bootstrap, - ], { - cwd, - windowsHide: true, - stdio: 'pipe', - env: { - ...cudaEnv, - PYTHONUTF8: '1', - PYTHONIOENCODING: 'utf-8', - PATH: pathWithAdb, - }, - }); - - // ANSI 颜色码 - const CYAN = '\x1b[36m'; - const RED = '\x1b[31m'; - const YELLOW = '\x1b[33m'; - const GREEN = '\x1b[32m'; - const DIM = '\x1b[2m'; - const RESET = '\x1b[0m'; - - const colorLine = (line: string): string => { - if (/\bERROR\b/i.test(line)) return `${RED}${line}${RESET}`; - if (/\bWARNING\b/i.test(line)) return `${YELLOW}${line}${RESET}`; - if (/\bINFO\b/i.test(line)) return `${GREEN}${line}${RESET}`; - if (/\bDEBUG\b/i.test(line)) return `${DIM}${line}${RESET}`; - return `${CYAN}${line}${RESET}`; - }; - - // loguru 新日志行以 "HH:mm:ss.SSS |" 开头 - const LOGURU_LINE_RE = /^\d{2}:\d{2}:\d{2}\.\d{3}\s*\|/; - let skipMultiline = false; - - const handleOutput = (data: Buffer) => { - for (const line of data.toString('utf-8').split('\n')) { - const trimmed = line.trim(); - if (!trimmed) continue; - console.log(`${CYAN}[Backend]${RESET} ${colorLine(trimmed)}`); - - const isNewEntry = LOGURU_LINE_RE.test(trimmed); - if (isNewEntry) { - // 新日志条目:判断级别,决定是否跳过后续续行 - skipMultiline = /\bDEBUG\b/i.test(trimmed); - } - // 跳过 DEBUG 级别的日志(包括其多行续行) - if (skipMultiline) continue; - // 跳过 uvicorn access log - if (/"(?:GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s+\//.test(trimmed)) continue; - mainWindow?.webContents.send('backend-log', trimmed); - } - }; - backendProcess.stdout?.on('data', handleOutput); - backendProcess.stderr?.on('data', handleOutput); - backendProcess.on('error', (err) => { - console.error('[Backend] 启动失败:', err.message); - backendProcess = null; - }); - backendProcess.on('close', (code) => { - console.log(`[Backend] 进程退出, code=${code}`); - backendProcess = null; - }); -} - -export function stopBackend(): void { - if (backendProcess) { - backendProcess.kill(); - backendProcess = null; - } -} diff --git a/electron/emulatorDetect.ts b/electron/emulatorDetect.ts index 8392539..03ab73b 100644 --- a/electron/emulatorDetect.ts +++ b/electron/emulatorDetect.ts @@ -1,6 +1,5 @@ /** - * 模拟器自动检测 (Windows 注册表)。 - * 从 main.ts 提取,无外部依赖。 + * 通过 Windows 注册表检测已安装的模拟器。 */ import * as path from 'path'; import * as fs from 'fs'; @@ -13,13 +12,14 @@ export interface EmulatorDetectResult { adbPath: string; } +/** 读取注册表中的单个值。 */ export function readRegistryValue(keyPath: string, valueName: string): string | null { try { const output = execSync( `reg query "${keyPath}" /v "${valueName}"`, { encoding: 'utf-8', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] }, ); - // 输出格式: " ValueName REG_SZ Value" + // reg query 输出格式为“名称、类型、值”。 const match = output.match(new RegExp(`${valueName}\\s+REG_\\w+\\s+(.+)`)); return match ? match[1].trim() : null; } catch { @@ -27,6 +27,7 @@ export function readRegistryValue(keyPath: string, valueName: string): string | } } +/** 读取注册表下的直接子键。 */ export function readRegistrySubKeys(keyPath: string): string[] { try { const output = execSync( @@ -42,10 +43,11 @@ export function readRegistrySubKeys(keyPath: string): string[] { } } +/** 按 MuMu、雷电、蓝叠的顺序检测模拟器。 */ export function detectEmulator(): EmulatorDetectResult | null { if (process.platform !== 'win32') return null; - // ── MuMu 12 ── + // MuMu 12 // 用单次 reg query /s 递归搜索 Uninstall 下的 UninstallString, // 再从输出中筛选含 MuMu 的条目,避免逐键启动子进程。 const uninstallBase = 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall'; @@ -54,11 +56,11 @@ export function detectEmulator(): EmulatorDetectResult | null { `reg query "${uninstallBase}" /s /v UninstallString`, { encoding: 'utf-8', windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }, ); - // 输出格式: 键路径行 + 空行 + " UninstallString REG_SZ value" + 空行 ... + // 输出由注册表键路径和 UninstallString 值交替组成。 for (const line of output.split('\n')) { const trimmed = line.trim(); if (trimmed.startsWith('HKEY')) { - // 键路径行: 当前实现不需要使用具体键名, 仅保留以便未来扩展或调试 + // 当前检测不需要具体键名。 continue; } if (/UninstallString/i.test(trimmed) && /MuMu/i.test(trimmed)) { @@ -80,9 +82,9 @@ export function detectEmulator(): EmulatorDetectResult | null { } } } - } catch { /* Uninstall 注册表扫描失败, 继续检测其他模拟器 */ } + } catch { /* 扫描失败时继续检测其他模拟器。 */ } - // ── 雷电模拟器 ── + // 雷电模拟器 try { const leidianSubs = readRegistrySubKeys('HKLM\\SOFTWARE\\leidian'); for (const subKey of leidianSubs) { @@ -100,9 +102,9 @@ export function detectEmulator(): EmulatorDetectResult | null { } } } - } catch { /* 未安装 */ } + } catch { /* 未安装时继续检测。 */ } - // ── 蓝叠 ── + // 蓝叠 for (const regKey of ['HKLM\\SOFTWARE\\BlueStacks_nxt_cn', 'HKLM\\SOFTWARE\\BlueStacks_nxt']) { const installDir = readRegistryValue(regKey, 'InstallDir'); if (installDir) { diff --git a/electron/ipc/BackendIpc.ts b/electron/ipc/BackendIpc.ts new file mode 100644 index 0000000..7c60356 --- /dev/null +++ b/electron/ipc/BackendIpc.ts @@ -0,0 +1,38 @@ +/** + * 连接后端进程 IPC 与 BackendService。 + */ +import type { ChildProcess } from 'child_process'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface BackendIpcDependencies { + getBackendProcess(): ChildProcess | null; + startBackend(): Promise; + runSetupScript(): Promise; +} + +/** 注册后端安装脚本和启动 IPC。 */ +export function registerBackendIpc( + ipc: IpcRegistrar, + dependencies: BackendIpcDependencies, +): void { + ipc.handle('run-setup', async () => { + return dependencies.runSetupScript(); + }); + + ipc.handle('start-backend', async () => { + if (dependencies.getBackendProcess()) { + return { success: true, message: '后端已在运行' }; + } + try { + await dependencies.startBackend(); + return { success: true, message: '后端启动中' }; + } catch (error) { + return { + success: false, + message: error instanceof Error + ? error.message + : String(error), + }; + } + }); +} diff --git a/electron/ipc/CombatPlanIpc.ts b/electron/ipc/CombatPlanIpc.ts new file mode 100644 index 0000000..1679217 --- /dev/null +++ b/electron/ipc/CombatPlanIpc.ts @@ -0,0 +1,215 @@ +/** + * 连接作战计划 IPC、文件对话框和计划服务。 + */ +import type { + MessageBoxOptions, + OpenDialogOptions, + SaveDialogOptions, +} from 'electron'; +import type { PlanManagementService } from '../services/PlanManagementService'; +import type { PlanExportService } from '../services/PlanExportService'; +import type { SafePathService } from '../services/SafePathService'; +import type { PlanPresetSource } from '../services/TeamPlanCodec'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface CombatPlanDialogAdapter { + showOpenDialog(options: OpenDialogOptions): Promise<{ + canceled: boolean; + filePaths: string[]; + }>; + showMessageBox(options: MessageBoxOptions): Promise<{ + response: number; + }>; + showSaveDialog(options: SaveDialogOptions): Promise<{ + canceled: boolean; + filePath?: string; + }>; +} + +export interface CombatPlanIpcDependencies { + dialog: CombatPlanDialogAdapter; + safePaths: SafePathService; + plans: PlanManagementService; + planExports: PlanExportService; +} + +/** 注册作战计划管理和运行时准备 IPC。 */ +export function registerCombatPlanIpc( + ipc: IpcRegistrar, + dependencies: CombatPlanIpcDependencies, +): void { + ipc.handle('get-plan-management', () => { + return dependencies.plans.get(); + }); + + ipc.handle('export-user-plans', async (_event, selections: unknown) => { + try { + const archive = await dependencies.planExports.createArchive( + selections, + ); + const selected = await dependencies.dialog.showSaveDialog({ + title: '批量导出用户配置', + defaultPath: dependencies.planExports.archiveFileName(), + filters: [{ + name: 'ZIP 压缩包', + extensions: ['zip'], + }], + }); + if (selected.canceled || !selected.filePath) { + return { success: false, canceled: true }; + } + dependencies.planExports.writeArchive(selected.filePath, archive); + return { + success: true, + path: selected.filePath, + count: archive.count, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }); + + ipc.handle('import-local-combat-plan', async () => { + try { + const selected = await dependencies.dialog.showOpenDialog({ + title: '添加本地出征计划', + properties: ['openFile'], + filters: [{ + name: '出征计划 YAML', + extensions: ['yaml', 'yml'], + }], + }); + if (selected.canceled || selected.filePaths.length === 0) { + return { success: false, canceled: true }; + } + + const selectedPath = selected.filePaths[0]; + const result = dependencies.plans.importLocal( + selectedPath, + false, + ); + if (result.exists !== true) return result; + + const conflicts = Array.isArray(result.conflicts) + ? result.conflicts.filter( + (value): value is string => typeof value === 'string', + ) + : []; + const confirmation = await dependencies.dialog.showMessageBox({ + type: 'warning', + title: '覆盖用户配置', + message: '导入目标存在同名配置,是否覆盖?', + detail: conflicts.join('\n'), + buttons: ['取消', '覆盖'], + defaultId: 0, + cancelId: 0, + noLink: true, + }); + if (confirmation.response !== 1) { + return { success: false, canceled: true }; + } + return dependencies.plans.importLocal(selectedPath, true); + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + }); + + ipc.handle( + 'set-plan-unlinked-ignored', + ( + _event, + kind: 'battle' | 'team', + source: PlanPresetSource, + file: string, + ignored: boolean, + ) => { + return dependencies.plans.setUnlinkedIgnored( + kind, + source, + file, + ignored, + ); + }, + ); + + ipc.handle( + 'read-managed-combat-plan', + (_event, source: PlanPresetSource, file: string) => { + return dependencies.plans.readManaged(source, file); + }, + ); + + ipc.handle( + 'read-combat-plan-file', + (_event, rawPath: string) => { + try { + if (typeof rawPath !== 'string' || !rawPath.trim()) { + throw new Error('出征计划路径不能为空'); + } + const resolved = dependencies.safePaths.resolveAppPath( + rawPath, + ); + return dependencies.plans.readResolvedFile(resolved); + } catch (error) { + return { + success: false, + error: error instanceof Error + ? error.message + : String(error), + }; + } + }, + ); + + ipc.handle( + 'prepare-combat-plan-execution', + (_event, content: string, hint: string) => { + return dependencies.plans.prepareExecution(content, hint); + }, + ); + + ipc.handle( + 'save-managed-combat-plan', + ( + _event, + rawName: string, + content: string, + overwrite: boolean, + currentFile?: string, + ) => { + return dependencies.plans.saveManaged( + rawName, + content, + overwrite, + currentFile, + ); + }, + ); + + ipc.handle( + 'rename-user-combat-plan', + (_event, file: string, newName: string) => { + return dependencies.plans.renameUser(file, newName); + }, + ); + + ipc.handle( + 'delete-user-combat-plan', + (_event, file: string) => { + return dependencies.plans.deleteUserCombat(file); + }, + ); + + ipc.handle( + 'delete-user-team-plan', + (_event, file: string) => { + return dependencies.plans.deleteUserTeam(file); + }, + ); +} diff --git a/electron/ipc/ConfigurationIpc.ts b/electron/ipc/ConfigurationIpc.ts new file mode 100644 index 0000000..c3fe9c8 --- /dev/null +++ b/electron/ipc/ConfigurationIpc.ts @@ -0,0 +1,181 @@ +/** + * 连接 GUI 配置 IPC 与配置、环境和窗口服务。 + */ +import type { CudaEnvironmentService } from '../services/CudaEnvironmentService'; +import type { + BackendStartupMode, + DecisivePlanSettings, + GuiAutomationSettings, + GuiConfigurationService, + OcrGpuMode, + UpdateMode, +} from '../services/GuiConfigurationService'; +import type { PythonEnvironmentService } from '../services/PythonEnvironmentService'; +import type { + WindowPreferences, + WindowService, +} from '../services/WindowService'; +import type { + LegacyDecisiveAutomationSettings, +} from '../../src/shared/legacyDecisiveAutomation'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface ConfigurationIpcDependencies { + getAppVersion(): string; + backendPort: number; + configuration: GuiConfigurationService; + cudaEnvironment: CudaEnvironmentService; + pythonEnvironment: PythonEnvironmentService; + windows: WindowService; +} + +/** 注册同步配置 getter 和异步配置操作。 */ +export function registerConfigurationIpc( + ipc: IpcRegistrar, + dependencies: ConfigurationIpcDependencies, +): void { + const configuration = dependencies.configuration; + + ipc.on('get-app-version-sync', (event) => { + event.returnValue = dependencies.getAppVersion(); + }); + + ipc.on('get-backend-port-sync', (event) => { + event.returnValue = dependencies.backendPort; + }); + + ipc.on('get-backend-startup-mode-sync', (event) => { + event.returnValue = configuration.backendStartupMode(); + }); + + ipc.on('get-backend-repo-path-sync', (event) => { + event.returnValue = configuration.backendRepoPath(); + }); + + ipc.on('get-ocr-gpu-mode-sync', (event) => { + event.returnValue = configuration.ocrGpuMode(); + }); + + ipc.on('get-cuda-path-sync', (event) => { + event.returnValue = configuration.cudaPath(); + }); + + ipc.on('get-save-backend-screenshots-sync', (event) => { + event.returnValue = configuration.saveBackendScreenshots(); + }); + + ipc.on('get-window-preferences-sync', (event) => { + event.returnValue = dependencies.windows.getPreferences(); + }); + + ipc.handle( + 'set-window-preferences', + (_event, preferences: Partial) => { + return dependencies.windows.setPreferences(preferences); + }, + ); + + ipc.handle('get-gui-automation-settings', () => { + return configuration.automation(); + }); + + ipc.handle( + 'set-gui-automation-settings', + (_event, settings: GuiAutomationSettings) => { + return configuration.setAutomation(settings); + }, + ); + + ipc.handle( + 'migrate-legacy-decisive-automation', + (_event, settings: LegacyDecisiveAutomationSettings) => { + return configuration.migrateLegacyDecisiveAutomation(settings); + }, + ); + + ipc.handle('get-decisive-plan-settings', () => { + return configuration.decisivePlan(); + }); + + ipc.handle( + 'set-decisive-plan-settings', + (_event, settings: DecisivePlanSettings) => { + return configuration.setDecisivePlan(settings); + }, + ); + + ipc.handle('set-backend-port', (_event, port: number) => { + configuration.setBackendPort(port); + }); + + ipc.handle( + 'set-backend-startup-mode', + (_event, mode: BackendStartupMode) => { + configuration.setBackendStartupMode(mode); + }, + ); + + ipc.handle( + 'set-backend-repo-path', + (_event, repoPath: string | null) => { + configuration.setBackendRepoPath(repoPath); + }, + ); + + ipc.handle( + 'set-ocr-gpu-mode', + (_event, mode: OcrGpuMode) => { + configuration.setOcrGpuMode(mode); + }, + ); + + ipc.handle( + 'set-cuda-path', + (_event, cudaPath: string | null) => { + configuration.setCudaPath(cudaPath); + }, + ); + + ipc.handle( + 'validate-cuda-path', + async (_event, cudaPath: string) => { + return dependencies.cudaEnvironment.detect(cudaPath); + }, + ); + + ipc.handle( + 'set-save-backend-screenshots', + (_event, enabled: boolean) => { + configuration.setSaveBackendScreenshots(enabled); + }, + ); + + ipc.on('get-python-path-sync', (event) => { + event.returnValue = configuration.configuredPythonPath(); + }); + + ipc.on('get-update-mode-sync', (event) => { + event.returnValue = configuration.updateMode(); + }); + + ipc.handle( + 'set-update-mode', + (_event, mode: UpdateMode) => { + configuration.setUpdateMode(mode); + }, + ); + + ipc.handle( + 'set-python-path', + (_event, pythonPath: string | null) => { + configuration.setPythonPath(pythonPath); + }, + ); + + ipc.handle( + 'validate-python', + async (_event, pythonPath: string) => { + return dependencies.pythonEnvironment.validate(pythonPath); + }, + ); +} diff --git a/electron/ipc/DailyPlanIpc.ts b/electron/ipc/DailyPlanIpc.ts new file mode 100644 index 0000000..0457c5f --- /dev/null +++ b/electron/ipc/DailyPlanIpc.ts @@ -0,0 +1,50 @@ +/** + * 连接日常任务计划服务与渲染进程。 + */ +import type { DailyPlanService } from '../services/DailyPlanService'; +import type { + DecisivePlanSettings, + GuiConfigurationService, +} from '../services/GuiConfigurationService'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface DailyPlanIpcDependencies { + dailyPlans: DailyPlanService; + configuration: GuiConfigurationService; +} + +/** 注册日常任务列表、读取和按章节保存决战配置的 IPC。 */ +export function registerDailyPlanIpc( + ipc: IpcRegistrar, + dependencies: DailyPlanIpcDependencies, +): void { + ipc.handle('list-daily-plans', () => { + return dependencies.dailyPlans.list(); + }); + + ipc.handle( + 'read-daily-plan', + (_event, source: 'system' | 'user', file: string) => { + return dependencies.dailyPlans.read(source, file); + }, + ); + + ipc.handle('get-daily-decisive-plan', (_event, chapter: number) => { + return dependencies.dailyPlans.decisivePlan(chapter); + }); + + ipc.handle( + 'get-system-daily-decisive-plan', + (_event, chapter: number) => { + return dependencies.dailyPlans.systemDecisivePlan(chapter); + }, + ); + + ipc.handle( + 'save-daily-decisive-plan', + (_event, settings: DecisivePlanSettings) => { + const saved = dependencies.dailyPlans.saveDecisivePlan(settings); + return dependencies.configuration.setDecisivePlan(saved); + }, + ); +} diff --git a/electron/ipc/DeviceIpc.ts b/electron/ipc/DeviceIpc.ts new file mode 100644 index 0000000..0dc6a0a --- /dev/null +++ b/electron/ipc/DeviceIpc.ts @@ -0,0 +1,42 @@ +/** + * 连接模拟器和 ADB 设备 IPC。 + */ +import type { AdbService } from '../services/AdbService'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface DeviceIpcDependencies { + adb: AdbService; + detectEmulator(): unknown; +} + +/** 注册模拟器检测和 ADB 设备操作 IPC。 */ +export function registerDeviceIpc( + ipc: IpcRegistrar, + dependencies: DeviceIpcDependencies, +): void { + ipc.handle('detect-emulator', async () => { + return dependencies.detectEmulator(); + }); + + ipc.handle('check-adb-devices', async () => { + try { + return await dependencies.adb.listDevices(); + } catch { + return []; + } + }); + + ipc.handle( + 'connect-adb-device', + async (_event, serial: string) => { + return dependencies.adb.runDeviceCommand('connect', serial); + }, + ); + + ipc.handle( + 'disconnect-adb-device', + async (_event, serial: string) => { + return dependencies.adb.runDeviceCommand('disconnect', serial); + }, + ); +} diff --git a/electron/ipc/EnvironmentIpc.ts b/electron/ipc/EnvironmentIpc.ts new file mode 100644 index 0000000..62fb75b --- /dev/null +++ b/electron/ipc/EnvironmentIpc.ts @@ -0,0 +1,23 @@ +/** + * 连接 Python 环境 IPC 与 PythonEnvironmentService。 + */ +import type { PythonEnvironmentService } from '../services/PythonEnvironmentService'; +import type { IpcRegistrar } from './IpcRegistrar'; + +/** 注册 Python 环境检查和安装 IPC。 */ +export function registerEnvironmentIpc( + ipc: IpcRegistrar, + pythonEnvironment: PythonEnvironmentService, +): void { + ipc.handle('check-environment', async () => { + return await pythonEnvironment.check(); + }); + + ipc.handle('install-deps', async () => { + return pythonEnvironment.installDependencies(); + }); + + ipc.handle('install-portable-python', async () => { + return pythonEnvironment.installPortablePython(); + }); +} diff --git a/electron/ipc/FileIpc.ts b/electron/ipc/FileIpc.ts new file mode 100644 index 0000000..960b279 --- /dev/null +++ b/electron/ipc/FileIpc.ts @@ -0,0 +1,151 @@ +/** + * 连接文件、路径和目录 IPC 与安全文件服务。 + */ +import * as fs from 'fs'; +import type { FileFilter } from 'electron'; +import type { CombatPlanRepository } from '../services/CombatPlanRepository'; +import type { SafePathService } from '../services/SafePathService'; +import type { SecureFileService } from '../services/SecureFileService'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface FileDialogAdapter { + showOpenDialog(options: { + properties: Array<'openFile' | 'openDirectory'>; + title?: string; + defaultPath?: string; + filters?: FileFilter[]; + }): Promise<{ canceled: boolean; filePaths: string[] }>; + showSaveDialog(options: { + defaultPath?: string; + filters?: FileFilter[]; + }): Promise<{ + canceled: boolean; + filePath?: string; + }>; +} + +export interface FolderAdapter { + openPath(folderPath: string): Promise; +} + +export interface FileIpcDependencies { + dialog: FileDialogAdapter; + shell: FolderAdapter; + secureFiles: SecureFileService; + safePaths: SafePathService; + combatPlans: CombatPlanRepository; + appRoot(): string; + userDataRoot(): string; +} + +/** 注册文件、路径和目录相关 IPC。 */ +export function registerFileIpc( + ipc: IpcRegistrar, + dependencies: FileIpcDependencies, +): void { + ipc.handle('open-directory-dialog', async (_event, title?: string) => { + const result = await dependencies.dialog.showOpenDialog({ + properties: ['openDirectory'], + title: title || '选择文件夹', + }); + if (result.canceled || result.filePaths.length === 0) return null; + return result.filePaths[0]; + }); + + ipc.handle( + 'open-file-dialog', + async ( + _event, + filters: FileFilter[], + defaultDir?: string, + ) => { + const result = await dependencies.dialog.showOpenDialog({ + properties: ['openFile'], + defaultPath: defaultDir || undefined, + filters, + }); + if (result.canceled || result.filePaths.length === 0) { + return null; + } + const filePath = result.filePaths[0]; + const content = dependencies.secureFiles.readSelectedFile( + filePath, + ); + return { path: filePath, content }; + }, + ); + + ipc.handle( + 'save-file', + async (_event, filePath: string, content: string) => { + dependencies.secureFiles.save(filePath, content); + }, + ); + + ipc.handle( + 'save-file-dialog', + async ( + _event, + defaultName: string, + content: string, + filters: FileFilter[], + ) => { + const result = await dependencies.dialog.showSaveDialog({ + defaultPath: defaultName, + filters, + }); + if (result.canceled || !result.filePath) return null; + dependencies.secureFiles.writeSelectedFile( + result.filePath, + content, + ); + return result.filePath; + }, + ); + + ipc.handle('read-file', async (_event, filePath: string) => { + return dependencies.secureFiles.read(filePath); + }); + + ipc.handle( + 'append-file', + async (_event, filePath: string, content: string) => { + dependencies.secureFiles.append(filePath, content); + }, + ); + + ipc.handle('get-app-root', () => dependencies.appRoot()); + + ipc.handle('resolve-app-path', (_event, filePath: string) => { + return dependencies.safePaths.resolveAppPath(filePath); + }); + + ipc.handle('get-plans-dir', () => { + return dependencies.combatPlans.directory('user'); + }); + + ipc.handle('list-plan-files', () => { + const directory = dependencies.combatPlans.directory('user'); + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory) + .filter(file => /\.ya?ml$/i.test(file)) + .map(file => ({ + name: file.replace(/\.ya?ml$/i, ''), + file, + })); + }); + + ipc.handle('get-config-dir', () => dependencies.userDataRoot()); + + ipc.handle( + 'open-folder', + async (_event, folderPath: string) => { + const resolved = dependencies.safePaths.resolveWritablePath( + folderPath, + ); + if (fs.existsSync(resolved)) { + await dependencies.shell.openPath(resolved); + } + }, + ); +} diff --git a/electron/ipc/IpcRegistrar.ts b/electron/ipc/IpcRegistrar.ts new file mode 100644 index 0000000..5583a51 --- /dev/null +++ b/electron/ipc/IpcRegistrar.ts @@ -0,0 +1,6 @@ +/** + * 定义 IPC Adapter 使用的最小注册接口。 + */ +import type { IpcMain } from 'electron'; + +export type IpcRegistrar = Pick; diff --git a/electron/ipc/MigrationConflictIpc.ts b/electron/ipc/MigrationConflictIpc.ts new file mode 100644 index 0000000..cbdf3d0 --- /dev/null +++ b/electron/ipc/MigrationConflictIpc.ts @@ -0,0 +1,19 @@ +/** + * 向 Renderer 暴露迁移冲突清单及受限处理入口。 + */ +import type { + MigrationConflictService, +} from '../services/MigrationConflictService'; +import type { IpcRegistrar } from './IpcRegistrar'; + +/** 注册迁移冲突读取和确认处理 IPC。 */ +export function registerMigrationConflictIpc( + ipc: IpcRegistrar, + conflicts: MigrationConflictService, +): void { + ipc.handle('get-migration-conflicts', () => conflicts.pending()); + ipc.handle( + 'resolve-migration-conflicts', + (_event, keepIds: unknown) => conflicts.resolve(keepIds), + ); +} diff --git a/electron/ipc/ShipLibraryIpc.ts b/electron/ipc/ShipLibraryIpc.ts new file mode 100644 index 0000000..d090637 --- /dev/null +++ b/electron/ipc/ShipLibraryIpc.ts @@ -0,0 +1,29 @@ +/** + * 连接舰船资料库 IPC 与查询、更新服务。 + */ +import type { ShipLibraryService } from '../services/ShipLibraryService'; +import type { ShipLibraryUpdater } from '../services/ShipLibraryUpdater'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface ShipLibraryIpcDependencies { + library: ShipLibraryService; + updater: ShipLibraryUpdater; +} + +/** 注册舰船资料库读取和更新 IPC。 */ +export function registerShipLibraryIpc( + ipc: IpcRegistrar, + dependencies: ShipLibraryIpcDependencies, +): void { + ipc.handle('get-ship-library-status', () => { + return dependencies.library.getStatus(); + }); + + ipc.handle('get-ship-library-manifest', () => { + return dependencies.library.getManifest(); + }); + + ipc.handle('update-ship-library', async () => { + return await dependencies.updater.update(); + }); +} diff --git a/electron/ipc/TeamPlanIpc.ts b/electron/ipc/TeamPlanIpc.ts new file mode 100644 index 0000000..6cc3fe8 --- /dev/null +++ b/electron/ipc/TeamPlanIpc.ts @@ -0,0 +1,59 @@ +/** + * 连接编队计划 IPC、文件对话框和编队服务。 + */ +import type { PlanPresetSource } from '../services/TeamPlanCodec'; +import type { TeamPlanRepository } from '../services/TeamPlanRepository'; +import type { TeamPlanService } from '../services/TeamPlanService'; +import type { FileDialogAdapter } from './FileIpc'; +import type { IpcRegistrar } from './IpcRegistrar'; + +export interface TeamPlanIpcDependencies { + dialog: FileDialogAdapter; + repository: TeamPlanRepository; + service: TeamPlanService; +} + +/** 注册编队保存、选择和列表 IPC。 */ +export function registerTeamPlanIpc( + ipc: IpcRegistrar, + dependencies: TeamPlanIpcDependencies, +): void { + ipc.handle( + 'save-user-team-plan', + ( + _event, + rawPlan: unknown, + overwrite: boolean, + currentFile?: string, + rawSource?: PlanPresetSource, + ) => { + return dependencies.service.save( + rawPlan, + overwrite, + currentFile, + rawSource, + ); + }, + ); + + ipc.handle('pick-user-team-plan', async () => { + const directory = dependencies.repository.directory('user'); + const result = await dependencies.dialog.showOpenDialog({ + title: '加载编队预设', + defaultPath: directory, + properties: ['openFile'], + filters: [{ + name: '编队 YAML', + extensions: ['yaml', 'yml'], + }], + }); + if (result.canceled || result.filePaths.length === 0) { + return { success: false, canceled: true }; + } + return dependencies.service.loadSelected(result.filePaths[0]); + }); + + ipc.handle('list-team-plans', () => { + return dependencies.service.list(); + }); +} diff --git a/electron/ipc/UpdaterIpc.ts b/electron/ipc/UpdaterIpc.ts new file mode 100644 index 0000000..2e60572 --- /dev/null +++ b/electron/ipc/UpdaterIpc.ts @@ -0,0 +1,165 @@ +/** + * 连接 GUI 自动更新 IPC 与 electron-updater。 + */ +import type { BrowserWindow } from 'electron'; +import { + autoUpdater, + type ProgressInfo, + type UpdateInfo, +} from 'electron-updater'; +import type { IpcRegistrar } from './IpcRegistrar'; +import { + classifyGuiUpdateCheck, + resolveGuiReleasePolicy, + validateGuiUpdateCandidate, +} from '../services/GuiUpdatePolicy'; + +export interface UpdaterContext { + getMainWindow(): BrowserWindow | null; + getAppVersion(): string; + stopBackend(): Promise; +} + +/** 注册 GUI 更新事件和 IPC。 */ +export function registerUpdaterIpc( + ipc: IpcRegistrar, + context: UpdaterContext, +): void { + const releasePolicy = resolveGuiReleasePolicy(context.getAppVersion()); + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.channel = releasePolicy.channel; + autoUpdater.allowPrerelease = releasePolicy.allowPrerelease; + autoUpdater.allowDowngrade = false; + + let approvedUpdateVersion: string | null = null; + let downloadedUpdateVersion: string | null = null; + const reportError = (message: string): void => { + context.getMainWindow()?.webContents.send('update-status', { + status: 'error', + message, + }); + }; + + autoUpdater.on('update-available', (info: UpdateInfo) => { + const mismatch = validateGuiUpdateCandidate( + releasePolicy, + info.version, + ); + if (mismatch) { + approvedUpdateVersion = null; + reportError(mismatch); + return; + } + approvedUpdateVersion = info.version; + context.getMainWindow()?.webContents.send('update-status', { + status: 'available', + version: info.version, + releaseNotes: typeof info.releaseNotes === 'string' + ? info.releaseNotes + : '', + }); + }); + autoUpdater.on('update-not-available', () => { + approvedUpdateVersion = null; + downloadedUpdateVersion = null; + context.getMainWindow()?.webContents.send('update-status', { + status: 'up-to-date', + }); + }); + autoUpdater.on('download-progress', (progress: ProgressInfo) => { + context.getMainWindow()?.webContents.send('update-status', { + status: 'downloading', + percent: Math.round(progress.percent), + transferred: progress.transferred, + total: progress.total, + }); + }); + autoUpdater.on('update-downloaded', (info: UpdateInfo) => { + const mismatch = validateGuiUpdateCandidate( + releasePolicy, + info.version, + ); + if (mismatch) { + downloadedUpdateVersion = null; + reportError(mismatch); + return; + } + downloadedUpdateVersion = info.version; + context.getMainWindow()?.webContents.send('update-status', { + status: 'downloaded', + version: info.version, + }); + }); + autoUpdater.on('error', (error: Error) => { + approvedUpdateVersion = null; + downloadedUpdateVersion = null; + reportError(error.message); + }); + + ipc.handle('check-gui-updates', async () => { + try { + const result = await autoUpdater.checkForUpdates(); + const classified = classifyGuiUpdateCheck( + releasePolicy, + result, + ); + approvedUpdateVersion = classified.status === 'available' + ? classified.version + : null; + return classified; + } catch (error) { + approvedUpdateVersion = null; + return { + status: 'error', + message: error instanceof Error + ? error.message + : String(error), + }; + } + }); + + ipc.handle('download-gui-update', async () => { + if (!approvedUpdateVersion) { + return { + success: false, + message: '当前频道没有已确认可下载的更新', + }; + } + try { + await autoUpdater.downloadUpdate(); + return { success: true }; + } catch (error) { + return { + success: false, + message: error instanceof Error + ? error.message + : String(error), + }; + } + }); + + ipc.handle('install-gui-update', async () => { + if (!downloadedUpdateVersion) { + return { + success: false, + message: '没有已下载并通过频道校验的更新', + }; + } + context.getMainWindow()?.webContents.send('update-status', { + status: 'installing', + message: '正在停止后端并准备安装更新', + }); + try { + await context.stopBackend(); + autoUpdater.quitAndInstall(false, true); + return { success: true }; + } catch (error) { + const message = error instanceof Error + ? error.message + : String(error); + reportError(`无法安全停止后端,已取消更新安装:${message}`); + return { success: false, message }; + } + }); +} diff --git a/electron/main.ts b/electron/main.ts index 85fde38..38ab5c4 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,629 +1,396 @@ /** - * Electron 主进程。 - * 负责创建窗口、注册 IPC handler。 + * 组装主进程服务、注册 IPC,并管理 Electron 生命周期。 */ -import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'; +import { app, BrowserWindow, ipcMain, dialog, screen, shell } from 'electron'; import * as path from 'path'; -import * as fs from 'fs'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { autoUpdater, UpdateInfo, ProgressInfo } from 'electron-updater'; import { initPythonEnv, clearPythonCache, isAllowedPythonVersion, findPython, checkEnvironment, - checkForUpdates, installDependencies, installPortablePython, - pullUpdates, + installDependencies, installPortablePython, } from './pythonEnv'; import { detectEmulator } from './emulatorDetect'; -import { initBackend, getBackendProcess, startBackend, stopBackend, runSetupScript } from './backend'; - -const execAsync = promisify(exec); - -/** GUI 设置文件路径(延迟到 app ready 后才有效,先用函数) */ -function guiSettingsPath(): string { - return path.join(appRoot(), 'gui_settings.json'); -} - -/** 读取 GUI 设置 */ -function readGuiSettings(): Record { - try { - const p = guiSettingsPath(); - if (fs.existsSync(p)) { - return JSON.parse(fs.readFileSync(p, 'utf-8')); - } - } catch { /* ignore */ } - return {}; -} - -/** 写入 GUI 设置(合并) */ -function writeGuiSettings(patch: Record): void { - const cur = readGuiSettings(); - Object.assign(cur, patch); - fs.writeFileSync(guiSettingsPath(), JSON.stringify(cur, null, 2), 'utf-8'); -} - -/** 后端端口:环境变量 > gui_settings.json > 默认 8438 */ -function getBackendPort(): number { - if (process.env.AUTOWSGR_PORT) { - return parseInt(process.env.AUTOWSGR_PORT, 10); - } - const settings = readGuiSettings(); - if (typeof settings.backend_port === 'number' && settings.backend_port > 0 && settings.backend_port < 65536) { - return settings.backend_port; - } - return 8438; -} - -const BACKEND_PORT = getBackendPort(); - -/** 用户配置的 Python 路径:gui_settings.json > null (自动检测) */ -function getConfiguredPythonPath(): string | null { - const settings = readGuiSettings(); - if (typeof settings.python_path === 'string' && settings.python_path.length > 0) { - return settings.python_path; - } - return null; -} - -function getUpdateMode(): 'auto' | 'manual' { - const settings = readGuiSettings(); - return settings.update_mode === 'manual' ? 'manual' : 'auto'; -} - -type BackendStartupMode = 'managed' | 'external'; -type OcrGpuMode = 'auto' | 'cpu' | 'cuda'; - -function getBackendStartupMode(): BackendStartupMode { - const settings = readGuiSettings(); - return settings.backend_startup_mode === 'external' ? 'external' : 'managed'; -} - -function getBackendRepoPath(): string { - const settings = readGuiSettings(); - if (typeof settings.backend_repo_path !== 'string') return ''; - return settings.backend_repo_path.trim(); -} - -function getOcrGpuMode(): OcrGpuMode { - const settings = readGuiSettings(); - const value = typeof settings.ocr_gpu_mode === 'string' ? settings.ocr_gpu_mode : ''; - if (value === 'cpu' || value === 'cuda') return value; - return 'auto'; -} - -function getCudaPath(): string { - const settings = readGuiSettings(); - if (typeof settings.cuda_path !== 'string') return ''; - return settings.cuda_path.trim(); -} - -function normalizeCudaPath(candidate: string): string { - const resolved = path.resolve(candidate.trim()); - if (findCudaRuntimeDll(resolved)) return resolved; - return path.basename(resolved).toLowerCase() === 'bin' ? path.dirname(resolved) : resolved; -} - -function findCudaRuntimeDll(directory: string): boolean { - try { - const names = fs.readdirSync(directory); - return names.some(name => /^cudart64.*\.dll$/i.test(name)) - && names.some(name => /^cublas64.*\.dll$/i.test(name)); - } catch { - return false; - } -} - -function validateCudaPath(candidate: string): { valid: boolean; path: string; version: string | null; kind?: 'toolkit' | 'runtime'; error?: string } { - if (!candidate.trim()) return { valid: false, path: '', version: null, error: '路径为空' }; - const cudaRoot = normalizeCudaPath(candidate); - if (!fs.existsSync(cudaRoot)) return { valid: false, path: cudaRoot, version: null, error: '目录不存在' }; - const binDir = path.join(cudaRoot, 'bin'); - const isToolkit = fs.existsSync(path.join(binDir, 'nvcc.exe')); - const runtimeDir = findCudaRuntimeDll(cudaRoot) - ? cudaRoot - : findCudaRuntimeDll(binDir) - ? binDir - : null; - if (!isToolkit && !runtimeDir) { - return { valid: false, path: cudaRoot, version: null, error: '未找到 CUDA Toolkit(bin\\nvcc.exe)或 PyTorch CUDA Runtime DLL' }; - } - - let version: string | null = null; - try { - const versionJson = path.join(cudaRoot, 'version.json'); - if (fs.existsSync(versionJson)) { - const raw = JSON.parse(fs.readFileSync(versionJson, 'utf-8').replace(/^\uFEFF/, '')) as Record; - version = raw.cuda?.version ?? raw.cuda_cudart?.version ?? null; - } - } catch { /* use directory name fallback */ } - version ??= path.basename(cudaRoot).match(/v\d+(?:\.\d+)?/i)?.[0] ?? null; - if (isToolkit) return { valid: true, path: cudaRoot, version, kind: 'toolkit' }; - - let runtimeVersion: string | null = null; - try { - const cudart = fs.readdirSync(runtimeDir!).find(name => /^cudart64.*\.dll$/i.test(name)); - runtimeVersion = cudart?.match(/^cudart64[_-]?(\d+)/i)?.[1] ?? null; - if (runtimeVersion?.length === 2) runtimeVersion = `${runtimeVersion[0]}.${runtimeVersion[1]}`; - else if (runtimeVersion?.length === 3) runtimeVersion = `${runtimeVersion.slice(0, 2)}.${runtimeVersion[2]}`; - } catch { /* version remains unknown */ } - return { valid: true, path: runtimeDir!, version: runtimeVersion, kind: 'runtime' }; -} - -function getSaveBackendScreenshots(): boolean { - const settings = readGuiSettings(); - return settings.save_backend_screenshots === true; +import { + initBackend, + getBackendProcess, + startBackend, + stopBackend, + runSetupScript, +} from './services/BackendService'; +import { AppPaths } from './services/AppPaths'; +import { AtomicFileStore } from './services/AtomicFileStore'; +import { GuiSettingsStore } from './services/GuiSettingsStore'; +import { SafePathService } from './services/SafePathService'; +import { SecureFileService } from './services/SecureFileService'; +import { WindowService } from './services/WindowService'; +import { UserDataMigrationService } from './services/UserDataMigrationService'; +import { LegacyPlanMigration } from './services/LegacyPlanMigration'; +import { + MigrationConflictService, +} from './services/MigrationConflictService'; +import { + mergeLegacyMigrationSummaries, +} from './services/LegacyMigrationSummary'; +import { + buildLegacyMigrationNotice, +} from './services/LegacyMigrationNotice'; +import { + TeamPlanCodec, + type UserTeamPlan, +} from './services/TeamPlanCodec'; +import { TeamPlanRepository } from './services/TeamPlanRepository'; +import { TeamPlanService } from './services/TeamPlanService'; +import { CombatPlanCodec } from './services/CombatPlanCodec'; +import { CombatPlanRepository } from './services/CombatPlanRepository'; +import { RuntimePlanService } from './services/RuntimePlanService'; +import { PlanManagementService } from './services/PlanManagementService'; +import { PlanExportService } from './services/PlanExportService'; +import { TaskPresetCodec } from './services/TaskPresetCodec'; +import { DailyPlanService } from './services/DailyPlanService'; +import { ShipLibraryService } from './services/ShipLibraryService'; +import { ShipLibraryUpdater } from './services/ShipLibraryUpdater'; +import { AdbService } from './services/AdbService'; +import { CudaEnvironmentService } from './services/CudaEnvironmentService'; +import { GuiConfigurationService } from './services/GuiConfigurationService'; +import { PythonEnvironmentService } from './services/PythonEnvironmentService'; +import { registerBackendIpc } from './ipc/BackendIpc'; +import { registerCombatPlanIpc } from './ipc/CombatPlanIpc'; +import { registerConfigurationIpc } from './ipc/ConfigurationIpc'; +import { registerDailyPlanIpc } from './ipc/DailyPlanIpc'; +import { registerDeviceIpc } from './ipc/DeviceIpc'; +import { registerEnvironmentIpc } from './ipc/EnvironmentIpc'; +import { registerFileIpc } from './ipc/FileIpc'; +import { + registerMigrationConflictIpc, +} from './ipc/MigrationConflictIpc'; +import { registerShipLibraryIpc } from './ipc/ShipLibraryIpc'; +import { registerTeamPlanIpc } from './ipc/TeamPlanIpc'; +import { registerUpdaterIpc } from './ipc/UpdaterIpc'; + +/** 启动终端关闭输出管道时,不让 EPIPE 终止 GUI 主进程。 */ +function ignoreBrokenPipe(stream: NodeJS.WriteStream): void { + stream.on('error', (error: NodeJS.ErrnoException) => { + if (error.code !== 'EPIPE') throw error; + }); } -let mainWindow: BrowserWindow | null = null; - -/** 是否处于打包后的生产模式 */ -function isPackaged(): boolean { - return app.isPackaged; -} +ignoreBrokenPipe(process.stdout); +ignoreBrokenPipe(process.stderr); + +const appPaths = new AppPaths({ + moduleDirectory: __dirname, + isPackaged: () => app.isPackaged, + getPath: name => app.getPath(name), + getResourcesPath: () => process.resourcesPath, +}); +const atomicFileStore = new AtomicFileStore(); +const userDataMigrationService = new UserDataMigrationService( + appPaths, + atomicFileStore, +); +const migrationConflictService = new MigrationConflictService( + appPaths, + atomicFileStore, +); +const legacyUserDataMigration = ( + userDataMigrationService.migrateLegacyUserDataFiles() +); +const guiSettingsStore = new GuiSettingsStore( + () => path.join(appPaths.userDataRoot(), 'gui_settings.json'), +); +const safePathService = new SafePathService(appPaths); +const secureFileService = new SecureFileService(safePathService); +const teamPlanCodec = new TeamPlanCodec(); +const teamPlanRepository = new TeamPlanRepository( + appPaths, + atomicFileStore, + teamPlanCodec, +); +const teamPlanService = new TeamPlanService( + teamPlanCodec, + teamPlanRepository, +); +const combatPlanRepository = new CombatPlanRepository( + appPaths, + atomicFileStore, +); +const combatPlanCodec = new CombatPlanCodec( + teamPlanCodec, + teamPlanRepository, +); +const taskPresetCodec = new TaskPresetCodec(); +const dailyPlanService = new DailyPlanService( + appPaths, + atomicFileStore, + combatPlanCodec, + taskPresetCodec, +); +const runtimePlanService = new RuntimePlanService( + combatPlanCodec, + combatPlanRepository, + atomicFileStore, + { + getTempDirectory: () => app.getPath('temp'), + processId: process.pid, + }, +); +const planManagementService = new PlanManagementService( + combatPlanCodec, + combatPlanRepository, + runtimePlanService, + teamPlanRepository, + guiSettingsStore, + taskPresetCodec, +); +const planExportService = new PlanExportService( + combatPlanRepository, + teamPlanRepository, +); +const shipLibraryService = new ShipLibraryService(appPaths, { + processId: process.pid, +}); +const adbService = new AdbService(appPaths); +const cudaEnvironmentService = new CudaEnvironmentService( + CudaEnvironmentService.createDependencies(findPython), +); +const guiConfigurationService = new GuiConfigurationService( + guiSettingsStore, + { + clearPythonCache, + normalizeCudaPath: candidate => ( + cudaEnvironmentService.normalizePath(candidate) + ), + environmentPort: () => process.env.AUTOWSGR_PORT, + }, +); +const pythonEnvironmentService = new PythonEnvironmentService( + PythonEnvironmentService.createDependencies({ + isAllowedVersion: isAllowedPythonVersion, + findPython, + checkEnvironment, + installDependencies, + installPortablePython, + }), +); +const BACKEND_PORT = guiConfigurationService.backendPort(); +const windowService = new WindowService(guiSettingsStore, { + backendPort: BACKEND_PORT, + moduleDirectory: __dirname, + createBrowserWindow: options => new BrowserWindow(options), + getDisplays: () => screen.getAllDisplays(), + getAppPath: () => app.getAppPath(), + isPackaged: () => appPaths.isPackaged(), + resourceRoot: () => appPaths.resourceRoot(), + showMessageBox: options => { + void dialog.showMessageBox(options); + }, +}); +const shipLibraryUpdater = new ShipLibraryUpdater( + shipLibraryService, + { + findPython, + appRoot, + sendProgress: message => { + windowService.getMainWindow()?.webContents.send( + 'ship-library-update-progress', + { message }, + ); + }, + }, +); -/** - * 应用工作目录(外部可写文件:autowsgr/、usersettings.yaml 等): - * - 开发模式: 项目根目录 - * - 打包模式: exe 所在目录 - */ +/** 返回开发项目根目录或打包后的 exe 目录。 */ function appRoot(): string { - if (isPackaged()) { - return path.dirname(app.getPath('exe')); - } - return path.join(__dirname, '..', '..'); + return appPaths.appRoot(); } -/** extraResources 目录 (resource/, plans/, setup.bat) */ +/** 返回包含 resource 和 setup.bat 的 extraResources 目录。 */ function resourceRoot(): string { - if (isPackaged()) { - return process.resourcesPath; - } - return path.join(__dirname, '..', '..'); + return appPaths.resourceRoot(); } -/** 将相对路径解析为绝对路径 */ -function resolveAppPath(filePath: string): string { - if (path.isAbsolute(filePath)) return filePath; - // resource/ 在打包后位于 extraResources(只读) - if (filePath.startsWith('resource')) { - return path.join(resourceRoot(), filePath); - } - // plans/ 及其他文件在 appRoot(可写,用户数据不会被覆盖安装覆盖) - return path.join(appRoot(), filePath); -} - -/** - * 初始化用户方案目录:将 extraResources 中的默认方案 - * 复制到 appRoot/plans(不覆盖已有文件,保留用户自定义方案)。 - */ -function initUserPlansDir(): void { - const bundledDir = path.join(resourceRoot(), 'plans'); - const userDir = path.join(appRoot(), 'plans'); - if (!fs.existsSync(bundledDir)) return; - copyDirNoOverwrite(bundledDir, userDir); -} - -/** 递归复制目录,跳过已存在的文件 */ -function copyDirNoOverwrite(src: string, dest: string): void { - if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); - for (const entry of fs.readdirSync(src, { withFileTypes: true })) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); - if (entry.isDirectory()) { - copyDirNoOverwrite(srcPath, destPath); - } else if (!fs.existsSync(destPath)) { - fs.copyFileSync(srcPath, destPath); - } - } -} - -function createWindow(): BrowserWindow { - const win = new BrowserWindow({ - width: 1280, - height: 720, - minWidth: 960, - minHeight: 540, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true, - nodeIntegration: false, - }, - titleBarStyle: 'hiddenInset', - backgroundColor: '#1a1a2e', - icon: path.join(isPackaged() ? process.resourcesPath : path.join(__dirname, '..', '..'), 'resource', 'images', 'logo.png'), - }); - - const appDir = app.getAppPath(); - const htmlPath = path.join(appDir, 'src', 'view', 'index.html'); - - // 根据 BACKEND_PORT 动态注入 CSP - win.webContents.session.webRequest.onHeadersReceived((details, callback) => { - callback({ - responseHeaders: { - ...details.responseHeaders, - 'Content-Security-Policy': [ - `default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' http://localhost:${BACKEND_PORT} ws://localhost:${BACKEND_PORT}` - ], - }, - }); - }); - - win.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { - const msg = `Page load failed!\nCode: ${errorCode}\nDesc: ${errorDescription}\nURL: ${validatedURL}\nPath: ${htmlPath}`; - console.error('[Main]', msg); - if (isPackaged()) { - dialog.showMessageBox({ type: 'error', title: 'Load Error', message: msg }); - } - }); - - win.loadFile(htmlPath).catch(err => { - console.error('[Main] loadFile failed:', err); - if (isPackaged()) { - dialog.showMessageBox({ type: 'error', title: 'loadFile Error', message: `${err.message}\nPath: ${htmlPath}` }); - } - }); - - mainWindow = win; - win.on('closed', () => { mainWindow = null; }); - return win; +/** 返回 Electron userData 根目录。 */ +function userDataRoot(): string { + return appPaths.userDataRoot(); } -// ════════════════════════════════════════ -// IPC Handlers -// ════════════════════════════════════════ - -ipcMain.handle('open-directory-dialog', async (_event, title?: string) => { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], - title: title || '选择文件夹', - }); - if (result.canceled || result.filePaths.length === 0) return null; - return result.filePaths[0]; -}); - -ipcMain.handle('open-file-dialog', async (_event, filters: Electron.FileFilter[], defaultDir?: string) => { - const result = await dialog.showOpenDialog({ - properties: ['openFile'], - defaultPath: defaultDir || undefined, - filters, - }); - - if (result.canceled || result.filePaths.length === 0) { - return null; - } - - const filePath = result.filePaths[0]; - const content = fs.readFileSync(filePath, 'utf-8'); - return { path: filePath, content }; -}); - -ipcMain.handle('save-file', async (_event, filePath: string, content: string) => { - const resolved = resolveAppPath(filePath); - const dir = path.dirname(resolved); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(resolved, content, 'utf-8'); -}); - -ipcMain.handle('save-file-dialog', async (_event, defaultName: string, content: string, filters: Electron.FileFilter[]) => { - const result = await dialog.showSaveDialog({ - defaultPath: defaultName, // caller can pass full path (dir + filename) - filters, - }); - if (result.canceled || !result.filePath) return null; - fs.writeFileSync(result.filePath, content, 'utf-8'); - return result.filePath; -}); - -ipcMain.handle('read-file', async (_event, filePath: string) => { - const resolved = resolveAppPath(filePath); - if (!fs.existsSync(resolved)) return ''; - return fs.readFileSync(resolved, 'utf-8'); -}); - -ipcMain.handle('append-file', async (_event, filePath: string, content: string) => { - const resolved = resolveAppPath(filePath); - const dir = path.dirname(resolved); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - fs.appendFileSync(resolved, content, 'utf-8'); -}); - - -ipcMain.handle('detect-emulator', async () => { - return detectEmulator(); -}); - -ipcMain.handle('check-adb-devices', async () => { - const adbDir = path.join(appRoot(), 'adb'); - const adbExe = path.join(adbDir, 'adb.exe'); - const adbCmd = fs.existsSync(adbExe) ? adbExe : 'adb'; - try { - const { stdout } = await execAsync(`"${adbCmd}" devices`, { windowsHide: true, timeout: 5000 }); - const lines = stdout.split('\n').slice(1); // skip header - return lines - .map(l => l.trim()) - .filter(l => l.length > 0) - .map(l => { - const [serial, status] = l.split(/\s+/); - return { serial, status: status || 'unknown' }; - }); - } catch { - return []; - } -}); - -ipcMain.on('get-app-version-sync', (event) => { - event.returnValue = app.getVersion(); -}); - -ipcMain.on('get-backend-port-sync', (event) => { - event.returnValue = BACKEND_PORT; -}); - -ipcMain.on('get-backend-startup-mode-sync', (event) => { - event.returnValue = getBackendStartupMode(); -}); - -ipcMain.on('get-backend-repo-path-sync', (event) => { - event.returnValue = getBackendRepoPath(); -}); - -ipcMain.on('get-ocr-gpu-mode-sync', (event) => { - event.returnValue = getOcrGpuMode(); -}); - -ipcMain.on('get-cuda-path-sync', (event) => { - event.returnValue = getCudaPath(); -}); - -ipcMain.on('get-save-backend-screenshots-sync', (event) => { - event.returnValue = getSaveBackendScreenshots(); -}); - -ipcMain.handle('set-backend-port', (_event, port: number) => { - // 防御性校验:仅在端口为有限数值且位于合法范围时才写入设置 - if (typeof port !== 'number' || !Number.isFinite(port)) { - return; - } - const normalizedPort = Math.trunc(port); - if (normalizedPort < 1 || normalizedPort > 65535) { - return; - } - writeGuiSettings({ backend_port: normalizedPort }); -}); - -ipcMain.handle('set-backend-startup-mode', (_event, mode: BackendStartupMode) => { - const normalized = mode === 'external' ? 'external' : 'managed'; - writeGuiSettings({ backend_startup_mode: normalized }); -}); - -ipcMain.handle('set-backend-repo-path', (_event, repoPath: string | null) => { - const normalized = typeof repoPath === 'string' ? repoPath.trim() : ''; - writeGuiSettings({ backend_repo_path: normalized }); -}); - -ipcMain.handle('set-ocr-gpu-mode', (_event, mode: OcrGpuMode) => { - const normalized: OcrGpuMode = mode === 'cpu' || mode === 'cuda' ? mode : 'auto'; - writeGuiSettings({ ocr_gpu_mode: normalized }); -}); - -ipcMain.handle('set-cuda-path', (_event, cudaPath: string | null) => { - const raw = typeof cudaPath === 'string' ? cudaPath.trim() : ''; - const normalized = raw ? normalizeCudaPath(raw) : ''; - writeGuiSettings({ cuda_path: normalized }); -}); - -ipcMain.handle('validate-cuda-path', (_event, cudaPath: string) => { - return validateCudaPath(cudaPath); -}); - -ipcMain.handle('set-save-backend-screenshots', (_event, enabled: boolean) => { - writeGuiSettings({ save_backend_screenshots: enabled === true }); -}); - -ipcMain.on('get-python-path-sync', (event) => { - event.returnValue = getConfiguredPythonPath(); -}); - -ipcMain.on('get-update-mode-sync', (event) => { - event.returnValue = getUpdateMode(); -}); - -ipcMain.handle('set-update-mode', (_event, mode: 'auto' | 'manual') => { - const normalized = mode === 'manual' ? 'manual' : 'auto'; - writeGuiSettings({ update_mode: normalized }); -}); - -ipcMain.handle('set-python-path', (_event, pythonPath: string | null) => { - writeGuiSettings({ python_path: pythonPath ?? '' }); - clearPythonCache(); // 清除缓存,下次查找时使用新路径 -}); - -ipcMain.handle('validate-python', async (_event, pythonPath: string) => { - if (!pythonPath) return { valid: false, version: null, error: '路径为空' }; - if (!fs.existsSync(pythonPath)) return { valid: false, version: null, error: '文件不存在' }; - try { - const { stdout } = await execAsync(`"${pythonPath}" --version`, { windowsHide: true, timeout: 10000 }); - const version = stdout.trim(); - if (!isAllowedPythonVersion(version)) { - return { valid: false, version, error: `版本不兼容: ${version}(需要 3.12 或 3.13)` }; - } - return { valid: true, version }; - } catch (e) { - return { valid: false, version: null, error: `执行失败: ${e instanceof Error ? e.message : String(e)}` }; - } -}); - -ipcMain.handle('get-app-root', () => { - return appRoot(); -}); - -ipcMain.handle('resolve-app-path', (_event, filePath: string) => { - return resolveAppPath(filePath); -}); - -ipcMain.handle('get-plans-dir', () => { - return resolveAppPath('plans'); -}); - -ipcMain.handle('list-plan-files', () => { - const dir = resolveAppPath('plans'); - if (!fs.existsSync(dir)) return []; - return fs.readdirSync(dir) - .filter(f => /\.ya?ml$/i.test(f)) - .map(f => ({ name: f.replace(/\.ya?ml$/i, ''), file: f })); -}); - -ipcMain.handle('get-config-dir', () => { - return appRoot(); -}); - -ipcMain.handle('open-folder', async (_event, folderPath: string) => { - if (fs.existsSync(folderPath)) { - await shell.openPath(folderPath); - } -}); - -ipcMain.handle('check-environment', async () => { - return await checkEnvironment(); -}); - -/* - * 测试期接口(后端源码更新)已停用,逻辑保留便于回滚恢复。 -ipcMain.handle('check-updates', async () => { - return await checkForUpdates(); -}); -*/ - -ipcMain.handle('install-deps', async () => { - const pythonCmd = await findPython(); - if (!pythonCmd) return { success: false, output: '找不到 Python' }; - return installDependencies(pythonCmd); -}); - -ipcMain.handle('run-setup', async () => { - return runSetupScript(); -}); - -ipcMain.handle('install-portable-python', async () => { - return installPortablePython(); -}); - -/* - * 测试期接口(后端源码更新)已停用,逻辑保留便于回滚恢复。 -ipcMain.handle('pull-updates', async () => { - return pullUpdates(); -}); -*/ - -ipcMain.handle('start-backend', async () => { - if (getBackendProcess()) return { success: true, message: '后端已在运行' }; - await startBackend(); - return { success: true, message: '后端启动中' }; -}); - -// ════════════════════════════════════════ -// GUI 自动更新 (electron-updater) -// ════════════════════════════════════════ - -/** 初始化自动更新 */ -function initAutoUpdater(): void { - autoUpdater.autoDownload = false; - autoUpdater.autoInstallOnAppQuit = true; - - autoUpdater.on('update-available', (info: UpdateInfo) => { - mainWindow?.webContents.send('update-status', { - status: 'available', - version: info.version, - releaseNotes: typeof info.releaseNotes === 'string' ? info.releaseNotes : '', - }); - }); - - autoUpdater.on('update-not-available', () => { - mainWindow?.webContents.send('update-status', { status: 'up-to-date' }); - }); - - autoUpdater.on('download-progress', (progress: ProgressInfo) => { - mainWindow?.webContents.send('update-status', { - status: 'downloading', - percent: Math.round(progress.percent), - transferred: progress.transferred, - total: progress.total, - }); - }); - - autoUpdater.on('update-downloaded', (info: UpdateInfo) => { - mainWindow?.webContents.send('update-status', { - status: 'downloaded', - version: info.version, - }); - }); - - autoUpdater.on('error', (err: Error) => { - mainWindow?.webContents.send('update-status', { - status: 'error', - message: err.message, - }); - }); -} - -ipcMain.handle('check-gui-updates', async () => { - try { - const result = await autoUpdater.checkForUpdates(); - return result?.updateInfo ? { version: result.updateInfo.version } : null; - } catch { - return null; - } -}); - -ipcMain.handle('download-gui-update', async () => { - try { - await autoUpdater.downloadUpdate(); - return { success: true }; - } catch (err: any) { - return { success: false, message: err.message }; - } -}); - -ipcMain.handle('install-gui-update', () => { - autoUpdater.quitAndInstall(false, true); -}); - -/** 向渲染进程发送环境检查进度 */ +// IPC 注册 + +registerFileIpc(ipcMain, { + dialog, + shell, + secureFiles: secureFileService, + safePaths: safePathService, + combatPlans: combatPlanRepository, + appRoot, + userDataRoot, +}); +registerDeviceIpc(ipcMain, { + adb: adbService, + detectEmulator, +}); +registerConfigurationIpc(ipcMain, { + getAppVersion: () => app.getVersion(), + backendPort: BACKEND_PORT, + configuration: guiConfigurationService, + cudaEnvironment: cudaEnvironmentService, + pythonEnvironment: pythonEnvironmentService, + windows: windowService, +}); +registerDailyPlanIpc(ipcMain, { + dailyPlans: dailyPlanService, + configuration: guiConfigurationService, +}); +registerMigrationConflictIpc(ipcMain, migrationConflictService); +registerEnvironmentIpc(ipcMain, pythonEnvironmentService); +registerTeamPlanIpc(ipcMain, { + dialog, + repository: teamPlanRepository, + service: teamPlanService, +}); +registerCombatPlanIpc(ipcMain, { + dialog, + safePaths: safePathService, + plans: planManagementService, + planExports: planExportService, +}); +registerShipLibraryIpc(ipcMain, { + library: shipLibraryService, + updater: shipLibraryUpdater, +}); +registerBackendIpc(ipcMain, { + getBackendProcess, + startBackend, + runSetupScript, +}); + +const legacyPlanMigration = new LegacyPlanMigration( + appPaths, + atomicFileStore, + userDataMigrationService, + { + yamlFiles: directory => combatPlanRepository.yamlFiles(directory), + safePlanBaseName: value => combatPlanCodec.safeBaseName(value), + normalizeUserTeamPlan: raw => teamPlanCodec.normalize(raw), + teamPlanMatches: (filePath, team) => ( + teamPlanRepository.matches(filePath, team) + ), + teamName: team => team.name, + renameTeam: (team, name) => ({ + ...structuredClone(team), + name, + }), + normalizeCombatPlanFleetPresets: ( + root, + source, + requireEmbeddedShips, + ) => combatPlanCodec.normalizeFleetPresets( + root, + source, + requireEmbeddedShips, + ), + buildTeamPlanWrites: (teams, directory) => ( + teamPlanRepository.buildWrites(teams, directory) + ), + serializeCombatPlan: (root, originalContent) => ( + combatPlanCodec.serialize(root, originalContent) + ), + isStandaloneTaskPreset: root => ( + taskPresetCodec.isStandalone(root) + ), + normalizeTaskPreset: root => taskPresetCodec.normalize(root), + }, +); + +/** 向渲染进程发送环境检查进度。 */ function sendProgress(msg: string): void { - mainWindow?.webContents.send('backend-log', msg); + windowService.getMainWindow()?.webContents.send('backend-log', msg); } -// ════════════════════════════════════════ -// App Lifecycle -// ════════════════════════════════════════ +// 应用生命周期 app.whenReady().then(() => { initPythonEnv({ appRoot, sendProgress, - getConfiguredPythonPath, - getUpdateMode, + getConfiguredPythonPath: () => ( + guiConfigurationService.configuredPythonPath() + ), + getUpdateMode: () => guiConfigurationService.updateMode(), + getBackendStartupMode: () => ( + guiConfigurationService.backendStartupMode() + ), + getBackendRepoPath: () => ( + guiConfigurationService.backendRepoPath() + ), getTempDir: () => app.getPath('temp'), }); initBackend({ appRoot, + userDataRoot, resourceRoot, BACKEND_PORT, - getMainWindow: () => mainWindow, + getMainWindow: () => windowService.getMainWindow(), + }); + combatPlanRepository.initializeUserDirectory(); + shipLibraryService.initialize(); + teamPlanRepository.initializeUserDirectory(); + const presetInventoryResult = ( + userDataMigrationService.migratePresetInventory() + ); + const legacyPlanResult = legacyPlanMigration.migrate(); + const legacyMigrationResult = mergeLegacyMigrationSummaries( + legacyUserDataMigration, + legacyPlanResult, + presetInventoryResult, + ); + migrationConflictService.prepareAfterMigration( + legacyMigrationResult.total > 0, + ); + registerUpdaterIpc(ipcMain, { + getMainWindow: () => windowService.getMainWindow(), + getAppVersion: () => app.getVersion(), + stopBackend, }); - initUserPlansDir(); - initAutoUpdater(); - createWindow(); + windowService.createWindow(); + const migrationNotice = buildLegacyMigrationNotice( + legacyMigrationResult, + ); + const mainWindow = windowService.getMainWindow(); + if (migrationNotice && mainWindow) { + void dialog.showMessageBox(mainWindow, migrationNotice); + } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); + windowService.createWindow(); } }); }); -app.on('before-quit', () => { - stopBackend(); +let backendShutdownInProgress = false; + +app.on('before-quit', (event) => { + windowService.captureWindowBounds(); + windowService.persistWindowBounds(); + if (backendShutdownInProgress) return; + if (getBackendProcess()) { + backendShutdownInProgress = true; + event.preventDefault(); + void stopBackend().then(() => { + backendShutdownInProgress = false; + app.quit(); + }).catch(error => { + backendShutdownInProgress = false; + const message = error instanceof Error + ? error.message + : String(error); + console.error('[Backend] 无法安全退出:', message); + dialog.showErrorBox( + '无法安全退出', + `后端进程仍在运行,应用没有退出:${message}`, + ); + }); + } }); app.on('window-all-closed', () => { diff --git a/electron/preload.ts b/electron/preload.ts index b75d86c..fa24161 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,7 +1,17 @@ /** - * Preload 脚本 —— 通过 contextBridge 安全暴露 IPC 方法给渲染进程。 + * 通过 contextBridge 向渲染进程安全暴露 IPC 方法。 */ import { contextBridge, ipcRenderer } from 'electron'; +import type { + LootAutomationPlan, + LootPlanSource, +} from '../src/shared/lootPlans'; +import type { + LegacyDecisiveAutomationSettings, +} from '../src/shared/legacyDecisiveAutomation'; +import type { + DecisiveAutomationSource, +} from '../src/shared/decisiveAutomation'; contextBridge.exposeInMainWorld('electronBridge', { getAppVersion: () => { @@ -32,6 +42,62 @@ contextBridge.exposeInMainWorld('electronBridge', { return ipcRenderer.sendSync('get-save-backend-screenshots-sync') as boolean; }, + getWindowPreferences: () => { + return ipcRenderer.sendSync('get-window-preferences-sync') as { + defaultWidth: number; + defaultHeight: number; + rememberBounds: boolean; + }; + }, + + setWindowPreferences: (preferences: { + defaultWidth: number; + defaultHeight: number; + rememberBounds: boolean; + }) => { + return ipcRenderer.invoke('set-window-preferences', preferences); + }, + + getGuiAutomationSettings: () => { + return ipcRenderer.invoke('get-gui-automation-settings'); + }, + + setGuiAutomationSettings: (settings: { + expeditionInterval: number; + battleTimes: number; + autoDecisive: boolean; + decisiveTemplateId: DecisiveAutomationSource; + autoLoot: boolean; + lootPlanSource: LootPlanSource; + lootPlanId: string; + lootPlans: LootAutomationPlan[]; + lootStopCount: number; + }) => { + return ipcRenderer.invoke('set-gui-automation-settings', settings); + }, + + migrateLegacyDecisiveAutomation: ( + settings: LegacyDecisiveAutomationSettings, + ) => { + return ipcRenderer.invoke( + 'migrate-legacy-decisive-automation', + settings, + ); + }, + + getDecisivePlanSettings: () => { + return ipcRenderer.invoke('get-decisive-plan-settings'); + }, + + setDecisivePlanSettings: (settings: { + chapter: number; + useQuickRepair: boolean; + level1: string[]; + level2: string[]; + }) => { + return ipcRenderer.invoke('set-decisive-plan-settings', settings); + }, + setBackendPort: (port: number) => { return ipcRenderer.invoke('set-backend-port', port); }, @@ -68,6 +134,165 @@ contextBridge.exposeInMainWorld('electronBridge', { return ipcRenderer.invoke('set-update-mode', mode); }, + getShipLibraryStatus: () => { + return ipcRenderer.invoke('get-ship-library-status'); + }, + + getShipLibraryManifest: () => { + return ipcRenderer.invoke('get-ship-library-manifest'); + }, + + updateShipLibrary: () => { + return ipcRenderer.invoke('update-ship-library'); + }, + + onShipLibraryUpdateProgress: (callback: (progress: { message: string }) => void) => { + ipcRenderer.on('ship-library-update-progress', (_event, progress) => callback(progress)); + }, + + saveUserTeamPlan: ( + plan: unknown, + overwrite = false, + currentFile?: string, + source: 'system' | 'user' = 'user', + ) => { + return ipcRenderer.invoke( + 'save-user-team-plan', + plan, + overwrite, + currentFile, + source, + ); + }, + + pickUserTeamPlan: () => { + return ipcRenderer.invoke('pick-user-team-plan'); + }, + + listTeamPlans: () => { + return ipcRenderer.invoke('list-team-plans'); + }, + + getPlanManagement: () => { + return ipcRenderer.invoke('get-plan-management'); + }, + + listDailyPlans: () => { + return ipcRenderer.invoke('list-daily-plans'); + }, + + readDailyPlan: ( + source: 'system' | 'user', + file: string, + ) => { + return ipcRenderer.invoke('read-daily-plan', source, file); + }, + + getDailyDecisivePlan: (chapter: number) => { + return ipcRenderer.invoke('get-daily-decisive-plan', chapter); + }, + + getSystemDailyDecisivePlan: (chapter: number) => { + return ipcRenderer.invoke( + 'get-system-daily-decisive-plan', + chapter, + ); + }, + + saveDailyDecisivePlan: (settings: { + chapter: number; + useQuickRepair: boolean; + level1: string[]; + level2: string[]; + }) => { + return ipcRenderer.invoke('save-daily-decisive-plan', settings); + }, + + getMigrationConflicts: () => { + return ipcRenderer.invoke('get-migration-conflicts'); + }, + + resolveMigrationConflicts: (keepIds: string[]) => { + return ipcRenderer.invoke('resolve-migration-conflicts', keepIds); + }, + + exportUserPlans: ( + selections: Array<{ + kind: 'battle' | 'team'; + file: string; + }>, + ) => { + return ipcRenderer.invoke('export-user-plans', selections); + }, + + importLocalCombatPlan: () => { + return ipcRenderer.invoke('import-local-combat-plan'); + }, + + setPlanUnlinkedIgnored: ( + kind: 'battle' | 'team', + source: 'system' | 'user', + file: string, + ignored: boolean, + ) => { + return ipcRenderer.invoke( + 'set-plan-unlinked-ignored', + kind, + source, + file, + ignored, + ); + }, + + readManagedCombatPlan: ( + source: 'system' | 'user', + file: string, + ) => { + return ipcRenderer.invoke('read-managed-combat-plan', source, file); + }, + + readCombatPlanFile: (filePath: string) => { + return ipcRenderer.invoke('read-combat-plan-file', filePath); + }, + + prepareCombatPlanExecution: ( + content: string, + hint: string, + ) => { + return ipcRenderer.invoke( + 'prepare-combat-plan-execution', + content, + hint, + ); + }, + + saveManagedCombatPlan: ( + name: string, + content: string, + overwrite = false, + currentFile?: string, + ) => { + return ipcRenderer.invoke( + 'save-managed-combat-plan', + name, + content, + overwrite, + currentFile, + ); + }, + + renameUserCombatPlan: (file: string, newName: string) => { + return ipcRenderer.invoke('rename-user-combat-plan', file, newName); + }, + + deleteUserCombatPlan: (file: string) => { + return ipcRenderer.invoke('delete-user-combat-plan', file); + }, + + deleteUserTeamPlan: (file: string) => { + return ipcRenderer.invoke('delete-user-team-plan', file); + }, + openDirectoryDialog: (title?: string) => { return ipcRenderer.invoke('open-directory-dialog', title); }, @@ -100,6 +325,14 @@ contextBridge.exposeInMainWorld('electronBridge', { return ipcRenderer.invoke('check-adb-devices'); }, + connectAdbDevice: (serial: string) => { + return ipcRenderer.invoke('connect-adb-device', serial); + }, + + disconnectAdbDevice: (serial: string) => { + return ipcRenderer.invoke('disconnect-adb-device', serial); + }, + getAppRoot: () => { return ipcRenderer.invoke('get-app-root'); }, @@ -128,24 +361,10 @@ contextBridge.exposeInMainWorld('electronBridge', { return ipcRenderer.invoke('check-environment'); }, - /* - * 测试期接口(后端源码更新)已停用,逻辑保留便于回滚恢复。 - checkUpdates: () => { - return ipcRenderer.invoke('check-updates'); - }, - */ - installDeps: () => { return ipcRenderer.invoke('install-deps'); }, - /* - * 测试期接口(后端源码更新)已停用,逻辑保留便于回滚恢复。 - pullUpdates: () => { - return ipcRenderer.invoke('pull-updates'); - }, - */ - startBackend: () => { return ipcRenderer.invoke('start-backend'); }, @@ -158,7 +377,7 @@ contextBridge.exposeInMainWorld('electronBridge', { return ipcRenderer.invoke('install-portable-python'); }, - // ── Python 路径配置 ── + // Python 路径配置 getPythonPath: () => { return ipcRenderer.sendSync('get-python-path-sync') as string | null; }, @@ -171,7 +390,7 @@ contextBridge.exposeInMainWorld('electronBridge', { return ipcRenderer.invoke('validate-python', pythonPath); }, - // ── GUI 自动更新 ── + // GUI 自动更新 checkGuiUpdates: () => { return ipcRenderer.invoke('check-gui-updates'); }, diff --git a/electron/pythonEnv/backendContractProbe.ts b/electron/pythonEnv/backendContractProbe.ts new file mode 100644 index 0000000..c2a0072 --- /dev/null +++ b/electron/pythonEnv/backendContractProbe.ts @@ -0,0 +1,76 @@ +/** + * 生成 AutoWSGR GUI 运行契约的隔离行为探针。 + * + * 探针只在短生命周期的 Python 检查进程中替换外部依赖,验证 + * Launcher 公共方法是否真正消费 GUI 环境变量,不修改实际后端进程。 + */ +export function buildBackendRuntimeContractProbeLines( + functionName = '_verify_gui_runtime_contract', +): string[] { + return [ + `def ${functionName}():`, + ' import os as _contract_os', + ' from types import SimpleNamespace as _ContractNamespace', + ' from unittest.mock import patch as _contract_patch', + ' from autowsgr.scheduler.launcher import Launcher as _ContractLauncher', + ' _log = _ContractNamespace(', + " dir='.',", + " level='INFO',", + ' effective_channels=[],', + ' )', + ' _ocr = _ContractNamespace(', + ' gpu=False,', + " mirror='modelscope',", + ' ship_name_match_confidence=0.0,', + ' ship_name_corrections={},', + ' ship_name_aliases={},', + ' )', + ' _config = _ContractNamespace(log=_log, ocr=_ocr)', + " _save_key = 'AUTOWSGR_SAVE_IMAGES'", + " _ocr_key = 'AUTOWSGR_OCR_GPU_MODE'", + ' _previous_save = _contract_os.environ.get(_save_key)', + ' _previous_ocr = _contract_os.environ.get(_ocr_key)', + ' try:', + ' _save_values = []', + ' def _capture_logger(*args, **kwargs):', + " _save_values.append(kwargs.get('save_images'))", + ' with _contract_patch(', + " 'autowsgr.scheduler.launcher.ConfigManager.load',", + ' return_value=_config,', + ' ), _contract_patch(', + " 'autowsgr.scheduler.launcher.setup_logger',", + ' side_effect=_capture_logger,', + ' ):', + " _contract_os.environ[_save_key] = 'true'", + ' _ContractLauncher().load_config()', + " _contract_os.environ[_save_key] = 'false'", + ' _ContractLauncher().load_config()', + ' if _save_values != [True, False]:', + " raise RuntimeError('AUTOWSGR_SAVE_IMAGES 行为不兼容')", + ' _ocr_values = []', + ' def _capture_ocr(*args, **kwargs):', + " _ocr_values.append(kwargs.get('gpu'))", + ' return object()', + ' _launcher = _ContractLauncher()', + ' _launcher.set_config(_config)', + ' with _contract_patch(', + " 'autowsgr.scheduler.launcher.EasyOCREngine.create',", + ' side_effect=_capture_ocr,', + ' ):', + " _contract_os.environ[_ocr_key] = 'cuda'", + ' _launcher.create_ocr()', + " _contract_os.environ[_ocr_key] = 'cpu'", + ' _launcher.create_ocr()', + ' if _ocr_values != [True, False]:', + " raise RuntimeError('AUTOWSGR_OCR_GPU_MODE 行为不兼容')", + ' finally:', + ' if _previous_save is None:', + ' _contract_os.environ.pop(_save_key, None)', + ' else:', + ' _contract_os.environ[_save_key] = _previous_save', + ' if _previous_ocr is None:', + ' _contract_os.environ.pop(_ocr_key, None)', + ' else:', + ' _contract_os.environ[_ocr_key] = _previous_ocr', + ]; +} diff --git a/electron/pythonEnv/backendRequirement.ts b/electron/pythonEnv/backendRequirement.ts new file mode 100644 index 0000000..3331f43 --- /dev/null +++ b/electron/pythonEnv/backendRequirement.ts @@ -0,0 +1,14 @@ +/** + * GUI 管理模式使用的 AutoWSGR 后端来源。 + * + * 此提交提供 GUI 所需的 OCR 与截图环境变量契约。 + * 安装和自动更新必须共用这里的来源,避免更新后退回不兼容版本。 + */ +export const MANAGED_AUTOWSGR_COMMIT = ( + 'b0f473fb1ec5318c2c4cff4795a804a3d2dd25bd' +); + +export const MANAGED_AUTOWSGR_REQUIREMENT = ( + 'https://github.com/ShiinaKuroko/AutoWSGR/archive/' + + `${MANAGED_AUTOWSGR_COMMIT}.zip` +); diff --git a/electron/pythonEnv/context.ts b/electron/pythonEnv/context.ts index d287ec6..869b42d 100644 --- a/electron/pythonEnv/context.ts +++ b/electron/pythonEnv/context.ts @@ -1,6 +1,5 @@ /** - * Python 环境共享上下文与缓存状态。 - * 由 main.ts 在启动时通过 initPythonEnv() 注入。 + * 保存由 main.ts 注入的 Python 环境上下文和路径缓存。 */ export interface PythonEnvContext { @@ -8,6 +7,8 @@ export interface PythonEnvContext { sendProgress: (msg: string) => void; getConfiguredPythonPath: () => string | null; getUpdateMode: () => 'auto' | 'manual'; + getBackendStartupMode: () => 'managed' | 'external'; + getBackendRepoPath: () => string; getTempDir: () => string; } @@ -17,19 +18,17 @@ export function initPythonEnv(context: PythonEnvContext): void { ctx = context; } -/** 内部访问器:获取已注入的上下文 */ +/** 获取已注入的 Python 环境上下文。 */ export function getCtx(): PythonEnvContext { return ctx; } -// ════════════════════════════════════════ // Python 路径缓存 -// ════════════════════════════════════════ -/** 缓存的 Python 路径 (undefined = 尚未查找) */ +/** 缓存的 Python 路径,undefined 表示尚未查找。 */ let cachedPythonCmd: string | null | undefined; -/** 清除 Python 路径缓存(用户切换路径后调用) */ +/** 清除 Python 路径缓存。 */ export function clearPythonCache(): void { cachedPythonCmd = undefined; } diff --git a/electron/pythonEnv/cuda.ts b/electron/pythonEnv/cuda.ts new file mode 100644 index 0000000..8651bbf --- /dev/null +++ b/electron/pythonEnv/cuda.ts @@ -0,0 +1,104 @@ +/** + * 解析 CUDA 配置并构造 Python 子进程环境。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { + buildPythonProcessEnv, + type PythonEnvironment, +} from './environment'; + +function normalizeCudaRoot(candidate: string): string { + const resolved = path.resolve(candidate.trim()); + if (isCudaRuntimeDirectory(resolved)) return resolved; + return path.basename(resolved).toLowerCase() === 'bin' + ? path.dirname(resolved) + : resolved; +} + +function isCudaRuntimeDirectory(candidate: string): boolean { + try { + const names = fs.readdirSync(candidate); + return names.some(name => /^cudart64.*\.dll$/i.test(name)) + && names.some(name => /^cublas64.*\.dll$/i.test(name)); + } catch { + return false; + } +} + +/** 校验用户配置并返回 CUDA Toolkit 根目录或运行库目录。 */ +export function resolveConfiguredCudaRoot(value: unknown): string | null { + if (typeof value !== 'string' || !value.trim()) return null; + const cudaRoot = normalizeCudaRoot(value); + const binDir = path.join(cudaRoot, 'bin'); + const runtimeDir = isCudaRuntimeDirectory(cudaRoot) + ? cudaRoot + : isCudaRuntimeDirectory(binDir) + ? binDir + : null; + if (!fs.existsSync(path.join(binDir, 'nvcc.exe')) && !runtimeDir) { + return null; + } + return fs.existsSync(path.join(binDir, 'nvcc.exe')) + ? cudaRoot + : runtimeDir; +} + +/** 在同一个 Python 基础环境上叠加 CUDA 运行变量。 */ +export function buildCudaEnvironment( + baseEnv: NodeJS.ProcessEnv, + configuredCudaRoot: string | null, +): NodeJS.ProcessEnv { + if (!configuredCudaRoot) return { ...baseEnv }; + + const cudaRoot = normalizeCudaRoot(configuredCudaRoot); + const isToolkit = fs.existsSync(path.join(cudaRoot, 'bin', 'nvcc.exe')); + const cudaBin = isToolkit ? path.join(cudaRoot, 'bin') : cudaRoot; + const existingPath = baseEnv.PATH || baseEnv.Path || ''; + const pathEntries = existingPath.split(path.delimiter).filter(Boolean); + const withoutDuplicate = pathEntries.filter( + entry => path.resolve(entry).toLowerCase() + !== path.resolve(cudaBin).toLowerCase(), + ); + const env: NodeJS.ProcessEnv = { ...baseEnv }; + for (const key of Object.keys(env)) { + if (key.toLowerCase() === 'path') delete env[key]; + } + if (isToolkit) { + env.CUDA_PATH = cudaRoot; + env.CUDA_HOME = cudaRoot; + } + env.PATH = [cudaBin, ...withoutDuplicate].join(path.delimiter); + + let version: string | null = null; + try { + const versionJson = path.join(cudaRoot, 'version.json'); + if (fs.existsSync(versionJson)) { + const raw = JSON.parse( + fs.readFileSync(versionJson, 'utf-8').replace(/^\uFEFF/, ''), + ) as Record; + version = raw.cuda?.version ?? raw.cuda_cudart?.version ?? null; + } + } catch { + // 版本文件无效时继续使用目录名。 + } + version ??= path.basename(cudaRoot).match(/v(\d+(?:\.\d+)?)/i)?.[1] + ?? null; + const versionMatch = version?.match(/^(\d+)\.(\d+)/); + if (isToolkit && versionMatch) { + env[`CUDA_PATH_V${versionMatch[1]}_${versionMatch[2]}`] = cudaRoot; + } + return env; +} + +/** 构造 CUDA 检测和后端启动共同使用的完整 Python 运行环境。 */ +export function buildBackendRuntimeEnvironment( + environment: PythonEnvironment, + configuredCudaRoot: string | null, + baseEnv: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + return buildCudaEnvironment( + buildPythonProcessEnv(environment, baseEnv), + configuredCudaRoot, + ); +} diff --git a/electron/pythonEnv/dependencies.ts b/electron/pythonEnv/dependencies.ts new file mode 100644 index 0000000..828916a --- /dev/null +++ b/electron/pythonEnv/dependencies.ts @@ -0,0 +1,34 @@ +/** GUI Python 运行时必须可导入的模块。 */ +export const PYTHON_DEPENDENCY_SPECS = Object.freeze([ + { + key: 'uvicorn', + importName: 'uvicorn', + packageName: 'uvicorn', + }, + { + key: 'fastapi', + importName: 'fastapi', + packageName: 'fastapi', + }, + { + key: 'scipy', + importName: 'scipy._lib', + packageName: 'scipy', + }, + { + key: 'requests', + importName: 'requests', + packageName: 'requests', + }, + { + key: 'beautifulSoup', + importName: 'bs4', + packageName: 'beautifulsoup4', + }, +] as const); + +/** 舰船资料库更新器不由 AutoWSGR 后端依赖间接保证的包。 */ +export const SHIP_LIBRARY_REQUIREMENTS: readonly string[] = Object.freeze([ + 'requests>=2.32.5', + 'beautifulsoup4>=4.12.0', +]); diff --git a/electron/pythonEnv/envCheck.ts b/electron/pythonEnv/envCheck.ts index f9b1b6e..a3b80a0 100644 --- a/electron/pythonEnv/envCheck.ts +++ b/electron/pythonEnv/envCheck.ts @@ -1,6 +1,5 @@ /** - * 环境校验主流程。 - * 包括 VC++ 检查、env marker 管理、依赖包验证。 + * 检查 VC++、Python、依赖包和环境就绪标记。 */ import * as path from 'path'; import * as fs from 'fs'; @@ -8,21 +7,42 @@ import { exec } from 'child_process'; import { promisify } from 'util'; import { getCtx, setCachedPythonCmd } from './context'; import { findPython } from './finder'; -import { type EnvCheckResult, ensurePthFile, localSitePackages, pipEnv, ensurePip, ensureSslCertForPython } from './utils'; +import { + type EnvCheckResult, + ensurePthFile, + localSitePackages, + pipEnv, + ensurePip, + ensureSslCertForPython, +} from './utils'; import { autoUpdateAutowsgr, type AutoUpdateDeps } from './updater'; +import { + buildPythonProcessEnv, + type PythonEnvironment, + resolvePythonEnvironment, +} from './environment'; +import { + buildBackendRuntimeContractProbeLines, +} from './backendContractProbe'; +import { PYTHON_DEPENDENCY_SPECS } from './dependencies'; const execAsync = promisify(exec); -// ════════════════════════════════════════ // VC++ Redistributable -// ════════════════════════════════════════ -/** 检查并安装 VC++ Redistributable(c10.dll 等依赖需要) */ +/** 检查并安装 VC++ Redistributable。 */ async function ensureVCRedist(): Promise { const ctx = getCtx(); - // vcruntime140.dll 存在于 system32 说明已安装 - const dllPath = path.join(process.env.SYSTEMROOT || 'C:\\Windows', 'System32', 'vcruntime140.dll'); - if (fs.existsSync(dllPath)) return; + // system32 中存在 vcruntime140.dll 即视为已安装。 + const systemRoot = process.env.SystemRoot + || process.env.SYSTEMROOT + || process.env.WINDIR; + if ( + systemRoot + && fs.existsSync(path.join(systemRoot, 'System32', 'vcruntime140.dll')) + ) { + return; + } ctx.sendProgress('Microsoft Visual C++ Redistributable is not installed, this may lead to the DLL load failure.'); const redistExe = path.join(ctx.appRoot(), 'redist', 'vc_redist.x64.exe'); @@ -40,42 +60,66 @@ async function ensureVCRedist(): Promise { } } -// ════════════════════════════════════════ // 环境就绪标记 (.env_ready) -// ════════════════════════════════════════ -/** 环境就绪标记文件路径 */ +/** 返回环境就绪标记路径。 */ export const ENV_READY_MARKER = () => path.join(getCtx().appRoot(), '.env_ready'); -/** 读取标记文件中保存的 autowsgr 版本;标记不存在或无效时返回 null */ -function readEnvMarker(): { pythonCmd: string; pythonVersion: string; autowsgrVersion: string } | null { +interface EnvironmentMarker { + pythonCmd: string; + pythonVersion: string; + autowsgrVersion: string; + environmentIdentity: string; + environment: PythonEnvironment; +} + +/** 读取环境标记;当前模式、解释器或仓库变化时返回 null。 */ +function readEnvMarker(): EnvironmentMarker | null { const ctx = getCtx(); try { const data = JSON.parse(fs.readFileSync(ENV_READY_MARKER(), 'utf-8')); - if (data && data.pythonCmd && data.autowsgrVersion) { - // 确保记录的 python 路径仍然存在 + if ( + data + && data.pythonCmd + && data.autowsgrVersion + && data.environmentIdentity + ) { + // 标记中的 Python 路径必须仍然存在。 if (!fs.existsSync(data.pythonCmd)) return null; - // 若用户切换了 Python 路径,旧标记自动失效 + // Python 路径变化后旧标记失效。 const configured = ctx.getConfiguredPythonPath(); if (configured && configured !== data.pythonCmd) return null; - return data; + const environment = resolvePythonEnvironment(data.pythonCmd); + if (environment.identity !== data.environmentIdentity) return null; + return { ...data, environment }; } - } catch { /* ignore */ } + } catch { /* 标记缺失或损坏时重新检查。 */ } return null; } -/** 写入环境就绪标记 */ -function writeEnvMarker(pythonCmd: string, pythonVersion: string, autowsgrVersion: string): void { +/** 写入环境就绪标记。 */ +function writeEnvMarker( + environment: PythonEnvironment, + pythonVersion: string, + autowsgrVersion: string, +): void { try { - fs.writeFileSync(ENV_READY_MARKER(), JSON.stringify({ pythonCmd, pythonVersion, autowsgrVersion }), 'utf-8'); - } catch { /* ignore */ } + fs.writeFileSync( + ENV_READY_MARKER(), + JSON.stringify({ + pythonCmd: environment.pythonCmd, + pythonVersion, + autowsgrVersion, + environmentIdentity: environment.identity, + }), + 'utf-8', + ); + } catch { /* 标记写入失败不阻断启动。 */ } } -// ════════════════════════════════════════ // autowsgr 更新桥接 -// ════════════════════════════════════════ -/** 构建 autoUpdateAutowsgr 所需的依赖对象 */ +/** 构造 autoUpdateAutowsgr 的依赖对象。 */ function buildAutoUpdateDeps(): AutoUpdateDeps { const ctx = getCtx(); return { @@ -88,75 +132,137 @@ function buildAutoUpdateDeps(): AutoUpdateDeps { }; } -function shouldAutoUpdate(): boolean { +function shouldAutoUpdate(environment: PythonEnvironment): boolean { const ctx = getCtx(); - return ctx.getUpdateMode() !== 'manual'; + return environment.startupMode === 'managed' + && ctx.getUpdateMode() !== 'manual'; +} + +function autoUpdateSkipMessage(environment: PythonEnvironment): string { + return environment.startupMode === 'external' + ? '本地后端调试模式:跳过 autowsgr 自动更新检查' + : '手动更新模式:跳过 autowsgr 自动更新检查'; } type CoreDepProbeResult = { uvicorn: boolean; fastapi: boolean; scipy: boolean; + requests: boolean; + beautifulSoup: boolean; autowsgr: string | null; + backendRuntimeContract: boolean; }; -/** - * 检查核心依赖可导入性。 - * 这里额外检查 scipy._lib,避免仅校验入口包导致“检查通过但运行时报错”。 - */ -async function probeCoreDependencies(pythonCmd: string): Promise { +/** 检查核心依赖及 scipy._lib 是否可导入。 */ +async function probeCoreDependencies( + environment: PythonEnvironment, +): Promise { const ctx = getCtx(); - const spFwd = localSitePackages().replace(/\\/g, '/'); + const { backendRoot, pythonCmd, useLocalSite } = environment; + const expectedRoot = backendRoot || environment.localSite; + const pythonPath = (value: string): string => value + .replace(/\\/g, '/') + .replace(/'/g, "\\'"); const checkScript = path.join(ctx.getTempDir(), 'autowsgr_depcheck.py'); - fs.writeFileSync(checkScript, [ + const scriptLines = [ 'import json, sys, site', - `sp = '${spFwd}'`, - 'sys.path.insert(0, sp)', - 'site.addsitedir(sp)', + ...(useLocalSite + ? [ + `sp = '${pythonPath(localSitePackages())}'`, + 'sys.path.insert(0, sp)', + 'site.addsitedir(sp)', + ] + : []), + ...(backendRoot + ? [ + `repo = '${pythonPath(backendRoot)}'`, + 'sys.path.insert(0, repo)', + ] + : []), 'r = {}', - "checks = [('uvicorn', 'uvicorn'), ('fastapi', 'fastapi'), ('scipy', 'scipy._lib')]", + `checks = ${JSON.stringify( + PYTHON_DEPENDENCY_SPECS.map( + dependency => [dependency.key, dependency.importName], + ), + )}`, 'for key, mod in checks:', ' try:', ' __import__(mod); r[key] = True', ' except Exception:', ' r[key] = False', 'try:', - ' import autowsgr; r["autowsgr"] = autowsgr.__version__', + ' import autowsgr', + ' r["autowsgr"] = getattr(autowsgr, "__version__", "source")', + ' r["autowsgr_path"] = autowsgr.__file__', 'except Exception:', ' r["autowsgr"] = None', + ' r["autowsgr_path"] = None', + ...buildBackendRuntimeContractProbeLines(), + 'try:', + ' _verify_gui_runtime_contract()', + ' r["backend_runtime_contract"] = True', + 'except Exception:', + ' r["backend_runtime_contract"] = False', 'print(json.dumps(r))', - ].join('\n'), 'utf-8'); + ]; + fs.writeFileSync(checkScript, scriptLines.join('\n'), 'utf-8'); try { const { stdout: depOut } = await execAsync( `"${pythonCmd}" "${checkScript}"`, - { windowsHide: true, timeout: 30000 }, + { + windowsHide: true, + timeout: 30000, + env: buildPythonProcessEnv(environment), + }, ); const depResult = JSON.parse(depOut.trim()); + const autowsgrPath = typeof depResult.autowsgr_path === 'string' + ? path.resolve(depResult.autowsgr_path) + : ''; + const relativePath = autowsgrPath + ? path.relative(path.resolve(expectedRoot), autowsgrPath) + : ''; + const usesExpectedAutowsgr = autowsgrPath !== '' + && relativePath !== '..' + && !relativePath.startsWith(`..${path.sep}`) + && !path.isAbsolute(relativePath); + if (depResult.autowsgr != null && !usesExpectedAutowsgr) { + ctx.sendProgress( + `WARNING 忽略来源不正确的 autowsgr: ${autowsgrPath}`, + ); + } return { uvicorn: Boolean(depResult.uvicorn), fastapi: Boolean(depResult.fastapi), scipy: Boolean(depResult.scipy), - autowsgr: depResult.autowsgr == null ? null : String(depResult.autowsgr), + requests: Boolean(depResult.requests), + beautifulSoup: Boolean(depResult.beautifulSoup), + autowsgr: depResult.autowsgr == null || !usesExpectedAutowsgr + ? null + : String(depResult.autowsgr), + backendRuntimeContract: ( + usesExpectedAutowsgr + && depResult.backend_runtime_contract === true + ), }; } catch { return null; } finally { - try { fs.unlinkSync(checkScript); } catch { /* ignore */ } + try { fs.unlinkSync(checkScript); } catch { /* 忽略清理失败。 */ } } } -// ════════════════════════════════════════ // 环境检查主流程 -// ════════════════════════════════════════ -/** 检查 Python 环境和所需包 */ +/** 检查 Python 环境和所需包。 */ export async function checkEnvironment(): Promise { const ctx = getCtx(); ctx.sendProgress('正在检查运行环境…'); await ensureVCRedist(); - // ── 快速路径: 如果标记文件存在且有效,跳过重量级依赖检查 ── + // 有效标记可跳过重量级依赖检查。 const marker = readEnvMarker(); if (marker) { setCachedPythonCmd(marker.pythonCmd); @@ -164,29 +270,38 @@ export async function checkEnvironment(): Promise { if (certFile) ctx.sendProgress(`TLS 证书已就绪: ${certFile}`); else ctx.sendProgress('WARNING 未检测到 TLS 根证书,后续联网操作可能失败'); - const markerProbe = await probeCoreDependencies(marker.pythonCmd); + const markerProbe = await probeCoreDependencies(marker.environment); const markerBrokenDeps: string[] = []; if (!markerProbe) { markerBrokenDeps.push('dep-check'); } else { - if (!markerProbe.uvicorn) markerBrokenDeps.push('uvicorn'); - if (!markerProbe.fastapi) markerBrokenDeps.push('fastapi'); - if (!markerProbe.scipy) markerBrokenDeps.push('scipy'); + for (const dependency of PYTHON_DEPENDENCY_SPECS) { + if (!markerProbe[dependency.key]) { + markerBrokenDeps.push(dependency.packageName); + } + } if (markerProbe.autowsgr == null) markerBrokenDeps.push('autowsgr'); + if (!markerProbe.backendRuntimeContract) { + markerBrokenDeps.push('autowsgr-runtime-contract'); + } } if (markerBrokenDeps.length === 0) { - // 每次启动检查并自动更新 autowsgr(可由更新模式关闭) + // 自动模式下每次启动检查 autowsgr 更新。 const markerAutowsgrVersion = markerProbe?.autowsgr ?? marker.autowsgrVersion; let finalVer = markerAutowsgrVersion; - if (shouldAutoUpdate()) { + if (shouldAutoUpdate(marker.environment)) { const updatedVer = await autoUpdateAutowsgr(marker.pythonCmd, buildAutoUpdateDeps()); finalVer = updatedVer ?? markerAutowsgrVersion; if (updatedVer && updatedVer !== markerAutowsgrVersion) { - writeEnvMarker(marker.pythonCmd, marker.pythonVersion, finalVer); + writeEnvMarker( + marker.environment, + marker.pythonVersion, + finalVer, + ); } } else { - ctx.sendProgress('手动更新模式:跳过 autowsgr 自动更新检查'); + ctx.sendProgress(autoUpdateSkipMessage(marker.environment)); } ctx.sendProgress(`环境就绪 (${marker.pythonVersion}, autowsgr ${finalVer}) ✓`); return { @@ -198,18 +313,33 @@ export async function checkEnvironment(): Promise { } ctx.sendProgress(`检测到依赖异常 (${markerBrokenDeps.join(', ')}),重新执行完整检查…`); - try { fs.unlinkSync(ENV_READY_MARKER()); } catch { /* ignore */ } + try { fs.unlinkSync(ENV_READY_MARKER()); } catch { /* 忽略清理失败。 */ } } - // ── 完整检查路径 ── + // 标记无效时执行完整检查。 ctx.sendProgress('正在检查 Python 环境…'); - ensurePthFile(); const pythonCmd = await findPython(); if (!pythonCmd) { ctx.sendProgress('WARNING 未找到兼容的 Python(需要 3.12 或 3.13)'); return { pythonCmd: null, pythonVersion: null, missingPackages: [], allReady: false }; } + let environment: PythonEnvironment; + try { + environment = resolvePythonEnvironment(pythonCmd); + } catch (error) { + ctx.sendProgress( + `ERROR ${error instanceof Error ? error.message : String(error)}`, + ); + return { + pythonCmd, + pythonVersion: null, + missingPackages: ['autowsgr'], + allReady: false, + }; + } + if (environment.useLocalSite) ensurePthFile(); + const certFile = await ensureSslCertForPython(pythonCmd); if (certFile) ctx.sendProgress(`TLS 证书已就绪: ${certFile}`); else ctx.sendProgress('WARNING 未检测到 TLS 根证书,后续联网操作可能失败'); @@ -219,24 +349,24 @@ export async function checkEnvironment(): Promise { const { stdout } = await execAsync(`"${pythonCmd}" --version`, { windowsHide: true }); pythonVersion = stdout.trim(); ctx.sendProgress(`${pythonVersion} ✓`); - } catch { /* ignore */ } + } catch { /* 版本读取失败时保留空值。 */ } ctx.sendProgress('正在检查依赖包…'); const missingPackages: string[] = []; let autowsgrVersion = ''; try { - const depResult = await probeCoreDependencies(pythonCmd); + const depResult = await probeCoreDependencies(environment); if (!depResult) { throw new Error('依赖探测失败'); } - for (const pkg of ['uvicorn', 'fastapi', 'scipy'] as const) { - if (depResult[pkg]) { - ctx.sendProgress(` ${pkg} \u2713`); + for (const dependency of PYTHON_DEPENDENCY_SPECS) { + if (depResult[dependency.key]) { + ctx.sendProgress(` ${dependency.packageName} \u2713`); } else { - missingPackages.push(pkg); - ctx.sendProgress(` ${pkg} \u2717`); + missingPackages.push(dependency.packageName); + ctx.sendProgress(` ${dependency.packageName} \u2717`); } } @@ -248,8 +378,21 @@ export async function checkEnvironment(): Promise { missingPackages.push('autowsgr'); ctx.sendProgress(` autowsgr \u2717`); } + if (depResult.backendRuntimeContract) { + ctx.sendProgress(' AutoWSGR GUI 运行契约 ✓'); + } else { + missingPackages.push('autowsgr-runtime-contract'); + ctx.sendProgress( + ' AutoWSGR GUI 运行契约 ✗ 请更新后端版本', + ); + } } catch { - missingPackages.push('uvicorn', 'fastapi', 'scipy', 'autowsgr'); + missingPackages.push( + ...PYTHON_DEPENDENCY_SPECS.map( + dependency => dependency.packageName, + ), + 'autowsgr', + ); ctx.sendProgress(' 依赖检查失败'); } @@ -257,7 +400,7 @@ export async function checkEnvironment(): Promise { if (allReady) { ctx.sendProgress('依赖检查通过 ✓'); - // 检查 ADB 可用性 + // 检查 ADB 可用性。 const adbDir = path.join(ctx.appRoot(), 'adb'); const builtinAdb = path.join(adbDir, 'adb.exe'); if (fs.existsSync(builtinAdb)) { @@ -266,15 +409,15 @@ export async function checkEnvironment(): Promise { ctx.sendProgress('ADB (内置) ✗ 将使用模拟器自带 ADB'); } - // 检查并自动更新 autowsgr(可由更新模式关闭) + // 自动模式下检查并更新 autowsgr。 let finalVer = autowsgrVersion; - if (shouldAutoUpdate()) { + if (shouldAutoUpdate(environment)) { const updatedVer = await autoUpdateAutowsgr(pythonCmd, buildAutoUpdateDeps()); finalVer = updatedVer || autowsgrVersion; } else { - ctx.sendProgress('手动更新模式:跳过 autowsgr 自动更新检查'); + ctx.sendProgress(autoUpdateSkipMessage(environment)); } - writeEnvMarker(pythonCmd, pythonVersion || '', finalVer); + writeEnvMarker(environment, pythonVersion || '', finalVer); } return { diff --git a/electron/pythonEnv/environment.ts b/electron/pythonEnv/environment.ts new file mode 100644 index 0000000..2c45db6 --- /dev/null +++ b/electron/pythonEnv/environment.ts @@ -0,0 +1,128 @@ +/** + * 统一描述 Python 安装、检查和启动环境。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { getCtx } from './context'; +import { isLocalPython, localSitePackages } from './utils'; + +export type BackendStartupMode = 'managed' | 'external'; + +/** 安装、检查和启动共同使用的 Python 环境。 */ +export interface PythonEnvironment { + startupMode: BackendStartupMode; + pythonCmd: string; + backendRoot: string | null; + localSite: string; + useLocalSite: boolean; + installTarget: string | null; + identity: string; +} + +function normalizeIdentityPath(value: string | null): string | null { + if (!value) return null; + const normalized = path.resolve(value); + return process.platform === 'win32' ? normalized.toLowerCase() : normalized; +} + +function isSamePath(left: string, right: string): boolean { + return normalizeIdentityPath(left) === normalizeIdentityPath(right); +} + +/** 解析 external 模式的仓库根目录;managed 模式返回 null。 */ +export function resolveExternalBackendRoot(): string | null { + const ctx = getCtx(); + if (ctx.getBackendStartupMode() !== 'external') return null; + + const configured = process.env.AUTOWSGR_BACKEND_REPO?.trim() + || ctx.getBackendRepoPath().trim(); + if (!configured) { + throw new Error('external 模式未配置 AutoWSGR 本地仓库路径'); + } + + let root = path.resolve(configured); + if (!fs.existsSync(root)) { + throw new Error(`external 后端仓库路径不存在: ${root}`); + } + + const isPackageDirectory = path.basename(root).toLowerCase() === 'autowsgr' + && fs.existsSync(path.join(root, '__init__.py')) + && fs.existsSync(path.join(root, 'server', 'main.py')); + if (isPackageDirectory) root = path.dirname(root); + + if (!fs.existsSync(path.join(root, 'autowsgr', 'server', 'main.py'))) { + throw new Error( + `external 后端仓库无效,未找到 autowsgr/server/main.py: ${root}`, + ); + } + return root; +} + +/** 根据当前设置生成唯一的 Python 环境描述。 */ +export function resolvePythonEnvironment( + pythonCmd: string, +): PythonEnvironment { + const startupMode = getCtx().getBackendStartupMode(); + const backendRoot = resolveExternalBackendRoot(); + const localSite = localSitePackages(); + const useLocalSite = startupMode === 'managed' || isLocalPython(pythonCmd); + const installTarget = useLocalSite ? localSite : null; + const identity = JSON.stringify({ + startupMode, + pythonCmd: normalizeIdentityPath(pythonCmd), + backendRoot: normalizeIdentityPath(backendRoot), + installTarget: normalizeIdentityPath(installTarget), + }); + + return { + startupMode, + pythonCmd, + backendRoot, + localSite, + useLocalSite, + installTarget, + identity, + }; +} + +/** 构造子进程环境,并隔离外部解释器与 managed 包目录。 */ +export function buildPythonProcessEnv( + environment: PythonEnvironment, + baseEnv: NodeJS.ProcessEnv = process.env, +): NodeJS.ProcessEnv { + const result: NodeJS.ProcessEnv = { ...baseEnv }; + if (environment.useLocalSite) { + const existing = result.PYTHONPATH || ''; + const entries = existing.split(path.delimiter).filter(Boolean); + if (!entries.some(entry => isSamePath(entry, environment.localSite))) { + entries.unshift(environment.localSite); + } + result.PYTHONPATH = entries.join(path.delimiter); + result.PYTHONUSERBASE = path.join(getCtx().appRoot(), 'python'); + return result; + } + + const entries = (result.PYTHONPATH || '') + .split(path.delimiter) + .filter(entry => entry && !isSamePath(entry, environment.localSite)); + if (entries.length > 0) result.PYTHONPATH = entries.join(path.delimiter); + else delete result.PYTHONPATH; + + const managedUserBase = path.join(getCtx().appRoot(), 'python'); + if ( + result.PYTHONUSERBASE + && isSamePath(result.PYTHONUSERBASE, managedUserBase) + ) { + delete result.PYTHONUSERBASE; + } + return result; +} + +/** 返回 pip 安装目标参数;外部解释器使用自身环境时返回空数组。 */ +export function installTargetArgs( + environment: PythonEnvironment, +): string[] { + return environment.installTarget + ? ['--target', environment.installTarget] + : []; +} diff --git a/electron/pythonEnv/finder.ts b/electron/pythonEnv/finder.ts index 4265e6c..0d1f0cb 100644 --- a/electron/pythonEnv/finder.ts +++ b/electron/pythonEnv/finder.ts @@ -1,6 +1,5 @@ /** - * Python 可执行文件查找逻辑。 - * 优先级: 用户配置 > 本地便携版 > 系统全局。 + * 按用户配置、便携版、系统全局的顺序查找 Python。 */ import * as path from 'path'; import * as fs from 'fs'; @@ -10,7 +9,7 @@ import { getCtx, getCachedPythonCmd, setCachedPythonCmd } from './context'; const execAsync = promisify(exec); -/** 检查 Python 版本是否为 3.12.x 或 3.13.x */ +/** 判断 Python 版本是否为 3.12 或 3.13。 */ export function isAllowedPythonVersion(versionOutput: string): boolean { const m = versionOutput.match(/(\d+)\.(\d+)/); if (!m) return false; @@ -19,14 +18,14 @@ export function isAllowedPythonVersion(versionOutput: string): boolean { return major === 3 && (minor === 12 || minor === 13); } -/** 查找可用的 Python 可执行文件 (用户配置 > 本地便携版 > 系统, 仅接受 3.12/3.13, 结果会缓存) */ +/** 异步查找并缓存兼容的 Python 可执行文件。 */ export async function findPython(): Promise { if (getCachedPythonCmd() !== undefined) return getCachedPythonCmd()!; const ctx = getCtx(); let found: string | null = null; - // 最高优先级:用户在配置页指定的 Python 路径 + // 优先使用配置页指定的 Python。 const configured = ctx.getConfiguredPythonPath(); if (configured && fs.existsSync(configured)) { try { @@ -41,26 +40,24 @@ export async function findPython(): Promise { ctx.sendProgress('WARNING 用户配置的 Python 路径不存在,回退自动检测'); } - // 优先使用本地便携版 Python + // 其次使用本地便携版 Python。 const localPython = path.join(ctx.appRoot(), 'python', 'python.exe'); - if (fs.existsSync(localPython)) { + if (!found && fs.existsSync(localPython)) { try { const { stdout, stderr } = await execAsync(`"${localPython}" --version`, { windowsHide: true }); const verStr = stdout || stderr; if (isAllowedPythonVersion(verStr)) found = localPython; else ctx.sendProgress(`WARNING 本地 Python 版本不兼容: ${verStr.trim()}(需要 3.12 或 3.13)`); - } catch { /* local Python broken */ } + } catch { /* 本地 Python 不可用时继续查找。 */ } } - if (!found && !configured) { // 仅在本地 Python 不可用且无用户配置时回退系统 Python - // 回退到系统全局 Python - // 注意: 必须解析出真实的 .exe 绝对路径,因为 pyenv 等工具使用 .bat shim, - // 而 Node.js spawn() 不经过 shell,无法执行 .bat 文件。 + if (!found) { // 用户配置和本地 Python 均不可用时回退系统 Python + // 最后回退到系统 Python;需解析真实 exe,避免 spawn 无法执行 .bat shim。 for (const cmd of ['python', 'python3']) { try { const { stdout: verOut, stderr: verErr } = await execAsync(`${cmd} --version`, { windowsHide: true }); if (!isAllowedPythonVersion(verOut || verErr)) continue; - // 通过 Python 自身获取真实可执行文件路径 (解决 pyenv/.bat shim 问题) + // 通过 Python 自身解析真实可执行文件。 const { stdout } = await execAsync( `${cmd} -c "import sys; print(sys.executable)"`, { windowsHide: true }, @@ -68,7 +65,7 @@ export async function findPython(): Promise { const resolved = stdout.trim(); found = (resolved && fs.existsSync(resolved)) ? resolved : cmd; break; - } catch { /* continue */ } + } catch { /* 当前命令不可用时继续查找。 */ } } } @@ -76,7 +73,7 @@ export async function findPython(): Promise { return found; } -/** 同步检查 Python 版本是否被允许 (shell 重定向以同时捕获 stdout/stderr) */ +/** 同步判断 Python 版本是否兼容。 */ function isAllowedPythonVersionSync(pythonCmd: string): boolean { try { const output = execSync( @@ -89,11 +86,11 @@ function isAllowedPythonVersionSync(pythonCmd: string): boolean { } } -/** 同步查找 Python (用于非 async 上下文) */ +/** 为同步调用方查找并缓存 Python。 */ export function findPythonSync(): string | null { if (getCachedPythonCmd() !== undefined) return getCachedPythonCmd()!; const ctx = getCtx(); - // 最高优先级:用户配置的 Python 路径 + // 优先使用用户配置的 Python。 const configured = ctx.getConfiguredPythonPath(); if (configured && fs.existsSync(configured) && isAllowedPythonVersionSync(configured)) { setCachedPythonCmd(configured); @@ -107,7 +104,7 @@ export function findPythonSync(): string | null { for (const cmd of ['python', 'python3']) { try { if (!isAllowedPythonVersionSync(cmd)) continue; - // 解析真实路径 (pyenv/.bat shim 兼容) + // 解析 pyenv 或 .bat shim 指向的真实路径。 const resolved = execSync( `${cmd} -c "import sys; print(sys.executable)"`, { windowsHide: true, encoding: 'utf-8' }, @@ -115,7 +112,7 @@ export function findPythonSync(): string | null { const result = (resolved && fs.existsSync(resolved)) ? resolved : cmd; setCachedPythonCmd(result); return result; - } catch { /* continue */ } + } catch { /* 当前命令不可用时继续查找。 */ } } return null; } diff --git a/electron/pythonEnv/index.ts b/electron/pythonEnv/index.ts index 16c53e7..c6d2c5f 100644 --- a/electron/pythonEnv/index.ts +++ b/electron/pythonEnv/index.ts @@ -1,21 +1,51 @@ /** - * Barrel re-exports — 保持外部导入路径 './pythonEnv' 不变。 + * 汇总 Python 环境模块的公共导出。 */ -// context export { type PythonEnvContext, initPythonEnv, clearPythonCache } from './context'; -// finder export { isAllowedPythonVersion, findPython, findPythonSync } from './finder'; -// utils -export { type EnvCheckResult, sysPathInsert, ensurePthFile, pipEnv, localSitePackages, ensurePip, ensureSslCertForPython } from './utils'; +export { + type EnvCheckResult, + sysPathInsert, + ensurePthFile, + pipEnv, + localSitePackages, + ensurePip, + ensureSslCertForPython, + isLocalPython, +} from './utils'; + +export { + type BackendStartupMode, + type PythonEnvironment, + resolveExternalBackendRoot, + resolvePythonEnvironment, + buildPythonProcessEnv, + installTargetArgs, +} from './environment'; + +export { + resolveConfiguredCudaRoot, + buildCudaEnvironment, + buildBackendRuntimeEnvironment, +} from './cuda'; -// envCheck export { checkEnvironment } from './envCheck'; -// installer -export { installPortablePython, checkForUpdates, installDependencies, pullUpdates } from './installer'; +export { + type DependencyInstallPlan, + installPortablePython, + checkForUpdates, + buildDependencyInstallPlan, + installDependencies, + pullUpdates, +} from './installer'; + +export { + MANAGED_AUTOWSGR_COMMIT, + MANAGED_AUTOWSGR_REQUIREMENT, +} from './backendRequirement'; -// updater (DI 接口,供外部直接调用时使用) export { type AutoUpdateDeps, autoUpdateAutowsgr } from './updater'; diff --git a/electron/pythonEnv/installer.ts b/electron/pythonEnv/installer.ts index be1514c..60d7efe 100644 --- a/electron/pythonEnv/installer.ts +++ b/electron/pythonEnv/installer.ts @@ -9,17 +9,20 @@ import { getCtx, setCachedPythonCmd } from './context'; import { findPython } from './finder'; import { ensurePthFile, localSitePackages, pipEnv, ensurePip, ensureSslCertForPython } from './utils'; import { ENV_READY_MARKER } from './envCheck'; +import { + buildPythonProcessEnv, + installTargetArgs, + type PythonEnvironment, + resolvePythonEnvironment, +} from './environment'; +import { SHIP_LIBRARY_REQUIREMENTS } from './dependencies'; +import { MANAGED_AUTOWSGR_REQUIREMENT } from './backendRequirement'; const execAsync = promisify(exec); -/** PyPI 2.2.2 尚未包含 2026-07-30 活动支持,临时固定到上游已合入提交。 */ -const AUTOWSGR_REQUIREMENT = 'https://github.com/OpenWSGR/AutoWSGR/archive/a38252d3.zip'; - -// ════════════════════════════════════════ // 便携版 Python 安装 -// ════════════════════════════════════════ -/** 安装/初始化便携版 Python(已随应用打包,仅需确保 pip 就绪) */ +/** 安装或初始化便携版 Python。 */ export async function installPortablePython(): Promise<{ success: boolean }> { const ctx = getCtx(); setCachedPythonCmd(undefined); // 安装后需重新检测 @@ -27,30 +30,30 @@ export async function installPortablePython(): Promise<{ success: boolean }> { const pythonExe = path.join(pythonDir, 'python.exe'); if (!fs.existsSync(pythonExe)) { - // 兜底: 如果打包产物缺失 python,尝试在线下载 + // 打包产物缺失时尝试在线下载。 ctx.sendProgress('WARNING 未找到内置 Python,尝试在线下载…'); return downloadPortablePython(); } - // 确保 ._pth 配置正确 + // 确保 ._pth 配置正确。 ensurePthFile(); - // 检查 pip 是否可用 + // 检查 pip 是否可用。 try { await execAsync(`"${pythonExe}" -m pip --version`, { windowsHide: true, timeout: 15000 }); const certFile = await ensureSslCertForPython(pythonExe); if (certFile) ctx.sendProgress(`TLS 证书已就绪: ${certFile}`); ctx.sendProgress('内置 Python + pip 就绪 ✓'); return { success: true }; - } catch { /* pip not available, install it */ } + } catch { /* pip 不可用时继续安装。 */ } - // pip 缺失则安装 + // pip 缺失时执行安装。 ctx.sendProgress('正在安装 pip…'); const getPipPath = path.join(ctx.getTempDir(), 'get-pip.py'); try { await execAsync(`curl -sSL -o "${getPipPath}" "https://bootstrap.pypa.io/get-pip.py"`, { windowsHide: true, timeout: 60000 }); await execAsync(`"${pythonExe}" "${getPipPath}"`, { windowsHide: true, timeout: 120000 }); - try { fs.unlinkSync(getPipPath); } catch { /* ignore */ } + try { fs.unlinkSync(getPipPath); } catch { /* 忽略清理失败。 */ } const certFile = await ensureSslCertForPython(pythonExe); if (certFile) ctx.sendProgress(`TLS 证书已就绪: ${certFile}`); else ctx.sendProgress('WARNING 未检测到 TLS 根证书,后续联网操作可能失败'); @@ -62,7 +65,7 @@ export async function installPortablePython(): Promise<{ success: boolean }> { } } -/** 兜底: 在线下载便携版 Python(仅在内置 Python 缺失时使用) */ +/** 在内置 Python 缺失时在线下载便携版。 */ async function downloadPortablePython(): Promise<{ success: boolean }> { const ctx = getCtx(); const pythonDir = path.join(ctx.appRoot(), 'python'); @@ -94,7 +97,7 @@ async function downloadPortablePython(): Promise<{ success: boolean }> { ensurePthFile(); - // 安装 pip + // 安装 pip。 ctx.sendProgress('正在安装 pip…'); const getPipPath = path.join(ctx.getTempDir(), 'get-pip.py'); try { @@ -108,16 +111,14 @@ async function downloadPortablePython(): Promise<{ success: boolean }> { return { success: false }; } - try { fs.unlinkSync(zipPath); } catch { /* ignore */ } - try { fs.unlinkSync(getPipPath); } catch { /* ignore */ } + try { fs.unlinkSync(zipPath); } catch { /* 忽略清理失败。 */ } + try { fs.unlinkSync(getPipPath); } catch { /* 忽略清理失败。 */ } ctx.sendProgress(`Python ${version} 便携版安装完成 ✓`); return { success: true }; } -// ════════════════════════════════════════ // 更新检查 -// ════════════════════════════════════════ interface UpdateCheckResult { gitAvailable: boolean; @@ -127,7 +128,7 @@ interface UpdateCheckResult { remoteUrl: string; } -/** 检查 autowsgr 包是否有可用更新 (对比本地已安装版本与 PyPI 最新版) */ +/** 比较本地与 PyPI 的 autowsgr 版本。 */ export async function checkForUpdates(): Promise { const result: UpdateCheckResult = { gitAvailable: false, @@ -140,17 +141,17 @@ export async function checkForUpdates(): Promise { const pythonCmd = await findPython(); if (!pythonCmd) return result; - result.gitAvailable = true; // reuse field: means "can check updates" + result.gitAvailable = true; // 复用字段表示可检查更新。 try { - // 获取已安装版本 + // 获取已安装版本。 const { stdout: localVer } = await execAsync( `"${pythonCmd}" -c "import autowsgr; print(autowsgr.__version__)"`, { windowsHide: true, env: pipEnv() }, ); - result.currentBranch = localVer.trim(); // reuse field: current version + result.currentBranch = localVer.trim(); // 复用字段保存当前版本。 - // 获取 PyPI 最新版本 + // 获取 PyPI 最新版本。 const { stdout: pipOut } = await execAsync( `"${pythonCmd}" -m pip index versions autowsgr`, { windowsHide: true, timeout: 15000, env: pipEnv() }, @@ -160,40 +161,95 @@ export async function checkForUpdates(): Promise { const latestVer = m[1].replace(/,$/,''); result.hasUpdates = latestVer !== result.currentBranch; } - } catch { /* ignore */ } + } catch { /* 检查失败时返回默认结果。 */ } return result; } -// ════════════════════════════════════════ // 依赖安装与更新 -// ════════════════════════════════════════ -/** 自动安装依赖 (pip install autowsgr),始终安装到项目目录,不动全局 */ +export interface DependencyInstallPlan { + buildArgs: string[]; + toolArgs: string[]; + backendArgs: string[]; +} + +/** 生成与运行环境一致的 pip 参数,供回归测试直接验证。 */ +export function buildDependencyInstallPlan( + environment: PythonEnvironment, + backendRequirement: string, +): DependencyInstallPlan { + const targetArgs = installTargetArgs(environment); + return { + buildArgs: [ + '-m', 'pip', 'install', + '--upgrade', + ...targetArgs, + 'setuptools', + 'hatchling', + 'hatch-vcs', + ], + toolArgs: [ + '-m', 'pip', 'install', + '--upgrade', + ...targetArgs, + ...SHIP_LIBRARY_REQUIREMENTS, + ], + backendArgs: [ + '-m', 'pip', 'install', + '--upgrade', + '--no-build-isolation', + ...targetArgs, + backendRequirement, + ], + }; +} + +/** 自动安装依赖;安装目标由统一 Python 环境描述决定。 */ export async function installDependencies(pythonCmd: string): Promise<{ success: boolean; output: string }> { const ctx = getCtx(); - // 安装后环境变化,清除标记以便下次重新检查 - try { fs.unlinkSync(ENV_READY_MARKER()); } catch { /* ignore */ } + let environment: PythonEnvironment; + try { + environment = resolvePythonEnvironment(pythonCmd); + } catch (error) { + return { + success: false, + output: error instanceof Error ? error.message : String(error), + }; + } + const backendRequirement = environment.backendRoot + ?? MANAGED_AUTOWSGR_REQUIREMENT; + const installPlan = buildDependencyInstallPlan( + environment, + backendRequirement, + ); + + // 安装后清除环境标记,触发下次完整检查。 + try { fs.unlinkSync(ENV_READY_MARKER()); } catch { /* 忽略清理失败。 */ } const certFile = await ensureSslCertForPython(pythonCmd); if (certFile) ctx.sendProgress(`TLS 证书已就绪: ${certFile}`); else ctx.sendProgress('WARNING 未检测到 TLS 根证书,后续联网操作可能失败'); - // 确保 pip 可用 + // 确保 pip 可用。 if (!(await ensurePip(pythonCmd))) { return { success: false, output: 'pip 安装失败,无法安装依赖' }; } const cwd = ctx.appRoot(); - const targetDir = localSitePackages(); - if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); + if ( + environment.installTarget + && !fs.existsSync(environment.installTarget) + ) { + fs.mkdirSync(environment.installTarget, { recursive: true }); + } const runPip = (args: string[]): Promise<{ code: number; output: string }> => new Promise((resolve) => { const proc = spawn(pythonCmd, args, { cwd, windowsHide: true, stdio: 'pipe', - env: pipEnv(), + env: buildPythonProcessEnv(environment), }); let output = ''; @@ -212,37 +268,34 @@ export async function installDependencies(pythonCmd: string): Promise<{ success: }); ctx.sendProgress('正在安装后端构建依赖…'); - const buildDeps = await runPip([ - '-m', 'pip', 'install', - '--upgrade', - '--target', targetDir, - 'setuptools', - 'hatchling', - 'hatch-vcs', - ]); + const buildDeps = await runPip(installPlan.buildArgs); if (buildDeps.code !== 0) { ctx.sendProgress('ERROR 后端构建依赖安装失败'); return { success: false, output: buildDeps.output.slice(-500) }; } - ctx.sendProgress('正在安装后端依赖到项目目录…'); - const install = await runPip([ - '-m', 'pip', 'install', - '--upgrade', - '--no-build-isolation', - '--target', targetDir, - AUTOWSGR_REQUIREMENT, - ]); + ctx.sendProgress('正在安装舰船资料库更新依赖…'); + const toolDeps = await runPip(installPlan.toolArgs); + if (toolDeps.code !== 0) { + ctx.sendProgress('ERROR 舰船资料库更新依赖安装失败'); + return { success: false, output: toolDeps.output.slice(-500) }; + } + + const installLocation = environment.installTarget + ? 'GUI 项目目录' + : '当前 Python 环境'; + ctx.sendProgress(`正在安装后端依赖到${installLocation}…`); + const install = await runPip(installPlan.backendArgs); if (install.code === 0) ctx.sendProgress('后端依赖安装完成 ✓'); else ctx.sendProgress('ERROR 依赖安装失败'); return { success: install.code === 0, output: install.output.slice(-500) }; } -/** 更新 autowsgr 包(仅升级 autowsgr 本体,不级联重装所有依赖) */ +/** 更新 autowsgr 本体且不级联重装依赖。 */ export async function pullUpdates(): Promise<{ success: boolean; output: string }> { const ctx = getCtx(); - // 更新后清除环境标记 - try { fs.unlinkSync(ENV_READY_MARKER()); } catch { /* ignore */ } + // 更新后清除环境标记。 + try { fs.unlinkSync(ENV_READY_MARKER()); } catch { /* 忽略清理失败。 */ } const pythonCmd = await findPython(); if (!pythonCmd) return { success: false, output: '找不到 Python' }; @@ -264,14 +317,14 @@ export async function pullUpdates(): Promise<{ success: boolean; output: string const targetDir = localSitePackages(); if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); - // 先删除旧版 autowsgr,再重新安装(不带 --upgrade 避免级联更新依赖) + // 先删除旧版,再安装且不级联更新依赖。 try { for (const entry of fs.readdirSync(targetDir)) { if (entry === 'autowsgr' || entry.startsWith('autowsgr-')) { fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true }); } } - } catch { /* ignore cleanup errors */ } + } catch { /* 忽略清理失败。 */ } const proc = spawn(pythonCmd, [ '-m', 'pip', 'install', @@ -280,7 +333,7 @@ export async function pullUpdates(): Promise<{ success: boolean; output: string '--no-deps', '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple', '--trusted-host', 'pypi.tuna.tsinghua.edu.cn', - AUTOWSGR_REQUIREMENT, + MANAGED_AUTOWSGR_REQUIREMENT, ], { cwd: ctx.appRoot(), windowsHide: true, diff --git a/electron/pythonEnv/updater.ts b/electron/pythonEnv/updater.ts index f80b620..4a72e22 100644 --- a/electron/pythonEnv/updater.ts +++ b/electron/pythonEnv/updater.ts @@ -1,17 +1,17 @@ /** - * autowsgr 自动更新逻辑。 - * 使用依赖注入,不直接依赖 context 模块。 + * 通过依赖注入检查并更新 autowsgr。 */ import * as path from 'path'; import * as fs from 'fs'; import { exec, spawn } from 'child_process'; import { promisify } from 'util'; +import { MANAGED_AUTOWSGR_REQUIREMENT } from './backendRequirement'; +import { + buildBackendRuntimeContractProbeLines, +} from './backendContractProbe'; const execAsync = promisify(exec); -/** PyPI 2.2.2 尚未包含 2026-07-30 活动支持,临时固定到上游已合入提交。 */ -const AUTOWSGR_REQUIREMENT = 'https://github.com/OpenWSGR/AutoWSGR/archive/a38252d3.zip'; - export interface AutoUpdateDeps { sendProgress: (msg: string) => void; getTempDir: () => string; @@ -21,12 +21,28 @@ export interface AutoUpdateDeps { ensurePip: (pythonCmd: string) => Promise; } -/** 检查 autowsgr 是否有 PyPI 更新,有则自动升级;返回最终的已安装版本 */ +/** 生成 managed 后端更新参数,确保自动更新不会改装 PyPI 裸包。 */ +export function buildManagedAutowsgrUpdateArgs( + targetDir: string, +): string[] { + return [ + '-m', 'pip', 'install', + '--upgrade', + '--target', targetDir, + '--no-build-isolation', + '--no-deps', + '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple', + '--trusted-host', 'pypi.tuna.tsinghua.edu.cn', + MANAGED_AUTOWSGR_REQUIREMENT, + ]; +} + +/** 确保 managed 环境使用 GUI 明确支持的后端版本。 */ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps): Promise { try { deps.sendProgress('正在检查 autowsgr 更新…'); - // 单次 Python 调用: 获取本地版本 + PyPI 最新版本 + // 单次 Python 调用检查本地版本、活动资源和 GUI 运行契约。 const spFwd = deps.localSitePackages().replace(/\\/g, '\\\\'); const checkScript = [ 'import json, sys', @@ -41,11 +57,12 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps 'except:', ' result["local"] = None', ' result["event20260730"] = False', + ...buildBackendRuntimeContractProbeLines(), 'try:', - ' import urllib.request', - ' data = json.loads(urllib.request.urlopen("https://pypi.org/pypi/autowsgr/json", timeout=10).read())', - ' result["latest"] = data["info"]["version"]', - 'except: result["latest"] = None', + ' _verify_gui_runtime_contract()', + ' result["runtime_contract"] = True', + 'except Exception:', + ' result["runtime_contract"] = False', 'print(json.dumps(result))', ].join('\n'); @@ -56,36 +73,34 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps `"${pythonCmd}" "${scriptPath}"`, { windowsHide: true, timeout: 20000, env: deps.pipEnv() }, ); - try { fs.unlinkSync(scriptPath); } catch { /* ignore */ } + try { fs.unlinkSync(scriptPath); } catch { /* 忽略清理失败。 */ } const info = JSON.parse(stdout.trim()); const localVer: string | null = info.local; - const latestVer: string | null = info.latest; const supportsLatestEvent = info.event20260730 === true; + const supportsRuntimeContract = info.runtime_contract === true; - if (!latestVer) { - deps.sendProgress('autowsgr 更新检查跳过(无法获取最新版本信息)'); - return localVer; - } - - if (localVer === latestVer && supportsLatestEvent) { - deps.sendProgress(`autowsgr ${localVer} 已是最新版 ✓`); + if ( + localVer + && supportsLatestEvent + && supportsRuntimeContract + ) { + deps.sendProgress(`autowsgr ${localVer} 与 GUI 运行契约兼容 ✓`); return localVer; } - // 有版本更新,或当前 PyPI 版本缺少最新活动热修复。 - // 仅当 PyPI 已无更高版本时使用固定提交;未来正式版本发布后优先回到 PyPI。 - const needsEventHotfix = !supportsLatestEvent && localVer === latestVer; - if (!supportsLatestEvent) { - deps.sendProgress('当前 autowsgr 缺少 20260730 活动支持,正在安装上游活动热修复…'); - } else { - deps.sendProgress(`发现 autowsgr 更新: ${localVer ?? '未安装'} → ${latestVer},正在自动升级…`); - } - const installRequirement = needsEventHotfix ? AUTOWSGR_REQUIREMENT : 'autowsgr'; + const incompatibilities = [ + ...(!localVer ? ['未安装'] : []), + ...(!supportsLatestEvent ? ['缺少 20260730 活动资源'] : []), + ...(!supportsRuntimeContract ? ['缺少 GUI 运行契约'] : []), + ]; + deps.sendProgress( + `当前 autowsgr ${incompatibilities.join('、')},正在安装 GUI 兼容版本…`, + ); const targetDir = deps.localSitePackages(); if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); - // 确保 pip 可用 + // 确保 pip 可用。 if (!(await deps.ensurePip(pythonCmd))) { deps.sendProgress('WARNING pip 不可用,autowsgr 升级跳过'); return localVer; @@ -110,26 +125,21 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps proc.on('error', () => resolve(1)); }); if (buildDepsCode !== 0) { - deps.sendProgress('WARNING 活动热修复构建依赖安装失败'); + deps.sendProgress('WARNING GUI 兼容后端构建依赖安装失败'); return localVer; } const exitCode = await new Promise((resolve) => { - const proc = spawn(pythonCmd, [ - '-m', 'pip', 'install', - '--upgrade', - '--target', targetDir, - '--no-build-isolation', - '--no-deps', - '-i', 'https://pypi.tuna.tsinghua.edu.cn/simple', - '--trusted-host', 'pypi.tuna.tsinghua.edu.cn', - installRequirement, - ], { + const proc = spawn( + pythonCmd, + buildManagedAutowsgrUpdateArgs(targetDir), + { cwd: deps.appRoot(), windowsHide: true, stdio: 'pipe', env: deps.pipEnv(), - }); + }, + ); proc.stdout?.on('data', (d: Buffer) => { for (const l of d.toString().split('\n')) { if (l.trim()) deps.sendProgress(l.trim()); } }); proc.stderr?.on('data', (d: Buffer) => { for (const l of d.toString().split('\n')) { if (l.trim()) deps.sendProgress(l.trim()); } }); proc.on('close', (code) => resolve(code ?? 1)); @@ -141,7 +151,7 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps return localVer; } - // 升级后:单次 Python 调用验证版本 + 关键依赖 + // 升级后一次性验证版本和关键依赖。 const postScript = path.join(deps.getTempDir(), 'autowsgr_post_upgrade.py'); fs.writeFileSync(postScript, [ 'import json, sys, site', @@ -155,6 +165,12 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps ' root = Path(autowsgr.__file__).resolve().parent', ' r["event20260730"] = (root / "data" / "map" / "event" / "20260730").is_dir()', 'except: pass', + ...buildBackendRuntimeContractProbeLines(), + 'try:', + ' _verify_gui_runtime_contract()', + ' r["runtime_contract"] = True', + 'except Exception:', + ' r["runtime_contract"] = False', "for m in ['fastapi', 'uvicorn']:", ' try: __import__(m)', ' except Exception: r["missing"].append(m)', @@ -166,14 +182,19 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps `"${pythonCmd}" "${postScript}"`, { windowsHide: true, timeout: 15000, env: deps.pipEnv() }, ); - try { fs.unlinkSync(postScript); } catch { /* ignore */ } + try { fs.unlinkSync(postScript); } catch { /* 忽略清理失败。 */ } const postResult = JSON.parse(postOut.trim()); const actualVer: string = postResult.version; const missing: string[] = postResult.missing; const eventReady = postResult.event20260730 === true; + const runtimeContractReady = postResult.runtime_contract === true; - if (needsEventHotfix && !eventReady) { - deps.sendProgress('WARNING 活动热修复安装后仍未检测到 20260730 资源'); + if (!eventReady) { + deps.sendProgress('WARNING GUI 兼容后端安装后仍缺少 20260730 活动资源'); + return localVer; + } + if (!runtimeContractReady) { + deps.sendProgress('WARNING GUI 兼容后端安装后仍不支持运行契约'); return localVer; } @@ -223,27 +244,19 @@ export async function autoUpdateAutowsgr(pythonCmd: string, deps: AutoUpdateDeps } if (actualVer !== 'unknown') { - const expectedVersion = needsEventHotfix ? localVer : latestVer; - const msg = actualVer === expectedVersion - ? needsEventHotfix - ? `autowsgr ${actualVer} 活动热修复已安装 ✓` - : `autowsgr 已升级至 ${latestVer} ✓` - : `autowsgr 已升级至 ${actualVer}(期望 ${expectedVersion})`; - deps.sendProgress(msg); + deps.sendProgress( + `autowsgr ${actualVer} GUI 兼容版本已安装 ✓`, + ); return actualVer; } } catch { - try { fs.unlinkSync(postScript); } catch { /* ignore */ } + try { fs.unlinkSync(postScript); } catch { /* 忽略清理失败。 */ } } - if (needsEventHotfix) { - deps.sendProgress(`autowsgr ${localVer} 活动热修复已安装 ✓`); - return localVer; - } - deps.sendProgress(`autowsgr 已升级至 ${latestVer} ✓`); - return latestVer; + deps.sendProgress('WARNING 无法验证 GUI 兼容后端安装结果'); + return localVer; } catch { - deps.sendProgress('autowsgr 更新检查跳过(网络不可用或超时)'); + deps.sendProgress('autowsgr 更新检查跳过(环境不可用或检查超时)'); return null; } } diff --git a/electron/pythonEnv/utils.ts b/electron/pythonEnv/utils.ts index 5cb0eb0..2d0917f 100644 --- a/electron/pythonEnv/utils.ts +++ b/electron/pythonEnv/utils.ts @@ -18,9 +18,7 @@ const CERT_ENV_KEYS: Array = [ const certFileCache = new Map(); -// ════════════════════════════════════════ // 共享接口 -// ════════════════════════════════════════ export interface EnvCheckResult { pythonCmd: string | null; @@ -29,25 +27,21 @@ export interface EnvCheckResult { allReady: boolean; } -// ════════════════════════════════════════ // 路径工具 -// ════════════════════════════════════════ -/** 项目本地包目录 */ +/** 返回项目本地包目录。 */ export function localSitePackages(): string { return path.join(getCtx().appRoot(), 'python', 'site-packages'); } -/** 生成在 Python 命令前插入 site-packages 路径的前缀代码 */ +/** 生成向 sys.path 插入 site-packages 的 Python 代码。 */ export function sysPathInsert(): string { - // 使用 sys.path.insert 而非 PYTHONPATH 环境变量,因为: - // 1. 嵌入式 Python 的 ._pth 会完全忽略 PYTHONPATH - // 2. 避免 Windows 环境变量传递的各种边界问题 + // 嵌入式 Python 会忽略 PYTHONPATH,直接写入 sys.path 更可靠。 const sp = localSitePackages().replace(/\\/g, '\\\\'); return `import sys; sys.path.insert(0, r'${sp}'); `; } -/** pip 命令的公共环境变量:确保项目目录的包优先于全局 */ +/** 构造优先使用项目包目录的 pip 环境。 */ export function pipEnv(): NodeJS.ProcessEnv { const localSite = localSitePackages(); const existing = process.env.PYTHONPATH || ''; @@ -130,19 +124,16 @@ async function probePythonCertFile(pythonCmd: string): Promise { return certFile; } } catch { - // ignore probe failures; caller will continue without TLS env override + // 探测失败时由调用方继续使用原环境。 } finally { - try { fs.unlinkSync(scriptPath); } catch { /* ignore */ } + try { fs.unlinkSync(scriptPath); } catch { /* 忽略清理失败。 */ } } certFileCache.set(pythonCmd, null); return null; } -/** - * 为给定 Python 解释器补齐 TLS 证书环境变量。 - * 优先沿用用户已配置的证书路径,否则自动探测 Python 默认/Certifi 证书。 - */ +/** 沿用或探测证书,并补齐 Python TLS 环境变量。 */ export async function ensureSslCertForPython(pythonCmd: string): Promise { const existing = pickExistingCertFromEnv(); const certFile = existing || await probePythonCertFile(pythonCmd); @@ -156,16 +147,20 @@ export async function ensureSslCertForPython(pythonCmd: string): Promise { const ctx = getCtx(); try { await execAsync(`"${pythonCmd}" -m pip --version`, { windowsHide: true, timeout: 15000 }); return true; - } catch { /* pip not available */ } + } catch { /* pip 不可用时继续安装。 */ } if (isLocalPython(pythonCmd)) ensurePthFile(); @@ -209,12 +202,12 @@ export async function ensurePip(pythonCmd: string): Promise { try { await execAsync(`curl -sSL -o "${getPipPath}" "https://bootstrap.pypa.io/get-pip.py"`, { windowsHide: true, timeout: 60000 }); await execAsync(`"${pythonCmd}" "${getPipPath}"`, { windowsHide: true, timeout: 120000 }); - try { fs.unlinkSync(getPipPath); } catch { /* ignore */ } + try { fs.unlinkSync(getPipPath); } catch { /* 忽略清理失败。 */ } ctx.sendProgress('pip 安装完成 ✓'); return true; } catch { ctx.sendProgress('ERROR pip 安装失败'); - try { fs.unlinkSync(getPipPath); } catch { /* ignore */ } + try { fs.unlinkSync(getPipPath); } catch { /* 忽略清理失败。 */ } return false; } } diff --git a/electron/services/AdbService.ts b/electron/services/AdbService.ts new file mode 100644 index 0000000..e90302c --- /dev/null +++ b/electron/services/AdbService.ts @@ -0,0 +1,160 @@ +/** + * 管理 ADB 设备发现、连接和断开。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { AppPaths } from './AppPaths'; + +const execFileAsync = promisify(execFile); + +export interface AdbDevice { + serial: string; + status: string; +} + +export interface AdbCommandResult { + stdout: unknown; + stderr: unknown; +} + +export interface AdbCommandOptions { + windowsHide: true; + timeout: number; + encoding: 'utf8'; +} + +export interface AdbServiceDependencies { + execute( + executable: string, + args: string[], + options: AdbCommandOptions, + ): Promise; +} + +export interface AdbDeviceCommandResult { + success: boolean; + serial: string; + status: string; + message: string; +} + +/** 执行 ADB 设备查询和连接状态确认。 */ +export class AdbService { + constructor( + private readonly appPaths: AppPaths, + private readonly dependencies: AdbServiceDependencies = { + execute: async (executable, args, options) => { + const result = await execFileAsync(executable, args, options); + return { + stdout: result.stdout, + stderr: result.stderr, + }; + }, + }, + ) {} + + /** 返回内置 ADB;不存在时继续使用系统命令。 */ + executable(): string { + const bundledAdb = path.join( + this.appPaths.appRoot(), + 'adb', + 'adb.exe', + ); + return fs.existsSync(bundledAdb) ? bundledAdb : 'adb'; + } + + /** 读取并解析 adb devices 的当前设备列表。 */ + async listDevices(): Promise { + const { stdout } = await this.dependencies.execute( + this.executable(), + ['devices'], + { + windowsHide: true, + timeout: 5000, + encoding: 'utf8', + }, + ); + return String(stdout) + .split(/\r?\n/) + .slice(1) + .map(line => line.trim()) + .filter(Boolean) + .map(line => { + const [serial, status] = line.split(/\s+/); + return { + serial, + status: status || 'unknown', + }; + }); + } + + /** 执行 connect 或 disconnect,并通过设备列表确认结果。 */ + async runDeviceCommand( + command: 'connect' | 'disconnect', + rawSerial: string, + ): Promise { + const serial = String(rawSerial ?? '').trim(); + if (!serial || !/^[A-Za-z0-9._:[\]-]+$/.test(serial)) { + return { + success: false, + serial, + status: 'invalid', + message: 'ADB 地址格式不正确', + }; + } + + try { + const { stdout, stderr } = await this.dependencies.execute( + this.executable(), + [command, serial], + { + windowsHide: true, + timeout: 10000, + encoding: 'utf8', + }, + ); + const devices = await this.listDevices(); + const status = devices.find( + device => device.serial === serial, + )?.status; + const success = command === 'connect' + ? status === 'device' + : status === undefined; + return { + success, + serial, + status: status ?? 'disconnected', + message: [stdout, stderr] + .map(value => String(value).trim()) + .filter(Boolean) + .join('\n') + || ( + success + ? '操作成功' + : '操作后设备状态未达到预期' + ), + }; + } catch (error) { + const details = error as { + message?: string; + stdout?: string; + stderr?: string; + }; + return { + success: false, + serial, + status: 'error', + message: [ + details.stderr, + details.stdout, + details.message, + ] + .map(value => String(value ?? '').trim()) + .find(Boolean) + || 'ADB 命令执行失败', + }; + } + } +} diff --git a/electron/services/AppPaths.ts b/electron/services/AppPaths.ts new file mode 100644 index 0000000..f6dfdce --- /dev/null +++ b/electron/services/AppPaths.ts @@ -0,0 +1,85 @@ +/** + * 集中提供应用、资源和用户数据目录。 + */ +import * as path from 'path'; + +/** Electron 路径能力的最小依赖,便于主进程注入和独立测试。 */ +export interface AppPathsDependencies { + readonly moduleDirectory: string; + isPackaged(): boolean; + getPath(name: 'exe' | 'userData'): string; + getResourcesPath(): string; +} + +/** 集中计算主进程使用的应用、资源和用户数据路径。 */ +export class AppPaths { + constructor(private readonly dependencies: AppPathsDependencies) {} + + /** 是否处于打包后的生产模式。 */ + isPackaged(): boolean { + return this.dependencies.isPackaged(); + } + + /** 返回开发项目根目录或打包后的可执行文件目录。 */ + appRoot(): string { + if (this.isPackaged()) { + return path.dirname(this.dependencies.getPath('exe')); + } + return path.join(this.dependencies.moduleDirectory, '..', '..'); + } + + /** extraResources 根目录。 */ + resourceRoot(): string { + if (this.isPackaged()) { + return this.dependencies.getResourcesPath(); + } + return path.join(this.dependencies.moduleDirectory, '..', '..'); + } + + /** Electron 管理的用户数据根目录。 */ + userDataRoot(): string { + return this.dependencies.getPath('userData'); + } + + /** 内置只读作战计划目录。 */ + systemBattlePlansDir(): string { + return path.join( + this.resourceRoot(), + 'resource', + 'system_battle_plans', + ); + } + + /** GUI 管理的用户作战计划目录。 */ + userBattlePlansDir(): string { + return path.join(this.userDataRoot(), 'user_battle_plans'); + } + + /** 内置只读日常任务计划目录。 */ + systemDailyPlansDir(): string { + return path.join( + this.resourceRoot(), + 'resource', + 'system_daily_plans', + ); + } + + /** GUI 管理的用户日常任务计划目录。 */ + userDailyPlansDir(): string { + return path.join(this.userDataRoot(), 'user_daily_plans'); + } + + /** 内置只读编队计划目录。 */ + systemTeamPlansDir(): string { + return path.join( + this.resourceRoot(), + 'resource', + 'system_team_plans', + ); + } + + /** GUI 管理的用户编队计划目录。 */ + userTeamPlansDir(): string { + return path.join(this.userDataRoot(), 'user_team_plans'); + } +} diff --git a/electron/services/AtomicFileStore.ts b/electron/services/AtomicFileStore.ts new file mode 100644 index 0000000..206d887 --- /dev/null +++ b/electron/services/AtomicFileStore.ts @@ -0,0 +1,57 @@ +/** + * 通过同目录临时文件和原子替换完成持久化写入。 + */ +import * as fs from 'fs'; + +const WINDOWS_RETRY_DELAYS_MS = [20, 50, 100]; +const WINDOWS_TRANSIENT_CODES = new Set(['EACCES', 'EBUSY', 'EPERM']); + +/** 为需要失败回滚的持久化模块提供统一写入能力。 */ +export class AtomicFileStore { + /** 把 UTF-8 文本写入目标文件,并在失败时保留原文件。 */ + write(filePath: string, content: string): void { + const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`; + try { + this.retryWindowsFileLock(() => { + fs.writeFileSync(temporary, content, 'utf-8'); + }); + this.retryWindowsFileLock(() => { + fs.renameSync(temporary, filePath); + }); + } catch (error) { + try { + fs.rmSync(temporary, { force: true }); + } catch { + // 清理失败不能覆盖最初的替换错误。 + } + throw error; + } + } + + /** Windows 文件扫描或短暂占用时,等待后重试文件操作。 */ + private retryWindowsFileLock(operation: () => void): void { + for (let attempt = 0; ; attempt += 1) { + try { + operation(); + return; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + const delay = WINDOWS_RETRY_DELAYS_MS[attempt]; + if ( + process.platform !== 'win32' + || !code + || !WINDOWS_TRANSIENT_CODES.has(code) + || delay === undefined + ) { + throw error; + } + Atomics.wait( + new Int32Array(new SharedArrayBuffer(4)), + 0, + 0, + delay, + ); + } + } + } +} diff --git a/electron/services/BackendRuntimeContract.ts b/electron/services/BackendRuntimeContract.ts new file mode 100644 index 0000000..4b3bb7a --- /dev/null +++ b/electron/services/BackendRuntimeContract.ts @@ -0,0 +1,122 @@ +/** + * 定义 GUI 与 AutoWSGR 后端之间的正式运行契约。 + * + * GUI 只通过环境变量控制 OCR 加速和截图保存,不修改后端类、 + * 函数或日志模块的私有成员。启动前会检查当前后端是否支持这些 + * 环境变量,并确认实际导入的源码来自当前环境声明的唯一来源。 + */ +import type { PythonEnvironment } from '../pythonEnv'; +import { + buildBackendRuntimeContractProbeLines, +} from '../pythonEnv/backendContractProbe'; + +export type BackendOcrGpuMode = 'auto' | 'cpu' | 'cuda'; +export type ResolvedBackendOcrGpuMode = Exclude< + BackendOcrGpuMode, + 'auto' +>; + +export interface BackendRuntimeSettings { + ocrGpuMode: ResolvedBackendOcrGpuMode; + saveImages: boolean; +} + +export const BACKEND_RUNTIME_CONTRACT = 'gui-runtime-env-v1'; + +/** 将 GUI 模式与 CUDA 探测结果转换为后端支持的明确模式。 */ +export function selectBackendOcrGpuMode( + requestedMode: BackendOcrGpuMode, + cudaAvailable: boolean, +): ResolvedBackendOcrGpuMode { + if (requestedMode === 'cpu') return 'cpu'; + if (cudaAvailable) return 'cuda'; + if (requestedMode === 'auto') return 'cpu'; + throw new Error('已强制使用 CUDA,但未检测到可用 CUDA'); +} + +function pythonLiteral(value: string): string { + return value + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'"); +} + +function pythonPathSetup(environment: PythonEnvironment): string[] { + return [ + 'import sys, os, site', + ...(environment.useLocalSite + ? [ + `sp = r'${pythonLiteral(environment.localSite)}'`, + 'sys.path.insert(0, sp)', + 'site.addsitedir(sp)', + ] + : []), + ...(environment.backendRoot + ? [ + `repo = r'${pythonLiteral(environment.backendRoot)}'`, + 'sys.path.insert(0, repo)', + ] + : []), + ]; +} + +function sourceVerification(environment: PythonEnvironment): string[] { + const expectedRoot = environment.backendRoot + ?? environment.localSite; + return [ + 'from pathlib import Path', + 'import autowsgr', + `_expected_root = Path(r'${pythonLiteral(expectedRoot)}').resolve()`, + '_autowsgr_file = Path(autowsgr.__file__).resolve()', + 'if not _autowsgr_file.is_relative_to(_expected_root):', + " raise RuntimeError('GUI 后端来源错误: ' + str(_autowsgr_file))", + ]; +} + +/** 将 GUI 运行设置写入后端已经公开支持的环境变量。 */ +export function applyBackendRuntimeSettings( + baseEnv: NodeJS.ProcessEnv, + settings: BackendRuntimeSettings, +): NodeJS.ProcessEnv { + return { + ...baseEnv, + AUTOWSGR_OCR_GPU_MODE: settings.ocrGpuMode, + AUTOWSGR_SAVE_IMAGES: settings.saveImages ? 'true' : 'false', + }; +} + +/** 生成启动前能力探测脚本;不满足契约时由 Python 明确报错。 */ +export function buildBackendCapabilityProbe( + environment: PythonEnvironment, +): string { + return [ + ...pythonPathSetup(environment), + ...sourceVerification(environment), + ...buildBackendRuntimeContractProbeLines(), + '_verify_gui_runtime_contract()', + 'from autowsgr.server.main import app as _backend_app', + 'import inspect as _inspect', + 'if not callable(_backend_app):', + " raise RuntimeError('AutoWSGR server app 不可调用')", + '_app_call = getattr(_backend_app, "__call__", None)', + 'if not (_inspect.iscoroutinefunction(_backend_app) or _inspect.iscoroutinefunction(_app_call)):', + " raise RuntimeError('AutoWSGR server app 不是 ASGI 应用')", + `print('[Contract] ${BACKEND_RUNTIME_CONTRACT}')`, + "print('[Contract] autowsgr=' + str(_autowsgr_file))", + ].join('\n'); +} + +/** 生成通过能力检查后使用的最小后端启动脚本。 */ +export function buildBackendBootstrap( + environment: PythonEnvironment, + port: number, +): string { + return [ + ...pythonPathSetup(environment), + ...sourceVerification(environment), + "print('[Bootstrap] autowsgr=' + str(_autowsgr_file))", + "print('[Bootstrap] ocr_gpu_mode=' + os.environ.get('AUTOWSGR_OCR_GPU_MODE', 'cpu'))", + "print('[Bootstrap] save_backend_screenshots=' + os.environ.get('AUTOWSGR_SAVE_IMAGES', 'false'))", + 'import uvicorn', + `uvicorn.run('autowsgr.server.main:app', host='127.0.0.1', port=${port})`, + ].join('\n'); +} diff --git a/electron/services/BackendService.ts b/electron/services/BackendService.ts new file mode 100644 index 0000000..4cd132b --- /dev/null +++ b/electron/services/BackendService.ts @@ -0,0 +1,461 @@ +/** + * 管理 Python 后端的环境、进程和日志。 + */ +import * as path from 'path'; +import * as fs from 'fs'; +import { + execFile, + execSync, + spawn, + ChildProcess, +} from 'child_process'; +import type { BrowserWindow } from 'electron'; +import { + buildBackendRuntimeEnvironment, + ensurePthFile, + ensureSslCertForPython, + findPython, + resolveConfiguredCudaRoot, + resolvePythonEnvironment, +} from '../pythonEnv'; +import { + applyBackendRuntimeSettings, + buildBackendBootstrap, + buildBackendCapabilityProbe, + selectBackendOcrGpuMode, + type BackendOcrGpuMode, + type ResolvedBackendOcrGpuMode, +} from './BackendRuntimeContract'; +import { shutdownBackendProcess } from './BackendShutdownService'; + +export { buildBackendRuntimeEnvironment } from '../pythonEnv'; + +export interface BackendContext { + appRoot: () => string; + userDataRoot: () => string; + resourceRoot: () => string; + BACKEND_PORT: number; + getMainWindow: () => BrowserWindow | null; +} + +let ctx: BackendContext; +let backendProcess: ChildProcess | null = null; +let backendStopPromise: Promise | null = null; + +/** 注入 Electron 运行时能力。 */ +export function initBackend(context: BackendContext): void { + ctx = context; +} + +/** 返回当前后端进程;未启动或已退出时返回 null。 */ +export function getBackendProcess(): ChildProcess | null { + return backendProcess; +} + +function readGuiSettings(): Record { + try { + const settingsPath = path.join( + ctx.userDataRoot(), + 'gui_settings.json', + ); + if (!fs.existsSync(settingsPath)) return {}; + return JSON.parse( + fs.readFileSync(settingsPath, 'utf-8'), + ) as Record; + } catch { + return {}; + } +} + +function readOcrGpuModeFromSettings(): BackendOcrGpuMode { + const value = readGuiSettings().ocr_gpu_mode; + if (value === 'cpu' || value === 'cuda') return value; + return 'auto'; +} + +function readCudaPathFromSettings(): string | null { + const value = readGuiSettings().cuda_path; + const cudaRoot = resolveConfiguredCudaRoot(value); + if ( + typeof value === 'string' + && value.trim() + && !cudaRoot + ) { + console.warn( + `[Backend] 忽略 cuda_path(未找到 Toolkit 或 CUDA Runtime DLL): ${path.resolve(value.trim())}`, + ); + } + return cudaRoot; +} + +function readSaveBackendScreenshotsFromSettings(): boolean { + return readGuiSettings().save_backend_screenshots === true; +} + +/** 使用后端实际运行环境解析 OCR 的自动/强制 CUDA 模式。 */ +function resolveBackendOcrGpuMode( + pythonCommand: string, + requestedMode: BackendOcrGpuMode, + cwd: string, + processEnv: NodeJS.ProcessEnv, +): Promise { + if (requestedMode === 'cpu') return Promise.resolve('cpu'); + + const probe = [ + 'import json', + 'try:', + ' import torch', + ' result = {', + ' "available": bool(torch.cuda.is_available()),', + ' "torch_version": str(torch.__version__),', + ' "cuda_version": getattr(torch.version, "cuda", None),', + ' }', + 'except Exception as exc:', + ' result = {"available": False, "error": str(exc)}', + 'print(json.dumps(result, ensure_ascii=False))', + ].join('\n'); + + return new Promise((resolve, reject) => { + execFile( + pythonCommand, + ['-X', 'utf8', '-c', probe], + { + cwd, + windowsHide: true, + timeout: 20000, + encoding: 'utf8', + env: processEnv, + }, + (error, stdout, stderr) => { + let detail = String(stderr || error?.message || '').trim(); + let available = false; + if (!error) { + try { + const output = String(stdout) + .trim() + .split(/\r?\n/) + .filter(Boolean) + .at(-1); + const result = JSON.parse(output || '{}') as { + available?: boolean; + torch_version?: string; + cuda_version?: string | null; + error?: string; + }; + available = result.available === true; + detail = result.error + ?? `PyTorch ${result.torch_version ?? 'unknown'}, CUDA ${ + result.cuda_version ?? '不可用' + }`; + } catch (parseError) { + detail = parseError instanceof Error + ? parseError.message + : String(parseError); + } + } + + try { + resolve(selectBackendOcrGpuMode(requestedMode, available)); + } catch { + reject(new Error( + `已强制使用 CUDA,但后端运行环境未检测到可用 CUDA:${detail || '无检测结果'}`, + )); + return; + } + if (!available) { + console.warn( + `[Backend] 未检测到可用 CUDA,OCR 自动切换为 CPU: ${detail || '无检测结果'}`, + ); + } + }, + ); + }); +} + +/** 启动前验证后端来源、服务入口和正式运行设置契约。 */ +function verifyBackendRuntimeContract( + pythonCommand: string, + environment: ReturnType, + cwd: string, + processEnv: NodeJS.ProcessEnv, +): Promise { + const probe = buildBackendCapabilityProbe(environment); + return new Promise((resolve, reject) => { + execFile( + pythonCommand, + ['-X', 'utf8', '-c', probe], + { + cwd, + windowsHide: true, + timeout: 30000, + encoding: 'utf8', + env: processEnv, + }, + (error, stdout, stderr) => { + if (!error) { + const message = String(stdout).trim(); + if (message) console.log(`[Backend] ${message}`); + resolve(); + return; + } + const detail = String(stderr || stdout || error.message) + .trim() + .slice(-2000); + reject(new Error( + `后端版本或能力不兼容,已阻止启动:${detail || error.message}`, + )); + }, + ); + }); +} + +/** 运行 setup.bat 安装环境。 */ +export function runSetupScript(): Promise<{ + success: boolean; + output: string; +}> { + return new Promise((resolve) => { + let setupPath = path.join(ctx.resourceRoot(), 'setup.bat'); + if (!fs.existsSync(setupPath)) { + setupPath = path.join(ctx.appRoot(), 'setup.bat'); + } + if (!fs.existsSync(setupPath)) { + resolve({ success: false, output: '找不到 setup.bat' }); + return; + } + + const proc = spawn('cmd.exe', ['/c', setupPath], { + cwd: ctx.appRoot(), + windowsHide: false, + stdio: 'pipe', + }); + + let output = ''; + proc.stdout?.on('data', (data: Buffer) => { + const text = data.toString(); + output += text; + ctx.getMainWindow()?.webContents.send('setup-log', text); + }); + proc.stderr?.on('data', (data: Buffer) => { + const text = data.toString(); + output += text; + ctx.getMainWindow()?.webContents.send('setup-log', text); + }); + proc.on('close', (code) => { + resolve({ success: code === 0, output: output.slice(-1000) }); + }); + proc.on('error', (error) => { + resolve({ success: false, output: error.message }); + }); + }); +} + +/** 使用当前唯一 Python 环境启动 AutoWSGR 后端。 */ +export async function startBackend(): Promise { + const pythonCmd = await findPython(); + if (!pythonCmd) { + throw new Error('找不到兼容的 Python(需要 3.12 或 3.13)'); + } + const environment = resolvePythonEnvironment(pythonCmd); + if (environment.useLocalSite) ensurePthFile(); + + const certFile = await ensureSslCertForPython(pythonCmd); + if (certFile) console.log(`[Backend] TLS cert: ${certFile}`); + else { + console.warn( + '[Backend] WARNING 未检测到 TLS 根证书,HTTPS 请求可能失败', + ); + } + + const cwd = ctx.appRoot(); + const localBackendRepo = environment.backendRoot; + const requestedOcrGpuMode = readOcrGpuModeFromSettings(); + const configuredCudaRoot = readCudaPathFromSettings(); + const saveBackendScreenshots = readSaveBackendScreenshotsFromSettings(); + const bootstrap = buildBackendBootstrap( + environment, + ctx.BACKEND_PORT, + ); + const mainWindow = ctx.getMainWindow(); + if (localBackendRepo) { + console.log(`[Backend] 使用本地后端仓库: ${localBackendRepo}`); + mainWindow?.webContents.send( + 'backend-log', + `[GUI] 使用本地后端仓库: ${localBackendRepo}`, + ); + } else { + mainWindow?.webContents.send( + 'backend-log', + '[GUI] 未启用本地后端仓库覆盖,使用 site-packages 中的 autowsgr', + ); + } + mainWindow?.webContents.send( + 'backend-log', + `[GUI] CUDA 路径: ${configuredCudaRoot ?? '系统自动检测'}`, + ); + mainWindow?.webContents.send( + 'backend-log', + `[GUI] 保存识别异常截图: ${saveBackendScreenshots ? '开启' : '关闭'}`, + ); + + const adbDir = path.join(ctx.appRoot(), 'adb'); + const cudaEnv = buildBackendRuntimeEnvironment( + environment, + configuredCudaRoot, + ); + const envPath = cudaEnv.PATH || ''; + const pathWithAdb = fs.existsSync(adbDir) + ? `${adbDir}${path.delimiter}${envPath}` + : envPath; + const runtimeEnv = { + ...cudaEnv, + PYTHONUTF8: '1', + PYTHONIOENCODING: 'utf-8', + PATH: pathWithAdb, + }; + const resolvedOcrGpuMode = await resolveBackendOcrGpuMode( + pythonCmd, + requestedOcrGpuMode, + cwd, + runtimeEnv, + ); + mainWindow?.webContents.send( + 'backend-log', + `[GUI] OCR 加速模式: ${requestedOcrGpuMode}${ + requestedOcrGpuMode === 'auto' + ? ` -> ${resolvedOcrGpuMode}` + : '' + }`, + ); + const backendEnv = applyBackendRuntimeSettings( + runtimeEnv, + { + ocrGpuMode: resolvedOcrGpuMode, + saveImages: saveBackendScreenshots, + }, + ); + + await verifyBackendRuntimeContract( + pythonCmd, + environment, + cwd, + backendEnv, + ); + mainWindow?.webContents.send( + 'backend-log', + '[GUI] 后端运行契约验证通过', + ); + + // MuMu 多开实例不会自动被 ADB 发现,因此启动前主动连接。 + try { + const cfgPath = path.join(ctx.userDataRoot(), 'usersettings.yaml'); + if (fs.existsSync(cfgPath)) { + const cfgText = fs.readFileSync(cfgPath, 'utf-8'); + const serialMatch = cfgText.match(/serial:\s*(\S+)/); + if (serialMatch) { + const serial = serialMatch[1]; + const adbExe = path.join(adbDir, 'adb.exe'); + const adbCmd = fs.existsSync(adbExe) ? adbExe : 'adb'; + execSync(`"${adbCmd}" connect ${serial}`, { + windowsHide: true, + timeout: 5000, + stdio: 'pipe', + }); + console.log(`[Backend] ADB connect ${serial} 完成`); + } + } + } catch (error: any) { + console.warn(`[Backend] ADB connect 失败 (非致命): ${error.message}`); + } + + const spawnedProcess = spawn( + pythonCmd, + ['-X', 'utf8', '-c', bootstrap], + { + cwd, + windowsHide: true, + stdio: 'pipe', + env: backendEnv, + }, + ); + backendProcess = spawnedProcess; + + const CYAN = '\x1b[36m'; + const RED = '\x1b[31m'; + const YELLOW = '\x1b[33m'; + const GREEN = '\x1b[32m'; + const DIM = '\x1b[2m'; + const RESET = '\x1b[0m'; + + const colorLine = (line: string): string => { + if (/\bERROR\b/i.test(line)) return `${RED}${line}${RESET}`; + if (/\bWARNING\b/i.test(line)) return `${YELLOW}${line}${RESET}`; + if (/\bINFO\b/i.test(line)) return `${GREEN}${line}${RESET}`; + if (/\bDEBUG\b/i.test(line)) return `${DIM}${line}${RESET}`; + return `${CYAN}${line}${RESET}`; + }; + + const LOGURU_LINE_RE = /^\d{2}:\d{2}:\d{2}\.\d{3}\s*\|/; + let skipMultiline = false; + + const handleOutput = (data: Buffer) => { + for (const line of data.toString('utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed) continue; + console.log(`${CYAN}[Backend]${RESET} ${colorLine(trimmed)}`); + + const isNewEntry = LOGURU_LINE_RE.test(trimmed); + if (isNewEntry) { + skipMultiline = /\bDEBUG\b/i.test(trimmed); + } + if (skipMultiline) continue; + if ( + /"(?:GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\s+\//.test(trimmed) + ) { + continue; + } + mainWindow?.webContents.send('backend-log', trimmed); + } + }; + spawnedProcess.stdout?.on('data', handleOutput); + spawnedProcess.stderr?.on('data', handleOutput); + spawnedProcess.on('error', (error) => { + console.error('[Backend] 启动失败:', error.message); + if (backendProcess === spawnedProcess) backendProcess = null; + }); + spawnedProcess.on('close', (code) => { + console.log(`[Backend] 进程退出, code=${code}`); + if (backendProcess === spawnedProcess) backendProcess = null; + }); +} + +/** 停止后端任务和完整进程树,并等待操作系统确认进程退出。 */ +export async function stopBackend(): Promise { + if (backendStopPromise) return backendStopPromise; + const activeProcess = backendProcess; + if (!activeProcess) return; + if ( + activeProcess.exitCode !== null + || activeProcess.signalCode !== null + ) { + if (backendProcess === activeProcess) backendProcess = null; + return; + } + + backendStopPromise = shutdownBackendProcess( + activeProcess, + { backendPort: ctx.BACKEND_PORT }, + ).finally(() => { + if ( + backendProcess === activeProcess + && ( + activeProcess.exitCode !== null + || activeProcess.signalCode !== null + ) + ) { + backendProcess = null; + } + backendStopPromise = null; + }); + return backendStopPromise; +} diff --git a/electron/services/BackendShutdownService.ts b/electron/services/BackendShutdownService.ts new file mode 100644 index 0000000..520b689 --- /dev/null +++ b/electron/services/BackendShutdownService.ts @@ -0,0 +1,241 @@ +/** + * 后端进程安全关闭流程。 + * + * 1. 调用后端系统停止接口,让运行中的任务完成清理。 + * 2. 终止后端服务进程;Windows 同时处理完整进程树。 + * 3. 等待进程 close,确认操作系统已经释放进程资源。 + * 4. 超时后才强制终止,并再次等待 close。 + * + * 更新安装和应用退出共用这一流程,任何阶段无法确认进程退出时都会 + * 抛出错误,调用方不得继续安装更新。 + */ +import { execFile } from 'child_process'; +import type { ChildProcess } from 'child_process'; +import * as http from 'http'; +import { platform as nodePlatform } from 'process'; + +export interface BackendShutdownOptions { + backendPort: number; + systemStopTimeoutMs?: number; + terminateTimeoutMs?: number; + forceTerminateTimeoutMs?: number; +} + +export interface BackendShutdownDependencies { + platform?: NodeJS.Platform; + requestSystemStop?: (port: number, timeoutMs: number) => Promise; + terminateProcessTree?: ( + process: ChildProcess, + force: boolean, + platform: NodeJS.Platform, + ) => Promise; + waitForProcessClose?: ( + process: ChildProcess, + timeoutMs: number, + ) => Promise; + warn?: (message: string) => void; +} + +const DEFAULT_SYSTEM_STOP_TIMEOUT_MS = 35000; +const DEFAULT_TERMINATE_TIMEOUT_MS = 5000; +const DEFAULT_FORCE_TERMINATE_TIMEOUT_MS = 5000; + +function hasExited(process: ChildProcess): boolean { + return process.exitCode !== null || process.signalCode !== null; +} + +function executeFile( + command: string, + args: string[], +): Promise { + return new Promise((resolve, reject) => { + execFile( + command, + args, + { + windowsHide: true, + timeout: 10000, + encoding: 'utf8', + }, + (error, _stdout, _stderr) => { + if (!error) { + resolve(); + return; + } + const exitCode = typeof error.code === 'number' + || typeof error.code === 'string' + ? error.code + : 'unknown'; + reject(new Error(`${command} 执行失败,退出码 ${exitCode}`)); + }, + ); + }); +} + +/** 调用后端正式停止接口并验证返回结果。 */ +export function requestBackendSystemStop( + port: number, + timeoutMs: number, +): Promise { + return new Promise((resolve, reject) => { + const request = http.request( + { + hostname: '127.0.0.1', + port, + path: '/api/system/stop', + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Length': '0', + }, + }, + response => { + let body = ''; + response.setEncoding('utf8'); + response.on('data', chunk => { + body += chunk; + }); + response.on('end', () => { + const statusCode = response.statusCode ?? 0; + if (statusCode < 200 || statusCode >= 300) { + reject(new Error( + `后端停止接口返回 HTTP ${statusCode}: ${body.trim()}`, + )); + return; + } + try { + const result = JSON.parse(body) as { + success?: boolean; + message?: string; + error?: string; + }; + if (result.success !== true) { + reject(new Error( + result.error + || result.message + || '后端拒绝停止当前任务', + )); + return; + } + resolve(); + } catch (error) { + reject(new Error( + `后端停止接口返回无效 JSON: ${ + error instanceof Error ? error.message : String(error) + }`, + )); + } + }); + }, + ); + request.setTimeout(timeoutMs, () => { + request.destroy(new Error(`后端任务停止等待超过 ${timeoutMs}ms`)); + }); + request.on('error', reject); + request.end(); + }); +} + +/** 等待进程真正退出;返回 false 表示超时。 */ +export function waitForProcessClose( + process: ChildProcess, + timeoutMs: number, +): Promise { + if (hasExited(process)) return Promise.resolve(true); + return new Promise(resolve => { + const finish = (closed: boolean): void => { + clearTimeout(timer); + process.removeListener('close', onClose); + resolve(closed); + }; + const onClose = (): void => finish(true); + const timer = setTimeout(() => finish(hasExited(process)), timeoutMs); + process.once('close', onClose); + }); +} + +/** 终止服务进程;Windows 使用 /T 保证完整进程树被处理。 */ +export async function terminateBackendProcessTree( + process: ChildProcess, + force: boolean, + platform: NodeJS.Platform, +): Promise { + if (hasExited(process)) return; + if (platform === 'win32') { + if (!process.pid) throw new Error('后端进程没有可用 PID'); + const args = ['/PID', String(process.pid), '/T']; + if (force) args.push('/F'); + await executeFile('taskkill.exe', args); + return; + } + + const signal: NodeJS.Signals = force ? 'SIGKILL' : 'SIGTERM'; + if (!process.kill(signal) && !hasExited(process)) { + throw new Error(`无法向后端进程发送 ${signal}`); + } +} + +/** 执行完整关闭流程;无法确认退出时抛错并阻止更新安装。 */ +export async function shutdownBackendProcess( + process: ChildProcess, + options: BackendShutdownOptions, + dependencies: BackendShutdownDependencies = {}, +): Promise { + if (hasExited(process)) return; + + const platform = dependencies.platform ?? nodePlatform; + const requestSystemStop = dependencies.requestSystemStop + ?? requestBackendSystemStop; + const terminateProcessTree = dependencies.terminateProcessTree + ?? terminateBackendProcessTree; + const waitForClose = dependencies.waitForProcessClose + ?? waitForProcessClose; + const warn = dependencies.warn ?? (message => console.warn(message)); + + try { + await requestSystemStop( + options.backendPort, + options.systemStopTimeoutMs ?? DEFAULT_SYSTEM_STOP_TIMEOUT_MS, + ); + } catch (error) { + warn( + `[Backend] 优雅停止任务失败,将继续关闭进程:${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + if (hasExited(process)) return; + + try { + await terminateProcessTree(process, false, platform); + } catch (error) { + warn( + `[Backend] 普通终止失败,将等待后强制终止:${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + const terminated = await waitForClose( + process, + options.terminateTimeoutMs ?? DEFAULT_TERMINATE_TIMEOUT_MS, + ); + if (terminated) return; + + let forceError: unknown = null; + try { + await terminateProcessTree(process, true, platform); + } catch (error) { + forceError = error; + } + const forced = await waitForClose( + process, + options.forceTerminateTimeoutMs + ?? DEFAULT_FORCE_TERMINATE_TIMEOUT_MS, + ); + if (forced) return; + + const detail = forceError instanceof Error + ? `:${forceError.message}` + : ''; + throw new Error(`无法确认后端进程树已经退出${detail}`); +} diff --git a/electron/services/CombatPlanCodec.ts b/electron/services/CombatPlanCodec.ts new file mode 100644 index 0000000..6a54181 --- /dev/null +++ b/electron/services/CombatPlanCodec.ts @@ -0,0 +1,200 @@ +/** + * 校验、拆分、展开并序列化出征计划。 + */ +import * as yaml from 'js-yaml'; +import { + type PlanPresetSource, + TeamPlanCodec, + type UserTeamPlan, +} from './TeamPlanCodec'; +import { TeamPlanRepository } from './TeamPlanRepository'; + +export interface SplitCombatPlan { + mapRoot: Record; + teams: UserTeamPlan[]; +} + +/** 负责出征计划的拆分、展开、序列化和名称清理。 */ +export class CombatPlanCodec { + constructor( + private readonly teamCodec: TeamPlanCodec, + private readonly teamRepository: TeamPlanRepository, + ) {} + + /** 判断未知值是否为可读取的普通对象。 */ + isPlainObject(value: unknown): value is Record { + return Boolean(value) + && typeof value === 'object' + && !Array.isArray(value); + } + + /** 解析 YAML,并使用调用方指定的既有错误文本校验根对象。 */ + parseRoot( + content: string, + invalidRootMessage: string, + ): Record { + const parsed = yaml.load(content); + if (!this.isPlainObject(parsed)) { + throw new Error(invalidRootMessage); + } + return parsed; + } + + /** 序列化计划并保留原文件开头的注释。 */ + serialize( + root: Record, + originalContent = '', + ): string { + const leadingComments: string[] = []; + for (const line of originalContent.split(/\r?\n/)) { + const trimmed = line.trimStart(); + if (trimmed.startsWith('#') || trimmed === '') { + leadingComments.push(line); + continue; + } + break; + } + const body = yaml.dump(root, { + lineWidth: -1, + noCompatMode: true, + noRefs: true, + sortKeys: false, + }); + const prefix = leadingComments.some( + line => line.trimStart().startsWith('#'), + ) + ? `${leadingComments.join('\n').replace(/\s+$/, '')}\n` + : ''; + return `${prefix}${body}`; + } + + /** 把内嵌舰队拆成独立编队,并在地图中只保留名称引用。 */ + normalizeFleetPresets( + root: Record, + source: PlanPresetSource, + requireEmbeddedShips: boolean, + ): SplitCombatPlan { + this.requireMapCoordinates(root); + if (root.fleet_presets === undefined) { + return { + mapRoot: structuredClone(root), + teams: [], + }; + } + if (!Array.isArray(root.fleet_presets)) { + throw new Error('fleet_presets 必须是列表'); + } + + const names = new Set(); + const teams: UserTeamPlan[] = []; + const references = root.fleet_presets.map((rawPreset, index) => { + if (!this.isPlainObject(rawPreset)) { + throw new Error(`fleet_presets[${index}] 必须是对象`); + } + const name = typeof rawPreset.name === 'string' + ? rawPreset.name.trim() + : ''; + if (!name) { + throw new Error(`fleet_presets[${index}].name 不能为空`); + } + if (names.has(name)) { + throw new Error(`fleet_presets 中存在重复舰队名称:${name}`); + } + names.add(name); + + if (Array.isArray(rawPreset.ships)) { + teams.push(this.teamCodec.normalize({ + name, + ships: rawPreset.ships, + })); + } else { + if (requireEmbeddedShips) { + throw new Error(`旧计划中的舰队「${name}」缺少 ships`); + } + if (!this.teamRepository.find(name, source)) { + throw new Error(`找不到舰队「${name}」的独立配置`); + } + } + return { name }; + }); + + return { + mapRoot: { + ...structuredClone(root), + fleet_presets: references, + }, + teams, + }; + } + + /** 为后端执行展开舰队引用,并保留引用对象的未知字段。 */ + expandRoot( + root: Record, + source: PlanPresetSource, + ): Record { + this.requireMapCoordinates(root); + if (root.fleet_presets === undefined) { + return structuredClone(root); + } + if (!Array.isArray(root.fleet_presets)) { + throw new Error('fleet_presets 必须是列表'); + } + + const listedTeams = this.teamRepository.list().plans; + const presets = root.fleet_presets.map((rawPreset, index) => { + if (!this.isPlainObject(rawPreset)) { + throw new Error(`fleet_presets[${index}] 必须是对象`); + } + const name = typeof rawPreset.name === 'string' + ? rawPreset.name.trim() + : ''; + if (!name) { + throw new Error(`fleet_presets[${index}].name 不能为空`); + } + + const sameSourceTeam = listedTeams.find(team => ( + team.name === name && team.source === source + )) ?? null; + const embeddedTeam = Array.isArray(rawPreset.ships) + ? this.teamCodec.normalize({ + name, + ships: rawPreset.ships, + }) + : null; + const team = sameSourceTeam + ?? embeddedTeam + ?? this.teamRepository.find(name, source, listedTeams); + if (!team) { + throw new Error(`地图引用的舰队「${name}」不存在`); + } + return { + ...structuredClone(rawPreset), + name, + ships: structuredClone(team.ships), + }; + }); + + return { + ...structuredClone(root), + fleet_presets: presets, + }; + } + + /** 清理出征计划名称,生成既有 bettle- 文件名前的名称部分。 */ + safeBaseName(value: string): string { + return value + .trim() + .replace(/\.ya?ml$/i, '') + .replace(/^bettle-/i, '') + .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') + .replace(/[. ]+$/g, '') + .slice(0, 100); + } + + /** 校验后端执行计划必须包含的地图坐标。 */ + private requireMapCoordinates(root: Record): void { + if (!('chapter' in root) || !('map' in root)) { + throw new Error('出征计划必须包含 chapter 和 map'); + } + } +} diff --git a/electron/services/CombatPlanRepository.ts b/electron/services/CombatPlanRepository.ts new file mode 100644 index 0000000..e6d2c1a --- /dev/null +++ b/electron/services/CombatPlanRepository.ts @@ -0,0 +1,116 @@ +/** + * 管理系统和用户出征计划文件。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { AppPaths } from './AppPaths'; +import { AtomicFileStore } from './AtomicFileStore'; +import { type PlanPresetSource } from './TeamPlanCodec'; + +/** 负责受管出征计划的路径、读取、写入、重命名和删除。 */ +export class CombatPlanRepository { + constructor( + private readonly appPaths: AppPaths, + private readonly atomicFiles: AtomicFileStore, + ) {} + + /** 初始化用户出征计划目录。 */ + initializeUserDirectory(): void { + fs.mkdirSync(this.appPaths.userBattlePlansDir(), { recursive: true }); + } + + /** 初始化系统出征计划目录。 */ + initializeSystemDirectory(): void { + fs.mkdirSync(this.appPaths.systemBattlePlansDir(), { recursive: true }); + } + + /** 初始化指定来源的出征计划目录。 */ + initializeDirectory(source: PlanPresetSource): void { + fs.mkdirSync(this.directory(source), { recursive: true }); + } + + /** 返回指定来源的权威出征计划目录。 */ + directory(source: PlanPresetSource): string { + return source === 'system' + ? this.appPaths.systemBattlePlansDir() + : this.appPaths.userBattlePlansDir(); + } + + /** 返回经过文件名边界校验的受管计划路径。 */ + safeManagedPath( + source: PlanPresetSource, + file: string, + ): string | null { + if ( + (source !== 'system' && source !== 'user') + || path.basename(file) !== file + || !/\.ya?ml$/i.test(file) + ) { + return null; + } + return path.join(this.directory(source), file); + } + + /** 返回经过文件名边界校验的用户计划路径。 */ + safeUserPath(file: string): string | null { + return this.safeManagedPath('user', file); + } + + /** 判断绝对路径是否为系统或用户受管计划。 */ + managedFromPath( + filePath: string, + ): { source: PlanPresetSource; file: string } | null { + const resolved = path.resolve(filePath); + for (const source of ['system', 'user'] as const) { + const directory = path.resolve(this.directory(source)); + if ( + path.dirname(resolved).toLowerCase() === directory.toLowerCase() + && /\.ya?ml$/i.test(path.basename(resolved)) + ) { + return { + source, + file: path.basename(resolved), + }; + } + } + return null; + } + + /** 列出目录中的 YAML 文件并保持既有排序规则。 */ + yamlFiles(directory: string): string[] { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory) + .filter(file => /\.ya?ml$/i.test(file)) + .sort((left, right) => left.localeCompare(right, 'zh-CN')); + } + + /** 读取一份出征计划原始文本。 */ + read(filePath: string): string { + return fs.readFileSync(filePath, 'utf-8'); + } + + /** 原子写入一份出征计划文本。 */ + write(filePath: string, content: string): void { + this.atomicFiles.write(filePath, content); + } + + /** 返回路径是否已存在。 */ + exists(filePath: string): boolean { + return fs.existsSync(filePath); + } + + /** 返回文件最后修改时间的毫秒值。 */ + modifiedAt(filePath: string): number { + return fs.statSync(filePath).mtimeMs; + } + + /** 使用既有文件系统语义重命名计划。 */ + rename(source: string, target: string): void { + fs.renameSync(source, target); + } + + /** 删除已有出征计划文件。 */ + remove(filePath: string): void { + fs.unlinkSync(filePath); + } +} diff --git a/electron/services/CudaEnvironmentService.ts b/electron/services/CudaEnvironmentService.ts new file mode 100644 index 0000000..65a99b7 --- /dev/null +++ b/electron/services/CudaEnvironmentService.ts @@ -0,0 +1,285 @@ +/** + * 校验 CUDA 路径并检测 PyTorch 的 CUDA 能力。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { + buildBackendRuntimeEnvironment, + resolvePythonEnvironment, +} from '../pythonEnv'; + +const execFileAsync = promisify(execFile); + +export interface CudaValidationResult { + valid: boolean; + path: string; + version: string | null; + kind?: 'toolkit' | 'runtime'; + torchVersion?: string | null; + device?: string | null; + error?: string; +} + +export interface CudaCommandOptions { + windowsHide: true; + timeout: 20000; + encoding: 'utf8'; + env: NodeJS.ProcessEnv; +} + +export interface CudaEnvironmentDependencies { + findPython(): Promise; + buildRuntimeEnvironment( + pythonCommand: string, + configuredCudaRoot: string | null, + ): NodeJS.ProcessEnv; + execute( + executable: string, + args: string[], + options: CudaCommandOptions, + ): Promise<{ stdout: unknown }>; +} + +/** 校验 CUDA 目录并检测当前 PyTorch 的实际 CUDA 能力。 */ +export class CudaEnvironmentService { + constructor( + private readonly dependencies: CudaEnvironmentDependencies, + ) {} + + /** 使用生产子进程执行器创建服务依赖。 */ + static createDependencies( + findPython: () => Promise, + ): CudaEnvironmentDependencies { + return { + findPython, + buildRuntimeEnvironment: (pythonCommand, configuredCudaRoot) => ( + buildBackendRuntimeEnvironment( + resolvePythonEnvironment(pythonCommand), + configuredCudaRoot, + ) + ), + execute: async (executable, args, options) => { + const result = await execFileAsync( + executable, + args, + options, + ); + return { stdout: result.stdout }; + }, + }; + } + + /** 将配置输入归一化为 Toolkit 根目录或 Runtime 目录。 */ + normalizePath(candidate: string): string { + const resolved = path.resolve(candidate.trim()); + if (this.findRuntimeDlls(resolved)) return resolved; + return path.basename(resolved).toLowerCase() === 'bin' + ? path.dirname(resolved) + : resolved; + } + + /** 只校验本地 CUDA 路径并读取可用版本信息。 */ + validatePath(candidate: string): CudaValidationResult { + if (!candidate.trim()) { + return { + valid: false, + path: '', + version: null, + error: '路径为空', + }; + } + const cudaRoot = this.normalizePath(candidate); + if (!fs.existsSync(cudaRoot)) { + return { + valid: false, + path: cudaRoot, + version: null, + error: '目录不存在', + }; + } + const binDirectory = path.join(cudaRoot, 'bin'); + const isToolkit = fs.existsSync( + path.join(binDirectory, 'nvcc.exe'), + ); + const runtimeDirectory = this.findRuntimeDlls(cudaRoot) + ? cudaRoot + : this.findRuntimeDlls(binDirectory) + ? binDirectory + : null; + if (!isToolkit && !runtimeDirectory) { + return { + valid: false, + path: cudaRoot, + version: null, + error: '未找到 CUDA Toolkit(bin\\nvcc.exe)或 PyTorch CUDA Runtime DLL', + }; + } + + let version: string | null = null; + try { + const versionJson = path.join(cudaRoot, 'version.json'); + if (fs.existsSync(versionJson)) { + const raw = JSON.parse( + fs.readFileSync(versionJson, 'utf-8') + .replace(/^\uFEFF/, ''), + ) as Record; + version = raw.cuda?.version + ?? raw.cuda_cudart?.version + ?? null; + } + } catch { + // 版本文件无效时回退到目录名。 + } + version ??= path.basename(cudaRoot) + .match(/v\d+(?:\.\d+)?/i)?.[0] + ?? null; + if (isToolkit) { + return { + valid: true, + path: cudaRoot, + version, + kind: 'toolkit', + }; + } + + let runtimeVersion: string | null = null; + try { + const cudart = fs.readdirSync(runtimeDirectory!) + .find(name => /^cudart64.*\.dll$/i.test(name)); + runtimeVersion = cudart + ?.match(/^cudart64[_-]?(\d+)/i)?.[1] + ?? null; + if (runtimeVersion?.length === 2) { + runtimeVersion = `${runtimeVersion[0]}.${runtimeVersion[1]}`; + } else if (runtimeVersion?.length === 3) { + runtimeVersion = `${ + runtimeVersion.slice(0, 2) + }.${runtimeVersion[2]}`; + } + } catch { + // 无法解析时保留未知版本。 + } + return { + valid: true, + path: runtimeDirectory!, + version: runtimeVersion, + kind: 'runtime', + }; + } + + /** 使用后端实际采用的 Python 检测 PyTorch、CUDA 和显卡。 */ + async detect(candidate: string): Promise { + const rawPath = candidate.trim(); + const pathResult = rawPath + ? this.validatePath(rawPath) + : null; + if (pathResult && !pathResult.valid) return pathResult; + + const pythonCommand = await this.dependencies.findPython(); + if (!pythonCommand) { + return { + valid: false, + path: pathResult?.path ?? '', + version: pathResult?.version ?? null, + kind: pathResult?.kind, + error: '未找到可用的 Python 3.12 或 3.13', + }; + } + + const script = [ + 'import json', + 'try:', + ' import torch', + ' available = bool(torch.cuda.is_available())', + ' result = {', + ' "available": available,', + ' "torch_version": str(torch.__version__),', + ' "cuda_version": getattr(torch.version, "cuda", None),', + ' "device": torch.cuda.get_device_name(0) if available else None,', + ' }', + 'except Exception as exc:', + ' result = {"available": False, "error": str(exc)}', + 'print(json.dumps(result, ensure_ascii=False))', + ].join('\n'); + + try { + const { stdout } = await this.dependencies.execute( + pythonCommand, + ['-c', script], + { + windowsHide: true, + timeout: 20000, + encoding: 'utf8', + env: this.dependencies.buildRuntimeEnvironment( + pythonCommand, + pathResult?.path ?? null, + ), + }, + ); + const output = String(stdout) + .trim() + .split(/\r?\n/) + .filter(Boolean) + .at(-1); + if (!output) throw new Error('Python 未返回检测结果'); + const detected = JSON.parse(output) as { + available?: boolean; + torch_version?: string; + cuda_version?: string | null; + device?: string | null; + error?: string; + }; + const version = detected.cuda_version + ?? pathResult?.version + ?? null; + if (!detected.available) { + return { + valid: false, + path: pathResult?.path ?? '', + version, + kind: pathResult?.kind, + torchVersion: detected.torch_version ?? null, + device: null, + error: detected.error + ? `PyTorch 检测失败:${detected.error}` + : `PyTorch ${ + detected.torch_version ?? '' + } 未检测到可用 CUDA` + .replace(/\s+/g, ' ') + .trim(), + }; + } + return { + valid: true, + path: pathResult?.path ?? '', + version, + kind: pathResult?.kind, + torchVersion: detected.torch_version ?? null, + device: detected.device ?? null, + }; + } catch (error) { + return { + valid: false, + path: pathResult?.path ?? '', + version: pathResult?.version ?? null, + kind: pathResult?.kind, + error: `硬件检测失败:${ + error instanceof Error ? error.message : String(error) + }`, + }; + } + } + + /** 检查目录中是否同时包含必要 CUDA Runtime DLL。 */ + private findRuntimeDlls(directory: string): boolean { + try { + const names = fs.readdirSync(directory); + return names.some(name => /^cudart64.*\.dll$/i.test(name)) + && names.some(name => /^cublas64.*\.dll$/i.test(name)); + } catch { + return false; + } + } +} diff --git a/electron/services/DailyPlanService.ts b/electron/services/DailyPlanService.ts new file mode 100644 index 0000000..3e35041 --- /dev/null +++ b/electron/services/DailyPlanService.ts @@ -0,0 +1,324 @@ +/** + * 管理演习、战役和决战三类独立日常任务 YAML。 + * + * 系统计划位于 resource/system_daily_plans,只读。 + * 用户计划位于 Electron userData/user_daily_plans,可覆盖同名系统计划。 + * 本服务只接受 exercise、campaign、decisive,避免它们再次混入普通出征计划。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { AppPaths } from './AppPaths'; +import { AtomicFileStore } from './AtomicFileStore'; +import { CombatPlanCodec } from './CombatPlanCodec'; +import { + TaskPresetCodec, + type TaskPresetDocument, +} from './TaskPresetCodec'; +import type { DecisivePlanSettings } from './GuiConfigurationService'; + +export type DailyPlanSource = 'system' | 'user'; +export type DailyPlanType = 'exercise' | 'campaign' | 'decisive'; + +export interface ManagedDailyPlan { + source: DailyPlanSource; + file: string; + name: string; + taskType: DailyPlanType; + times: number; + fleetId?: number; + campaignName?: string; + chapter?: number; + useQuickRepair?: boolean; +} + +export interface DailyPlanReadError { + source: DailyPlanSource; + file: string; + message: string; +} + +export interface DailyPlanListResult { + plans: ManagedDailyPlan[]; + errors: DailyPlanReadError[]; +} + +const DAILY_PLAN_PREFIXES: Readonly> = { + exercise: 'exercise-', + campaign: 'campaign-', + decisive: 'decisive-', +}; + +const DAILY_PLAN_ORDER: Readonly> = { + exercise: 0, + campaign: 1, + decisive: 2, +}; + +/** 提供日常任务计划的受管路径、读取、列表和决战章节保存能力。 */ +export class DailyPlanService { + constructor( + private readonly appPaths: AppPaths, + private readonly atomicFiles: AtomicFileStore, + private readonly combatCodec: CombatPlanCodec, + private readonly taskPresetCodec: TaskPresetCodec, + ) {} + + /** 列出日常任务;用户同名文件覆盖系统文件。 */ + list(): DailyPlanListResult { + const plans = new Map(); + const errors: DailyPlanReadError[] = []; + + for (const source of ['system', 'user'] as const) { + for (const file of this.yamlFiles(this.directory(source))) { + try { + const plan = this.readPlan(source, file); + plans.set(file.toLocaleLowerCase(), plan); + } catch (error) { + errors.push({ + source, + file, + message: error instanceof Error + ? error.message + : String(error), + }); + } + } + } + + return { + plans: [...plans.values()].sort((left, right) => ( + DAILY_PLAN_ORDER[left.taskType] + - DAILY_PLAN_ORDER[right.taskType] + || left.name.localeCompare(right.name, 'zh-CN') + )), + errors, + }; + } + + /** 读取一份受管日常任务,并返回任务列表执行所需的 YAML。 */ + read( + source: DailyPlanSource, + file: string, + ): Record { + try { + const filePath = this.safeManagedPath(source, file); + if (!filePath || !fs.existsSync(filePath)) { + throw new Error('日常任务计划不存在'); + } + const content = fs.readFileSync(filePath, 'utf-8'); + this.parseDailyPlan(content, file); + return { + success: true, + kind: 'daily', + path: filePath, + sourcePath: filePath, + content, + source, + file, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 读取指定章节的用户决战配置;没有用户副本时回退到系统配置。 */ + decisivePlan(chapter: number): DecisivePlanSettings { + const normalizedChapter = this.decisiveChapter(chapter); + const file = this.decisiveFile(normalizedChapter); + const userPath = this.safeManagedPath('user', file); + const source: DailyPlanSource = ( + userPath && fs.existsSync(userPath) + ) + ? 'user' + : 'system'; + return this.readDecisiveSettings(source, file); + } + + /** 读取 GUI 2.0 提供的指定章节系统默认决战配置。 */ + systemDecisivePlan(chapter: number): DecisivePlanSettings { + const normalizedChapter = this.decisiveChapter(chapter); + return this.readDecisiveSettings( + 'system', + this.decisiveFile(normalizedChapter), + ); + } + + /** 保存一章用户决战配置,其他章节文件保持不变。 */ + saveDecisivePlan( + settings: DecisivePlanSettings, + ): DecisivePlanSettings { + const chapter = this.decisiveChapter(settings.chapter); + const root = this.taskPresetCodec.normalize({ + task_type: 'decisive', + chapter, + times: 1, + level1: this.shipNames(settings.level1, '主选队列'), + level2: this.shipNames(settings.level2, '备选队列'), + use_quick_repair: settings.useQuickRepair === true, + }); + const directory = this.directory('user'); + fs.mkdirSync(directory, { recursive: true }); + this.atomicFiles.write( + path.join(directory, this.decisiveFile(chapter)), + this.combatCodec.serialize(root), + ); + return this.settingsFromDecisive(root); + } + + /** 返回系统或用户日常任务目录。 */ + directory(source: DailyPlanSource): string { + return source === 'system' + ? this.appPaths.systemDailyPlansDir() + : this.appPaths.userDailyPlansDir(); + } + + /** 校验文件名并返回受管目录内路径。 */ + safeManagedPath( + source: DailyPlanSource, + file: string, + ): string | null { + if ( + (source !== 'system' && source !== 'user') + || path.basename(file) !== file + || !/^(exercise|campaign|decisive)-.+\.ya?ml$/i.test(file) + ) { + return null; + } + return path.join(this.directory(source), file); + } + + private readPlan( + source: DailyPlanSource, + file: string, + ): ManagedDailyPlan { + const filePath = this.safeManagedPath(source, file); + if (!filePath) throw new Error('日常任务文件名不符合规则'); + const preset = this.parseDailyPlan( + fs.readFileSync(filePath, 'utf-8'), + file, + ); + const taskType = preset.task_type as DailyPlanType; + return { + source, + file, + name: this.displayName(file, taskType), + taskType, + times: typeof preset.times === 'number' ? preset.times : 1, + fleetId: typeof preset.fleet_id === 'number' + ? preset.fleet_id + : undefined, + campaignName: typeof preset.campaign_name === 'string' + ? preset.campaign_name + : undefined, + chapter: typeof preset.chapter === 'number' + ? preset.chapter + : undefined, + useQuickRepair: typeof preset.use_quick_repair === 'boolean' + ? preset.use_quick_repair + : true, + }; + } + + private parseDailyPlan( + content: string, + file: string, + ): TaskPresetDocument { + const root = this.combatCodec.parseRoot( + content, + '日常任务计划根节点必须是对象', + ); + const preset = this.taskPresetCodec.normalize(root); + if (!this.isDailyPlanType(preset.task_type)) { + throw new Error(`不支持的日常任务类型:${preset.task_type}`); + } + const prefix = DAILY_PLAN_PREFIXES[preset.task_type]; + if (!file.toLocaleLowerCase().startsWith(prefix)) { + throw new Error(`文件名必须以 ${prefix} 开头`); + } + if (preset.task_type === 'decisive') { + this.decisiveChapter(preset.chapter); + } + return preset; + } + + private readDecisiveSettings( + source: DailyPlanSource, + file: string, + ): DecisivePlanSettings { + const filePath = this.safeManagedPath(source, file); + if (!filePath || !fs.existsSync(filePath)) { + throw new Error(`第 ${this.chapterFromFile(file)} 章决战配置不存在`); + } + const preset = this.parseDailyPlan( + fs.readFileSync(filePath, 'utf-8'), + file, + ); + if (preset.task_type !== 'decisive') { + throw new Error('目标文件不是决战配置'); + } + return this.settingsFromDecisive(preset); + } + + private settingsFromDecisive( + preset: TaskPresetDocument, + ): DecisivePlanSettings { + return { + chapter: this.decisiveChapter(preset.chapter), + useQuickRepair: preset.use_quick_repair !== false, + level1: this.shipNames(preset.level1, '主选队列'), + level2: this.shipNames(preset.level2, '备选队列'), + }; + } + + private isDailyPlanType(value: string): value is DailyPlanType { + return ( + value === 'exercise' + || value === 'campaign' + || value === 'decisive' + ); + } + + private decisiveChapter(value: unknown): number { + const chapter = Number(value); + if (!Number.isInteger(chapter) || chapter < 1 || chapter > 6) { + throw new Error('决战章节必须是 1 到 6'); + } + return chapter; + } + + private shipNames(value: unknown, label: string): string[] { + if (!Array.isArray(value)) { + throw new Error(`${label}必须是舰名列表`); + } + return value.map((item, index) => { + if (typeof item !== 'string' || !item.trim()) { + throw new Error(`${label}第 ${index + 1} 项不能为空`); + } + return item.trim(); + }); + } + + private decisiveFile(chapter: number): string { + return `decisive-决战第${chapter}章.yaml`; + } + + private chapterFromFile(file: string): string { + return /第(\d+)章/.exec(file)?.[1] ?? '?'; + } + + private displayName(file: string, taskType: DailyPlanType): string { + return file + .replace(new RegExp(`^${DAILY_PLAN_PREFIXES[taskType]}`, 'i'), '') + .replace(/\.ya?ml$/i, ''); + } + + private yamlFiles(directory: string): string[] { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory) + .filter(file => /\.ya?ml$/i.test(file)) + .sort((left, right) => left.localeCompare(right, 'zh-CN')); + } +} diff --git a/electron/services/GuiConfigurationService.ts b/electron/services/GuiConfigurationService.ts new file mode 100644 index 0000000..ac3f33f --- /dev/null +++ b/electron/services/GuiConfigurationService.ts @@ -0,0 +1,638 @@ +/** + * 读取、归一化并保存 GUI 业务设置。 + */ +import { GuiSettingsStore } from './GuiSettingsStore'; +import { + DEFAULT_LOOT_PLAN_ID, + DEFAULT_LOOT_PLANS, + INTERIM_LOOT_PLAN_IDS, + findLootAutomationPlan, + lootPlanIdFromIndex, + migrateLootPlanId, + normalizeLootAutomationPlans, + type LootAutomationPlan, + type LootPlanSource, +} from '../../src/shared/lootPlans'; +import type { + LegacyDecisiveAutomationSettings, +} from '../../src/shared/legacyDecisiveAutomation'; +import { + normalizeDecisiveAutomationSource, + type DecisiveAutomationSource, +} from '../../src/shared/decisiveAutomation'; + +export type BackendStartupMode = 'managed' | 'external'; +export type OcrGpuMode = 'auto' | 'cpu' | 'cuda'; +export type UpdateMode = 'auto' | 'manual'; + +export interface GuiAutomationSettings { + expeditionInterval: number; + battleTimes: number; + autoDecisive: boolean; + decisiveTemplateId: DecisiveAutomationSource; + autoLoot: boolean; + lootPlanSource: LootPlanSource; + lootPlanId: string; + lootPlans: LootAutomationPlan[]; + lootStopCount: number; +} + +export interface DecisivePlanSettings { + chapter: number; + useQuickRepair: boolean; + level1: string[]; + level2: string[]; +} + +export interface GuiConfigurationDependencies { + clearPythonCache(): void; + normalizeCudaPath(candidate: string): string; + environmentPort?(): string | undefined; +} + +/** 只接受有限数字或非空数字字符串,避免把 null/false 当成 0。 */ +function finiteNumber(value: unknown): number | null { + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + if (typeof value === 'string' && value.trim()) { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + return null; +} + +const DEFAULT_DECISIVE_PLAN: DecisivePlanSettings = { + chapter: 6, + useQuickRepair: true, + level1: [ + 'U-47', + 'U-1405', + 'U-1206', + 'U-2540', + 'U-81', + 'U-96', + ], + level2: [ + 'U-505', + '射水鱼', + '大青花鱼', + 'M-296', + '鹦鹉螺', + 'S-49', + 'IIIA', + 'K-21', + 'U-441', + '潜甲', + '潜乙', + '伊-201', + '伊-25', + '鲃鱼', + '伊-400', + '激流', + 'U-4501', + 'U-459', + 'U-14', + 'U-35', + 'K1', + ], +}; + +/** 解释 GUI 设置字段并执行原有归一化和迁移规则。 */ +export class GuiConfigurationService { + constructor( + private readonly store: GuiSettingsStore, + private readonly dependencies: GuiConfigurationDependencies, + ) {} + + /** 返回启动时使用的后端端口。 */ + backendPort(): number { + const environmentPort = this.dependencies.environmentPort?.(); + if (environmentPort) { + return parseInt(environmentPort, 10); + } + const settings = this.store.read(); + if ( + typeof settings.backend_port === 'number' + && settings.backend_port > 0 + && settings.backend_port < 65536 + ) { + return settings.backend_port; + } + return 8438; + } + + /** 仅在端口是合法有限整数时写入。 */ + setBackendPort(port: number): void { + if (typeof port !== 'number' || !Number.isFinite(port)) return; + const normalized = Math.trunc(port); + if (normalized < 1 || normalized > 65535) return; + this.store.write({ backend_port: normalized }); + } + + /** 返回用户指定的 Python 路径;空值表示自动检测。 */ + configuredPythonPath(): string | null { + const settings = this.store.read(); + if ( + typeof settings.python_path === 'string' + && settings.python_path.length > 0 + ) { + return settings.python_path; + } + return null; + } + + /** 保存 Python 路径并清除 Python 发现缓存。 */ + setPythonPath(pythonPath: string | null): void { + this.store.write({ python_path: pythonPath ?? '' }); + this.dependencies.clearPythonCache(); + } + + /** 返回 autowsgr 更新模式。 */ + updateMode(): UpdateMode { + return this.store.read().update_mode === 'manual' + ? 'manual' + : 'auto'; + } + + /** 保存归一化后的 autowsgr 更新模式。 */ + setUpdateMode(mode: UpdateMode): void { + this.store.write({ + update_mode: mode === 'manual' ? 'manual' : 'auto', + }); + } + + /** 返回 managed 或 external 后端启动模式。 */ + backendStartupMode(): BackendStartupMode { + return this.store.read().backend_startup_mode === 'external' + ? 'external' + : 'managed'; + } + + /** 保存归一化后的后端启动模式。 */ + setBackendStartupMode(mode: BackendStartupMode): void { + this.store.write({ + backend_startup_mode: mode === 'external' + ? 'external' + : 'managed', + }); + } + + /** 返回去除首尾空白的 external 后端仓库路径。 */ + backendRepoPath(): string { + const value = this.store.read().backend_repo_path; + return typeof value === 'string' ? value.trim() : ''; + } + + /** 保存去除首尾空白的 external 后端仓库路径。 */ + setBackendRepoPath(repoPath: string | null): void { + this.store.write({ + backend_repo_path: typeof repoPath === 'string' + ? repoPath.trim() + : '', + }); + } + + /** 返回 OCR GPU 模式。 */ + ocrGpuMode(): OcrGpuMode { + const value = this.store.read().ocr_gpu_mode; + return value === 'cpu' || value === 'cuda' ? value : 'auto'; + } + + /** 保存归一化后的 OCR GPU 模式。 */ + setOcrGpuMode(mode: OcrGpuMode): void { + this.store.write({ + ocr_gpu_mode: mode === 'cpu' || mode === 'cuda' + ? mode + : 'auto', + }); + } + + /** 返回去除首尾空白的 CUDA 配置路径。 */ + cudaPath(): string { + const value = this.store.read().cuda_path; + return typeof value === 'string' ? value.trim() : ''; + } + + /** 保存空路径或统一归一化后的 CUDA 路径。 */ + setCudaPath(cudaPath: string | null): void { + const raw = typeof cudaPath === 'string' + ? cudaPath.trim() + : ''; + this.store.write({ + cuda_path: raw + ? this.dependencies.normalizeCudaPath(raw) + : '', + }); + } + + /** 返回是否保存后端异常截图。 */ + saveBackendScreenshots(): boolean { + return this.store.read().save_backend_screenshots === true; + } + + /** 保存后端异常截图开关。 */ + setSaveBackendScreenshots(enabled: boolean): void { + this.store.write({ + save_backend_screenshots: enabled === true, + }); + } + + /** 读取已有的 GUI 自动化字段,不为缺失字段补默认值。 */ + automation(): { + exists: boolean; + settings: Partial; + } { + const raw = this.store.read().automation; + const hasAutomation = !!raw + && typeof raw === 'object' + && !Array.isArray(raw); + const value = hasAutomation + ? raw as Record + : {}; + const settings: Partial = {}; + const legacyDecisive = this.legacyDecisiveAutomation().settings; + let rewritten: Record | null = null; + const expeditionInterval = finiteNumber(value.expeditionInterval); + if (expeditionInterval !== null) { + settings.expeditionInterval = expeditionInterval; + } + const battleTimes = finiteNumber(value.battleTimes); + if (battleTimes !== null) { + settings.battleTimes = battleTimes; + } + if (typeof value.autoDecisive === 'boolean') { + settings.autoDecisive = value.autoDecisive; + } else if (typeof legacyDecisive.autoDecisive === 'boolean') { + settings.autoDecisive = legacyDecisive.autoDecisive; + rewritten = { ...value, autoDecisive: legacyDecisive.autoDecisive }; + } + const storedDecisiveSource = ( + typeof value.decisiveTemplateId === 'string' + ? value.decisiveTemplateId.trim() + : legacyDecisive.templateId + ); + if (storedDecisiveSource) { + const normalizedSource = normalizeDecisiveAutomationSource( + storedDecisiveSource, + ); + settings.decisiveTemplateId = normalizedSource; + if (storedDecisiveSource !== normalizedSource) { + rewritten = { + ...(rewritten ?? value), + decisiveTemplateId: normalizedSource, + }; + } + } + if (typeof value.autoLoot === 'boolean') { + settings.autoLoot = value.autoLoot; + } + const hasStoredLootPlans = Array.isArray(value.lootPlans); + const lootPlans = normalizeLootAutomationPlans(value.lootPlans); + if (hasStoredLootPlans) { + settings.lootPlans = lootPlans; + if (JSON.stringify(value.lootPlans) !== JSON.stringify(lootPlans)) { + rewritten = { + ...(rewritten ?? value), + lootPlans, + }; + } + } + const lootPlanSource: LootPlanSource = ( + value.lootPlanSource === 'user' ? 'user' : 'system' + ); + if (typeof value.lootPlanId === 'string') { + const selected = findLootAutomationPlan( + lootPlans, + lootPlanSource, + value.lootPlanId, + ); + if (selected) { + settings.lootPlanSource = selected.source; + settings.lootPlanId = selected.file; + if ( + selected.source !== value.lootPlanSource + || selected.file !== value.lootPlanId + ) { + rewritten = { + ...(rewritten ?? value), + lootPlanSource: selected.source, + lootPlanId: selected.file, + }; + } + } else { + settings.autoLoot = false; + rewritten = { + ...(rewritten ?? value), + autoLoot: false, + }; + } + } else if ( + Object.prototype.hasOwnProperty.call(value, 'lootPlanIndex') + ) { + const resolved = lootPlanIdFromIndex( + value.lootPlanIndex, + INTERIM_LOOT_PLAN_IDS, + ); + rewritten = { ...(rewritten ?? value) }; + delete rewritten.lootPlanIndex; + if (resolved) { + settings.lootPlanSource = 'system'; + settings.lootPlanId = resolved; + rewritten.lootPlanSource = 'system'; + rewritten.lootPlanId = resolved; + } else { + settings.autoLoot = false; + rewritten.autoLoot = false; + } + } else if ( + Object.prototype.hasOwnProperty.call(value, 'lootPlanId') + ) { + settings.autoLoot = false; + rewritten = { + ...(rewritten ?? value), + autoLoot: false, + }; + } + const lootStopCount = finiteNumber(value.lootStopCount); + if (lootStopCount !== null) { + settings.lootStopCount = lootStopCount; + } + if (rewritten) { + this.store.write({ automation: rewritten }); + } + return { + exists: hasAutomation || Object.keys(settings).length > 0, + settings, + }; + } + + /** 归一化并保存 GUI 自动化字段。 */ + setAutomation( + settings: GuiAutomationSettings, + ): GuiAutomationSettings { + const lootPlans = normalizeLootAutomationPlans(settings?.lootPlans); + const requestedSource: LootPlanSource = ( + settings?.lootPlanSource === 'user' ? 'user' : 'system' + ); + const selected = findLootAutomationPlan( + lootPlans, + requestedSource, + settings?.lootPlanId, + ); + const fallback = lootPlans[0] ?? DEFAULT_LOOT_PLANS[0]; + const decisiveTemplateId = normalizeDecisiveAutomationSource( + settings?.decisiveTemplateId, + ); + const normalized: GuiAutomationSettings = { + expeditionInterval: Math.max( + 1, + Math.min( + 120, + Math.trunc(Number(settings?.expeditionInterval) || 15), + ), + ), + battleTimes: Math.max( + 1, + Math.trunc(Number(settings?.battleTimes) || 3), + ), + autoDecisive: settings?.autoDecisive === true, + decisiveTemplateId, + autoLoot: settings?.autoLoot === true && selected !== null, + lootPlanSource: selected?.source ?? fallback?.source ?? 'system', + lootPlanId: selected?.file ?? fallback?.file ?? DEFAULT_LOOT_PLAN_ID, + lootPlans, + lootStopCount: Math.max( + 1, + Math.min( + 50, + Math.trunc(Number(settings?.lootStopCount) || 50), + ), + ), + }; + const raw = this.store.read().automation; + const output: Record = ( + raw && typeof raw === 'object' && !Array.isArray(raw) + ? { ...raw as Record, ...normalized } + : { ...normalized } + ); + delete output.lootPlanIndex; + this.store.write({ automation: output }); + return normalized; + } + + /** 读取已经保留到 GUI JSON 的旧版决战自动化原值。 */ + legacyDecisiveAutomation(): { + exists: boolean; + settings: LegacyDecisiveAutomationSettings; + } { + const raw = this.store.read().legacy_decisive_automation; + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return { exists: false, settings: {} }; + } + const value = raw as Record; + const settings: LegacyDecisiveAutomationSettings = {}; + if (typeof value.auto_decisive === 'boolean') { + settings.autoDecisive = value.auto_decisive; + } + if ( + typeof value.decisive_ticket_reserve === 'number' + && Number.isFinite(value.decisive_ticket_reserve) + ) { + settings.ticketReserve = value.decisive_ticket_reserve; + } + if ( + typeof value.decisive_template_id === 'string' + && value.decisive_template_id.length > 0 + ) { + settings.templateId = value.decisive_template_id; + } + return { exists: true, settings }; + } + + /** + * 原样归档旧版决战设置;开关和模板由 automation() 升级。 + * 写入后立即回读并逐字段校验,失败时由调用方保留 YAML 原字段。 + */ + migrateLegacyDecisiveAutomation( + settings: LegacyDecisiveAutomationSettings, + ): LegacyDecisiveAutomationSettings { + const fields = [ + 'autoDecisive', + 'ticketReserve', + 'templateId', + ] as const; + const supplied = fields.filter(field => ( + Object.prototype.hasOwnProperty.call(settings, field) + )); + if (supplied.length === 0) { + throw new Error('没有可迁移的旧版决战配置'); + } + if ( + supplied.includes('autoDecisive') + && typeof settings.autoDecisive !== 'boolean' + ) { + throw new Error('auto_decisive 必须是布尔值'); + } + if ( + supplied.includes('ticketReserve') + && ( + typeof settings.ticketReserve !== 'number' + || !Number.isFinite(settings.ticketReserve) + ) + ) { + throw new Error('decisive_ticket_reserve 必须是有限数字'); + } + if ( + supplied.includes('templateId') + && ( + typeof settings.templateId !== 'string' + || settings.templateId.length === 0 + ) + ) { + throw new Error('decisive_template_id 必须是非空文字'); + } + + const current = this.store.read().legacy_decisive_automation; + const output = current + && typeof current === 'object' + && !Array.isArray(current) + ? { ...current as Record } + : {}; + if (supplied.includes('autoDecisive')) { + output.auto_decisive = settings.autoDecisive; + } + if (supplied.includes('ticketReserve')) { + output.decisive_ticket_reserve = settings.ticketReserve; + } + if (supplied.includes('templateId')) { + output.decisive_template_id = settings.templateId; + } + this.store.write({ legacy_decisive_automation: output }); + + const verified = this.legacyDecisiveAutomation(); + for (const field of supplied) { + if ( + !verified.exists + || verified.settings[field] !== settings[field] + ) { + throw new Error(`旧版决战配置回读校验失败: ${field}`); + } + } + return verified.settings; + } + + /** 读取决战计划,并在发现旧字段时原地迁移。 */ + decisivePlan(): DecisivePlanSettings { + const rawPlan = this.store.read().decisive_plan; + const normalized = this.normalizeDecisivePlan(rawPlan); + if ( + rawPlan + && typeof rawPlan === 'object' + && !Array.isArray(rawPlan) + && ( + Object.prototype.hasOwnProperty.call(rawPlan, 'level3') + || ( + Array.isArray( + (rawPlan as Record).level1, + ) + && ( + (rawPlan as Record) + .level1 as unknown[] + ).length > 6 + ) + ) + ) { + this.writeDecisivePlan(normalized); + } + return normalized; + } + + /** 归一化并保存决战计划。 */ + setDecisivePlan( + settings: DecisivePlanSettings, + ): DecisivePlanSettings { + const normalized = this.normalizeDecisivePlan(settings); + this.writeDecisivePlan(normalized); + return normalized; + } + + /** 归一化决战章节、修理设置和两级舰船列表。 */ + private normalizeDecisivePlan( + value: unknown, + ): DecisivePlanSettings { + const raw = value + && typeof value === 'object' + && !Array.isArray(value) + ? value as Record + : {}; + const chapter = Math.trunc(Number(raw.chapter)); + const requestedMainShips = this.normalizeDecisiveShips( + raw.level1, + DEFAULT_DECISIVE_PLAN.level1, + ); + const mainShips = requestedMainShips.slice(0, 6); + const requestedBackupShips = this.normalizeDecisiveShips( + raw.level2, + DEFAULT_DECISIVE_PLAN.level2, + ); + const legacyLevel3 = Array.isArray(raw.level3) + ? this.normalizeDecisiveShips(raw.level3, []) + : []; + const backupShips: string[] = []; + for ( + const name of [ + ...requestedMainShips.slice(6), + ...requestedBackupShips, + ...legacyLevel3, + ] + ) { + if ( + !mainShips.includes(name) + && !backupShips.includes(name) + ) { + backupShips.push(name); + } + } + return { + chapter: Number.isFinite(chapter) + ? Math.max(1, Math.min(6, chapter)) + : DEFAULT_DECISIVE_PLAN.chapter, + useQuickRepair: typeof raw.use_quick_repair === 'boolean' + ? raw.use_quick_repair + : typeof raw.useQuickRepair === 'boolean' + ? raw.useQuickRepair + : DEFAULT_DECISIVE_PLAN.useQuickRepair, + level1: mainShips, + level2: backupShips, + }; + } + + /** 清理舰船名、长度和重复项。 */ + private normalizeDecisiveShips( + value: unknown, + fallback: string[], + ): string[] { + if (!Array.isArray(value)) return [...fallback]; + return value + .filter(item => typeof item === 'string') + .map(item => item.trim()) + .filter((item, index, values) => ( + item.length > 0 + && item.length <= 80 + && values.indexOf(item) === index + )); + } + + /** 使用兼容的 snake_case 结构写回决战计划。 */ + private writeDecisivePlan(settings: DecisivePlanSettings): void { + this.store.write({ + decisive_plan: { + chapter: settings.chapter, + use_quick_repair: settings.useQuickRepair, + level1: settings.level1, + level2: settings.level2, + }, + }); + } +} diff --git a/electron/services/GuiSettingsStore.ts b/electron/services/GuiSettingsStore.ts new file mode 100644 index 0000000..ddbceee --- /dev/null +++ b/electron/services/GuiSettingsStore.ts @@ -0,0 +1,38 @@ +/** + * 读取并浅合并写入 gui_settings.json。 + */ +import * as fs from 'fs'; + +/** 管理唯一 GUI JSON 设置文件的读取和浅合并写入。 */ +export class GuiSettingsStore { + constructor(private readonly getFilePath: () => string) {} + + /** 返回当前 GUI 设置文件路径。 */ + filePath(): string { + return this.getFilePath(); + } + + /** 读取设置;不存在或无法解析时保持原有空对象回退。 */ + read(): Record { + try { + const filePath = this.filePath(); + if (fs.existsSync(filePath)) { + return JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } + } catch { + // 文件缺失或无效时返回空设置。 + } + return {}; + } + + /** 浅合并 patch 后覆盖写回唯一设置文件。 */ + write(patch: Record): void { + const current = this.read(); + Object.assign(current, patch); + fs.writeFileSync( + this.filePath(), + JSON.stringify(current, null, 2), + 'utf-8', + ); + } +} diff --git a/electron/services/GuiUpdatePolicy.ts b/electron/services/GuiUpdatePolicy.ts new file mode 100644 index 0000000..dc45e44 --- /dev/null +++ b/electron/services/GuiUpdatePolicy.ts @@ -0,0 +1,108 @@ +/** + * GUI 版本和更新频道规则。 + * + * 稳定版使用 X.Y.Z 和 latest 频道。 + * 预发布版使用 X.Y.Z-beta.N 和 beta 频道。 + * 开发版使用 X.Y.Z-dev 或 X.Y.Z-dev.N 和 dev 频道。 + * + * 发布端和客户端必须使用同一规则。客户端还会验证服务端返回的候选 + * 版本,防止 electron-updater 在频道清单缺失时回退到其他频道。 + */ + +export type GuiReleaseChannel = 'latest' | 'beta' | 'dev'; +export type GuiReleaseStage = 'stable' | 'prerelease' | 'development'; + +export interface GuiReleasePolicy { + channel: GuiReleaseChannel; + stage: GuiReleaseStage; + allowPrerelease: boolean; +} + +export type GuiUpdateCheckResult = + | { status: 'available'; version: string } + | { status: 'up-to-date' } + | { status: 'error'; message: string }; + +export interface UpdaterCheckResultLike { + isUpdateAvailable: boolean; + updateInfo?: { + version?: string; + }; +} + +const STABLE_VERSION = /^\d+\.\d+\.\d+$/; +const BETA_VERSION = /^\d+\.\d+\.\d+-beta\.\d+$/; +const DEVELOPMENT_VERSION = /^\d+\.\d+\.\d+-dev(?:\.\d+)?$/; + +/** 严格解析 GUI 版本,拒绝没有明确频道的版本后缀。 */ +export function resolveGuiReleasePolicy(version: string): GuiReleasePolicy { + const normalized = version.trim(); + if (STABLE_VERSION.test(normalized)) { + return { + channel: 'latest', + stage: 'stable', + allowPrerelease: false, + }; + } + if (BETA_VERSION.test(normalized)) { + return { + channel: 'beta', + stage: 'prerelease', + allowPrerelease: true, + }; + } + if (DEVELOPMENT_VERSION.test(normalized)) { + return { + channel: 'dev', + stage: 'development', + allowPrerelease: true, + }; + } + throw new Error( + `GUI 版本 ${version} 不符合规范;只允许 X.Y.Z、` + + 'X.Y.Z-beta.N、X.Y.Z-dev 或 X.Y.Z-dev.N', + ); +} + +/** 返回候选版本不属于当前频道时的明确错误。 */ +export function validateGuiUpdateCandidate( + currentPolicy: GuiReleasePolicy, + candidateVersion: string, +): string | null { + let candidatePolicy: GuiReleasePolicy; + try { + candidatePolicy = resolveGuiReleasePolicy(candidateVersion); + } catch (error) { + return error instanceof Error ? error.message : String(error); + } + if (candidatePolicy.channel === currentPolicy.channel) return null; + return `更新版本 ${candidateVersion} 属于 ${candidatePolicy.channel} 频道,` + + `当前客户端只允许 ${currentPolicy.channel} 频道`; +} + +/** 将 electron-updater 的结果转换为可靠的三态结果。 */ +export function classifyGuiUpdateCheck( + currentPolicy: GuiReleasePolicy, + result: UpdaterCheckResultLike | null, +): GuiUpdateCheckResult { + if (result === null) { + return { + status: 'error', + message: '当前运行环境未启用 GUI 自动更新', + }; + } + if (!result.isUpdateAvailable) { + return { status: 'up-to-date' }; + } + + const version = result.updateInfo?.version?.trim(); + if (!version) { + return { + status: 'error', + message: '更新服务返回了无效的版本信息', + }; + } + const mismatch = validateGuiUpdateCandidate(currentPolicy, version); + if (mismatch) return { status: 'error', message: mismatch }; + return { status: 'available', version }; +} diff --git a/electron/services/LegacyMigrationNotice.ts b/electron/services/LegacyMigrationNotice.ts new file mode 100644 index 0000000..01e3c97 --- /dev/null +++ b/electron/services/LegacyMigrationNotice.ts @@ -0,0 +1,55 @@ +/** + * 构造旧版数据迁移完成后的用户提示。 + */ +import * as path from 'path'; +import type { LegacyMigrationSummary } from './LegacyMigrationSummary'; + +/** Electron 提示框所需的迁移结果文案。 */ +export interface LegacyMigrationNotice { + type: 'info' | 'warning'; + title: string; + message: string; + detail: string; + buttons: string[]; +} + +/** 本次没有实际迁移项时不重复打扰用户。 */ +export function buildLegacyMigrationNotice( + summary: LegacyMigrationSummary, +): LegacyMigrationNotice | null { + if (!summary.detected || summary.total === 0) return null; + + const failedNames = summary.failedFiles + .slice(0, 10) + .map(file => path.basename(file)); + const hiddenFailureCount = Math.max( + 0, + summary.failedFiles.length - failedNames.length, + ); + const failureDetail = failedNames.length > 0 + ? [ + '', + '失败文件:', + ...failedNames.map(file => `- ${file}`), + ...(hiddenFailureCount > 0 + ? [`- 另有 ${hiddenFailureCount} 个文件`] + : []), + ] + : []; + + return { + type: summary.failed > 0 ? 'warning' : 'info', + title: '旧版数据迁移完成', + message: [ + `当前已迁移旧版数据:${summary.total} 项`, + `成功:${summary.succeeded} 项`, + `失败:${summary.failed} 项`, + ].join('\n'), + detail: [ + '旧版原始文件均已保留。', + '迁移失败的文件仍位于旧版本原始目录,下次启动时会继续尝试。', + ...failureDetail, + ].join('\n'), + buttons: ['确定'], + }; +} diff --git a/electron/services/LegacyMigrationSummary.ts b/electron/services/LegacyMigrationSummary.ts new file mode 100644 index 0000000..3660925 --- /dev/null +++ b/electron/services/LegacyMigrationSummary.ts @@ -0,0 +1,44 @@ +/** + * 汇总一次旧版数据迁移的执行结果。 + */ + +/** 仅统计本次实际处理的旧版配置项。 */ +export interface LegacyMigrationSummary { + detected: boolean; + total: number; + succeeded: number; + failed: number; + failedFiles: string[]; +} + +/** 创建不含迁移项的初始结果。 */ +export function emptyLegacyMigrationSummary( + detected = false, +): LegacyMigrationSummary { + return { + detected, + total: 0, + succeeded: 0, + failed: 0, + failedFiles: [], + }; +} + +/** 合并设置、任务组和计划等多个迁移阶段的结果。 */ +export function mergeLegacyMigrationSummaries( + ...summaries: LegacyMigrationSummary[] +): LegacyMigrationSummary { + return summaries.reduce( + (merged, summary) => ({ + detected: merged.detected || summary.detected, + total: merged.total + summary.total, + succeeded: merged.succeeded + summary.succeeded, + failed: merged.failed + summary.failed, + failedFiles: [ + ...merged.failedFiles, + ...summary.failedFiles, + ], + }), + emptyLegacyMigrationSummary(), + ); +} diff --git a/electron/services/LegacyPlanMigration.ts b/electron/services/LegacyPlanMigration.ts new file mode 100644 index 0000000..a0b5410 --- /dev/null +++ b/electron/services/LegacyPlanMigration.ts @@ -0,0 +1,1027 @@ +/** + * 幂等迁移旧作战计划和对应任务组引用。 + */ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; +import { AppPaths } from './AppPaths'; +import { AtomicFileStore } from './AtomicFileStore'; +import { + emptyLegacyMigrationSummary, + type LegacyMigrationSummary, +} from './LegacyMigrationSummary'; +import { + UserDataMigrationService, + type LegacyPlanReferenceTarget, +} from './UserDataMigrationService'; + +/** v7 将演习、战役和决战从普通出征计划迁移到独立日常目录。 */ +const LEGACY_PLAN_MIGRATION_VERSION = 7; + +type DailyPlanType = 'exercise' | 'campaign' | 'decisive'; + +/** 编队 Codec 为迁移生成的单个原子写入。 */ +export interface LegacyTeamWrite { + name: string; + file: string; + path: string; + content: string; +} + +/** 由现有计划 Codec 注入的迁移领域规则。 */ +export interface LegacyPlanMigrationDependencies { + yamlFiles(directory: string): string[]; + safePlanBaseName(value: string): string; + normalizeUserTeamPlan(raw: unknown): TTeam; + teamPlanMatches(filePath: string, team: TTeam): boolean; + teamName?(team: TTeam): string; + renameTeam?(team: TTeam, name: string): TTeam; + normalizeCombatPlanFleetPresets( + root: Record, + source: 'user', + requireEmbeddedShips: boolean, + ): { + mapRoot: Record; + teams: TTeam[]; + }; + buildTeamPlanWrites( + teams: TTeam[], + directory: string, + ): LegacyTeamWrite[]; + serializeCombatPlan( + root: Record, + originalContent: string, + ): string; + isStandaloneTaskPreset?( + root: Record, + ): boolean; + normalizeTaskPreset?( + root: Record, + ): Record; +} + +/** 编排旧计划迁移,不在本服务中重复任何计划格式规则。 */ +export class LegacyPlanMigration { + constructor( + private readonly appPaths: AppPaths, + private readonly atomicFiles: AtomicFileStore, + private readonly userDataMigration: UserDataMigrationService, + private readonly dependencies: LegacyPlanMigrationDependencies, + ) {} + + /** 扫描旧安装中的有效 YAML,并返回本次实际迁移结果。 */ + migrate(): LegacyMigrationSummary { + const legacyDetected = this.userDataMigration.hasLegacyInstallation(); + const misplacedDailyPlans = this.misplacedDailyPlanFiles(); + const detected = legacyDetected || misplacedDailyPlans.length > 0; + const summary = emptyLegacyMigrationSummary(detected); + if (!detected) return summary; + + const state = this.userDataMigration.readState(); + const completed = new Set(state.completed); + const fileMap = new Map(); + const decisiveFailed = legacyDetected + ? this.migrateLegacyDecisiveSettings(completed, summary) + : false; + const teamsFailed = legacyDetected + ? this.migrateLegacyTeams(completed, summary) + : false; + const plansFailed = legacyDetected + ? this.migrateLegacyPlans(completed, fileMap, summary) + : false; + const misplacedFailed = this.migrateMisplacedDailyPlans( + misplacedDailyPlans, + completed, + fileMap, + summary, + ); + const additionalFailed = legacyDetected + ? this.migrateAdditionalYaml(completed, fileMap, summary) + : false; + + this.userDataMigration.migrateLegacyTaskGroupPlanPaths(fileMap); + const failed = ( + decisiveFailed + || teamsFailed + || plansFailed + || misplacedFailed + || additionalFailed + ); + const nextState = { + version: failed + ? state.version + : Math.max(state.version, LEGACY_PLAN_MIGRATION_VERSION), + completed: [...completed].sort(), + }; + const currentCompleted = [...state.completed].sort(); + if ( + state.version !== nextState.version + || JSON.stringify(currentCompleted) !== JSON.stringify( + nextState.completed, + ) + ) { + this.userDataMigration.writeState(nextState); + } + return summary; + } + + /** 先迁移旧版独立编队,供引用型旧计划继续使用。 */ + private migrateLegacyTeams( + completed: Set, + summary: LegacyMigrationSummary, + ): boolean { + const targetDirectory = this.appPaths.userTeamPlansDir(); + let failed = false; + for ( + const legacyDirectory of this.legacyDirectories( + 'user_team_plans', + targetDirectory, + ) + ) { + for ( + const file of this.dependencies.yamlFiles(legacyDirectory) + ) { + const source = path.join(legacyDirectory, file); + const content = fs.readFileSync(source, 'utf-8'); + const key = this.migrationKey('team', source, content); + if (completed.has(key)) continue; + summary.total += 1; + try { + const team = this.dependencies.normalizeUserTeamPlan( + yaml.load(content), + ); + const resolved = this.resolveTeamWrite( + team, + targetDirectory, + ); + if (!fs.existsSync(resolved.write.path)) { + this.atomicFiles.write( + resolved.write.path, + resolved.write.content, + ); + } + completed.add(key); + summary.succeeded += 1; + } catch (error) { + failed = true; + summary.failed += 1; + summary.failedFiles.push(source); + console.error(`[Migration] ${source} failed:`, error); + } + } + } + return failed; + } + + /** 升级旧计划并写入 GUI 管理的用户计划目录。 */ + private migrateLegacyPlans( + completed: Set, + fileMap: Map, + summary: LegacyMigrationSummary, + ): boolean { + const targetDirectory = this.appPaths.userBattlePlansDir(); + let failed = false; + for ( + const legacyDirectory of [ + ...this.legacyDirectories('plans', targetDirectory), + ...this.legacyDirectories( + 'user_battle_plans', + targetDirectory, + ), + ] + ) { + for ( + const file of this.dependencies.yamlFiles(legacyDirectory) + ) { + const source = path.join(legacyDirectory, file); + const content = fs.readFileSync(source, 'utf-8'); + const key = this.migrationKey('plan', source, content); + if (completed.has(key)) { + this.registerFileMapping( + fileMap, + source, + this.completedPlanTarget(completed, key, file), + ); + continue; + } + summary.total += 1; + try { + const parsed = yaml.load(content); + if (!this.isPlainObject(parsed)) { + throw new Error('旧计划根节点必须是对象'); + } + const standalone = ( + this.dependencies.isStandaloneTaskPreset?.(parsed) === true + ); + const target = standalone + ? this.migrateTaskPreset(file, content, parsed) + : this.migrateCombatPlan( + file, + content, + this.dependencies.normalizeCombatPlanFleetPresets( + parsed, + 'user', + false, + ), + ); + this.registerFileMapping(fileMap, source, target); + this.rememberPlanTarget(completed, key, target); + completed.add(key); + summary.succeeded += 1; + } catch (error) { + failed = true; + summary.failed += 1; + summary.failedFiles.push(source); + console.error(`[Migration] ${source} failed:`, error); + } + } + } + return failed; + } + + /** 把已被旧升级器放入普通计划目录的日常任务重新归类。 */ + private migrateMisplacedDailyPlans( + sources: string[], + completed: Set, + fileMap: Map, + summary: LegacyMigrationSummary, + ): boolean { + let failed = false; + for (const source of sources) { + const file = path.basename(source); + const content = fs.readFileSync(source, 'utf-8'); + const key = this.migrationKey('plan', source, content); + if (completed.has(key)) { + this.registerFileMapping( + fileMap, + source, + this.completedPlanTarget(completed, key, file), + ); + continue; + } + summary.total += 1; + try { + const parsed = yaml.load(content); + if (!this.isPlainObject(parsed)) { + throw new Error('旧日常任务根节点必须是对象'); + } + const target = this.migrateTaskPreset(file, content, parsed); + if (target.kind !== 'daily') { + throw new Error('目标文件不是日常任务'); + } + this.registerFileMapping(fileMap, source, target); + this.rememberPlanTarget(completed, key, target); + completed.add(key); + summary.succeeded += 1; + } catch (error) { + failed = true; + summary.failed += 1; + summary.failedFiles.push(source); + console.error(`[Migration] ${source} failed:`, error); + } + } + return failed; + } + + /** 把旧决战页面最后保存的配置写入对应章节日常 YAML。 */ + private migrateLegacyDecisiveSettings( + completed: Set, + summary: LegacyMigrationSummary, + ): boolean { + const source = path.join(this.appPaths.appRoot(), 'gui_settings.json'); + if (!fs.existsSync(source)) return false; + const content = fs.readFileSync(source, 'utf-8'); + let root: unknown; + try { + root = JSON.parse(content); + } catch { + return false; + } + if (!this.isPlainObject(root) || !this.isPlainObject(root.decisive_plan)) { + return false; + } + + const decisive = root.decisive_plan; + const decisiveContent = JSON.stringify(decisive); + const key = this.migrationKey('plan', source, decisiveContent); + if (completed.has(key)) return false; + summary.total += 1; + try { + const level1 = Array.isArray(decisive.level1) + ? decisive.level1 + : []; + const level2 = Array.isArray(decisive.level2) + ? decisive.level2 + : []; + const level3 = Array.isArray(decisive.level3) + ? decisive.level3 + : []; + const chapter = Number(decisive.chapter); + const preset = { + task_type: 'decisive', + chapter, + times: 1, + use_quick_repair: typeof decisive.use_quick_repair === 'boolean' + ? decisive.use_quick_repair + : decisive.useQuickRepair, + level1: level1.slice(0, 6), + level2: [ + ...level1.slice(6), + ...level2, + ...level3, + ], + }; + this.migrateTaskPreset( + `决战第${chapter}章.yaml`, + '', + preset, + ); + completed.add(key); + summary.succeeded += 1; + return false; + } catch (error) { + summary.failed += 1; + summary.failedFiles.push(source); + console.error(`[Migration] ${source} failed:`, error); + return true; + } + } + + /** 递归扫描旧安装其余目录,只迁移能明确识别的计划 YAML。 */ + private migrateAdditionalYaml( + completed: Set, + fileMap: Map, + summary: LegacyMigrationSummary, + ): boolean { + let failed = false; + for (const source of this.recursiveLegacyYamlFiles()) { + if (this.isHandledLegacyYaml(source)) continue; + const content = fs.readFileSync(source, 'utf-8'); + let parsed: unknown; + try { + parsed = yaml.load(content); + } catch { + // 非计划目录中的普通 YAML 不属于用户迁移失败。 + continue; + } + if (!this.isPlainObject(parsed)) continue; + + const isTeam = ( + typeof parsed.name === 'string' + && Array.isArray(parsed.ships) + ); + const isPreset = ( + this.dependencies.isStandaloneTaskPreset?.(parsed) === true + ); + const isPlan = 'chapter' in parsed && 'map' in parsed; + if (!isTeam && !isPreset && !isPlan) continue; + + const kind = isTeam ? 'team' : 'plan'; + const key = this.migrationKey(kind, source, content); + const file = path.basename(source); + if (completed.has(key)) { + if (!isTeam) { + this.registerFileMapping( + fileMap, + source, + this.completedPlanTarget(completed, key, file), + ); + } + continue; + } + + summary.total += 1; + try { + if (isTeam) { + this.migrateAdditionalTeam(parsed); + } else { + const target = this.migrateAdditionalPlan( + file, + content, + parsed, + isPreset, + ); + this.registerFileMapping(fileMap, source, target); + this.rememberPlanTarget(completed, key, target); + } + completed.add(key); + summary.succeeded += 1; + } catch (error) { + failed = true; + summary.failed += 1; + summary.failedFiles.push(source); + console.error(`[Migration] ${source} failed:`, error); + } + } + return failed; + } + + private migrateAdditionalTeam(raw: Record): void { + const team = this.dependencies.normalizeUserTeamPlan(raw); + const resolved = this.resolveTeamWrite( + team, + this.appPaths.userTeamPlansDir(), + ); + if (!fs.existsSync(resolved.write.path)) { + this.atomicFiles.write( + resolved.write.path, + resolved.write.content, + ); + } + } + + private migrateAdditionalPlan( + file: string, + content: string, + parsed: Record, + standalone: boolean, + ): LegacyPlanReferenceTarget { + if (standalone) { + return this.migrateTaskPreset(file, content, parsed); + } + return this.migrateCombatPlan( + file, + content, + this.dependencies.normalizeCombatPlanFleetPresets( + parsed, + 'user', + false, + ), + ); + } + + private migrateCombatPlan( + file: string, + content: string, + split: { + mapRoot: Record; + teams: TTeam[]; + }, + ): LegacyPlanReferenceTarget { + return { + kind: 'plan', + file: this.writeMigratedPlan( + file, + content, + split.mapRoot, + split.teams, + ), + }; + } + + /** 按预设类型选择普通计划目录或日常任务目录。 */ + private migrateTaskPreset( + file: string, + content: string, + parsed: Record, + ): LegacyPlanReferenceTarget { + const normalized = this.dependencies.normalizeTaskPreset?.(parsed); + if (!normalized) { + throw new Error('缺少任务预设迁移规则'); + } + const taskType = normalized.task_type; + if (!this.isDailyTaskType(taskType)) { + return { + kind: 'plan', + file: this.writeMigratedPlan( + file, + content, + normalized, + [], + ), + }; + } + const dailyPreset: Record & { + task_type: DailyPlanType; + } = { + ...normalized, + task_type: taskType, + }; + if ( + dailyPreset.task_type === 'campaign' + && typeof dailyPreset.campaign_name === 'string' + && dailyPreset.campaign_name.startsWith('普通') + ) { + dailyPreset.campaign_name = dailyPreset.campaign_name.replace( + /^普通/, + '简单', + ); + } + return { + kind: 'daily', + taskType, + file: this.writeMigratedDailyPlan(file, content, dailyPreset), + }; + } + + private writeMigratedDailyPlan( + file: string, + content: string, + preset: Record & { task_type: DailyPlanType }, + ): string { + const serialized = this.dependencies.serializeCombatPlan( + preset, + content, + ); + const target = this.resolveDailyPlanTarget( + this.migratedDailyPlanFileName(file, preset), + serialized, + ); + if (!target.matches) { + fs.mkdirSync(this.appPaths.userDailyPlansDir(), { recursive: true }); + this.atomicFiles.write(target.path, serialized); + } + return target.file; + } + + private resolveDailyPlanTarget( + defaultFile: string, + content: string, + ): { file: string; path: string; matches: boolean } { + for (let suffix = 0; ; suffix += 1) { + const file = this.legacyPlanFileName(defaultFile, suffix); + const target = path.join(this.appPaths.userDailyPlansDir(), file); + if (!fs.existsSync(target)) { + return { file, path: target, matches: false }; + } + if (fs.readFileSync(target, 'utf-8') === content) { + return { file, path: target, matches: true }; + } + } + } + + private migratedDailyPlanFileName( + file: string, + preset: Record & { task_type: DailyPlanType }, + ): string { + if (preset.task_type === 'decisive') { + const chapter = Number(preset.chapter); + if (!Number.isInteger(chapter) || chapter < 1 || chapter > 6) { + throw new Error('决战章节必须是 1 到 6'); + } + return `decisive-决战第${chapter}章.yaml`; + } + if (preset.task_type === 'exercise') { + return `exercise-队伍${String(preset.fleet_id)}演习.yaml`; + } + const campaignName = typeof preset.campaign_name === 'string' + ? preset.campaign_name.replace(/^简单/, '普通') + : this.dependencies.safePlanBaseName(file); + return `campaign-${campaignName}.yaml`; + } + + private isDailyTaskType(value: unknown): value is DailyPlanType { + return ( + value === 'exercise' + || value === 'campaign' + || value === 'decisive' + ); + } + + private writeMigratedPlan( + file: string, + content: string, + mapRoot: Record, + teams: TTeam[], + ): string { + const defaultTargetFile = this.migratedUserPlanFileName(file); + const originalPlan = this.dependencies.serializeCombatPlan( + mapRoot, + content, + ); + const existingOriginal = this.matchingPlanTarget( + defaultTargetFile, + originalPlan, + ); + if (existingOriginal) { + this.writeMissingOriginalTeams(teams); + return existingOriginal.file; + } + + const resolved = this.resolvePlanTeams(mapRoot, teams); + const serializedPlan = this.dependencies.serializeCombatPlan( + resolved.mapRoot, + content, + ); + const planTarget = this.resolvePlanTarget( + defaultTargetFile, + serializedPlan, + ); + const createdTeams: string[] = []; + try { + for (const team of resolved.writes) { + if (!fs.existsSync(team.path)) { + this.atomicFiles.write(team.path, team.content); + createdTeams.push(team.path); + } + } + if (!planTarget.matches) { + this.atomicFiles.write(planTarget.path, serializedPlan); + } + } catch (error) { + for (const teamPath of createdTeams) { + try { + fs.rmSync(teamPath, { force: true }); + } catch { + // 清理失败不能覆盖最初的迁移错误。 + } + } + throw error; + } + return planTarget.file; + } + + /** 已有同内容计划表示曾迁移过,只补缺失编队而不覆盖现有编队。 */ + private writeMissingOriginalTeams(teams: TTeam[]): void { + const writes = this.dependencies.buildTeamPlanWrites( + teams, + this.appPaths.userTeamPlansDir(), + ); + for (const write of writes) { + if (!fs.existsSync(write.path)) { + this.atomicFiles.write(write.path, write.content); + } + } + } + + private matchingPlanTarget( + defaultFile: string, + content: string, + ): { file: string; path: string } | null { + for (let suffix = 0; ; suffix += 1) { + const file = this.legacyPlanFileName(defaultFile, suffix); + const target = path.join( + this.appPaths.userBattlePlansDir(), + file, + ); + if (!fs.existsSync(target)) return null; + if (fs.readFileSync(target, 'utf-8') === content) { + return { file, path: target }; + } + } + } + + private resolvePlanTarget( + defaultFile: string, + content: string, + ): { file: string; path: string; matches: boolean } { + for (let suffix = 0; ; suffix += 1) { + const file = this.legacyPlanFileName(defaultFile, suffix); + const target = path.join( + this.appPaths.userBattlePlansDir(), + file, + ); + if (!fs.existsSync(target)) { + return { file, path: target, matches: false }; + } + if (fs.readFileSync(target, 'utf-8') === content) { + return { file, path: target, matches: true }; + } + } + } + + private legacyPlanFileName(defaultFile: string, suffix: number): string { + if (suffix === 0) return defaultFile; + const extension = path.extname(defaultFile); + const base = defaultFile.slice(0, -extension.length); + const label = suffix === 1 + ? '(旧版)' + : `(旧版 ${suffix})`; + return `${base}${label}${extension}`; + } + + private resolvePlanTeams( + mapRoot: Record, + teams: TTeam[], + ): { + mapRoot: Record; + teams: TTeam[]; + writes: LegacyTeamWrite[]; + } { + const resolvedRoot = structuredClone(mapRoot); + const resolvedTeams: TTeam[] = []; + const writes: LegacyTeamWrite[] = []; + const reservedPaths = new Set(); + for (const team of teams) { + const resolved = this.resolveTeamWrite( + team, + this.appPaths.userTeamPlansDir(), + reservedPaths, + ); + const oldName = this.dependencies.teamName?.(team); + const newName = this.dependencies.teamName?.(resolved.team); + if (oldName && newName && oldName !== newName) { + this.replaceTeamReference(resolvedRoot, oldName, newName); + } + reservedPaths.add(this.pathKey(resolved.write.path)); + resolvedTeams.push(resolved.team); + writes.push(resolved.write); + } + return { + mapRoot: resolvedRoot, + teams: resolvedTeams, + writes, + }; + } + + private resolveTeamWrite( + team: TTeam, + directory: string, + reservedPaths = new Set(), + ): { team: TTeam; write: LegacyTeamWrite } { + const originalName = this.dependencies.teamName?.(team); + let candidate = team; + let suffix = 1; + for (;;) { + const [write] = this.dependencies.buildTeamPlanWrites( + [candidate], + directory, + ); + if (!write) throw new Error('旧舰队未生成迁移文件'); + const reserved = reservedPaths.has(this.pathKey(write.path)); + const conflicts = ( + fs.existsSync(write.path) + && !this.dependencies.teamPlanMatches(write.path, candidate) + ); + if (!reserved && !conflicts) { + return { team: candidate, write }; + } + if ( + !originalName + || !this.dependencies.renameTeam + ) { + throw new Error(`迁移目标已存在,未覆盖:${write.path}`); + } + const name = suffix === 1 + ? `${originalName}(旧版)` + : `${originalName}(旧版 ${suffix})`; + candidate = this.dependencies.renameTeam(team, name); + suffix += 1; + } + } + + private replaceTeamReference( + mapRoot: Record, + oldName: string, + newName: string, + ): void { + if (!Array.isArray(mapRoot.fleet_presets)) return; + mapRoot.fleet_presets = mapRoot.fleet_presets.map(preset => ( + this.isPlainObject(preset) && preset.name === oldName + ? { ...preset, name: newName } + : preset + )); + } + + /** 查找旧版升级后仍滞留在普通计划目录的三类日常任务。 */ + private misplacedDailyPlanFiles(): string[] { + const directory = this.appPaths.userBattlePlansDir(); + return this.dependencies.yamlFiles(directory).flatMap(file => { + const source = path.join(directory, file); + try { + const parsed = yaml.load(fs.readFileSync(source, 'utf-8')); + return ( + this.isPlainObject(parsed) + && this.dependencies.isStandaloneTaskPreset?.(parsed) === true + && this.isDailyTaskType(parsed.task_type) + ) + ? [source] + : []; + } catch { + return []; + } + }); + } + + private recursiveLegacyYamlFiles(): string[] { + const files: string[] = []; + this.collectYamlFiles(this.appPaths.appRoot(), files); + return files.sort((left, right) => left.localeCompare(right)); + } + + private collectYamlFiles(directory: string, files: string[]): void { + if (!fs.existsSync(directory) || this.isExcludedDirectory(directory)) { + return; + } + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.isSymbolicLink()) continue; + const target = path.join(directory, entry.name); + if (entry.isDirectory()) { + this.collectYamlFiles(target, files); + } else if ( + entry.isFile() + && /\.ya?ml$/i.test(entry.name) + ) { + files.push(target); + } + } + } + + private isExcludedDirectory(directory: string): boolean { + const name = path.basename(directory).toLowerCase(); + if (new Set([ + '.git', + '.venv', + 'venv', + 'node_modules', + 'dist', + 'dist-electron', + ]).has(name)) { + return true; + } + return [ + this.appPaths.userBattlePlansDir(), + this.appPaths.userDailyPlansDir(), + this.appPaths.userTeamPlansDir(), + this.appPaths.systemBattlePlansDir(), + this.appPaths.systemDailyPlansDir(), + this.appPaths.systemTeamPlansDir(), + ].some(excluded => this.pathWithin(directory, excluded)); + } + + private isHandledLegacyYaml(source: string): boolean { + const handledDirectories = [ + ...this.legacyDirectories( + 'plans', + this.appPaths.userBattlePlansDir(), + ), + ...this.legacyDirectories( + 'user_battle_plans', + this.appPaths.userBattlePlansDir(), + ), + ...this.legacyDirectories( + 'user_team_plans', + this.appPaths.userTeamPlansDir(), + ), + ]; + return handledDirectories.some(directory => ( + this.pathWithin(source, directory) + )); + } + + private pathWithin(candidate: string, root: string): boolean { + const relative = path.relative( + path.resolve(root), + path.resolve(candidate), + ); + return relative === '' + || ( + relative !== '..' + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); + } + + private migrationKey( + kind: 'plan' | 'team', + source: string, + content: string, + ): string { + const hash = crypto + .createHash('sha256') + .update(content) + .digest('hex'); + return `${kind}-v7:${this.pathKey(source)}:${hash}`; + } + + private rememberPlanTarget( + completed: Set, + migrationKey: string, + target: LegacyPlanReferenceTarget, + ): void { + const prefix = this.planTargetMarkerPrefix(migrationKey); + for (const value of completed) { + if (value.startsWith(prefix)) completed.delete(value); + } + completed.add(`${prefix}${encodeURIComponent(target.file)}`); + } + + private completedPlanTarget( + completed: Set, + migrationKey: string, + sourceFile: string, + ): LegacyPlanReferenceTarget { + const prefix = this.planTargetMarkerPrefix(migrationKey); + const marker = [...completed].find(value => value.startsWith(prefix)); + if (marker) { + try { + const targetFile = decodeURIComponent(marker.slice(prefix.length)); + if ( + targetFile + && !/[\\/]/.test(targetFile) + && /\.ya?ml$/i.test(targetFile) + ) { + return this.referenceTargetFromFile(targetFile); + } + } catch { + // 无效输出记录回退到旧版本默认目标文件名。 + } + } + return { + kind: 'plan', + file: this.migratedUserPlanFileName(sourceFile), + }; + } + + private planTargetMarkerPrefix(migrationKey: string): string { + const hash = crypto + .createHash('sha256') + .update(migrationKey) + .digest('hex'); + return `plan-output-v7:${hash}:`; + } + + private registerFileMapping( + fileMap: Map, + source: string, + target: LegacyPlanReferenceTarget, + ): void { + const relative = path.relative(this.appPaths.appRoot(), source); + for (const value of [source, relative, path.basename(source)]) { + const key = this.planReferenceKey(value); + if (key && !fileMap.has(key)) fileMap.set(key, target); + } + } + + private referenceTargetFromFile( + file: string, + ): LegacyPlanReferenceTarget { + const match = /^(exercise|campaign|decisive)-/.exec( + file.toLowerCase(), + ); + return match + ? { + kind: 'daily', + file, + taskType: match[1] as DailyPlanType, + } + : { kind: 'plan', file }; + } + + private planReferenceKey(value: string): string { + const normalized = value + .trim() + .replace(/\\/g, '/') + .replace(/^\.\/+/, ''); + return process.platform === 'win32' + ? normalized.toLowerCase() + : normalized; + } + + /** 返回去重后的旧目录,并排除当前 userData 目标目录。 */ + private legacyDirectories( + directoryName: 'plans' | 'user_battle_plans' | 'user_team_plans', + targetDirectory: string, + ): string[] { + const candidates = directoryName === 'plans' + ? [ + path.join(this.appPaths.appRoot(), 'plans'), + path.join(this.appPaths.userDataRoot(), 'plans'), + ] + : [ + path.join( + this.appPaths.appRoot(), + 'resource', + directoryName, + ), + path.join( + this.appPaths.resourceRoot(), + 'resource', + directoryName, + ), + ]; + const targetKey = this.pathKey(targetDirectory); + const unique = new Map(); + for (const candidate of candidates) { + const key = this.pathKey(candidate); + if (key !== targetKey && !unique.has(key)) { + unique.set(key, candidate); + } + } + return [...unique.values()]; + } + + private pathKey(value: string): string { + const resolved = path.resolve(value); + return process.platform === 'win32' + ? resolved.toLowerCase() + : resolved; + } + + private migratedUserPlanFileName(file: string): string { + const baseName = this.dependencies.safePlanBaseName(file); + if (!baseName) { + throw new Error(`旧计划文件名不合法: ${file}`); + } + return `bettle-${baseName}.yaml`; + } + + private isPlainObject( + value: unknown, + ): value is Record { + return Boolean(value) + && typeof value === 'object' + && !Array.isArray(value); + } +} diff --git a/electron/services/MigrationConflictService.ts b/electron/services/MigrationConflictService.ts new file mode 100644 index 0000000..482725a --- /dev/null +++ b/electron/services/MigrationConflictService.ts @@ -0,0 +1,623 @@ +/** + * 管理旧配置迁移后需要用户确认的 YAML 冲突。 + * + * 处理流程: + * 1. 迁移结束后扫描系统与用户受管目录。 + * 2. YAML 先解析为结构化数据,再比较内容是否完全一致。 + * 3. 同名异内容和迁移生成的“旧版”副本也作为冲突原因记录。 + * 4. 待处理清单写入 userData,关闭窗口不会丢失确认任务。 + * 5. Renderer 只获得冲突 ID、文件名和原因,不获得绝对路径。 + * 6. 用户提交的是保留 ID;其余文件才进入删除候选。 + * 7. 删除前再次校验文件名、文件类型和内容哈希。 + * 8. 内容已变化的文件不会删除,并留到下次继续处理。 + * 9. 与系统预设完全相同的文件删除前会重写任务列表和自动胖次引用。 + * 10. 同名异内容没有安全替代项,只按用户明确选择删除。 + */ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { isDeepStrictEqual } from 'util'; +import * as yaml from 'js-yaml'; +import type { + MigrationConflictItem, + MigrationConflictKind, + MigrationConflictListResult, + MigrationConflictReason, + MigrationConflictResolutionResult, +} from '../../src/shared/migrationConflicts'; +import { AppPaths } from './AppPaths'; +import { AtomicFileStore } from './AtomicFileStore'; + +const MIGRATION_CONFLICT_STATE_VERSION = 1; + +interface StoredMigrationConflict extends MigrationConflictItem { + contentHash: string; + systemReplacement?: string; +} + +interface MigrationConflictState { + version: number; + status: 'pending' | 'resolved'; + conflicts: StoredMigrationConflict[]; + acceptedIds: string[]; +} + +interface ParsedYamlFile { + file: string; + value: unknown; +} + +/** 检测、持久化并安全处理迁移产生的用户 YAML 冲突。 */ +export class MigrationConflictService { + constructor( + private readonly appPaths: AppPaths, + private readonly atomicFiles: AtomicFileStore, + ) {} + + /** + * 应用升级到该功能后至少扫描一次;本次实际发生迁移时强制重扫。 + */ + prepareAfterMigration(forceScan: boolean): void { + const current = this.readState(); + if (current && !forceScan) return; + const acceptedIds = current?.acceptedIds ?? []; + const accepted = new Set(acceptedIds); + const conflicts = this.detectConflicts().filter(conflict => ( + !accepted.has(conflict.id) + )); + this.writeState({ + version: MIGRATION_CONFLICT_STATE_VERSION, + status: conflicts.length > 0 ? 'pending' : 'resolved', + conflicts, + acceptedIds, + }); + } + + /** 返回仍存在且内容未变化的待处理文件。 */ + pending(): MigrationConflictListResult { + const state = this.readState(); + if (!state || state.status !== 'pending') { + return { pending: false, conflicts: [] }; + } + const conflicts = state.conflicts.filter(conflict => ( + this.conflictFileStillMatches(conflict) + )); + if (conflicts.length !== state.conflicts.length) { + this.writeState({ + ...state, + status: conflicts.length > 0 ? 'pending' : 'resolved', + conflicts, + }); + } + return { + pending: conflicts.length > 0, + conflicts: conflicts.map(conflict => this.publicConflict(conflict)), + }; + } + + /** + * 保留 keepIds 中的文件,删除其余待处理文件。 + * + * 删除目标始终由持久化冲突记录反查,Renderer 不能传入路径。 + */ + resolve(keepIds: unknown): MigrationConflictResolutionResult { + const state = this.readState(); + if (!state || state.status !== 'pending') { + return { + success: true, + kept: 0, + deleted: 0, + errors: [], + remaining: [], + }; + } + if (!Array.isArray(keepIds) || keepIds.some(id => ( + typeof id !== 'string' + ))) { + throw new Error('保留清单格式无效'); + } + const knownIds = new Set(state.conflicts.map(conflict => conflict.id)); + const keep = new Set(keepIds as string[]); + if ([...keep].some(id => !knownIds.has(id))) { + throw new Error('保留清单包含未知冲突项'); + } + + const kept = state.conflicts.filter(conflict => keep.has(conflict.id)); + const requestedDeletes = state.conflicts.filter(conflict => ( + !keep.has(conflict.id) + )); + const validDeletes: StoredMigrationConflict[] = []; + const remaining: StoredMigrationConflict[] = []; + const errors: string[] = []; + + for (const conflict of requestedDeletes) { + if (this.conflictFileStillMatches(conflict)) { + validDeletes.push(conflict); + } else { + remaining.push(conflict); + errors.push(`${conflict.file} 已变化,未执行删除`); + } + } + + try { + this.rewriteTaskGroupReferences(validDeletes); + this.rewriteAutomationReferences(validDeletes); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + errors.push(`配置引用更新失败:${message}`); + remaining.push(...validDeletes); + validDeletes.length = 0; + } + + let deleted = 0; + for (const conflict of validDeletes) { + try { + fs.rmSync(this.userFilePath(conflict.kind, conflict.file)); + deleted += 1; + } catch (error) { + remaining.push(conflict); + const message = error instanceof Error ? error.message : String(error); + errors.push(`${conflict.file} 删除失败:${message}`); + } + } + + this.writeState({ + version: MIGRATION_CONFLICT_STATE_VERSION, + status: remaining.length > 0 ? 'pending' : 'resolved', + conflicts: remaining, + acceptedIds: [ + ...new Set([ + ...state.acceptedIds, + ...kept.map(conflict => conflict.id), + ]), + ].sort(), + }); + return { + success: errors.length === 0, + kept: kept.length, + deleted, + errors, + remaining: remaining.map(conflict => this.publicConflict(conflict)), + }; + } + + /** 比较普通计划和日常任务两个受管目录。 */ + private detectConflicts(): StoredMigrationConflict[] { + const conflicts = [ + ...this.detectDirectoryConflicts( + 'battle', + this.appPaths.userBattlePlansDir(), + this.appPaths.systemBattlePlansDir(), + ), + ...this.detectDirectoryConflicts( + 'daily', + this.appPaths.userDailyPlansDir(), + this.appPaths.systemDailyPlansDir(), + ), + ]; + return conflicts.sort((left, right) => ( + left.kind.localeCompare(right.kind) + || left.file.localeCompare(right.file, 'zh-CN') + )); + } + + private detectDirectoryConflicts( + kind: MigrationConflictKind, + userDirectory: string, + systemDirectory: string, + ): StoredMigrationConflict[] { + const systemFiles = this.parsedYamlFiles(systemDirectory); + const systemByName = new Map(systemFiles.map(file => [ + file.file.toLocaleLowerCase(), + file, + ])); + const userFiles = this.parsedYamlFiles(userDirectory); + const userNames = new Set(userFiles.map(file => ( + file.file.toLocaleLowerCase() + ))); + const output: StoredMigrationConflict[] = []; + + for (const userFile of userFiles) { + const reasons: MigrationConflictReason[] = []; + const exactSystem = systemFiles.find(systemFile => ( + isDeepStrictEqual(userFile.value, systemFile.value) + )); + if (exactSystem) { + reasons.push({ + reasonCode: 'same_as_system_preset', + reason: `与系统预设「${this.displayName(exactSystem.file)}」内容完全相同`, + relatedFile: exactSystem.file, + }); + } else { + const sameName = systemByName.get(userFile.file.toLocaleLowerCase()); + if (sameName) { + reasons.push({ + reasonCode: 'same_name_as_system_preset', + reason: `与系统预设「${this.displayName(sameName.file)}」文件名相同,但配置内容不同`, + relatedFile: sameName.file, + }); + } + } + + const originalFile = this.originalFileForLegacyCopy(userFile.file); + if ( + originalFile + && userNames.has(originalFile.toLocaleLowerCase()) + ) { + reasons.push({ + reasonCode: 'legacy_copy_name_conflict', + reason: `迁移时因名称冲突保存为旧版副本;现有文件为「${this.displayName(originalFile)}」`, + relatedFile: originalFile, + }); + } + if (reasons.length === 0) continue; + + const content = fs.readFileSync( + path.join(userDirectory, userFile.file), + 'utf-8', + ); + const contentHash = this.contentHash(content); + const id = this.contentHash([ + kind, + userFile.file, + contentHash, + ...reasons.map(reason => reason.reasonCode), + ].join('\n')); + output.push({ + id, + kind, + file: userFile.file, + name: this.displayName(userFile.file), + reasons, + contentHash, + systemReplacement: exactSystem?.file, + }); + } + return output; + } + + private parsedYamlFiles(directory: string): ParsedYamlFile[] { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory, { withFileTypes: true }) + .filter(entry => ( + entry.isFile() + && this.isManagedYamlFile(entry.name) + )) + .flatMap(entry => { + try { + return [{ + file: entry.name, + value: yaml.load( + fs.readFileSync(path.join(directory, entry.name), 'utf-8'), + ), + }]; + } catch { + return []; + } + }); + } + + private conflictFileStillMatches( + conflict: StoredMigrationConflict, + ): boolean { + if (!this.isManagedYamlFile(conflict.file)) return false; + const target = this.userFilePath(conflict.kind, conflict.file); + try { + if (!fs.existsSync(target)) return false; + const stat = fs.lstatSync(target); + if (!stat.isFile() || stat.isSymbolicLink()) return false; + return this.contentHash(fs.readFileSync(target, 'utf-8')) + === conflict.contentHash; + } catch { + return false; + } + } + + /** 删除完全相同副本时,把任务组身份切换到对应系统预设。 */ + private rewriteTaskGroupReferences( + conflicts: StoredMigrationConflict[], + ): void { + const replacements = new Map(); + for (const conflict of conflicts) { + if (!conflict.systemReplacement) continue; + replacements.set( + `${conflict.kind}:${conflict.file.toLocaleLowerCase()}`, + conflict.systemReplacement, + ); + } + if (replacements.size === 0) return; + + const target = path.join( + this.appPaths.userDataRoot(), + 'task_groups.json', + ); + if (!fs.existsSync(target)) return; + const root = JSON.parse( + fs.readFileSync(target, 'utf-8'), + ) as Record; + if (!Array.isArray(root.groups)) return; + let changed = false; + const groups = root.groups.map(group => { + if (!this.isPlainObject(group) || !Array.isArray(group.items)) { + return group; + } + return { + ...group, + items: group.items.map(item => { + if (!this.isPlainObject(item)) return item; + const managedFile = typeof item.managedFile === 'string' + ? item.managedFile + : ''; + const battleReplacement = item.managedSource === 'user' + ? replacements.get( + `battle:${managedFile.toLocaleLowerCase()}`, + ) + : undefined; + if (battleReplacement) { + changed = true; + return { + ...item, + managedSource: 'system', + managedFile: battleReplacement, + }; + } + const dailyFile = typeof item.dailyFile === 'string' + ? item.dailyFile + : ''; + const dailyReplacement = item.dailySource === 'user' + ? replacements.get( + `daily:${dailyFile.toLocaleLowerCase()}`, + ) + : undefined; + if (!dailyReplacement) return item; + changed = true; + return { + ...item, + dailySource: 'system', + dailyFile: dailyReplacement, + }; + }), + }; + }); + if (changed) { + this.atomicFiles.write( + target, + JSON.stringify({ ...root, groups }, null, 2), + ); + } + } + + /** 删除完全相同副本时,同步切换自动胖次候选和当前选择。 */ + private rewriteAutomationReferences( + conflicts: StoredMigrationConflict[], + ): void { + const replacements = new Map(); + for (const conflict of conflicts) { + if (conflict.kind !== 'battle' || !conflict.systemReplacement) { + continue; + } + replacements.set( + conflict.file.toLocaleLowerCase(), + conflict.systemReplacement, + ); + } + if (replacements.size === 0) return; + + const target = path.join( + this.appPaths.userDataRoot(), + 'gui_settings.json', + ); + if (!fs.existsSync(target)) return; + const root = JSON.parse(fs.readFileSync(target, 'utf-8')) as unknown; + if (!this.isPlainObject(root)) return; + const automation = root.automation; + if (!this.isPlainObject(automation)) return; + + let changed = false; + let lootPlans = automation.lootPlans; + if (Array.isArray(lootPlans)) { + const seen = new Set(); + lootPlans = lootPlans.map(item => { + if ( + !this.isPlainObject(item) + || item.source !== 'user' + || typeof item.file !== 'string' + ) { + return item; + } + const replacement = replacements.get( + item.file.toLocaleLowerCase(), + ); + if (!replacement) return item; + changed = true; + return { + ...item, + source: 'system', + file: replacement, + }; + }).filter(item => { + if ( + !this.isPlainObject(item) + || (item.source !== 'system' && item.source !== 'user') + || typeof item.file !== 'string' + ) { + return true; + } + const key = `${item.source}:${item.file.toLocaleLowerCase()}`; + if (seen.has(key)) { + changed = true; + return false; + } + seen.add(key); + return true; + }); + } + + const selectedFile = typeof automation.lootPlanId === 'string' + ? automation.lootPlanId + : ''; + const selectedReplacement = automation.lootPlanSource === 'user' + ? replacements.get(selectedFile.toLocaleLowerCase()) + : undefined; + if (selectedReplacement) changed = true; + if (!changed) return; + + this.atomicFiles.write( + target, + JSON.stringify({ + ...root, + automation: { + ...automation, + lootPlans, + ...(selectedReplacement + ? { + lootPlanSource: 'system', + lootPlanId: selectedReplacement, + } + : {}), + }, + }, null, 2), + ); + } + + private readState(): MigrationConflictState | null { + const target = this.statePath(); + if (!fs.existsSync(target)) return null; + try { + const parsed = JSON.parse(fs.readFileSync(target, 'utf-8')) as unknown; + if ( + !this.isPlainObject(parsed) + || parsed.version !== MIGRATION_CONFLICT_STATE_VERSION + || (parsed.status !== 'pending' && parsed.status !== 'resolved') + || !Array.isArray(parsed.conflicts) + || ( + parsed.acceptedIds !== undefined + && ( + !Array.isArray(parsed.acceptedIds) + || parsed.acceptedIds.some(id => typeof id !== 'string') + ) + ) + ) { + return null; + } + const conflicts = parsed.conflicts.filter( + (value): value is StoredMigrationConflict => ( + this.isStoredConflict(value) + ), + ); + return { + version: MIGRATION_CONFLICT_STATE_VERSION, + status: parsed.status, + conflicts, + acceptedIds: Array.isArray(parsed.acceptedIds) + ? parsed.acceptedIds as string[] + : [], + }; + } catch { + return null; + } + } + + private writeState(state: MigrationConflictState): void { + fs.mkdirSync(this.appPaths.userDataRoot(), { recursive: true }); + this.atomicFiles.write( + this.statePath(), + JSON.stringify(state, null, 2), + ); + } + + private statePath(): string { + return path.join( + this.appPaths.userDataRoot(), + '.migration-conflicts.json', + ); + } + + private userFilePath( + kind: MigrationConflictKind, + file: string, + ): string { + if (!this.isManagedYamlFile(file)) { + throw new Error('冲突文件名无效'); + } + const directory = kind === 'battle' + ? this.appPaths.userBattlePlansDir() + : this.appPaths.userDailyPlansDir(); + return path.join(directory, file); + } + + private isStoredConflict( + value: unknown, + ): value is StoredMigrationConflict { + if (!this.isPlainObject(value)) return false; + return ( + typeof value.id === 'string' + && (value.kind === 'battle' || value.kind === 'daily') + && typeof value.file === 'string' + && this.isManagedYamlFile(value.file) + && typeof value.name === 'string' + && Array.isArray(value.reasons) + && value.reasons.every(reason => ( + this.isPlainObject(reason) + && typeof reason.reasonCode === 'string' + && typeof reason.reason === 'string' + )) + && typeof value.contentHash === 'string' + && ( + value.systemReplacement === undefined + || ( + typeof value.systemReplacement === 'string' + && this.isManagedYamlFile(value.systemReplacement) + ) + ) + ); + } + + private publicConflict( + conflict: StoredMigrationConflict, + ): MigrationConflictItem { + return { + id: conflict.id, + kind: conflict.kind, + file: conflict.file, + name: conflict.name, + reasons: conflict.reasons.map(reason => ({ ...reason })), + }; + } + + private originalFileForLegacyCopy(file: string): string | null { + const extension = path.extname(file); + const base = file.slice(0, -extension.length); + const original = base.replace(/(旧版(?: \d+)?)$/, ''); + return original === base ? null : `${original}${extension}`; + } + + private displayName(file: string): string { + return file + .replace(/\.ya?ml$/i, '') + .replace(/^(?:bettle|exercise|campaign|decisive)-/i, ''); + } + + private isManagedYamlFile(file: string): boolean { + return ( + file.length > 0 + && file.length <= 255 + && !/[\\/\x00-\x1f]/.test(file) + && /\.ya?ml$/i.test(file) + ); + } + + private contentHash(content: string): string { + return crypto.createHash('sha256').update(content).digest('hex'); + } + + private isPlainObject( + value: unknown, + ): value is Record { + return ( + typeof value === 'object' + && value !== null + && !Array.isArray(value) + ); + } +} diff --git a/electron/services/PlanExportService.ts b/electron/services/PlanExportService.ts new file mode 100644 index 0000000..ee70f8b --- /dev/null +++ b/electron/services/PlanExportService.ts @@ -0,0 +1,135 @@ +/** + * 把用户勾选的出征计划和舰队方案导出为 ZIP。 + * Renderer 只提交计划类型和文件名,主进程始终从受管用户目录重新定位文件。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import JSZip from 'jszip'; +import type { CombatPlanRepository } from './CombatPlanRepository'; +import type { TeamPlanRepository } from './TeamPlanRepository'; + +export interface UserPlanExportSelection { + kind: 'battle' | 'team'; + file: string; +} + +export interface UserPlanArchive { + content: Buffer; + count: number; +} + +/** 用户计划 ZIP 的两个固定目录名。 */ +const ARCHIVE_DIRECTORIES = { + battle: 'user_battle_plans', + team: 'user_team_plans', +} as const; + +/** 负责用户计划导出的输入校验、路径约束和 ZIP 生成。 */ +export class PlanExportService { + constructor( + private readonly combatRepository: CombatPlanRepository, + private readonly teamRepository: TeamPlanRepository, + ) {} + + /** 使用本地日期生成导出文件名。 */ + archiveFileName(now = new Date()): string { + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}-plans.zip`; + } + + /** 校验用户选择并生成包含固定分类目录的 ZIP。 */ + async createArchive(rawSelections: unknown): Promise { + const selections = this.normalizeSelections(rawSelections); + const zip = new JSZip(); + zip.folder(ARCHIVE_DIRECTORIES.battle); + zip.folder(ARCHIVE_DIRECTORIES.team); + + selections.forEach(selection => { + const filePath = this.resolveUserPlan(selection); + const stats = fs.statSync(filePath); + zip.file( + `${ARCHIVE_DIRECTORIES[selection.kind]}/${selection.file}`, + fs.readFileSync(filePath), + { date: stats.mtime }, + ); + }); + + return { + content: await zip.generateAsync({ + type: 'nodebuffer', + compression: 'DEFLATE', + compressionOptions: { level: 6 }, + platform: 'DOS', + }), + count: selections.length, + }; + } + + /** 将已生成的 ZIP 写到系统保存对话框返回的位置。 */ + writeArchive(filePath: string, archive: UserPlanArchive): void { + fs.writeFileSync(filePath, archive.content); + } + + /** Renderer 输入必须是去重后的用户计划文件清单。 */ + private normalizeSelections(rawSelections: unknown): UserPlanExportSelection[] { + if (!Array.isArray(rawSelections) || rawSelections.length === 0) { + throw new Error('请至少选择一个用户配置'); + } + if (rawSelections.length > 1000) { + throw new Error('单次最多导出 1000 个用户配置'); + } + + const selections: UserPlanExportSelection[] = []; + const keys = new Set(); + rawSelections.forEach(value => { + if ( + typeof value !== 'object' + || value === null + || !('kind' in value) + || !('file' in value) + ) { + throw new Error('导出配置清单格式不正确'); + } + const { kind, file } = value as Record; + if ( + (kind !== 'battle' && kind !== 'team') + || typeof file !== 'string' + || path.basename(file) !== file + || !/\.ya?ml$/i.test(file) + ) { + throw new Error('导出配置包含非法文件名'); + } + const key = `${kind}:${file.toLocaleLowerCase('en-US')}`; + if (keys.has(key)) return; + keys.add(key); + selections.push({ kind, file }); + }); + return selections; + } + + /** 只允许读取对应用户计划目录内真实存在的普通文件。 */ + private resolveUserPlan(selection: UserPlanExportSelection): string { + const directory = selection.kind === 'battle' + ? this.combatRepository.directory('user') + : this.teamRepository.directory('user'); + const candidate = path.join(directory, selection.file); + if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) { + throw new Error(`用户配置不存在:${selection.file}`); + } + + const canonicalDirectory = fs.realpathSync.native(directory); + const canonicalCandidate = fs.realpathSync.native(candidate); + const relative = path.relative(canonicalDirectory, canonicalCandidate); + if ( + relative === '' + || relative === '..' + || relative.startsWith(`..${path.sep}`) + || path.isAbsolute(relative) + ) { + throw new Error(`用户配置超出允许目录:${selection.file}`); + } + return canonicalCandidate; + } +} diff --git a/electron/services/PlanManagementService.ts b/electron/services/PlanManagementService.ts new file mode 100644 index 0000000..d83b9f3 --- /dev/null +++ b/electron/services/PlanManagementService.ts @@ -0,0 +1,814 @@ +/** + * 编排作战计划的查询、保存和删除。 + */ +import * as path from 'path'; +import { CombatPlanCodec } from './CombatPlanCodec'; +import { CombatPlanRepository } from './CombatPlanRepository'; +import { GuiSettingsStore } from './GuiSettingsStore'; +import { RuntimePlanService } from './RuntimePlanService'; +import { + type PlanFileReadError, + type PlanPresetSource, + TEAM_FILE_PATTERN, + type UserTeamPlan, +} from './TeamPlanCodec'; +import { TeamPlanRepository } from './TeamPlanRepository'; +import { + TaskPresetCodec, + type TaskPresetType, +} from './TaskPresetCodec'; + +export type ManagedBattleResult = 'D' | 'C' | 'B' | 'A' | 'S' | 'SS'; + +export interface ManagedBattlePlanFleetSummary { + name: string; + source: PlanPresetSource | 'deleted'; + primaryCount: number; + backupCount: number; +} + +export interface ManagedBattlePlanSummary { + kind: 'battle' | 'preset'; + file: string; + name: string; + source: PlanPresetSource; + modifiedAt: number; + chapter: number | string; + map: number | string; + times: number; + gap: number; + fleetId: number; + repairMode: number | number[]; + result: ManagedBattleResult | null; + lootCountGe: number; + shipCountGe: number; + fleetCount: number; + nodeCount: number; + fleets: ManagedBattlePlanFleetSummary[]; + taskType?: TaskPresetType; + campaignName?: string; +} + +export interface PlanManagementResult { + bindings: Array<{ + planFile: string; + planName: string; + source: PlanPresetSource; + teamName: string | null; + }>; + battlePlans: ManagedBattlePlanSummary[]; + teamPlans: Array<{ + file: string; + name: string; + source: PlanPresetSource; + }>; + errors: PlanFileReadError[]; + ignoredUnlinkedPlans: string[]; +} + +/** 编排管理页查询、计划读取、保存、重命名和删除。 */ +export class PlanManagementService { + constructor( + private readonly combatCodec: CombatPlanCodec, + private readonly combatRepository: CombatPlanRepository, + private readonly runtimePlans: RuntimePlanService, + private readonly teamRepository: TeamPlanRepository, + private readonly settings: GuiSettingsStore, + private readonly taskPresetCodec: TaskPresetCodec, + ) {} + + /** 读取管理页所需的出征计划、舰队方案及名称关联。 */ + get(): PlanManagementResult { + const bindings: PlanManagementResult['bindings'] = []; + const battlePlans: ManagedBattlePlanSummary[] = []; + const errors: PlanFileReadError[] = []; + const listedTeams = this.teamRepository.list(); + errors.push(...listedTeams.errors); + const sources: Array<{ + directory: string; + source: PlanPresetSource; + }> = [ + { + directory: this.combatRepository.directory('system'), + source: 'system', + }, + { + directory: this.combatRepository.directory('user'), + source: 'user', + }, + ]; + + sources.forEach(({ directory, source }) => { + this.combatRepository.yamlFiles(directory).forEach((file) => { + try { + const planPath = path.join(directory, file); + const root = this.combatCodec.parseRoot( + this.combatRepository.read(planPath), + '无效的方案文件', + ); + const rootName = typeof root.name === 'string' + ? root.name.trim() + : ''; + const fileName = file.replace(/\.ya?ml$/i, ''); + const standardName = fileName.match(/^bettle-(.+)$/i)?.[1]; + const planName = standardName || rootName || fileName; + if (this.taskPresetCodec.isStandalone(root)) { + const preset = this.taskPresetCodec.normalize(root); + if (this.isDailyTaskType(preset.task_type)) return; + const times = Number(preset.times); + const gap = Number(preset.gap); + const fleetId = Number(preset.fleet_id); + battlePlans.push({ + kind: 'preset', + file, + name: planName, + source, + modifiedAt: this.combatRepository.modifiedAt(planPath), + chapter: ( + typeof preset.chapter === 'number' + || typeof preset.chapter === 'string' + ) + ? preset.chapter + : '-', + map: '-', + times: Number.isFinite(times) && times > 0 ? times : 1, + gap: Number.isFinite(gap) && gap >= 0 ? gap : 0, + fleetId: Number.isFinite(fleetId) && fleetId > 0 + ? fleetId + : 1, + repairMode: 1, + result: null, + lootCountGe: -1, + shipCountGe: -1, + fleetCount: 0, + nodeCount: 0, + fleets: [], + taskType: preset.task_type, + campaignName: typeof preset.campaign_name === 'string' + ? preset.campaign_name + : undefined, + }); + bindings.push({ + planFile: file, + planName, + source, + teamName: null, + }); + return; + } + const presets = Array.isArray(root.fleet_presets) + ? root.fleet_presets + : []; + const selectedNodes = Array.isArray(root.selected_nodes) + ? root.selected_nodes + : []; + const times = Number(root.times); + const gap = Number(root.gap); + const fleetId = Number(root.fleet_id); + const repairModeValue = Number(root.repair_mode); + const repairModeList = Array.isArray(root.repair_mode) + ? root.repair_mode + .map(value => Number(value)) + .filter(value => Number.isFinite(value)) + : []; + const normalizedResult = typeof root.result === 'string' + ? root.result.trim().toUpperCase() + : ''; + const result = ( + ['D', 'C', 'B', 'A', 'S', 'SS'] as ManagedBattleResult[] + ).includes(normalizedResult as ManagedBattleResult) + ? normalizedResult as ManagedBattleResult + : null; + const stopCondition = this.combatCodec.isPlainObject( + root.stop_condition, + ) + ? root.stop_condition + : {}; + const lootCountGe = Number(stopCondition.loot_count_ge); + const shipCountGe = Number(stopCondition.ship_count_ge); + const fleets = presets.flatMap((preset, index) => { + if (!this.combatCodec.isPlainObject(preset)) return []; + const name = ( + typeof preset.name === 'string' && preset.name.trim() + ) + ? preset.name.trim() + : `编队 ${index + 1}`; + const sameSourceTeam = listedTeams.plans.find(team => ( + team.name === name && team.source === source + )) ?? null; + const embeddedShips = Array.isArray(preset.ships) + ? preset.ships + : null; + const matchingPlan = sameSourceTeam + ?? (embeddedShips + ? null + : this.teamRepository.find( + name, + source, + listedTeams.plans, + )); + const ships = matchingPlan?.ships ?? embeddedShips ?? []; + return [{ + name, + source: ( + matchingPlan?.source ?? 'deleted' + ) as ManagedBattlePlanFleetSummary['source'], + primaryCount: ships.filter(ship => ( + (typeof ship === 'string' && Boolean(ship.trim())) + || ( + this.combatCodec.isPlainObject(ship) + && typeof ship.name === 'string' + && Boolean(ship.name.trim()) + ) + )).length, + backupCount: ships.reduce((count, ship) => ( + count + ( + this.combatCodec.isPlainObject(ship) + && Array.isArray(ship.candidates) + ? ship.candidates.length + : 0 + ) + ), 0), + }]; + }); + battlePlans.push({ + kind: 'battle', + file, + name: planName, + source, + modifiedAt: this.combatRepository.modifiedAt(planPath), + chapter: ( + typeof root.chapter === 'number' + || typeof root.chapter === 'string' + ) + ? root.chapter + : '?', + map: ( + typeof root.map === 'number' + || typeof root.map === 'string' + ) + ? root.map + : '?', + times: Number.isFinite(times) && times > 0 ? times : 1, + gap: Number.isFinite(gap) && gap >= 0 ? gap : 0, + fleetId: Number.isFinite(fleetId) && fleetId > 0 + ? fleetId + : 1, + repairMode: repairModeList.length > 0 + ? repairModeList + : Number.isFinite(repairModeValue) + ? repairModeValue + : 1, + result, + lootCountGe: Number.isFinite(lootCountGe) && lootCountGe > 0 + ? lootCountGe + : -1, + shipCountGe: Number.isFinite(shipCountGe) && shipCountGe > 0 + ? shipCountGe + : -1, + fleetCount: fleets.length, + nodeCount: selectedNodes.length, + fleets, + }); + if (presets.length === 0) { + bindings.push({ + planFile: file, + planName, + source, + teamName: null, + }); + return; + } + presets.forEach((preset) => { + const teamName = this.combatCodec.isPlainObject(preset) + && typeof preset.name === 'string' + ? preset.name.trim() || null + : null; + bindings.push({ + planFile: file, + planName, + source, + teamName, + }); + }); + } catch (error) { + const message = error instanceof Error + ? error.message + : String(error); + errors.push({ + file, + source, + kind: 'battle', + message, + }); + } + }); + }); + + const teamPlans = listedTeams.plans.map(plan => ({ + file: plan.file ?? '', + name: plan.name, + source: plan.source ?? 'user', + })); + return { + bindings, + battlePlans, + teamPlans, + errors, + ignoredUnlinkedPlans: this.getIgnoredUnlinkedPlans(), + }; + } + + /** 更新一条管理页未关联计划的忽略状态。 */ + setUnlinkedIgnored( + kind: 'battle' | 'team', + source: PlanPresetSource, + file: string, + ignored: boolean, + ): string[] { + const key = this.ignoredUnlinkedPlanKey(kind, source, file); + if (!key) return this.getIgnoredUnlinkedPlans(); + const values = new Set(this.getIgnoredUnlinkedPlans()); + if (ignored === true) values.add(key); + else values.delete(key); + return this.writeIgnoredUnlinkedPlans(values); + } + + /** 读取一份受管计划,并返回后端执行所需的展开文件。 */ + readManaged( + source: PlanPresetSource, + file: string, + ): Record { + try { + const sourcePath = this.combatRepository.safeManagedPath(source, file); + if (!sourcePath || !this.combatRepository.exists(sourcePath)) { + throw new Error('出征计划不存在'); + } + const originalContent = this.combatRepository.read(sourcePath); + const root = this.combatCodec.parseRoot( + originalContent, + '无效的方案文件', + ); + if (this.taskPresetCodec.isStandalone(root)) { + const preset = this.taskPresetCodec.normalize(root); + if (this.isDailyTaskType(preset.task_type)) { + throw new Error('该配置属于日常任务,请从“加载日常任务”使用'); + } + return { + success: true, + kind: 'preset', + path: sourcePath, + sourcePath, + content: originalContent, + source, + }; + } + const prepared = this.runtimePlans.prepare(source, file); + return { + success: true, + kind: 'battle', + path: prepared.sourcePath, + sourcePath: prepared.sourcePath, + runtimePath: prepared.runtimePath, + content: prepared.content, + source, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 读取已经过应用路径边界校验的计划文件。 */ + readResolvedFile(resolved: string): Record { + try { + const managed = this.combatRepository.managedFromPath(resolved); + if (managed) { + return this.readManaged(managed.source, managed.file); + } + if (!this.combatRepository.exists(resolved)) { + throw new Error('出征计划不存在'); + } + return { + success: true, + path: resolved, + sourcePath: resolved, + content: this.combatRepository.read(resolved), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 把编辑器内容校验并写成后端执行用临时文件。 */ + prepareExecution( + content: string, + hint: string, + ): Record { + try { + if (typeof content !== 'string') { + throw new Error('出征计划内容不合法'); + } + return { + success: true, + path: this.runtimePlans.write( + content, + typeof hint === 'string' ? hint : 'plan', + ), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 升级用户选择的本地 YAML,并写入用户受管计划目录。 */ + importLocal( + selectedPath: string, + overwrite = false, + ): Record { + try { + if ( + typeof selectedPath !== 'string' + || !path.isAbsolute(selectedPath) + || !/\.ya?ml$/i.test(path.basename(selectedPath)) + ) { + throw new Error('本地出征计划路径不合法'); + } + if (!this.combatRepository.exists(selectedPath)) { + throw new Error('本地出征计划不存在'); + } + return this.saveManaged( + path.basename(selectedPath), + this.combatRepository.read(selectedPath), + overwrite, + ); + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 保存、覆盖或重命名一份受管出征计划。 */ + saveManaged( + rawName: string, + content: string, + overwrite: boolean, + currentFile?: string, + ): Record { + try { + const name = typeof rawName === 'string' + ? this.combatCodec.safeBaseName(rawName) + : ''; + if (!name) throw new Error('请先填写预设名称'); + if (typeof content !== 'string') { + throw new Error('出征计划内容不合法'); + } + const parsed = this.combatCodec.parseRoot( + content, + '出征计划根节点必须是对象', + ); + + if (this.taskPresetCodec.isStandalone(parsed)) { + const preset = this.taskPresetCodec.normalize(parsed); + if (this.isDailyTaskType(preset.task_type)) { + throw new Error( + '演习、战役和决战配置请使用“加载日常任务”管理', + ); + } + return this.saveManagedTaskPreset( + name, + content, + preset, + overwrite, + currentFile, + ); + } + const split = this.combatCodec.normalizeFleetPresets( + parsed, + 'user', + false, + ); + const file = `bettle-${name}.yaml`; + const target = this.combatRepository.safeUserPath(file); + if (!target) throw new Error('出征计划名称不合法'); + this.combatRepository.initializeUserDirectory(); + + let currentPath: string | null = null; + if (currentFile !== undefined) { + currentPath = this.combatRepository.safeUserPath(currentFile); + if (!currentPath) { + throw new Error('当前出征计划文件名不符合规则'); + } + } + const updatesCurrentFile = currentPath !== null + && path.resolve(currentPath).toLowerCase() + === path.resolve(target).toLowerCase(); + const mapConflict = ( + this.combatRepository.exists(target) + && !updatesCurrentFile + ); + const teamDirectory = this.teamRepository.directory('user'); + const teamWrites = this.teamRepository.buildWrites( + split.teams, + teamDirectory, + ).map((item, index) => ({ + ...item, + unchanged: this.teamRepository.matches( + item.path, + split.teams[index], + ), + })); + const conflicts = [ + ...(mapConflict ? [`地图:${file}`] : []), + ...teamWrites + .filter(item => ( + this.teamRepository.exists(item.path) && !item.unchanged + )) + .map(item => `舰队:${item.name}`), + ]; + if (conflicts.length > 0 && overwrite !== true) { + return { + success: false, + exists: true, + file, + source: 'user', + conflicts, + error: '存在同名配置', + }; + } + + this.teamRepository.initializeUserDirectory(); + for (const item of teamWrites) { + if (!item.unchanged) { + this.teamRepository.write(item.path, item.content); + } + } + this.combatRepository.write( + target, + this.combatCodec.serialize(split.mapRoot, content), + ); + if ( + currentPath + && !updatesCurrentFile + && this.combatRepository.exists(currentPath) + ) { + this.combatRepository.remove(currentPath); + } + + if (currentFile && currentFile !== file) { + this.moveIgnoredKey('battle', 'user', currentFile, file); + } + return { + success: true, + kind: 'battle', + file, + path: target, + source: 'user', + teamFiles: teamWrites.map(item => item.file), + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 保存独立任务预设,不把它错误展开成地图计划。 */ + private saveManagedTaskPreset( + name: string, + content: string, + parsed: Record, + overwrite: boolean, + currentFile: string | undefined, + ): Record { + const preset = this.taskPresetCodec.normalize(parsed); + const file = `bettle-${name}.yaml`; + const target = this.combatRepository.safeUserPath(file); + if (!target) throw new Error('任务预设名称不合法'); + this.combatRepository.initializeUserDirectory(); + + const currentPath = currentFile === undefined + ? null + : this.combatRepository.safeUserPath(currentFile); + if (currentFile !== undefined && !currentPath) { + throw new Error('当前任务预设文件名不符合规则'); + } + const updatesCurrentFile = currentPath !== null + && path.resolve(currentPath).toLowerCase() + === path.resolve(target).toLowerCase(); + if ( + this.combatRepository.exists(target) + && !updatesCurrentFile + && overwrite !== true + ) { + return { + success: false, + exists: true, + kind: 'preset', + file, + source: 'user', + conflicts: [`任务预设:${file}`], + error: '存在同名配置', + }; + } + + this.combatRepository.write( + target, + this.combatCodec.serialize(preset, content), + ); + if ( + currentPath + && !updatesCurrentFile + && this.combatRepository.exists(currentPath) + ) { + this.combatRepository.remove(currentPath); + } + if (currentFile && currentFile !== file) { + this.moveIgnoredKey('battle', 'user', currentFile, file); + } + return { + success: true, + kind: 'preset', + file, + path: target, + source: 'user', + teamFiles: [], + }; + } + + /** 重命名一份用户出征计划。 */ + renameUser( + file: string, + newName: string, + ): Record { + const source = this.combatRepository.safeUserPath(file); + const safeName = this.combatCodec.safeBaseName(newName); + if ( + !source + || !this.combatRepository.exists(source) + || !safeName + ) { + return { success: false, error: '出征计划名称或文件不合法' }; + } + const targetFile = `bettle-${safeName}.yaml`; + const target = this.combatRepository.safeUserPath(targetFile); + if (!target) { + return { success: false, error: '出征计划名称不合法' }; + } + if ( + source.toLowerCase() !== target.toLowerCase() + && this.combatRepository.exists(target) + ) { + return { success: false, error: '同名出征计划已存在' }; + } + try { + this.combatRepository.rename(source, target); + this.moveIgnoredKey('battle', 'user', file, targetFile); + return { success: true, file: targetFile }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 删除一份用户出征计划。 */ + deleteUserCombat(file: string): Record { + const target = this.combatRepository.safeUserPath(file); + if (!target || !this.combatRepository.exists(target)) { + return { success: false, error: '用户出征计划不存在' }; + } + try { + this.combatRepository.remove(target); + this.removeIgnoredKey('battle', 'user', file); + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 删除一份用户独立编队计划。 */ + deleteUserTeam(file: string): Record { + const target = this.safeManagedTeamPath('user', file); + if (!target || !this.teamRepository.exists(target)) { + return { success: false, error: '用户舰队方案不存在' }; + } + try { + this.teamRepository.remove(target); + this.removeIgnoredKey('team', 'user', file); + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 读取并兼容旧格式的未关联忽略项。 */ + private getIgnoredUnlinkedPlans(): string[] { + const raw = this.settings.read().plan_management_ignored_unlinked; + if (!Array.isArray(raw)) return []; + return raw.flatMap((value) => { + if (typeof value !== 'string') return []; + if ( + /^(battle|team)\/(system|user)\/[^/\\]+\.ya?ml$/i.test(value) + ) { + return [value]; + } + const legacy = /^(system|user)\/([^/\\]+\.ya?ml)$/i.exec(value); + return legacy ? [`battle/${legacy[1]}/${legacy[2]}`] : []; + }); + } + + /** 去重、排序并保存未关联忽略项。 */ + private writeIgnoredUnlinkedPlans(values: Iterable): string[] { + const normalized = [...new Set(values)].sort((left, right) => ( + left.localeCompare(right, 'zh-CN') + )); + this.settings.write({ + plan_management_ignored_unlinked: normalized, + }); + return normalized; + } + + /** 为有效的受管计划构造稳定忽略键。 */ + private ignoredUnlinkedPlanKey( + kind: 'battle' | 'team', + source: PlanPresetSource, + file: string, + ): string | null { + const valid = kind === 'battle' + ? this.combatRepository.safeManagedPath(source, file) + : this.safeManagedTeamPath(source, file); + return valid ? `${kind}/${source}/${file}` : null; + } + + /** 返回经过文件名边界校验的受管编队路径。 */ + private safeManagedTeamPath( + source: PlanPresetSource, + file: string, + ): string | null { + if ( + (source !== 'system' && source !== 'user') + || path.basename(file) !== file + || !TEAM_FILE_PATTERN.test(file) + ) { + return null; + } + return path.join(this.teamRepository.directory(source), file); + } + + /** 在计划重命名后同步迁移对应忽略键。 */ + private moveIgnoredKey( + kind: 'battle' | 'team', + source: PlanPresetSource, + oldFile: string, + newFile: string, + ): void { + const oldKey = this.ignoredUnlinkedPlanKey(kind, source, oldFile); + const newKey = this.ignoredUnlinkedPlanKey(kind, source, newFile); + const ignored = new Set(this.getIgnoredUnlinkedPlans()); + if (oldKey && newKey && ignored.delete(oldKey)) { + ignored.add(newKey); + this.writeIgnoredUnlinkedPlans(ignored); + } + } + + /** 删除计划后同步清理对应忽略键。 */ + private removeIgnoredKey( + kind: 'battle' | 'team', + source: PlanPresetSource, + file: string, + ): void { + const key = this.ignoredUnlinkedPlanKey(kind, source, file); + if (!key) return; + const ignored = new Set(this.getIgnoredUnlinkedPlans()); + if (ignored.delete(key)) { + this.writeIgnoredUnlinkedPlans(ignored); + } + } + + private isDailyTaskType(value: unknown): boolean { + return ( + value === 'exercise' + || value === 'campaign' + || value === 'decisive' + ); + } +} diff --git a/electron/services/PythonEnvironmentService.ts b/electron/services/PythonEnvironmentService.ts new file mode 100644 index 0000000..57f25d8 --- /dev/null +++ b/electron/services/PythonEnvironmentService.ts @@ -0,0 +1,118 @@ +/** + * 校验 Python 并编排环境检查和依赖安装。 + */ +import { exec } from 'child_process'; +import * as fs from 'fs'; +import { promisify } from 'util'; +import type { EnvCheckResult } from '../pythonEnv'; + +const execAsync = promisify(exec); + +export interface PythonValidationResult { + valid: boolean; + version: string | null; + error?: string; +} + +export interface PythonEnvironmentDependencies { + fileExists(filePath: string): boolean; + readVersion(pythonPath: string): Promise; + isAllowedVersion(version: string): boolean; + findPython(): Promise; + checkEnvironment(): Promise; + installDependencies( + pythonPath: string, + ): Promise<{ success: boolean; output: string }>; + installPortablePython(): Promise<{ success: boolean }>; +} + +/** 统一 Python 校验、环境检查和安装入口。 */ +export class PythonEnvironmentService { + constructor( + private readonly dependencies: PythonEnvironmentDependencies, + ) {} + + /** 构造保持原文件检查、命令格式和超时的系统依赖。 */ + static createDependencies( + dependencies: Omit< + PythonEnvironmentDependencies, + 'fileExists' | 'readVersion' + >, + ): PythonEnvironmentDependencies { + return { + ...dependencies, + fileExists: filePath => fs.existsSync(filePath), + readVersion: async pythonPath => { + const { stdout } = await execAsync( + `"${pythonPath}" --version`, + { + windowsHide: true, + timeout: 10000, + }, + ); + return stdout.trim(); + }, + }; + } + + /** 校验指定解释器是否存在且版本兼容。 */ + async validate( + pythonPath: string, + ): Promise { + if (!pythonPath) { + return { + valid: false, + version: null, + error: '路径为空', + }; + } + if (!this.dependencies.fileExists(pythonPath)) { + return { + valid: false, + version: null, + error: '文件不存在', + }; + } + try { + const version = await this.dependencies.readVersion(pythonPath); + if (!this.dependencies.isAllowedVersion(version)) { + return { + valid: false, + version, + error: `版本不兼容: ${version}(需要 3.12 或 3.13)`, + }; + } + return { valid: true, version }; + } catch (error) { + return { + valid: false, + version: null, + error: `执行失败: ${ + error instanceof Error ? error.message : String(error) + }`, + }; + } + } + + /** 执行现有 Python 环境检查流程。 */ + check(): Promise { + return this.dependencies.checkEnvironment(); + } + + /** 查找 Python 后安装后端依赖。 */ + async installDependencies(): Promise<{ + success: boolean; + output: string; + }> { + const pythonPath = await this.dependencies.findPython(); + if (!pythonPath) { + return { success: false, output: '找不到 Python' }; + } + return this.dependencies.installDependencies(pythonPath); + } + + /** 安装或初始化现有便携 Python。 */ + installPortablePython(): Promise<{ success: boolean }> { + return this.dependencies.installPortablePython(); + } +} diff --git a/electron/services/RuntimePlanService.ts b/electron/services/RuntimePlanService.ts new file mode 100644 index 0000000..62371a7 --- /dev/null +++ b/electron/services/RuntimePlanService.ts @@ -0,0 +1,96 @@ +/** + * 生成供后端执行的已展开临时计划。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { AtomicFileStore } from './AtomicFileStore'; +import { CombatPlanCodec } from './CombatPlanCodec'; +import { CombatPlanRepository } from './CombatPlanRepository'; +import { type PlanPresetSource } from './TeamPlanCodec'; + +export interface PreparedCombatPlan { + sourcePath: string; + runtimePath: string; + content: string; +} + +export interface RuntimePlanDependencies { + getTempDirectory(): string; + processId: number; + now?(): number; +} + +/** 负责生成后端执行使用的已展开临时计划。 */ +export class RuntimePlanService { + private sequence = 0; + + constructor( + private readonly codec: CombatPlanCodec, + private readonly repository: CombatPlanRepository, + private readonly atomicFiles: AtomicFileStore, + private readonly dependencies: RuntimePlanDependencies, + ) {} + + /** 返回当前进程专用的运行时计划目录。 */ + directory(): string { + return path.join( + this.dependencies.getTempDirectory(), + 'AutoWSGR-GUI', + 'runtime_battle_plans', + String(this.dependencies.processId), + ); + } + + /** 校验并写入一份已经展开的运行时计划。 */ + write(content: string, hint: string): string { + const parsed = this.codec.parseRoot( + content, + '运行时出征计划必须包含 chapter 和 map', + ); + if (!('chapter' in parsed) || !('map' in parsed)) { + throw new Error('运行时出征计划必须包含 chapter 和 map'); + } + if ( + Array.isArray(parsed.fleet_presets) + && parsed.fleet_presets.some(preset => ( + !this.codec.isPlainObject(preset) + || !Array.isArray(preset.ships) + )) + ) { + throw new Error('运行时出征计划包含尚未展开的舰队引用'); + } + + const directory = this.directory(); + fs.mkdirSync(directory, { recursive: true }); + this.sequence++; + const safeHint = this.codec.safeBaseName(hint) || 'plan'; + const timestamp = this.dependencies.now?.() ?? Date.now(); + const file = `${safeHint}-${timestamp}-${this.sequence}.yaml`; + const target = path.join(directory, file); + this.atomicFiles.write(target, content); + return target; + } + + /** 读取受管计划、展开舰队引用并生成运行时文件。 */ + prepare( + source: PlanPresetSource, + file: string, + ): PreparedCombatPlan { + const sourcePath = this.repository.safeManagedPath(source, file); + if (!sourcePath || !this.repository.exists(sourcePath)) { + throw new Error('出征计划不存在'); + } + const originalContent = this.repository.read(sourcePath); + const parsed = this.codec.parseRoot( + originalContent, + '无效的出征计划', + ); + const expanded = this.codec.expandRoot(parsed, source); + const content = this.codec.serialize(expanded, originalContent); + return { + sourcePath, + runtimePath: this.write(content, file), + content, + }; + } +} diff --git a/electron/services/SafePathService.ts b/electron/services/SafePathService.ts new file mode 100644 index 0000000..4ccf020 --- /dev/null +++ b/electron/services/SafePathService.ts @@ -0,0 +1,141 @@ +/** + * 解析应用路径并阻止越界、穿越和链接逃逸。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { AppPaths } from './AppPaths'; + +type FileCapability = 'read' | 'write'; + +/** 统一执行主进程文件能力的目录边界检查。 */ +export class SafePathService { + constructor(private readonly appPaths: AppPaths) {} + + /** 为兼容现有调用方解析一个可读取的应用路径。 */ + resolveAppPath(filePath: string): string { + return this.resolve(filePath, 'read'); + } + + /** 解析只允许读取的 userData 或打包资源路径。 */ + resolveReadablePath(filePath: string): string { + return this.resolve(filePath, 'read'); + } + + /** 解析只允许写入的 userData 路径。 */ + resolveWritablePath(filePath: string): string { + return this.resolve(filePath, 'write'); + } + + private resolve(filePath: string, capability: FileCapability): string { + const raw = typeof filePath === 'string' ? filePath.trim() : ''; + if (!raw) throw new Error('文件路径不能为空'); + if (raw.includes('\0')) throw new Error('文件路径包含非法字符'); + if (/^[\\/]{2}/.test(raw)) throw new Error('不允许使用 UNC 路径'); + + const hasDrivePrefix = /^[a-zA-Z]:/.test(raw); + const nativeAbsolute = path.isAbsolute(raw); + const portableAbsolute = path.win32.isAbsolute(raw) + || path.posix.isAbsolute(raw); + if (hasDrivePrefix && !nativeAbsolute) { + throw new Error('不允许使用盘符相对路径'); + } + if (portableAbsolute && !nativeAbsolute) { + throw new Error('不允许切换路径根目录'); + } + const pathWithoutDrive = hasDrivePrefix ? raw.slice(2) : raw; + if (pathWithoutDrive.includes(':')) { + throw new Error('文件路径包含非法字符'); + } + + const segments = raw.split(/[\\/]+/); + if (segments.includes('..')) { + throw new Error('文件路径不允许包含 ..'); + } + + const resourceDirectory = path.join( + this.appPaths.resourceRoot(), + 'resource', + ); + const isResourceRelative = !nativeAbsolute + && segments[0]?.toLowerCase() === 'resource'; + if (capability === 'write' && isResourceRelative) { + throw new Error('安装资源目录为只读'); + } + + const relativeSegments = isResourceRelative + ? segments.slice(1) + : segments; + const relativeRoot = isResourceRelative + ? resourceDirectory + : this.appPaths.userDataRoot(); + const candidate = nativeAbsolute + ? path.resolve(raw) + : path.resolve(relativeRoot, ...relativeSegments); + const roots = capability === 'read' + ? [this.appPaths.userDataRoot(), resourceDirectory] + : [this.appPaths.userDataRoot()]; + const allowed = roots.some(root => this.isContained(candidate, root)); + if (!allowed) throw new Error('文件路径超出应用允许目录'); + return candidate; + } + + /** 展开现有链接后验证目标仍位于允许根目录。 */ + private isContained(candidate: string, root: string): boolean { + const canonicalCandidate = this.canonicalizePotentialPath(candidate); + const canonicalRoot = this.canonicalizePotentialPath(root); + const normalizedCandidate = this.normalizeForComparison( + canonicalCandidate, + ); + const normalizedRoot = this.normalizeForComparison(canonicalRoot); + const relative = path.relative(normalizedRoot, normalizedCandidate); + return relative === '' + || ( + relative !== '..' + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); + } + + /** 展开最近存在的祖先,阻止新路径通过目录链接逃逸。 */ + private canonicalizePotentialPath(value: string): string { + let current = path.resolve(value); + const missingSegments: string[] = []; + while (!this.pathEntryExists(current)) { + const parent = path.dirname(current); + if (parent === current) { + return path.resolve(current, ...missingSegments); + } + missingSegments.unshift(path.basename(current)); + current = parent; + } + let canonicalExisting: string; + try { + canonicalExisting = fs.realpathSync.native(current); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === 'ENOENT' || code === 'ENOTDIR') { + throw new Error('文件路径包含无法解析的符号链接'); + } + throw error; + } + return path.resolve(canonicalExisting, ...missingSegments); + } + + /** lstat 不跟随链接,因此悬空链接也会被视为现有路径节点。 */ + private pathEntryExists(value: string): boolean { + try { + fs.lstatSync(value); + return true; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === 'ENOENT' || code === 'ENOTDIR') return false; + throw error; + } + } + + /** Windows 文件系统路径按不区分大小写规则比较。 */ + private normalizeForComparison(value: string): string { + const resolved = path.resolve(value); + return process.platform === 'win32' ? resolved.toLowerCase() : resolved; + } +} diff --git a/electron/services/SecureFileService.ts b/electron/services/SecureFileService.ts new file mode 100644 index 0000000..72dae2c --- /dev/null +++ b/electron/services/SecureFileService.ts @@ -0,0 +1,50 @@ +/** + * 在路径能力边界内读写主进程文本文件。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { SafePathService } from './SafePathService'; + +/** 集中管理主进程对普通文本文件的现有访问规则。 */ +export class SecureFileService { + constructor(private readonly safePaths: SafePathService) {} + + /** 在允许目录内覆盖保存 UTF-8 文本。 */ + save(filePath: string, content: string): void { + let resolved = this.safePaths.resolveWritablePath(filePath); + const directory = path.dirname(resolved); + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + resolved = this.safePaths.resolveWritablePath(resolved); + fs.writeFileSync(resolved, content, 'utf-8'); + } + + /** 在允许目录内读取 UTF-8 文本,不存在时返回空字符串。 */ + read(filePath: string): string { + const resolved = this.safePaths.resolveReadablePath(filePath); + if (!fs.existsSync(resolved)) return ''; + return fs.readFileSync(resolved, 'utf-8'); + } + + /** 在允许目录内追加 UTF-8 文本。 */ + append(filePath: string, content: string): void { + let resolved = this.safePaths.resolveWritablePath(filePath); + const directory = path.dirname(resolved); + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + resolved = this.safePaths.resolveWritablePath(resolved); + fs.appendFileSync(resolved, content, 'utf-8'); + } + + /** 读取用户通过系统文件对话框显式选择的文件。 */ + readSelectedFile(filePath: string): string { + return fs.readFileSync(filePath, 'utf-8'); + } + + /** 写入用户通过系统保存对话框显式选择的文件。 */ + writeSelectedFile(filePath: string, content: string): void { + fs.writeFileSync(filePath, content, 'utf-8'); + } +} diff --git a/electron/services/ShipLibraryService.ts b/electron/services/ShipLibraryService.ts new file mode 100644 index 0000000..4005560 --- /dev/null +++ b/electron/services/ShipLibraryService.ts @@ -0,0 +1,378 @@ +/** + * 管理舰船资料库同步、状态和渲染清单。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { pathToFileURL } from 'url'; +import { AppPaths } from './AppPaths'; + +const NATIVE_SHIP_TYPE_SCHEMA_VERSION = 3; +const BACKEND_CANONICAL_SHIP_TYPE_SCHEMA_VERSION = 4; +const LEGACY_SHIP_LIBRARY_TYPE_CODES: Readonly> = + Object.freeze({ + cbg: 'bg', + cg: 'kp', + cgaa: 'cg', + ddg: 'asdg', + ddgaa: 'aadg', + }); + +export interface ShipLibraryStatus { + exists: boolean; + path: string; + generatedAt?: string; + shipCount: number; + assetCount: number; + missingAssets: number; + error?: string; +} + +export interface ShipLibraryManifest { + schemaVersion: number; + generatedAt: string; + labels: Record; + typeGroups: Record; + ships: Array>; +} + +export interface ShipLibraryDependencies { + processId: number; + now?(): number; +} + +/** 管理舰船资料库目录、内置升级、状态和渲染清单。 */ +export class ShipLibraryService { + constructor( + private readonly appPaths: AppPaths, + private readonly dependencies: ShipLibraryDependencies, + ) {} + + /** 返回当前可写的用户舰船资料库目录。 */ + directory(): string { + return path.join(this.appPaths.userDataRoot(), 'ship-library'); + } + + /** 返回开发或打包模式下的资料库更新脚本。 */ + updaterPath(): string { + const root = this.appPaths.isPackaged() + ? this.appPaths.resourceRoot() + : this.appPaths.appRoot(); + return path.join( + root, + 'tools', + 'ship_library', + 'update_ship_library.py', + ); + } + + /** 按清单版本把内置资料库安全同步到用户目录。 */ + initialize(): void { + const bundledDir = path.join( + this.appPaths.resourceRoot(), + 'resource', + 'ship-library', + ); + const bundledManifestPath = path.join( + bundledDir, + 'manifest.json', + ); + const userManifestPath = path.join( + this.directory(), + 'manifest.json', + ); + if (!fs.existsSync(bundledManifestPath)) return; + + let shouldSync = !fs.existsSync(userManifestPath); + if (!shouldSync) { + try { + const bundled = JSON.parse( + fs.readFileSync(bundledManifestPath, 'utf-8'), + ) as Record; + const user = JSON.parse( + fs.readFileSync(userManifestPath, 'utf-8'), + ) as Record; + shouldSync = Number( + bundled.schema_version ?? bundled.schemaVersion ?? 0, + ) > Number( + user.schema_version ?? user.schemaVersion ?? 0, + ) + || String( + bundled.generated_at ?? bundled.generatedAt ?? '', + ) > String( + user.generated_at ?? user.generatedAt ?? '', + ); + } catch { + shouldSync = true; + } + } + if (!shouldSync) return; + + const temporary = `${this.directory()}.${this.dependencies.processId}.${this.now()}.sync`; + const backup = `${this.directory()}.${this.dependencies.processId}.${this.now()}.backup`; + let movedExisting = false; + try { + fs.rmSync(temporary, { recursive: true, force: true }); + this.copyDirectoryNoOverwrite(bundledDir, temporary); + if (fs.existsSync(this.directory())) { + fs.renameSync(this.directory(), backup); + movedExisting = true; + } + fs.renameSync(temporary, this.directory()); + if (movedExisting) { + fs.rmSync(backup, { recursive: true, force: true }); + } + } catch (error) { + fs.rmSync(temporary, { recursive: true, force: true }); + if ( + movedExisting + && !fs.existsSync(this.directory()) + && fs.existsSync(backup) + ) { + try { + fs.renameSync(backup, this.directory()); + } catch { + console.error( + '[ShipLibrary] 资料库旧版本恢复失败:', + backup, + ); + } + } + console.error('[ShipLibrary] 资料库升级失败:', error); + } + } + + /** 读取清单,为配置页提供当前资料库状态。 */ + getStatus(): ShipLibraryStatus { + const directory = this.directory(); + const manifestPath = path.join(directory, 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + return { + exists: false, + path: directory, + shipCount: 0, + assetCount: 0, + missingAssets: 0, + }; + } + try { + const manifest = JSON.parse( + fs.readFileSync(manifestPath, 'utf-8'), + ) as { + generated_at?: unknown; + counts?: Record; + }; + const counts = manifest.counts ?? {}; + return { + exists: true, + path: directory, + generatedAt: typeof manifest.generated_at === 'string' + ? manifest.generated_at + : undefined, + shipCount: typeof counts.ships === 'number' + ? counts.ships + : 0, + assetCount: typeof counts.assets === 'number' + ? counts.assets + : 0, + missingAssets: typeof counts.missing_assets === 'number' + ? counts.missing_assets + : 0, + }; + } catch (error) { + return { + exists: false, + path: directory, + shipCount: 0, + assetCount: 0, + missingAssets: 0, + error: `资料库清单读取失败: ${ + error instanceof Error ? error.message : String(error) + }`, + }; + } + } + + /** 返回舰队规划使用的清单字段和受限本地资源 URL。 */ + getManifest(): ShipLibraryManifest { + const manifestPath = path.join(this.directory(), 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + throw new Error( + '舰船资料库尚未建立,请先在配置页更新舰船数据库', + ); + } + const raw = JSON.parse( + fs.readFileSync(manifestPath, 'utf-8'), + ) as { + schema_version?: unknown; + generated_at?: unknown; + labels?: unknown; + type_groups?: unknown; + ships?: unknown; + }; + if (!Array.isArray(raw.ships)) { + throw new Error('舰船资料库清单格式无效'); + } + const schemaVersion = typeof raw.schema_version === 'number' + ? raw.schema_version + : 0; + return { + schemaVersion, + generatedAt: typeof raw.generated_at === 'string' + ? raw.generated_at + : '', + labels: this.normalizeLegacyLabels(raw.labels, schemaVersion), + typeGroups: this.normalizeLegacyTypeGroups( + raw.type_groups, + schemaVersion, + ), + ships: raw.ships.map((entry) => { + const ship = entry && typeof entry === 'object' + ? entry as Record + : {}; + const normalizedShip = this.normalizeLegacyShip( + ship, + schemaVersion, + ); + return { + ...normalizedShip, + portraitUrl: this.assetUrl(normalizedShip.portrait), + backgroundUrl: this.assetUrl(normalizedShip.background), + frameUrl: this.assetUrl(normalizedShip.frame), + typeIconUrl: this.assetUrl(normalizedShip.type_icon), + }; + }), + }; + } + + /** + * schema 2 及更早版本使用 Wiki 旧舰种代码,schema 3 仍保留 CF。 + * 只在资料库读取边界转换,不把源代码加入编队 API 的允许集合。 + */ + private normalizeLegacyShipTypeCode( + value: string, + schemaVersion: number, + ): string { + let code = value.trim().toLowerCase(); + if (schemaVersion < NATIVE_SHIP_TYPE_SCHEMA_VERSION) { + code = LEGACY_SHIP_LIBRARY_TYPE_CODES[code] ?? code; + } + if ( + schemaVersion < BACKEND_CANONICAL_SHIP_TYPE_SCHEMA_VERSION + && code === 'cf' + ) { + return 'cav'; + } + return code; + } + + private normalizeLegacyShip( + ship: Record, + schemaVersion: number, + ): Record { + if (typeof ship.ship_type !== 'string') return ship; + return { + ...ship, + ship_type: this.normalizeLegacyShipTypeCode( + ship.ship_type, + schemaVersion, + ), + }; + } + + private normalizeLegacyLabels( + value: unknown, + schemaVersion: number, + ): Record { + const labels = value && typeof value === 'object' + ? { ...value as Record } + : {}; + if ( + schemaVersion >= BACKEND_CANONICAL_SHIP_TYPE_SCHEMA_VERSION + || !labels.ship_types + || typeof labels.ship_types !== 'object' + || Array.isArray(labels.ship_types) + ) { + return labels; + } + const normalizedLabels: Record = {}; + for ( + const [sourceCode, label] + of Object.entries(labels.ship_types as Record) + ) { + const code = this.normalizeLegacyShipTypeCode( + sourceCode, + schemaVersion, + ); + const sourceIsCanonical = sourceCode.trim().toLowerCase() === code; + if (!(code in normalizedLabels) || sourceIsCanonical) { + normalizedLabels[code] = label; + } + } + labels.ship_types = normalizedLabels; + return labels; + } + + private normalizeLegacyTypeGroups( + value: unknown, + schemaVersion: number, + ): Record { + const groups = value && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : {}; + if (schemaVersion >= BACKEND_CANONICAL_SHIP_TYPE_SCHEMA_VERSION) { + return groups; + } + const normalize = (entry: unknown): unknown => { + if (Array.isArray(entry)) { + return [...new Set(entry.map(item => ( + typeof item === 'string' + ? this.normalizeLegacyShipTypeCode(item, schemaVersion) + : item + )))]; + } + if (entry && typeof entry === 'object') { + return Object.fromEntries( + Object.entries(entry as Record) + .map(([key, item]) => [key, normalize(item)]), + ); + } + return entry; + }; + return normalize(groups) as Record; + } + + /** 将资料库内部相对路径转换为本地 file URL。 */ + assetUrl(relativePath: unknown): string { + if (typeof relativePath !== 'string' || !relativePath) return ''; + const root = path.resolve(this.directory()); + const absolutePath = path.resolve(root, relativePath); + if ( + absolutePath !== root + && !absolutePath.startsWith(`${root}${path.sep}`) + ) { + return ''; + } + return pathToFileURL(absolutePath).href; + } + + /** 递归复制目录,并继续跳过已存在的文件。 */ + private copyDirectoryNoOverwrite(source: string, target: string): void { + if (!fs.existsSync(target)) { + fs.mkdirSync(target, { recursive: true }); + } + for (const entry of fs.readdirSync(source, { withFileTypes: true })) { + const sourcePath = path.join(source, entry.name); + const targetPath = path.join(target, entry.name); + if (entry.isDirectory()) { + this.copyDirectoryNoOverwrite(sourcePath, targetPath); + } else if (!fs.existsSync(targetPath)) { + fs.copyFileSync(sourcePath, targetPath); + } + } + } + + /** 返回可注入的时间戳或当前系统时间。 */ + private now(): number { + return this.dependencies.now?.() ?? Date.now(); + } +} diff --git a/electron/services/ShipLibraryUpdater.ts b/electron/services/ShipLibraryUpdater.ts new file mode 100644 index 0000000..358201e --- /dev/null +++ b/electron/services/ShipLibraryUpdater.ts @@ -0,0 +1,167 @@ +/** + * 互斥运行舰船资料库更新并解析进度。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { spawn } from 'child_process'; +import { ShipLibraryService } from './ShipLibraryService'; + +export interface ShipLibraryUpdateResult { + success: boolean; + output?: string; + generated_at?: string; + ship_count?: number; + asset_count?: number; + added?: number; + updated?: number; + removed?: number; + downloaded?: number; + failed?: number; + failures?: string[]; + error?: string; +} + +export interface ShipLibraryUpdaterDependencies { + findPython(): Promise; + appRoot(): string; + sendProgress(message: string): void; + spawnProcess?: typeof spawn; +} + +/** 启动更新脚本、解析进度并保护单实例更新状态。 */ +export class ShipLibraryUpdater { + private running = false; + + constructor( + private readonly library: ShipLibraryService, + private readonly dependencies: ShipLibraryUpdaterDependencies, + ) {} + + /** 执行一次互斥的舰船资料库更新。 */ + async update(): Promise { + if (this.running) { + return { + success: false, + error: '舰船资料库正在更新,请稍候', + }; + } + this.running = true; + try { + return await this.run(); + } finally { + this.running = false; + } + } + + /** 启动 Python 更新脚本并解析机器可读输出。 */ + private async run(): Promise { + const pythonCmd = await this.dependencies.findPython(); + if (!pythonCmd) { + return { + success: false, + error: '找不到可用的 Python 3.12 或 3.13', + }; + } + const updaterPath = this.library.updaterPath(); + if (!fs.existsSync(updaterPath)) { + return { + success: false, + error: `找不到舰船资料库更新程序: ${updaterPath}`, + }; + } + + return new Promise((resolve) => { + const spawnProcess = this.dependencies.spawnProcess ?? spawn; + // 便携版 Python 的 ._pth 隔离模式不会自动加入脚本目录。 + const bootstrap = [ + 'import runpy, sys', + `sys.path.insert(0, ${JSON.stringify(path.dirname(updaterPath))})`, + 'script = sys.argv[1]', + 'sys.argv = sys.argv[1:]', + 'runpy.run_path(script, run_name="__main__")', + ].join('; '); + const child = spawnProcess( + pythonCmd, + [ + '-c', + bootstrap, + updaterPath, + '--output', + this.library.directory(), + '--workers', + '8', + '--force-assets', + ], + { + cwd: this.dependencies.appRoot(), + windowsHide: true, + }, + ); + let stdoutBuffer = ''; + let stderr = ''; + let result: ShipLibraryUpdateResult | null = null; + + const handleLine = (rawLine: string): void => { + const line = rawLine.trim(); + if (!line) return; + if (line.startsWith('PROGRESS sources')) { + this.dependencies.sendProgress('正在获取舰R百科数据…'); + } else { + const records = line.match( + /^PROGRESS records parsed=(\d+)$/, + ); + const assets = line.match( + /^PROGRESS assets (\d+)\/(\d+) downloaded=(\d+) failed=(\d+)$/, + ); + if (records) { + this.dependencies.sendProgress( + `已读取 ${records[1]} 艘舰船,正在检查本地资源…`, + ); + } else if (assets) { + this.dependencies.sendProgress( + `正在检查资源 ${assets[1]}/${assets[2]},已下载 ${assets[3]},失败 ${assets[4]}`, + ); + } + } + if (line.startsWith('RESULT_JSON=')) { + try { + result = JSON.parse( + line.slice('RESULT_JSON='.length), + ) as ShipLibraryUpdateResult; + } catch { + result = { + success: false, + error: '更新程序返回了无效结果', + }; + } + } + }; + + child.stdout.setEncoding('utf-8'); + child.stdout.on('data', (chunk: string) => { + stdoutBuffer += chunk; + const lines = stdoutBuffer.split(/\r?\n/); + stdoutBuffer = lines.pop() ?? ''; + lines.forEach(handleLine); + }); + child.stderr.setEncoding('utf-8'); + child.stderr.on('data', (chunk: string) => { + stderr += chunk; + }); + child.once('error', (error) => { + resolve({ + success: false, + error: `更新程序启动失败: ${error.message}`, + }); + }); + child.once('close', (code) => { + if (stdoutBuffer) handleLine(stdoutBuffer); + resolve(result ?? { + success: false, + error: stderr.trim() + || `更新程序异常退出(代码 ${code ?? 'unknown'})`, + }); + }); + }); + } +} diff --git a/electron/services/TaskPresetCodec.ts b/electron/services/TaskPresetCodec.ts new file mode 100644 index 0000000..672a194 --- /dev/null +++ b/electron/services/TaskPresetCodec.ts @@ -0,0 +1,150 @@ +/** + * 识别、校验并归一化独立任务预设 YAML。 + */ + +export type TaskPresetType = + | 'normal_fight' + | 'event_fight' + | 'campaign' + | 'exercise' + | 'decisive'; + +export type TaskPresetDocument = Record & { + task_type: TaskPresetType; +}; + +const TASK_PRESET_TYPES = new Set([ + 'normal_fight', + 'event_fight', + 'campaign', + 'exercise', + 'decisive', +]); + +/** 负责独立任务预设的类型判断和字段校验。 */ +export class TaskPresetCodec { + /** 含 task_type 但不含完整地图坐标的 YAML 是独立任务预设。 */ + isStandalone(root: Record): boolean { + return ( + typeof root.task_type === 'string' + && (!('chapter' in root) || !('map' in root)) + ); + } + + /** 校验已识别的独立任务预设,并保留未知业务字段。 */ + normalize(root: Record): TaskPresetDocument { + const rawType = typeof root.task_type === 'string' + ? root.task_type.trim().toLowerCase() + : ''; + if (!TASK_PRESET_TYPES.has(rawType as TaskPresetType)) { + throw new Error(`不支持的任务预设类型:${rawType || '空'}`); + } + const taskType = rawType as TaskPresetType; + const result: TaskPresetDocument = { + ...structuredClone(root), + task_type: taskType, + }; + + this.normalizeCommonFields(result); + if (taskType === 'normal_fight' || taskType === 'event_fight') { + result.plan_id = this.safePlanReference(root.plan_id); + } else if (taskType === 'campaign') { + result.campaign_name = this.nonEmptyText( + root.campaign_name, + 'campaign_name', + ); + } else if (taskType === 'exercise') { + result.fleet_id = this.integerInRange( + root.fleet_id, + 'fleet_id', + 1, + 4, + ); + } else { + result.chapter = this.positiveInteger(root.chapter, 'chapter'); + result.level1 = this.shipNames(root.level1, 'level1'); + result.level2 = this.shipNames(root.level2, 'level2'); + if (root.flagship_priority !== undefined) { + result.flagship_priority = this.shipNames( + root.flagship_priority, + 'flagship_priority', + ); + } + if ( + root.use_quick_repair !== undefined + && typeof root.use_quick_repair !== 'boolean' + ) { + throw new Error('use_quick_repair 必须是布尔值'); + } + } + return result; + } + + private normalizeCommonFields(result: TaskPresetDocument): void { + if (result.times !== undefined) { + result.times = this.positiveInteger(result.times, 'times'); + } + if (result.gap !== undefined) { + if ( + typeof result.gap !== 'number' + || !Number.isFinite(result.gap) + || result.gap < 0 + ) { + throw new Error('gap 必须是大于或等于 0 的数字'); + } + } + } + + /** 独立任务预设不能保留任意绝对路径或目录跳转。 */ + private safePlanReference(value: unknown): string { + const reference = this.nonEmptyText(value, 'plan_id'); + if ( + /^[A-Za-z]:[/\\]/.test(reference) + || reference.startsWith('/') + || reference.startsWith('\\\\') + || reference.split(/[\\/]/).includes('..') + ) { + throw new Error('plan_id 不能引用受管目录外的路径'); + } + return reference; + } + + private nonEmptyText(value: unknown, field: string): string { + if (typeof value !== 'string' || !value.trim()) { + throw new Error(`${field} 必须是非空字符串`); + } + return value.trim(); + } + + private positiveInteger(value: unknown, field: string): number { + if (!Number.isInteger(value) || Number(value) < 1) { + throw new Error(`${field} 必须是大于或等于 1 的整数`); + } + return Number(value); + } + + private integerInRange( + value: unknown, + field: string, + minimum: number, + maximum: number, + ): number { + const result = this.positiveInteger(value, field); + if (result < minimum || result > maximum) { + throw new Error(`${field} 必须是 ${minimum} 到 ${maximum}`); + } + return result; + } + + private shipNames(value: unknown, field: string): string[] { + if (!Array.isArray(value)) { + throw new Error(`${field} 必须是舰名列表`); + } + return value.map((item, index) => { + if (typeof item !== 'string' || !item.trim()) { + throw new Error(`${field}[${index}] 必须是非空舰名`); + } + return item.trim(); + }); + } +} diff --git a/electron/services/TeamPlanCodec.ts b/electron/services/TeamPlanCodec.ts new file mode 100644 index 0000000..0cc7931 --- /dev/null +++ b/electron/services/TeamPlanCodec.ts @@ -0,0 +1,408 @@ +/** + * 归一化、校验并序列化独立编队计划。 + */ +import * as yaml from 'js-yaml'; + +import { normalizeFleetShipTypeCode } from '../../src/shared/fleetShipTypes'; + +export type PlanPresetSource = 'system' | 'user'; + +export interface PlanFileReadError { + file: string; + source: PlanPresetSource; + kind: 'battle' | 'team'; + message: string; +} + +export interface UserTeamShipRule { + name: string; + search_name?: string; + ship_type?: string[]; + min_level?: number; + max_level?: number; +} + +export interface UserTeamPlanSlot { + name?: string; + search_name?: string; + ship_type?: string[]; + min_level?: number; + max_level?: number; + candidates?: UserTeamShipRule[]; +} + +export interface UserTeamPlan { + file?: string; + modifiedAt?: number; + source?: PlanPresetSource; + name: string; + ships: UserTeamPlanSlot[]; +} + +/** 受管编队文件必须使用的既有命名规则。 */ +export const TEAM_FILE_PATTERN = /^team[-_][^\\/]+\.ya?ml$/i; + +/** 负责独立编队计划的归一化、序列化和文件名生成。 */ +export class TeamPlanCodec { + /** 校验并归一化一份独立编队计划。 */ + normalize(raw: unknown): UserTeamPlan { + if (!this.isPlainObject(raw)) { + throw new Error('编队 YAML 根节点必须是对象'); + } + if (typeof raw.name !== 'string' || !raw.name.trim()) { + throw new Error('name 不能为空'); + } + if ( + !Array.isArray(raw.ships) + || raw.ships.length < 1 + || raw.ships.length > 6 + ) { + throw new Error('ships 必须包含 1 到 6 个位置'); + } + if (raw.fleet_id !== undefined && ( + !Number.isInteger(raw.fleet_id) + || Number(raw.fleet_id) < 1 + || Number(raw.fleet_id) > 4 + )) { + throw new Error('旧版 fleet_id 必须是 1 到 4'); + } + const ships = raw.ships + .map(value => this.normalizeSlot(value)) + .filter((slot): slot is UserTeamPlanSlot => slot !== null); + if (ships.length === 0) { + throw new Error('ships 至少需要一个有效位置'); + } + return { ...raw, name: raw.name.trim(), ships }; + } + + /** 序列化编队计划并保留未知业务字段。 */ + serialize(plan: UserTeamPlan): string { + const output: Record = { + ...plan, + name: plan.name, + ships: plan.ships, + }; + delete output.file; + delete output.modifiedAt; + delete output.source; + const lines = [ + ...Object.entries(output) + .filter(([key]) => key !== 'ships') + .map(([key, value]) => `${key}: ${this.inlineYaml(value)}`), + 'ships:', + ]; + for (const slot of plan.ships) { + const candidateOnly = slot.name === undefined; + const knownSlotKeys = new Set([ + 'name', + 'search_name', + 'ship_type', + 'min_level', + 'max_level', + 'candidates', + 'priority', + ]); + const extraEntries = Object.entries(slot) + .filter( + ([key, value]) => ( + !knownSlotKeys.has(key) && value !== undefined + ), + ); + const anonymousEntries = Object.entries(slot) + .filter( + ([key, value]) => ( + key !== 'candidates' + && key !== 'priority' + && value !== undefined + ), + ); + if (slot.name !== undefined) { + lines.push(` - name: ${this.inlineYaml(slot.name)}`); + if (slot.search_name !== undefined) { + lines.push( + ` search_name: ${this.inlineYaml(slot.search_name)}`, + ); + } + if (slot.ship_type !== undefined) { + lines.push( + ` ship_type: ${this.inlineYaml(slot.ship_type)}`, + ); + } + if (slot.min_level !== undefined) { + lines.push(` min_level: ${slot.min_level}`); + } + if (slot.max_level !== undefined) { + lines.push(` max_level: ${slot.max_level}`); + } + } + if (candidateOnly && anonymousEntries.length > 0) { + const [first, ...rest] = anonymousEntries; + lines.push(` - ${first[0]}: ${this.inlineYaml(first[1])}`); + for (const [key, value] of rest) { + lines.push(` ${key}: ${this.inlineYaml(value)}`); + } + } else if (!candidateOnly) { + for (const [key, value] of extraEntries) { + lines.push(` ${key}: ${this.inlineYaml(value)}`); + } + } + if (slot.candidates?.length) { + lines.push( + candidateOnly && anonymousEntries.length === 0 + ? ' - candidates:' + : ' candidates:', + ); + for (const candidate of slot.candidates) { + lines.push(` - ${this.inlineYaml(candidate)}`); + } + } + } + return `${lines.join('\n')}\n`; + } + + /** 把编队名称转换为既有 team-*.yaml 文件名。 */ + fileName(name: string): string { + const safeName = name + .trim() + .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') + .replace(/[. ]+$/g, '') + .slice(0, 80); + if (!safeName) { + throw new Error('编队预设名称不能用于文件名'); + } + return `team-${safeName}.yaml`; + } + + /** 校验单个位置;位置必须至少包含一艘主选或备选。 */ + private normalizeSlot(raw: unknown): UserTeamPlanSlot | null { + if (raw === null) return null; + if (typeof raw === 'string') { + const name = raw.trim(); + if (!name) throw new Error('ships 中的舰名不能为空'); + return { name }; + } + if (!this.isPlainObject(raw)) { + throw new Error('ships 中的槽位必须是对象'); + } + + if (raw.candidates !== undefined && !Array.isArray(raw.candidates)) { + throw new Error('candidates 必须是列表'); + } + if (raw.priority !== undefined && !Array.isArray(raw.priority)) { + throw new Error('旧版 priority 必须是列表'); + } + const rawCandidates = ( + raw.candidates ?? raw.priority + ) as unknown[] | undefined; + const primaryRaw: Record | null = ( + typeof raw.name === 'string' && raw.name.trim() + ) + ? raw + : null; + const candidatesRaw = rawCandidates ?? []; + const candidateOnlyWithLegacyNames = ( + primaryRaw === null + && candidatesRaw.some(value => typeof value === 'string') + ); + + const candidates = candidatesRaw.map((candidate, index) => { + if (typeof candidate === 'string') { + const name = candidate.trim(); + if (!name) { + throw new Error(`candidates[${index}] 舰名不能为空`); + } + return this.legacyCandidateRule(name, raw); + } + return this.normalizeShipRule( + candidate, + `candidates[${index}]`, + ); + }); + + if (primaryRaw === null) { + const hasAnonymousFilter = ( + raw.ship_type !== undefined + || raw.nation !== undefined + || raw.min_level !== undefined + || raw.max_level !== undefined + ); + if ( + raw.search_name !== undefined + && !candidateOnlyWithLegacyNames + ) { + throw new Error('没有主选 name 时不能填写 search_name'); + } + if (!hasAnonymousFilter && candidates.length === 0) { + throw new Error('位置至少需要一艘主选或备选舰船'); + } + const shipTypes = this.normalizeShipTypes( + raw.ship_type, + '无固定舰名位置.ship_type', + ); + const minLevel = this.positiveInteger( + raw.min_level, + '无固定舰名位置.min_level', + ); + const maxLevel = this.positiveInteger( + raw.max_level, + '无固定舰名位置.max_level', + ); + if ( + minLevel !== undefined + && maxLevel !== undefined + && maxLevel < minLevel + ) { + throw new Error( + '无固定舰名位置.max_level 必须大于或等于 min_level', + ); + } + const { + priority: _legacyPriority, + search_name: _legacySearchName, + ...candidateOnlyFields + } = candidateOnlyWithLegacyNames ? raw : { + ...raw, + priority: undefined, + }; + return { + ...candidateOnlyFields, + ...(shipTypes === undefined ? {} : { ship_type: shipTypes }), + ...(minLevel === undefined ? {} : { min_level: minLevel }), + ...(maxLevel === undefined ? {} : { max_level: maxLevel }), + candidates, + }; + } + + const { + candidates: _ignoredCandidates, + priority: _ignoredPriority, + ...primaryFields + } = primaryRaw; + const result: UserTeamPlanSlot = { + ...raw, + ...this.normalizeShipRule(primaryFields, '主选'), + }; + if (candidates.length > 0) { + result.candidates = candidates; + } + return result; + } + + /** 校验一艘主选或备选舰船自己的规则。 */ + private normalizeShipRule( + raw: unknown, + field: string, + ): UserTeamShipRule { + if (!this.isPlainObject(raw)) { + throw new Error(`${field} 必须是对象`); + } + if (typeof raw.name !== 'string' || !raw.name.trim()) { + throw new Error(`${field}.name 必须是非空字符串`); + } + const result: UserTeamShipRule = { + ...raw, + name: raw.name.trim(), + }; + if (raw.search_name !== undefined) { + if ( + typeof raw.search_name !== 'string' + || !raw.search_name.trim() + ) { + throw new Error(`${field}.search_name 必须是非空字符串`); + } + result.search_name = raw.search_name.trim(); + } + const shipTypes = this.normalizeShipTypes( + raw.ship_type, + `${field}.ship_type`, + ); + if (shipTypes) result.ship_type = shipTypes; + const minLevel = this.positiveInteger( + raw.min_level, + `${field}.min_level`, + ); + const maxLevel = this.positiveInteger( + raw.max_level, + `${field}.max_level`, + ); + if (minLevel !== undefined) result.min_level = minLevel; + if (maxLevel !== undefined) result.max_level = maxLevel; + if ( + minLevel !== undefined + && maxLevel !== undefined + && maxLevel < minLevel + ) { + throw new Error( + `${field}.max_level 必须大于或等于 min_level`, + ); + } + return result; + } + + /** 旧字符串候选继承原槽位的舰种和等级限制。 */ + private legacyCandidateRule( + name: string, + raw: Record, + ): UserTeamShipRule { + return this.normalizeShipRule({ + name, + search_name: raw.search_name, + ship_type: raw.ship_type, + min_level: raw.min_level, + max_level: raw.max_level, + }, `旧版候选 ${name}`); + } + + private normalizeShipTypes( + raw: unknown, + field: string, + ): string[] | undefined { + if (raw === undefined || raw === null || raw === '') return undefined; + const values = typeof raw === 'string' ? [raw] : raw; + if (!Array.isArray(values) || values.length === 0) { + throw new Error(`${field} 必须是非空字符串列表`); + } + const result = values.map(value => { + if (typeof value !== 'string' || !value.trim()) { + throw new Error(`${field} 必须是非空字符串列表`); + } + const input = value.trim().toLowerCase(); + const shipType = normalizeFleetShipTypeCode(input); + if (!shipType) { + throw new Error(`${field} 不符合后端接口: ${input}`); + } + return shipType; + }); + return [...new Set(result)]; + } + + private positiveInteger( + value: unknown, + field: string, + ): number | undefined { + if (value === undefined || value === null || value === '') { + return undefined; + } + if (!Number.isInteger(value) || Number(value) < 1) { + throw new Error(`${field} 必须是大于或等于 1 的整数`); + } + return Number(value); + } + + private inlineYaml(value: unknown): string { + return yaml.dump(value, { + flowLevel: 0, + lineWidth: -1, + noRefs: true, + sortKeys: false, + }).trim(); + } + + private isPlainObject( + value: unknown, + ): value is Record { + return Boolean(value) + && typeof value === 'object' + && !Array.isArray(value); + } +} diff --git a/electron/services/TeamPlanRepository.ts b/electron/services/TeamPlanRepository.ts new file mode 100644 index 0000000..c4513e2 --- /dev/null +++ b/electron/services/TeamPlanRepository.ts @@ -0,0 +1,175 @@ +/** + * 管理系统和用户独立编队文件。 + */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; +import { AppPaths } from './AppPaths'; +import { AtomicFileStore } from './AtomicFileStore'; +import { + PlanFileReadError, + PlanPresetSource, + TEAM_FILE_PATTERN, + TeamPlanCodec, + UserTeamPlan, +} from './TeamPlanCodec'; + +export interface TeamPlanWrite { + name: string; + file: string; + path: string; + content: string; +} + +/** 负责独立编队文件的目录、读取、枚举和原子写入。 */ +export class TeamPlanRepository { + constructor( + private readonly appPaths: AppPaths, + private readonly atomicFiles: AtomicFileStore, + private readonly codec: TeamPlanCodec, + ) {} + + /** 初始化用户编队目录。 */ + initializeUserDirectory(): void { + fs.mkdirSync(this.appPaths.userTeamPlansDir(), { recursive: true }); + } + + /** 初始化系统编队目录。 */ + initializeSystemDirectory(): void { + fs.mkdirSync(this.appPaths.systemTeamPlansDir(), { recursive: true }); + } + + /** 返回指定来源的权威编队目录。 */ + directory(source: PlanPresetSource): string { + return source === 'system' + ? this.appPaths.systemTeamPlansDir() + : this.appPaths.userTeamPlansDir(); + } + + /** 读取并归一化一份编队 YAML。 */ + read(filePath: string): UserTeamPlan { + return this.codec.normalize( + yaml.load(fs.readFileSync(filePath, 'utf-8')), + ); + } + + /** 判断磁盘文件的归一化内容是否与计划一致。 */ + matches(filePath: string, plan: UserTeamPlan): boolean { + if (!fs.existsSync(filePath)) return false; + try { + return JSON.stringify(this.read(filePath)) + === JSON.stringify(plan); + } catch { + return false; + } + } + + /** 为一组计划生成待原子写入的文件列表。 */ + buildWrites( + teams: UserTeamPlan[], + directory: string, + ): TeamPlanWrite[] { + const files = new Set(); + return teams.map(team => { + const file = this.codec.fileName(team.name); + const normalizedFile = file.toLowerCase(); + if (files.has(normalizedFile)) { + throw new Error( + `舰队名称生成了重复文件名,请修改名称:${team.name}`, + ); + } + files.add(normalizedFile); + return { + name: team.name, + file, + path: path.join(directory, file), + content: this.codec.serialize(team), + }; + }); + } + + /** 原子写入已经序列化的编队内容。 */ + write(filePath: string, content: string): void { + this.atomicFiles.write(filePath, content); + } + + /** 返回路径是否已存在。 */ + exists(filePath: string): boolean { + return fs.existsSync(filePath); + } + + /** 删除已有编队文件。 */ + remove(filePath: string): void { + fs.unlinkSync(filePath); + } + + /** 列出系统和用户目录中命名、内容均合法的编队文件。 */ + list(): { + plans: UserTeamPlan[]; + errors: PlanFileReadError[]; + } { + const plans: UserTeamPlan[] = []; + const errors: PlanFileReadError[] = []; + const sources: Array<{ + directory: string; + source: PlanPresetSource; + }> = [ + { + directory: this.appPaths.systemTeamPlansDir(), + source: 'system', + }, + { + directory: this.appPaths.userTeamPlansDir(), + source: 'user', + }, + ]; + for (const { directory, source } of sources) { + for (const file of this.yamlFiles(directory)) { + if (!TEAM_FILE_PATTERN.test(file)) { + errors.push({ + file, + source, + kind: 'team', + message: '舰队文件名必须以 team- 或 team_ 开头', + }); + continue; + } + try { + const filePath = path.join(directory, file); + const plan = this.read(filePath); + plans.push({ + ...plan, + file, + modifiedAt: fs.statSync(filePath).mtimeMs, + source, + }); + } catch (error) { + const message = error instanceof Error + ? error.message + : String(error); + errors.push({ file, source, kind: 'team', message }); + } + } + } + return { plans, errors }; + } + + /** 查找同来源同名编队,未找到时保持既有跨来源回退。 */ + find( + name: string, + source: PlanPresetSource, + plans = this.list().plans, + ): UserTeamPlan | null { + return plans.find(plan => ( + plan.name === name && plan.source === source + )) ?? plans.find(plan => plan.name === name) ?? null; + } + + /** 列出目录中的 YAML 文件并保持既有排序规则。 */ + yamlFiles(directory: string): string[] { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory) + .filter(file => /\.ya?ml$/i.test(file)) + .sort((left, right) => left.localeCompare(right, 'zh-CN')); + } +} diff --git a/electron/services/TeamPlanService.ts b/electron/services/TeamPlanService.ts new file mode 100644 index 0000000..d191a48 --- /dev/null +++ b/electron/services/TeamPlanService.ts @@ -0,0 +1,121 @@ +/** + * 编排独立编队的保存、加载和列表查询。 + */ +import * as path from 'path'; +import { + PlanPresetSource, + TEAM_FILE_PATTERN, + TeamPlanCodec, +} from './TeamPlanCodec'; +import { TeamPlanRepository } from './TeamPlanRepository'; + +/** 编排编队保存、用户选择结果加载和列表查询。 */ +export class TeamPlanService { + constructor( + private readonly codec: TeamPlanCodec, + private readonly repository: TeamPlanRepository, + ) {} + + /** 保存或重命名一份受管编队计划。 */ + save( + rawPlan: unknown, + overwrite: boolean, + currentFile?: string, + rawSource?: PlanPresetSource, + ): Record { + try { + const plan = this.codec.normalize(rawPlan); + const currentSource: PlanPresetSource = rawSource === 'system' + ? 'system' + : 'user'; + const directory = this.repository.directory('user'); + const file = this.codec.fileName(plan.name); + const filePath = path.join(directory, file); + let currentPath: string | null = null; + if (currentFile !== undefined) { + if ( + typeof currentFile !== 'string' + || path.basename(currentFile) !== currentFile + || !TEAM_FILE_PATTERN.test(currentFile) + ) { + throw new Error('当前编队文件名不符合规则'); + } + currentPath = path.join( + this.repository.directory(currentSource), + currentFile, + ); + } + const updatesCurrentFile = currentSource === 'user' + && currentPath !== null + && path.resolve(currentPath).toLowerCase() + === path.resolve(filePath).toLowerCase(); + if ( + this.repository.exists(filePath) + && !updatesCurrentFile + && overwrite !== true + ) { + return { + success: false, + exists: true, + file, + error: '存在同名配置', + }; + } + const content = this.codec.serialize(plan); + this.repository.write(filePath, content); + if ( + currentPath + && currentSource === 'user' + && !updatesCurrentFile + && this.repository.exists(currentPath) + ) { + this.repository.remove(currentPath); + } + return { + success: true, + file, + plan: { ...plan, file, source: 'user' }, + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** 加载系统文件对话框返回的用户编队路径。 */ + loadSelected(filePathValue: string): Record { + const directory = this.repository.directory('user'); + const filePath = path.resolve(filePathValue); + const file = path.basename(filePath); + if ( + path.dirname(filePath).toLowerCase() + !== path.resolve(directory).toLowerCase() + || !TEAM_FILE_PATTERN.test(file) + ) { + return { + success: false, + error: '当前yaml格式不符合规则', + }; + } + try { + const plan = this.repository.read(filePath); + return { + success: true, + file, + plan: { ...plan, file, source: 'user' }, + }; + } catch { + return { + success: false, + error: '当前yaml格式不符合规则', + }; + } + } + + /** 列出所有受管编队和逐文件读取错误。 */ + list(): ReturnType { + return this.repository.list(); + } +} diff --git a/electron/services/UserDataMigrationService.ts b/electron/services/UserDataMigrationService.ts new file mode 100644 index 0000000..c64e5c5 --- /dev/null +++ b/electron/services/UserDataMigrationService.ts @@ -0,0 +1,922 @@ +/** + * 迁移旧用户配置并维护迁移状态。 + */ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'js-yaml'; +import { isDeepStrictEqual } from 'util'; +import { + DEFAULT_LOOT_PLAN_ID, + LEGACY_LOOT_PLAN_IDS, + lootPlanIdFromIndex, + lootPlanIdFromLegacyPath, + migrateLootPlanId, + parseLootPlanIndex, + type LootPlanId, +} from '../../src/shared/lootPlans'; +import { AppPaths } from './AppPaths'; +import { AtomicFileStore } from './AtomicFileStore'; +import { + emptyLegacyMigrationSummary, + type LegacyMigrationSummary, +} from './LegacyMigrationSummary'; + +/** 当前用户数据迁移状态格式。 */ +export interface UserDataMigrationState { + version: number; + completed: string[]; +} + +/** 旧任务组中的计划路径迁移后所指向的受管配置。 */ +export type LegacyPlanReferenceTarget = + | { + kind: 'plan'; + file: string; + } + | { + kind: 'daily'; + file: string; + taskType: 'exercise' | 'campaign' | 'decisive'; + }; + +/** 当前旧用户数据迁移版本。 */ +export const USER_DATA_MIGRATION_VERSION = 6; + +const OBSOLETE_SYSTEM_PLAN_FILES = new Set([ + 'bettle-E1炸鱼.yaml', + 'bettle-E5夜战.yaml', + 'bettle-H1炸鱼.yaml', + 'bettle-H5夜战.yaml', + 'bettle-捞胖次-8-5.yaml', + 'bettle-捞胖次-9-4-6SS.yaml', + 'bettle-周常-1-2-v1.yaml', + 'bettle-周常-3-3-v1.yaml', + 'bettle-周常-6-3-v1.yaml', +]); + +const OBSOLETE_SYSTEM_PLAN_ALIASES: Readonly> = { + '活动20260730-E1炸鱼.yaml': 'bettle-E1炸鱼.yaml', + '活动20260730-E5夜战.yaml': 'bettle-E5夜战.yaml', + '活动20260730-H1炸鱼.yaml': 'bettle-H1炸鱼.yaml', + '活动20260730-H5夜战.yaml': 'bettle-H5夜战.yaml', + 'E1炸鱼.yaml': 'bettle-E1炸鱼.yaml', + 'E5夜战.yaml': 'bettle-E5夜战.yaml', + 'H1炸鱼.yaml': 'bettle-H1炸鱼.yaml', + 'H5夜战.yaml': 'bettle-H5夜战.yaml', + '周常1章-1-2.yaml': 'bettle-周常-1-2-v1.yaml', + '周常3章-3-3.yaml': 'bettle-周常-3-3-v1.yaml', + '周常6章-6-3.yaml': 'bettle-周常-6-3-v1.yaml', +}; + +/** 管理迁移状态、旧配置复制和任务组路径更新。 */ +export class UserDataMigrationService { + constructor( + private readonly appPaths: AppPaths, + private readonly atomicFiles: AtomicFileStore, + ) {} + + /** 读取当前迁移状态;无效文件按未迁移处理。 */ + readState(): UserDataMigrationState { + try { + const raw = JSON.parse( + fs.readFileSync(this.statePath(), 'utf-8'), + ) as Partial; + return { + version: typeof raw.version === 'number' ? raw.version : 0, + completed: Array.isArray(raw.completed) + ? raw.completed.filter(value => typeof value === 'string') + : [], + }; + } catch { + return { version: 0, completed: [] }; + } + } + + /** 原子写入当前迁移状态。 */ + writeState(state: UserDataMigrationState): void { + this.atomicFiles.write( + this.statePath(), + JSON.stringify(state, null, 2), + ); + } + + /** 判断 EXE 目录是否包含旧版用户数据。 */ + hasLegacyInstallation(): boolean { + const legacyRoot = this.appPaths.appRoot(); + return [ + 'usersettings.yaml', + 'gui_settings.json', + 'task_groups.json', + 'plans', + 'templates', + path.join('resource', 'user_battle_plans'), + path.join('resource', 'user_team_plans'), + ].some(relativePath => fs.existsSync( + path.join(legacyRoot, relativePath), + )); + } + + /** 合并旧设置、任务组和模板,并保留旧目录中的源文件。 */ + migrateLegacyUserDataFiles(): LegacyMigrationSummary { + const detected = this.hasLegacyInstallation(); + const summary = emptyLegacyMigrationSummary(detected); + if (!detected) return summary; + + const legacyRoot = this.appPaths.appRoot(); + const targetRoot = this.appPaths.userDataRoot(); + for ( + const [file, format] of [ + ['usersettings.yaml', 'yaml'], + ['gui_settings.json', 'json'], + ] as const + ) { + this.recordResult( + summary, + path.join(legacyRoot, file), + () => this.migrateStructuredFile( + path.join(legacyRoot, file), + path.join(targetRoot, file), + format, + ), + ); + } + this.recordResult( + summary, + path.join(targetRoot, 'gui_settings.json'), + () => this.reconcilePreviouslyMigratedLootPlanSelection(), + ); + this.recordResult( + summary, + path.join(legacyRoot, 'task_groups.json'), + () => this.migrateLegacyTaskGroups( + path.join(legacyRoot, 'task_groups.json'), + path.join(targetRoot, 'task_groups.json'), + ), + ); + const legacyTemplates = path.join(legacyRoot, 'templates'); + if (fs.existsSync(legacyTemplates)) { + for (const source of this.regularFiles(legacyTemplates)) { + this.recordResult( + summary, + source, + () => this.migrateTemplate( + source, + path.join( + targetRoot, + 'templates', + path.relative(legacyTemplates, source), + ), + ), + ); + } + } + return summary; + } + + /** + * 升级 v6 系统预设库存。 + * + * 已删除且没有等价系统方案的引用会复制为个人计划;旧胖次标识按地图 + * 映射到当前主库计划。迁移只修改 userData,安装资源始终只读。 + */ + migratePresetInventory(): LegacyMigrationSummary { + const state = this.readState(); + if (state.version >= USER_DATA_MIGRATION_VERSION) { + return emptyLegacyMigrationSummary(); + } + + const summary = emptyLegacyMigrationSummary(); + const userRoot = this.appPaths.userDataRoot(); + const operations: Array<[string, () => boolean]> = [ + [ + path.join(userRoot, 'gui_settings.json'), + () => this.migrateStoredLootPlanId( + path.join(userRoot, 'gui_settings.json'), + 'json', + ), + ], + [ + path.join(userRoot, 'usersettings.yaml'), + () => this.migrateStoredLootPlanId( + path.join(userRoot, 'usersettings.yaml'), + 'yaml', + ), + ], + [ + path.join(userRoot, 'task_groups.json'), + () => this.migrateObsoleteTaskGroupPlans( + path.join(userRoot, 'task_groups.json'), + ), + ], + [ + path.join(userRoot, 'templates', 'templates.json'), + () => this.migrateObsoleteTemplatePlans( + path.join(userRoot, 'templates', 'templates.json'), + ), + ], + [ + path.join(userRoot, 'templates.json'), + () => this.migrateObsoleteTemplatePlans( + path.join(userRoot, 'templates.json'), + ), + ], + ]; + + for (const [source, operation] of operations) { + this.recordResult(summary, source, operation); + } + summary.detected = summary.total > 0; + if (summary.failed === 0) { + this.writeState({ + version: USER_DATA_MIGRATION_VERSION, + completed: state.completed, + }); + } + return summary; + } + + /** 根据旧计划文件名映射更新任务组的受管计划引用。 */ + migrateLegacyTaskGroupPlanPaths( + fileMap: Map, + ): void { + const taskGroupsPath = path.join( + this.appPaths.userDataRoot(), + 'task_groups.json', + ); + if (!fs.existsSync(taskGroupsPath) || fileMap.size === 0) return; + const raw = JSON.parse( + fs.readFileSync(taskGroupsPath, 'utf-8'), + ) as Record; + if (!Array.isArray(raw.groups)) return; + let changed = false; + const groups = raw.groups.map(group => { + if (!this.isPlainObject(group) || !Array.isArray(group.items)) { + return group; + } + return { + ...group, + items: group.items.map(item => { + if (!this.isPlainObject(item)) return item; + const oldPath = typeof item.path === 'string' ? item.path : ''; + const oldFile = oldPath.split(/[\\/]/).pop() ?? ''; + const target = fileMap.get( + this.planReferenceKey(oldPath), + ) ?? fileMap.get(this.planReferenceKey(oldFile)); + if (!target) return item; + changed = true; + const { + managedSource: _managedSource, + managedFile: _managedFile, + dailySource: _dailySource, + dailyFile: _dailyFile, + dailyTaskType: _dailyTaskType, + ...preserved + } = item; + if (target.kind === 'daily') { + return { + ...preserved, + kind: 'daily', + dailySource: 'user', + dailyFile: target.file, + dailyTaskType: target.taskType, + path: oldPath, + }; + } + return { + ...preserved, + managedSource: 'user', + managedFile: target.file, + path: oldPath, + }; + }), + }; + }); + if (changed) { + this.atomicFiles.write( + taskGroupsPath, + JSON.stringify({ + ...raw, + version: 4, + groups, + }, null, 2), + ); + } + } + + private planReferenceKey(value: string): string { + const normalized = value + .trim() + .replace(/\\/g, '/') + .replace(/^\.\/+/, ''); + return process.platform === 'win32' + ? normalized.toLowerCase() + : normalized; + } + + /** 复制或合并旧任务组,同名但内容不同的组保留为旧版副本。 */ + private migrateLegacyTaskGroups(source: string, target: string): boolean { + if (!fs.existsSync(source) || this.pathKey(source) === this.pathKey(target)) { + return false; + } + const sourceContent = fs.readFileSync(source, 'utf-8'); + this.parseTaskGroups(sourceContent, '旧任务组'); + const state = this.readState(); + const completed = new Set(state.completed); + const key = this.taskGroupMigrationKey(source, sourceContent); + if (completed.has(key)) return false; + + fs.mkdirSync(path.dirname(target), { recursive: true }); + if (!fs.existsSync(target)) { + this.atomicFiles.write(target, sourceContent); + } else { + this.mergeTaskGroupFiles(sourceContent, target); + } + completed.add(key); + this.writeState({ + version: state.version, + completed: [...completed].sort(), + }); + return true; + } + + private mergeTaskGroupFiles(sourceContent: string, target: string): void { + const source = this.parseTaskGroups(sourceContent, '旧任务组'); + const targetContent = fs.readFileSync(target, 'utf-8'); + const current = this.parseTaskGroups(targetContent, '当前任务组'); + const groups = [...current.groups]; + let changed = false; + + for (const rawGroup of source.groups) { + if (!this.isPlainObject(rawGroup)) continue; + const group = { + ...rawGroup, + name: this.taskGroupName(rawGroup), + }; + const sameName = groups.filter(candidate => ( + this.isPlainObject(candidate) + && this.taskGroupName(candidate) === group.name + )); + if (sameName.some(candidate => this.sameJson(candidate, group))) { + continue; + } + if (sameName.length > 0) { + group.name = this.uniqueLegacyTaskGroupName(group.name, groups); + } + groups.push(group); + changed = true; + } + if (!changed) return; + + const activeGroup = ( + typeof current.activeGroup === 'string' + && groups.some(group => ( + this.isPlainObject(group) + && this.taskGroupName(group) === current.activeGroup + )) + ) + ? current.activeGroup + : ( + typeof source.activeGroup === 'string' + ? source.activeGroup + : this.firstTaskGroupName(groups) + ); + this.atomicFiles.write( + target, + JSON.stringify({ + ...current, + version: 1, + activeGroup, + groups, + }, null, 2), + ); + } + + private parseTaskGroups( + content: string, + label: string, + ): Record & { groups: unknown[] } { + const parsed = JSON.parse(content) as unknown; + if (!this.isPlainObject(parsed) || !Array.isArray(parsed.groups)) { + throw new Error(`${label}文件格式无效`); + } + return { + ...parsed, + groups: parsed.groups, + }; + } + + private taskGroupName(group: Record): string { + return typeof group.name === 'string' && group.name.trim() + ? group.name.trim() + : '默认'; + } + + private uniqueLegacyTaskGroupName( + name: string, + groups: unknown[], + ): string { + const usedNames = new Set(groups.flatMap(group => ( + this.isPlainObject(group) ? [this.taskGroupName(group)] : [] + ))); + let candidate = `${name}(旧版)`; + let suffix = 2; + while (usedNames.has(candidate)) { + candidate = `${name}(旧版 ${suffix})`; + suffix += 1; + } + return candidate; + } + + private firstTaskGroupName(groups: unknown[]): string { + const first = groups.find(group => this.isPlainObject(group)); + return first && this.isPlainObject(first) + ? this.taskGroupName(first) + : ''; + } + + private taskGroupMigrationKey(source: string, content: string): string { + const hash = crypto + .createHash('sha256') + .update(content) + .digest('hex'); + return `task-groups-v5:${this.pathKey(source)}:${hash}`; + } + + private pathKey(value: string): string { + const resolved = path.resolve(value); + return process.platform === 'win32' + ? resolved.toLowerCase() + : resolved; + } + + private sameJson(left: unknown, right: unknown): boolean { + return isDeepStrictEqual(left, right); + } + + private statePath(): string { + return path.join( + this.appPaths.userDataRoot(), + '.migration-state.json', + ); + } + + private migrateStoredLootPlanId( + target: string, + format: 'yaml' | 'json', + ): boolean { + const root = this.parseStructuredContent( + fs.readFileSync(target, 'utf-8'), + format, + path.basename(target), + ); + const automation = format === 'yaml' + ? root.daily_automation + : root.automation; + if (!this.isPlainObject(automation)) return false; + const key = format === 'yaml' ? 'loot_plan_id' : 'lootPlanId'; + const migrated = migrateLootPlanId(automation[key]); + if (!migrated || migrated === automation[key]) return false; + + automation[key] = migrated; + const content = format === 'yaml' + ? yaml.dump(root, { + lineWidth: -1, + noCompatMode: true, + noRefs: true, + sortKeys: false, + }) + : JSON.stringify(root, null, 2); + this.atomicFiles.write(target, content); + return true; + } + + private migrateObsoleteTaskGroupPlans(target: string): boolean { + const root = this.parseTaskGroups( + fs.readFileSync(target, 'utf-8'), + '当前任务组', + ); + let changed = false; + const groups = root.groups.map(group => { + if (!this.isPlainObject(group) || !Array.isArray(group.items)) { + return group; + } + return { + ...group, + items: group.items.map(item => { + if (!this.isPlainObject(item)) return item; + const file = this.obsoleteSystemPlanFile(item); + if (!file) return item; + const userFile = this.preserveObsoleteSystemPlan(file); + changed = true; + return { + ...item, + managedSource: 'user', + managedFile: userFile, + }; + }), + }; + }); + if (!changed) return false; + this.atomicFiles.write( + target, + JSON.stringify({ ...root, groups }, null, 2), + ); + return true; + } + + private migrateObsoleteTemplatePlans(target: string): boolean { + const parsed = JSON.parse(fs.readFileSync(target, 'utf-8')) as unknown; + if (!Array.isArray(parsed)) { + throw new Error('用户模板文件根节点必须是数组'); + } + let changed = false; + const templates = parsed.map(template => { + if (!this.isPlainObject(template)) return template; + const output = { ...template }; + if (typeof output.planPath === 'string') { + const migrated = this.migrateObsoleteTemplatePath(output.planPath); + if (migrated) { + output.planPath = migrated; + changed = true; + } + } + if (Array.isArray(output.planPaths)) { + output.planPaths = output.planPaths.map(planPath => { + const migrated = this.migrateObsoleteTemplatePath(planPath); + if (migrated) changed = true; + return migrated ?? planPath; + }); + } + return output; + }); + if (!changed) return false; + this.atomicFiles.write(target, JSON.stringify(templates, null, 2)); + return true; + } + + private migrateObsoleteTemplatePath(value: unknown): string | null { + if (typeof value !== 'string') return null; + const normalized = value.replace(/\\/g, '/'); + if (/(?:^|\/)user_battle_plans\//i.test(normalized)) return null; + const file = this.obsoletePlanFileName(value); + if (!file) return null; + return `user_battle_plans/${this.preserveObsoleteSystemPlan(file)}`; + } + + private obsoleteSystemPlanFile( + item: Record, + ): string | null { + if (item.managedSource === 'user') return null; + const pathValue = typeof item.path === 'string' ? item.path : ''; + const isSystemReference = item.managedSource === 'system' + || /(?:system_battle_plans|builtin_plans|system[\\/])/i.test(pathValue); + if (!isSystemReference) return null; + const managedFile = typeof item.managedFile === 'string' + ? item.managedFile + : ''; + return this.obsoletePlanFileName(managedFile) + ?? this.obsoletePlanFileName(pathValue); + } + + private obsoletePlanFileName(value: string): string | null { + const file = value.split(/[\\/]/).pop() ?? ''; + const normalized = OBSOLETE_SYSTEM_PLAN_ALIASES[file] ?? file; + return OBSOLETE_SYSTEM_PLAN_FILES.has(normalized) + ? normalized + : null; + } + + private preserveObsoleteSystemPlan(file: string): string { + const source = path.join( + this.appPaths.resourceRoot(), + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + file, + ); + if (!fs.existsSync(source)) { + throw new Error(`缺少 v6 迁移资源: ${file}`); + } + const content = fs.readFileSync(source, 'utf-8'); + const directory = this.appPaths.userBattlePlansDir(); + fs.mkdirSync(directory, { recursive: true }); + const extension = path.extname(file); + const base = file.slice(0, -extension.length); + let suffix = 1; + let candidate = path.join(directory, file); + while (fs.existsSync(candidate)) { + if (fs.readFileSync(candidate, 'utf-8') === content) { + return path.basename(candidate); + } + const label = suffix === 1 ? '(旧版)' : `(旧版 ${suffix})`; + candidate = path.join(directory, `${base}${label}${extension}`); + suffix += 1; + } + this.atomicFiles.write(candidate, content); + return path.basename(candidate); + } + + private migrateStructuredFile( + source: string, + target: string, + format: 'yaml' | 'json', + ): boolean { + if (!fs.existsSync(source) || this.pathKey(source) === this.pathKey(target)) { + return false; + } + const sourceContent = fs.readFileSync(source, 'utf-8'); + const state = this.readState(); + const completed = new Set(state.completed); + const key = this.contentMigrationKey(format, source, sourceContent); + if (completed.has(key)) return false; + + const legacy = this.parseStructuredContent( + sourceContent, + format, + `旧版 ${path.basename(source)}`, + ); + if (format === 'yaml' && path.basename(source) === 'usersettings.yaml') { + this.migrateLegacyLootPlanSelection(legacy, source); + } + const current = fs.existsSync(target) + ? this.parseStructuredContent( + fs.readFileSync(target, 'utf-8'), + format, + `当前 ${path.basename(target)}`, + ) + : {}; + const merged = this.deepMerge(current, legacy); + const content = format === 'yaml' + ? yaml.dump(merged, { + lineWidth: -1, + noCompatMode: true, + noRefs: true, + sortKeys: false, + }) + : JSON.stringify(merged, null, 2); + fs.mkdirSync(path.dirname(target), { recursive: true }); + this.atomicFiles.write(target, content); + completed.add(key); + this.writeState({ + version: state.version, + completed: [...completed].sort(), + }); + return true; + } + + /** + * 把旧数字索引转换为稳定计划文件名。 + * + * 完整旧安装优先使用其 builtin_templates.json 解释真实数组顺序; + * 找不到模板资源时,才按 PR 前四项布局迁移。 + */ + private migrateLegacyLootPlanSelection( + settings: Record, + source: string, + ): void { + const daily = settings.daily_automation; + if (!this.isPlainObject(daily)) return; + if (typeof daily.loot_plan_id === 'string') return; + if ( + !Object.prototype.hasOwnProperty.call(daily, 'loot_plan_index') + ) { + return; + } + + const index = parseLootPlanIndex(daily.loot_plan_index); + if (index === null) { + daily.loot_plan_id = DEFAULT_LOOT_PLAN_ID; + daily.auto_loot = false; + delete daily.loot_plan_index; + return; + } + const resolved = this.resolveLegacyLootPlanId( + index, + path.dirname(source), + ); + daily.loot_plan_id = resolved ?? DEFAULT_LOOT_PLAN_ID; + if (!resolved) daily.auto_loot = false; + delete daily.loot_plan_index; + } + + /** + * 纠正已由中间版本搬到 GUI JSON、但尚未解释语义的旧索引。 + * 两边索引不一致说明用户后来改过选择,此时保留 GUI JSON 的新布局语义。 + */ + private reconcilePreviouslyMigratedLootPlanSelection(): boolean { + const legacyRoot = this.appPaths.appRoot(); + const source = path.join(legacyRoot, 'usersettings.yaml'); + const target = path.join( + this.appPaths.userDataRoot(), + 'gui_settings.json', + ); + if (!fs.existsSync(source) || !fs.existsSync(target)) return false; + + const settings = this.parseStructuredContent( + fs.readFileSync(source, 'utf-8'), + 'yaml', + '旧版 usersettings.yaml', + ); + const daily = settings.daily_automation; + if (!this.isPlainObject(daily)) return false; + const sourceIndex = parseLootPlanIndex(daily.loot_plan_index); + if (sourceIndex === null) return false; + + const gui = this.parseStructuredContent( + fs.readFileSync(target, 'utf-8'), + 'json', + '当前 gui_settings.json', + ); + const automation = gui.automation; + if (!this.isPlainObject(automation)) return false; + if (typeof automation.lootPlanId === 'string') return false; + const targetIndex = parseLootPlanIndex(automation.lootPlanIndex); + if (targetIndex === null || sourceIndex !== targetIndex) return false; + + const resolved = this.resolveLegacyLootPlanId( + sourceIndex, + legacyRoot, + ); + const migrated: Record = { + ...automation, + lootPlanId: resolved ?? DEFAULT_LOOT_PLAN_ID, + }; + delete migrated.lootPlanIndex; + if (!resolved) migrated.autoLoot = false; + this.atomicFiles.write( + target, + JSON.stringify({ ...gui, automation: migrated }, null, 2), + ); + return true; + } + + /** 按旧安装自己的模板顺序解释刷取计划索引。 */ + private resolveLegacyLootPlanId( + index: number, + legacyRoot: string, + ): LootPlanId | null { + const legacyPaths = this.legacyLootPlanPaths(legacyRoot); + if (legacyPaths) { + return lootPlanIdFromLegacyPath(legacyPaths[index]); + } + return lootPlanIdFromIndex(index, LEGACY_LOOT_PLAN_IDS); + } + + /** 读取旧安装自己保存的刷胖次模板顺序。 */ + private legacyLootPlanPaths(legacyRoot: string): unknown[] | null { + const candidates = [ + path.join( + legacyRoot, + 'resources', + 'resource', + 'builtin_templates.json', + ), + path.join(legacyRoot, 'resource', 'builtin_templates.json'), + ]; + let templateFileFound = false; + for (const file of candidates) { + if (!fs.existsSync(file)) continue; + templateFileFound = true; + try { + const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as unknown; + if (!Array.isArray(parsed)) continue; + const template = parsed.find(item => ( + this.isPlainObject(item) + && item.id === 'builtin_farm_loot' + && Array.isArray(item.planPaths) + )); + if (!this.isPlainObject(template) || !Array.isArray(template.planPaths)) { + continue; + } + return [...template.planPaths]; + } catch { + // 继续检查另一种旧安装目录结构。 + } + } + return templateFileFound ? [] : null; + } + + private migrateTemplate(source: string, target: string): boolean { + const content = fs.readFileSync(source); + const state = this.readState(); + const completed = new Set(state.completed); + const key = this.contentMigrationKey( + 'template', + source, + content, + ); + if (completed.has(key)) return false; + + let destination = target; + if ( + fs.existsSync(destination) + && !fs.readFileSync(destination).equals(content) + ) { + destination = this.uniqueLegacyPath(destination); + } + if (!fs.existsSync(destination)) { + fs.mkdirSync(path.dirname(destination), { recursive: true }); + this.atomicFiles.write(destination, content.toString('utf-8')); + } + completed.add(key); + this.writeState({ + version: state.version, + completed: [...completed].sort(), + }); + return true; + } + + private recordResult( + summary: LegacyMigrationSummary, + source: string, + operation: () => boolean, + ): void { + if (!fs.existsSync(source)) return; + try { + if (!operation()) return; + summary.total += 1; + summary.succeeded += 1; + } catch (error) { + summary.total += 1; + summary.failed += 1; + summary.failedFiles.push(source); + console.error(`[Migration] ${source} failed:`, error); + } + } + + private parseStructuredContent( + content: string, + format: 'yaml' | 'json', + label: string, + ): Record { + const parsed = format === 'yaml' + ? yaml.load(content) + : JSON.parse(content); + if (!this.isPlainObject(parsed)) { + throw new Error(`${label}根节点必须是对象`); + } + return parsed; + } + + private deepMerge( + defaults: Record, + legacy: Record, + ): Record { + const result = structuredClone(defaults); + for (const [key, value] of Object.entries(legacy)) { + const current = result[key]; + result[key] = this.isPlainObject(current) && this.isPlainObject(value) + ? this.deepMerge(current, value) + : structuredClone(value); + } + return result; + } + + private contentMigrationKey( + kind: string, + source: string, + content: string | Buffer, + ): string { + const hash = crypto + .createHash('sha256') + .update(content) + .digest('hex'); + return `${kind}-v5:${this.pathKey(source)}:${hash}`; + } + + private regularFiles(directory: string): string[] { + const files: string[] = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.isSymbolicLink()) continue; + const target = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...this.regularFiles(target)); + } else if (entry.isFile()) { + files.push(target); + } + } + return files; + } + + private uniqueLegacyPath(target: string): string { + const extension = path.extname(target); + const base = target.slice(0, -extension.length); + let candidate = `${base}(旧版)${extension}`; + let suffix = 2; + while (fs.existsSync(candidate)) { + candidate = `${base}(旧版 ${suffix})${extension}`; + suffix += 1; + } + return candidate; + } + + private isPlainObject( + value: unknown, + ): value is Record { + return Boolean(value) + && typeof value === 'object' + && !Array.isArray(value); + } +} diff --git a/electron/services/WindowService.ts b/electron/services/WindowService.ts new file mode 100644 index 0000000..5970047 --- /dev/null +++ b/electron/services/WindowService.ts @@ -0,0 +1,268 @@ +/** + * 创建主窗口并管理窗口状态和偏好。 + */ +import type { + BrowserWindow, + BrowserWindowConstructorOptions, + MessageBoxOptions, +} from 'electron'; +import * as path from 'path'; +import { GuiSettingsStore } from './GuiSettingsStore'; + +/** renderer 可读取和修改的窗口设置。 */ +export interface WindowPreferences { + defaultWidth: number; + defaultHeight: number; + rememberBounds: boolean; +} + +interface WindowBounds { + x: number; + y: number; + width: number; + height: number; +} + +interface WindowDisplay { + workArea: WindowBounds; +} + +/** 由 main.ts 注入的 Electron 生命周期能力。 */ +export interface WindowServiceDependencies { + readonly backendPort: number; + readonly moduleDirectory: string; + createBrowserWindow( + options: BrowserWindowConstructorOptions, + ): BrowserWindow; + getDisplays(): WindowDisplay[]; + getAppPath(): string; + isPackaged(): boolean; + resourceRoot(): string; + showMessageBox(options: MessageBoxOptions): void; +} + +const DEFAULT_WINDOW_WIDTH = 1280; +const DEFAULT_WINDOW_HEIGHT = 720; +const MIN_WINDOW_WIDTH = 854; +const MIN_WINDOW_HEIGHT = 480; + +/** 创建窗口并管理窗口偏好、引用和边界持久化。 */ +export class WindowService { + private mainWindow: BrowserWindow | null = null; + private lastWindowBounds: WindowBounds | null = null; + + constructor( + private readonly settings: GuiSettingsStore, + private readonly dependencies: WindowServiceDependencies, + ) {} + + /** 返回当前主窗口;窗口销毁后返回 null。 */ + getMainWindow(): BrowserWindow | null { + return this.mainWindow; + } + + /** 读取并归一化默认窗口大小和边界记忆开关。 */ + getPreferences(): WindowPreferences { + const settings = this.settings.read(); + return { + defaultWidth: this.normalizeWindowSize( + settings.default_window_width, + MIN_WINDOW_WIDTH, + DEFAULT_WINDOW_WIDTH, + ), + defaultHeight: this.normalizeWindowSize( + settings.default_window_height, + MIN_WINDOW_HEIGHT, + DEFAULT_WINDOW_HEIGHT, + ), + rememberBounds: settings.remember_window_bounds === true, + }; + } + + /** 写入归一化后的窗口偏好,并返回实际持久化结果。 */ + setPreferences( + preferences: Partial, + ): WindowPreferences { + const current = this.getPreferences(); + this.settings.write({ + default_window_width: this.normalizeWindowSize( + preferences?.defaultWidth, + MIN_WINDOW_WIDTH, + current.defaultWidth, + ), + default_window_height: this.normalizeWindowSize( + preferences?.defaultHeight, + MIN_WINDOW_HEIGHT, + current.defaultHeight, + ), + remember_window_bounds: preferences?.rememberBounds === true, + }); + return this.getPreferences(); + } + + /** 缓存最后一次正常窗口边界,供窗口销毁后的退出阶段使用。 */ + captureWindowBounds( + win: BrowserWindow | null = this.mainWindow, + ): void { + if (!win || win.isDestroyed()) return; + this.lastWindowBounds = win.getNormalBounds(); + } + + /** 仅在用户启用窗口记忆时写入最后一次正常窗口边界。 */ + persistWindowBounds(): void { + if (this.getPreferences().rememberBounds && this.lastWindowBounds) { + this.settings.write({ window_bounds: this.lastWindowBounds }); + } + } + + /** 使用当前偏好创建并持有 Electron 主窗口。 */ + createWindow(): BrowserWindow { + const preferences = this.getPreferences(); + const rememberedBounds = preferences.rememberBounds + ? this.readRememberedWindowBounds() + : null; + const initialBounds = rememberedBounds + && this.isWindowBoundsVisible(rememberedBounds) + ? rememberedBounds + : null; + const win = this.dependencies.createBrowserWindow({ + width: initialBounds?.width ?? preferences.defaultWidth, + height: initialBounds?.height ?? preferences.defaultHeight, + x: initialBounds?.x, + y: initialBounds?.y, + center: initialBounds === null, + minWidth: MIN_WINDOW_WIDTH, + minHeight: MIN_WINDOW_HEIGHT, + webPreferences: { + preload: path.join( + this.dependencies.moduleDirectory, + 'preload.js', + ), + contextIsolation: true, + nodeIntegration: false, + }, + titleBarStyle: 'hiddenInset', + backgroundColor: '#1a1a2e', + icon: path.join( + this.dependencies.resourceRoot(), + 'resource', + 'images', + 'logo.png', + ), + }); + + const htmlPath = path.join( + this.dependencies.getAppPath(), + 'src', + 'view', + 'index.html', + ); + + // 根据后端端口动态注入现有 CSP。 + win.webContents.session.webRequest.onHeadersReceived( + (details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + 'Content-Security-Policy': [ + `default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' file: data:; connect-src 'self' http://localhost:${this.dependencies.backendPort} ws://localhost:${this.dependencies.backendPort}`, + ], + }, + }); + }, + ); + + win.webContents.on( + 'did-fail-load', + (_event, errorCode, errorDescription, validatedURL) => { + const message = `Page load failed!\nCode: ${errorCode}\nDesc: ${errorDescription}\nURL: ${validatedURL}\nPath: ${htmlPath}`; + console.error('[Main]', message); + if (this.dependencies.isPackaged()) { + this.dependencies.showMessageBox({ + type: 'error', + title: 'Load Error', + message, + }); + } + }, + ); + + win.loadFile(htmlPath).catch((error: Error) => { + console.error('[Main] loadFile failed:', error); + if (this.dependencies.isPackaged()) { + this.dependencies.showMessageBox({ + type: 'error', + title: 'loadFile Error', + message: `${error.message}\nPath: ${htmlPath}`, + }); + } + }); + + this.mainWindow = win; + this.captureWindowBounds(win); + win.on('move', () => this.captureWindowBounds(win)); + win.on('resize', () => this.captureWindowBounds(win)); + win.on('close', () => { + this.captureWindowBounds(win); + this.persistWindowBounds(); + }); + win.on('closed', () => { + this.mainWindow = null; + }); + return win; + } + + private normalizeWindowSize( + value: unknown, + minimum: number, + fallback: number, + ): number { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return fallback; + } + return Math.max(minimum, Math.trunc(value)); + } + + private readRememberedWindowBounds(): WindowBounds | null { + const raw = this.settings.read().window_bounds; + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null; + const bounds = raw as Record; + if ( + typeof bounds.x !== 'number' + || typeof bounds.y !== 'number' + || !Number.isFinite(bounds.x) + || !Number.isFinite(bounds.y) + ) { + return null; + } + return { + x: Math.trunc(bounds.x), + y: Math.trunc(bounds.y), + width: this.normalizeWindowSize( + bounds.width, + MIN_WINDOW_WIDTH, + DEFAULT_WINDOW_WIDTH, + ), + height: this.normalizeWindowSize( + bounds.height, + MIN_WINDOW_HEIGHT, + DEFAULT_WINDOW_HEIGHT, + ), + }; + } + + /** 至少保留一块可拖动区域,避免窗口完全落在屏幕外。 */ + private isWindowBoundsVisible(bounds: WindowBounds): boolean { + return this.dependencies.getDisplays().some(({ workArea }) => { + const visibleWidth = Math.min( + bounds.x + bounds.width, + workArea.x + workArea.width, + ) - Math.max(bounds.x, workArea.x); + const visibleHeight = Math.min( + bounds.y + bounds.height, + workArea.y + workArea.height, + ) - Math.max(bounds.y, workArea.y); + return visibleWidth >= 160 && visibleHeight >= 80; + }); + } +} diff --git a/package-lock.json b/package-lock.json index 286f98b..afeea79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "wsgrgui", - "version": "1.2.4", + "version": "2.0.4-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wsgrgui", - "version": "1.2.4", + "version": "2.0.4-dev", "dependencies": { "electron-updater": "^6.8.3", - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.0", + "jszip": "^3.10.1" }, "devDependencies": { "@types/js-yaml": "^4.0.9", @@ -2492,9 +2493,7 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/crc": { "version": "3.8.0", @@ -3917,6 +3916,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", @@ -3950,7 +3955,6 @@ "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ip-address": { @@ -4021,6 +4025,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isbinaryfile": { "version": "5.0.7", "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.7.tgz", @@ -4145,6 +4155,48 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -4161,6 +4213,15 @@ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "license": "MIT" }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", @@ -4743,6 +4804,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -4884,6 +4951,12 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", @@ -5205,6 +5278,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5730,7 +5809,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/verror": { diff --git a/package.json b/package.json index 2f1ca6b..626c978 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,36 @@ { "name": "wsgrgui", - "version": "1.4.1", - "description": "AutoWSGR 图形界面 — MVP", + "version": "2.0.4-dev", + "description": "AutoWSGR YAML 管理与自动化桌面界面", "main": "dist/electron/main.js", "scripts": { "build:css": "sass src/view/styles/main.scss src/view/styles/styles.css --no-source-map --style=expanded", - "clean": "node -e \"fs.rmSync('dist/src',{recursive:true,force:true});fs.rmSync('dist/electron',{recursive:true,force:true});console.log('dist cleaned')\" ", + "clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true});console.log('dist cleaned')\"", + "clean:release": "node -e \"fs.rmSync('release',{recursive:true,force:true});console.log('release cleaned')\"", "build": "npm run clean && npm run build:css && tsc && node scripts/bundle.js", + "sync:fleet-types": "node scripts/sync-fleet-ship-types.js", + "check:fleet-types": "node scripts/sync-fleet-ship-types.js --check", + "sync:maps": "node scripts/sync-map-resources.js", + "check:maps": "node scripts/sync-map-resources.js --check", + "test:legacy-config-upgrade": "npm run build && node scripts/test-legacy-config-upgrade.js && node scripts/test-task-group-migration.js", + "test:legacy-plan": "npm run build && node scripts/test-legacy-plan-migration.js", + "test:task-group-migration": "npm run build && node scripts/test-task-group-migration.js", + "test:api-contract": "npm run check:fleet-types && npm run build && node scripts/test-api-contract.js", + "test:fleet-domain": "npm run build && node scripts/test-fleet-domain.js", + "test:scheduler-domain": "npm run build && node scripts/test-scheduler-domain.js", + "test:daily-sortie-stats": "node scripts/test-daily-sortie-stats.js", + "test:ship-library-updater": "node scripts/test-ship-library-updater.js", + "test:event-resources": "node scripts/test-event-20260730-resources.js && node scripts/test-event-map-loader.js", + "test:settings": "npm run build && electron scripts/test-settings-persistence.js", + "test:main-services": "npm run build && node scripts/test-main-services.js", + "test:main-ipc": "npm run build && node scripts/test-main-ipc.js", + "test:python-environment": "npm run build && node scripts/test-python-environment.js", "start": "chcp 65001 >nul && npm run build && electron .", "dev": "chcp 65001 >nul && npm run clean && npm run build:css && tsc && node scripts/bundle.js && electron .", "prepare-python": "node scripts/prepare-python.js", "prepare-adb": "node scripts/prepare-adb.js", - "pack": "npm run build && electron-builder --dir", - "dist": "npm run prepare-python && npm run prepare-adb && npm run build && electron-builder" + "pack": "npm run clean:release && npm run build && electron-builder --dir --publish never", + "dist": "npm run clean:release && npm run prepare-python && npm run prepare-adb && npm run build && electron-builder --publish never" }, "devDependencies": { "@types/js-yaml": "^4.0.9", @@ -25,7 +43,8 @@ }, "dependencies": { "electron-updater": "^6.8.3", - "js-yaml": "^4.1.0" + "js-yaml": "^4.1.0", + "jszip": "^3.10.1" }, "build": { "appId": "com.autowsgr.gui", @@ -35,8 +54,8 @@ }, "files": [ "dist/**/*", - "src/view/**/*", - "scripts/**/*", + "src/view/index.html", + "src/view/styles/styles.css", "!node_modules/.cache" ], "extraResources": [ @@ -44,14 +63,9 @@ "from": "resource", "to": "resource", "filter": [ - "**/*" - ] - }, - { - "from": "plans", - "to": "plans", - "filter": [ - "**/*" + "**/*", + "!user_battle_plans/**/*", + "!user_team_plans/**/*" ] }, { @@ -61,6 +75,14 @@ { "from": "debug_deps.bat", "to": "debug_deps.bat" + }, + { + "from": "tools/ship_library", + "to": "tools/ship_library", + "filter": [ + "update_ship_library.py", + "native_fleet_types.py" + ] } ], "extraFiles": [ @@ -113,7 +135,8 @@ "publish": { "provider": "github", "owner": "yltx", - "repo": "AutoWSGR-GUI" + "repo": "AutoWSGR-GUI", + "channel": "dev" } } } diff --git "a/resource/builtin_plans/\345\206\263\346\210\230.yaml" "b/resource/builtin_plans/\345\206\263\346\210\230.yaml" deleted file mode 100644 index 299e2c2..0000000 --- "a/resource/builtin_plans/\345\206\263\346\210\230.yaml" +++ /dev/null @@ -1,19 +0,0 @@ -# 决战第 6 章 — 潜艇编队 -# 对应后端 API: POST /api/task/start { type: "decisive" } -# 需根据自己的舰船配置修改 level1 / level2 / flagship_priority - -task_type: decisive -chapter: 6 -level1: - - U-1206 - - U-96 - - U-47 - - 鹦鹉螺 - - 鲃鱼 - - 伊-25 -level2: - - M-296 - - 大青花鱼 - - U-1405 -flagship_priority: - - U-1206 diff --git "a/resource/builtin_plans/\345\221\250\345\270\27010\347\253\240-10-1.yaml" "b/resource/builtin_plans/\345\221\250\345\270\27010\347\253\240-10-1.yaml" deleted file mode 100644 index 5f3e8ed..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\27010\347\253\240-10-1.yaml" +++ /dev/null @@ -1,46 +0,0 @@ -# 周常第10章 — 地图 10-1 -# 编队: 空想(改型) + 351 + 4条改型潜艇(不限国籍模糊匹配) -# 目标等级: 全员 >= 110 -# 路线: B-D-F-G-H-L-M-O (L 迂回) - -chapter: 10 -map: 1 -selected_nodes: [B, D, F, G, H, L, M, O] -endpoint_nodes: [O] -fleet_id: 2 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - L: - formation: 2 - night: false - proceed: true - detour: true - M: - formation: 2 - night: false - proceed: true - long_missile_support: true - O: - formation: 4 - night: true - proceed: true - long_missile_support: true - -fleet_presets: - - name: 空想351潜艇队 - ships: - - { name: 空想·改, min_level: 110 } - - { name: 351, min_level: 110 } - - { ship_type: ss_or_ssg, min_level: 110 } - - { ship_type: ss_or_ssg, min_level: 110 } - - { ship_type: ss_or_ssg, min_level: 110 } - - { ship_type: ss_or_ssg, min_level: 110 } - -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2701\347\253\240-1-5.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2701\347\253\240-1-5.yaml" deleted file mode 100644 index 269ec5e..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2701\347\253\240-1-5.yaml" +++ /dev/null @@ -1,24 +0,0 @@ -# 周常第1章 — 地图 1-5 -# 编队: 任意3条潜艇/导潜 -# 路线: 直通 Boss (A) - -chapter: 1 -map: 5 -selected_nodes: [A] -endpoint_nodes: [A] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -fleet_presets: - - name: 潜艇队 - ships: - - { ship_type: ss_or_ssg, min_level: 100 } - - { ship_type: ss_or_ssg, min_level: 100 } - - { ship_type: ss_or_ssg, min_level: 100 } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2702\347\253\240-2-1.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2702\347\253\240-2-1.yaml" deleted file mode 100644 index d29bbfa..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2702\347\253\240-2-1.yaml" +++ /dev/null @@ -1,44 +0,0 @@ -# 周常第2章 — 地图 2-1 -# 编队方案: -# 1) SS*4 -# 2) T-23·改 + 3条德国驱逐 -# 3) 岛风·改 + 3条日本驱逐 -# 默认复纵阵 - -chapter: 2 -map: 1 -selected_nodes: [B, D, F] -endpoint_nodes: [F] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - F: - night: true - -fleet_presets: - - name: SS*4 - ships: - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - name: T-23德驱队 - ships: - - { name: T-23·改, min_level: 100 } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - name: 岛风日驱队 - ships: - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2703\347\253\240-3-1.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2703\347\253\240-3-1.yaml" deleted file mode 100644 index 6c16132..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2703\347\253\240-3-1.yaml" +++ /dev/null @@ -1,37 +0,0 @@ -# 周常第3章 — 地图 3-1 -# 编队方案: -# 1) 85工程 + T-23·改 + 2条德国驱逐 -# 2) 85工程 + 岛风·改 + 2条日本驱逐 -# 默认复纵阵 - -chapter: 3 -map: 1 -selected_nodes: [A, B, C, E, F, H, I] -endpoint_nodes: [I] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - I: - night: true - -fleet_presets: - - name: 85工程德驱队 - ships: - - { name: 85工程, min_level: 100 } - - { name: T-23·改, min_level: 100 } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - name: 85工程日驱队 - ships: - - { name: 85工程, min_level: 100 } - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2703\347\253\240-3-3.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2703\347\253\240-3-3.yaml" deleted file mode 100644 index f9e0d05..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2703\347\253\240-3-3.yaml" +++ /dev/null @@ -1,34 +0,0 @@ -# 周常第3章(备选) — 地图 3-3 -# 编队方案同 3-1 -# 默认复纵阵 - -chapter: 3 -map: 3 -endpoint_nodes: [K] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - K: - night: true - -fleet_presets: - - name: 85工程德驱队 - ships: - - { name: 85工程, min_level: 100 } - - { name: T-23·改, min_level: 100 } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - name: 85工程日驱队 - ships: - - { name: 85工程, min_level: 100 } - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2704\347\253\240-4-1.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2704\347\253\240-4-1.yaml" deleted file mode 100644 index f87bd75..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2704\347\253\240-4-1.yaml" +++ /dev/null @@ -1,35 +0,0 @@ -# 周常第4章 — 地图 4-1 -# 编队方案同第3章 -# 默认复纵阵 - -chapter: 4 -map: 1 -selected_nodes: [A, B, C, D, F, G, I] -endpoint_nodes: [I] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - I: - night: true - -fleet_presets: - - name: 85工程德驱队 - ships: - - { name: 85工程, min_level: 100 } - - { name: T-23·改, min_level: 100 } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - name: 85工程日驱队 - ships: - - { name: 85工程, min_level: 100 } - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2705\347\253\240-5-5.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2705\347\253\240-5-5.yaml" deleted file mode 100644 index f40e5ef..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2705\347\253\240-5-5.yaml" +++ /dev/null @@ -1,32 +0,0 @@ -# 周常第5章 — 地图 5-5 -# 编队: SS*6 -# 默认复纵阵 - -chapter: 5 -map: 5 -selected_nodes: [C, F, H, I] -endpoint_nodes: [I] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - I: - formation: 4 - night: true - -fleet_presets: - - name: SS*6 - ships: - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } -times: 3 \ No newline at end of file diff --git "a/resource/builtin_plans/\345\221\250\345\270\2706\347\253\240-6-3.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2706\347\253\240-6-3.yaml" deleted file mode 100644 index c00e036..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2706\347\253\240-6-3.yaml" +++ /dev/null @@ -1,31 +0,0 @@ -# 周常第6章(备选) — 地图 6-3 -# 编队: SS*6 -# 默认复纵阵 - -chapter: 6 -map: 3 -selected_nodes: [B, E, H, J] -endpoint_nodes: [J] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - J: - night: true - -fleet_presets: - - name: SS*6 - ships: - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2706\347\253\240-6-4.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2706\347\253\240-6-4.yaml" deleted file mode 100644 index ac9c9aa..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2706\347\253\240-6-4.yaml" +++ /dev/null @@ -1,41 +0,0 @@ -# 周常第6章 — 地图 6-4 -# 编队方案: -# 1) 克劳塞维茨·改 + T-23·改 + 2条德国驱逐 + 赛尔弗里吉 + 85工程 -# 2) 85工程 + 斯佩伯爵海军上将·改 + 鲃鱼·改 + 岛风·改 + 2条日本驱逐 -# 默认复纵阵 - -chapter: 6 -map: 4 -selected_nodes: [A, C, F, H, J, K] -endpoint_nodes: [K] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - K: - night: true - -fleet_presets: - - name: 克劳塞维茨德系队 - ships: - - { name: 克劳塞维茨·改, min_level: 100 } - - { name: T-23·改, min_level: 100 } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { nation: 德国, ship_type: dd, min_level: 100, priority: ['吕贝克·改', 'Z17·改', 'Z16·改', 'Z46·改', 'Z31·改', 'Z1·改', 'Z22·改', 'Z21·改'] } - - { name: 赛尔弗里吉, min_level: 100 } - - { name: 85工程, min_level: 100 } - - name: 85工程日系混编 - ships: - - { name: 85工程, min_level: 100 } - - { name: 斯佩伯爵海军上将·改, min_level: 100 } - - { name: 鲃鱼·改, min_level: 100 } - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } - - { nation: 日本, ship_type: dd, min_level: 100, priority: ['岛风(岛风型驱逐舰)·改', '黑潮·改', '阳炎·改', '早春·改'] } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2707\347\253\240-7-4.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2707\347\253\240-7-4.yaml" deleted file mode 100644 index e0a2260..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2707\347\253\240-7-4.yaml" +++ /dev/null @@ -1,32 +0,0 @@ -# 周常第7章 — 地图 7-4 -# 编队: SS*6 -# 默认复纵阵,Boss(M点)梯形阵+夜战 - -chapter: 7 -map: 4 -selected_nodes: [B, D, E, G, H, L, K, M] -endpoint_nodes: [M] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - M: - formation: 4 - night: true - -fleet_presets: - - name: SS*6 - ships: - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2708\347\253\240-8-2.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2708\347\253\240-8-2.yaml" deleted file mode 100644 index ba7b22d..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2708\347\253\240-8-2.yaml" +++ /dev/null @@ -1,39 +0,0 @@ -# 周常第8章 — 地图 8-2 -# 编队方案: -# 1) 大淀(苍青幻影) + AIII + 85工程 + 潜艇 -# 2) SS*6 -# 默认复纵阵 - -chapter: 8 -map: 2 -selected_nodes: [B, J, M] -endpoint_nodes: [M] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - M: - night: true - -fleet_presets: - - name: 大淀AIII队 - ships: - - { name: 大淀(苍青幻影), ship_type: cl, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 85工程, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - name: SS*6 - ships: - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } - - { ship_type: ss, min_level: 100 } -times: 3 diff --git "a/resource/builtin_plans/\345\221\250\345\270\2709\347\253\240-9-2.yaml" "b/resource/builtin_plans/\345\221\250\345\270\2709\347\253\240-9-2.yaml" deleted file mode 100644 index b66ada4..0000000 --- "a/resource/builtin_plans/\345\221\250\345\270\2709\347\253\240-9-2.yaml" +++ /dev/null @@ -1,103 +0,0 @@ -# 周常第9章 — 地图 9-2 -# 编队同 9-2 捞胖次配队 -# 默认复纵阵 - -chapter: 9 -map: 2 -selected_nodes: - - A - - D - - G - - H - - M - - O -endpoint_nodes: [O] -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -node_args: - A: - detour: true - sl_when_detour_fails: true - D: - detour: true - sl_when_detour_fails: true - G: - detour: true - sl_when_detour_fails: true - H: - detour: true - sl_when_detour_fails: true - M: - detour: true - sl_when_detour_fails: true - O: - formation: 4 - night: true - proceed: true - detour: false - -fleet_presets: - - name: 传统AIII双装母 - ships: - - { name: 飞龙·改, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 鞍山·改, min_level: 100 } - - { name: 长春, min_level: 100 } - - { name: 大凤·改, min_level: 100 } - - { name: 不挠(装甲航母), min_level: 100 } - - name: 传统85不挠 - ships: - - { name: 85工程, min_level: 100 } - - { name: 鞍山·改, min_level: 100 } - - { name: 长春, min_level: 100 } - - { name: 飞龙·改, min_level: 100 } - - { name: 济南, min_level: 100 } - - { name: 不挠(装甲航母), min_level: 100 } - - name: 三响岛风 - ships: - - { name: 85工程, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 早春·改, min_level: 100 } - - { name: 黑潮·改, min_level: 100 } - - { name: 炽热·改, min_level: 100 } - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - name: 双响岛风 - ships: - - { name: 85工程, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 炽热·改, min_level: 100 } - - { name: 絮弗伦, min_level: 100 } - - { name: 岛风(岛风型驱逐舰)·改, min_level: 100 } - - { name: 黑潮·改, min_level: 100 } - - name: 85AIII四防驱 - ships: - - { name: 85工程, min_level: 100 } - - { name: 不惧(防空导弹驱逐舰), min_level: 100 } - - { name: 炽热·改, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 基阿特·改, min_level: 100 } - - { name: 絮弗伦, min_level: 100 } - - name: 传统85AIII零铝 - ships: - - { name: 飞龙·改, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 鞍山·改, min_level: 100 } - - { name: 85工程, min_level: 100 } - - { name: 基阿特·改, min_level: 100 } - - { name: 长春, min_level: 100 } - - name: 炽热85AIII双装母 - ships: - - { name: 85工程, min_level: 100 } - - { name: AIII, min_level: 100 } - - { name: 絮弗伦, min_level: 100 } - - { name: 炽热·改, min_level: 100 } - - { name: 不挠(装甲航母), min_level: 100 } - - { name: 大凤·改, min_level: 100 } -times: 3 \ No newline at end of file diff --git "a/resource/builtin_plans/\346\210\230\345\275\271.yaml" "b/resource/builtin_plans/\346\210\230\345\275\271.yaml" deleted file mode 100644 index 0720489..0000000 --- "a/resource/builtin_plans/\346\210\230\345\275\271.yaml" +++ /dev/null @@ -1,6 +0,0 @@ -# 困难航母战役 — 刷 8 次困难航母 -# 对应后端 API: POST /api/task/start { type: "campaign" } - -task_type: campaign -campaign_name: 困难航母 -times: 8 diff --git "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2412-1.yaml" "b/resource/builtin_plans/\346\215\236\350\203\226\346\254\2412-1.yaml" deleted file mode 100644 index 306b09e..0000000 --- "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2412-1.yaml" +++ /dev/null @@ -1,15 +0,0 @@ -# 2-1 捞胖次 — 简单的低耗图,适合挂机 -# 默认复纵阵,Boss 梯形阵+夜战 - -chapter: 2 -map: 1 -fleet_id: 1 -fight_condition: 1 -repair_mode: 1 - -node_defaults: - formation: 2 - night: false - proceed: true - -times: 10 diff --git "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2417-4.yaml" "b/resource/builtin_plans/\346\215\236\350\203\226\346\254\2417-4.yaml" deleted file mode 100644 index 366e243..0000000 --- "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2417-4.yaml" +++ /dev/null @@ -1,28 +0,0 @@ -# 功能: 7-4 漂流捞胖次 -# 推荐配置: 6 鱼, 选择 1-2 个三级后备弹, 其余大角度 - -chapter: 7 -map: 4 -selected_nodes: [A, B, C, E, D, F, G, H, I, J, L, M, K] -fight_condition: 4 -repair_mode: 1 -fleet_id: 3 - -node_defaults: - enemy_rules: - - [DD + CL <= 1, 4] - - [CVL == 1 and CV == 0, 4] - formation: 2 - night: false - proceed: true - proceed_stop: [2, 2, 2, 2, 2, 2] - -node_args: - M: - enemy_rules: - - [SAP < 1, retreat] - formation: 4 - night: true - I: - enemy_rules: - - [SAP < 1, retreat] diff --git "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2418-5.yaml" "b/resource/builtin_plans/\346\215\236\350\203\226\346\254\2418-5.yaml" deleted file mode 100644 index b71a523..0000000 --- "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2418-5.yaml" +++ /dev/null @@ -1,22 +0,0 @@ -# 功能: -# 1)练6鱼 2)捞胖次,电表倒转 -# 3)捞稀有池[鲃鱼、霞飞、尼古拉斯、哥伦比亚、B65、塞班、托戈、吸血鬼、七省联盟、U47、墨尔波墨涅、龙骧、哈曼、留里克、埃德加·居内] -# 带路条件:平均航速<=27,最低航速<=21 -# 推荐配置:6鱼 -# 来源:https://nga.178.com/read.php?tid=16860675 -# MapLevel -chapter: 8 -map: 5 -selected_nodes: - - A - - I -fleet_id: 1 -repair_mode: 1 -node_args: - A: - formation: 4 - night: false - proceed: true - I: - formation: 4 - night: true diff --git "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2419-2.yaml" "b/resource/builtin_plans/\346\215\236\350\203\226\346\254\2419-2.yaml" deleted file mode 100644 index 88c3025..0000000 --- "a/resource/builtin_plans/\346\215\236\350\203\226\346\254\2419-2.yaml" +++ /dev/null @@ -1,129 +0,0 @@ -# 作战计划: 9-2 捞胖次 -# 迂回点 (A D G H M): 有活动补给舰(AP)梯形阵战斗, 无则迂回 -# E K O: 无活动补给舰撤退, 有则梯形阵战斗 -chapter: 9 -map: 2 -selected_nodes: - - A - - D - - G - - H - - M - - O - - E - - K -endpoint_nodes: - - E - - K - - O -fleet_id: 2 -fight_condition: 1 -repair_mode: 2 -node_defaults: - formation: 4 - night: false - proceed: true -node_args: - A: - enemy_rules: - - - AP >= 1 - - 4 - - - AP < 1 - - detour - D: - enemy_rules: - - - AP >= 1 - - 4 - - - AP < 1 - - detour - G: - enemy_rules: - - - AP >= 1 - - 4 - - - AP < 1 - - detour - H: - enemy_rules: - - - AP >= 1 - - 4 - - - AP < 1 - - detour - M: - enemy_rules: - - - AP >= 1 - - 4 - - - AP < 1 - - detour - E: - enemy_rules: - - - AP < 1 - - retreat - K: - enemy_rules: - - - AP < 1 - - retreat - O: - enemy_rules: - - - AP < 1 - - retreat -fleet_presets: - - name: 传统AIII双装母 - ships: - - 飞龙·改 - - AIII - - 鞍山·改 - - 长春 - - 大凤·改 - - 不挠(装甲航母) - - name: 传统85不挠 - ships: - - 85工程 - - 鞍山·改 - - 长春 - - 飞龙·改 - - 济南 - - 不挠(装甲航母) - - name: 三响岛风 - ships: - - 85工程 - - AIII - - 早春·改 - - 黑潮·改 - - 炽热·改 - - 岛风(岛风型驱逐舰)·改 - - name: 双响岛风 - ships: - - 85工程 - - AIII - - 炽热·改 - - 絮弗伦 - - 岛风(岛风型驱逐舰)·改 - - 黑潮·改 - - name: 85AIII四防驱 - ships: - - 85工程 - - 不惧(防空导弹驱逐舰) - - 炽热·改 - - AIII - - 基阿特·改 - - 絮弗伦 - - name: 传统85AIII零铝 - ships: - - 飞龙·改 - - AIII - - 鞍山·改 - - 85工程 - - 基阿特·改 - - 长春 - - name: 炽热85AIII双装母 - ships: - - 85工程 - - AIII - - 絮弗伦 - - 炽热·改 - - 不挠(装甲航母) - - 大凤·改 -times: 9999 -gap: 0 -stop_condition: - loot_count_ge: 50 diff --git "a/resource/builtin_plans/\346\264\273\345\212\25020260730-E1\347\202\270\351\261\274.yaml" "b/resource/builtin_plans/\346\264\273\345\212\25020260730-E1\347\202\270\351\261\274.yaml" deleted file mode 100644 index ba1da3e..0000000 --- "a/resource/builtin_plans/\346\264\273\345\212\25020260730-E1\347\202\270\351\261\274.yaml" +++ /dev/null @@ -1,8 +0,0 @@ -# 20260730 激斗漩涡 E-1 α 入口示例 -event: "20260730" -chapter: E -map: 1a -selected_nodes: [B] -node_defaults: - proceed: false - formation: 5 diff --git "a/resource/builtin_plans/\346\264\273\345\212\25020260730-E5\345\244\234\346\210\230.yaml" "b/resource/builtin_plans/\346\264\273\345\212\25020260730-E5\345\244\234\346\210\230.yaml" deleted file mode 100644 index 12eb446..0000000 --- "a/resource/builtin_plans/\346\264\273\345\212\25020260730-E5\345\244\234\346\210\230.yaml" +++ /dev/null @@ -1,16 +0,0 @@ -# 20260730 激斗漩涡 E-5 α 入口示例 -event: "20260730" -chapter: E -map: 5a -selected_nodes: [A, B, C, D, F] -node_defaults: - night: true - proceed: true - formation: 4 -node_args: - C: - proceed: false - D: - proceed: false - F: - proceed: false diff --git "a/resource/builtin_plans/\346\264\273\345\212\25020260730-H1\347\202\270\351\261\274.yaml" "b/resource/builtin_plans/\346\264\273\345\212\25020260730-H1\347\202\270\351\261\274.yaml" deleted file mode 100644 index 6d8bc6f..0000000 --- "a/resource/builtin_plans/\346\264\273\345\212\25020260730-H1\347\202\270\351\261\274.yaml" +++ /dev/null @@ -1,8 +0,0 @@ -# 20260730 激斗漩涡 H-1 α 入口示例 -event: "20260730" -chapter: H -map: 1a -selected_nodes: [B] -node_defaults: - proceed: false - formation: 5 diff --git "a/resource/builtin_plans/\346\264\273\345\212\25020260730-H5\345\244\234\346\210\230.yaml" "b/resource/builtin_plans/\346\264\273\345\212\25020260730-H5\345\244\234\346\210\230.yaml" deleted file mode 100644 index eb49c81..0000000 --- "a/resource/builtin_plans/\346\264\273\345\212\25020260730-H5\345\244\234\346\210\230.yaml" +++ /dev/null @@ -1,16 +0,0 @@ -# 20260730 激斗漩涡 H-5 α 入口示例 -event: "20260730" -chapter: H -map: 5a -selected_nodes: [A, B, C, D, F] -node_defaults: - night: true - proceed: true - formation: 4 -node_args: - C: - proceed: false - D: - proceed: false - F: - proceed: false diff --git "a/resource/builtin_plans/\350\207\252\345\212\250\346\274\224\344\271\240.yaml" "b/resource/builtin_plans/\350\207\252\345\212\250\346\274\224\344\271\240.yaml" deleted file mode 100644 index ad11bfa..0000000 --- "a/resource/builtin_plans/\350\207\252\345\212\250\346\274\224\344\271\240.yaml" +++ /dev/null @@ -1,5 +0,0 @@ -# 自动演习 — 使用第 4 舰队挑战所有可用对手 -# 对应后端 API: POST /api/task/start { type: "exercise" } - -task_type: exercise -fleet_id: 4 diff --git a/resource/builtin_templates.json b/resource/builtin_templates.json index 9b0fa1c..4792078 100644 --- a/resource/builtin_templates.json +++ b/resource/builtin_templates.json @@ -7,10 +7,14 @@ "createdAt": "2026-01-01T00:00:00.000Z", "description": "自动刷取战利品(胖次),可选择不同地图方案", "planPaths": [ - "resource/builtin_plans/捞胖次9-2.yaml", - "resource/builtin_plans/捞胖次7-4.yaml", - "resource/builtin_plans/捞胖次8-5.yaml", - "resource/builtin_plans/捞胖次2-1.yaml" + "resource/system_battle_plans/bettle-old-9-2ADGHM速刷胖次.yaml", + "resource/system_battle_plans/bettle-old-8-5AI六潜胖次.yaml", + "resource/system_battle_plans/bettle-old-8-2BJ低耗胖次.yaml", + "resource/system_battle_plans/bettle-old-9-4六潜练级.yaml", + "resource/system_battle_plans/bettle-周常-9-2.yaml", + "resource/system_battle_plans/bettle-周常-7-4.yaml", + "resource/system_battle_plans/bettle-周常-8-2.yaml", + "resource/system_battle_plans/bettle-周常-2-1.yaml" ], "defaultTimes": 99, "defaultStopCondition": { @@ -18,26 +22,24 @@ } }, { - "id": "builtin_weekly", + "id": "builtin_weekly_v2", "name": "周常任务", "type": "normal_fight", "builtin": true, "createdAt": "2026-01-01T00:00:00.000Z", - "description": "每周战斗任务(第1-9章),选择对应章节方案执行", + "description": "每周战斗任务(第1-10章),选择对应章节方案执行", "forceRetry": true, "planPaths": [ - "resource/builtin_plans/周常1章-1-5.yaml", - "resource/builtin_plans/周常2章-2-1.yaml", - "resource/builtin_plans/周常3章-3-1.yaml", - "resource/builtin_plans/周常3章-3-3.yaml", - "resource/builtin_plans/周常4章-4-1.yaml", - "resource/builtin_plans/周常5章-5-5.yaml", - "resource/builtin_plans/周常6章-6-4.yaml", - "resource/builtin_plans/周常6章-6-3.yaml", - "resource/builtin_plans/周常7章-7-4.yaml", - "resource/builtin_plans/周常8章-8-2.yaml", - "resource/builtin_plans/周常9章-9-2.yaml", - "resource/builtin_plans/周常10章-10-1.yaml" + "resource/system_battle_plans/bettle-周常-1-1.yaml", + "resource/system_battle_plans/bettle-周常-2-1.yaml", + "resource/system_battle_plans/bettle-周常-3-1.yaml", + "resource/system_battle_plans/bettle-周常-4-1.yaml", + "resource/system_battle_plans/bettle-周常-5-5.yaml", + "resource/system_battle_plans/bettle-周常-6-4.yaml", + "resource/system_battle_plans/bettle-周常-7-4.yaml", + "resource/system_battle_plans/bettle-周常-8-2.yaml", + "resource/system_battle_plans/bettle-周常-9-2.yaml", + "resource/system_battle_plans/bettle-周常-10-1.yaml" ], "defaultTimes": 3 }, @@ -51,22 +53,6 @@ "fleet_id": 1, "defaultTimes": 1 }, - { - "id": "builtin_event_20260730", - "name": "活动·激斗漩涡", - "type": "event_fight", - "builtin": true, - "createdAt": "2026-07-30T00:00:00.000Z", - "description": "2026-07-30 活动方案(简单/困难 E1、E5)", - "forceRetry": true, - "planPaths": [ - "resource/builtin_plans/活动20260730-E1炸鱼.yaml", - "resource/builtin_plans/活动20260730-E5夜战.yaml", - "resource/builtin_plans/活动20260730-H1炸鱼.yaml", - "resource/builtin_plans/活动20260730-H5夜战.yaml" - ], - "defaultTimes": 1 - }, { "id": "builtin_campaign", "name": "战役", @@ -85,9 +71,39 @@ "createdAt": "2026-01-01T00:00:00.000Z", "description": "自动完成决战", "chapter": 6, - "level1": [], - "level2": [], + "level1": [ + "U-47", + "U-1405", + "U-1206", + "U-2540", + "U-81", + "U-96" + ], + "level2": [ + "U-505", + "射水鱼", + "大青花鱼", + "M-296", + "鹦鹉螺", + "S-49", + "IIIA", + "K-21", + "U-441", + "潜甲", + "潜乙", + "伊-201", + "伊-25", + "鲃鱼", + "伊-400", + "激流", + "U-4501", + "U-459", + "U-14", + "U-35", + "K1" + ], "flagship_priority": [], + "use_quick_repair": true, "defaultTimes": 1 } ] diff --git a/resource/maps/1-1.json b/resource/maps/1-1.json deleted file mode 100644 index c159ea7..0000000 --- a/resource/maps/1-1.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 342.0496268013302, - 623.8015088901824 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 568.8014005840529, - 564.4882165006527 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 780.1705691983113, - 356.0096039033276 - ], - "next": [] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 790.5479138661225, - 628.4413764122166 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/1-2.json b/resource/maps/1-2.json deleted file mode 100644 index 7f15f0a..0000000 --- a/resource/maps/1-2.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 464.52999957419564, - 766.0469222564623 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 508.92566539872536, - 547.8943335087678 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 736.8932161204344, - 682.6391390631162 - ], - "next": [ - "C", - "D" - ] - }, - "C": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 704.8672218996168, - 365.55611057123554 - ], - "next": [] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1001.4659217463056, - 792.9748386923764 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/1-3.json b/resource/maps/1-3.json deleted file mode 100644 index 1a9cbef..0000000 --- a/resource/maps/1-3.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 460.73066622137094, - 804.8418701704805 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 671.4825219088976, - 637.3631672397887 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 865.8343407429034, - 865.5707075919601 - ], - "next": [ - "C", - "D" - ] - }, - "C": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 950.4160969843265, - 604.455647488114 - ], - "next": [ - "D", - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1186.3216203336679, - 753.5560765183117 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 848.4599969397933, - 365.0413020562982 - ], - "next": [] - }, - "F": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1257.1612947391404, - 428.97762888928514 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/1-4.json b/resource/maps/1-4.json deleted file mode 100644 index 421c15d..0000000 --- a/resource/maps/1-4.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1222.984294882409, - 942.6320024483405 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 993.6698465143312, - 788.378713765838 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1328.5024962614234, - 645.0127704163385 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 768.0660098545866, - 716.2369941127886 - ], - "next": [ - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1223.3123799792677, - 346.4975050011242 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 448.85064856766274, - 693.0098909262957 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 802.2625981613963, - 429.051783487788 - ], - "next": [ - "E", - "G" - ] - }, - "G": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 560.573022721956, - 291.05395060600034 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/1-5.json b/resource/maps/1-5.json deleted file mode 100644 index e43ca76..0000000 --- a/resource/maps/1-5.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 705.2234740467446, - 730.3625111200122 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1177.7281429463299, - 495.0953889716992 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/10-1.json b/resource/maps/10-1.json deleted file mode 100644 index 43d0fc5..0000000 --- a/resource/maps/10-1.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1799.263907619874, - 853.4973657396408 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1664.4643416438812, - 343.2523580911082 - ], - "next": [ - "C", - "I" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1574.1647767944776, - 600.0931928513504 - ], - "next": [ - "D", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 1364.7766599577924, - 120.35045890303472 - ], - "next": [] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1365.2221028019817, - 884.6733241160524 - ], - "next": [ - "E", - "G" - ] - }, - "E": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 974.8405599861525, - 990.1046306142182 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1259.9348417833385, - 494.7809303573815 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1005.9070126332639, - 794.3749186233719 - ], - "next": [ - "L" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 898.4304498656261, - 555.2570302582286 - ], - "next": [ - "L" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1095.654151743309, - 360.3851545399037 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 870.5241253546842, - 149.21580824185867 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 689.9048803199616, - 479.4474243243405 - ], - "next": [ - "N", - "O" - ] - }, - "L": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 631.4155887727388, - 809.9736942402918 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 255.08757222916236, - 733.9583556669783 - ], - "next": [ - "O" - ] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 479.771612392546, - 286.0089221581164 - ], - "next": [ - "O" - ] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 119.29022731467502, - 405.22856056992674 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/2-1.json b/resource/maps/2-1.json deleted file mode 100644 index ab8d92f..0000000 --- a/resource/maps/2-1.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 348.5220450253415, - 292.72530386723986 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 660.1404625541776, - 270.6241866129301 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 490.7995602670431, - 532.7618952749323 - ], - "next": [ - "C", - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 939.2672077100074, - 341.26627219667165 - ], - "next": [ - "E" - ] - }, - "D": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 623.2409644807201, - 810.8206613548159 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1110.7558605337147, - 600.7565175434956 - ], - "next": [ - "F" - ] - }, - "F": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 973.9586646582817, - 883.4839500815564 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/2-2.json b/resource/maps/2-2.json deleted file mode 100644 index 2f53470..0000000 --- a/resource/maps/2-2.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1005.7578805225036, - 254.5106069228003 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1094.8306845747193, - 509.99126866550085 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 794.760650329527, - 480.1092080547052 - ], - "next": [ - "E", - "F" - ] - }, - "C": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1350.5365145032358, - 420.86760924772005 - ], - "next": [] - }, - "D": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 1185.5788516741466, - 795.8341914124146 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 855.9820456607144, - 735.5486108606005 - ], - "next": [ - "G" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 488.39355628573145, - 455.90045039150084 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 689.9615609315553, - 945.0590011655319 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 398.68636887974554, - 216.8285651389588 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/2-3.json b/resource/maps/2-3.json deleted file mode 100644 index a36c516..0000000 --- a/resource/maps/2-3.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 329.019934707859, - 958.897026529007 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 262.52038593366166, - 667.5666610966833 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 645.1271764612636, - 855.166578914029 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 382.32520813849925, - 411.58477904186356 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 696.6155875717645, - 562.5066446323111 - ], - "next": [ - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1118.4923033165408, - 954.0767103601106 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 674.8574617487648, - 226.64157559752454 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 915.2507937470662, - 411.22356131774916 - ], - "next": [ - "I", - "J" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1395.107411448158, - 877.5019952214778 - ], - "next": [ - "K" - ] - }, - "I": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 1035.2695840579393, - 180.72729058121698 - ], - "next": [] - }, - "J": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1342.7530131783792, - 396.3535692570664 - ], - "next": [] - }, - "K": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1574.9022211716945, - 570.4044493915073 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/2-4.json b/resource/maps/2-4.json deleted file mode 100644 index 0c9a95e..0000000 --- a/resource/maps/2-4.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 336.0897752601243, - 974.2497151494462 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 226.01929994924728, - 742.424576700613 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 569.3547920242959, - 765.0305427505905 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 478.97459933361506, - 395.84343077020185 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 876.8844977303955, - 638.0427888420875 - ], - "next": [ - "G", - "J" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 825.4339380769782, - 841.5695183880293 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 884.5246136843461, - 443.7341599900581 - ], - "next": [ - "I", - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1139.8170387803789, - 749.9430636294775 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1027.2650634069635, - 975.0358905684483 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1050.5873712557898, - 238.5960943107587 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1222.789759479638, - 434.5132775542741 - ], - "next": [ - "L" - ] - }, - "K": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1493.4541222961666, - 870.9237410414762 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1627.1587250528137, - 330.18544102720483 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/2-5.json b/resource/maps/2-5.json deleted file mode 100644 index 511f360..0000000 --- a/resource/maps/2-5.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 113.65363772696783, - 193.57937960964324 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 406.4325676627073, - 216.7572933655012 - ], - "next": [ - "B" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 675.1847527509382, - 315.09102577120217 - ], - "next": [ - "C", - "D", - "E" - ] - }, - "C": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 916.8549503844541, - 239.31190636947719 - ], - "next": [ - "F", - "G" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 779.9720096021973, - 495.0847879540482 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 429.36464070728414, - 622.49131042202 - ], - "next": [ - "I" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1288.7314554313737, - 149.2100946528277 - ], - "next": [ - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1222.6033248596098, - 382.5981914257193 - ], - "next": [ - "J" - ] - }, - "H": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 931.1073722955571, - 660.50297450414 - ], - "next": [ - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 915.4528358134713, - 809.7922210983436 - ], - "next": [ - "L", - "M", - "N" - ] - }, - "J": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1560.2386681678934, - 331.83576447448723 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1214.9529768688285, - 568.1731282882549 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1484.7437113301255, - 795.0889507438566 - ], - "next": [] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1251.7816187305075, - 960.6843927055361 - ], - "next": [] - }, - "N": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 592.019758832097, - 990.653855509674 - ], - "next": [] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1649.5459631292358, - 615.1354758201618 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/2-6.json b/resource/maps/2-6.json deleted file mode 100644 index 5116b07..0000000 --- a/resource/maps/2-6.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 285.3922910770917, - 900.4409806684415 - ], - "next": [ - "A", - "B", - "E" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 269.45844473197747, - 630.1771555001744 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 675.4293613882732, - 974.0771033927155 - ], - "next": [ - "F" - ] - }, - "C": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 223.95209088039815, - 330.99228456614 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 571.1420315095089, - 526.386750761736 - ], - "next": [ - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 719.6237950286453, - 750.696525615435 - ], - "next": [ - "D", - "K" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1033.1874516538244, - 803.3235245892082 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 718.6314826966874, - 209.49566930495791 - ], - "next": [ - "I", - "J" - ] - }, - "H": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1304.6496155864709, - 839.3135484443613 - ], - "next": [ - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1104.2127575810432, - 231.6119494951799 - ], - "next": [ - "L" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1006.1725854058005, - 419.0533055127643 - ], - "next": [ - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1261.2027355837931, - 547.9810082292375 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1410.1338652790846, - 211.5716378599281 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1679.9838465352354, - 645.0273304206632 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/3-1.json b/resource/maps/3-1.json deleted file mode 100644 index fbeb6a2..0000000 --- a/resource/maps/3-1.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1231.1637386987466, - 240.5978937099884 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 869.3394825428303, - 226.83960657480637 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 758.4690065798098, - 405.63494909430125 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 599.2275814903505, - 284.86114765572853 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1290.128640820417, - 509.7103103916063 - ], - "next": [] - }, - "E": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 773.8225931872115, - 600.7271515994877 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 374.70531707758903, - 511.112845552866 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 976.6831459850483, - 734.7423975611086 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 419.9441331594407, - 809.8911779594414 - ], - "next": [ - "I" - ] - }, - "I": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 750.1550833498547, - 916.9456310806986 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/3-2.json b/resource/maps/3-2.json deleted file mode 100644 index ee44a44..0000000 --- a/resource/maps/3-2.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1455.0657509163916, - 239.81468838746073 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1575.2522417086518, - 435.2930612451133 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1245.2339611255964, - 554.4035118326142 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1201.1233351509102, - 390.2703339692709 - ], - "next": [ - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1418.5679622497169, - 644.6469644192368 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 931.637898116548, - 614.2812487383908 - ], - "next": [ - "G" - ] - }, - "F": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1365.0839504665723, - 899.9166040613193 - ], - "next": [] - }, - "G": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 884.5846809133732, - 915.7585506879047 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/3-3.json b/resource/maps/3-3.json deleted file mode 100644 index a1a1be7..0000000 --- a/resource/maps/3-3.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1559.6028347929619, - 239.3840851284253 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1575.3206818493381, - 586.8281143989435 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1424.392922639534, - 481.19182186481305 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1312.6239626690667, - 741.9120847795547 - ], - "next": [ - "F", - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1184.844440310326, - 502.54775156354094 - ], - "next": [ - "G", - "I" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 928.4889227704589, - 314.60568257505 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1079.0742840843648, - 936.9034314222288 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1065.0030546683226, - 704.9867308817016 - ], - "next": [ - "J" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 615.2046899314423, - 509.28387058443553 - ], - "next": [ - "J" - ] - }, - "I": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 884.8063897631004, - 600.7809001598511 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 703.4583762106016, - 825.9660997613286 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 269.4908753828779, - 840.4189336576683 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/3-4.json b/resource/maps/3-4.json deleted file mode 100644 index 5cb5606..0000000 --- a/resource/maps/3-4.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1545.8907584335418, - 209.13013761029777 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1318.8376018939782, - 412.4534559966367 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1275.0285725865801, - 683.7565001477362 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1080.582223394257, - 502.3630364645676 - ], - "next": [ - "D", - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1050.493215665892, - 869.7525004475152 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 855.5345390930474, - 517.107978994129 - ], - "next": [ - "G", - "H", - "I" - ] - }, - "F": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 721.368731351228, - 944.8276391166992 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 766.4286053233698, - 719.9320816315283 - ], - "next": [ - "F" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 420.7239721162904, - 658.7331657503895 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 510.43554377374414, - 442.9286443861617 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 255.51421912048684, - 270.2476210065831 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/4-1.json b/resource/maps/4-1.json deleted file mode 100644 index 5dcb8a1..0000000 --- a/resource/maps/4-1.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1514.9077233538283, - 255.07357416592274 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1283.3078330106098, - 523.8548917233569 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1216.401603945872, - 351.9252394581006 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1169.7146124116894, - 749.4365981405716 - ], - "next": [ - "E", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 975.8658363398279, - 435.06313213741976 - ], - "next": [ - "F", - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1320.2255060309703, - 915.5318653769235 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 892.4583762583416, - 614.8659048914967 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 495.95115762491963, - 525.3937705535767 - ], - "next": [ - "I", - "J" - ] - }, - "H": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1588.685328973883, - 884.6731884765442 - ], - "next": [] - }, - "I": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 524.8446407610601, - 915.5884326423525 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 181.2731217990133, - 613.5162493748934 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/4-2.json b/resource/maps/4-2.json deleted file mode 100644 index c1dd093..0000000 --- a/resource/maps/4-2.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1590.5897743122082, - 868.4831013428814 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1261.1152014766762, - 809.6186646983954 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1516.3703277106285, - 553.9371388679608 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1036.9776607654771, - 757.3102554318594 - ], - "next": [ - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1275.7794377748216, - 616.2498351139297 - ], - "next": [ - "C", - "F" - ] - }, - "E": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 593.5009298956508, - 675.8833006440989 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1102.6091457744992, - 345.201943403866 - ], - "next": [ - "H", - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 502.8508247151945, - 878.6160375828094 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 802.3604910582023, - 442.0818265948292 - ], - "next": [ - "J" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 645.1376059702368, - 225.534602152432 - ], - "next": [] - }, - "J": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 420.41548470639896, - 269.9052302324397 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/4-3.json b/resource/maps/4-3.json deleted file mode 100644 index ba91c53..0000000 --- a/resource/maps/4-3.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1665.2310399776916, - 359.7367577729413 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1499.5626845575132, - 541.8169723139176 - ], - "next": [ - "C", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1244.9768308829937, - 277.48883289135244 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1228.9262935961494, - 591.9729267164739 - ], - "next": [ - "E", - "G" - ] - }, - "D": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 689.9729333977194, - 239.73076245697715 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 973.9626929903657, - 563.9274858617733 - ], - "next": [ - "G" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 396.2828064757763, - 569.6152800860933 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 713.408014852085, - 556.1709809937508 - ], - "next": [ - "F", - "H" - ] - }, - "H": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 307.4854674524384, - 914.8837738892113 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/4-4.json b/resource/maps/4-4.json deleted file mode 100644 index 948e5f0..0000000 --- a/resource/maps/4-4.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1604.6023080657983, - 524.9743500222263 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1199.4735138297156, - 922.3194915728304 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1333.1415859942051, - 644.7680662394133 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 609.1478201271942, - 899.406304396071 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 848.4827404739979, - 853.764893387766 - ], - "next": [ - "K" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1102.6603837517534, - 614.9856491491112 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 390.40002636411486, - 718.9422879582426 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 856.6572301884279, - 456.430805070646 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1050.1667062870972, - 330.30606527411805 - ], - "next": [ - "J" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 277.90173739860705, - 509.8537120657524 - ], - "next": [ - "L" - ] - }, - "J": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 796.440430180028, - 248.55733799282538 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 517.8193725860231, - 396.645664954804 - ], - "next": [ - "L" - ] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 262.92233408247114, - 246.45864297476817 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/5-1.json b/resource/maps/5-1.json deleted file mode 100644 index b3b6991..0000000 --- a/resource/maps/5-1.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1708.461784154883, - 839.5110707330215 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1364.9499393808396, - 779.862645696202 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1537.011382563373, - 406.24634248231456 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 989.0416394977403, - 930.0214669919632 - ], - "next": [ - "E", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1035.2005026302263, - 494.66560395768846 - ], - "next": [ - "F", - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 230.64709389734537, - 735.3830551236064 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 510.2845136797863, - 585.4484313829624 - ], - "next": [ - "G", - "H" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 689.4253531276223, - 226.3138805831391 - ], - "next": [] - }, - "H": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 269.9799953916257, - 193.21322505657517 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/5-2.json b/resource/maps/5-2.json deleted file mode 100644 index ed15c9c..0000000 --- a/resource/maps/5-2.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1635.1601213041404, - 885.2319855965192 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1349.701644812314, - 931.2073586780687 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1305.0117258269897, - 591.8796073430221 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1680.2943780795629, - 478.3656637984865 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 810.1979221531482, - 841.3040390527458 - ], - "next": [ - "G", - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 968.8259695966647, - 576.5722745115028 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1169.8747526738107, - 224.99083729466537 - ], - "next": [ - "H", - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 360.3005961158091, - 969.1443322397664 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 532.6082267860029, - 539.555266933098 - ], - "next": [ - "J" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 615.3101472513181, - 226.14594551482702 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 194.44496944932413, - 314.89630407747086 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/5-3.json b/resource/maps/5-3.json deleted file mode 100644 index 8313e8c..0000000 --- a/resource/maps/5-3.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1605.7897417724623, - 254.79535820513675 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1424.4563349114546, - 704.8256406199195 - ], - "next": [ - "C", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1259.898779137804, - 322.7618775967946 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1544.475099171926, - 922.2468563160462 - ], - "next": [] - }, - "D": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1080.0007056413851, - 614.9964472949603 - ], - "next": [ - "F", - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1140.453829733087, - 915.2244527739168 - ], - "next": [ - "G" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 953.3765797872107, - 381.5642042030091 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 562.3261550363311, - 817.5273549361505 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 361.8812319236884, - 465.1877787149731 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/5-4.json b/resource/maps/5-4.json deleted file mode 100644 index 11ddd46..0000000 --- a/resource/maps/5-4.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1499.8370732993253, - 255.43697450028003 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1229.0549836255425, - 525.2143711297831 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1080.0304399694908, - 187.50935120598993 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1380.652557459465, - 824.1488688902963 - ], - "next": [ - "E", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 644.2968460363745, - 374.5718185249428 - ], - "next": [ - "A", - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 974.9902957684532, - 945.005516109377 - ], - "next": [] - }, - "F": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 899.9176675174034, - 734.8058974099079 - ], - "next": [ - "G" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 470.90618944202237, - 816.3004774458432 - ], - "next": [ - "E", - "H" - ] - }, - "H": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 166.5757852856291, - 480.6449503238055 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/5-5.json b/resource/maps/5-5.json deleted file mode 100644 index 6001b77..0000000 --- a/resource/maps/5-5.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1694.8897506063706, - 404.92411282201135 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1531.6936779711862, - 899.2749822603927 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1364.164931414962, - 660.1419735528121 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1185.2245581939628, - 238.80759595550933 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1019.9262720732825, - 900.1317739019134 - ], - "next": [ - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 929.3139443498915, - 510.12158314930497 - ], - "next": [ - "F", - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 840.3287838946658, - 230.7812637335481 - ], - "next": [ - "H", - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 509.95414273344244, - 907.5780615413197 - ], - "next": [ - "I" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 519.1857099490043, - 592.1755318614951 - ], - "next": [ - "I" - ] - }, - "I": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 181.11314612238036, - 451.6584287956898 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/6-1.json b/resource/maps/6-1.json deleted file mode 100644 index bdba3fa..0000000 --- a/resource/maps/6-1.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1343.6121209658752, - 943.5915974916717 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 779.1252479997423, - 930.6722888875564 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1049.8094175629847, - 704.6818322020947 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1261.6854535238924, - 674.9212916441194 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 465.36203850669347, - 734.6327116409936 - ], - "next": [ - "G", - "H" - ] - }, - "E": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 779.7238074282558, - 733.1632417820185 - ], - "next": [ - "D" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 930.7679155112329, - 448.505845587762 - ], - "next": [ - "H", - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 276.5902029308818, - 944.8158372702519 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 495.6992709468046, - 451.03172843322625 - ], - "next": [ - "J", - "K", - "L" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 885.9367320014402, - 224.38611264210996 - ], - "next": [] - }, - "J": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 164.98617133663652, - 629.9888423986445 - ], - "next": [] - }, - "K": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 285.5854940668296, - 301.69759898361383 - ], - "next": [] - }, - "L": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 658.2147121680047, - 194.76074579300723 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/6-2.json b/resource/maps/6-2.json deleted file mode 100644 index d8173bd..0000000 --- a/resource/maps/6-2.json +++ /dev/null @@ -1,174 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 390.4803868051543, - 960.357244643417 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 135.950642177183, - 464.33925592561985 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 352.652590236112, - 675.6443178337366 - ], - "next": [ - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 780.0460188393795, - 938.4079354636717 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 480.0478982036327, - 344.9329911449487 - ], - "next": [ - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 630.1661448749189, - 495.1020216842969 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1034.9343586145094, - 735.0367857104529 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 854.3634953542348, - 329.9485384105985 - ], - "next": [ - "J" - ] - }, - "H": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 975.1480792606914, - 541.6142398620104 - ], - "next": [ - "I", - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1379.6411811467851, - 636.1903135750092 - ], - "next": [ - "M" - ] - }, - "J": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1154.610750524388, - 238.65832624159532 - ], - "next": [ - "M" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1260.1267316824021, - 427.7602487614074 - ], - "next": [ - "J", - "L" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1529.8680548917037, - 436.2950535332576 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1664.5431965245114, - 194.56691596433578 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/6-3.json b/resource/maps/6-3.json deleted file mode 100644 index c3193f6..0000000 --- a/resource/maps/6-3.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 465.32746671093946, - 870.0316778665298 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 811.4083449579427, - 931.3941419565674 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 584.6440855961363, - 586.5150772149544 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 330.7126165737391, - 553.4408896763847 - ], - "next": [ - "E", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1126.3624063601537, - 877.2630595074077 - ], - "next": [ - "G", - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 899.9262021763662, - 457.312177608504 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 688.9078117988242, - 201.69490584034605 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 1393.8257845962805, - 644.8612566942064 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1275.1877151581223, - 493.89395754366024 - ], - "next": [ - "J" - ] - }, - "I": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1080.595122572657, - 293.32276025461243 - ], - "next": [] - }, - "J": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1687.6962019202715, - 307.9877165670531 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/6-4.json b/resource/maps/6-4.json deleted file mode 100644 index 6f6af1d..0000000 --- a/resource/maps/6-4.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 249.33836866428678, - 862.2116379274455 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 275.9397051684513, - 585.3345422466359 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 758.4256229833728, - 586.1712868063099 - ], - "next": [ - "D", - "E", - "G" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 480.1880675502702, - 322.29682131171216 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 434.71035080042736, - 825.2621785721507 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 839.9900807404213, - 930.0984668297136 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 853.871073795341, - 271.3596220493616 - ], - "next": [ - "G", - "H", - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1020.0574201340584, - 645.1049999926453 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1199.6267642294508, - 455.6839624469106 - ], - "next": [ - "J" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1267.4955685942323, - 195.7847617540713 - ], - "next": [] - }, - "J": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 1544.775741397609, - 629.9223799164618 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1320.3039332812862, - 914.1658834916254 - ], - "next": [] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1799.0212468533023, - 870.5221871262953 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/7-1.json b/resource/maps/7-1.json deleted file mode 100644 index c565ab5..0000000 --- a/resource/maps/7-1.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1665.2682849342937, - 704.36534183761 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1424.3958871702084, - 839.7250723977604 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1336.5281028826546, - 390.8187330147272 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1161.6165591005526, - 733.2346776143717 - ], - "next": [ - "F", - "G" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1087.975762662482, - 464.24897418047186 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 1198.6939499739428, - 210.04117569418779 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 870.4707369581204, - 885.0779675295058 - ], - "next": [ - "L" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 869.4469818132792, - 660.4895248000979 - ], - "next": [ - "I" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 833.0120013573265, - 391.5285745775184 - ], - "next": [ - "J", - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 593.5982979138325, - 620.8831789351738 - ], - "next": [ - "K", - "M", - "N" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 465.12982410860394, - 239.21737492914585 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 539.8597731197817, - 418.3882145223093 - ], - "next": [ - "N" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 547.1291570779399, - 922.2628327359841 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 435.2214428110236, - 742.5228283371144 - ], - "next": [] - }, - "N": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 179.49563601421073, - 539.7610900945723 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/7-2.json b/resource/maps/7-2.json deleted file mode 100644 index 6256f60..0000000 --- a/resource/maps/7-2.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1484.9983866777447, - 720.5627407618101 - ], - "next": [ - "A", - "B", - "F" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1035.4175430486575, - 614.7924890053912 - ], - "next": [ - "C", - "D", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1260.9249147498044, - 495.2816527443359 - ], - "next": [ - "G" - ] - }, - "C": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 1004.7905220931151, - 840.83082492961 - ], - "next": [ - "I" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 773.1700575399798, - 704.5737898001652 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 914.5506471592191, - 360.48233324562796 - ], - "next": [ - "H", - "K" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1515.4547411270569, - 419.1662957658226 - ], - "next": [ - "G", - "L" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1319.7045839872783, - 239.39306553818952 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 539.7951809842624, - 631.1001539127074 - ], - "next": [ - "J", - "K" - ] - }, - "I": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 667.0658340248311, - 944.7861535617066 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 254.6826874920465, - 733.8309815599754 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 511.5253385742541, - 321.343270039014 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1747.9113680806963, - 261.58803372439564 - ], - "next": [] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 225.29763458258319, - 253.076393050207 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/7-3.json b/resource/maps/7-3.json deleted file mode 100644 index 5f127ee..0000000 --- a/resource/maps/7-3.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1604.6213534176484, - 598.1471356390157 - ], - "next": [ - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1289.9873504304119, - 870.0048021128314 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1320.6339272515622, - 614.1011237514363 - ], - "next": [ - "A", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1506.959465351096, - 351.7511855364379 - ], - "next": [ - "F", - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 950.948877809424, - 952.2437600403151 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1086.3742516634945, - 749.1489128784152 - ], - "next": [ - "D", - "I" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1125.661850855132, - 509.69943788719587 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1229.8682218714637, - 231.68064334986224 - ], - "next": [ - "J", - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 645.3118024397419, - 855.0802757770399 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 727.5000471288826, - 719.7035968998524 - ], - "next": [ - "L" - ] - }, - "J": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 856.0258200838888, - 419.2720411700256 - ], - "next": [ - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 983.6486771406293, - 210.5411987256289 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 419.7271397738155, - 568.2744788567637 - ], - "next": [ - "N" - ] - }, - "M": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 629.4550367427498, - 286.65148530174235 - ], - "next": [] - }, - "N": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 238.59684795485836, - 344.1960323595157 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/7-4.json b/resource/maps/7-4.json deleted file mode 100644 index 3063f9b..0000000 --- a/resource/maps/7-4.json +++ /dev/null @@ -1,171 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1568.6243275802303, - 353.5194242490425 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1515.287137591737, - 577.7342873477702 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1364.8859635749236, - 204.40756960282118 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1381.373486817474, - 757.7471101160576 - ], - "next": [ - "D", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1110.4996305380675, - 472.4901104332113 - ], - "next": [ - "B", - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 997.0401855429602, - 262.28477999907295 - ], - "next": [ - "D" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1206.644896397755, - 877.9198023086636 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 832.7038823558015, - 465.0228178608706 - ], - "next": [ - "E", - "K", - "L" - ] - }, - "H": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 764.7460840716899, - 232.73520662905696 - ], - "next": [ - "M" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 952.5452909163439, - 938.8975902519344 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 764.5148650581143, - 848.1001644181346 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 674.706007843587, - 704.9853781142763 - ], - "next": [ - "J", - "M" - ] - }, - "L": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 599.0792395545881, - 338.2269059239462 - ], - "next": [ - "H", - "M" - ] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 352.645372580893, - 337.5030146581181 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/7-5.json b/resource/maps/7-5.json deleted file mode 100644 index 206ea1c..0000000 --- a/resource/maps/7-5.json +++ /dev/null @@ -1,220 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1725.9419558620211, - 728.0095588183752 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1529.9489356693941, - 862.3697734550091 - ], - "next": [ - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1469.4212052237522, - 562.1682493471185 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1260.3666946595774, - 930.729954585338 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1259.9721492960978, - 652.5898603918575 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1214.9093567999441, - 442.60626885007696 - ], - "next": [ - "I", - "J" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1410.0766030796992, - 292.32322518739824 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 899.9294732357611, - 952.4897337477636 - ], - "next": [ - "K", - "L" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 997.8234958461525, - 667.9208872956522 - ], - "next": [ - "L" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 921.1907076184482, - 336.19520781733667 - ], - "next": [ - "L", - "O" - ] - }, - "J": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1125.113159216786, - 219.3374411052439 - ], - "next": [ - "F", - "M" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 583.9894651197682, - 906.8931165074873 - ], - "next": [ - "P" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 734.7304911176816, - 637.7347052311098 - ], - "next": [ - "N" - ] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 825.74557669338, - 202.68007487592982 - ], - "next": [ - "O" - ] - }, - "N": { - "type": "Resource", - "detour": false, - "night": false, - "position": [ - 464.72215542349903, - 568.6614009314184 - ], - "next": [ - "Q" - ] - }, - "O": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 472.5040646918321, - 262.7510090278795 - ], - "next": [ - "Q" - ] - }, - "P": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [ - 284.86578261715357, - 749.4203460032626 - ], - "next": [ - "Q" - ] - }, - "Q": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 195.22944982402706, - 458.6359836829281 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/8-1.json b/resource/maps/8-1.json deleted file mode 100644 index f866717..0000000 --- a/resource/maps/8-1.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 219.41765784943473, - 304.55953911669184 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 442.00369698526896, - 475.5924083455516 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 623.9564203998914, - 256.5176937835543 - ], - "next": [ - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 560.3599754592497, - 739.5025163167579 - ], - "next": [] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 796.2280763514152, - 609.1116831461219 - ], - "next": [ - "G", - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 908.9454492598787, - 389.704017577261 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1102.5455006131047, - 261.58995069620045 - ], - "next": [ - "I", - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1061.4505110896907, - 909.9218896988058 - ], - "next": [ - "K", - "L" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1186.5248377172659, - 722.494964661571 - ], - "next": [ - "L" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1253.8217990468545, - 475.3944368624017 - ], - "next": [ - "H" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1506.6513159773651, - 304.51681499722883 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 755.1468335014479, - 931.6794908075055 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1572.207858240964, - 763.7675518153293 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/8-2.json b/resource/maps/8-2.json deleted file mode 100644 index 49105c1..0000000 --- a/resource/maps/8-2.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 615.6399561107439, - 763.9757589607793 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 975.0406806905675, - 854.9875567171945 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Aerial", - "detour": false, - "night": false, - "position": [ - 990.3691859112635, - 585.2086414133698 - ], - "next": [ - "E", - "J" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 419.4603079446919, - 511.6340249786144 - ], - "next": [ - "F", - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1320.4202545653766, - 810.0144082479532 - ], - "next": [ - "H", - "I" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1289.7403520785492, - 585.076417928965 - ], - "next": [ - "I" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 750.1353676899498, - 283.42346033147203 - ], - "next": [ - "J", - "K" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 434.7967772483315, - 210.0055868814688 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1619.202880232293, - 915.7654150724168 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1575.298098123635, - 676.1575241848911 - ], - "next": [ - "M" - ] - }, - "J": { - "type": "Aerial", - "detour": false, - "night": false, - "position": [ - 1139.3145560562737, - 404.52449687526087 - ], - "next": [ - "M" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1108.8496907080462, - 211.11230348809536 - ], - "next": [ - "L" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1439.800811561368, - 241.40056833352537 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1681.5849921768704, - 344.13318565569114 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/8-3.json b/resource/maps/8-3.json deleted file mode 100644 index 11039ba..0000000 --- a/resource/maps/8-3.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1455.3826025785418, - 870.2071349018582 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1604.8072807014044, - 390.44319440733324 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1169.2494376187467, - 690.9052585594126 - ], - "next": [ - "E", - "F", - "G" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1351.4611436282908, - 464.71831413695827 - ], - "next": [ - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1079.9838684816539, - 238.67500640487947 - ], - "next": [ - "H", - "K" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 944.7047155475492, - 433.0645580325513 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 749.4878000315247, - 794.8615595473094 - ], - "next": [ - "I", - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 780.2174446930902, - 571.0898835034228 - ], - "next": [ - "H", - "I" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 613.9033227999809, - 403.6404656235353 - ], - "next": [ - "K", - "L" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 344.5355572621953, - 614.381454246159 - ], - "next": [ - "L" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 931.2081302882148, - 961.5432154683213 - ], - "next": [] - }, - "K": { - "type": "Hard", - "detour": false, - "night": false, - "position": [ - 570.435955616978, - 165.06120959634083 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 284.62589791022447, - 329.31180169778526 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/8-4.json b/resource/maps/8-4.json deleted file mode 100644 index 41efc72..0000000 --- a/resource/maps/8-4.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1771.454845441345, - 316.0076298798695 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1363.7373148790016, - 239.37319890849628 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1590.6873264214976, - 569.9820930276561 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 990.3220344307426, - 225.16772963850946 - ], - "next": [ - "F", - "G", - "H" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1186.794539340201, - 585.7130762682099 - ], - "next": [ - "H", - "I", - "J" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1543.55860241814, - 885.0390315335827 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 614.8460993689329, - 194.23895575071055 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 555.4662936913875, - 329.35199981660327 - ], - "next": [ - "M" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 795.0108242535922, - 525.3212686900338 - ], - "next": [ - "M" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 885.2297681501223, - 779.8332872600265 - ], - "next": [ - "K", - "L", - "M" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1124.7471452280079, - 959.9977949699999 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 674.422118904426, - 1005.4237723200507 - ], - "next": [] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 525.1620646363646, - 766.5575186688618 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 256.16428998705686, - 583.994803407445 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/8-5.json b/resource/maps/8-5.json deleted file mode 100644 index 6779323..0000000 --- a/resource/maps/8-5.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 373.7504246283573, - 780.3887432684843 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 345.1219083791954, - 404.96362692226216 - ], - "next": [ - "D", - "I" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 585.2830799026727, - 434.38206049093264 - ], - "next": [ - "D" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 750.0101651037339, - 961.4400695013741 - ], - "next": [ - "E", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 870.3012606528386, - 269.1593691289103 - ], - "next": [ - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 961.7668473645082, - 569.637735194316 - ], - "next": [ - "G" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1154.4549080310976, - 779.6716619008674 - ], - "next": [ - "H", - "K", - "L" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1350.1852539032238, - 254.9177757267552 - ], - "next": [ - "J", - "M" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1424.2318912531832, - 524.4856819968722 - ], - "next": [ - "M" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 525.1180112343951, - 165.34327441026593 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1680.1846849576598, - 149.76431943418996 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1665.099513003299, - 810.7089668370592 - ], - "next": [] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1469.7765026390448, - 989.6140283982601 - ], - "next": [] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1768.733724291477, - 388.72878942667165 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/9-1.json b/resource/maps/9-1.json deleted file mode 100644 index 5a9b073..0000000 --- a/resource/maps/9-1.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 164.01308640513648, - 330.8559876932155 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Aerial", - "detour": false, - "night": false, - "position": [ - 539.8626325036834, - 194.65948110050527 - ], - "next": [ - "D", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 434.7256230832524, - 495.2015224912505 - ], - "next": [ - "F", - "G" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 268.2340664674107, - 675.8930578950567 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1231.2863521668376, - 240.6037755747528 - ], - "next": [ - "L", - "M" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 960.1392016145844, - 375.0362017475778 - ], - "next": [ - "J" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 749.047449032305, - 645.9588585596329 - ], - "next": [ - "I", - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 600.0307956688699, - 841.5790926411477 - ], - "next": [ - "H", - "I" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 870.0255161078318, - 1004.9912440903024 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 958.4514095155733, - 899.9706671640091 - ], - "next": [ - "O" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1079.794921911965, - 794.2361006516353 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1214.4387191776989, - 419.61017204442913 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1531.0340909895694, - 148.35473988706508 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1469.868689948667, - 464.8263683980615 - ], - "next": [ - "N", - "O" - ] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1738.172578595985, - 659.8719905981541 - ], - "next": [] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1469.3456823812678, - 839.6384418186371 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/9-2.json b/resource/maps/9-2.json deleted file mode 100644 index d2a73c4..0000000 --- a/resource/maps/9-2.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 121.02095752271309, - 358.42678681663807 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 434.59467736551346, - 464.1086892114048 - ], - "next": [ - "D", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 509.2799785457534, - 285.3249368864698 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 404.579999580791, - 134.7060270231522 - ], - "next": [ - "E", - "F" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 734.54300882493, - 614.6628871593595 - ], - "next": [ - "G", - "H" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 885.0872127978804, - 389.68678997175164 - ], - "next": [ - "H", - "K" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 780.2490881801388, - 149.16879346914533 - ], - "next": [ - "I" - ] - }, - "G": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 885.5236576413212, - 914.4418235529854 - ], - "next": [ - "H", - "L" - ] - }, - "H": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1125.8634960198206, - 630.3250793082143 - ], - "next": [ - "M" - ] - }, - "I": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1093.7548110107548, - 194.43679380052555 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1514.9724152190875, - 209.2687226041109 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1381.1654037438882, - 406.0817688052861 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1245.0091603037058, - 989.8325294993809 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1364.427555156964, - 870.1619710260555 - ], - "next": [ - "N", - "O" - ] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1769.9721023868035, - 960.051296249409 - ], - "next": [] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1679.522744622057, - 600.1565842499357 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/9-3.json b/resource/maps/9-3.json deleted file mode 100644 index 4c0d192..0000000 --- a/resource/maps/9-3.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 660.7576451855128, - 239.2546903431207 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Aerial", - "detour": false, - "night": false, - "position": [ - 990.3928738208414, - 149.92066557993223 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 856.4146552369215, - 570.2257234052728 - ], - "next": [ - "H", - "I" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1124.771848528775, - 360.8990699770466 - ], - "next": [ - "I", - "K" - ] - }, - "D": { - "type": "Aerial", - "detour": false, - "night": false, - "position": [ - 1320.0909757770173, - 165.15858724921964 - ], - "next": [ - "E", - "F", - "G" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1738.5166872107109, - 165.21145382775268 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1664.2945958227717, - 299.89221727902594 - ], - "next": [ - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1486.5396544787404, - 434.1722973772852 - ], - "next": [ - "J", - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 524.9228164287343, - 734.6789237493449 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1184.9531468100024, - 690.6064326047033 - ], - "next": [ - "N" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1830.0256687837914, - 629.4570843999924 - ], - "next": [ - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1425.9470431410734, - 751.7609990266517 - ], - "next": [ - "M", - "N" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1693.9113475704348, - 884.6267005367259 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1290.3172509026801, - 1019.4507322735534 - ], - "next": [] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1065.072756953479, - 960.129559463695 - ], - "next": [ - "O" - ] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 614.385228760604, - 915.8265664166115 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/9-4.json b/resource/maps/9-4.json deleted file mode 100644 index 413ad55..0000000 --- a/resource/maps/9-4.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1695.239775149028, - 164.93857123623255 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1576.386302763074, - 448.7062201387248 - ], - "next": [ - "C", - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1319.7485822753572, - 256.2149034642672 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1604.396596334289, - 766.1229941836837 - ], - "next": [ - "F", - "G" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1290.9047962751902, - 584.1471640054696 - ], - "next": [ - "G" - ] - }, - "E": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1096.798658222215, - 450.4094019431119 - ], - "next": [ - "H", - "I", - "J" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1318.280615740547, - 944.4883470529517 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1079.5092071168226, - 883.2660574481055 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 930.0705805640358, - 736.839955602217 - ], - "next": [ - "K", - "L" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 766.8342435181376, - 510.7820712229965 - ], - "next": [ - "L", - "M" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 781.6164081615252, - 390.6386559194377 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 734.5373253990392, - 990.6341433699519 - ], - "next": [ - "N", - "O" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 539.9592937457595, - 675.3230408793498 - ], - "next": [ - "O" - ] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 388.1440149767701, - 450.07031023936287 - ], - "next": [] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 329.7189794796564, - 945.2477093445608 - ], - "next": [] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 194.87736291301488, - 824.7830117289966 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/9-5.json b/resource/maps/9-5.json deleted file mode 100644 index 1a27bda..0000000 --- a/resource/maps/9-5.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1770.2930224672878, - 569.8795318850564 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1454.5861492323584, - 254.74971550086642 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1409.3438116948907, - 465.0646215943268 - ], - "next": [ - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1349.4664987289234, - 705.5167876383987 - ], - "next": [ - "G", - "H" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 1051.1842987394054, - 179.47737549987144 - ], - "next": [ - "I" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1079.4959459073477, - 449.97473534999216 - ], - "next": [ - "J" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1109.8371943449208, - 690.005124916067 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1485.0789298632485, - 1005.0279644174692 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1065.3919991196224, - 930.3721323142764 - ], - "next": [ - "N" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 673.6215006301846, - 240.25218564493238 - ], - "next": [ - "J", - "L", - "M" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 796.8461197219264, - 494.709096436412 - ], - "next": [ - "K", - "O" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 734.1750007336182, - 808.8213521036603 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 405.75338091632136, - 150.27094046991726 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 405.12398652756025, - 405.02101285174444 - ], - "next": [ - "O" - ] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 706.2352716686916, - 988.7824990258182 - ], - "next": [ - "O" - ] - }, - "O": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 361.0780174079276, - 841.1661978595665 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-1.json b/resource/maps/Ex-1.json deleted file mode 100644 index 1aa2036..0000000 --- a/resource/maps/Ex-1.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 163.95589290383825, - 299.1690571374928 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 149.83002530329165, - 765.0275068486372 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 405.24113107923415, - 556.2626919525313 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 585.8392746170606, - 211.51128144486478 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 675.5901032206158, - 885.2923713667752 - ], - "next": [ - "H", - "I" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 764.2757293341205, - 480.6441917903931 - ], - "next": [ - "F", - "I" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1081.0082221584596, - 298.94647709062315 - ], - "next": [ - "G", - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1499.5709485472735, - 270.54031855642273 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1140.2655505588518, - 884.3043056261058 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1065.877047784646, - 645.5604973452923 - ], - "next": [ - "J", - "L" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1470.0978912687751, - 539.8168227175515 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1756.8081538933466, - 360.4792573370694 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1650.9595323050987, - 899.2749169012935 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-10.json b/resource/maps/Ex-10.json deleted file mode 100644 index 86505e9..0000000 --- a/resource/maps/Ex-10.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1649.0457597321986, - 524.7526835534575 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1184.5683277541402, - 406.20614389766087 - ], - "next": [ - "D", - "F" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1304.147155629061, - 675.8959557424067 - ], - "next": [ - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1469.556979817755, - 840.5200942354624 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1020.2320657649688, - 164.17396330020452 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 915.6083596118168, - 885.7188329255903 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 869.2618704095887, - 658.7758597844112 - ], - "next": [ - "K", - "L" - ] - }, - "G": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1126.3639423687682, - 1004.0852356857512 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 689.3070696921296, - 1003.2266550630967 - ], - "next": [ - "I", - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 193.1768370431478, - 749.5498890386579 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 240.02248238649418, - 313.384010370797 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 464.81779756652634, - 704.9649283174117 - ], - "next": [ - "L" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 571.866422098502, - 419.4150361771711 - ], - "next": [ - "M", - "N" - ] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 570.5887686395761, - 136.5810641316191 - ], - "next": [] - }, - "N": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 824.5974732402271, - 149.625269621838 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-11.json b/resource/maps/Ex-11.json deleted file mode 100644 index 6bd2f16..0000000 --- a/resource/maps/Ex-11.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 133.28891647634202, - 824.0463768810149 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 208.74556097275794, - 375.30928538420187 - ], - "next": [ - "D", - "G" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 480.12716931295915, - 794.209635898714 - ], - "next": [ - "H" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 480.9423444474651, - 1003.4884347578522 - ], - "next": [ - "I" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 555.6155402692343, - 180.54984172202654 - ], - "next": [ - "E", - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 989.9982337113403, - 149.99988769562972 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 959.5331577134199, - 330.53403050739814 - ], - "next": [ - "L", - "M" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 466.3383216370636, - 556.1538543472059 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 809.1659049401313, - 646.1684976977015 - ], - "next": [ - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 900.3507260840993, - 975.0575891178495 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1274.9254799037662, - 975.3216023091811 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1244.6785747244833, - 658.7101089842016 - ], - "next": [ - "N", - "P" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1320.0076071607027, - 509.93296512332796 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1379.9273999852262, - 315.041061332535 - ], - "next": [ - "N" - ] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1679.1969938469608, - 600.5760014550575 - ], - "next": [ - "O", - "P" - ] - }, - "O": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1786.1266016368322, - 283.90000234903437 - ], - "next": [] - }, - "P": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1576.497469468441, - 974.9299467954314 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-12.json b/resource/maps/Ex-12.json deleted file mode 100644 index 3f157bd..0000000 --- a/resource/maps/Ex-12.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 135.42804649022426, - 915.2294799683676 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 313.97809579935904, - 613.6002133242896 - ], - "next": [ - "C", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 510.659402572687, - 975.2058784704699 - ], - "next": [ - "F", - "G" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 420.0043294095158, - 225.2518243813738 - ], - "next": [ - "D", - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 825.5822669436692, - 149.4171627947026 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 736.6492113176779, - 480.80118399116304 - ], - "next": [ - "H", - "I", - "J" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 750.608730851684, - 720.80566518116 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 854.9169903557786, - 944.9276316097074 - ], - "next": [ - "N", - "O" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1109.9421812295427, - 660.0301177221007 - ], - "next": [ - "J", - "O" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1005.192479274899, - 210.5498625505018 - ], - "next": [ - "K" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1230.3598338506404, - 389.6207884249215 - ], - "next": [ - "L", - "Q" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1365.4027811581134, - 104.95960553432654 - ], - "next": [ - "M", - "Q" - ] - }, - "L": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1544.986288433945, - 316.0716286673489 - ], - "next": [] - }, - "M": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1740.7791990659712, - 134.9056985199999 - ], - "next": [] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1200.7424862435457, - 855.49927690382 - ], - "next": [ - "O" - ] - }, - "O": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1500.3783411961047, - 718.1138210537109 - ], - "next": [ - "P", - "Q" - ] - }, - "P": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1799.6561429424073, - 885.5667284178029 - ], - "next": [] - }, - "Q": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1799.8251971267714, - 480.079981080063 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-2.json b/resource/maps/Ex-2.json deleted file mode 100644 index 38c3dad..0000000 --- a/resource/maps/Ex-2.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 331.4058809016384, - 914.9085075317213 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 719.7090933733128, - 809.4235726152896 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 434.5417773044059, - 540.0178957407362 - ], - "next": [ - "D", - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1109.9797957722753, - 840.01107136416 - ], - "next": [ - "G", - "J" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 224.55204610775328, - 299.7601953576241 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 869.9499709851609, - 554.8778702090312 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 870.2574761423414, - 225.0284938294855 - ], - "next": [ - "L" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1500.7932742954767, - 974.9390708045712 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 1185.3254543142862, - 675.1935555713669 - ], - "next": [ - "I", - "J" - ] - }, - "I": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1036.1702703474014, - 360.19739219008807 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1544.6653272473934, - 808.878636327567 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1771.037208782198, - 584.7710435615191 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1410.6512237624622, - 285.4817831140915 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-3.json b/resource/maps/Ex-3.json deleted file mode 100644 index c3257b7..0000000 --- a/resource/maps/Ex-3.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 150.22681758982694, - 315.9513841771045 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 448.84332014679956, - 540.0623787437706 - ], - "next": [ - "C", - "D", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 541.4155938100058, - 271.2134346969862 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 554.9779583087848, - 840.0147448372927 - ], - "next": [] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 810.1871932395695, - 689.437558700184 - ], - "next": [ - "J" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 945.4018628841138, - 421.50130803997786 - ], - "next": [ - "F", - "G", - "H" - ] - }, - "F": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1168.8619214487194, - 194.4897088138715 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1125.71767918974, - 691.8579065906825 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1380.0120926749846, - 436.6434818676804 - ], - "next": [ - "I", - "M" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1559.8615971504407, - 180.10192840724835 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1171.3226809651453, - 990.5403538449083 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1470.2930905105973, - 764.1497404255045 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1559.6434829751813, - 974.9773852494012 - ], - "next": [] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1799.4718303692105, - 570.681925086532 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-4.json b/resource/maps/Ex-4.json deleted file mode 100644 index 97260c9..0000000 --- a/resource/maps/Ex-4.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 1651.4853686278989, - 524.4197920600761 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1184.3339495673658, - 404.2179583096678 - ], - "next": [ - "D", - "F" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1306.2526236569033, - 676.2604979505247 - ], - "next": [ - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1469.7286492264618, - 840.8773877572071 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1019.5890650171965, - 163.08438056090753 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 914.5765640803512, - 886.1741627357509 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 870.0010820970742, - 659.9991913368538 - ], - "next": [ - "K", - "L" - ] - }, - "G": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 1124.8598331375904, - 1005.2147059326144 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 688.8258948910935, - 1004.9604841191086 - ], - "next": [ - "I", - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 195.55533118832224, - 748.51642214519 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 240.04881670338017, - 315.5346881684002 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 464.9393316542726, - 705.360609039038 - ], - "next": [ - "L" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 569.303153492812, - 421.7682590175781 - ], - "next": [ - "M", - "N" - ] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 570.3652212000602, - 135.17823335252058 - ], - "next": [] - }, - "N": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 825.5023103136546, - 151.77854314777386 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-5.json b/resource/maps/Ex-5.json deleted file mode 100644 index 89ea4b7..0000000 --- a/resource/maps/Ex-5.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 135.63293158882152, - 823.8660998562602 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 210.12299364220033, - 374.3323813028563 - ], - "next": [ - "D", - "G" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 479.7415077662089, - 796.2167669538295 - ], - "next": [ - "H" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 479.25001398115285, - 1004.0101986658223 - ], - "next": [ - "I" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 554.0261652550703, - 181.36467445259706 - ], - "next": [ - "E", - "F" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 991.4392532514989, - 149.61388173829087 - ], - "next": [] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 960.0529215443987, - 328.19917350690844 - ], - "next": [ - "L", - "M" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 465.1185745043285, - 555.2007583512024 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 811.2284476992019, - 646.0983486833037 - ], - "next": [ - "K" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 900.1194043993605, - 974.1459299966069 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1273.0998389818437, - 974.4041322225021 - ], - "next": [] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1245.2799164959163, - 658.8464621682203 - ], - "next": [ - "N", - "P" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1319.6516395538197, - 510.14178732542973 - ], - "next": [] - }, - "M": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1379.8878695824112, - 314.6254396843998 - ], - "next": [ - "N" - ] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1679.8970348286966, - 599.8759787470746 - ], - "next": [ - "O", - "P" - ] - }, - "O": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1784.959852164058, - 284.8788701249006 - ], - "next": [] - }, - "P": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1574.4549493715397, - 973.3156311285851 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-6.json b/resource/maps/Ex-6.json deleted file mode 100644 index a256973..0000000 --- a/resource/maps/Ex-6.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 134.0566997285202, - 914.6218978768547 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 316.0326296248211, - 614.7942518722775 - ], - "next": [ - "C", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 509.99469798737925, - 974.7960680715454 - ], - "next": [ - "F", - "G" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 421.98403948767395, - 224.80643745301273 - ], - "next": [ - "D", - "E" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 825.1781256392709, - 150.02197194311202 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 736.4883486359753, - 479.5273318547455 - ], - "next": [ - "H", - "I", - "J" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 750.8697676016286, - 718.6356727125212 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 853.1211040213609, - 944.4116177449254 - ], - "next": [ - "N", - "O" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1109.0020371391208, - 659.7270825144511 - ], - "next": [ - "J", - "O" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1004.3523875705189, - 210.20026646027802 - ], - "next": [ - "K" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1229.9171315157362, - 390.2478375573642 - ], - "next": [ - "L", - "Q" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1365.6677189089453, - 104.50104110488918 - ], - "next": [ - "M", - "Q" - ] - }, - "L": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1545.0586087191903, - 315.0439457670568 - ], - "next": [] - }, - "M": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1740.219584762278, - 135.23120361569775 - ], - "next": [] - }, - "N": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1200.5632657779354, - 855.6252193443827 - ], - "next": [ - "O" - ] - }, - "O": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1499.8297963989432, - 721.446156314519 - ], - "next": [ - "P", - "Q" - ] - }, - "P": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1800.4747046606258, - 885.1918621401311 - ], - "next": [] - }, - "Q": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1799.9840513048937, - 479.4683247745689 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-7.json b/resource/maps/Ex-7.json deleted file mode 100644 index 72c2cd3..0000000 --- a/resource/maps/Ex-7.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 165.61602013971384, - 299.488643442369 - ], - "next": [ - "A", - "B", - "C" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 148.56494137414262, - 763.9841572604365 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 403.9096611434494, - 555.400492641525 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 584.9890332461449, - 209.60417228471994 - ], - "next": [ - "F" - ] - }, - "D": { - "type": "Normal", - "detour": true, - "night": false, - "position": [ - 675.1092392345489, - 885.0768152490803 - ], - "next": [ - "H", - "I" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 764.2893955765068, - 480.4214853334314 - ], - "next": [ - "F", - "I" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1080.1888678434207, - 300.23720236903637 - ], - "next": [ - "G", - "J" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1500.0809216108255, - 269.87983560292724 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1141.4533994209498, - 884.2204149714955 - ], - "next": [] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1064.8999665581014, - 644.4687709853258 - ], - "next": [ - "J", - "L" - ] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1470.579011174874, - 540.9630518430936 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1755.4223674030598, - 359.63818498401326 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1650.9098449132161, - 898.6668038462207 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-8.json b/resource/maps/Ex-8.json deleted file mode 100644 index 1d9a945..0000000 --- a/resource/maps/Ex-8.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 328.1411310932095, - 915.0233014727328 - ], - "next": [ - "A" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 719.659923183686, - 809.6173837124036 - ], - "next": [ - "B", - "C" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 436.3012710444643, - 539.5059614259573 - ], - "next": [ - "D", - "E", - "F" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1109.0945243178362, - 840.8769911553431 - ], - "next": [ - "G", - "J" - ] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 223.84895668314775, - 299.342230342605 - ], - "next": [] - }, - "E": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 869.9699340918113, - 554.167825788809 - ], - "next": [ - "H" - ] - }, - "F": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 870.307282542177, - 226.6863248139185 - ], - "next": [ - "L" - ] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1498.7537178981886, - 975.149290298698 - ], - "next": [] - }, - "H": { - "type": "Normal", - "detour": false, - "night": true, - "position": [ - 1183.939257743899, - 673.5323944363596 - ], - "next": [ - "I", - "J" - ] - }, - "I": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1034.6854516854182, - 360.13743724143905 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1544.1201832673703, - 809.6052160271802 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1769.1159391968586, - 583.5224672532704 - ], - "next": [] - }, - "L": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1410.942263038662, - 285.17759978960424 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-9.json b/resource/maps/Ex-9.json deleted file mode 100644 index faf4a56..0000000 --- a/resource/maps/Ex-9.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [ - 148.86266172683312, - 314.4570390379954 - ], - "next": [ - "A", - "B" - ] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 449.0135842651083, - 539.9006319918567 - ], - "next": [ - "C", - "D", - "E" - ] - }, - "B": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 539.104860484191, - 270.267266014338 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 555.4776682885491, - 841.5340044769132 - ], - "next": [] - }, - "D": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 810.2223325935295, - 690.7289334618571 - ], - "next": [ - "J" - ] - }, - "E": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 944.7784655615579, - 420.136441400125 - ], - "next": [ - "F", - "G", - "H" - ] - }, - "F": { - "type": "Suppress", - "detour": false, - "night": false, - "position": [ - 1169.790338365201, - 196.91103014249023 - ], - "next": [] - }, - "G": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1125.6594170592823, - 689.8870672710001 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1380.3483944970615, - 434.1178054969663 - ], - "next": [ - "I", - "M" - ] - }, - "I": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1558.4851862782455, - 178.9993888734879 - ], - "next": [] - }, - "J": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1170.180015737722, - 990.4483105090378 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1468.5175583439407, - 765.8333594432995 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "Normal", - "detour": false, - "night": false, - "position": [ - 1560.7855428759221, - 974.5272275846231 - ], - "next": [] - }, - "M": { - "type": "Boss", - "detour": false, - "night": false, - "position": [ - 1798.2684903132035, - 570.6270735621033 - ], - "next": [] - } -} \ No newline at end of file diff --git a/resource/maps/Ex-99.json b/resource/maps/Ex-99.json deleted file mode 100644 index c4575e3..0000000 --- a/resource/maps/Ex-99.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "0": { - "type": "Start", - "detour": false, - "night": false, - "position": [100, 400], - "next": ["A", "B"] - }, - "A": { - "type": "Normal", - "detour": false, - "night": false, - "position": [300, 250], - "next": ["C", "D"] - }, - "B": { - "type": "Normal", - "detour": true, - "night": false, - "position": [300, 550], - "next": ["D"] - }, - "C": { - "type": "Resource", - "detour": false, - "night": false, - "position": [500, 150], - "next": ["E"] - }, - "D": { - "type": "Penalty", - "detour": false, - "night": false, - "position": [500, 400], - "next": ["E", "F"] - }, - "E": { - "type": "Aerial", - "detour": false, - "night": false, - "position": [700, 200], - "next": ["G"] - }, - "F": { - "type": "Suppress", - "detour": false, - "night": true, - "position": [700, 500], - "next": ["G", "H"] - }, - "G": { - "type": "Hard", - "detour": false, - "night": true, - "position": [900, 300], - "next": ["H"] - }, - "H": { - "type": "Boss", - "detour": false, - "night": false, - "position": [1100, 400], - "next": [] - } -} diff --git a/resource/maps/decisive_battle/enemy_spec.yaml b/resource/maps/decisive_battle/enemy_spec.yaml new file mode 100644 index 0000000..4801bc6 --- /dev/null +++ b/resource/maps/decisive_battle/enemy_spec.yaml @@ -0,0 +1,135 @@ +key_points: + 1: + - "" + 2: + - + 3: + - "" + 4: + - "" + - "CFH" + - "BFH" + - "DHJ" + 5: + - "" + - "DFH" + - "DGJ" + - "CGJ" + 6: + - "" + - "BGJ" + - "CHJ" + - "DGJ" +map_end: + - "" + - " FHH" + - " FHH" + - " HHJ" + - " HHJ" + - " HJJ" + - " JJJ" +enemy: + - "" + - "" + - "" + - "" + - + - "" + - + A: ['', 'CL', 'CL', 'DD'] + B: ['', 'CA', 'CA', 'DD', 'DD'] + C: ['', "CLT", "CLT", "CL", "CL", "SS"] + D: ['', 'BC', 'CVL', 'CVL', 'CL', 'DD'] + E: ['', 'BB', 'CA', 'CL', 'CVL', 'CL', 'DD'] + F: ['', 'CV', 'BC', 'CL', 'DD', 'DD', 'AADG'] + G: ['', 'BC', 'BG', 'CA', 'CA', 'CL', 'DD'] + H: ['', 'CV', 'CV', 'CVL', 'CL', 'BBV', 'BBV'] + - + A: ['', 'BC', 'CV', 'CV', 'CL', 'DD', 'DD'] + B: ['', 'BB', 'BC', 'BC', 'CA', 'DD', 'SS'] + C: ['', 'CV', 'CV', 'BB', 'CLT', 'CL', 'CL'] + D: ['', 'BB', 'CV', 'BG', 'CA', 'DD', 'AADG'] + E: ['', 'BC', 'BC', 'CV', 'CL', 'CL', 'ASDG'] + F: ['', 'BB', 'BB', 'BB', 'CA', 'DD', 'SS'] + G: ['', 'BB', 'BB', 'CV', 'CVL', 'CL', 'CL'] + H: ['', 'AF', 'BC', 'BB', 'BB', 'BB', 'CA'] + - + A: ['', 'BB', 'BC', 'BB', 'CA', 'SS', 'SS'] + B: ['', 'CV', 'BC', 'BC', 'CVL', 'DD', 'ASDG'] + C: ['', 'CV', 'CV', 'BB', 'BC', 'DD', 'DD'] + D: ['', 'BB', 'CV', 'BC', 'CA', 'CL', 'BG'] + E: ['', 'CV', 'BC', 'BC', 'CA', 'SS', 'CL'] + F: ['', 'BB', 'BB', 'BC', 'BC', 'CL', 'CL'] + G: ['', 'BB', 'BB', 'CA', 'CA', 'CA'] + H: ['', 'BB', 'CV', 'BB', 'BC', 'CL', 'SS'] + I: ['', 'BB', 'BBG', 'CV', 'CA', 'CA', 'CL'] + J: ['', 'BB', 'CA', 'BBV', 'BBV', 'SS', 'BC'] + + - + - "" + - + A: ['', 'DD', 'SS', '', '', '', ''] + B: ['', 'CA', 'CL', 'AADG', '', '', ''] + C: ['', 'BB', 'CA', 'DD', '', '', ''] + D: ['', 'BC', 'CA', 'SS', 'SS', '', ''] + E: ['', 'BB', 'CVL', 'CVL', 'DD', 'DD', ''] + F: ['', 'CV', 'CV', 'CL', 'CL', 'DD', 'DD'] + G: ['', 'BC', 'CVL', 'CA', 'CL', 'DD', 'AADG'] + H: ['', 'AV', 'CV', 'CV', 'BB', 'DD', 'BBV'] + - + A: ['', 'BB', 'CV', 'CV', 'CVL', 'CL', 'CL'] + B: ['', 'CV', 'BB', 'BB', 'CA', 'CL', 'CL'] + C: ['', 'BC', 'BB', 'BB', 'CA', 'CL', 'SS'] + D: ['', 'BB', 'BB', 'BC', 'SS', 'SS', 'SS'] + E: ['', 'BB', 'BB', 'BB', 'CL', 'CL', 'SS'] + F: ['', 'BC', 'BC', 'BB', 'CA', 'CA', 'AADG'] + G: ['', 'BC', 'CV', 'BB', 'BG', 'CL', 'CL'] + H: ['', 'BB', 'BB', 'BB', 'ASDG', 'CL', 'CL'] + I: ['', 'CV', 'BB', 'BB', 'CL', 'AADG', 'AADG'] + J: ['', 'AV', 'AF', 'BC', 'BB', 'CLT', 'CLT'] + - + A: ['', 'CV', 'CV', 'BC', 'CA', 'CL', 'SS'] + B: ['', 'BB', 'BB', 'BC', 'CL', 'CL', 'ASDG'] + C: ['', 'BB', 'BBG', 'BC', 'BC', 'CL', 'CL'] + D: ['', 'BB', 'BC', 'BC', 'CA', 'CA', 'SS'] + E: ['', 'BB', 'BB', 'CVL', 'CVL', 'CL', 'AADG'] + F: ['', 'CV', 'CVL', 'BB', 'BC', 'CA', 'CL'] + G: ['', 'CV', 'CV', 'BB', 'BB', 'CA', 'CA'] + H: ['', 'CV', 'BC', 'BB', 'BC', 'CA', 'CL'] + I: ['', 'BB', 'BB', 'BB', 'BB', 'CL', 'CL'] + J: ['', 'BB', 'BC', 'BC', 'CA', 'CA', 'DD'] + + - + - "" + - A : ['', 'CL', 'CL'] + B : ['', 'CLT', 'SS', 'SS'] + C : ['', 'CA', 'CA', 'CA', 'CA'] + D : ['', 'BC', 'CA', 'CA', 'CL', 'DD', 'AADG'] + E : ['', 'CV', 'BC', 'CA', 'CA', 'DD', 'DD'] + F : ['', 'CVL', 'BC', 'BC', 'CL', 'DD', 'SS'] + G : ['', 'BB', 'BB', 'CA', 'CL', 'CL', 'AADG'] + H : ['', 'BB', 'BB', 'BC', 'CVL', 'CL', 'DD'] + I : ['', 'BC', 'BB', 'CV', 'CVL', 'CL', 'DD'] + J : ['', 'AF', 'BB', 'DD', 'BB', 'BC', 'AV'] + - + A: ["", BB, BB, BC, CVL, CL, CL] + B: ["", CV, BC, BB, CA, CA, CL] + C: ["", BC, BB, BB, BC, CA, SS] + D: ["", CV, CV, BB, CA, CL, SS] + E: ["", BC, BC, CV, CV, CL, CL] + F: ["", BB, BB, BB, CA, CA, AADG] + G: ["", CV, BBG, CV, CA, CA, CA] + H: ["", BB, BB, CV, BC, CA, ASDG] + I: ["", CV, CVL, BB, BB, CA, SS] + J: ["", AV, CV, CV, AF, CVL, DD] + - + A: ["", CV, BC, BB, BB, CL, AADG] + B: ["", BB, BC, BB, BB, CA, SS] + C: ["", BB, BC, BB, BG, BG, CL] + D: ["", BB, BC, BB, CA, ASDG, CL] + E: ["", CV, BB, CV, BB, CL, CL] + F: ["", BB, BC, BB, BB, CA, CA] + G: ["", BB, CV, BC, BB, SS, SS] + H: ["", CV, BB, BB, BC, CL, AADG] + I: ["", CV, CV, BB, BB, CL, BBG] + J: ["", BB, BB, CA, BBV, BBV, BC] diff --git a/resource/maps/event/20220928/E-1.json b/resource/maps/event/20220928/E-1.json new file mode 100644 index 0000000..8b0a98c --- /dev/null +++ b/resource/maps/event/20220928/E-1.json @@ -0,0 +1,112 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 98 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 163 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 186, + 317 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 136 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 488, + 234 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 328 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 360, + 426 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 812, + 167 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 256 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 539, + 366 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 452 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/E-2.json b/resource/maps/event/20220928/E-2.json new file mode 100644 index 0000000..f1492fd --- /dev/null +++ b/resource/maps/event/20220928/E-2.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 84 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 181, + 258 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 665, + 113 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 135 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 314, + 222 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 372 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 411, + 338 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 635, + 220 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 841, + 193 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 482 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 549, + 404 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 741, + 374 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/E-3.json b/resource/maps/event/20220928/E-3.json new file mode 100644 index 0000000..f98dac0 --- /dev/null +++ b/resource/maps/event/20220928/E-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 444 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 791, + 324 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 538, + 405 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 188 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 284 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 395 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 278, + 459 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 841, + 91 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 556, + 118 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 269 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 288, + 321 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 339 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 162, + 184 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 321, + 151 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/E-4.json b/resource/maps/event/20220928/E-4.json new file mode 100644 index 0000000..ebe23b4 --- /dev/null +++ b/resource/maps/event/20220928/E-4.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 226, + 193 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 202 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 584, + 82 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 186 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 74, + 329 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 375 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 335 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 436, + 327 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 744, + 143 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 886, + 256 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 322 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 306 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 481 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 530, + 478 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/E-5.json b/resource/maps/event/20220928/E-5.json new file mode 100644 index 0000000..4d8f179 --- /dev/null +++ b/resource/maps/event/20220928/E-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 388, + 506 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 460 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 467 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 332 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 140, + 261 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 275, + 216 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 210 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 557, + 343 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 142 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 80, + 184 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 126 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 629, + 171 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 73 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 700, + 436 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 874, + 436 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 802, + 278 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/E-6.json b/resource/maps/event/20220928/E-6.json new file mode 100644 index 0000000..1c46540 --- /dev/null +++ b/resource/maps/event/20220928/E-6.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 530, + 106 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 709, + 94 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 691, + 195 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 253 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 196 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 767, + 352 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 144 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 261, + 293 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 73, + 297 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 68, + 459 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 308 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 366 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 423 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 626, + 492 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 453, + 442 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 235, + 491 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 224, + 382 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/H-1.json b/resource/maps/event/20220928/H-1.json new file mode 100644 index 0000000..8b0a98c --- /dev/null +++ b/resource/maps/event/20220928/H-1.json @@ -0,0 +1,112 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 98 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 163 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 186, + 317 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 136 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 488, + 234 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 328 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 360, + 426 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 812, + 167 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 256 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 539, + 366 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 452 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/H-2.json b/resource/maps/event/20220928/H-2.json new file mode 100644 index 0000000..f1492fd --- /dev/null +++ b/resource/maps/event/20220928/H-2.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 84 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 181, + 258 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 665, + 113 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 135 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 314, + 222 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 372 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 411, + 338 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 635, + 220 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 841, + 193 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 482 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 549, + 404 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 741, + 374 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/H-3.json b/resource/maps/event/20220928/H-3.json new file mode 100644 index 0000000..f98dac0 --- /dev/null +++ b/resource/maps/event/20220928/H-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 444 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 791, + 324 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 538, + 405 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 188 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 284 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 395 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 278, + 459 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 841, + 91 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 556, + 118 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 269 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 288, + 321 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 339 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 162, + 184 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 321, + 151 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/H-4.json b/resource/maps/event/20220928/H-4.json new file mode 100644 index 0000000..ebe23b4 --- /dev/null +++ b/resource/maps/event/20220928/H-4.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 226, + 193 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 202 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 584, + 82 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 186 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 74, + 329 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 375 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 335 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 436, + 327 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 744, + 143 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 886, + 256 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 322 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 306 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 481 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 530, + 478 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/H-5.json b/resource/maps/event/20220928/H-5.json new file mode 100644 index 0000000..4d8f179 --- /dev/null +++ b/resource/maps/event/20220928/H-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 388, + 506 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 460 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 467 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 332 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 140, + 261 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 275, + 216 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 210 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 557, + 343 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 142 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 80, + 184 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 126 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 629, + 171 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 73 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 700, + 436 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 874, + 436 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 802, + 278 + ], + "next": [] + } +} diff --git a/resource/maps/event/20220928/H-6.json b/resource/maps/event/20220928/H-6.json new file mode 100644 index 0000000..1c46540 --- /dev/null +++ b/resource/maps/event/20220928/H-6.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 530, + 106 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 709, + 94 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 691, + 195 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 253 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 196 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 767, + 352 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 144 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 261, + 293 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 73, + 297 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 68, + 459 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 308 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 366 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 423 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 626, + 492 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 453, + 442 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 235, + 491 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 224, + 382 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/E-1.json b/resource/maps/event/20221118/E-1.json new file mode 100644 index 0000000..6f56a05 --- /dev/null +++ b/resource/maps/event/20221118/E-1.json @@ -0,0 +1,112 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 81 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 248 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 185, + 314 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 156 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 96, + 443 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 405 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 337 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 711, + 239 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 309 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 502 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 713, + 427 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/E-2.json b/resource/maps/event/20221118/E-2.json new file mode 100644 index 0000000..1ebab03 --- /dev/null +++ b/resource/maps/event/20221118/E-2.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 119 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 300 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 143 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 295 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 171, + 441 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 689, + 140 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 523, + 309 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 384, + 479 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 826, + 187 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 863, + 338 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 574, + 474 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 781, + 476 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/E-3.json b/resource/maps/event/20221118/E-3.json new file mode 100644 index 0000000..36d7cbe --- /dev/null +++ b/resource/maps/event/20221118/E-3.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 374 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 222 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 121 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 405 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 336 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 471 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 549, + 223 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 576, + 75 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 158 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 534, + 511 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 593, + 434 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 719, + 308 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 464 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/E-4.json b/resource/maps/event/20221118/E-4.json new file mode 100644 index 0000000..4c38466 --- /dev/null +++ b/resource/maps/event/20221118/E-4.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 69 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 186 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 369, + 254 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 481, + 142 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 112 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 254 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 736, + 256 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 142, + 290 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 283, + 375 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 126, + 383 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 43, + 498 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 200, + 505 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 437 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 709, + 325 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 494 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 485 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/E-5.json b/resource/maps/event/20221118/E-5.json new file mode 100644 index 0000000..e06675d --- /dev/null +++ b/resource/maps/event/20221118/E-5.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 170, + 158 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 306 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 157, + 442 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 111 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 335, + 204 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 376 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 361, + 465 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 518, + 79 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 159 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 223 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 308 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 413 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 486 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 136 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 644, + 306 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 470 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 813, + 470 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 831, + 336 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/E-6.json b/resource/maps/event/20221118/E-6.json new file mode 100644 index 0000000..5ef6310 --- /dev/null +++ b/resource/maps/event/20221118/E-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 74 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 256, + 263 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 212, + 352 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 452, + 76 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 387, + 156 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 414, + 323 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 359, + 418 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 484 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 550, + 412 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 593, + 174 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 284 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 109 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 298 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 827, + 69 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 793, + 270 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 917, + 136 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 881, + 407 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 740, + 467 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/H-1.json b/resource/maps/event/20221118/H-1.json new file mode 100644 index 0000000..6f56a05 --- /dev/null +++ b/resource/maps/event/20221118/H-1.json @@ -0,0 +1,112 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 81 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 248 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 185, + 314 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 156 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 96, + 443 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 405 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 337 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 711, + 239 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 309 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 502 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 713, + 427 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/H-2.json b/resource/maps/event/20221118/H-2.json new file mode 100644 index 0000000..1ebab03 --- /dev/null +++ b/resource/maps/event/20221118/H-2.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 119 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 300 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 143 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 295 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 171, + 441 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 689, + 140 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 523, + 309 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 384, + 479 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 826, + 187 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 863, + 338 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 574, + 474 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 781, + 476 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/H-3.json b/resource/maps/event/20221118/H-3.json new file mode 100644 index 0000000..36d7cbe --- /dev/null +++ b/resource/maps/event/20221118/H-3.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 374 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 222 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 121 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 405 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 336 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 471 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 549, + 223 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 576, + 75 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 158 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 534, + 511 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 593, + 434 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 719, + 308 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 464 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/H-4.json b/resource/maps/event/20221118/H-4.json new file mode 100644 index 0000000..4c38466 --- /dev/null +++ b/resource/maps/event/20221118/H-4.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 69 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 186 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 369, + 254 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 481, + 142 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 112 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 254 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 736, + 256 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 142, + 290 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 283, + 375 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 126, + 383 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 43, + 498 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 200, + 505 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 437 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 709, + 325 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 494 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 485 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/H-5.json b/resource/maps/event/20221118/H-5.json new file mode 100644 index 0000000..e06675d --- /dev/null +++ b/resource/maps/event/20221118/H-5.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 170, + 158 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 306 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 157, + 442 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 111 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 335, + 204 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 376 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 361, + 465 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 518, + 79 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 159 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 223 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 308 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 413 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 486 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 136 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 644, + 306 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 470 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 813, + 470 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 831, + 336 + ], + "next": [] + } +} diff --git a/resource/maps/event/20221118/H-6.json b/resource/maps/event/20221118/H-6.json new file mode 100644 index 0000000..5ef6310 --- /dev/null +++ b/resource/maps/event/20221118/H-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 74 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 256, + 263 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 212, + 352 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 452, + 76 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 387, + 156 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 414, + 323 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 359, + 418 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 484 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 550, + 412 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 593, + 174 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 284 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 109 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 298 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 827, + 69 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 793, + 270 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 917, + 136 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 881, + 407 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 740, + 467 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/E-1.json b/resource/maps/event/20230117/E-1.json new file mode 100644 index 0000000..b2447c9 --- /dev/null +++ b/resource/maps/event/20230117/E-1.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 208, + 186 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 126, + 353 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 278 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 444 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 458, + 299 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 464, + 144 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 505 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 399 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 252 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 698, + 465 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 323 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 94 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/E-2.json b/resource/maps/event/20230117/E-2.json new file mode 100644 index 0000000..5efeaca --- /dev/null +++ b/resource/maps/event/20230117/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 89, + 230 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 188 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 354, + 96 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 223, + 299 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 260 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 537, + 150 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 416, + 359 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 633, + 296 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 402, + 501 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 768, + 132 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 217 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 285 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 710, + 471 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/E-3.json b/resource/maps/event/20230117/E-3.json new file mode 100644 index 0000000..4094eba --- /dev/null +++ b/resource/maps/event/20230117/E-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 359 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 481 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 344 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 219 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 83 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 494 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 366, + 184 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 148 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 330 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 699, + 215 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 793, + 150 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 556, + 485 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 329 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 421 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 749, + 497 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/E-4.json b/resource/maps/event/20230117/E-4.json new file mode 100644 index 0000000..2fbb897 --- /dev/null +++ b/resource/maps/event/20230117/E-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 235, + 137 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 118, + 231 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 223, + 390 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 57, + 472 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 384, + 493 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 314 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 208 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 497, + 380 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 572, + 155 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 385 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 353 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 707, + 248 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 909, + 309 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 591, + 79 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 854, + 93 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/E-5.json b/resource/maps/event/20230117/E-5.json new file mode 100644 index 0000000..2c90f1b --- /dev/null +++ b/resource/maps/event/20230117/E-5.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 125, + 284 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 234, + 356 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 333, + 486 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 130, + 103 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 75 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 194 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 436, + 236 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 621, + 255 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 383, + 318 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 546, + 479 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 552, + 364 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 670, + 472 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 747, + 260 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 920, + 304 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 149 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 787, + 342 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 863, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/E-6.json b/resource/maps/event/20230117/E-6.json new file mode 100644 index 0000000..aff3580 --- /dev/null +++ b/resource/maps/event/20230117/E-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 67, + 203 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 253, + 90 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 234, + 239 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 343 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 520 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 446 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 406, + 182 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 498, + 498 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 439, + 310 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 130 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 77 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 753, + 196 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 574, + 317 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 67 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 737, + 330 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 883, + 331 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 880, + 507 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 496 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/H-1.json b/resource/maps/event/20230117/H-1.json new file mode 100644 index 0000000..b2447c9 --- /dev/null +++ b/resource/maps/event/20230117/H-1.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 208, + 186 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 126, + 353 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 278 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 444 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 458, + 299 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 464, + 144 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 505 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 399 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 252 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 698, + 465 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 323 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 94 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/H-2.json b/resource/maps/event/20230117/H-2.json new file mode 100644 index 0000000..5efeaca --- /dev/null +++ b/resource/maps/event/20230117/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 89, + 230 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 188 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 354, + 96 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 223, + 299 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 260 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 537, + 150 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 416, + 359 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 633, + 296 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 402, + 501 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 768, + 132 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 217 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 285 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 710, + 471 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/H-3.json b/resource/maps/event/20230117/H-3.json new file mode 100644 index 0000000..4094eba --- /dev/null +++ b/resource/maps/event/20230117/H-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 359 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 481 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 344 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 219 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 83 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 494 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 366, + 184 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 148 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 330 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 699, + 215 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 793, + 150 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 556, + 485 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 329 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 421 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 749, + 497 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/H-4.json b/resource/maps/event/20230117/H-4.json new file mode 100644 index 0000000..2fbb897 --- /dev/null +++ b/resource/maps/event/20230117/H-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 235, + 137 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 118, + 231 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 223, + 390 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 57, + 472 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 384, + 493 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 314 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 208 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 497, + 380 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 572, + 155 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 385 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 353 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 707, + 248 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 909, + 309 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 591, + 79 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 854, + 93 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/H-5.json b/resource/maps/event/20230117/H-5.json new file mode 100644 index 0000000..2c90f1b --- /dev/null +++ b/resource/maps/event/20230117/H-5.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 125, + 284 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 234, + 356 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 333, + 486 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 130, + 103 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 75 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 194 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 436, + 236 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 621, + 255 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 383, + 318 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 546, + 479 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 552, + 364 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 670, + 472 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 747, + 260 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 920, + 304 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 149 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 787, + 342 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 863, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230117/H-6.json b/resource/maps/event/20230117/H-6.json new file mode 100644 index 0000000..aff3580 --- /dev/null +++ b/resource/maps/event/20230117/H-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 67, + 203 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 253, + 90 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 234, + 239 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 343 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 520 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 446 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 406, + 182 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 498, + 498 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 439, + 310 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 130 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 77 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 753, + 196 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 574, + 317 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 67 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 737, + 330 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 883, + 331 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 880, + 507 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 496 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/E-1.json b/resource/maps/event/20230613/E-1.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/resource/maps/event/20230613/E-1.json @@ -0,0 +1 @@ +{} diff --git a/resource/maps/event/20230613/E-2.json b/resource/maps/event/20230613/E-2.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/resource/maps/event/20230613/E-2.json @@ -0,0 +1 @@ +{} diff --git a/resource/maps/event/20230613/E-3.json b/resource/maps/event/20230613/E-3.json new file mode 100644 index 0000000..dc39932 --- /dev/null +++ b/resource/maps/event/20230613/E-3.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 232 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 326, + 333 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 230 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 471, + 336 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 287 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 418, + 80 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 202 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 519, + 97 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 739, + 388 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 447 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 706, + 477 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 489 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 802, + 279 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/E-4.json b/resource/maps/event/20230613/E-4.json new file mode 100644 index 0000000..a862f3d --- /dev/null +++ b/resource/maps/event/20230613/E-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 629, + 182 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 185 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 299 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 216, + 337 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 80, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 462 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 211, + 197 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 321, + 101 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 426, + 306 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 491, + 468 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 731, + 492 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 651, + 389 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 760, + 268 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 94 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 864, + 319 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/E-5.json b/resource/maps/event/20230613/E-5.json new file mode 100644 index 0000000..c59fa2b --- /dev/null +++ b/resource/maps/event/20230613/E-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 228 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 276 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 434, + 208 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 449, + 117 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 699, + 74 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 635, + 150 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 808, + 124 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 372 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 362, + 490 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 363 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 648, + 331 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 499, + 511 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 737, + 290 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 910, + 418 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 83, + 434 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 684, + 473 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/E-6.json b/resource/maps/event/20230613/E-6.json new file mode 100644 index 0000000..945f2b9 --- /dev/null +++ b/resource/maps/event/20230613/E-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 702, + 177 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 237 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 84 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 467, + 103 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 242, + 91 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 92, + 302 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 143, + 149 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 246, + 325 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 428, + 173 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 174 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 281 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 575, + 247 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 357 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 351, + 443 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 403 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 523, + 488 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 779, + 382 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 806, + 490 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/H-1.json b/resource/maps/event/20230613/H-1.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/resource/maps/event/20230613/H-1.json @@ -0,0 +1 @@ +{} diff --git a/resource/maps/event/20230613/H-2.json b/resource/maps/event/20230613/H-2.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/resource/maps/event/20230613/H-2.json @@ -0,0 +1 @@ +{} diff --git a/resource/maps/event/20230613/H-3.json b/resource/maps/event/20230613/H-3.json new file mode 100644 index 0000000..dc39932 --- /dev/null +++ b/resource/maps/event/20230613/H-3.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 232 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 326, + 333 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 230 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 471, + 336 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 287 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 418, + 80 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 202 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 519, + 97 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 739, + 388 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 447 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 706, + 477 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 489 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 802, + 279 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/H-4.json b/resource/maps/event/20230613/H-4.json new file mode 100644 index 0000000..a862f3d --- /dev/null +++ b/resource/maps/event/20230613/H-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 629, + 182 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 185 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 299 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 216, + 337 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 80, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 462 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 211, + 197 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 321, + 101 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 426, + 306 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 491, + 468 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 731, + 492 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 651, + 389 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 760, + 268 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 94 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 864, + 319 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/H-5.json b/resource/maps/event/20230613/H-5.json new file mode 100644 index 0000000..c59fa2b --- /dev/null +++ b/resource/maps/event/20230613/H-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 228 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 276 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 434, + 208 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 449, + 117 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 699, + 74 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 635, + 150 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 808, + 124 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 372 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 362, + 490 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 363 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 648, + 331 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 499, + 511 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 737, + 290 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 910, + 418 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 83, + 434 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 684, + 473 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230613/H-6.json b/resource/maps/event/20230613/H-6.json new file mode 100644 index 0000000..945f2b9 --- /dev/null +++ b/resource/maps/event/20230613/H-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 702, + 177 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 237 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 84 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 467, + 103 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 242, + 91 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 92, + 302 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 143, + 149 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 246, + 325 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 428, + 173 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 174 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 281 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 575, + 247 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 357 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 351, + 443 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 403 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 523, + 488 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 779, + 382 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 806, + 490 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/E-1.json b/resource/maps/event/20230809/E-1.json new file mode 100644 index 0000000..c6fae2b --- /dev/null +++ b/resource/maps/event/20230809/E-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 315 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 422 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 164 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 399, + 238 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 337 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 518, + 465 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 527, + 118 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 224 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 779, + 88 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 669, + 480 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 292 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 255 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 834, + 451 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/E-2.json b/resource/maps/event/20230809/E-2.json new file mode 100644 index 0000000..d3808b8 --- /dev/null +++ b/resource/maps/event/20230809/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 284 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 226, + 178 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 263, + 410 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 308 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 414, + 165 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 94 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 591, + 224 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 503, + 481 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 470, + 367 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 683, + 336 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 465 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 796, + 388 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 794, + 189 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/E-3.json b/resource/maps/event/20230809/E-3.json new file mode 100644 index 0000000..707da5f --- /dev/null +++ b/resource/maps/event/20230809/E-3.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 448 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 674, + 321 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 841, + 290 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 375 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 400, + 479 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 489, + 283 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 703, + 171 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 76 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 446, + 135 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 318, + 227 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 290, + 336 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 228, + 420 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 66, + 424 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 307 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 274, + 80 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 126 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/E-4.json b/resource/maps/event/20230809/E-4.json new file mode 100644 index 0000000..70c064b --- /dev/null +++ b/resource/maps/event/20230809/E-4.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 181, + 73 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 196 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 192, + 299 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 407, + 80 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 428, + 165 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 377, + 270 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 97 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 209 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 583, + 263 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 883, + 229 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 390 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 782, + 353 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 575, + 397 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 653, + 487 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 397 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 369 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 477, + 478 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 272, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/E-5.json b/resource/maps/event/20230809/E-5.json new file mode 100644 index 0000000..4faa4af --- /dev/null +++ b/resource/maps/event/20230809/E-5.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 54, + 268 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 146, + 262 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 203 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 134, + 404 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 473 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 351, + 357 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 398, + 240 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 384, + 119 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 554, + 186 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 338 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 496, + 427 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 194 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 871, + 130 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 810, + 322 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 698, + 275 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 907, + 431 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 505 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 462 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/E-6.json b/resource/maps/event/20230809/E-6.json new file mode 100644 index 0000000..2f32f81 --- /dev/null +++ b/resource/maps/event/20230809/E-6.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 112 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 158, + 204 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 61, + 253 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 481, + 88 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 196 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 299 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 174, + 374 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 661, + 157 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 558, + 208 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 429, + 369 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 505 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 413 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 678, + 332 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 580, + 348 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 496, + 499 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 839, + 233 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 315 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 893, + 405 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 462 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/H-1.json b/resource/maps/event/20230809/H-1.json new file mode 100644 index 0000000..c6fae2b --- /dev/null +++ b/resource/maps/event/20230809/H-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 315 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 422 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 164 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 399, + 238 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 337 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 518, + 465 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 527, + 118 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 224 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 779, + 88 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 669, + 480 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 292 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 255 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 834, + 451 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/H-2.json b/resource/maps/event/20230809/H-2.json new file mode 100644 index 0000000..d3808b8 --- /dev/null +++ b/resource/maps/event/20230809/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 284 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 226, + 178 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 263, + 410 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 308 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 414, + 165 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 94 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 591, + 224 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 503, + 481 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 470, + 367 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 683, + 336 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 465 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 796, + 388 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 794, + 189 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/H-3.json b/resource/maps/event/20230809/H-3.json new file mode 100644 index 0000000..707da5f --- /dev/null +++ b/resource/maps/event/20230809/H-3.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 448 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 674, + 321 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 841, + 290 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 375 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 400, + 479 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 489, + 283 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 703, + 171 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 76 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 446, + 135 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 318, + 227 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 290, + 336 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 228, + 420 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 66, + 424 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 307 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 274, + 80 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 126 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/H-4.json b/resource/maps/event/20230809/H-4.json new file mode 100644 index 0000000..70c064b --- /dev/null +++ b/resource/maps/event/20230809/H-4.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 181, + 73 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 196 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 192, + 299 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 407, + 80 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 428, + 165 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 377, + 270 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 97 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 209 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 583, + 263 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 883, + 229 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 390 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 782, + 353 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 575, + 397 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 653, + 487 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 397 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 369 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 477, + 478 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 272, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/H-5.json b/resource/maps/event/20230809/H-5.json new file mode 100644 index 0000000..4faa4af --- /dev/null +++ b/resource/maps/event/20230809/H-5.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 54, + 268 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 146, + 262 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 203 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 134, + 404 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 473 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 351, + 357 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 398, + 240 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 384, + 119 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 554, + 186 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 338 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 496, + 427 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 194 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 871, + 130 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 810, + 322 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 698, + 275 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 907, + 431 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 505 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 462 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230809/H-6.json b/resource/maps/event/20230809/H-6.json new file mode 100644 index 0000000..2f32f81 --- /dev/null +++ b/resource/maps/event/20230809/H-6.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 112 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 158, + 204 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 61, + 253 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 481, + 88 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 196 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 299 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 174, + 374 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 661, + 157 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 558, + 208 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 429, + 369 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 309, + 505 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 413 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 678, + 332 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 580, + 348 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 496, + 499 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 839, + 233 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 315 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 893, + 405 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 462 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-1.json b/resource/maps/event/20230927/E-1.json new file mode 100644 index 0000000..0b6956a --- /dev/null +++ b/resource/maps/event/20230927/E-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 151, + 218 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 246, + 112 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 360 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 332 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 103, + 489 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 166 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 136 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 242 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 465 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 451 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 627, + 329 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 794, + 256 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 482 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-2.json b/resource/maps/event/20230927/E-2.json new file mode 100644 index 0000000..9cef2e2 --- /dev/null +++ b/resource/maps/event/20230927/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 309 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 347 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 458 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 150, + 165 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 233 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 488, + 343 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 75 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 114 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 246 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 90 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 178 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 813, + 292 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 880, + 181 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-3.json b/resource/maps/event/20230927/E-3.json new file mode 100644 index 0000000..21fddac --- /dev/null +++ b/resource/maps/event/20230927/E-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 95 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 225 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 877, + 279 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 98 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 180 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 323 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 699, + 451 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 878, + 442 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 503, + 480 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 464, + 293 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 314, + 73 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 210 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 188, + 183 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 386 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 280, + 292 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-4.json b/resource/maps/event/20230927/E-4.json new file mode 100644 index 0000000..e89fc13 --- /dev/null +++ b/resource/maps/event/20230927/E-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 298 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 646, + 368 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 625, + 483 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 426, + 471 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 485, + 339 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 901, + 172 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 711, + 158 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 241 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 591, + 75 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 142 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 294, + 82 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 253 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 407 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 215, + 267 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 148, + 466 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 74, + 344 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 72, + 171 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-5.json b/resource/maps/event/20230927/E-5.json new file mode 100644 index 0000000..c62eeab --- /dev/null +++ b/resource/maps/event/20230927/E-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 98, + 443 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 292 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 325, + 233 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 533 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 261, + 374 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 471 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 330 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 494, + 140 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 584, + 472 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 569, + 345 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 539 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 726, + 392 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 676, + 210 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 923, + 480 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 913, + 336 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 884, + 157 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-6.json b/resource/maps/event/20230927/E-6.json new file mode 100644 index 0000000..14e7090 --- /dev/null +++ b/resource/maps/event/20230927/E-6.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 98, + 263 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 113 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 52 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 352 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 261, + 195 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 293 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 419, + 148 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 489, + 4 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 480 + ], + "next": [] + }, + "V": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 565, + 405 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 291 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 674, + 29 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 165 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 729, + 208 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 373 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-7.json b/resource/maps/event/20230927/E-7.json new file mode 100644 index 0000000..e6389ac --- /dev/null +++ b/resource/maps/event/20230927/E-7.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 98, + 263 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 225 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 623, + 338 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 232 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 810, + 428 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 677, + 539 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 479 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 338 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 445, + 479 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 406, + 157 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 484, + 60 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 257, + 90 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 279, + 539 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 195, + 210 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 91, + 340 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 390 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/E-8.json b/resource/maps/event/20230927/E-8.json new file mode 100644 index 0000000..17ecdca --- /dev/null +++ b/resource/maps/event/20230927/E-8.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 390 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 698, + 43 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 158 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 51 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 813, + 247 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 632, + 436 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 301 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 434, + 155 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 299 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 394, + 4 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 449, + 420 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 326, + 3 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 275, + 372 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 482 + ], + "next": [] + }, + "V": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 80, + 405 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 104, + 291 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 194, + 30 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 88, + 165 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 209 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-1.json b/resource/maps/event/20230927/H-1.json new file mode 100644 index 0000000..0b6956a --- /dev/null +++ b/resource/maps/event/20230927/H-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 151, + 218 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 246, + 112 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 360 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 332 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 103, + 489 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 166 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 136 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 242 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 465 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 451 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 627, + 329 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 794, + 256 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 482 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-2.json b/resource/maps/event/20230927/H-2.json new file mode 100644 index 0000000..9cef2e2 --- /dev/null +++ b/resource/maps/event/20230927/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 309 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 347 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 458 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 150, + 165 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 233 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 488, + 343 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 75 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 114 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 246 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 90 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 178 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 813, + 292 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 880, + 181 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-3.json b/resource/maps/event/20230927/H-3.json new file mode 100644 index 0000000..21fddac --- /dev/null +++ b/resource/maps/event/20230927/H-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 95 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 225 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 877, + 279 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 98 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 180 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 323 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 699, + 451 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 878, + 442 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 503, + 480 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 464, + 293 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 314, + 73 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 210 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 188, + 183 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 386 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 280, + 292 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-4.json b/resource/maps/event/20230927/H-4.json new file mode 100644 index 0000000..e89fc13 --- /dev/null +++ b/resource/maps/event/20230927/H-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 298 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 646, + 368 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 625, + 483 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 426, + 471 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 485, + 339 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 901, + 172 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 711, + 158 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 241 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 591, + 75 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 142 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 294, + 82 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 253 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 407 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 215, + 267 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 148, + 466 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 74, + 344 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 72, + 171 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-5.json b/resource/maps/event/20230927/H-5.json new file mode 100644 index 0000000..c62eeab --- /dev/null +++ b/resource/maps/event/20230927/H-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 98, + 443 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 292 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 325, + 233 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 533 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 261, + 374 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 471 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 330 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 494, + 140 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 584, + 472 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 569, + 345 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 539 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 726, + 392 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 676, + 210 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 923, + 480 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 913, + 336 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 884, + 157 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-6.json b/resource/maps/event/20230927/H-6.json new file mode 100644 index 0000000..14e7090 --- /dev/null +++ b/resource/maps/event/20230927/H-6.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 98, + 263 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 113 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 52 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 352 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 261, + 195 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 293 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 419, + 148 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 489, + 4 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 480 + ], + "next": [] + }, + "V": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 565, + 405 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 291 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 674, + 29 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 165 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 729, + 208 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 373 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-7.json b/resource/maps/event/20230927/H-7.json new file mode 100644 index 0000000..e6389ac --- /dev/null +++ b/resource/maps/event/20230927/H-7.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 98, + 263 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 225 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 623, + 338 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 232 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 810, + 428 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 677, + 539 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 479 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 338 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 445, + 479 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 406, + 157 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 484, + 60 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 257, + 90 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 279, + 539 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 195, + 210 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 91, + 340 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 390 + ], + "next": [] + } +} diff --git a/resource/maps/event/20230927/H-8.json b/resource/maps/event/20230927/H-8.json new file mode 100644 index 0000000..17ecdca --- /dev/null +++ b/resource/maps/event/20230927/H-8.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 390 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 698, + 43 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 158 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 51 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 813, + 247 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 632, + 436 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 301 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 434, + 155 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 299 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 394, + 4 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 449, + 420 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 326, + 3 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 275, + 372 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 482 + ], + "next": [] + }, + "V": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 80, + 405 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 104, + 291 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 194, + 30 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 88, + 165 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 252, + 209 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/E-1.json b/resource/maps/event/20231215/E-1.json new file mode 100644 index 0000000..7426bc3 --- /dev/null +++ b/resource/maps/event/20231215/E-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 249 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 165 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 96 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 413 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 323 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 373, + 239 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 193, + 97 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 189 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 56, + 282 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 163, + 344 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 381 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 488 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 470, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/E-2.json b/resource/maps/event/20231215/E-2.json new file mode 100644 index 0000000..24b797b --- /dev/null +++ b/resource/maps/event/20231215/E-2.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 270 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 675, + 135 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 864, + 422 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 449 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 350 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 526, + 232 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 108 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 358, + 289 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 489 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 196, + 127 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 302, + 231 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 396 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 129, + 307 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 49, + 291 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 174, + 471 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/E-3.json b/resource/maps/event/20231215/E-3.json new file mode 100644 index 0000000..89a4acf --- /dev/null +++ b/resource/maps/event/20231215/E-3.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 886, + 217 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 674, + 194 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 526, + 83 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 871, + 366 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 715, + 254 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 516, + 252 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 224 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 346, + 141 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 338 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 239 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 781, + 494 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 548, + 471 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 628, + 412 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 396 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 53, + 395 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 208, + 474 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/E-4.json b/resource/maps/event/20231215/E-4.json new file mode 100644 index 0000000..da7eb01 --- /dev/null +++ b/resource/maps/event/20231215/E-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 639, + 503 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 696, + 403 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 832, + 317 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 863, + 134 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 616, + 276 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 342 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 447, + 443 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 714, + 178 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 119 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 535, + 232 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 328 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 152, + 430 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 170 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 207, + 83 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 191, + 190 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 90, + 269 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 73 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/E-5.json b/resource/maps/event/20231215/E-5.json new file mode 100644 index 0000000..2f88de6 --- /dev/null +++ b/resource/maps/event/20231215/E-5.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 293 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 140 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 914, + 81 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 583, + 186 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 80 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 345 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 261 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 157 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 98 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 238, + 218 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 302, + 293 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 362, + 372 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 442 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 66, + 68 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 132, + 202 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 35, + 369 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 216, + 481 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 148, + 398 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/E-6.json b/resource/maps/event/20231215/E-6.json new file mode 100644 index 0000000..e9b2b98 --- /dev/null +++ b/resource/maps/event/20231215/E-6.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 878, + 158 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 696, + 254 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 774, + 391 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 736, + 127 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 495 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 420 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 548, + 78 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 579, + 181 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 494 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 415 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 413, + 299 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 449, + 81 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 404, + 201 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 87 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 295, + 330 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 185, + 459 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 125, + 270 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 218, + 202 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 89 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/H-1.json b/resource/maps/event/20231215/H-1.json new file mode 100644 index 0000000..7426bc3 --- /dev/null +++ b/resource/maps/event/20231215/H-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 249 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 165 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 96 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 413 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 323 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 373, + 239 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 193, + 97 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 189 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 56, + 282 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 163, + 344 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 381 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 488 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 470, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/H-2.json b/resource/maps/event/20231215/H-2.json new file mode 100644 index 0000000..24b797b --- /dev/null +++ b/resource/maps/event/20231215/H-2.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 270 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 675, + 135 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 864, + 422 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 449 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 350 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 526, + 232 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 108 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 358, + 289 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 489 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 196, + 127 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 302, + 231 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 324, + 396 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 129, + 307 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 49, + 291 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 174, + 471 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/H-3.json b/resource/maps/event/20231215/H-3.json new file mode 100644 index 0000000..89a4acf --- /dev/null +++ b/resource/maps/event/20231215/H-3.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 886, + 217 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 674, + 194 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 526, + 83 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 871, + 366 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 715, + 254 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 516, + 252 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 224 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 346, + 141 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 338 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 239 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 781, + 494 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 548, + 471 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 628, + 412 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 396 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 53, + 395 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 208, + 474 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/H-4.json b/resource/maps/event/20231215/H-4.json new file mode 100644 index 0000000..da7eb01 --- /dev/null +++ b/resource/maps/event/20231215/H-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 639, + 503 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 696, + 403 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 832, + 317 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 863, + 134 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 616, + 276 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 342 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 447, + 443 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 714, + 178 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 119 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 535, + 232 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 328 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 152, + 430 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 170 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 207, + 83 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 191, + 190 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 90, + 269 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 73 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/H-5.json b/resource/maps/event/20231215/H-5.json new file mode 100644 index 0000000..2f88de6 --- /dev/null +++ b/resource/maps/event/20231215/H-5.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 293 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 140 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 914, + 81 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 583, + 186 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 80 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 345 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 261 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 157 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 98 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 238, + 218 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 302, + 293 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 362, + 372 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 442 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 66, + 68 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 132, + 202 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 35, + 369 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 216, + 481 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 148, + 398 + ], + "next": [] + } +} diff --git a/resource/maps/event/20231215/H-6.json b/resource/maps/event/20231215/H-6.json new file mode 100644 index 0000000..e9b2b98 --- /dev/null +++ b/resource/maps/event/20231215/H-6.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 878, + 158 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 696, + 254 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 774, + 391 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 736, + 127 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 495 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 420 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 548, + 78 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 579, + 181 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 494 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 415 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 413, + 299 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 449, + 81 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 404, + 201 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 87 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 295, + 330 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 185, + 459 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 125, + 270 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 218, + 202 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 89 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-1.json b/resource/maps/event/20240206/E-1.json new file mode 100644 index 0000000..6d42e16 --- /dev/null +++ b/resource/maps/event/20240206/E-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 366 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 290, + 233 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 225 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 465 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 546, + 149 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 315 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 471, + 465 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 696, + 277 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 663, + 494 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 659, + 412 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 839, + 195 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 405 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-2.json b/resource/maps/event/20240206/E-2.json new file mode 100644 index 0000000..2a8eb84 --- /dev/null +++ b/resource/maps/event/20240206/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 91, + 224 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 93, + 451 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 284, + 277 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 489 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 293, + 112 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 383 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 466 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 208 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 587, + 89 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 609, + 181 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 360 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 792, + 403 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 826, + 263 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-3.json b/resource/maps/event/20240206/E-3.json new file mode 100644 index 0000000..3d31972 --- /dev/null +++ b/resource/maps/event/20240206/E-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 380 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 321, + 111 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 232 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 472 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 344 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 418, + 324 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 479, + 466 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 576, + 324 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 545, + 188 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 103 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 710, + 215 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 664, + 492 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 886, + 196 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 753, + 388 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-4.json b/resource/maps/event/20240206/E-4.json new file mode 100644 index 0000000..d6eb744 --- /dev/null +++ b/resource/maps/event/20240206/E-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 812, + 353 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 355 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 487 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 894, + 231 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 172 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 628, + 280 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 876, + 98 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 366 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 218, + 411 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 277 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 96 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 374, + 186 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 328, + 81 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 260, + 255 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 63, + 491 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 127, + 306 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 137 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-5.json b/resource/maps/event/20240206/E-5.json new file mode 100644 index 0000000..6feec1e --- /dev/null +++ b/resource/maps/event/20240206/E-5.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 434, + 81 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 215 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 540, + 171 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 96 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 480 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 352 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 853, + 164 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 268 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 458, + 483 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 831, + 354 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 662, + 263 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 367 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 683, + 369 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-6.json b/resource/maps/event/20240206/E-6.json new file mode 100644 index 0000000..d9fea6c --- /dev/null +++ b/resource/maps/event/20240206/E-6.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 21, + 24 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 233, + 299 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 173 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 366, + 457 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 511, + 421 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 716, + 463 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 447, + 83 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 299 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 80 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 185 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 621, + 328 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 679, + 189 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-7.json b/resource/maps/event/20240206/E-7.json new file mode 100644 index 0000000..e54132d --- /dev/null +++ b/resource/maps/event/20240206/E-7.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 923, + 48 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 377, + 48 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 368 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 548, + 472 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 503 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 525 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 367 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 494, + 269 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 234 + ], + "next": [] + }, + "V": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 295, + 391 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 143, + 390 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 200, + 248 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/E-8.json b/resource/maps/event/20240206/E-8.json new file mode 100644 index 0000000..58df794 --- /dev/null +++ b/resource/maps/event/20240206/E-8.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 1, + 1 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 278, + 96 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 135 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 737, + 299 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 165 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 263 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 58, + 264 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 488 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 390 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 445, + 353 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 262 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 142, + 367 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 290, + 366 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-1.json b/resource/maps/event/20240206/H-1.json new file mode 100644 index 0000000..6d42e16 --- /dev/null +++ b/resource/maps/event/20240206/H-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 366 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 290, + 233 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 225 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 465 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 546, + 149 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 315 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 471, + 465 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 696, + 277 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 663, + 494 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 659, + 412 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 839, + 195 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 405 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-2.json b/resource/maps/event/20240206/H-2.json new file mode 100644 index 0000000..2a8eb84 --- /dev/null +++ b/resource/maps/event/20240206/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 91, + 224 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 93, + 451 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 284, + 277 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 489 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 293, + 112 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 383 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 466 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 208 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 587, + 89 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 609, + 181 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 360 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 792, + 403 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 826, + 263 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-3.json b/resource/maps/event/20240206/H-3.json new file mode 100644 index 0000000..3d31972 --- /dev/null +++ b/resource/maps/event/20240206/H-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 380 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 321, + 111 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 232 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 472 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 344 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 418, + 324 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 479, + 466 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 576, + 324 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 545, + 188 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 103 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 710, + 215 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 664, + 492 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 886, + 196 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 753, + 388 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-4.json b/resource/maps/event/20240206/H-4.json new file mode 100644 index 0000000..d6eb744 --- /dev/null +++ b/resource/maps/event/20240206/H-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 812, + 353 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 571, + 355 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 487 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 894, + 231 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 172 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 628, + 280 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 876, + 98 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 366 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 218, + 411 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 277 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 96 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 374, + 186 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 328, + 81 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 260, + 255 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 63, + 491 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 127, + 306 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 137 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-5.json b/resource/maps/event/20240206/H-5.json new file mode 100644 index 0000000..6feec1e --- /dev/null +++ b/resource/maps/event/20240206/H-5.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 434, + 81 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 306, + 215 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 540, + 171 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 96 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 480 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 352 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 853, + 164 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 268 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 458, + 483 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 831, + 354 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 662, + 263 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 367 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 683, + 369 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-6.json b/resource/maps/event/20240206/H-6.json new file mode 100644 index 0000000..d9fea6c --- /dev/null +++ b/resource/maps/event/20240206/H-6.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 21, + 24 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 233, + 299 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 173 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 366, + 457 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 511, + 421 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 716, + 463 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 447, + 83 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 299 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 668, + 80 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 185 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 621, + 328 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 679, + 189 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-7.json b/resource/maps/event/20240206/H-7.json new file mode 100644 index 0000000..e54132d --- /dev/null +++ b/resource/maps/event/20240206/H-7.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 923, + 48 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 377, + 48 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 368 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 548, + 472 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 503 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 525 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 367 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 494, + 269 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 234 + ], + "next": [] + }, + "V": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 295, + 391 + ], + "next": [] + }, + "Y": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 143, + 390 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 200, + 248 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240206/H-8.json b/resource/maps/event/20240206/H-8.json new file mode 100644 index 0000000..58df794 --- /dev/null +++ b/resource/maps/event/20240206/H-8.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 1, + 1 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 278, + 96 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 135 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 737, + 299 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 165 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 263 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 58, + 264 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 488 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 390 + ], + "next": [] + }, + "U": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 445, + 353 + ], + "next": [] + }, + "W": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 262 + ], + "next": [] + }, + "X": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 142, + 367 + ], + "next": [] + }, + "Z": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 290, + 366 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/E-1.json b/resource/maps/event/20240419/E-1.json new file mode 100644 index 0000000..16bb2f3 --- /dev/null +++ b/resource/maps/event/20240419/E-1.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 180 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 256 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 254 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 408 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 128 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 761, + 78 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 202 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 330 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 463 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 840, + 347 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 500, + 480 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/E-2.json b/resource/maps/event/20240419/E-2.json new file mode 100644 index 0000000..fa08b98 --- /dev/null +++ b/resource/maps/event/20240419/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 299, + 96 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 308 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 89 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 354, + 232 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 374, + 383 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 538, + 250 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 506, + 495 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 421 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 136 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 52 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 759, + 271 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 842, + 185 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 781, + 466 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/E-3.json b/resource/maps/event/20240419/E-3.json new file mode 100644 index 0000000..871e92d --- /dev/null +++ b/resource/maps/event/20240419/E-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 276 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 160 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 270 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 389 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 148 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 395, + 502 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 764, + 406 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 412 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 262 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 331 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 194 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 853, + 142 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 666, + 497 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 713, + 81 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/E-4.json b/resource/maps/event/20240419/E-4.json new file mode 100644 index 0000000..983b99a --- /dev/null +++ b/resource/maps/event/20240419/E-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 189, + 174 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 67 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 286 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 389 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 331, + 225 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 345 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 576, + 463 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 503, + 239 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 129 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 210 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 376 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 797, + 476 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 812, + 112 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 724, + 74 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 831, + 286 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/E-5.json b/resource/maps/event/20240419/E-5.json new file mode 100644 index 0000000..13333be --- /dev/null +++ b/resource/maps/event/20240419/E-5.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 314 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 398 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 68 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 204 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 98 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 359, + 248 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 339 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 539, + 300 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 621, + 166 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 329 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 450 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 752, + 68 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 819, + 255 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 458 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 669, + 495 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/E-6.json b/resource/maps/event/20240419/E-6.json new file mode 100644 index 0000000..81a0345 --- /dev/null +++ b/resource/maps/event/20240419/E-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 59, + 196 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 166 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 652, + 113 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 96, + 338 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 173, + 302 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 150, + 490 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 434 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 185 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 873, + 356 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 254 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 519, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 171 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 287 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 655, + 423 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 360 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 389, + 503 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 479, + 375 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 580, + 487 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/H-1.json b/resource/maps/event/20240419/H-1.json new file mode 100644 index 0000000..16bb2f3 --- /dev/null +++ b/resource/maps/event/20240419/H-1.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 180 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 308, + 256 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 254 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 408 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 128 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 761, + 78 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 202 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 330 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 463 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 840, + 347 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 500, + 480 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/H-2.json b/resource/maps/event/20240419/H-2.json new file mode 100644 index 0000000..fa08b98 --- /dev/null +++ b/resource/maps/event/20240419/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 299, + 96 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 308 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 89 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 354, + 232 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 374, + 383 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 538, + 250 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 506, + 495 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 421 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 136 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 52 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 759, + 271 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 842, + 185 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 781, + 466 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/H-3.json b/resource/maps/event/20240419/H-3.json new file mode 100644 index 0000000..871e92d --- /dev/null +++ b/resource/maps/event/20240419/H-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 276 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 160 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 376, + 270 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 389 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 148 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 395, + 502 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 764, + 406 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 412 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 262 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 331 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 608, + 194 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 853, + 142 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 666, + 497 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 713, + 81 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/H-4.json b/resource/maps/event/20240419/H-4.json new file mode 100644 index 0000000..983b99a --- /dev/null +++ b/resource/maps/event/20240419/H-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 189, + 174 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 67 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 286 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 389 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 331, + 225 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 345 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 576, + 463 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 503, + 239 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 129 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 210 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 376 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 797, + 476 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 812, + 112 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 724, + 74 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 831, + 286 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/H-5.json b/resource/maps/event/20240419/H-5.json new file mode 100644 index 0000000..13333be --- /dev/null +++ b/resource/maps/event/20240419/H-5.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 314 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 398 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 68 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 204 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 98 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 359, + 248 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 339 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 539, + 300 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 621, + 166 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 329 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 450 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 752, + 68 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 819, + 255 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 458 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 669, + 495 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240419/H-6.json b/resource/maps/event/20240419/H-6.json new file mode 100644 index 0000000..81a0345 --- /dev/null +++ b/resource/maps/event/20240419/H-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 59, + 196 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 166 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 652, + 113 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 96, + 338 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 173, + 302 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 150, + 490 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 434 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 185 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 873, + 356 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 254 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 519, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 465, + 171 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 586, + 287 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 655, + 423 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 360 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 389, + 503 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 479, + 375 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 580, + 487 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/E-1.json b/resource/maps/event/20240930/E-1.json new file mode 100644 index 0000000..c6e04dd --- /dev/null +++ b/resource/maps/event/20240930/E-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 143, + 246 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 156 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 83 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 390 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 255 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 173 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 494 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 489, + 398 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 587, + 305 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 180 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 811, + 277 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 683, + 496 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 691, + 370 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/E-2.json b/resource/maps/event/20240930/E-2.json new file mode 100644 index 0000000..8d77f09 --- /dev/null +++ b/resource/maps/event/20240930/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 90 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 106 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 766, + 315 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 112 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 391, + 188 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 486, + 262 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 262 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 219, + 233 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 339 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 540, + 406 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 744, + 472 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 486 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 185, + 467 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/E-3.json b/resource/maps/event/20240930/E-3.json new file mode 100644 index 0000000..5741846 --- /dev/null +++ b/resource/maps/event/20240930/E-3.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 217 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 241, + 201 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 104 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 369 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 377 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 399, + 241 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 352 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 244 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 354 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 186 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 714, + 300 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 176, + 495 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 256, + 443 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 413, + 482 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 782, + 449 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 609, + 495 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/E-4.json b/resource/maps/event/20240930/E-4.json new file mode 100644 index 0000000..09b7641 --- /dev/null +++ b/resource/maps/event/20240930/E-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 826, + 301 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 148 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 478, + 80 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 232 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 892, + 457 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 396 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 496 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 356, + 152 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 315 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 556, + 486 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 253, + 96 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 216 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 311, + 220 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 330 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 458, + 331 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 361, + 480 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 134, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/E-5.json b/resource/maps/event/20240930/E-5.json new file mode 100644 index 0000000..4b0d009 --- /dev/null +++ b/resource/maps/event/20240930/E-5.json @@ -0,0 +1,202 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 893, + 204 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 165 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 104 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 871, + 376 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 338 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 767, + 484 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 542, + 203 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 625, + 286 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 616, + 451 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 358, + 174 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 73 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 246 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 143 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 212 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 451, + 376 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 164, + 303 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 283 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 116, + 443 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 496 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 272, + 465 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/H-1.json b/resource/maps/event/20240930/H-1.json new file mode 100644 index 0000000..c6e04dd --- /dev/null +++ b/resource/maps/event/20240930/H-1.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 143, + 246 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 269, + 156 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 83 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 390 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 255 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 173 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 466, + 494 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 489, + 398 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 587, + 305 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 180 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 811, + 277 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 683, + 496 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 691, + 370 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/H-2.json b/resource/maps/event/20240930/H-2.json new file mode 100644 index 0000000..8d77f09 --- /dev/null +++ b/resource/maps/event/20240930/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 90 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 106 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 766, + 315 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 112 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 391, + 188 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 486, + 262 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 262 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 219, + 233 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 338, + 339 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 540, + 406 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 744, + 472 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 486 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 185, + 467 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/H-3.json b/resource/maps/event/20240930/H-3.json new file mode 100644 index 0000000..5741846 --- /dev/null +++ b/resource/maps/event/20240930/H-3.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 217 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 241, + 201 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 104 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 369 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 209, + 377 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 399, + 241 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 352 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 244 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 354 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 186 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 714, + 300 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 176, + 495 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 256, + 443 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 413, + 482 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 782, + 449 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 609, + 495 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/H-4.json b/resource/maps/event/20240930/H-4.json new file mode 100644 index 0000000..09b7641 --- /dev/null +++ b/resource/maps/event/20240930/H-4.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 826, + 301 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 533, + 148 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 478, + 80 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 232 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 892, + 457 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 396 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 496 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 356, + 152 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 315 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 556, + 486 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 253, + 96 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 216 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 311, + 220 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 330 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 458, + 331 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 361, + 480 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 134, + 486 + ], + "next": [] + } +} diff --git a/resource/maps/event/20240930/H-5.json b/resource/maps/event/20240930/H-5.json new file mode 100644 index 0000000..4b0d009 --- /dev/null +++ b/resource/maps/event/20240930/H-5.json @@ -0,0 +1,202 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 893, + 204 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 165 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 561, + 104 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 871, + 376 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 728, + 338 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 767, + 484 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 542, + 203 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 625, + 286 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 616, + 451 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 358, + 174 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 73 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 246 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 143 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 212 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 451, + 376 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 164, + 303 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 283 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 116, + 443 + ], + "next": [] + }, + "S": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 496 + ], + "next": [] + }, + "T": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 272, + 465 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/E-1.json b/resource/maps/event/20241219/E-1.json new file mode 100644 index 0000000..5b8493d --- /dev/null +++ b/resource/maps/event/20241219/E-1.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 269 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 330 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 211, + 143 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 99 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 407 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 193 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 464, + 299 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 560, + 91 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 202 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 775, + 273 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 663, + 339 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 563, + 418 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 800, + 116 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 796, + 482 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/E-2.json b/resource/maps/event/20241219/E-2.json new file mode 100644 index 0000000..0af1a6d --- /dev/null +++ b/resource/maps/event/20241219/E-2.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 658, + 345 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 496 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 473 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 171, + 406 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 354 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 255 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 212, + 286 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 155, + 129 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 171 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 127 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 91 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 201 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 775, + 345 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 830, + 90 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/E-3.json b/resource/maps/event/20241219/E-3.json new file mode 100644 index 0000000..ab5f7d2 --- /dev/null +++ b/resource/maps/event/20241219/E-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 226, + 120 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 374, + 76 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 218, + 339 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 428 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 496, + 159 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 328, + 240 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 494, + 303 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 583, + 76 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 629, + 203 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 303 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 679, + 316 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 554, + 437 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 503 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 482 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 744, + 86 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/E-4.json b/resource/maps/event/20241219/E-4.json new file mode 100644 index 0000000..093b094 --- /dev/null +++ b/resource/maps/event/20241219/E-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 89 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 90 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 173 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 404, + 300 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 554, + 299 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 188 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 756, + 189 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 222, + 344 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 736, + 344 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 477 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 595, + 473 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 94, + 201 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 200 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 94, + 481 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 482 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/E-5.json b/resource/maps/event/20241219/E-5.json new file mode 100644 index 0000000..d854507 --- /dev/null +++ b/resource/maps/event/20241219/E-5.json @@ -0,0 +1,52 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 284, + 67 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 159 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 467, + 98 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 675, + 113 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 249 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/E-6.json b/resource/maps/event/20241219/E-6.json new file mode 100644 index 0000000..4f9822e --- /dev/null +++ b/resource/maps/event/20241219/E-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 67, + 277 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 201, + 186 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 83 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 195, + 322 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 466 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 218 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 389, + 340 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 76 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 730, + 74 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 135 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 523, + 240 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 408 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 834, + 226 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 743, + 279 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 623, + 363 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 407 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 500 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 476 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/H-1.json b/resource/maps/event/20241219/H-1.json new file mode 100644 index 0000000..5b8493d --- /dev/null +++ b/resource/maps/event/20241219/H-1.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 111, + 269 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 330 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 211, + 143 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 99 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 407 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 193 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 464, + 299 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 560, + 91 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 202 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 775, + 273 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 663, + 339 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 563, + 418 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 800, + 116 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 796, + 482 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/H-2.json b/resource/maps/event/20241219/H-2.json new file mode 100644 index 0000000..0af1a6d --- /dev/null +++ b/resource/maps/event/20241219/H-2.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 658, + 345 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 496 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 473 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 171, + 406 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 354 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 255 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 212, + 286 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 155, + 129 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 171 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 443, + 127 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 91 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 201 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 775, + 345 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 830, + 90 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/H-3.json b/resource/maps/event/20241219/H-3.json new file mode 100644 index 0000000..ab5f7d2 --- /dev/null +++ b/resource/maps/event/20241219/H-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 226, + 120 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 374, + 76 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 218, + 339 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 428 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 496, + 159 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 328, + 240 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 494, + 303 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 583, + 76 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 629, + 203 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 303 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 679, + 316 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 554, + 437 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 503 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 482 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 744, + 86 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/H-4.json b/resource/maps/event/20241219/H-4.json new file mode 100644 index 0000000..093b094 --- /dev/null +++ b/resource/maps/event/20241219/H-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 89 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 90 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 173 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 404, + 300 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 554, + 299 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 188 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 756, + 189 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 222, + 344 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 736, + 344 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 477 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 595, + 473 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 94, + 201 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 200 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 94, + 481 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 861, + 482 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/H-5.json b/resource/maps/event/20241219/H-5.json new file mode 100644 index 0000000..d854507 --- /dev/null +++ b/resource/maps/event/20241219/H-5.json @@ -0,0 +1,52 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 284, + 67 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 271, + 159 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 467, + 98 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 675, + 113 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 249 + ], + "next": [] + } +} diff --git a/resource/maps/event/20241219/H-6.json b/resource/maps/event/20241219/H-6.json new file mode 100644 index 0000000..4f9822e --- /dev/null +++ b/resource/maps/event/20241219/H-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 67, + 277 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 201, + 186 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 83 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 195, + 322 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 286, + 466 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 353, + 218 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 389, + 340 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 76 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 730, + 74 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 135 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 523, + 240 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 408 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 834, + 226 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 743, + 279 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 623, + 363 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 407 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 500 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 476 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/E-1.json b/resource/maps/event/20250123/E-1.json new file mode 100644 index 0000000..71eda0a --- /dev/null +++ b/resource/maps/event/20250123/E-1.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 128, + 237 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 147 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 238, + 373 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 493 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 381, + 253 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 488, + 156 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 457 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 338 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 471 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 644, + 256 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 787, + 494 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 710, + 346 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/E-2.json b/resource/maps/event/20250123/E-2.json new file mode 100644 index 0000000..dbe3afd --- /dev/null +++ b/resource/maps/event/20250123/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 119 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 314, + 264 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 210 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 234 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 352 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 96, + 488 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 284, + 465 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 360 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 451, + 509 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 691, + 232 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 846, + 366 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 502 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 412 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/E-3.json b/resource/maps/event/20250123/E-3.json new file mode 100644 index 0000000..748c3f1 --- /dev/null +++ b/resource/maps/event/20250123/E-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 229 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 217 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 88 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 110, + 382 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 367 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 486 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 373 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 230 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 119 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 809, + 171 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 501 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 575, + 396 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 764, + 322 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/E-4.json b/resource/maps/event/20250123/E-4.json new file mode 100644 index 0000000..3154bac --- /dev/null +++ b/resource/maps/event/20250123/E-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 216 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 404, + 201 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 471, + 86 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 380 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 359 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 501 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 403 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 510 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 306 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 172 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 298 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 771, + 165 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 786, + 388 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 898, + 487 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 489 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/E-5.json b/resource/maps/event/20250123/E-5.json new file mode 100644 index 0000000..a3a331b --- /dev/null +++ b/resource/maps/event/20250123/E-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 75, + 286 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 215 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 127, + 457 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 201 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 238, + 351 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 285 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 405 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 491 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 398 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 395 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 336 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 229 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 606, + 263 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 231 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 593, + 88 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/E-6.json b/resource/maps/event/20250123/E-6.json new file mode 100644 index 0000000..7d4cd07 --- /dev/null +++ b/resource/maps/event/20250123/E-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 794, + 209 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 689, + 149 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 75 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 352 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 322 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 321 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 224 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 389, + 74 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 173, + 83 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 164 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 155 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 625, + 472 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 503 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 440, + 389 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 292 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 179, + 299 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 174, + 465 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 489 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/H-1.json b/resource/maps/event/20250123/H-1.json new file mode 100644 index 0000000..71eda0a --- /dev/null +++ b/resource/maps/event/20250123/H-1.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 128, + 237 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 147 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 238, + 373 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 493 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 381, + 253 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 488, + 156 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 457 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 338 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 471 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 644, + 256 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 787, + 494 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 710, + 346 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/H-2.json b/resource/maps/event/20250123/H-2.json new file mode 100644 index 0000000..dbe3afd --- /dev/null +++ b/resource/maps/event/20250123/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 448, + 119 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 314, + 264 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 210 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 524, + 234 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 352 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 96, + 488 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 284, + 465 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 433, + 360 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 451, + 509 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 691, + 232 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 846, + 366 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 502 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 412 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/H-3.json b/resource/maps/event/20250123/H-3.json new file mode 100644 index 0000000..748c3f1 --- /dev/null +++ b/resource/maps/event/20250123/H-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 229 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 217 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 88 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 110, + 382 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 248, + 367 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 486 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 373 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 230 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 119 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 809, + 171 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 501 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 575, + 396 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 764, + 322 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/H-4.json b/resource/maps/event/20250123/H-4.json new file mode 100644 index 0000000..3154bac --- /dev/null +++ b/resource/maps/event/20250123/H-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 216 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 404, + 201 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 471, + 86 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 380 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 359 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 501 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 403 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 510 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 306 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 172 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 298 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 771, + 165 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 786, + 388 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 898, + 487 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 636, + 489 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/H-5.json b/resource/maps/event/20250123/H-5.json new file mode 100644 index 0000000..a3a331b --- /dev/null +++ b/resource/maps/event/20250123/H-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 75, + 286 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 215 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 127, + 457 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 336, + 201 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 238, + 351 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 441, + 285 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 405 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 491 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 509, + 398 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 395 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 336 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 229 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 606, + 263 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 231 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 593, + 88 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250123/H-6.json b/resource/maps/event/20250123/H-6.json new file mode 100644 index 0000000..7d4cd07 --- /dev/null +++ b/resource/maps/event/20250123/H-6.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 794, + 209 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 689, + 149 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 75 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 352 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 891, + 322 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 614, + 321 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 224 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 389, + 74 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 173, + 83 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 164 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 254, + 155 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 625, + 472 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 503 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 440, + 389 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 292 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 179, + 299 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 174, + 465 + ], + "next": [] + }, + "R": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 489 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/E-1.json b/resource/maps/event/20250710/E-1.json new file mode 100644 index 0000000..6b7f2a1 --- /dev/null +++ b/resource/maps/event/20250710/E-1.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 74 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 623, + 194 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 734, + 114 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 836, + 262 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 553, + 306 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 240 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 455, + 397 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 315 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 253, + 339 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 466 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 459 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 103, + 279 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 464 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 785, + 477 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/E-2.json b/resource/maps/event/20250710/E-2.json new file mode 100644 index 0000000..00bd470 --- /dev/null +++ b/resource/maps/event/20250710/E-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 180 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 501, + 76 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 271 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 413, + 346 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 253 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 209 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 713, + 113 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 446 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 299, + 444 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 270 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 496 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 354 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 743, + 480 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/E-3.json b/resource/maps/event/20250710/E-3.json new file mode 100644 index 0000000..fdc0178 --- /dev/null +++ b/resource/maps/event/20250710/E-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 300 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 264, + 354 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 210 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 134, + 105 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 451, + 264 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 414 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 617, + 361 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 247 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 167 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 598, + 166 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 802, + 105 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 278 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 659, + 505 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 795, + 218 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 819, + 456 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/E-4.json b/resource/maps/event/20250710/E-4.json new file mode 100644 index 0000000..e108f17 --- /dev/null +++ b/resource/maps/event/20250710/E-4.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 331 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 488 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 465 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 693, + 138 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 827, + 218 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 158 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 276 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 395 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 104 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 255 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 734, + 347 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 397 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 194, + 196 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 495 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 118, + 66 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 876, + 471 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/E-5.json b/resource/maps/event/20250710/E-5.json new file mode 100644 index 0000000..f4db369 --- /dev/null +++ b/resource/maps/event/20250710/E-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 359, + 75 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 140, + 204 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 158 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 140, + 359 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 78 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 166 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 481, + 190 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 572, + 264 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 267 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 312, + 357 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 206, + 495 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 492, + 346 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 364, + 450 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 426 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 860, + 361 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 514, + 494 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/E-6.json b/resource/maps/event/20250710/E-6.json new file mode 100644 index 0000000..7970237 --- /dev/null +++ b/resource/maps/event/20250710/E-6.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 96 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 202 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 94, + 219 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 353 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 487 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 225 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 539, + 76 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 526, + 180 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 337 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 467, + 450 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 504 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 584, + 316 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 157 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 839, + 247 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 301 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 633, + 422 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 834, + 467 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/H-1.json b/resource/maps/event/20250710/H-1.json new file mode 100644 index 0000000..6b7f2a1 --- /dev/null +++ b/resource/maps/event/20250710/H-1.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 74 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 623, + 194 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 734, + 114 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 836, + 262 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 553, + 306 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 396, + 240 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 455, + 397 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 315 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 253, + 339 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 466 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 329, + 459 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 103, + 279 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 149, + 464 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 785, + 477 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/H-2.json b/resource/maps/event/20250710/H-2.json new file mode 100644 index 0000000..00bd470 --- /dev/null +++ b/resource/maps/event/20250710/H-2.json @@ -0,0 +1,132 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 180 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 501, + 76 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 271 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 413, + 346 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 253 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 638, + 209 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 713, + 113 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 446 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 299, + 444 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 803, + 270 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 531, + 496 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 354 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 743, + 480 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/H-3.json b/resource/maps/event/20250710/H-3.json new file mode 100644 index 0000000..fdc0178 --- /dev/null +++ b/resource/maps/event/20250710/H-3.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 119, + 300 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 264, + 354 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 210 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 134, + 105 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 451, + 264 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 414 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 617, + 361 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 247 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 167 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 598, + 166 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 802, + 105 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 278 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 659, + 505 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 795, + 218 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 819, + 456 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/H-4.json b/resource/maps/event/20250710/H-4.json new file mode 100644 index 0000000..e108f17 --- /dev/null +++ b/resource/maps/event/20250710/H-4.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 331 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 488 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 465 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 693, + 138 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 827, + 218 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 158 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 276 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 344, + 395 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 343, + 104 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 368, + 255 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 734, + 347 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 397 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 194, + 196 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 495 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 118, + 66 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 876, + 471 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/H-5.json b/resource/maps/event/20250710/H-5.json new file mode 100644 index 0000000..f4db369 --- /dev/null +++ b/resource/maps/event/20250710/H-5.json @@ -0,0 +1,162 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 359, + 75 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 140, + 204 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 158 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 140, + 359 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 578, + 78 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 166 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 481, + 190 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 572, + 264 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 323, + 267 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 312, + 357 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 206, + 495 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 492, + 346 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 364, + 450 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 426 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 860, + 361 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 514, + 494 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250710/H-6.json b/resource/maps/event/20250710/H-6.json new file mode 100644 index 0000000..7970237 --- /dev/null +++ b/resource/maps/event/20250710/H-6.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 96 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 202 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 94, + 219 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 353 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 487 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 225 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 539, + 76 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 526, + 180 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 390, + 337 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 467, + 450 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 631, + 504 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 584, + 316 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 697, + 157 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 839, + 247 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 758, + 301 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 633, + 422 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 834, + 467 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/E-1.json b/resource/maps/event/20250930/E-1.json new file mode 100644 index 0000000..c465b35 --- /dev/null +++ b/resource/maps/event/20250930/E-1.json @@ -0,0 +1,112 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 135 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 367 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 352 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 180, + 480 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 127 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 285 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 187 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 307 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 330 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 465 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/E-2.json b/resource/maps/event/20250930/E-2.json new file mode 100644 index 0000000..40cd6c1 --- /dev/null +++ b/resource/maps/event/20250930/E-2.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 390 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 255 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 337 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 472 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 90 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 142 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 202 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 330 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 307, + 427 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 337 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 360, + 120 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/E-3.json b/resource/maps/event/20250930/E-3.json new file mode 100644 index 0000000..ed5cd1a --- /dev/null +++ b/resource/maps/event/20250930/E-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 262 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 397 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 502 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 90 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 195 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 300 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 435 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 502 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 360 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 195 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 67 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 195 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 285 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 75 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/E-4.json b/resource/maps/event/20250930/E-4.json new file mode 100644 index 0000000..38920d7 --- /dev/null +++ b/resource/maps/event/20250930/E-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 435 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 300 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 292 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 180 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 382 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 210 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 90 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 165 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 262 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 375 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 67 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 487 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 480 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 262 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 82 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/E-5.json b/resource/maps/event/20250930/E-5.json new file mode 100644 index 0000000..715f0c9 --- /dev/null +++ b/resource/maps/event/20250930/E-5.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 67, + 247 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 105 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 127, + 397 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 487 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 300 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 97 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 202 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 60 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 420 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 600, + 495 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 330 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 240 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 210 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 345 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 435 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/E-6.json b/resource/maps/event/20250930/E-6.json new file mode 100644 index 0000000..f738272 --- /dev/null +++ b/resource/maps/event/20250930/E-6.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 300 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 247 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 150 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 397 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 255 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 157 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 502 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 420 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 105 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 202 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 292 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 795, + 60 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 465 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 915, + 405 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 330 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 817, + 225 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 907, + 82 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/H-1.json b/resource/maps/event/20250930/H-1.json new file mode 100644 index 0000000..c465b35 --- /dev/null +++ b/resource/maps/event/20250930/H-1.json @@ -0,0 +1,112 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 135 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 60, + 367 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 352 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 180, + 480 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 127 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 285 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 187 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 307 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 330 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 465 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/H-2.json b/resource/maps/event/20250930/H-2.json new file mode 100644 index 0000000..40cd6c1 --- /dev/null +++ b/resource/maps/event/20250930/H-2.json @@ -0,0 +1,122 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 390 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 255 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 577, + 337 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 472 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 90 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 142 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 202 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 330 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 307, + 427 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 337 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 360, + 120 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/H-3.json b/resource/maps/event/20250930/H-3.json new file mode 100644 index 0000000..ed5cd1a --- /dev/null +++ b/resource/maps/event/20250930/H-3.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 262 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 397 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 502 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 90 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 195 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 300 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 435 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 502 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 360 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 195 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 67 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 195 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 285 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 75 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/H-4.json b/resource/maps/event/20250930/H-4.json new file mode 100644 index 0000000..38920d7 --- /dev/null +++ b/resource/maps/event/20250930/H-4.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 435 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 727, + 300 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 292 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 180 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 382 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 210 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 90 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 165 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 262 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 375 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 322, + 67 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 487 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 480 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 262 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 82 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/H-5.json b/resource/maps/event/20250930/H-5.json new file mode 100644 index 0000000..715f0c9 --- /dev/null +++ b/resource/maps/event/20250930/H-5.json @@ -0,0 +1,152 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 67, + 247 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 262, + 105 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 127, + 397 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 487 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 300 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 97 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 202 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 60 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 420 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 600, + 495 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 330 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 240 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 210 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 345 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 435 + ], + "next": [] + } +} diff --git a/resource/maps/event/20250930/H-6.json b/resource/maps/event/20250930/H-6.json new file mode 100644 index 0000000..f738272 --- /dev/null +++ b/resource/maps/event/20250930/H-6.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 300 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 247 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 150 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 397 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 420, + 255 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 157 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 502 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 420 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 105 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 607, + 202 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 292 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 795, + 60 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 720, + 465 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 915, + 405 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 330 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 817, + 225 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 907, + 82 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/E-1.json b/resource/maps/event/20260104/E-1.json new file mode 100644 index 0000000..39db3ff --- /dev/null +++ b/resource/maps/event/20260104/E-1.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 142, + 217 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 270 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 150 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 367 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 255 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 420 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 412 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 337 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 187 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 487 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 502 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 487 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 450 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 225, + 105 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/E-2.json b/resource/maps/event/20260104/E-2.json new file mode 100644 index 0000000..294b154 --- /dev/null +++ b/resource/maps/event/20260104/E-2.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 225 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 217 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 345 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 397 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 330 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 502 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 435 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 502 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 502 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 412 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 240 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 82 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 202, + 82 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/E-3.json b/resource/maps/event/20260104/E-3.json new file mode 100644 index 0000000..f836c5f --- /dev/null +++ b/resource/maps/event/20260104/E-3.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 75, + 285 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 330 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 195, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 232 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 97 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 840, + 232 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 82 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 172 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 180 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 292 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 307 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 390 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 412 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 382 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 502 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 142, + 457 + ], + "next": [] + }, + "β": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 840, + 67 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/E-4.json b/resource/maps/event/20260104/E-4.json new file mode 100644 index 0000000..69d176b --- /dev/null +++ b/resource/maps/event/20260104/E-4.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 240 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 67 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 97 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 180 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 367 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 502 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 472 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 360 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 240 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 165 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 352 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 825, + 262 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 495 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 825, + 352 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 487 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 180 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 120, + 135 + ], + "next": [] + }, + "β": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 705, + 75 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/E-5.json b/resource/maps/event/20260104/E-5.json new file mode 100644 index 0000000..6401121 --- /dev/null +++ b/resource/maps/event/20260104/E-5.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 307, + 442 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 330 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 187 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 457 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 382 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 255 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 877, + 472 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 360 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 262 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 240 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 82 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 97 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 150 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 52 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 105, + 127 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 112, + 457 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/E-6.json b/resource/maps/event/20260104/E-6.json new file mode 100644 index 0000000..649b7e9 --- /dev/null +++ b/resource/maps/event/20260104/E-6.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 367 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 307, + 495 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 210 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 105 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 90 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 172 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 142 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 45 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 345 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 270 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 502 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 375 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 465 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 457 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 847, + 307 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 270 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 90, + 450 + ], + "next": [] + }, + "β": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 135, + 52 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/H-1.json b/resource/maps/event/20260104/H-1.json new file mode 100644 index 0000000..39db3ff --- /dev/null +++ b/resource/maps/event/20260104/H-1.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 142, + 217 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 270 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 397, + 150 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 367 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 255 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 420 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 412 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 337 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 187 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 487 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 502 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 165, + 487 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 862, + 450 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 225, + 105 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/H-2.json b/resource/maps/event/20260104/H-2.json new file mode 100644 index 0000000..294b154 --- /dev/null +++ b/resource/maps/event/20260104/H-2.json @@ -0,0 +1,142 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 82, + 225 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 217 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 345 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 397 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 330 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 502 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 435 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 502 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 502 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 412 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 247 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 240 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 82 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 202, + 82 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/H-3.json b/resource/maps/event/20260104/H-3.json new file mode 100644 index 0000000..f836c5f --- /dev/null +++ b/resource/maps/event/20260104/H-3.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 75, + 285 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 330 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 195, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 247, + 232 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 97 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 840, + 232 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 82 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 315, + 172 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 180 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 292 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 667, + 307 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 390 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 412 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 382 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 765, + 502 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 142, + 457 + ], + "next": [] + }, + "β": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 840, + 67 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/H-4.json b/resource/maps/event/20260104/H-4.json new file mode 100644 index 0000000..69d176b --- /dev/null +++ b/resource/maps/event/20260104/H-4.json @@ -0,0 +1,182 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 240 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 300, + 67 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 487, + 97 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 180 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 367 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 135, + 502 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 472 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 442, + 360 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 240 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 165 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 352 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 825, + 262 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 495 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 825, + 352 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 742, + 487 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 180 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 120, + 135 + ], + "next": [] + }, + "β": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 705, + 75 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/H-5.json b/resource/maps/event/20260104/H-5.json new file mode 100644 index 0000000..6401121 --- /dev/null +++ b/resource/maps/event/20260104/H-5.json @@ -0,0 +1,172 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 307, + 442 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 187, + 330 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 172, + 187 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 457 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 382 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 255 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 877, + 472 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 360 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 262 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 240 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 82 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 97 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 352, + 150 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 52 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 105, + 127 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 112, + 457 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260104/H-6.json b/resource/maps/event/20260104/H-6.json new file mode 100644 index 0000000..649b7e9 --- /dev/null +++ b/resource/maps/event/20260104/H-6.json @@ -0,0 +1,192 @@ +{ + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 367 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 307, + 495 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 210 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 330, + 105 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 90 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 630, + 172 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 757, + 142 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 45 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 345 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 547, + 270 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 525, + 502 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 375 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 712, + 465 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 457 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 847, + 307 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 270 + ], + "next": [] + }, + "α": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 90, + 450 + ], + "next": [] + }, + "β": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 135, + 52 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/E-1.json b/resource/maps/event/20260212/E-1.json new file mode 100644 index 0000000..3dfd30f --- /dev/null +++ b/resource/maps/event/20260212/E-1.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 82, + 150 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 75, + 382 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 277 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 105 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 442 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 540, + 150 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 135 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 442 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 322 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 270 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 877, + 180 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 825, + 450 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/E-2.json b/resource/maps/event/20260212/E-2.json new file mode 100644 index 0000000..e74c4a4 --- /dev/null +++ b/resource/maps/event/20260212/E-2.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 165, + 457 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 360, + 405 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 270 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 420 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 150 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 277 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 112 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 337 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 180 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 405 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 292 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 142 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/E-3.json b/resource/maps/event/20260212/E-3.json new file mode 100644 index 0000000..6bb0ba9 --- /dev/null +++ b/resource/maps/event/20260212/E-3.json @@ -0,0 +1,142 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 75, + 157 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 270 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 135 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 420 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 345 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 210 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 97 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 345 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 217 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 90 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 495 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 382 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 487 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 285 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/E-4.json b/resource/maps/event/20260212/E-4.json new file mode 100644 index 0000000..e45c509 --- /dev/null +++ b/resource/maps/event/20260212/E-4.json @@ -0,0 +1,152 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 825, + 262 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 202 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 652, + 337 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 420 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 82 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 442 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 330 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 502 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 502 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 375 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 157 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 352 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 210 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 67 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 75 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/E-5.json b/resource/maps/event/20260212/E-5.json new file mode 100644 index 0000000..86cac2a --- /dev/null +++ b/resource/maps/event/20260212/E-5.json @@ -0,0 +1,172 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 67, + 412 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 105, + 187 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 397 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 502 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 90 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 75 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 165 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 277 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 322 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 487 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 487 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 330 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 255 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 157 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 840, + 300 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 892, + 142 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 787, + 487 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/E-6.json b/resource/maps/event/20260212/E-6.json new file mode 100644 index 0000000..8af6cef --- /dev/null +++ b/resource/maps/event/20260212/E-6.json @@ -0,0 +1,182 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 67, + 457 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 157, + 307 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 487 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 75 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 360 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 472 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 330 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 502, + 105 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 195 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 52 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 157 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 67 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 600, + 427 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 360 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 442 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 240 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/H-1.json b/resource/maps/event/20260212/H-1.json new file mode 100644 index 0000000..3dfd30f --- /dev/null +++ b/resource/maps/event/20260212/H-1.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 82, + 150 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 75, + 382 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 202, + 277 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 292, + 105 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 337, + 442 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 382, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 540, + 150 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 135 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 570, + 442 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 532, + 322 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 270 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 877, + 180 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 825, + 450 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/H-2.json b/resource/maps/event/20260212/H-2.json new file mode 100644 index 0000000..e74c4a4 --- /dev/null +++ b/resource/maps/event/20260212/H-2.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 165, + 457 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 360, + 405 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 217, + 270 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 420 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 112, + 150 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 277 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 112 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 487 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 337 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 517, + 180 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 405 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 885, + 292 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 705, + 142 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/H-3.json b/resource/maps/event/20260212/H-3.json new file mode 100644 index 0000000..6bb0ba9 --- /dev/null +++ b/resource/maps/event/20260212/H-3.json @@ -0,0 +1,142 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 75, + 157 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 225, + 270 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 270, + 135 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 420 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 345 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 472, + 210 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 97 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 345 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 217 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 90 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 585, + 495 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 382 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 780, + 487 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 285 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/H-4.json b/resource/maps/event/20260212/H-4.json new file mode 100644 index 0000000..e45c509 --- /dev/null +++ b/resource/maps/event/20260212/H-4.json @@ -0,0 +1,152 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 825, + 262 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 592, + 202 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 652, + 337 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 735, + 420 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 510, + 82 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 457, + 442 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 435, + 330 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 562, + 502 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 345, + 502 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 97, + 375 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 120, + 157 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 352 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 210 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 285, + 67 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 75 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/H-5.json b/resource/maps/event/20260212/H-5.json new file mode 100644 index 0000000..86cac2a --- /dev/null +++ b/resource/maps/event/20260212/H-5.json @@ -0,0 +1,172 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 67, + 412 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 105, + 187 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 397 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 240, + 502 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 277, + 90 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 495, + 75 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 480, + 165 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 232, + 277 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 405, + 322 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 450, + 487 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 637, + 487 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 622, + 330 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 660, + 255 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 690, + 157 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 840, + 300 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 892, + 142 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 787, + 487 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260212/H-6.json b/resource/maps/event/20260212/H-6.json new file mode 100644 index 0000000..8af6cef --- /dev/null +++ b/resource/maps/event/20260212/H-6.json @@ -0,0 +1,182 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 67, + 457 + ], + "next": [] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 157, + 307 + ], + "next": [] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 255, + 487 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 210, + 112 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 412, + 75 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 367, + 240 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 375, + 360 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 427, + 472 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 555, + 330 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 502, + 105 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 615, + 195 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 682, + 52 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 772, + 157 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 870, + 67 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 600, + 427 + ], + "next": [] + }, + "O": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 750, + 360 + ], + "next": [] + }, + "P": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 442 + ], + "next": [] + }, + "Q": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 900, + 240 + ], + "next": [] + } +} diff --git a/resource/maps/event/20260730/E-1a.json b/resource/maps/event/20260730/E-1a.json deleted file mode 100644 index 054e187..0000000 --- a/resource/maps/event/20260730/E-1a.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 223.229, - 403.361 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 481.464, - 497.4 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 869.106, - 407.398 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 300.975, - 765.809 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 687.906, - 780.811 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 615.049, - 960.0 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1034.546, - 737.914 - ], - "next": [ - "I", - "J" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1035.69, - 1002.38 - ], - "next": [ - "J", - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1332.723, - 555.668 - ], - "next": [] - }, - "J": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1336.855, - 764.368 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1664.794, - 852.54 - ], - "next": [] - }, - "L": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1710.396, - 554.781 - ], - "next": [] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1486.624, - 945.574 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 463.363, - 150.829 - ], - "next": [ - "A", - "B", - "C" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-2a.json b/resource/maps/event/20260730/E-2a.json deleted file mode 100644 index 1f9975e..0000000 --- a/resource/maps/event/20260730/E-2a.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1123.579, - 764.81 - ], - "next": [ - "B", - "E", - "H" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1514.972, - 569.784 - ], - "next": [ - "C", - "D" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1648.539, - 299.552 - ], - "next": [] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1095.098, - 464.992 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "FIGTH", - "detour": true, - "night": false, - "position": [ - 734.022, - 797.05 - ], - "next": [ - "F", - "G" - ] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 451.64, - 959.728 - ], - "next": [] - }, - "G": { - "type": "FIGTH", - "detour": true, - "night": false, - "position": [ - 344.684, - 737.543 - ], - "next": [ - "I" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 704.592, - 524.522 - ], - "next": [ - "I", - "L" - ] - }, - "I": { - "type": "FIGTH", - "detour": true, - "night": false, - "position": [ - 269.104, - 388.215 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 437.14, - 133.544 - ], - "next": [] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 704.283, - 177.87 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 959.426, - 316.415 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1348.055, - 226.181 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 1617.971, - 856.077 - ], - "next": [ - "A", - "B" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-3a.json b/resource/maps/event/20260730/E-3a.json deleted file mode 100644 index 9a7bc6e..0000000 --- a/resource/maps/event/20260730/E-3a.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 331.07, - 615.285 - ], - "next": [ - "B", - "E" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 585.47, - 418.412 - ], - "next": [ - "I" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 615.635, - 148.854 - ], - "next": [ - "D", - "I" - ] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1123.202, - 164.917 - ], - "next": [] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 660.154, - 690.039 - ], - "next": [ - "F", - "H" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 961.302, - 527.498 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 961.093, - 732.995 - ], - "next": [ - "K", - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 932.339, - 331.292 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1288.621, - 359.774 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1335.427, - 674.461 - ], - "next": [ - "L", - "N" - ] - }, - "L": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1681.177, - 526.784 - ], - "next": [] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1259.918, - 975.136 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1576.594, - 945.872 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 271.132, - 226.466 - ], - "next": [ - "A", - "B", - "C" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-3b.json b/resource/maps/event/20260730/E-3b.json deleted file mode 100644 index 11643cb..0000000 --- a/resource/maps/event/20260730/E-3b.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 331.07, - 615.285 - ], - "next": [ - "B", - "E" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 585.47, - 418.412 - ], - "next": [ - "I" - ] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 660.635, - 688.854 - ], - "next": [ - "F", - "H" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 958.202, - 524.917 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 660.154, - 930.039 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 961.302, - 737.498 - ], - "next": [ - "K", - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 931.093, - 327.995 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1292.339, - 361.292 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1333.621, - 674.774 - ], - "next": [ - "L", - "N" - ] - }, - "L": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1680.427, - 524.461 - ], - "next": [] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1261.177, - 976.784 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1574.918, - 945.136 - ], - "next": [] - }, - "β": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 301.594, - 975.872 - ], - "next": [ - "A", - "G" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-4a.json b/resource/maps/event/20260730/E-4a.json deleted file mode 100644 index 5d3bafa..0000000 --- a/resource/maps/event/20260730/E-4a.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 448.393, - 374.845 - ], - "next": [ - "B", - "L" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 748.313, - 527.159 - ], - "next": [ - "F", - "G", - "L" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1170.382, - 582.419 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 976.131, - 721.886 - ], - "next": [ - "H", - "I", - "L" - ] - }, - "H": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 855.89, - 1005.306 - ], - "next": [] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1349.684, - 795.026 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1694.819, - 869.884 - ], - "next": [] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1576.732, - 465.515 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 599.366, - 868.096 - ], - "next": [ - "M", - "N" - ] - }, - "M": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 239.189, - 662.015 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 210.243, - 856.325 - ], - "next": [] - }, - "O": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1470.754, - 148.63 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 151.297, - 390.419 - ], - "next": [ - "A" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-4b.json b/resource/maps/event/20260730/E-4b.json deleted file mode 100644 index 820480e..0000000 --- a/resource/maps/event/20260730/E-4b.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 748.393, - 524.845 - ], - "next": [ - "E", - "F", - "G" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 883.313, - 182.159 - ], - "next": [ - "D", - "E" - ] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1290.382, - 237.419 - ], - "next": [] - }, - "E": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1156.131, - 406.886 - ], - "next": [ - "K" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1170.89, - 585.306 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 974.684, - 720.026 - ], - "next": [ - "H", - "I", - "L" - ] - }, - "H": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 854.819, - 1004.884 - ], - "next": [] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1351.732, - 795.515 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1694.366, - 868.096 - ], - "next": [] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1574.189, - 467.015 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 600.243, - 871.325 - ], - "next": [ - "M", - "N" - ] - }, - "M": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 240.754, - 658.63 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 211.297, - 855.419 - ], - "next": [] - }, - "O": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1469.576, - 150.228 - ], - "next": [] - }, - "β": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 479.892, - 181.642 - ], - "next": [ - "B", - "C" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-5a.json b/resource/maps/event/20260730/E-5a.json deleted file mode 100644 index 8a600c3..0000000 --- a/resource/maps/event/20260730/E-5a.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 540.176, - 781.005 - ], - "next": [ - "C", - "F" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 721.067, - 931.928 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 403.56, - 542.138 - ], - "next": [] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 1049.598, - 869.44 - ], - "next": [] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": true, - "position": [ - 901.349, - 691.285 - ], - "next": [ - "I" - ] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 751.566, - 601.808 - ], - "next": [ - "G", - "I" - ] - }, - "G": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 568.517, - 315.353 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 271.368, - 211.171 - ], - "next": [] - }, - "I": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 855.653, - 345.344 - ], - "next": [ - "J", - "K", - "L" - ] - }, - "J": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1110.889, - 135.078 - ], - "next": [] - }, - "K": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1246.282, - 315.375 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1152.229, - 584.724 - ], - "next": [ - "N", - "O" - ] - }, - "M": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1438.879, - 553.531 - ], - "next": [ - "O" - ] - }, - "N": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1170.682, - 945.192 - ], - "next": [] - }, - "O": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1425.44, - 885.483 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 373.673, - 1004.619 - ], - "next": [ - "A", - "B" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-6a.json b/resource/maps/event/20260730/E-6a.json deleted file mode 100644 index 895d844..0000000 --- a/resource/maps/event/20260730/E-6a.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 614.798, - 149.991 - ], - "next": [ - "E", - "N" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 103.927, - 508.279 - ], - "next": [ - "C", - "D", - "F" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 148.165, - 854.41 - ], - "next": [] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 510.22, - 392.127 - ], - "next": [ - "M", - "N" - ] - }, - "E": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1048.312, - 224.577 - ], - "next": [] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 330.352, - 676.446 - ], - "next": [ - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1276.706, - 315.282 - ], - "next": [] - }, - "M": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 658.199, - 748.541 - ], - "next": [ - "N", - "O" - ] - }, - "N": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 929.815, - 421.072 - ], - "next": [ - "I", - "O", - "Q" - ] - }, - "O": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 974.716, - 766.995 - ], - "next": [ - "P", - "Q" - ] - }, - "P": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 733.744, - 957.767 - ], - "next": [] - }, - "Q": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1320.593, - 689.216 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 252.306, - 209.6 - ], - "next": [ - "A", - "B" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/E-6b.json b/resource/maps/event/20260730/E-6b.json deleted file mode 100644 index c9dc268..0000000 --- a/resource/maps/event/20260730/E-6b.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "G": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1694.798, - 314.991 - ], - "next": [ - "H", - "I" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1783.927, - 598.279 - ], - "next": [ - "J", - "K" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1273.165, - 314.41 - ], - "next": [ - "N" - ] - }, - "J": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1680.22, - 842.127 - ], - "next": [ - "L", - "Q" - ] - }, - "K": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1423.312, - 509.577 - ], - "next": [] - }, - "L": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1380.352, - 976.446 - ], - "next": [] - }, - "N": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 931.706, - 420.282 - ], - "next": [ - "O", - "Q" - ] - }, - "O": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 973.199, - 763.541 - ], - "next": [ - "P", - "Q" - ] - }, - "P": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 734.815, - 961.072 - ], - "next": [] - }, - "Q": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1319.716, - 691.995 - ], - "next": [] - }, - "β": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 1438.744, - 132.767 - ], - "next": [ - "G" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-1a.json b/resource/maps/event/20260730/H-1a.json deleted file mode 100644 index f55ec3c..0000000 --- a/resource/maps/event/20260730/H-1a.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 224.855, - 405.028 - ], - "next": [ - "D" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 480.723, - 497.764 - ], - "next": [ - "E" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 871.713, - 402.807 - ], - "next": [ - "G" - ] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 300.197, - 766.305 - ], - "next": [ - "F" - ] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 689.729, - 780.477 - ], - "next": [ - "G", - "H" - ] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 614.843, - 959.3 - ], - "next": [ - "H" - ] - }, - "G": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1034.762, - 734.666 - ], - "next": [ - "I", - "J" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1035.735, - 1005.814 - ], - "next": [ - "J", - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1335.273, - 554.311 - ], - "next": [] - }, - "J": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1334.086, - 765.364 - ], - "next": [ - "K", - "L" - ] - }, - "K": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1664.063, - 856.406 - ], - "next": [] - }, - "L": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1712.275, - 556.834 - ], - "next": [] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1484.378, - 944.705 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 464.519, - 150.45 - ], - "next": [ - "A", - "B", - "C" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-2a.json b/resource/maps/event/20260730/H-2a.json deleted file mode 100644 index 47a0d93..0000000 --- a/resource/maps/event/20260730/H-2a.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1123.747, - 765.731 - ], - "next": [ - "B", - "E", - "H" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1515.494, - 569.875 - ], - "next": [ - "C", - "D" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1649.746, - 299.29 - ], - "next": [] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1092.196, - 464.867 - ], - "next": [ - "H" - ] - }, - "E": { - "type": "FIGTH", - "detour": true, - "night": false, - "position": [ - 734.329, - 793.92 - ], - "next": [ - "F", - "G" - ] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 450.033, - 959.457 - ], - "next": [] - }, - "G": { - "type": "FIGTH", - "detour": true, - "night": false, - "position": [ - 342.362, - 733.675 - ], - "next": [ - "I" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 704.613, - 522.063 - ], - "next": [ - "I", - "L" - ] - }, - "I": { - "type": "FIGTH", - "detour": true, - "night": false, - "position": [ - 269.014, - 388.103 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 435.041, - 132.37 - ], - "next": [] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 706.369, - 179.777 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 959.874, - 314.987 - ], - "next": [ - "M" - ] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1349.039, - 223.276 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 1617.529, - 854.812 - ], - "next": [ - "A", - "B" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-3a.json b/resource/maps/event/20260730/H-3a.json deleted file mode 100644 index 4801a10..0000000 --- a/resource/maps/event/20260730/H-3a.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 329.533, - 613.636 - ], - "next": [ - "B", - "E" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 586.864, - 418.54 - ], - "next": [ - "I" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 615.993, - 149.468 - ], - "next": [ - "D", - "I" - ] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1125.402, - 165.508 - ], - "next": [] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 660.716, - 690.802 - ], - "next": [ - "F", - "H" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 959.252, - 525.602 - ], - "next": [ - "K" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 961.499, - 735.191 - ], - "next": [ - "K", - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 930.518, - 332.339 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1288.68, - 360.682 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1335.572, - 674.157 - ], - "next": [ - "L", - "N" - ] - }, - "L": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1679.777, - 524.445 - ], - "next": [] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1259.147, - 977.578 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1574.918, - 943.576 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 270.196, - 224.901 - ], - "next": [ - "A", - "B", - "C" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-3b.json b/resource/maps/event/20260730/H-3b.json deleted file mode 100644 index a389547..0000000 --- a/resource/maps/event/20260730/H-3b.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 329.533, - 613.636 - ], - "next": [ - "B", - "E" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 586.864, - 418.54 - ], - "next": [ - "I" - ] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 660.993, - 689.468 - ], - "next": [ - "F", - "H" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 960.402, - 525.508 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 660.716, - 930.802 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 959.252, - 735.602 - ], - "next": [ - "K", - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 931.499, - 330.191 - ], - "next": [ - "J" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1290.518, - 362.339 - ], - "next": [ - "K" - ] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1333.68, - 675.682 - ], - "next": [ - "L", - "N" - ] - }, - "L": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1680.572, - 524.157 - ], - "next": [] - }, - "M": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1259.777, - 974.445 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1574.147, - 947.578 - ], - "next": [] - }, - "β": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 299.918, - 973.576 - ], - "next": [ - "A", - "G" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-4a.json b/resource/maps/event/20260730/H-4a.json deleted file mode 100644 index 016035f..0000000 --- a/resource/maps/event/20260730/H-4a.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 450.321, - 372.319 - ], - "next": [ - "B", - "L" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 751.707, - 525.442 - ], - "next": [ - "F", - "G", - "L" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1171.025, - 582.555 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 974.972, - 719.348 - ], - "next": [ - "H", - "I", - "L" - ] - }, - "H": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 855.422, - 1006.06 - ], - "next": [] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1350.66, - 796.331 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1693.849, - 867.782 - ], - "next": [] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1577.797, - 466.061 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 599.435, - 869.171 - ], - "next": [ - "M", - "N" - ] - }, - "M": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 237.327, - 661.287 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 210.041, - 855.312 - ], - "next": [] - }, - "O": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1468.373, - 148.505 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 150.001, - 389.978 - ], - "next": [ - "A" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-4b.json b/resource/maps/event/20260730/H-4b.json deleted file mode 100644 index ef45127..0000000 --- a/resource/maps/event/20260730/H-4b.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 750.321, - 522.319 - ], - "next": [ - "E", - "F", - "G" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 886.707, - 180.442 - ], - "next": [ - "D", - "E" - ] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1291.025, - 237.555 - ], - "next": [] - }, - "E": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1154.972, - 404.348 - ], - "next": [ - "K" - ] - }, - "F": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1170.422, - 586.06 - ], - "next": [ - "K" - ] - }, - "G": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 975.66, - 721.331 - ], - "next": [ - "H", - "I", - "L" - ] - }, - "H": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 853.849, - 1002.782 - ], - "next": [] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1352.797, - 796.061 - ], - "next": [ - "J", - "K" - ] - }, - "J": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1694.435, - 869.171 - ], - "next": [] - }, - "K": { - "type": "SPECIAL_BOSS", - "detour": true, - "night": false, - "position": [ - 1572.327, - 466.287 - ], - "next": [ - "O" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 600.041, - 870.312 - ], - "next": [ - "M", - "N" - ] - }, - "M": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 238.373, - 658.505 - ], - "next": [] - }, - "N": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 210.001, - 854.978 - ], - "next": [] - }, - "O": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1470.723, - 150.006 - ], - "next": [] - }, - "β": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 482.023, - 179.125 - ], - "next": [ - "B", - "C" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-5a.json b/resource/maps/event/20260730/H-5a.json deleted file mode 100644 index 1ea35b8..0000000 --- a/resource/maps/event/20260730/H-5a.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 542.055, - 779.101 - ], - "next": [ - "C", - "F" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 721.397, - 929.646 - ], - "next": [ - "D", - "E" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 405.871, - 540.075 - ], - "next": [] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 1051.49, - 869.562 - ], - "next": [] - }, - "E": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": true, - "position": [ - 901.157, - 692.477 - ], - "next": [ - "I" - ] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": true, - "position": [ - 750.376, - 599.316 - ], - "next": [ - "G", - "I" - ] - }, - "G": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 569.869, - 315.292 - ], - "next": [ - "H" - ] - }, - "H": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 271.077, - 209.956 - ], - "next": [] - }, - "I": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 855.022, - 344.685 - ], - "next": [ - "J", - "K", - "L" - ] - }, - "J": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1112.681, - 133.704 - ], - "next": [] - }, - "K": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1246.292, - 313.183 - ], - "next": [ - "M" - ] - }, - "L": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1155.038, - 583.832 - ], - "next": [ - "N", - "O" - ] - }, - "M": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1439.665, - 557.566 - ], - "next": [ - "O" - ] - }, - "N": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1170.596, - 946.829 - ], - "next": [] - }, - "O": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1425.272, - 885.473 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 373.302, - 1006.338 - ], - "next": [ - "A", - "B" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-6a.json b/resource/maps/event/20260730/H-6a.json deleted file mode 100644 index 89c6daa..0000000 --- a/resource/maps/event/20260730/H-6a.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "A": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 613.73, - 149.958 - ], - "next": [ - "E", - "N" - ] - }, - "B": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 105.898, - 509.654 - ], - "next": [ - "C", - "D", - "F" - ] - }, - "C": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 150.078, - 855.233 - ], - "next": [] - }, - "D": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 511.339, - 389.024 - ], - "next": [ - "M", - "N" - ] - }, - "E": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1051.464, - 225.756 - ], - "next": [] - }, - "F": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 329.893, - 674.815 - ], - "next": [ - "M" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1274.528, - 316.04 - ], - "next": [] - }, - "M": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 659.02, - 750.542 - ], - "next": [ - "N", - "O" - ] - }, - "N": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 930.534, - 420.116 - ], - "next": [ - "I", - "O", - "Q" - ] - }, - "O": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 975.206, - 764.933 - ], - "next": [ - "P", - "Q" - ] - }, - "P": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 734.417, - 962.179 - ], - "next": [] - }, - "Q": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1319.583, - 690.713 - ], - "next": [] - }, - "α": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 257.034, - 208.675 - ], - "next": [ - "A", - "B" - ] - } -} \ No newline at end of file diff --git a/resource/maps/event/20260730/H-6b.json b/resource/maps/event/20260730/H-6b.json deleted file mode 100644 index bead988..0000000 --- a/resource/maps/event/20260730/H-6b.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "G": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1693.73, - 314.958 - ], - "next": [ - "H", - "I" - ] - }, - "H": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1785.898, - 599.654 - ], - "next": [ - "J", - "K" - ] - }, - "I": { - "type": "FIGTH", - "detour": false, - "night": false, - "position": [ - 1275.078, - 315.233 - ], - "next": [ - "N" - ] - }, - "J": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 1681.339, - 839.024 - ], - "next": [ - "L", - "Q" - ] - }, - "K": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1426.464, - 510.756 - ], - "next": [] - }, - "L": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 1379.893, - 974.815 - ], - "next": [] - }, - "N": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 929.528, - 421.04 - ], - "next": [ - "O", - "Q" - ] - }, - "O": { - "type": "SPECIAL_BOSS", - "detour": false, - "night": false, - "position": [ - 974.02, - 765.542 - ], - "next": [ - "P", - "Q" - ] - }, - "P": { - "type": "LITTLE_BOSS", - "detour": false, - "night": false, - "position": [ - 735.534, - 960.116 - ], - "next": [] - }, - "Q": { - "type": "BOSS", - "detour": false, - "night": false, - "position": [ - 1320.206, - 689.933 - ], - "next": [] - }, - "β": { - "type": "NULL", - "detour": false, - "night": false, - "position": [ - 1439.417, - 137.179 - ], - "next": [ - "G" - ] - } -} \ No newline at end of file diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-1-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-1-\316\261.json" new file mode 100644 index 0000000..bb0587d --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-1-\316\261.json" @@ -0,0 +1,168 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.116, + 0.373 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.251, + 0.461 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.453, + 0.377 + ], + "next": [ + "G" + ] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.157, + 0.709 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.358, + 0.723 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.889 + ], + "next": [ + "H" + ] + }, + "G": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.539, + 0.683 + ], + "next": [ + "I", + "J" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.539, + 0.928 + ], + "next": [ + "J", + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.694, + 0.515 + ], + "next": [] + }, + "J": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.696, + 0.708 + ], + "next": [ + "K", + "L" + ] + }, + "K": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.867, + 0.789 + ], + "next": [] + }, + "L": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.891, + 0.514 + ], + "next": [] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.774, + 0.876 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.241, + 0.14 + ], + "next": [ + "A", + "B", + "C" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-2-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-2-\316\261.json" new file mode 100644 index 0000000..f0b3950 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-2-\316\261.json" @@ -0,0 +1,169 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.585, + 0.708 + ], + "next": [ + "B", + "E", + "H" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.789, + 0.528 + ], + "next": [ + "C", + "D" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.859, + 0.277 + ], + "next": [] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.57, + 0.431 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "FIGTH", + "detour": true, + "night": false, + "position": [ + 0.382, + 0.738 + ], + "next": [ + "F", + "G" + ] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.235, + 0.889 + ], + "next": [] + }, + "G": { + "type": "FIGTH", + "detour": true, + "night": false, + "position": [ + 0.18, + 0.683 + ], + "next": [ + "I" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.367, + 0.486 + ], + "next": [ + "I", + "L" + ] + }, + "I": { + "type": "FIGTH", + "detour": true, + "night": false, + "position": [ + 0.14, + 0.359 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.228, + 0.124 + ], + "next": [] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.367, + 0.165 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.293 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.702, + 0.209 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.843, + 0.793 + ], + "next": [ + "A", + "B" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-3-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-3-\316\261.json" new file mode 100644 index 0000000..c06a9ed --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-3-\316\261.json" @@ -0,0 +1,169 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.57 + ], + "next": [ + "B", + "E" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.305, + 0.387 + ], + "next": [ + "I" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.321, + 0.138 + ], + "next": [ + "D", + "I" + ] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.585, + 0.153 + ], + "next": [] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.344, + 0.639 + ], + "next": [ + "F", + "H" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.501, + 0.488 + ], + "next": [ + "K" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.501, + 0.679 + ], + "next": [ + "K", + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.486, + 0.307 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.671, + 0.333 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.696, + 0.625 + ], + "next": [ + "L", + "N" + ] + }, + "L": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.876, + 0.488 + ], + "next": [] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.903 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.821, + 0.876 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.21 + ], + "next": [ + "A", + "B", + "C" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-3-\316\262.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-3-\316\262.json" new file mode 100644 index 0000000..971a7b7 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-3-\316\262.json" @@ -0,0 +1,157 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.57 + ], + "next": [ + "B", + "E" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.305, + 0.387 + ], + "next": [ + "I" + ] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.344, + 0.638 + ], + "next": [ + "F", + "H" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.499, + 0.486 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.344, + 0.861 + ], + "next": [ + "H" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.501, + 0.683 + ], + "next": [ + "K", + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.304 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.673, + 0.335 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.695, + 0.625 + ], + "next": [ + "L", + "N" + ] + }, + "L": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.875, + 0.486 + ], + "next": [] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.657, + 0.904 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.82, + 0.875 + ], + "next": [] + }, + "β": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.157, + 0.904 + ], + "next": [ + "A", + "G" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-4-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-4-\316\261.json" new file mode 100644 index 0000000..70fca67 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-4-\316\261.json" @@ -0,0 +1,155 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.234, + 0.347 + ], + "next": [ + "B", + "L" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.39, + 0.488 + ], + "next": [ + "F", + "G", + "L" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.61, + 0.539 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.668 + ], + "next": [ + "H", + "I", + "L" + ] + }, + "H": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.446, + 0.931 + ], + "next": [] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.703, + 0.736 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.883, + 0.805 + ], + "next": [] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.821, + 0.431 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.312, + 0.804 + ], + "next": [ + "M", + "N" + ] + }, + "M": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.125, + 0.613 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.11, + 0.793 + ], + "next": [] + }, + "O": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.766, + 0.138 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.079, + 0.361 + ], + "next": [ + "A" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-4-\316\262.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-4-\316\262.json" new file mode 100644 index 0000000..5ebd21c --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-4-\316\262.json" @@ -0,0 +1,178 @@ +{ + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.39, + 0.486 + ], + "next": [ + "E", + "F", + "G" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.46, + 0.169 + ], + "next": [ + "D", + "E" + ] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.672, + 0.22 + ], + "next": [] + }, + "E": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.602, + 0.377 + ], + "next": [ + "K" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.61, + 0.542 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.667 + ], + "next": [ + "H", + "I", + "L" + ] + }, + "H": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.93 + ], + "next": [] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.704, + 0.737 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.882, + 0.804 + ], + "next": [] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.82, + 0.432 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.313, + 0.807 + ], + "next": [ + "M", + "N" + ] + }, + "M": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.125, + 0.61 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.11, + 0.792 + ], + "next": [] + }, + "O": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.765, + 0.139 + ], + "next": [] + }, + "β": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.25, + 0.168 + ], + "next": [ + "B", + "C" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-5-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-5-\316\261.json" new file mode 100644 index 0000000..b696f58 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-5-\316\261.json" @@ -0,0 +1,189 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.281, + 0.723 + ], + "next": [ + "C", + "F" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.376, + 0.863 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.21, + 0.502 + ], + "next": [] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.547, + 0.805 + ], + "next": [] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": true, + "position": [ + 0.469, + 0.64 + ], + "next": [ + "I" + ] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.391, + 0.557 + ], + "next": [ + "G", + "I" + ] + }, + "G": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.296, + 0.292 + ], + "next": [ + "H" + ] + }, + "H": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.196 + ], + "next": [] + }, + "I": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.446, + 0.32 + ], + "next": [ + "J", + "K", + "L" + ] + }, + "J": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.579, + 0.125 + ], + "next": [] + }, + "K": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.649, + 0.292 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.6, + 0.541 + ], + "next": [ + "N", + "O" + ] + }, + "M": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.749, + 0.513 + ], + "next": [ + "O" + ] + }, + "N": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.61, + 0.875 + ], + "next": [] + }, + "O": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.742, + 0.82 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.195, + 0.93 + ], + "next": [ + "A", + "B" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-6-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-6-\316\261.json" new file mode 100644 index 0000000..69e14dd --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-6-\316\261.json" @@ -0,0 +1,157 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.139 + ], + "next": [ + "E", + "N" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.054, + 0.471 + ], + "next": [ + "C", + "D", + "F" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.077, + 0.791 + ], + "next": [] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.266, + 0.363 + ], + "next": [ + "M", + "N" + ] + }, + "E": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.546, + 0.208 + ], + "next": [] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.626 + ], + "next": [ + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.665, + 0.292 + ], + "next": [] + }, + "M": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.343, + 0.693 + ], + "next": [ + "N", + "O" + ] + }, + "N": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.484, + 0.39 + ], + "next": [ + "I", + "O", + "Q" + ] + }, + "O": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.71 + ], + "next": [ + "P", + "Q" + ] + }, + "P": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.382, + 0.887 + ], + "next": [] + }, + "Q": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.688, + 0.638 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.131, + 0.194 + ], + "next": [ + "A", + "B" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-6-\316\262.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-6-\316\262.json" new file mode 100644 index 0000000..2e8ec8b --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241-Ex-6-\316\262.json" @@ -0,0 +1,131 @@ +{ + "G": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.883, + 0.292 + ], + "next": [ + "H", + "I" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.929, + 0.554 + ], + "next": [ + "J", + "K" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.663, + 0.291 + ], + "next": [ + "N" + ] + }, + "J": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.875, + 0.78 + ], + "next": [ + "L", + "Q" + ] + }, + "K": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.741, + 0.472 + ], + "next": [] + }, + "L": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.719, + 0.904 + ], + "next": [] + }, + "N": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.389 + ], + "next": [ + "O", + "Q" + ] + }, + "O": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.507, + 0.707 + ], + "next": [ + "P", + "Q" + ] + }, + "P": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.383, + 0.89 + ], + "next": [] + }, + "Q": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.687, + 0.641 + ], + "next": [] + }, + "β": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.749, + 0.123 + ], + "next": [ + "G" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-1-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-1-\316\261.json" new file mode 100644 index 0000000..78dd568 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-1-\316\261.json" @@ -0,0 +1,168 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.117, + 0.375 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.25, + 0.461 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.454, + 0.373 + ], + "next": [ + "G" + ] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.156, + 0.71 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.359, + 0.723 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.888 + ], + "next": [ + "H" + ] + }, + "G": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.539, + 0.68 + ], + "next": [ + "I", + "J" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.539, + 0.931 + ], + "next": [ + "J", + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.695, + 0.513 + ], + "next": [] + }, + "J": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.695, + 0.709 + ], + "next": [ + "K", + "L" + ] + }, + "K": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.867, + 0.793 + ], + "next": [] + }, + "L": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.892, + 0.516 + ], + "next": [] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.773, + 0.875 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.242, + 0.139 + ], + "next": [ + "A", + "B", + "C" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-2-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-2-\316\261.json" new file mode 100644 index 0000000..844d5a9 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-2-\316\261.json" @@ -0,0 +1,169 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.585, + 0.709 + ], + "next": [ + "B", + "E", + "H" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.789, + 0.528 + ], + "next": [ + "C", + "D" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.859, + 0.277 + ], + "next": [] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.569, + 0.43 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "FIGTH", + "detour": true, + "night": false, + "position": [ + 0.382, + 0.735 + ], + "next": [ + "F", + "G" + ] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.234, + 0.888 + ], + "next": [] + }, + "G": { + "type": "FIGTH", + "detour": true, + "night": false, + "position": [ + 0.178, + 0.679 + ], + "next": [ + "I" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.367, + 0.483 + ], + "next": [ + "I", + "L" + ] + }, + "I": { + "type": "FIGTH", + "detour": true, + "night": false, + "position": [ + 0.14, + 0.359 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.227, + 0.123 + ], + "next": [] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.368, + 0.166 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.292 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.703, + 0.207 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.842, + 0.791 + ], + "next": [ + "A", + "B" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-3-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-3-\316\261.json" new file mode 100644 index 0000000..50f8446 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-3-\316\261.json" @@ -0,0 +1,169 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.568 + ], + "next": [ + "B", + "E" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.306, + 0.388 + ], + "next": [ + "I" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.321, + 0.138 + ], + "next": [ + "D", + "I" + ] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.586, + 0.153 + ], + "next": [] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.344, + 0.64 + ], + "next": [ + "F", + "H" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.487 + ], + "next": [ + "K" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.501, + 0.681 + ], + "next": [ + "K", + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.308 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.671, + 0.334 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.696, + 0.624 + ], + "next": [ + "L", + "N" + ] + }, + "L": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.875, + 0.486 + ], + "next": [] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.905 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.82, + 0.874 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.208 + ], + "next": [ + "A", + "B", + "C" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-3-\316\262.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-3-\316\262.json" new file mode 100644 index 0000000..d6680f7 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-3-\316\262.json" @@ -0,0 +1,157 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.568 + ], + "next": [ + "B", + "E" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.306, + 0.388 + ], + "next": [ + "I" + ] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.344, + 0.638 + ], + "next": [ + "F", + "H" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.487 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.344, + 0.862 + ], + "next": [ + "H" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.681 + ], + "next": [ + "K", + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.306 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.672, + 0.335 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.695, + 0.626 + ], + "next": [ + "L", + "N" + ] + }, + "L": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.875, + 0.485 + ], + "next": [] + }, + "M": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.902 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.82, + 0.877 + ], + "next": [] + }, + "β": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.156, + 0.901 + ], + "next": [ + "A", + "G" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-4-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-4-\316\261.json" new file mode 100644 index 0000000..73aec6b --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-4-\316\261.json" @@ -0,0 +1,155 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.235, + 0.345 + ], + "next": [ + "B", + "L" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.392, + 0.487 + ], + "next": [ + "F", + "G", + "L" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.61, + 0.539 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.666 + ], + "next": [ + "H", + "I", + "L" + ] + }, + "H": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.446, + 0.932 + ], + "next": [] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.703, + 0.737 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.882, + 0.804 + ], + "next": [] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.822, + 0.432 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.312, + 0.805 + ], + "next": [ + "M", + "N" + ] + }, + "M": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.124, + 0.612 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.109, + 0.792 + ], + "next": [] + }, + "O": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.765, + 0.138 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.078, + 0.361 + ], + "next": [ + "A" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-4-\316\262.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-4-\316\262.json" new file mode 100644 index 0000000..2e6c07c --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-4-\316\262.json" @@ -0,0 +1,178 @@ +{ + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.391, + 0.484 + ], + "next": [ + "E", + "F", + "G" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.462, + 0.167 + ], + "next": [ + "D", + "E" + ] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.672, + 0.22 + ], + "next": [] + }, + "E": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.602, + 0.374 + ], + "next": [ + "K" + ] + }, + "F": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.61, + 0.543 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.668 + ], + "next": [ + "H", + "I", + "L" + ] + }, + "H": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.929 + ], + "next": [] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.705, + 0.737 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.883, + 0.805 + ], + "next": [] + }, + "K": { + "type": "SPECIAL_BOSS", + "detour": true, + "night": false, + "position": [ + 0.819, + 0.432 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.313, + 0.806 + ], + "next": [ + "M", + "N" + ] + }, + "M": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.124, + 0.61 + ], + "next": [] + }, + "N": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.109, + 0.792 + ], + "next": [] + }, + "O": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.766, + 0.139 + ], + "next": [] + }, + "β": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.251, + 0.166 + ], + "next": [ + "B", + "C" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-5-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-5-\316\261.json" new file mode 100644 index 0000000..2ffb916 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-5-\316\261.json" @@ -0,0 +1,189 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.282, + 0.721 + ], + "next": [ + "C", + "F" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.376, + 0.861 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.211, + 0.5 + ], + "next": [] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.548, + 0.805 + ], + "next": [] + }, + "E": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": true, + "position": [ + 0.469, + 0.641 + ], + "next": [ + "I" + ] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": true, + "position": [ + 0.391, + 0.555 + ], + "next": [ + "G", + "I" + ] + }, + "G": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.297, + 0.292 + ], + "next": [ + "H" + ] + }, + "H": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.194 + ], + "next": [] + }, + "I": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.319 + ], + "next": [ + "J", + "K", + "L" + ] + }, + "J": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.58, + 0.124 + ], + "next": [] + }, + "K": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.649, + 0.29 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.602, + 0.541 + ], + "next": [ + "N", + "O" + ] + }, + "M": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.75, + 0.516 + ], + "next": [ + "O" + ] + }, + "N": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.61, + 0.877 + ], + "next": [] + }, + "O": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.742, + 0.82 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.194, + 0.932 + ], + "next": [ + "A", + "B" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-6-\316\261.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-6-\316\261.json" new file mode 100644 index 0000000..8d26d33 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-6-\316\261.json" @@ -0,0 +1,157 @@ +{ + "A": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.139 + ], + "next": [ + "E", + "N" + ] + }, + "B": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.055, + 0.472 + ], + "next": [ + "C", + "D", + "F" + ] + }, + "C": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.078, + 0.792 + ], + "next": [] + }, + "D": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.266, + 0.36 + ], + "next": [ + "M", + "N" + ] + }, + "E": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.548, + 0.209 + ], + "next": [] + }, + "F": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.625 + ], + "next": [ + "M" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.664, + 0.293 + ], + "next": [] + }, + "M": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.343, + 0.695 + ], + "next": [ + "N", + "O" + ] + }, + "N": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.389 + ], + "next": [ + "I", + "O", + "Q" + ] + }, + "O": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.708 + ], + "next": [ + "P", + "Q" + ] + }, + "P": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.383, + 0.891 + ], + "next": [] + }, + "Q": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.687, + 0.64 + ], + "next": [] + }, + "α": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.134, + 0.193 + ], + "next": [ + "A", + "B" + ] + } +} diff --git "a/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-6-\316\262.json" "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-6-\316\262.json" new file mode 100644 index 0000000..df75f20 --- /dev/null +++ "b/resource/maps/event/20260730/\346\277\200\346\226\227\346\274\251\346\266\241H-Ex-6-\316\262.json" @@ -0,0 +1,131 @@ +{ + "G": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.882, + 0.292 + ], + "next": [ + "H", + "I" + ] + }, + "H": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.93, + 0.555 + ], + "next": [ + "J", + "K" + ] + }, + "I": { + "type": "FIGTH", + "detour": false, + "night": false, + "position": [ + 0.664, + 0.292 + ], + "next": [ + "N" + ] + }, + "J": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.876, + 0.777 + ], + "next": [ + "L", + "Q" + ] + }, + "K": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.743, + 0.473 + ], + "next": [] + }, + "L": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.719, + 0.903 + ], + "next": [] + }, + "N": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.484, + 0.39 + ], + "next": [ + "O", + "Q" + ] + }, + "O": { + "type": "SPECIAL_BOSS", + "detour": false, + "night": false, + "position": [ + 0.507, + 0.709 + ], + "next": [ + "P", + "Q" + ] + }, + "P": { + "type": "LITTLE_BOSS", + "detour": false, + "night": false, + "position": [ + 0.383, + 0.889 + ], + "next": [] + }, + "Q": { + "type": "BOSS", + "detour": false, + "night": false, + "position": [ + 0.688, + 0.639 + ], + "next": [] + }, + "β": { + "type": "NULL", + "detour": false, + "night": false, + "position": [ + 0.75, + 0.127 + ], + "next": [ + "G" + ] + } +} diff --git a/resource/maps/event/index.json b/resource/maps/event/index.json new file mode 100644 index 0000000..41a5ddb --- /dev/null +++ b/resource/maps/event/index.json @@ -0,0 +1,692 @@ +{ + "schema_version": 2, + "events": [ + { + "event": "20260730", + "chapters": { + "E": [ + "1a", + "2a", + "3a", + "3b", + "4a", + "4b", + "5a", + "6a", + "6b" + ], + "H": [ + "1a", + "2a", + "3a", + "3b", + "4a", + "4b", + "5a", + "6a", + "6b" + ] + }, + "files": { + "E": { + "1a": "激斗漩涡-Ex-1-α.json", + "2a": "激斗漩涡-Ex-2-α.json", + "3a": "激斗漩涡-Ex-3-α.json", + "3b": "激斗漩涡-Ex-3-β.json", + "4a": "激斗漩涡-Ex-4-α.json", + "4b": "激斗漩涡-Ex-4-β.json", + "5a": "激斗漩涡-Ex-5-α.json", + "6a": "激斗漩涡-Ex-6-α.json", + "6b": "激斗漩涡-Ex-6-β.json" + }, + "H": { + "1a": "激斗漩涡H-Ex-1-α.json", + "2a": "激斗漩涡H-Ex-2-α.json", + "3a": "激斗漩涡H-Ex-3-α.json", + "3b": "激斗漩涡H-Ex-3-β.json", + "4a": "激斗漩涡H-Ex-4-α.json", + "4b": "激斗漩涡H-Ex-4-β.json", + "5a": "激斗漩涡H-Ex-5-α.json", + "6a": "激斗漩涡H-Ex-6-α.json", + "6b": "激斗漩涡H-Ex-6-β.json" + } + } + }, + { + "event": "20260212", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20260104", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20250930", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20250710", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20250123", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20241219", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20240930", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5" + ], + "H": [ + "1", + "2", + "3", + "4", + "5" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json" + } + } + }, + { + "event": "20240419", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20240206", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json", + "7": "E-7.json", + "8": "E-8.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json", + "7": "H-7.json", + "8": "H-8.json" + } + } + }, + { + "event": "20231215", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20230927", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json", + "7": "E-7.json", + "8": "E-8.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json", + "7": "H-7.json", + "8": "H-8.json" + } + } + }, + { + "event": "20230809", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20230613", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20230117", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20221118", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + }, + { + "event": "20220928", + "chapters": { + "E": [ + "1", + "2", + "3", + "4", + "5", + "6" + ], + "H": [ + "1", + "2", + "3", + "4", + "5", + "6" + ] + }, + "files": { + "E": { + "1": "E-1.json", + "2": "E-2.json", + "3": "E-3.json", + "4": "E-4.json", + "5": "E-5.json", + "6": "E-6.json" + }, + "H": { + "1": "H-1.json", + "2": "H-2.json", + "3": "H-3.json", + "4": "H-4.json", + "5": "H-5.json", + "6": "H-6.json" + } + } + } + ] +} diff --git a/resource/maps/normal/1-1.json b/resource/maps/normal/1-1.json new file mode 100644 index 0000000..6418c22 --- /dev/null +++ b/resource/maps/normal/1-1.json @@ -0,0 +1,47 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.178, + 0.577 + ], + "next": [ + "A" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.294, + 0.522 + ], + "next": [ + "B", + "C" + ] + }, + "B": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.407, + 0.329 + ], + "next": [] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.412, + 0.583 + ], + "next": [] + } +} diff --git a/resource/maps/normal/1-2.json b/resource/maps/normal/1-2.json new file mode 100644 index 0000000..5677405 --- /dev/null +++ b/resource/maps/normal/1-2.json @@ -0,0 +1,60 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.243, + 0.711 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.265, + 0.506 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.383, + 0.632 + ], + "next": [ + "C", + "D" + ] + }, + "C": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.367, + 0.339 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.521, + 0.735 + ], + "next": [] + } +} diff --git a/resource/maps/normal/1-3.json b/resource/maps/normal/1-3.json new file mode 100644 index 0000000..c54ab8e --- /dev/null +++ b/resource/maps/normal/1-3.json @@ -0,0 +1,85 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.239, + 0.748 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.349, + 0.591 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.452, + 0.803 + ], + "next": [ + "C", + "D" + ] + }, + "C": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.494, + 0.56 + ], + "next": [ + "D", + "E" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.618, + 0.697 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.442, + 0.335 + ], + "next": [] + }, + "F": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.655, + 0.397 + ], + "next": [] + } +} diff --git a/resource/maps/normal/1-4.json b/resource/maps/normal/1-4.json new file mode 100644 index 0000000..2547a46 --- /dev/null +++ b/resource/maps/normal/1-4.json @@ -0,0 +1,96 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.638, + 0.875 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.518, + 0.73 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.693, + 0.596 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.4, + 0.663 + ], + "next": [ + "E" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.637, + 0.322 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.234, + 0.64 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.417, + 0.396 + ], + "next": [ + "E", + "G" + ] + }, + "G": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.292, + 0.269 + ], + "next": [] + } +} diff --git a/resource/maps/normal/1-5.json b/resource/maps/normal/1-5.json new file mode 100644 index 0000000..ba8a794 --- /dev/null +++ b/resource/maps/normal/1-5.json @@ -0,0 +1,24 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.368, + 0.677 + ], + "next": [ + "A" + ] + }, + "A": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.614, + 0.458 + ], + "next": [] + } +} diff --git a/resource/maps/normal/10-1.json b/resource/maps/normal/10-1.json new file mode 100644 index 0000000..8bbc855 --- /dev/null +++ b/resource/maps/normal/10-1.json @@ -0,0 +1,192 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.938, + 0.793 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.868, + 0.321 + ], + "next": [ + "C", + "I" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.82, + 0.554 + ], + "next": [ + "D", + "F" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": true, + "position": [ + 0.711, + 0.11 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.712, + 0.819 + ], + "next": [ + "E", + "G" + ] + }, + "E": { + "type": "Suppress", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.917 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.457 + ], + "next": [ + "H" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.523, + 0.735 + ], + "next": [ + "L" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.469, + 0.513 + ], + "next": [ + "L" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.57, + 0.334 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "Suppress", + "detour": false, + "night": false, + "position": [ + 0.452, + 0.137 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.36, + 0.445 + ], + "next": [ + "N", + "O" + ] + }, + "L": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.329, + 0.751 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.133, + 0.682 + ], + "next": [ + "O" + ] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.25, + 0.264 + ], + "next": [ + "O" + ] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.064, + 0.375 + ], + "next": [] + } +} diff --git a/resource/maps/normal/2-1.json b/resource/maps/normal/2-1.json new file mode 100644 index 0000000..df6bece --- /dev/null +++ b/resource/maps/normal/2-1.json @@ -0,0 +1,86 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.181, + 0.268 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.343, + 0.248 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.255, + 0.492 + ], + "next": [ + "C", + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.489, + 0.315 + ], + "next": [ + "E" + ] + }, + "D": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.324, + 0.75 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.578, + 0.556 + ], + "next": [ + "F" + ] + }, + "F": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.507, + 0.821 + ], + "next": [] + } +} diff --git a/resource/maps/normal/2-2.json b/resource/maps/normal/2-2.json new file mode 100644 index 0000000..1999962 --- /dev/null +++ b/resource/maps/normal/2-2.json @@ -0,0 +1,105 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.524, + 0.236 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.571, + 0.473 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.414, + 0.445 + ], + "next": [ + "E", + "F" + ] + }, + "C": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.704, + 0.386 + ], + "next": [] + }, + "D": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.616, + 0.738 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.679 + ], + "next": [ + "G" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.255, + 0.421 + ], + "next": [ + "H" + ] + }, + "G": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.358, + 0.875 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.208, + 0.201 + ], + "next": [] + } +} diff --git a/resource/maps/normal/2-3.json b/resource/maps/normal/2-3.json new file mode 100644 index 0000000..b976978 --- /dev/null +++ b/resource/maps/normal/2-3.json @@ -0,0 +1,144 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.171, + 0.887 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.137, + 0.619 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.336, + 0.79 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.199, + 0.382 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.364, + 0.521 + ], + "next": [ + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.583, + 0.881 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.35, + 0.21 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.475, + 0.381 + ], + "next": [ + "I", + "J" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.726, + 0.811 + ], + "next": [ + "K" + ] + }, + "I": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.538, + 0.166 + ], + "next": [] + }, + "J": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.7, + 0.369 + ], + "next": [] + }, + "K": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.82, + 0.527 + ], + "next": [] + } +} diff --git a/resource/maps/normal/2-4.json b/resource/maps/normal/2-4.json new file mode 100644 index 0000000..4761bd5 --- /dev/null +++ b/resource/maps/normal/2-4.json @@ -0,0 +1,156 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.176, + 0.904 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.117, + 0.689 + ], + "next": [ + "B", + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.297, + 0.709 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.25, + 0.37 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.457, + 0.591 + ], + "next": [ + "G", + "J" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.43, + 0.778 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.461, + 0.407 + ], + "next": [ + "I", + "J" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.592, + 0.694 + ], + "next": [ + "K" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.536, + 0.902 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.548, + 0.221 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.637, + 0.403 + ], + "next": [ + "L" + ] + }, + "K": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.778, + 0.808 + ], + "next": [] + }, + "L": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.848, + 0.306 + ], + "next": [] + } +} diff --git a/resource/maps/normal/2-5.json b/resource/maps/normal/2-5.json new file mode 100644 index 0000000..4dd4c9a --- /dev/null +++ b/resource/maps/normal/2-5.json @@ -0,0 +1,189 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.059, + 0.179 + ], + "next": [ + "A" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.211, + 0.201 + ], + "next": [ + "B" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.35, + 0.293 + ], + "next": [ + "C", + "D", + "E" + ] + }, + "C": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.477, + 0.221 + ], + "next": [ + "F", + "G" + ] + }, + "D": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.405, + 0.457 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.223, + 0.576 + ], + "next": [ + "I" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.673, + 0.14 + ], + "next": [ + "J" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.637, + 0.353 + ], + "next": [ + "J" + ] + }, + "H": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.485, + 0.612 + ], + "next": [ + "K" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.476, + 0.748 + ], + "next": [ + "L", + "M", + "N" + ] + }, + "J": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.812, + 0.305 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.633, + 0.53 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.773, + 0.738 + ], + "next": [] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.653, + 0.889 + ], + "next": [] + }, + "N": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.309, + 0.917 + ], + "next": [] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.858, + 0.567 + ], + "next": [] + } +} diff --git a/resource/maps/normal/2-6.json b/resource/maps/normal/2-6.json new file mode 100644 index 0000000..36786f8 --- /dev/null +++ b/resource/maps/normal/2-6.json @@ -0,0 +1,171 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.149, + 0.832 + ], + "next": [ + "A", + "B", + "E" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.584 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.351, + 0.902 + ], + "next": [ + "F" + ] + }, + "C": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.118, + 0.306 + ], + "next": [ + "G" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.296, + 0.486 + ], + "next": [ + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.376, + 0.697 + ], + "next": [ + "D", + "K" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.54, + 0.744 + ], + "next": [ + "H" + ] + }, + "G": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.374, + 0.193 + ], + "next": [ + "I", + "J" + ] + }, + "H": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.68, + 0.779 + ], + "next": [ + "K" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.574, + 0.218 + ], + "next": [ + "L" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.524, + 0.388 + ], + "next": [ + "L" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.657, + 0.507 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.734, + 0.194 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.876, + 0.597 + ], + "next": [] + } +} diff --git a/resource/maps/normal/3-1.json b/resource/maps/normal/3-1.json new file mode 100644 index 0000000..a78c39f --- /dev/null +++ b/resource/maps/normal/3-1.json @@ -0,0 +1,119 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.641, + 0.222 + ], + "next": [ + "A" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.452, + 0.21 + ], + "next": [ + "B", + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.394, + 0.375 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.312, + 0.264 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.671, + 0.472 + ], + "next": [] + }, + "E": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.402, + 0.556 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.195, + 0.472 + ], + "next": [ + "H" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.68 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.219, + 0.751 + ], + "next": [ + "I" + ] + }, + "I": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.391, + 0.847 + ], + "next": [] + } +} diff --git a/resource/maps/normal/3-2.json b/resource/maps/normal/3-2.json new file mode 100644 index 0000000..ffb39b7 --- /dev/null +++ b/resource/maps/normal/3-2.json @@ -0,0 +1,97 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.758, + 0.222 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.821, + 0.402 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.649, + 0.513 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.625, + 0.361 + ], + "next": [ + "E" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.739, + 0.599 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.484, + 0.572 + ], + "next": [ + "G" + ] + }, + "F": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.712, + 0.833 + ], + "next": [] + }, + "G": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.461, + 0.847 + ], + "next": [] + } +} diff --git a/resource/maps/normal/3-3.json b/resource/maps/normal/3-3.json new file mode 100644 index 0000000..b31b36e --- /dev/null +++ b/resource/maps/normal/3-3.json @@ -0,0 +1,146 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.812, + 0.22 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.82, + 0.541 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.742, + 0.444 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.683, + 0.689 + ], + "next": [ + "F", + "G" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.616, + 0.467 + ], + "next": [ + "G", + "I" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.484, + 0.291 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.561, + 0.867 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.554, + 0.652 + ], + "next": [ + "J" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.474 + ], + "next": [ + "J" + ] + }, + "I": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.462, + 0.554 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.367, + 0.764 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.142, + 0.777 + ], + "next": [] + } +} diff --git a/resource/maps/normal/3-4.json b/resource/maps/normal/3-4.json new file mode 100644 index 0000000..718c1dc --- /dev/null +++ b/resource/maps/normal/3-4.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.805, + 0.194 + ], + "next": [ + "A" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.687, + 0.383 + ], + "next": [ + "B", + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.665, + 0.632 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.563, + 0.468 + ], + "next": [ + "D", + "E" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.547, + 0.805 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.479 + ], + "next": [ + "G", + "H", + "I" + ] + }, + "F": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.375, + 0.873 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.398, + 0.668 + ], + "next": [ + "F" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.219, + 0.612 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.265, + 0.41 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.133, + 0.249 + ], + "next": [] + } +} diff --git a/resource/maps/normal/4-1.json b/resource/maps/normal/4-1.json new file mode 100644 index 0000000..107cbd4 --- /dev/null +++ b/resource/maps/normal/4-1.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.788, + 0.236 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.668, + 0.487 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.633, + 0.327 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.609, + 0.695 + ], + "next": [ + "E", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.507, + 0.404 + ], + "next": [ + "F", + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.686, + 0.848 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.464, + 0.57 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.258, + 0.486 + ], + "next": [ + "I", + "J" + ] + }, + "H": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.828, + 0.818 + ], + "next": [] + }, + "I": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.274, + 0.846 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.093, + 0.571 + ], + "next": [] + } +} diff --git a/resource/maps/normal/4-2.json b/resource/maps/normal/4-2.json new file mode 100644 index 0000000..4a7b417 --- /dev/null +++ b/resource/maps/normal/4-2.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.827, + 0.804 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.657, + 0.751 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.789, + 0.513 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.538, + 0.699 + ], + "next": [ + "E" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.664, + 0.568 + ], + "next": [ + "C", + "F" + ] + }, + "E": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.308, + 0.627 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.574, + 0.322 + ], + "next": [ + "H", + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.262, + 0.812 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.419, + 0.41 + ], + "next": [ + "J" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.336, + 0.21 + ], + "next": [] + }, + "J": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.219, + 0.25 + ], + "next": [] + } +} diff --git a/resource/maps/normal/4-3.json b/resource/maps/normal/4-3.json new file mode 100644 index 0000000..678e8a9 --- /dev/null +++ b/resource/maps/normal/4-3.json @@ -0,0 +1,110 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.867, + 0.334 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.782, + 0.502 + ], + "next": [ + "C", + "E" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.648, + 0.256 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.639, + 0.548 + ], + "next": [ + "E", + "G" + ] + }, + "D": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.359, + 0.222 + ], + "next": [ + "F" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.508, + 0.522 + ], + "next": [ + "G" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.208, + 0.527 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.37, + 0.513 + ], + "next": [ + "F", + "H" + ] + }, + "H": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.16, + 0.847 + ], + "next": [] + } +} diff --git a/resource/maps/normal/4-4.json b/resource/maps/normal/4-4.json new file mode 100644 index 0000000..cd02849 --- /dev/null +++ b/resource/maps/normal/4-4.json @@ -0,0 +1,159 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.836, + 0.485 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.624, + 0.854 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.695, + 0.597 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.316, + 0.831 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.443, + 0.792 + ], + "next": [ + "K" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.574, + 0.569 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.203, + 0.665 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.424 + ], + "next": [ + "K" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.547, + 0.304 + ], + "next": [ + "J" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.144, + 0.474 + ], + "next": [ + "L" + ] + }, + "J": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.414, + 0.229 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.269, + 0.368 + ], + "next": [ + "L" + ] + }, + "L": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.137, + 0.23 + ], + "next": [] + } +} diff --git a/resource/maps/normal/5-1.json b/resource/maps/normal/5-1.json new file mode 100644 index 0000000..bdbbf7b --- /dev/null +++ b/resource/maps/normal/5-1.json @@ -0,0 +1,111 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.891, + 0.779 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.711, + 0.722 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.802, + 0.375 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.516, + 0.861 + ], + "next": [ + "E", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.54, + 0.46 + ], + "next": [ + "F", + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.121, + 0.679 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.265, + 0.542 + ], + "next": [ + "G", + "H" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.358, + 0.209 + ], + "next": [] + }, + "H": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.178 + ], + "next": [] + } +} diff --git a/resource/maps/normal/5-2.json b/resource/maps/normal/5-2.json new file mode 100644 index 0000000..7f84520 --- /dev/null +++ b/resource/maps/normal/5-2.json @@ -0,0 +1,134 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.852, + 0.82 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.704, + 0.863 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.679, + 0.547 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.875, + 0.445 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.421, + 0.778 + ], + "next": [ + "G", + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.503, + 0.534 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.609, + 0.207 + ], + "next": [ + "H", + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.188, + 0.895 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.277, + 0.497 + ], + "next": [ + "J" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.208 + ], + "next": [ + "J" + ] + }, + "J": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.101, + 0.291 + ], + "next": [] + } +} diff --git a/resource/maps/normal/5-3.json b/resource/maps/normal/5-3.json new file mode 100644 index 0000000..cf0bd1e --- /dev/null +++ b/resource/maps/normal/5-3.json @@ -0,0 +1,107 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.836, + 0.236 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.742, + 0.652 + ], + "next": [ + "C", + "E" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.657, + 0.298 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.805, + 0.854 + ], + "next": [] + }, + "D": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.563, + 0.571 + ], + "next": [ + "F", + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.594, + 0.85 + ], + "next": [ + "G" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.497, + 0.355 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.293, + 0.757 + ], + "next": [ + "H" + ] + }, + "H": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.188, + 0.429 + ], + "next": [] + } +} diff --git a/resource/maps/normal/5-4.json b/resource/maps/normal/5-4.json new file mode 100644 index 0000000..969f6fe --- /dev/null +++ b/resource/maps/normal/5-4.json @@ -0,0 +1,110 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.78, + 0.236 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.64, + 0.489 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.562, + 0.174 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.718, + 0.765 + ], + "next": [ + "E", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.336, + 0.347 + ], + "next": [ + "A", + "F" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.507, + 0.876 + ], + "next": [] + }, + "F": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.468, + 0.681 + ], + "next": [ + "G" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.245, + 0.758 + ], + "next": [ + "E", + "H" + ] + }, + "H": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.086, + 0.446 + ], + "next": [] + } +} diff --git a/resource/maps/normal/5-5.json b/resource/maps/normal/5-5.json new file mode 100644 index 0000000..4c70f10 --- /dev/null +++ b/resource/maps/normal/5-5.json @@ -0,0 +1,125 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.883, + 0.375 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.797, + 0.832 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.711, + 0.611 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.617, + 0.222 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.532, + 0.832 + ], + "next": [ + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.484, + 0.472 + ], + "next": [ + "F", + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.437, + 0.216 + ], + "next": [ + "H", + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.266, + 0.841 + ], + "next": [ + "I" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.27, + 0.546 + ], + "next": [ + "I" + ] + }, + "I": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.092, + 0.417 + ], + "next": [] + } +} diff --git a/resource/maps/normal/6-1.json b/resource/maps/normal/6-1.json new file mode 100644 index 0000000..fbbbd21 --- /dev/null +++ b/resource/maps/normal/6-1.json @@ -0,0 +1,154 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.699, + 0.872 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.406, + 0.86 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.547, + 0.653 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.624 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.243, + 0.68 + ], + "next": [ + "G", + "H" + ] + }, + "E": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.405, + 0.68 + ], + "next": [ + "D" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.417 + ], + "next": [ + "H", + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.144, + 0.872 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.257, + 0.415 + ], + "next": [ + "J", + "K", + "L" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.46, + 0.208 + ], + "next": [] + }, + "J": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.085, + 0.582 + ], + "next": [] + }, + "K": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.149, + 0.278 + ], + "next": [] + }, + "L": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.343, + 0.181 + ], + "next": [] + } +} diff --git a/resource/maps/normal/6-2.json b/resource/maps/normal/6-2.json new file mode 100644 index 0000000..ecdf277 --- /dev/null +++ b/resource/maps/normal/6-2.json @@ -0,0 +1,174 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.204, + 0.888 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.07, + 0.432 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.184, + 0.623 + ], + "next": [ + "E", + "F" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.406, + 0.868 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.25, + 0.319 + ], + "next": [ + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.328, + 0.458 + ], + "next": [ + "G", + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.54, + 0.681 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.445, + 0.306 + ], + "next": [ + "J" + ] + }, + "H": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.506, + 0.501 + ], + "next": [ + "I", + "K" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.719, + 0.59 + ], + "next": [ + "M" + ] + }, + "J": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.601, + 0.222 + ], + "next": [ + "M" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.395 + ], + "next": [ + "J", + "L" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.798, + 0.402 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.868, + 0.182 + ], + "next": [] + } +} diff --git a/resource/maps/normal/6-3.json b/resource/maps/normal/6-3.json new file mode 100644 index 0000000..ef50f35 --- /dev/null +++ b/resource/maps/normal/6-3.json @@ -0,0 +1,132 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.241, + 0.807 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.423, + 0.863 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.304, + 0.541 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.173, + 0.514 + ], + "next": [ + "E", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.587, + 0.812 + ], + "next": [ + "G", + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.469, + 0.422 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.358, + 0.186 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.727, + 0.597 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.664, + 0.457 + ], + "next": [ + "J" + ] + }, + "I": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.563, + 0.271 + ], + "next": [] + }, + "J": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.88, + 0.284 + ], + "next": [] + } +} diff --git a/resource/maps/normal/6-4.json b/resource/maps/normal/6-4.json new file mode 100644 index 0000000..9421960 --- /dev/null +++ b/resource/maps/normal/6-4.json @@ -0,0 +1,154 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.129, + 0.799 + ], + "next": [ + "A" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.145, + 0.542 + ], + "next": [ + "B", + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.394, + 0.542 + ], + "next": [ + "D", + "E", + "G" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.249, + 0.299 + ], + "next": [ + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.226, + 0.765 + ], + "next": [] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.439, + 0.86 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.445, + 0.25 + ], + "next": [ + "G", + "H", + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.532, + 0.595 + ], + "next": [ + "K" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.625, + 0.425 + ], + "next": [ + "J" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.66, + 0.178 + ], + "next": [] + }, + "J": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.804, + 0.583 + ], + "next": [ + "K", + "L" + ] + }, + "K": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.688, + 0.848 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.937, + 0.808 + ], + "next": [] + } +} diff --git a/resource/maps/normal/7-1.json b/resource/maps/normal/7-1.json new file mode 100644 index 0000000..83f6e83 --- /dev/null +++ b/resource/maps/normal/7-1.json @@ -0,0 +1,180 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.867, + 0.65 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.743, + 0.778 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.695, + 0.361 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.606, + 0.682 + ], + "next": [ + "F", + "G" + ] + }, + "D": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.567, + 0.431 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.624, + 0.194 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.454, + 0.821 + ], + "next": [ + "L" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.454, + 0.611 + ], + "next": [ + "I" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.433, + 0.36 + ], + "next": [ + "J", + "K" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.308, + 0.575 + ], + "next": [ + "K", + "M", + "N" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.244, + 0.223 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.281, + 0.388 + ], + "next": [ + "N" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.284, + 0.852 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.226, + 0.686 + ], + "next": [] + }, + "N": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.094, + 0.501 + ], + "next": [] + } +} diff --git a/resource/maps/normal/7-2.json b/resource/maps/normal/7-2.json new file mode 100644 index 0000000..10c63af --- /dev/null +++ b/resource/maps/normal/7-2.json @@ -0,0 +1,167 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.775, + 0.666 + ], + "next": [ + "A", + "B", + "F" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.539, + 0.571 + ], + "next": [ + "C", + "D", + "E" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.458 + ], + "next": [ + "G" + ] + }, + "C": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.524, + 0.776 + ], + "next": [ + "I" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.402, + 0.654 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.478, + 0.334 + ], + "next": [ + "H", + "K" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.789, + 0.389 + ], + "next": [ + "G", + "L" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.687, + 0.222 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.282, + 0.585 + ], + "next": [ + "J", + "K" + ] + }, + "I": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.347, + 0.877 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.133, + 0.681 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.266, + 0.301 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.91, + 0.243 + ], + "next": [] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.117, + 0.236 + ], + "next": [] + } +} diff --git a/resource/maps/normal/7-3.json b/resource/maps/normal/7-3.json new file mode 100644 index 0000000..408d51b --- /dev/null +++ b/resource/maps/normal/7-3.json @@ -0,0 +1,181 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.837, + 0.556 + ], + "next": [ + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.673, + 0.806 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.689, + 0.57 + ], + "next": [ + "A", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.786, + 0.328 + ], + "next": [ + "F", + "G" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.495, + 0.881 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.565, + 0.695 + ], + "next": [ + "D", + "I" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.586, + 0.473 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.64, + 0.217 + ], + "next": [ + "J", + "K" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.335, + 0.79 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.379, + 0.667 + ], + "next": [ + "L" + ] + }, + "J": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.389 + ], + "next": [ + "L" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.512, + 0.196 + ], + "next": [ + "M" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.219, + 0.528 + ], + "next": [ + "N" + ] + }, + "M": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.328, + 0.265 + ], + "next": [] + }, + "N": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.125, + 0.322 + ], + "next": [] + } +} diff --git a/resource/maps/normal/7-4.json b/resource/maps/normal/7-4.json new file mode 100644 index 0000000..5e4a78b --- /dev/null +++ b/resource/maps/normal/7-4.json @@ -0,0 +1,171 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.817, + 0.329 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.79, + 0.534 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.711, + 0.188 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.719, + 0.701 + ], + "next": [ + "D", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.579, + 0.438 + ], + "next": [ + "B", + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.521, + 0.244 + ], + "next": [ + "D" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.63, + 0.813 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.434, + 0.431 + ], + "next": [ + "E", + "K", + "L" + ] + }, + "H": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.397, + 0.216 + ], + "next": [ + "M" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.496, + 0.867 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.397, + 0.784 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.351, + 0.651 + ], + "next": [ + "J", + "M" + ] + }, + "L": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.313, + 0.313 + ], + "next": [ + "H", + "M" + ] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.184, + 0.311 + ], + "next": [] + } +} diff --git a/resource/maps/normal/7-5.json b/resource/maps/normal/7-5.json new file mode 100644 index 0000000..479f046 --- /dev/null +++ b/resource/maps/normal/7-5.json @@ -0,0 +1,220 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.899, + 0.675 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.797, + 0.8 + ], + "next": [ + "C" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.766, + 0.521 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.658, + 0.862 + ], + "next": [ + "G" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.656, + 0.604 + ], + "next": [ + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.634, + 0.411 + ], + "next": [ + "I", + "J" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.734, + 0.272 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.47, + 0.883 + ], + "next": [ + "K", + "L" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.52, + 0.617 + ], + "next": [ + "L" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.479, + 0.313 + ], + "next": [ + "L", + "O" + ] + }, + "J": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.587, + 0.199 + ], + "next": [ + "F", + "M" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.305, + 0.84 + ], + "next": [ + "P" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": true, + "position": [ + 0.383, + 0.591 + ], + "next": [ + "N" + ] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.429, + 0.186 + ], + "next": [ + "O" + ] + }, + "N": { + "type": "Resource", + "detour": false, + "night": false, + "position": [ + 0.243, + 0.529 + ], + "next": [ + "Q" + ] + }, + "O": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.246, + 0.243 + ], + "next": [ + "Q" + ] + }, + "P": { + "type": "Penalty", + "detour": false, + "night": false, + "position": [ + 0.148, + 0.695 + ], + "next": [ + "Q" + ] + }, + "Q": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.102, + 0.421 + ], + "next": [] + } +} diff --git a/resource/maps/normal/8-1.json b/resource/maps/normal/8-1.json new file mode 100644 index 0000000..be1e998 --- /dev/null +++ b/resource/maps/normal/8-1.json @@ -0,0 +1,156 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.115, + 0.282 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.229, + 0.439 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.324, + 0.238 + ], + "next": [ + "E", + "F" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.292, + 0.686 + ], + "next": [] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.416, + 0.565 + ], + "next": [ + "G", + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.474, + 0.359 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.574, + 0.243 + ], + "next": [ + "I", + "J" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.554, + 0.842 + ], + "next": [ + "K", + "L" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.619, + 0.667 + ], + "next": [ + "L" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.653, + 0.441 + ], + "next": [ + "H" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.783, + 0.284 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.393, + 0.864 + ], + "next": [] + }, + "L": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.819, + 0.704 + ], + "next": [] + } +} diff --git a/resource/maps/normal/8-2.json b/resource/maps/normal/8-2.json new file mode 100644 index 0000000..34e000b --- /dev/null +++ b/resource/maps/normal/8-2.json @@ -0,0 +1,170 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.321, + 0.709 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.507, + 0.791 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Aerial", + "detour": false, + "night": false, + "position": [ + 0.516, + 0.541 + ], + "next": [ + "E", + "J" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.217, + 0.472 + ], + "next": [ + "F", + "G" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.688, + 0.752 + ], + "next": [ + "H", + "I" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.673, + 0.542 + ], + "next": [ + "I" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.391, + 0.263 + ], + "next": [ + "J", + "K" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.227, + 0.194 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.844, + 0.849 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.821, + 0.626 + ], + "next": [ + "M" + ] + }, + "J": { + "type": "Aerial", + "detour": false, + "night": false, + "position": [ + 0.595, + 0.373 + ], + "next": [ + "M" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.578, + 0.195 + ], + "next": [ + "L" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.749, + 0.22 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.875, + 0.32 + ], + "next": [] + } +} diff --git a/resource/maps/normal/8-3.json b/resource/maps/normal/8-3.json new file mode 100644 index 0000000..5cfea69 --- /dev/null +++ b/resource/maps/normal/8-3.json @@ -0,0 +1,160 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.758, + 0.805 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.836, + 0.359 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.608, + 0.64 + ], + "next": [ + "E", + "F", + "G" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.703, + 0.431 + ], + "next": [ + "E" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.562, + 0.222 + ], + "next": [ + "H", + "K" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.493, + 0.401 + ], + "next": [ + "H" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.39, + 0.735 + ], + "next": [ + "I", + "J" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.407, + 0.528 + ], + "next": [ + "H", + "I" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.374 + ], + "next": [ + "K", + "L" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.181, + 0.57 + ], + "next": [ + "L" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.485, + 0.89 + ], + "next": [] + }, + "K": { + "type": "Hard", + "detour": false, + "night": false, + "position": [ + 0.297, + 0.152 + ], + "next": [] + }, + "L": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.149, + 0.308 + ], + "next": [] + } +} diff --git a/resource/maps/normal/8-4.json b/resource/maps/normal/8-4.json new file mode 100644 index 0000000..f8f0bef --- /dev/null +++ b/resource/maps/normal/8-4.json @@ -0,0 +1,169 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.923, + 0.293 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.711, + 0.223 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.829, + 0.528 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.516, + 0.207 + ], + "next": [ + "F", + "G", + "H" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.617, + 0.541 + ], + "next": [ + "H", + "I", + "J" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.805, + 0.819 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.32, + 0.181 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.289, + 0.306 + ], + "next": [ + "M" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.413, + 0.486 + ], + "next": [ + "M" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.462, + 0.721 + ], + "next": [ + "K", + "L", + "M" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.585, + 0.89 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.351, + 0.93 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.273, + 0.709 + ], + "next": [ + "M" + ] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.132, + 0.541 + ], + "next": [] + } +} diff --git a/resource/maps/normal/8-5.json b/resource/maps/normal/8-5.json new file mode 100644 index 0000000..4191f5a --- /dev/null +++ b/resource/maps/normal/8-5.json @@ -0,0 +1,167 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.196, + 0.721 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.18, + 0.374 + ], + "next": [ + "D", + "I" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.305, + 0.403 + ], + "next": [ + "D" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.391, + 0.888 + ], + "next": [ + "E", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.454, + 0.249 + ], + "next": [ + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.528 + ], + "next": [ + "G" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.601, + 0.722 + ], + "next": [ + "H", + "K", + "L" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.703, + 0.237 + ], + "next": [ + "J", + "M" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.743, + 0.486 + ], + "next": [ + "M" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": true, + "position": [ + 0.274, + 0.151 + ], + "next": [] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.876, + 0.14 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.867, + 0.753 + ], + "next": [] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.766, + 0.917 + ], + "next": [] + }, + "M": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.921, + 0.361 + ], + "next": [] + } +} diff --git a/resource/maps/normal/9-1.json b/resource/maps/normal/9-1.json new file mode 100644 index 0000000..9993c76 --- /dev/null +++ b/resource/maps/normal/9-1.json @@ -0,0 +1,194 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.086, + 0.308 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Aerial", + "detour": false, + "night": false, + "position": [ + 0.281, + 0.18 + ], + "next": [ + "D", + "E" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.226, + 0.458 + ], + "next": [ + "F", + "G" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.141, + 0.626 + ], + "next": [ + "G" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.642, + 0.221 + ], + "next": [ + "L", + "M" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.499, + 0.348 + ], + "next": [ + "J" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.391, + 0.597 + ], + "next": [ + "I", + "J" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.314, + 0.778 + ], + "next": [ + "H", + "I" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.453, + 0.931 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.5, + 0.833 + ], + "next": [ + "O" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.561, + 0.737 + ], + "next": [ + "K" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.634, + 0.391 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.797, + 0.139 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.765, + 0.432 + ], + "next": [ + "N", + "O" + ] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.906, + 0.614 + ], + "next": [] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.765, + 0.777 + ], + "next": [] + } +} diff --git a/resource/maps/normal/9-2.json b/resource/maps/normal/9-2.json new file mode 100644 index 0000000..dc732d0 --- /dev/null +++ b/resource/maps/normal/9-2.json @@ -0,0 +1,195 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.062, + 0.333 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.227, + 0.43 + ], + "next": [ + "D", + "E" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.266, + 0.266 + ], + "next": [ + "E" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.21, + 0.125 + ], + "next": [ + "E", + "F" + ] + }, + "D": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.382, + 0.571 + ], + "next": [ + "G", + "H" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.461, + 0.362 + ], + "next": [ + "H", + "K" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.406, + 0.139 + ], + "next": [ + "I" + ] + }, + "G": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.461, + 0.847 + ], + "next": [ + "H", + "L" + ] + }, + "H": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.586, + 0.583 + ], + "next": [ + "M" + ] + }, + "I": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.571, + 0.18 + ], + "next": [ + "J", + "K" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.789, + 0.192 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.719, + 0.377 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.648, + 0.918 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.711, + 0.806 + ], + "next": [ + "N", + "O" + ] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.923, + 0.89 + ], + "next": [] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.876, + 0.557 + ], + "next": [] + } +} diff --git a/resource/maps/normal/9-3.json b/resource/maps/normal/9-3.json new file mode 100644 index 0000000..95f7f86 --- /dev/null +++ b/resource/maps/normal/9-3.json @@ -0,0 +1,192 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.345, + 0.224 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Aerial", + "detour": false, + "night": false, + "position": [ + 0.516, + 0.138 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.445, + 0.528 + ], + "next": [ + "H", + "I" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.587, + 0.332 + ], + "next": [ + "I", + "K" + ] + }, + "D": { + "type": "Aerial", + "detour": false, + "night": false, + "position": [ + 0.687, + 0.153 + ], + "next": [ + "E", + "F", + "G" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.906, + 0.151 + ], + "next": [] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.867, + 0.278 + ], + "next": [ + "J" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.774, + 0.404 + ], + "next": [ + "J", + "K" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.272, + 0.679 + ], + "next": [] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.617, + 0.64 + ], + "next": [ + "N" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.953, + 0.584 + ], + "next": [ + "L" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.742, + 0.693 + ], + "next": [ + "M", + "N" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.883, + 0.819 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.672, + 0.944 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.555, + 0.889 + ], + "next": [ + "O" + ] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.321, + 0.847 + ], + "next": [] + } +} diff --git a/resource/maps/normal/9-4.json b/resource/maps/normal/9-4.json new file mode 100644 index 0000000..e7fa191 --- /dev/null +++ b/resource/maps/normal/9-4.json @@ -0,0 +1,193 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.884, + 0.154 + ], + "next": [ + "A", + "B" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.821, + 0.415 + ], + "next": [ + "C", + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.688, + 0.235 + ], + "next": [ + "D", + "E" + ] + }, + "C": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.836, + 0.708 + ], + "next": [ + "F", + "G" + ] + }, + "D": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.673, + 0.542 + ], + "next": [ + "G" + ] + }, + "E": { + "type": "Normal", + "detour": true, + "night": false, + "position": [ + 0.571, + 0.416 + ], + "next": [ + "H", + "I", + "J" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.688, + 0.877 + ], + "next": [] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.563, + 0.82 + ], + "next": [ + "K" + ] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.484, + 0.68 + ], + "next": [ + "K", + "L" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.399, + 0.473 + ], + "next": [ + "L", + "M" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.406, + 0.362 + ], + "next": [] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.384, + 0.918 + ], + "next": [ + "N", + "O" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.281, + 0.624 + ], + "next": [ + "O" + ] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.202, + 0.415 + ], + "next": [] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.172, + 0.875 + ], + "next": [] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.101, + 0.762 + ], + "next": [] + } +} diff --git a/resource/maps/normal/9-5.json b/resource/maps/normal/9-5.json new file mode 100644 index 0000000..433a4e9 --- /dev/null +++ b/resource/maps/normal/9-5.json @@ -0,0 +1,195 @@ +{ + "0": { + "type": "Start", + "detour": false, + "night": false, + "position": [ + 0.921, + 0.529 + ], + "next": [ + "A", + "B", + "C" + ] + }, + "A": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.758, + 0.238 + ], + "next": [ + "D" + ] + }, + "B": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.734, + 0.433 + ], + "next": [ + "E", + "F" + ] + }, + "C": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.704, + 0.653 + ], + "next": [ + "G", + "H" + ] + }, + "D": { + "type": "Normal", + "detour": false, + "night": true, + "position": [ + 0.547, + 0.168 + ], + "next": [ + "I" + ] + }, + "E": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.564, + 0.416 + ], + "next": [ + "J" + ] + }, + "F": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.578, + 0.638 + ], + "next": [ + "K" + ] + }, + "G": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.774, + 0.933 + ], + "next": [] + }, + "H": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.555, + 0.859 + ], + "next": [ + "N" + ] + }, + "I": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.352, + 0.219 + ], + "next": [ + "J", + "L", + "M" + ] + }, + "J": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.414, + 0.456 + ], + "next": [ + "K", + "O" + ] + }, + "K": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.383, + 0.75 + ], + "next": [ + "O" + ] + }, + "L": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.211, + 0.14 + ], + "next": [] + }, + "M": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.21, + 0.373 + ], + "next": [ + "O" + ] + }, + "N": { + "type": "Normal", + "detour": false, + "night": false, + "position": [ + 0.367, + 0.916 + ], + "next": [ + "O" + ] + }, + "O": { + "type": "Boss", + "detour": false, + "night": false, + "position": [ + 0.187, + 0.777 + ], + "next": [] + } +} diff --git a/resource/ship-library/assets/backgrounds/rarity-1.webp b/resource/ship-library/assets/backgrounds/rarity-1.webp new file mode 100644 index 0000000..1fe19dd Binary files /dev/null and b/resource/ship-library/assets/backgrounds/rarity-1.webp differ diff --git a/resource/ship-library/assets/backgrounds/rarity-2.webp b/resource/ship-library/assets/backgrounds/rarity-2.webp new file mode 100644 index 0000000..4077a33 Binary files /dev/null and b/resource/ship-library/assets/backgrounds/rarity-2.webp differ diff --git a/resource/ship-library/assets/backgrounds/rarity-3.webp b/resource/ship-library/assets/backgrounds/rarity-3.webp new file mode 100644 index 0000000..8b497ce Binary files /dev/null and b/resource/ship-library/assets/backgrounds/rarity-3.webp differ diff --git a/resource/ship-library/assets/backgrounds/rarity-4.webp b/resource/ship-library/assets/backgrounds/rarity-4.webp new file mode 100644 index 0000000..2f02c0f Binary files /dev/null and b/resource/ship-library/assets/backgrounds/rarity-4.webp differ diff --git a/resource/ship-library/assets/backgrounds/rarity-5.webp b/resource/ship-library/assets/backgrounds/rarity-5.webp new file mode 100644 index 0000000..b8308b1 Binary files /dev/null and b/resource/ship-library/assets/backgrounds/rarity-5.webp differ diff --git a/resource/ship-library/assets/backgrounds/rarity-6.webp b/resource/ship-library/assets/backgrounds/rarity-6.webp new file mode 100644 index 0000000..107afdc Binary files /dev/null and b/resource/ship-library/assets/backgrounds/rarity-6.webp differ diff --git a/resource/ship-library/assets/frames/rarity-1.webp b/resource/ship-library/assets/frames/rarity-1.webp new file mode 100644 index 0000000..b2912a1 Binary files /dev/null and b/resource/ship-library/assets/frames/rarity-1.webp differ diff --git a/resource/ship-library/assets/frames/rarity-2.webp b/resource/ship-library/assets/frames/rarity-2.webp new file mode 100644 index 0000000..0b595c6 Binary files /dev/null and b/resource/ship-library/assets/frames/rarity-2.webp differ diff --git a/resource/ship-library/assets/frames/rarity-3.webp b/resource/ship-library/assets/frames/rarity-3.webp new file mode 100644 index 0000000..77ba4cd Binary files /dev/null and b/resource/ship-library/assets/frames/rarity-3.webp differ diff --git a/resource/ship-library/assets/frames/rarity-4.webp b/resource/ship-library/assets/frames/rarity-4.webp new file mode 100644 index 0000000..bf7ba96 Binary files /dev/null and b/resource/ship-library/assets/frames/rarity-4.webp differ diff --git a/resource/ship-library/assets/frames/rarity-5.webp b/resource/ship-library/assets/frames/rarity-5.webp new file mode 100644 index 0000000..bd027a5 Binary files /dev/null and b/resource/ship-library/assets/frames/rarity-5.webp differ diff --git a/resource/ship-library/assets/frames/rarity-6.webp b/resource/ship-library/assets/frames/rarity-6.webp new file mode 100644 index 0000000..cf01e7a Binary files /dev/null and b/resource/ship-library/assets/frames/rarity-6.webp differ diff --git a/resource/ship-library/assets/portraits/1.webp b/resource/ship-library/assets/portraits/1.webp new file mode 100644 index 0000000..4c3b73b Binary files /dev/null and b/resource/ship-library/assets/portraits/1.webp differ diff --git a/resource/ship-library/assets/portraits/10.webp b/resource/ship-library/assets/portraits/10.webp new file mode 100644 index 0000000..6a8ba13 Binary files /dev/null and b/resource/ship-library/assets/portraits/10.webp differ diff --git a/resource/ship-library/assets/portraits/100.webp b/resource/ship-library/assets/portraits/100.webp new file mode 100644 index 0000000..dae8b97 Binary files /dev/null and b/resource/ship-library/assets/portraits/100.webp differ diff --git a/resource/ship-library/assets/portraits/1001.webp b/resource/ship-library/assets/portraits/1001.webp new file mode 100644 index 0000000..c0ebac6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1001.webp differ diff --git a/resource/ship-library/assets/portraits/1002.webp b/resource/ship-library/assets/portraits/1002.webp new file mode 100644 index 0000000..10ee510 Binary files /dev/null and b/resource/ship-library/assets/portraits/1002.webp differ diff --git a/resource/ship-library/assets/portraits/1003.webp b/resource/ship-library/assets/portraits/1003.webp new file mode 100644 index 0000000..52de6f2 Binary files /dev/null and b/resource/ship-library/assets/portraits/1003.webp differ diff --git a/resource/ship-library/assets/portraits/1004.webp b/resource/ship-library/assets/portraits/1004.webp new file mode 100644 index 0000000..523fece Binary files /dev/null and b/resource/ship-library/assets/portraits/1004.webp differ diff --git a/resource/ship-library/assets/portraits/1005.webp b/resource/ship-library/assets/portraits/1005.webp new file mode 100644 index 0000000..2d4a15c Binary files /dev/null and b/resource/ship-library/assets/portraits/1005.webp differ diff --git a/resource/ship-library/assets/portraits/1006.webp b/resource/ship-library/assets/portraits/1006.webp new file mode 100644 index 0000000..491383e Binary files /dev/null and b/resource/ship-library/assets/portraits/1006.webp differ diff --git a/resource/ship-library/assets/portraits/1007.webp b/resource/ship-library/assets/portraits/1007.webp new file mode 100644 index 0000000..98e7169 Binary files /dev/null and b/resource/ship-library/assets/portraits/1007.webp differ diff --git a/resource/ship-library/assets/portraits/1008.webp b/resource/ship-library/assets/portraits/1008.webp new file mode 100644 index 0000000..55f829d Binary files /dev/null and b/resource/ship-library/assets/portraits/1008.webp differ diff --git a/resource/ship-library/assets/portraits/1009.webp b/resource/ship-library/assets/portraits/1009.webp new file mode 100644 index 0000000..76beee1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1009.webp differ diff --git a/resource/ship-library/assets/portraits/101.webp b/resource/ship-library/assets/portraits/101.webp new file mode 100644 index 0000000..64a0c63 Binary files /dev/null and b/resource/ship-library/assets/portraits/101.webp differ diff --git a/resource/ship-library/assets/portraits/1010.webp b/resource/ship-library/assets/portraits/1010.webp new file mode 100644 index 0000000..4f93261 Binary files /dev/null and b/resource/ship-library/assets/portraits/1010.webp differ diff --git a/resource/ship-library/assets/portraits/1011.webp b/resource/ship-library/assets/portraits/1011.webp new file mode 100644 index 0000000..6057510 Binary files /dev/null and b/resource/ship-library/assets/portraits/1011.webp differ diff --git a/resource/ship-library/assets/portraits/1012.webp b/resource/ship-library/assets/portraits/1012.webp new file mode 100644 index 0000000..fdae8ad Binary files /dev/null and b/resource/ship-library/assets/portraits/1012.webp differ diff --git a/resource/ship-library/assets/portraits/1013.webp b/resource/ship-library/assets/portraits/1013.webp new file mode 100644 index 0000000..50950cc Binary files /dev/null and b/resource/ship-library/assets/portraits/1013.webp differ diff --git a/resource/ship-library/assets/portraits/1014.webp b/resource/ship-library/assets/portraits/1014.webp new file mode 100644 index 0000000..ce8fa8b Binary files /dev/null and b/resource/ship-library/assets/portraits/1014.webp differ diff --git a/resource/ship-library/assets/portraits/1018.webp b/resource/ship-library/assets/portraits/1018.webp new file mode 100644 index 0000000..816804a Binary files /dev/null and b/resource/ship-library/assets/portraits/1018.webp differ diff --git a/resource/ship-library/assets/portraits/1019.webp b/resource/ship-library/assets/portraits/1019.webp new file mode 100644 index 0000000..a307976 Binary files /dev/null and b/resource/ship-library/assets/portraits/1019.webp differ diff --git a/resource/ship-library/assets/portraits/102.webp b/resource/ship-library/assets/portraits/102.webp new file mode 100644 index 0000000..9037b6d Binary files /dev/null and b/resource/ship-library/assets/portraits/102.webp differ diff --git a/resource/ship-library/assets/portraits/1020.webp b/resource/ship-library/assets/portraits/1020.webp new file mode 100644 index 0000000..b29c022 Binary files /dev/null and b/resource/ship-library/assets/portraits/1020.webp differ diff --git a/resource/ship-library/assets/portraits/1021.webp b/resource/ship-library/assets/portraits/1021.webp new file mode 100644 index 0000000..021e53d Binary files /dev/null and b/resource/ship-library/assets/portraits/1021.webp differ diff --git a/resource/ship-library/assets/portraits/1022.webp b/resource/ship-library/assets/portraits/1022.webp new file mode 100644 index 0000000..aadb492 Binary files /dev/null and b/resource/ship-library/assets/portraits/1022.webp differ diff --git a/resource/ship-library/assets/portraits/1023.webp b/resource/ship-library/assets/portraits/1023.webp new file mode 100644 index 0000000..3e30e4e Binary files /dev/null and b/resource/ship-library/assets/portraits/1023.webp differ diff --git a/resource/ship-library/assets/portraits/1024.webp b/resource/ship-library/assets/portraits/1024.webp new file mode 100644 index 0000000..c687121 Binary files /dev/null and b/resource/ship-library/assets/portraits/1024.webp differ diff --git a/resource/ship-library/assets/portraits/1025.webp b/resource/ship-library/assets/portraits/1025.webp new file mode 100644 index 0000000..27cf46d Binary files /dev/null and b/resource/ship-library/assets/portraits/1025.webp differ diff --git a/resource/ship-library/assets/portraits/1026.webp b/resource/ship-library/assets/portraits/1026.webp new file mode 100644 index 0000000..ae514ef Binary files /dev/null and b/resource/ship-library/assets/portraits/1026.webp differ diff --git a/resource/ship-library/assets/portraits/1027.webp b/resource/ship-library/assets/portraits/1027.webp new file mode 100644 index 0000000..5c8dc82 Binary files /dev/null and b/resource/ship-library/assets/portraits/1027.webp differ diff --git a/resource/ship-library/assets/portraits/1028.webp b/resource/ship-library/assets/portraits/1028.webp new file mode 100644 index 0000000..fb64f32 Binary files /dev/null and b/resource/ship-library/assets/portraits/1028.webp differ diff --git a/resource/ship-library/assets/portraits/1029.webp b/resource/ship-library/assets/portraits/1029.webp new file mode 100644 index 0000000..0021810 Binary files /dev/null and b/resource/ship-library/assets/portraits/1029.webp differ diff --git a/resource/ship-library/assets/portraits/103.webp b/resource/ship-library/assets/portraits/103.webp new file mode 100644 index 0000000..d592244 Binary files /dev/null and b/resource/ship-library/assets/portraits/103.webp differ diff --git a/resource/ship-library/assets/portraits/1030.webp b/resource/ship-library/assets/portraits/1030.webp new file mode 100644 index 0000000..5466226 Binary files /dev/null and b/resource/ship-library/assets/portraits/1030.webp differ diff --git a/resource/ship-library/assets/portraits/1031.webp b/resource/ship-library/assets/portraits/1031.webp new file mode 100644 index 0000000..169e0bc Binary files /dev/null and b/resource/ship-library/assets/portraits/1031.webp differ diff --git a/resource/ship-library/assets/portraits/1032.webp b/resource/ship-library/assets/portraits/1032.webp new file mode 100644 index 0000000..27f6bc8 Binary files /dev/null and b/resource/ship-library/assets/portraits/1032.webp differ diff --git a/resource/ship-library/assets/portraits/1033.webp b/resource/ship-library/assets/portraits/1033.webp new file mode 100644 index 0000000..f6c9ad2 Binary files /dev/null and b/resource/ship-library/assets/portraits/1033.webp differ diff --git a/resource/ship-library/assets/portraits/1034.webp b/resource/ship-library/assets/portraits/1034.webp new file mode 100644 index 0000000..58e30a0 Binary files /dev/null and b/resource/ship-library/assets/portraits/1034.webp differ diff --git a/resource/ship-library/assets/portraits/1035.webp b/resource/ship-library/assets/portraits/1035.webp new file mode 100644 index 0000000..bc2bdf4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1035.webp differ diff --git a/resource/ship-library/assets/portraits/1036.webp b/resource/ship-library/assets/portraits/1036.webp new file mode 100644 index 0000000..8efe63a Binary files /dev/null and b/resource/ship-library/assets/portraits/1036.webp differ diff --git a/resource/ship-library/assets/portraits/1037.webp b/resource/ship-library/assets/portraits/1037.webp new file mode 100644 index 0000000..b5f8173 Binary files /dev/null and b/resource/ship-library/assets/portraits/1037.webp differ diff --git a/resource/ship-library/assets/portraits/1038.webp b/resource/ship-library/assets/portraits/1038.webp new file mode 100644 index 0000000..44b2bbe Binary files /dev/null and b/resource/ship-library/assets/portraits/1038.webp differ diff --git a/resource/ship-library/assets/portraits/1039.webp b/resource/ship-library/assets/portraits/1039.webp new file mode 100644 index 0000000..ccfd465 Binary files /dev/null and b/resource/ship-library/assets/portraits/1039.webp differ diff --git a/resource/ship-library/assets/portraits/104.webp b/resource/ship-library/assets/portraits/104.webp new file mode 100644 index 0000000..461c49f Binary files /dev/null and b/resource/ship-library/assets/portraits/104.webp differ diff --git a/resource/ship-library/assets/portraits/1040.webp b/resource/ship-library/assets/portraits/1040.webp new file mode 100644 index 0000000..07a1546 Binary files /dev/null and b/resource/ship-library/assets/portraits/1040.webp differ diff --git a/resource/ship-library/assets/portraits/1041.webp b/resource/ship-library/assets/portraits/1041.webp new file mode 100644 index 0000000..978fdcc Binary files /dev/null and b/resource/ship-library/assets/portraits/1041.webp differ diff --git a/resource/ship-library/assets/portraits/1042.webp b/resource/ship-library/assets/portraits/1042.webp new file mode 100644 index 0000000..800ce1e Binary files /dev/null and b/resource/ship-library/assets/portraits/1042.webp differ diff --git a/resource/ship-library/assets/portraits/1043.webp b/resource/ship-library/assets/portraits/1043.webp new file mode 100644 index 0000000..35e69cb Binary files /dev/null and b/resource/ship-library/assets/portraits/1043.webp differ diff --git a/resource/ship-library/assets/portraits/1044.webp b/resource/ship-library/assets/portraits/1044.webp new file mode 100644 index 0000000..314fe03 Binary files /dev/null and b/resource/ship-library/assets/portraits/1044.webp differ diff --git a/resource/ship-library/assets/portraits/1045.webp b/resource/ship-library/assets/portraits/1045.webp new file mode 100644 index 0000000..3655a42 Binary files /dev/null and b/resource/ship-library/assets/portraits/1045.webp differ diff --git a/resource/ship-library/assets/portraits/1046.webp b/resource/ship-library/assets/portraits/1046.webp new file mode 100644 index 0000000..070ebec Binary files /dev/null and b/resource/ship-library/assets/portraits/1046.webp differ diff --git a/resource/ship-library/assets/portraits/1047.webp b/resource/ship-library/assets/portraits/1047.webp new file mode 100644 index 0000000..bb05179 Binary files /dev/null and b/resource/ship-library/assets/portraits/1047.webp differ diff --git a/resource/ship-library/assets/portraits/1048.webp b/resource/ship-library/assets/portraits/1048.webp new file mode 100644 index 0000000..086a9d5 Binary files /dev/null and b/resource/ship-library/assets/portraits/1048.webp differ diff --git a/resource/ship-library/assets/portraits/1049.webp b/resource/ship-library/assets/portraits/1049.webp new file mode 100644 index 0000000..40a7eeb Binary files /dev/null and b/resource/ship-library/assets/portraits/1049.webp differ diff --git a/resource/ship-library/assets/portraits/105.webp b/resource/ship-library/assets/portraits/105.webp new file mode 100644 index 0000000..899e642 Binary files /dev/null and b/resource/ship-library/assets/portraits/105.webp differ diff --git a/resource/ship-library/assets/portraits/1050.webp b/resource/ship-library/assets/portraits/1050.webp new file mode 100644 index 0000000..2bc6ce4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1050.webp differ diff --git a/resource/ship-library/assets/portraits/1054.webp b/resource/ship-library/assets/portraits/1054.webp new file mode 100644 index 0000000..a8ab65a Binary files /dev/null and b/resource/ship-library/assets/portraits/1054.webp differ diff --git a/resource/ship-library/assets/portraits/1055.webp b/resource/ship-library/assets/portraits/1055.webp new file mode 100644 index 0000000..caca1ce Binary files /dev/null and b/resource/ship-library/assets/portraits/1055.webp differ diff --git a/resource/ship-library/assets/portraits/1056.webp b/resource/ship-library/assets/portraits/1056.webp new file mode 100644 index 0000000..b4365bc Binary files /dev/null and b/resource/ship-library/assets/portraits/1056.webp differ diff --git a/resource/ship-library/assets/portraits/1057.webp b/resource/ship-library/assets/portraits/1057.webp new file mode 100644 index 0000000..9ff1685 Binary files /dev/null and b/resource/ship-library/assets/portraits/1057.webp differ diff --git a/resource/ship-library/assets/portraits/1058.webp b/resource/ship-library/assets/portraits/1058.webp new file mode 100644 index 0000000..e30b055 Binary files /dev/null and b/resource/ship-library/assets/portraits/1058.webp differ diff --git a/resource/ship-library/assets/portraits/1059.webp b/resource/ship-library/assets/portraits/1059.webp new file mode 100644 index 0000000..bdde4ac Binary files /dev/null and b/resource/ship-library/assets/portraits/1059.webp differ diff --git a/resource/ship-library/assets/portraits/106.webp b/resource/ship-library/assets/portraits/106.webp new file mode 100644 index 0000000..edc6e6f Binary files /dev/null and b/resource/ship-library/assets/portraits/106.webp differ diff --git a/resource/ship-library/assets/portraits/1060.webp b/resource/ship-library/assets/portraits/1060.webp new file mode 100644 index 0000000..1b8c810 Binary files /dev/null and b/resource/ship-library/assets/portraits/1060.webp differ diff --git a/resource/ship-library/assets/portraits/1061.webp b/resource/ship-library/assets/portraits/1061.webp new file mode 100644 index 0000000..761c582 Binary files /dev/null and b/resource/ship-library/assets/portraits/1061.webp differ diff --git a/resource/ship-library/assets/portraits/1062.webp b/resource/ship-library/assets/portraits/1062.webp new file mode 100644 index 0000000..04c828f Binary files /dev/null and b/resource/ship-library/assets/portraits/1062.webp differ diff --git a/resource/ship-library/assets/portraits/1063.webp b/resource/ship-library/assets/portraits/1063.webp new file mode 100644 index 0000000..6c8f372 Binary files /dev/null and b/resource/ship-library/assets/portraits/1063.webp differ diff --git a/resource/ship-library/assets/portraits/1064.webp b/resource/ship-library/assets/portraits/1064.webp new file mode 100644 index 0000000..9d1cead Binary files /dev/null and b/resource/ship-library/assets/portraits/1064.webp differ diff --git a/resource/ship-library/assets/portraits/1065.webp b/resource/ship-library/assets/portraits/1065.webp new file mode 100644 index 0000000..ef3222f Binary files /dev/null and b/resource/ship-library/assets/portraits/1065.webp differ diff --git a/resource/ship-library/assets/portraits/1066.webp b/resource/ship-library/assets/portraits/1066.webp new file mode 100644 index 0000000..2f3901b Binary files /dev/null and b/resource/ship-library/assets/portraits/1066.webp differ diff --git a/resource/ship-library/assets/portraits/1067.webp b/resource/ship-library/assets/portraits/1067.webp new file mode 100644 index 0000000..d891477 Binary files /dev/null and b/resource/ship-library/assets/portraits/1067.webp differ diff --git a/resource/ship-library/assets/portraits/1068.webp b/resource/ship-library/assets/portraits/1068.webp new file mode 100644 index 0000000..3e57108 Binary files /dev/null and b/resource/ship-library/assets/portraits/1068.webp differ diff --git a/resource/ship-library/assets/portraits/1069.webp b/resource/ship-library/assets/portraits/1069.webp new file mode 100644 index 0000000..54b214d Binary files /dev/null and b/resource/ship-library/assets/portraits/1069.webp differ diff --git a/resource/ship-library/assets/portraits/107.webp b/resource/ship-library/assets/portraits/107.webp new file mode 100644 index 0000000..a7ba462 Binary files /dev/null and b/resource/ship-library/assets/portraits/107.webp differ diff --git a/resource/ship-library/assets/portraits/1070.webp b/resource/ship-library/assets/portraits/1070.webp new file mode 100644 index 0000000..f0c1c48 Binary files /dev/null and b/resource/ship-library/assets/portraits/1070.webp differ diff --git a/resource/ship-library/assets/portraits/1071.webp b/resource/ship-library/assets/portraits/1071.webp new file mode 100644 index 0000000..6dcb937 Binary files /dev/null and b/resource/ship-library/assets/portraits/1071.webp differ diff --git a/resource/ship-library/assets/portraits/1072.webp b/resource/ship-library/assets/portraits/1072.webp new file mode 100644 index 0000000..29a49db Binary files /dev/null and b/resource/ship-library/assets/portraits/1072.webp differ diff --git a/resource/ship-library/assets/portraits/1073.webp b/resource/ship-library/assets/portraits/1073.webp new file mode 100644 index 0000000..c82f9ea Binary files /dev/null and b/resource/ship-library/assets/portraits/1073.webp differ diff --git a/resource/ship-library/assets/portraits/1074.webp b/resource/ship-library/assets/portraits/1074.webp new file mode 100644 index 0000000..fd51777 Binary files /dev/null and b/resource/ship-library/assets/portraits/1074.webp differ diff --git a/resource/ship-library/assets/portraits/1075.webp b/resource/ship-library/assets/portraits/1075.webp new file mode 100644 index 0000000..159593d Binary files /dev/null and b/resource/ship-library/assets/portraits/1075.webp differ diff --git a/resource/ship-library/assets/portraits/1076.webp b/resource/ship-library/assets/portraits/1076.webp new file mode 100644 index 0000000..8b225c3 Binary files /dev/null and b/resource/ship-library/assets/portraits/1076.webp differ diff --git a/resource/ship-library/assets/portraits/1077.webp b/resource/ship-library/assets/portraits/1077.webp new file mode 100644 index 0000000..b87b585 Binary files /dev/null and b/resource/ship-library/assets/portraits/1077.webp differ diff --git a/resource/ship-library/assets/portraits/1079.webp b/resource/ship-library/assets/portraits/1079.webp new file mode 100644 index 0000000..a2807d6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1079.webp differ diff --git a/resource/ship-library/assets/portraits/108.webp b/resource/ship-library/assets/portraits/108.webp new file mode 100644 index 0000000..d2475a3 Binary files /dev/null and b/resource/ship-library/assets/portraits/108.webp differ diff --git a/resource/ship-library/assets/portraits/1080.webp b/resource/ship-library/assets/portraits/1080.webp new file mode 100644 index 0000000..8b8d023 Binary files /dev/null and b/resource/ship-library/assets/portraits/1080.webp differ diff --git a/resource/ship-library/assets/portraits/1081.webp b/resource/ship-library/assets/portraits/1081.webp new file mode 100644 index 0000000..bb37233 Binary files /dev/null and b/resource/ship-library/assets/portraits/1081.webp differ diff --git a/resource/ship-library/assets/portraits/1082.webp b/resource/ship-library/assets/portraits/1082.webp new file mode 100644 index 0000000..d6ab3e5 Binary files /dev/null and b/resource/ship-library/assets/portraits/1082.webp differ diff --git a/resource/ship-library/assets/portraits/1083.webp b/resource/ship-library/assets/portraits/1083.webp new file mode 100644 index 0000000..f96e4c0 Binary files /dev/null and b/resource/ship-library/assets/portraits/1083.webp differ diff --git a/resource/ship-library/assets/portraits/1084.webp b/resource/ship-library/assets/portraits/1084.webp new file mode 100644 index 0000000..623b170 Binary files /dev/null and b/resource/ship-library/assets/portraits/1084.webp differ diff --git a/resource/ship-library/assets/portraits/1085.webp b/resource/ship-library/assets/portraits/1085.webp new file mode 100644 index 0000000..e610bf5 Binary files /dev/null and b/resource/ship-library/assets/portraits/1085.webp differ diff --git a/resource/ship-library/assets/portraits/1086.webp b/resource/ship-library/assets/portraits/1086.webp new file mode 100644 index 0000000..f630113 Binary files /dev/null and b/resource/ship-library/assets/portraits/1086.webp differ diff --git a/resource/ship-library/assets/portraits/1087.webp b/resource/ship-library/assets/portraits/1087.webp new file mode 100644 index 0000000..cf0c3e3 Binary files /dev/null and b/resource/ship-library/assets/portraits/1087.webp differ diff --git a/resource/ship-library/assets/portraits/1088.webp b/resource/ship-library/assets/portraits/1088.webp new file mode 100644 index 0000000..1286f78 Binary files /dev/null and b/resource/ship-library/assets/portraits/1088.webp differ diff --git a/resource/ship-library/assets/portraits/1089.webp b/resource/ship-library/assets/portraits/1089.webp new file mode 100644 index 0000000..1cf580e Binary files /dev/null and b/resource/ship-library/assets/portraits/1089.webp differ diff --git a/resource/ship-library/assets/portraits/109.webp b/resource/ship-library/assets/portraits/109.webp new file mode 100644 index 0000000..a684d19 Binary files /dev/null and b/resource/ship-library/assets/portraits/109.webp differ diff --git a/resource/ship-library/assets/portraits/1092.webp b/resource/ship-library/assets/portraits/1092.webp new file mode 100644 index 0000000..7fc1a1f Binary files /dev/null and b/resource/ship-library/assets/portraits/1092.webp differ diff --git a/resource/ship-library/assets/portraits/1093.webp b/resource/ship-library/assets/portraits/1093.webp new file mode 100644 index 0000000..3569775 Binary files /dev/null and b/resource/ship-library/assets/portraits/1093.webp differ diff --git a/resource/ship-library/assets/portraits/1094.webp b/resource/ship-library/assets/portraits/1094.webp new file mode 100644 index 0000000..00791a1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1094.webp differ diff --git a/resource/ship-library/assets/portraits/1097.webp b/resource/ship-library/assets/portraits/1097.webp new file mode 100644 index 0000000..a88ccc6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1097.webp differ diff --git a/resource/ship-library/assets/portraits/1098.webp b/resource/ship-library/assets/portraits/1098.webp new file mode 100644 index 0000000..6c3a27a Binary files /dev/null and b/resource/ship-library/assets/portraits/1098.webp differ diff --git a/resource/ship-library/assets/portraits/1099.webp b/resource/ship-library/assets/portraits/1099.webp new file mode 100644 index 0000000..58448b0 Binary files /dev/null and b/resource/ship-library/assets/portraits/1099.webp differ diff --git a/resource/ship-library/assets/portraits/11.webp b/resource/ship-library/assets/portraits/11.webp new file mode 100644 index 0000000..e402792 Binary files /dev/null and b/resource/ship-library/assets/portraits/11.webp differ diff --git a/resource/ship-library/assets/portraits/110.webp b/resource/ship-library/assets/portraits/110.webp new file mode 100644 index 0000000..a99039a Binary files /dev/null and b/resource/ship-library/assets/portraits/110.webp differ diff --git a/resource/ship-library/assets/portraits/1100.webp b/resource/ship-library/assets/portraits/1100.webp new file mode 100644 index 0000000..7677e3b Binary files /dev/null and b/resource/ship-library/assets/portraits/1100.webp differ diff --git a/resource/ship-library/assets/portraits/1101.webp b/resource/ship-library/assets/portraits/1101.webp new file mode 100644 index 0000000..c6afd79 Binary files /dev/null and b/resource/ship-library/assets/portraits/1101.webp differ diff --git a/resource/ship-library/assets/portraits/1102.webp b/resource/ship-library/assets/portraits/1102.webp new file mode 100644 index 0000000..c203eb1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1102.webp differ diff --git a/resource/ship-library/assets/portraits/1105.webp b/resource/ship-library/assets/portraits/1105.webp new file mode 100644 index 0000000..05d2a66 Binary files /dev/null and b/resource/ship-library/assets/portraits/1105.webp differ diff --git a/resource/ship-library/assets/portraits/1106.webp b/resource/ship-library/assets/portraits/1106.webp new file mode 100644 index 0000000..629f369 Binary files /dev/null and b/resource/ship-library/assets/portraits/1106.webp differ diff --git a/resource/ship-library/assets/portraits/1107.webp b/resource/ship-library/assets/portraits/1107.webp new file mode 100644 index 0000000..9247905 Binary files /dev/null and b/resource/ship-library/assets/portraits/1107.webp differ diff --git a/resource/ship-library/assets/portraits/1108.webp b/resource/ship-library/assets/portraits/1108.webp new file mode 100644 index 0000000..f4356ff Binary files /dev/null and b/resource/ship-library/assets/portraits/1108.webp differ diff --git a/resource/ship-library/assets/portraits/1109.webp b/resource/ship-library/assets/portraits/1109.webp new file mode 100644 index 0000000..74ade36 Binary files /dev/null and b/resource/ship-library/assets/portraits/1109.webp differ diff --git a/resource/ship-library/assets/portraits/111.webp b/resource/ship-library/assets/portraits/111.webp new file mode 100644 index 0000000..afa5941 Binary files /dev/null and b/resource/ship-library/assets/portraits/111.webp differ diff --git a/resource/ship-library/assets/portraits/1110.webp b/resource/ship-library/assets/portraits/1110.webp new file mode 100644 index 0000000..016dfce Binary files /dev/null and b/resource/ship-library/assets/portraits/1110.webp differ diff --git a/resource/ship-library/assets/portraits/1111.webp b/resource/ship-library/assets/portraits/1111.webp new file mode 100644 index 0000000..c57d635 Binary files /dev/null and b/resource/ship-library/assets/portraits/1111.webp differ diff --git a/resource/ship-library/assets/portraits/1112.webp b/resource/ship-library/assets/portraits/1112.webp new file mode 100644 index 0000000..ae107ff Binary files /dev/null and b/resource/ship-library/assets/portraits/1112.webp differ diff --git a/resource/ship-library/assets/portraits/1113.webp b/resource/ship-library/assets/portraits/1113.webp new file mode 100644 index 0000000..ea6895b Binary files /dev/null and b/resource/ship-library/assets/portraits/1113.webp differ diff --git a/resource/ship-library/assets/portraits/1117.webp b/resource/ship-library/assets/portraits/1117.webp new file mode 100644 index 0000000..3448461 Binary files /dev/null and b/resource/ship-library/assets/portraits/1117.webp differ diff --git a/resource/ship-library/assets/portraits/1118.webp b/resource/ship-library/assets/portraits/1118.webp new file mode 100644 index 0000000..5e229da Binary files /dev/null and b/resource/ship-library/assets/portraits/1118.webp differ diff --git a/resource/ship-library/assets/portraits/112.webp b/resource/ship-library/assets/portraits/112.webp new file mode 100644 index 0000000..307f7b8 Binary files /dev/null and b/resource/ship-library/assets/portraits/112.webp differ diff --git a/resource/ship-library/assets/portraits/1120.webp b/resource/ship-library/assets/portraits/1120.webp new file mode 100644 index 0000000..278e968 Binary files /dev/null and b/resource/ship-library/assets/portraits/1120.webp differ diff --git a/resource/ship-library/assets/portraits/1121.webp b/resource/ship-library/assets/portraits/1121.webp new file mode 100644 index 0000000..b19cfef Binary files /dev/null and b/resource/ship-library/assets/portraits/1121.webp differ diff --git a/resource/ship-library/assets/portraits/1123.webp b/resource/ship-library/assets/portraits/1123.webp new file mode 100644 index 0000000..5b06121 Binary files /dev/null and b/resource/ship-library/assets/portraits/1123.webp differ diff --git a/resource/ship-library/assets/portraits/1124.webp b/resource/ship-library/assets/portraits/1124.webp new file mode 100644 index 0000000..cae677f Binary files /dev/null and b/resource/ship-library/assets/portraits/1124.webp differ diff --git a/resource/ship-library/assets/portraits/1125.webp b/resource/ship-library/assets/portraits/1125.webp new file mode 100644 index 0000000..38434b0 Binary files /dev/null and b/resource/ship-library/assets/portraits/1125.webp differ diff --git a/resource/ship-library/assets/portraits/1126.webp b/resource/ship-library/assets/portraits/1126.webp new file mode 100644 index 0000000..b8884bf Binary files /dev/null and b/resource/ship-library/assets/portraits/1126.webp differ diff --git a/resource/ship-library/assets/portraits/113.webp b/resource/ship-library/assets/portraits/113.webp new file mode 100644 index 0000000..f1a79bc Binary files /dev/null and b/resource/ship-library/assets/portraits/113.webp differ diff --git a/resource/ship-library/assets/portraits/1130.webp b/resource/ship-library/assets/portraits/1130.webp new file mode 100644 index 0000000..81773a7 Binary files /dev/null and b/resource/ship-library/assets/portraits/1130.webp differ diff --git a/resource/ship-library/assets/portraits/1131.webp b/resource/ship-library/assets/portraits/1131.webp new file mode 100644 index 0000000..7a21f02 Binary files /dev/null and b/resource/ship-library/assets/portraits/1131.webp differ diff --git a/resource/ship-library/assets/portraits/1132.webp b/resource/ship-library/assets/portraits/1132.webp new file mode 100644 index 0000000..cefbed7 Binary files /dev/null and b/resource/ship-library/assets/portraits/1132.webp differ diff --git a/resource/ship-library/assets/portraits/1133.webp b/resource/ship-library/assets/portraits/1133.webp new file mode 100644 index 0000000..de6de7c Binary files /dev/null and b/resource/ship-library/assets/portraits/1133.webp differ diff --git a/resource/ship-library/assets/portraits/1135.webp b/resource/ship-library/assets/portraits/1135.webp new file mode 100644 index 0000000..96e0715 Binary files /dev/null and b/resource/ship-library/assets/portraits/1135.webp differ diff --git a/resource/ship-library/assets/portraits/1136.webp b/resource/ship-library/assets/portraits/1136.webp new file mode 100644 index 0000000..85bea33 Binary files /dev/null and b/resource/ship-library/assets/portraits/1136.webp differ diff --git a/resource/ship-library/assets/portraits/1137.webp b/resource/ship-library/assets/portraits/1137.webp new file mode 100644 index 0000000..c6ce219 Binary files /dev/null and b/resource/ship-library/assets/portraits/1137.webp differ diff --git a/resource/ship-library/assets/portraits/1139.webp b/resource/ship-library/assets/portraits/1139.webp new file mode 100644 index 0000000..0bc80ee Binary files /dev/null and b/resource/ship-library/assets/portraits/1139.webp differ diff --git a/resource/ship-library/assets/portraits/114.webp b/resource/ship-library/assets/portraits/114.webp new file mode 100644 index 0000000..de21ae8 Binary files /dev/null and b/resource/ship-library/assets/portraits/114.webp differ diff --git a/resource/ship-library/assets/portraits/1141.webp b/resource/ship-library/assets/portraits/1141.webp new file mode 100644 index 0000000..15c3b49 Binary files /dev/null and b/resource/ship-library/assets/portraits/1141.webp differ diff --git a/resource/ship-library/assets/portraits/1142.webp b/resource/ship-library/assets/portraits/1142.webp new file mode 100644 index 0000000..289d7d2 Binary files /dev/null and b/resource/ship-library/assets/portraits/1142.webp differ diff --git a/resource/ship-library/assets/portraits/1143.webp b/resource/ship-library/assets/portraits/1143.webp new file mode 100644 index 0000000..169501d Binary files /dev/null and b/resource/ship-library/assets/portraits/1143.webp differ diff --git a/resource/ship-library/assets/portraits/1145.webp b/resource/ship-library/assets/portraits/1145.webp new file mode 100644 index 0000000..a1c68a7 Binary files /dev/null and b/resource/ship-library/assets/portraits/1145.webp differ diff --git a/resource/ship-library/assets/portraits/1149.webp b/resource/ship-library/assets/portraits/1149.webp new file mode 100644 index 0000000..c399e99 Binary files /dev/null and b/resource/ship-library/assets/portraits/1149.webp differ diff --git a/resource/ship-library/assets/portraits/115.webp b/resource/ship-library/assets/portraits/115.webp new file mode 100644 index 0000000..0109a79 Binary files /dev/null and b/resource/ship-library/assets/portraits/115.webp differ diff --git a/resource/ship-library/assets/portraits/1150.webp b/resource/ship-library/assets/portraits/1150.webp new file mode 100644 index 0000000..02db4e6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1150.webp differ diff --git a/resource/ship-library/assets/portraits/1151.webp b/resource/ship-library/assets/portraits/1151.webp new file mode 100644 index 0000000..5b86446 Binary files /dev/null and b/resource/ship-library/assets/portraits/1151.webp differ diff --git a/resource/ship-library/assets/portraits/1158.webp b/resource/ship-library/assets/portraits/1158.webp new file mode 100644 index 0000000..72b4d9b Binary files /dev/null and b/resource/ship-library/assets/portraits/1158.webp differ diff --git a/resource/ship-library/assets/portraits/1160.webp b/resource/ship-library/assets/portraits/1160.webp new file mode 100644 index 0000000..0ec6be5 Binary files /dev/null and b/resource/ship-library/assets/portraits/1160.webp differ diff --git a/resource/ship-library/assets/portraits/1162.webp b/resource/ship-library/assets/portraits/1162.webp new file mode 100644 index 0000000..518dc1e Binary files /dev/null and b/resource/ship-library/assets/portraits/1162.webp differ diff --git a/resource/ship-library/assets/portraits/1164.webp b/resource/ship-library/assets/portraits/1164.webp new file mode 100644 index 0000000..257928c Binary files /dev/null and b/resource/ship-library/assets/portraits/1164.webp differ diff --git a/resource/ship-library/assets/portraits/1165.webp b/resource/ship-library/assets/portraits/1165.webp new file mode 100644 index 0000000..376ad41 Binary files /dev/null and b/resource/ship-library/assets/portraits/1165.webp differ diff --git a/resource/ship-library/assets/portraits/1166.webp b/resource/ship-library/assets/portraits/1166.webp new file mode 100644 index 0000000..5534a58 Binary files /dev/null and b/resource/ship-library/assets/portraits/1166.webp differ diff --git a/resource/ship-library/assets/portraits/1167.webp b/resource/ship-library/assets/portraits/1167.webp new file mode 100644 index 0000000..2a92899 Binary files /dev/null and b/resource/ship-library/assets/portraits/1167.webp differ diff --git a/resource/ship-library/assets/portraits/1168.webp b/resource/ship-library/assets/portraits/1168.webp new file mode 100644 index 0000000..8fd94b1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1168.webp differ diff --git a/resource/ship-library/assets/portraits/1169.webp b/resource/ship-library/assets/portraits/1169.webp new file mode 100644 index 0000000..1a367d3 Binary files /dev/null and b/resource/ship-library/assets/portraits/1169.webp differ diff --git a/resource/ship-library/assets/portraits/117.webp b/resource/ship-library/assets/portraits/117.webp new file mode 100644 index 0000000..0a4a292 Binary files /dev/null and b/resource/ship-library/assets/portraits/117.webp differ diff --git a/resource/ship-library/assets/portraits/1171.webp b/resource/ship-library/assets/portraits/1171.webp new file mode 100644 index 0000000..d119943 Binary files /dev/null and b/resource/ship-library/assets/portraits/1171.webp differ diff --git a/resource/ship-library/assets/portraits/1175.webp b/resource/ship-library/assets/portraits/1175.webp new file mode 100644 index 0000000..c2fc61b Binary files /dev/null and b/resource/ship-library/assets/portraits/1175.webp differ diff --git a/resource/ship-library/assets/portraits/1178.webp b/resource/ship-library/assets/portraits/1178.webp new file mode 100644 index 0000000..70872aa Binary files /dev/null and b/resource/ship-library/assets/portraits/1178.webp differ diff --git a/resource/ship-library/assets/portraits/118.webp b/resource/ship-library/assets/portraits/118.webp new file mode 100644 index 0000000..c3eb878 Binary files /dev/null and b/resource/ship-library/assets/portraits/118.webp differ diff --git a/resource/ship-library/assets/portraits/1181.webp b/resource/ship-library/assets/portraits/1181.webp new file mode 100644 index 0000000..a566fa9 Binary files /dev/null and b/resource/ship-library/assets/portraits/1181.webp differ diff --git a/resource/ship-library/assets/portraits/1182.webp b/resource/ship-library/assets/portraits/1182.webp new file mode 100644 index 0000000..3213541 Binary files /dev/null and b/resource/ship-library/assets/portraits/1182.webp differ diff --git a/resource/ship-library/assets/portraits/1183.webp b/resource/ship-library/assets/portraits/1183.webp new file mode 100644 index 0000000..c7846e4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1183.webp differ diff --git a/resource/ship-library/assets/portraits/1185.webp b/resource/ship-library/assets/portraits/1185.webp new file mode 100644 index 0000000..a7ace90 Binary files /dev/null and b/resource/ship-library/assets/portraits/1185.webp differ diff --git a/resource/ship-library/assets/portraits/1187.webp b/resource/ship-library/assets/portraits/1187.webp new file mode 100644 index 0000000..ed40bf8 Binary files /dev/null and b/resource/ship-library/assets/portraits/1187.webp differ diff --git a/resource/ship-library/assets/portraits/1188.webp b/resource/ship-library/assets/portraits/1188.webp new file mode 100644 index 0000000..1d79290 Binary files /dev/null and b/resource/ship-library/assets/portraits/1188.webp differ diff --git a/resource/ship-library/assets/portraits/119.webp b/resource/ship-library/assets/portraits/119.webp new file mode 100644 index 0000000..1d4efee Binary files /dev/null and b/resource/ship-library/assets/portraits/119.webp differ diff --git a/resource/ship-library/assets/portraits/1192.webp b/resource/ship-library/assets/portraits/1192.webp new file mode 100644 index 0000000..6032acf Binary files /dev/null and b/resource/ship-library/assets/portraits/1192.webp differ diff --git a/resource/ship-library/assets/portraits/1194.webp b/resource/ship-library/assets/portraits/1194.webp new file mode 100644 index 0000000..42e03cf Binary files /dev/null and b/resource/ship-library/assets/portraits/1194.webp differ diff --git a/resource/ship-library/assets/portraits/1195.webp b/resource/ship-library/assets/portraits/1195.webp new file mode 100644 index 0000000..3ca8c54 Binary files /dev/null and b/resource/ship-library/assets/portraits/1195.webp differ diff --git a/resource/ship-library/assets/portraits/1197.webp b/resource/ship-library/assets/portraits/1197.webp new file mode 100644 index 0000000..21c3ec1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1197.webp differ diff --git a/resource/ship-library/assets/portraits/1198.webp b/resource/ship-library/assets/portraits/1198.webp new file mode 100644 index 0000000..1c942b0 Binary files /dev/null and b/resource/ship-library/assets/portraits/1198.webp differ diff --git a/resource/ship-library/assets/portraits/1199.webp b/resource/ship-library/assets/portraits/1199.webp new file mode 100644 index 0000000..15c235a Binary files /dev/null and b/resource/ship-library/assets/portraits/1199.webp differ diff --git a/resource/ship-library/assets/portraits/12.webp b/resource/ship-library/assets/portraits/12.webp new file mode 100644 index 0000000..4ca428c Binary files /dev/null and b/resource/ship-library/assets/portraits/12.webp differ diff --git a/resource/ship-library/assets/portraits/120.webp b/resource/ship-library/assets/portraits/120.webp new file mode 100644 index 0000000..3be67bf Binary files /dev/null and b/resource/ship-library/assets/portraits/120.webp differ diff --git a/resource/ship-library/assets/portraits/1205.webp b/resource/ship-library/assets/portraits/1205.webp new file mode 100644 index 0000000..c4bf80c Binary files /dev/null and b/resource/ship-library/assets/portraits/1205.webp differ diff --git a/resource/ship-library/assets/portraits/1206.webp b/resource/ship-library/assets/portraits/1206.webp new file mode 100644 index 0000000..7d152fe Binary files /dev/null and b/resource/ship-library/assets/portraits/1206.webp differ diff --git a/resource/ship-library/assets/portraits/1207.webp b/resource/ship-library/assets/portraits/1207.webp new file mode 100644 index 0000000..61cd8fa Binary files /dev/null and b/resource/ship-library/assets/portraits/1207.webp differ diff --git a/resource/ship-library/assets/portraits/1209.webp b/resource/ship-library/assets/portraits/1209.webp new file mode 100644 index 0000000..1c4524c Binary files /dev/null and b/resource/ship-library/assets/portraits/1209.webp differ diff --git a/resource/ship-library/assets/portraits/121.webp b/resource/ship-library/assets/portraits/121.webp new file mode 100644 index 0000000..4d8bbf1 Binary files /dev/null and b/resource/ship-library/assets/portraits/121.webp differ diff --git a/resource/ship-library/assets/portraits/1211.webp b/resource/ship-library/assets/portraits/1211.webp new file mode 100644 index 0000000..b4da38f Binary files /dev/null and b/resource/ship-library/assets/portraits/1211.webp differ diff --git a/resource/ship-library/assets/portraits/1215.webp b/resource/ship-library/assets/portraits/1215.webp new file mode 100644 index 0000000..f9cc65e Binary files /dev/null and b/resource/ship-library/assets/portraits/1215.webp differ diff --git a/resource/ship-library/assets/portraits/1219.webp b/resource/ship-library/assets/portraits/1219.webp new file mode 100644 index 0000000..a4f5dbc Binary files /dev/null and b/resource/ship-library/assets/portraits/1219.webp differ diff --git a/resource/ship-library/assets/portraits/122.webp b/resource/ship-library/assets/portraits/122.webp new file mode 100644 index 0000000..43c98dc Binary files /dev/null and b/resource/ship-library/assets/portraits/122.webp differ diff --git a/resource/ship-library/assets/portraits/1220.webp b/resource/ship-library/assets/portraits/1220.webp new file mode 100644 index 0000000..306b5c4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1220.webp differ diff --git a/resource/ship-library/assets/portraits/1221.webp b/resource/ship-library/assets/portraits/1221.webp new file mode 100644 index 0000000..b24a6b6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1221.webp differ diff --git a/resource/ship-library/assets/portraits/1222.webp b/resource/ship-library/assets/portraits/1222.webp new file mode 100644 index 0000000..b986bf2 Binary files /dev/null and b/resource/ship-library/assets/portraits/1222.webp differ diff --git a/resource/ship-library/assets/portraits/1223.webp b/resource/ship-library/assets/portraits/1223.webp new file mode 100644 index 0000000..1b1653d Binary files /dev/null and b/resource/ship-library/assets/portraits/1223.webp differ diff --git a/resource/ship-library/assets/portraits/1224.webp b/resource/ship-library/assets/portraits/1224.webp new file mode 100644 index 0000000..2bd4905 Binary files /dev/null and b/resource/ship-library/assets/portraits/1224.webp differ diff --git a/resource/ship-library/assets/portraits/1225.webp b/resource/ship-library/assets/portraits/1225.webp new file mode 100644 index 0000000..51ccb90 Binary files /dev/null and b/resource/ship-library/assets/portraits/1225.webp differ diff --git a/resource/ship-library/assets/portraits/1227.webp b/resource/ship-library/assets/portraits/1227.webp new file mode 100644 index 0000000..9358099 Binary files /dev/null and b/resource/ship-library/assets/portraits/1227.webp differ diff --git a/resource/ship-library/assets/portraits/1228.webp b/resource/ship-library/assets/portraits/1228.webp new file mode 100644 index 0000000..9757259 Binary files /dev/null and b/resource/ship-library/assets/portraits/1228.webp differ diff --git a/resource/ship-library/assets/portraits/123.webp b/resource/ship-library/assets/portraits/123.webp new file mode 100644 index 0000000..ba36ee7 Binary files /dev/null and b/resource/ship-library/assets/portraits/123.webp differ diff --git a/resource/ship-library/assets/portraits/1232.webp b/resource/ship-library/assets/portraits/1232.webp new file mode 100644 index 0000000..782bb91 Binary files /dev/null and b/resource/ship-library/assets/portraits/1232.webp differ diff --git a/resource/ship-library/assets/portraits/1233.webp b/resource/ship-library/assets/portraits/1233.webp new file mode 100644 index 0000000..40be9c2 Binary files /dev/null and b/resource/ship-library/assets/portraits/1233.webp differ diff --git a/resource/ship-library/assets/portraits/1238.webp b/resource/ship-library/assets/portraits/1238.webp new file mode 100644 index 0000000..08b21ba Binary files /dev/null and b/resource/ship-library/assets/portraits/1238.webp differ diff --git a/resource/ship-library/assets/portraits/1239.webp b/resource/ship-library/assets/portraits/1239.webp new file mode 100644 index 0000000..3f555d6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1239.webp differ diff --git a/resource/ship-library/assets/portraits/124.webp b/resource/ship-library/assets/portraits/124.webp new file mode 100644 index 0000000..4760609 Binary files /dev/null and b/resource/ship-library/assets/portraits/124.webp differ diff --git a/resource/ship-library/assets/portraits/1240.webp b/resource/ship-library/assets/portraits/1240.webp new file mode 100644 index 0000000..f714b80 Binary files /dev/null and b/resource/ship-library/assets/portraits/1240.webp differ diff --git a/resource/ship-library/assets/portraits/1241.webp b/resource/ship-library/assets/portraits/1241.webp new file mode 100644 index 0000000..3057785 Binary files /dev/null and b/resource/ship-library/assets/portraits/1241.webp differ diff --git a/resource/ship-library/assets/portraits/1244.webp b/resource/ship-library/assets/portraits/1244.webp new file mode 100644 index 0000000..bd130c1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1244.webp differ diff --git a/resource/ship-library/assets/portraits/1247.webp b/resource/ship-library/assets/portraits/1247.webp new file mode 100644 index 0000000..acfa3b4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1247.webp differ diff --git a/resource/ship-library/assets/portraits/1248.webp b/resource/ship-library/assets/portraits/1248.webp new file mode 100644 index 0000000..02a0d5a Binary files /dev/null and b/resource/ship-library/assets/portraits/1248.webp differ diff --git a/resource/ship-library/assets/portraits/1249.webp b/resource/ship-library/assets/portraits/1249.webp new file mode 100644 index 0000000..ad762a9 Binary files /dev/null and b/resource/ship-library/assets/portraits/1249.webp differ diff --git a/resource/ship-library/assets/portraits/125.webp b/resource/ship-library/assets/portraits/125.webp new file mode 100644 index 0000000..11f0ff3 Binary files /dev/null and b/resource/ship-library/assets/portraits/125.webp differ diff --git a/resource/ship-library/assets/portraits/1258.webp b/resource/ship-library/assets/portraits/1258.webp new file mode 100644 index 0000000..36d232d Binary files /dev/null and b/resource/ship-library/assets/portraits/1258.webp differ diff --git a/resource/ship-library/assets/portraits/126.webp b/resource/ship-library/assets/portraits/126.webp new file mode 100644 index 0000000..f7a1302 Binary files /dev/null and b/resource/ship-library/assets/portraits/126.webp differ diff --git a/resource/ship-library/assets/portraits/1265.webp b/resource/ship-library/assets/portraits/1265.webp new file mode 100644 index 0000000..60f303f Binary files /dev/null and b/resource/ship-library/assets/portraits/1265.webp differ diff --git a/resource/ship-library/assets/portraits/1266.webp b/resource/ship-library/assets/portraits/1266.webp new file mode 100644 index 0000000..63e02ec Binary files /dev/null and b/resource/ship-library/assets/portraits/1266.webp differ diff --git a/resource/ship-library/assets/portraits/1267.webp b/resource/ship-library/assets/portraits/1267.webp new file mode 100644 index 0000000..cc16f76 Binary files /dev/null and b/resource/ship-library/assets/portraits/1267.webp differ diff --git a/resource/ship-library/assets/portraits/1269.webp b/resource/ship-library/assets/portraits/1269.webp new file mode 100644 index 0000000..030b557 Binary files /dev/null and b/resource/ship-library/assets/portraits/1269.webp differ diff --git a/resource/ship-library/assets/portraits/127.webp b/resource/ship-library/assets/portraits/127.webp new file mode 100644 index 0000000..7d39cea Binary files /dev/null and b/resource/ship-library/assets/portraits/127.webp differ diff --git a/resource/ship-library/assets/portraits/1270.webp b/resource/ship-library/assets/portraits/1270.webp new file mode 100644 index 0000000..3de6199 Binary files /dev/null and b/resource/ship-library/assets/portraits/1270.webp differ diff --git a/resource/ship-library/assets/portraits/1273.webp b/resource/ship-library/assets/portraits/1273.webp new file mode 100644 index 0000000..880f24f Binary files /dev/null and b/resource/ship-library/assets/portraits/1273.webp differ diff --git a/resource/ship-library/assets/portraits/1275.webp b/resource/ship-library/assets/portraits/1275.webp new file mode 100644 index 0000000..52a93de Binary files /dev/null and b/resource/ship-library/assets/portraits/1275.webp differ diff --git a/resource/ship-library/assets/portraits/1277.webp b/resource/ship-library/assets/portraits/1277.webp new file mode 100644 index 0000000..7c8d11a Binary files /dev/null and b/resource/ship-library/assets/portraits/1277.webp differ diff --git a/resource/ship-library/assets/portraits/1278.webp b/resource/ship-library/assets/portraits/1278.webp new file mode 100644 index 0000000..fe55d44 Binary files /dev/null and b/resource/ship-library/assets/portraits/1278.webp differ diff --git a/resource/ship-library/assets/portraits/1279.webp b/resource/ship-library/assets/portraits/1279.webp new file mode 100644 index 0000000..00784f4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1279.webp differ diff --git a/resource/ship-library/assets/portraits/128.webp b/resource/ship-library/assets/portraits/128.webp new file mode 100644 index 0000000..b4c2242 Binary files /dev/null and b/resource/ship-library/assets/portraits/128.webp differ diff --git a/resource/ship-library/assets/portraits/1280.webp b/resource/ship-library/assets/portraits/1280.webp new file mode 100644 index 0000000..ef0f985 Binary files /dev/null and b/resource/ship-library/assets/portraits/1280.webp differ diff --git a/resource/ship-library/assets/portraits/1289.webp b/resource/ship-library/assets/portraits/1289.webp new file mode 100644 index 0000000..ba60a92 Binary files /dev/null and b/resource/ship-library/assets/portraits/1289.webp differ diff --git a/resource/ship-library/assets/portraits/129.webp b/resource/ship-library/assets/portraits/129.webp new file mode 100644 index 0000000..f9e0c07 Binary files /dev/null and b/resource/ship-library/assets/portraits/129.webp differ diff --git a/resource/ship-library/assets/portraits/1290.webp b/resource/ship-library/assets/portraits/1290.webp new file mode 100644 index 0000000..970fa9d Binary files /dev/null and b/resource/ship-library/assets/portraits/1290.webp differ diff --git a/resource/ship-library/assets/portraits/1291.webp b/resource/ship-library/assets/portraits/1291.webp new file mode 100644 index 0000000..11dea29 Binary files /dev/null and b/resource/ship-library/assets/portraits/1291.webp differ diff --git a/resource/ship-library/assets/portraits/1293.webp b/resource/ship-library/assets/portraits/1293.webp new file mode 100644 index 0000000..b1f26e6 Binary files /dev/null and b/resource/ship-library/assets/portraits/1293.webp differ diff --git a/resource/ship-library/assets/portraits/1299.webp b/resource/ship-library/assets/portraits/1299.webp new file mode 100644 index 0000000..5864737 Binary files /dev/null and b/resource/ship-library/assets/portraits/1299.webp differ diff --git a/resource/ship-library/assets/portraits/13.webp b/resource/ship-library/assets/portraits/13.webp new file mode 100644 index 0000000..387219a Binary files /dev/null and b/resource/ship-library/assets/portraits/13.webp differ diff --git a/resource/ship-library/assets/portraits/130.webp b/resource/ship-library/assets/portraits/130.webp new file mode 100644 index 0000000..4809d98 Binary files /dev/null and b/resource/ship-library/assets/portraits/130.webp differ diff --git a/resource/ship-library/assets/portraits/1301.webp b/resource/ship-library/assets/portraits/1301.webp new file mode 100644 index 0000000..e54ee30 Binary files /dev/null and b/resource/ship-library/assets/portraits/1301.webp differ diff --git a/resource/ship-library/assets/portraits/1306.webp b/resource/ship-library/assets/portraits/1306.webp new file mode 100644 index 0000000..43920a4 Binary files /dev/null and b/resource/ship-library/assets/portraits/1306.webp differ diff --git a/resource/ship-library/assets/portraits/131.webp b/resource/ship-library/assets/portraits/131.webp new file mode 100644 index 0000000..6b0b22c Binary files /dev/null and b/resource/ship-library/assets/portraits/131.webp differ diff --git a/resource/ship-library/assets/portraits/1311.webp b/resource/ship-library/assets/portraits/1311.webp new file mode 100644 index 0000000..8bd8ce3 Binary files /dev/null and b/resource/ship-library/assets/portraits/1311.webp differ diff --git a/resource/ship-library/assets/portraits/1316.webp b/resource/ship-library/assets/portraits/1316.webp new file mode 100644 index 0000000..c4f2457 Binary files /dev/null and b/resource/ship-library/assets/portraits/1316.webp differ diff --git a/resource/ship-library/assets/portraits/132.webp b/resource/ship-library/assets/portraits/132.webp new file mode 100644 index 0000000..a96c4c7 Binary files /dev/null and b/resource/ship-library/assets/portraits/132.webp differ diff --git a/resource/ship-library/assets/portraits/1323.webp b/resource/ship-library/assets/portraits/1323.webp new file mode 100644 index 0000000..53e3b27 Binary files /dev/null and b/resource/ship-library/assets/portraits/1323.webp differ diff --git a/resource/ship-library/assets/portraits/1325.webp b/resource/ship-library/assets/portraits/1325.webp new file mode 100644 index 0000000..eeb67db Binary files /dev/null and b/resource/ship-library/assets/portraits/1325.webp differ diff --git a/resource/ship-library/assets/portraits/133.webp b/resource/ship-library/assets/portraits/133.webp new file mode 100644 index 0000000..84900a2 Binary files /dev/null and b/resource/ship-library/assets/portraits/133.webp differ diff --git a/resource/ship-library/assets/portraits/1332.webp b/resource/ship-library/assets/portraits/1332.webp new file mode 100644 index 0000000..cea1828 Binary files /dev/null and b/resource/ship-library/assets/portraits/1332.webp differ diff --git a/resource/ship-library/assets/portraits/1335.webp b/resource/ship-library/assets/portraits/1335.webp new file mode 100644 index 0000000..b245afe Binary files /dev/null and b/resource/ship-library/assets/portraits/1335.webp differ diff --git a/resource/ship-library/assets/portraits/1339.webp b/resource/ship-library/assets/portraits/1339.webp new file mode 100644 index 0000000..b8bf06e Binary files /dev/null and b/resource/ship-library/assets/portraits/1339.webp differ diff --git a/resource/ship-library/assets/portraits/134.webp b/resource/ship-library/assets/portraits/134.webp new file mode 100644 index 0000000..8db697a Binary files /dev/null and b/resource/ship-library/assets/portraits/134.webp differ diff --git a/resource/ship-library/assets/portraits/1340.webp b/resource/ship-library/assets/portraits/1340.webp new file mode 100644 index 0000000..4084d4f Binary files /dev/null and b/resource/ship-library/assets/portraits/1340.webp differ diff --git a/resource/ship-library/assets/portraits/1342.webp b/resource/ship-library/assets/portraits/1342.webp new file mode 100644 index 0000000..46bd307 Binary files /dev/null and b/resource/ship-library/assets/portraits/1342.webp differ diff --git a/resource/ship-library/assets/portraits/1343.webp b/resource/ship-library/assets/portraits/1343.webp new file mode 100644 index 0000000..1af9e8a Binary files /dev/null and b/resource/ship-library/assets/portraits/1343.webp differ diff --git a/resource/ship-library/assets/portraits/1344.webp b/resource/ship-library/assets/portraits/1344.webp new file mode 100644 index 0000000..036b466 Binary files /dev/null and b/resource/ship-library/assets/portraits/1344.webp differ diff --git a/resource/ship-library/assets/portraits/135.webp b/resource/ship-library/assets/portraits/135.webp new file mode 100644 index 0000000..d347a34 Binary files /dev/null and b/resource/ship-library/assets/portraits/135.webp differ diff --git a/resource/ship-library/assets/portraits/1351.webp b/resource/ship-library/assets/portraits/1351.webp new file mode 100644 index 0000000..24d192f Binary files /dev/null and b/resource/ship-library/assets/portraits/1351.webp differ diff --git a/resource/ship-library/assets/portraits/136.webp b/resource/ship-library/assets/portraits/136.webp new file mode 100644 index 0000000..6760520 Binary files /dev/null and b/resource/ship-library/assets/portraits/136.webp differ diff --git a/resource/ship-library/assets/portraits/1362.webp b/resource/ship-library/assets/portraits/1362.webp new file mode 100644 index 0000000..fd2c556 Binary files /dev/null and b/resource/ship-library/assets/portraits/1362.webp differ diff --git a/resource/ship-library/assets/portraits/1363.webp b/resource/ship-library/assets/portraits/1363.webp new file mode 100644 index 0000000..4e07ccf Binary files /dev/null and b/resource/ship-library/assets/portraits/1363.webp differ diff --git a/resource/ship-library/assets/portraits/1366.webp b/resource/ship-library/assets/portraits/1366.webp new file mode 100644 index 0000000..5ed6b8b Binary files /dev/null and b/resource/ship-library/assets/portraits/1366.webp differ diff --git a/resource/ship-library/assets/portraits/137.webp b/resource/ship-library/assets/portraits/137.webp new file mode 100644 index 0000000..6fe965f Binary files /dev/null and b/resource/ship-library/assets/portraits/137.webp differ diff --git a/resource/ship-library/assets/portraits/1372.webp b/resource/ship-library/assets/portraits/1372.webp new file mode 100644 index 0000000..202028b Binary files /dev/null and b/resource/ship-library/assets/portraits/1372.webp differ diff --git a/resource/ship-library/assets/portraits/138.webp b/resource/ship-library/assets/portraits/138.webp new file mode 100644 index 0000000..6ae4c98 Binary files /dev/null and b/resource/ship-library/assets/portraits/138.webp differ diff --git a/resource/ship-library/assets/portraits/1380.webp b/resource/ship-library/assets/portraits/1380.webp new file mode 100644 index 0000000..6aa8c13 Binary files /dev/null and b/resource/ship-library/assets/portraits/1380.webp differ diff --git a/resource/ship-library/assets/portraits/139.webp b/resource/ship-library/assets/portraits/139.webp new file mode 100644 index 0000000..d7ec958 Binary files /dev/null and b/resource/ship-library/assets/portraits/139.webp differ diff --git a/resource/ship-library/assets/portraits/14.webp b/resource/ship-library/assets/portraits/14.webp new file mode 100644 index 0000000..584c6c5 Binary files /dev/null and b/resource/ship-library/assets/portraits/14.webp differ diff --git a/resource/ship-library/assets/portraits/140.webp b/resource/ship-library/assets/portraits/140.webp new file mode 100644 index 0000000..c5ecd20 Binary files /dev/null and b/resource/ship-library/assets/portraits/140.webp differ diff --git a/resource/ship-library/assets/portraits/1400.webp b/resource/ship-library/assets/portraits/1400.webp new file mode 100644 index 0000000..3b4f915 Binary files /dev/null and b/resource/ship-library/assets/portraits/1400.webp differ diff --git a/resource/ship-library/assets/portraits/1408.webp b/resource/ship-library/assets/portraits/1408.webp new file mode 100644 index 0000000..ff27871 Binary files /dev/null and b/resource/ship-library/assets/portraits/1408.webp differ diff --git a/resource/ship-library/assets/portraits/141.webp b/resource/ship-library/assets/portraits/141.webp new file mode 100644 index 0000000..1f71302 Binary files /dev/null and b/resource/ship-library/assets/portraits/141.webp differ diff --git a/resource/ship-library/assets/portraits/1413.webp b/resource/ship-library/assets/portraits/1413.webp new file mode 100644 index 0000000..0765e94 Binary files /dev/null and b/resource/ship-library/assets/portraits/1413.webp differ diff --git a/resource/ship-library/assets/portraits/1414.webp b/resource/ship-library/assets/portraits/1414.webp new file mode 100644 index 0000000..507687b Binary files /dev/null and b/resource/ship-library/assets/portraits/1414.webp differ diff --git a/resource/ship-library/assets/portraits/1416.webp b/resource/ship-library/assets/portraits/1416.webp new file mode 100644 index 0000000..2d0db1a Binary files /dev/null and b/resource/ship-library/assets/portraits/1416.webp differ diff --git a/resource/ship-library/assets/portraits/142.webp b/resource/ship-library/assets/portraits/142.webp new file mode 100644 index 0000000..695b285 Binary files /dev/null and b/resource/ship-library/assets/portraits/142.webp differ diff --git a/resource/ship-library/assets/portraits/1425.webp b/resource/ship-library/assets/portraits/1425.webp new file mode 100644 index 0000000..7f3fa34 Binary files /dev/null and b/resource/ship-library/assets/portraits/1425.webp differ diff --git a/resource/ship-library/assets/portraits/1426.webp b/resource/ship-library/assets/portraits/1426.webp new file mode 100644 index 0000000..183c3a5 Binary files /dev/null and b/resource/ship-library/assets/portraits/1426.webp differ diff --git a/resource/ship-library/assets/portraits/1429.webp b/resource/ship-library/assets/portraits/1429.webp new file mode 100644 index 0000000..5f023eb Binary files /dev/null and b/resource/ship-library/assets/portraits/1429.webp differ diff --git a/resource/ship-library/assets/portraits/143.webp b/resource/ship-library/assets/portraits/143.webp new file mode 100644 index 0000000..72e2c72 Binary files /dev/null and b/resource/ship-library/assets/portraits/143.webp differ diff --git a/resource/ship-library/assets/portraits/1430.webp b/resource/ship-library/assets/portraits/1430.webp new file mode 100644 index 0000000..f00cb51 Binary files /dev/null and b/resource/ship-library/assets/portraits/1430.webp differ diff --git a/resource/ship-library/assets/portraits/1431.webp b/resource/ship-library/assets/portraits/1431.webp new file mode 100644 index 0000000..e847540 Binary files /dev/null and b/resource/ship-library/assets/portraits/1431.webp differ diff --git a/resource/ship-library/assets/portraits/144.webp b/resource/ship-library/assets/portraits/144.webp new file mode 100644 index 0000000..bd3921f Binary files /dev/null and b/resource/ship-library/assets/portraits/144.webp differ diff --git a/resource/ship-library/assets/portraits/145.webp b/resource/ship-library/assets/portraits/145.webp new file mode 100644 index 0000000..711b8f8 Binary files /dev/null and b/resource/ship-library/assets/portraits/145.webp differ diff --git a/resource/ship-library/assets/portraits/1456.webp b/resource/ship-library/assets/portraits/1456.webp new file mode 100644 index 0000000..506a1ef Binary files /dev/null and b/resource/ship-library/assets/portraits/1456.webp differ diff --git a/resource/ship-library/assets/portraits/146.webp b/resource/ship-library/assets/portraits/146.webp new file mode 100644 index 0000000..1be3deb Binary files /dev/null and b/resource/ship-library/assets/portraits/146.webp differ diff --git a/resource/ship-library/assets/portraits/1467.webp b/resource/ship-library/assets/portraits/1467.webp new file mode 100644 index 0000000..672bf65 Binary files /dev/null and b/resource/ship-library/assets/portraits/1467.webp differ diff --git a/resource/ship-library/assets/portraits/147.webp b/resource/ship-library/assets/portraits/147.webp new file mode 100644 index 0000000..3058fe3 Binary files /dev/null and b/resource/ship-library/assets/portraits/147.webp differ diff --git a/resource/ship-library/assets/portraits/148.webp b/resource/ship-library/assets/portraits/148.webp new file mode 100644 index 0000000..f39191b Binary files /dev/null and b/resource/ship-library/assets/portraits/148.webp differ diff --git a/resource/ship-library/assets/portraits/1483.webp b/resource/ship-library/assets/portraits/1483.webp new file mode 100644 index 0000000..21630d3 Binary files /dev/null and b/resource/ship-library/assets/portraits/1483.webp differ diff --git a/resource/ship-library/assets/portraits/149.webp b/resource/ship-library/assets/portraits/149.webp new file mode 100644 index 0000000..3d18d95 Binary files /dev/null and b/resource/ship-library/assets/portraits/149.webp differ diff --git a/resource/ship-library/assets/portraits/1490.webp b/resource/ship-library/assets/portraits/1490.webp new file mode 100644 index 0000000..b9164c7 Binary files /dev/null and b/resource/ship-library/assets/portraits/1490.webp differ diff --git a/resource/ship-library/assets/portraits/1496.webp b/resource/ship-library/assets/portraits/1496.webp new file mode 100644 index 0000000..14bb818 Binary files /dev/null and b/resource/ship-library/assets/portraits/1496.webp differ diff --git a/resource/ship-library/assets/portraits/15.webp b/resource/ship-library/assets/portraits/15.webp new file mode 100644 index 0000000..e3560da Binary files /dev/null and b/resource/ship-library/assets/portraits/15.webp differ diff --git a/resource/ship-library/assets/portraits/150.webp b/resource/ship-library/assets/portraits/150.webp new file mode 100644 index 0000000..271e61a Binary files /dev/null and b/resource/ship-library/assets/portraits/150.webp differ diff --git a/resource/ship-library/assets/portraits/1503.webp b/resource/ship-library/assets/portraits/1503.webp new file mode 100644 index 0000000..ba81f55 Binary files /dev/null and b/resource/ship-library/assets/portraits/1503.webp differ diff --git a/resource/ship-library/assets/portraits/1509.webp b/resource/ship-library/assets/portraits/1509.webp new file mode 100644 index 0000000..c22cc4e Binary files /dev/null and b/resource/ship-library/assets/portraits/1509.webp differ diff --git a/resource/ship-library/assets/portraits/151.webp b/resource/ship-library/assets/portraits/151.webp new file mode 100644 index 0000000..c33690d Binary files /dev/null and b/resource/ship-library/assets/portraits/151.webp differ diff --git a/resource/ship-library/assets/portraits/1516.webp b/resource/ship-library/assets/portraits/1516.webp new file mode 100644 index 0000000..e136e59 Binary files /dev/null and b/resource/ship-library/assets/portraits/1516.webp differ diff --git a/resource/ship-library/assets/portraits/1517.webp b/resource/ship-library/assets/portraits/1517.webp new file mode 100644 index 0000000..339387b Binary files /dev/null and b/resource/ship-library/assets/portraits/1517.webp differ diff --git a/resource/ship-library/assets/portraits/1519.webp b/resource/ship-library/assets/portraits/1519.webp new file mode 100644 index 0000000..f95f4ae Binary files /dev/null and b/resource/ship-library/assets/portraits/1519.webp differ diff --git a/resource/ship-library/assets/portraits/152.webp b/resource/ship-library/assets/portraits/152.webp new file mode 100644 index 0000000..95c33c0 Binary files /dev/null and b/resource/ship-library/assets/portraits/152.webp differ diff --git a/resource/ship-library/assets/portraits/1527.webp b/resource/ship-library/assets/portraits/1527.webp new file mode 100644 index 0000000..1d97189 Binary files /dev/null and b/resource/ship-library/assets/portraits/1527.webp differ diff --git a/resource/ship-library/assets/portraits/1528.webp b/resource/ship-library/assets/portraits/1528.webp new file mode 100644 index 0000000..825b4bd Binary files /dev/null and b/resource/ship-library/assets/portraits/1528.webp differ diff --git a/resource/ship-library/assets/portraits/153.webp b/resource/ship-library/assets/portraits/153.webp new file mode 100644 index 0000000..4dff0fe Binary files /dev/null and b/resource/ship-library/assets/portraits/153.webp differ diff --git a/resource/ship-library/assets/portraits/1530.webp b/resource/ship-library/assets/portraits/1530.webp new file mode 100644 index 0000000..dab6c78 Binary files /dev/null and b/resource/ship-library/assets/portraits/1530.webp differ diff --git a/resource/ship-library/assets/portraits/1532.webp b/resource/ship-library/assets/portraits/1532.webp new file mode 100644 index 0000000..eac88af Binary files /dev/null and b/resource/ship-library/assets/portraits/1532.webp differ diff --git a/resource/ship-library/assets/portraits/1538.webp b/resource/ship-library/assets/portraits/1538.webp new file mode 100644 index 0000000..cd95a29 Binary files /dev/null and b/resource/ship-library/assets/portraits/1538.webp differ diff --git a/resource/ship-library/assets/portraits/154.webp b/resource/ship-library/assets/portraits/154.webp new file mode 100644 index 0000000..e1d8c94 Binary files /dev/null and b/resource/ship-library/assets/portraits/154.webp differ diff --git a/resource/ship-library/assets/portraits/1545.webp b/resource/ship-library/assets/portraits/1545.webp new file mode 100644 index 0000000..8c5d41c Binary files /dev/null and b/resource/ship-library/assets/portraits/1545.webp differ diff --git a/resource/ship-library/assets/portraits/155.webp b/resource/ship-library/assets/portraits/155.webp new file mode 100644 index 0000000..e846f09 Binary files /dev/null and b/resource/ship-library/assets/portraits/155.webp differ diff --git a/resource/ship-library/assets/portraits/156.webp b/resource/ship-library/assets/portraits/156.webp new file mode 100644 index 0000000..f98f646 Binary files /dev/null and b/resource/ship-library/assets/portraits/156.webp differ diff --git a/resource/ship-library/assets/portraits/1560.webp b/resource/ship-library/assets/portraits/1560.webp new file mode 100644 index 0000000..7538d6d Binary files /dev/null and b/resource/ship-library/assets/portraits/1560.webp differ diff --git a/resource/ship-library/assets/portraits/157.webp b/resource/ship-library/assets/portraits/157.webp new file mode 100644 index 0000000..3aa837b Binary files /dev/null and b/resource/ship-library/assets/portraits/157.webp differ diff --git a/resource/ship-library/assets/portraits/1575.webp b/resource/ship-library/assets/portraits/1575.webp new file mode 100644 index 0000000..b3019a1 Binary files /dev/null and b/resource/ship-library/assets/portraits/1575.webp differ diff --git a/resource/ship-library/assets/portraits/158.webp b/resource/ship-library/assets/portraits/158.webp new file mode 100644 index 0000000..c2faa73 Binary files /dev/null and b/resource/ship-library/assets/portraits/158.webp differ diff --git a/resource/ship-library/assets/portraits/1583.webp b/resource/ship-library/assets/portraits/1583.webp new file mode 100644 index 0000000..d64177c Binary files /dev/null and b/resource/ship-library/assets/portraits/1583.webp differ diff --git a/resource/ship-library/assets/portraits/1589.webp b/resource/ship-library/assets/portraits/1589.webp new file mode 100644 index 0000000..84c1115 Binary files /dev/null and b/resource/ship-library/assets/portraits/1589.webp differ diff --git a/resource/ship-library/assets/portraits/159.webp b/resource/ship-library/assets/portraits/159.webp new file mode 100644 index 0000000..b712ff3 Binary files /dev/null and b/resource/ship-library/assets/portraits/159.webp differ diff --git a/resource/ship-library/assets/portraits/1590.webp b/resource/ship-library/assets/portraits/1590.webp new file mode 100644 index 0000000..865bbf0 Binary files /dev/null and b/resource/ship-library/assets/portraits/1590.webp differ diff --git a/resource/ship-library/assets/portraits/16.webp b/resource/ship-library/assets/portraits/16.webp new file mode 100644 index 0000000..775c04f Binary files /dev/null and b/resource/ship-library/assets/portraits/16.webp differ diff --git a/resource/ship-library/assets/portraits/160.webp b/resource/ship-library/assets/portraits/160.webp new file mode 100644 index 0000000..0ca2e5c Binary files /dev/null and b/resource/ship-library/assets/portraits/160.webp differ diff --git a/resource/ship-library/assets/portraits/1606.webp b/resource/ship-library/assets/portraits/1606.webp new file mode 100644 index 0000000..caea75f Binary files /dev/null and b/resource/ship-library/assets/portraits/1606.webp differ diff --git a/resource/ship-library/assets/portraits/161.webp b/resource/ship-library/assets/portraits/161.webp new file mode 100644 index 0000000..9861832 Binary files /dev/null and b/resource/ship-library/assets/portraits/161.webp differ diff --git a/resource/ship-library/assets/portraits/1618.webp b/resource/ship-library/assets/portraits/1618.webp new file mode 100644 index 0000000..3e5e6b5 Binary files /dev/null and b/resource/ship-library/assets/portraits/1618.webp differ diff --git a/resource/ship-library/assets/portraits/162.webp b/resource/ship-library/assets/portraits/162.webp new file mode 100644 index 0000000..8a06eae Binary files /dev/null and b/resource/ship-library/assets/portraits/162.webp differ diff --git a/resource/ship-library/assets/portraits/163.webp b/resource/ship-library/assets/portraits/163.webp new file mode 100644 index 0000000..0e60ee4 Binary files /dev/null and b/resource/ship-library/assets/portraits/163.webp differ diff --git a/resource/ship-library/assets/portraits/164.webp b/resource/ship-library/assets/portraits/164.webp new file mode 100644 index 0000000..3186fb7 Binary files /dev/null and b/resource/ship-library/assets/portraits/164.webp differ diff --git a/resource/ship-library/assets/portraits/165.webp b/resource/ship-library/assets/portraits/165.webp new file mode 100644 index 0000000..c8b38f7 Binary files /dev/null and b/resource/ship-library/assets/portraits/165.webp differ diff --git a/resource/ship-library/assets/portraits/166.webp b/resource/ship-library/assets/portraits/166.webp new file mode 100644 index 0000000..a5f74e2 Binary files /dev/null and b/resource/ship-library/assets/portraits/166.webp differ diff --git a/resource/ship-library/assets/portraits/167.webp b/resource/ship-library/assets/portraits/167.webp new file mode 100644 index 0000000..c40802c Binary files /dev/null and b/resource/ship-library/assets/portraits/167.webp differ diff --git a/resource/ship-library/assets/portraits/168.webp b/resource/ship-library/assets/portraits/168.webp new file mode 100644 index 0000000..99d099d Binary files /dev/null and b/resource/ship-library/assets/portraits/168.webp differ diff --git a/resource/ship-library/assets/portraits/169.webp b/resource/ship-library/assets/portraits/169.webp new file mode 100644 index 0000000..fe62d7f Binary files /dev/null and b/resource/ship-library/assets/portraits/169.webp differ diff --git a/resource/ship-library/assets/portraits/17.webp b/resource/ship-library/assets/portraits/17.webp new file mode 100644 index 0000000..072654b Binary files /dev/null and b/resource/ship-library/assets/portraits/17.webp differ diff --git a/resource/ship-library/assets/portraits/170.webp b/resource/ship-library/assets/portraits/170.webp new file mode 100644 index 0000000..fc9abd2 Binary files /dev/null and b/resource/ship-library/assets/portraits/170.webp differ diff --git a/resource/ship-library/assets/portraits/171.webp b/resource/ship-library/assets/portraits/171.webp new file mode 100644 index 0000000..5f7033c Binary files /dev/null and b/resource/ship-library/assets/portraits/171.webp differ diff --git a/resource/ship-library/assets/portraits/172.webp b/resource/ship-library/assets/portraits/172.webp new file mode 100644 index 0000000..178fe41 Binary files /dev/null and b/resource/ship-library/assets/portraits/172.webp differ diff --git a/resource/ship-library/assets/portraits/173.webp b/resource/ship-library/assets/portraits/173.webp new file mode 100644 index 0000000..0a48ac9 Binary files /dev/null and b/resource/ship-library/assets/portraits/173.webp differ diff --git a/resource/ship-library/assets/portraits/174.webp b/resource/ship-library/assets/portraits/174.webp new file mode 100644 index 0000000..6caf1ba Binary files /dev/null and b/resource/ship-library/assets/portraits/174.webp differ diff --git a/resource/ship-library/assets/portraits/175.webp b/resource/ship-library/assets/portraits/175.webp new file mode 100644 index 0000000..b20ab16 Binary files /dev/null and b/resource/ship-library/assets/portraits/175.webp differ diff --git a/resource/ship-library/assets/portraits/176.webp b/resource/ship-library/assets/portraits/176.webp new file mode 100644 index 0000000..551bec0 Binary files /dev/null and b/resource/ship-library/assets/portraits/176.webp differ diff --git a/resource/ship-library/assets/portraits/177.webp b/resource/ship-library/assets/portraits/177.webp new file mode 100644 index 0000000..ef746e9 Binary files /dev/null and b/resource/ship-library/assets/portraits/177.webp differ diff --git a/resource/ship-library/assets/portraits/178.webp b/resource/ship-library/assets/portraits/178.webp new file mode 100644 index 0000000..20d0243 Binary files /dev/null and b/resource/ship-library/assets/portraits/178.webp differ diff --git a/resource/ship-library/assets/portraits/179.webp b/resource/ship-library/assets/portraits/179.webp new file mode 100644 index 0000000..8be9d7c Binary files /dev/null and b/resource/ship-library/assets/portraits/179.webp differ diff --git a/resource/ship-library/assets/portraits/18.webp b/resource/ship-library/assets/portraits/18.webp new file mode 100644 index 0000000..3981629 Binary files /dev/null and b/resource/ship-library/assets/portraits/18.webp differ diff --git a/resource/ship-library/assets/portraits/180.webp b/resource/ship-library/assets/portraits/180.webp new file mode 100644 index 0000000..68a2b10 Binary files /dev/null and b/resource/ship-library/assets/portraits/180.webp differ diff --git a/resource/ship-library/assets/portraits/181.webp b/resource/ship-library/assets/portraits/181.webp new file mode 100644 index 0000000..508c116 Binary files /dev/null and b/resource/ship-library/assets/portraits/181.webp differ diff --git a/resource/ship-library/assets/portraits/182.webp b/resource/ship-library/assets/portraits/182.webp new file mode 100644 index 0000000..a4ca25a Binary files /dev/null and b/resource/ship-library/assets/portraits/182.webp differ diff --git a/resource/ship-library/assets/portraits/183.webp b/resource/ship-library/assets/portraits/183.webp new file mode 100644 index 0000000..943ae15 Binary files /dev/null and b/resource/ship-library/assets/portraits/183.webp differ diff --git a/resource/ship-library/assets/portraits/184.webp b/resource/ship-library/assets/portraits/184.webp new file mode 100644 index 0000000..53037ff Binary files /dev/null and b/resource/ship-library/assets/portraits/184.webp differ diff --git a/resource/ship-library/assets/portraits/185.webp b/resource/ship-library/assets/portraits/185.webp new file mode 100644 index 0000000..7dbccd5 Binary files /dev/null and b/resource/ship-library/assets/portraits/185.webp differ diff --git a/resource/ship-library/assets/portraits/186.webp b/resource/ship-library/assets/portraits/186.webp new file mode 100644 index 0000000..fd9708e Binary files /dev/null and b/resource/ship-library/assets/portraits/186.webp differ diff --git a/resource/ship-library/assets/portraits/187.webp b/resource/ship-library/assets/portraits/187.webp new file mode 100644 index 0000000..214476a Binary files /dev/null and b/resource/ship-library/assets/portraits/187.webp differ diff --git a/resource/ship-library/assets/portraits/188.webp b/resource/ship-library/assets/portraits/188.webp new file mode 100644 index 0000000..99d1b1a Binary files /dev/null and b/resource/ship-library/assets/portraits/188.webp differ diff --git a/resource/ship-library/assets/portraits/189.webp b/resource/ship-library/assets/portraits/189.webp new file mode 100644 index 0000000..75a36b4 Binary files /dev/null and b/resource/ship-library/assets/portraits/189.webp differ diff --git a/resource/ship-library/assets/portraits/19.webp b/resource/ship-library/assets/portraits/19.webp new file mode 100644 index 0000000..a2de2c3 Binary files /dev/null and b/resource/ship-library/assets/portraits/19.webp differ diff --git a/resource/ship-library/assets/portraits/190.webp b/resource/ship-library/assets/portraits/190.webp new file mode 100644 index 0000000..ffc1051 Binary files /dev/null and b/resource/ship-library/assets/portraits/190.webp differ diff --git a/resource/ship-library/assets/portraits/191.webp b/resource/ship-library/assets/portraits/191.webp new file mode 100644 index 0000000..db82719 Binary files /dev/null and b/resource/ship-library/assets/portraits/191.webp differ diff --git a/resource/ship-library/assets/portraits/192.webp b/resource/ship-library/assets/portraits/192.webp new file mode 100644 index 0000000..6a47eea Binary files /dev/null and b/resource/ship-library/assets/portraits/192.webp differ diff --git a/resource/ship-library/assets/portraits/193.webp b/resource/ship-library/assets/portraits/193.webp new file mode 100644 index 0000000..086f569 Binary files /dev/null and b/resource/ship-library/assets/portraits/193.webp differ diff --git a/resource/ship-library/assets/portraits/194.webp b/resource/ship-library/assets/portraits/194.webp new file mode 100644 index 0000000..79868c5 Binary files /dev/null and b/resource/ship-library/assets/portraits/194.webp differ diff --git a/resource/ship-library/assets/portraits/195.webp b/resource/ship-library/assets/portraits/195.webp new file mode 100644 index 0000000..f9f1f3c Binary files /dev/null and b/resource/ship-library/assets/portraits/195.webp differ diff --git a/resource/ship-library/assets/portraits/196.webp b/resource/ship-library/assets/portraits/196.webp new file mode 100644 index 0000000..a22d9c0 Binary files /dev/null and b/resource/ship-library/assets/portraits/196.webp differ diff --git a/resource/ship-library/assets/portraits/197.webp b/resource/ship-library/assets/portraits/197.webp new file mode 100644 index 0000000..5babcfe Binary files /dev/null and b/resource/ship-library/assets/portraits/197.webp differ diff --git a/resource/ship-library/assets/portraits/198.webp b/resource/ship-library/assets/portraits/198.webp new file mode 100644 index 0000000..67cdd95 Binary files /dev/null and b/resource/ship-library/assets/portraits/198.webp differ diff --git a/resource/ship-library/assets/portraits/199.webp b/resource/ship-library/assets/portraits/199.webp new file mode 100644 index 0000000..65dfd7a Binary files /dev/null and b/resource/ship-library/assets/portraits/199.webp differ diff --git a/resource/ship-library/assets/portraits/2.webp b/resource/ship-library/assets/portraits/2.webp new file mode 100644 index 0000000..e145976 Binary files /dev/null and b/resource/ship-library/assets/portraits/2.webp differ diff --git a/resource/ship-library/assets/portraits/20.webp b/resource/ship-library/assets/portraits/20.webp new file mode 100644 index 0000000..729ad0c Binary files /dev/null and b/resource/ship-library/assets/portraits/20.webp differ diff --git a/resource/ship-library/assets/portraits/200.webp b/resource/ship-library/assets/portraits/200.webp new file mode 100644 index 0000000..b463d9c Binary files /dev/null and b/resource/ship-library/assets/portraits/200.webp differ diff --git a/resource/ship-library/assets/portraits/201.webp b/resource/ship-library/assets/portraits/201.webp new file mode 100644 index 0000000..ca7fb53 Binary files /dev/null and b/resource/ship-library/assets/portraits/201.webp differ diff --git a/resource/ship-library/assets/portraits/202.webp b/resource/ship-library/assets/portraits/202.webp new file mode 100644 index 0000000..4e6e9f7 Binary files /dev/null and b/resource/ship-library/assets/portraits/202.webp differ diff --git a/resource/ship-library/assets/portraits/203.webp b/resource/ship-library/assets/portraits/203.webp new file mode 100644 index 0000000..7b644a6 Binary files /dev/null and b/resource/ship-library/assets/portraits/203.webp differ diff --git a/resource/ship-library/assets/portraits/205.webp b/resource/ship-library/assets/portraits/205.webp new file mode 100644 index 0000000..677cadc Binary files /dev/null and b/resource/ship-library/assets/portraits/205.webp differ diff --git a/resource/ship-library/assets/portraits/206.webp b/resource/ship-library/assets/portraits/206.webp new file mode 100644 index 0000000..6fc3829 Binary files /dev/null and b/resource/ship-library/assets/portraits/206.webp differ diff --git a/resource/ship-library/assets/portraits/207.webp b/resource/ship-library/assets/portraits/207.webp new file mode 100644 index 0000000..caec2d9 Binary files /dev/null and b/resource/ship-library/assets/portraits/207.webp differ diff --git a/resource/ship-library/assets/portraits/208.webp b/resource/ship-library/assets/portraits/208.webp new file mode 100644 index 0000000..dead11c Binary files /dev/null and b/resource/ship-library/assets/portraits/208.webp differ diff --git a/resource/ship-library/assets/portraits/209.webp b/resource/ship-library/assets/portraits/209.webp new file mode 100644 index 0000000..3879229 Binary files /dev/null and b/resource/ship-library/assets/portraits/209.webp differ diff --git a/resource/ship-library/assets/portraits/21.webp b/resource/ship-library/assets/portraits/21.webp new file mode 100644 index 0000000..39c4d32 Binary files /dev/null and b/resource/ship-library/assets/portraits/21.webp differ diff --git a/resource/ship-library/assets/portraits/210.webp b/resource/ship-library/assets/portraits/210.webp new file mode 100644 index 0000000..f83f429 Binary files /dev/null and b/resource/ship-library/assets/portraits/210.webp differ diff --git a/resource/ship-library/assets/portraits/211.webp b/resource/ship-library/assets/portraits/211.webp new file mode 100644 index 0000000..2f1f9df Binary files /dev/null and b/resource/ship-library/assets/portraits/211.webp differ diff --git a/resource/ship-library/assets/portraits/212.webp b/resource/ship-library/assets/portraits/212.webp new file mode 100644 index 0000000..0a23511 Binary files /dev/null and b/resource/ship-library/assets/portraits/212.webp differ diff --git a/resource/ship-library/assets/portraits/213.webp b/resource/ship-library/assets/portraits/213.webp new file mode 100644 index 0000000..e18781c Binary files /dev/null and b/resource/ship-library/assets/portraits/213.webp differ diff --git a/resource/ship-library/assets/portraits/214.webp b/resource/ship-library/assets/portraits/214.webp new file mode 100644 index 0000000..4d17cff Binary files /dev/null and b/resource/ship-library/assets/portraits/214.webp differ diff --git a/resource/ship-library/assets/portraits/215.webp b/resource/ship-library/assets/portraits/215.webp new file mode 100644 index 0000000..c283d3f Binary files /dev/null and b/resource/ship-library/assets/portraits/215.webp differ diff --git a/resource/ship-library/assets/portraits/216.webp b/resource/ship-library/assets/portraits/216.webp new file mode 100644 index 0000000..271cf9f Binary files /dev/null and b/resource/ship-library/assets/portraits/216.webp differ diff --git a/resource/ship-library/assets/portraits/217.webp b/resource/ship-library/assets/portraits/217.webp new file mode 100644 index 0000000..fea93f1 Binary files /dev/null and b/resource/ship-library/assets/portraits/217.webp differ diff --git a/resource/ship-library/assets/portraits/218.webp b/resource/ship-library/assets/portraits/218.webp new file mode 100644 index 0000000..ba31426 Binary files /dev/null and b/resource/ship-library/assets/portraits/218.webp differ diff --git a/resource/ship-library/assets/portraits/219.webp b/resource/ship-library/assets/portraits/219.webp new file mode 100644 index 0000000..70b3d20 Binary files /dev/null and b/resource/ship-library/assets/portraits/219.webp differ diff --git a/resource/ship-library/assets/portraits/22.webp b/resource/ship-library/assets/portraits/22.webp new file mode 100644 index 0000000..e6648d6 Binary files /dev/null and b/resource/ship-library/assets/portraits/22.webp differ diff --git a/resource/ship-library/assets/portraits/220.webp b/resource/ship-library/assets/portraits/220.webp new file mode 100644 index 0000000..0b3654f Binary files /dev/null and b/resource/ship-library/assets/portraits/220.webp differ diff --git a/resource/ship-library/assets/portraits/221.webp b/resource/ship-library/assets/portraits/221.webp new file mode 100644 index 0000000..9d91579 Binary files /dev/null and b/resource/ship-library/assets/portraits/221.webp differ diff --git a/resource/ship-library/assets/portraits/222.webp b/resource/ship-library/assets/portraits/222.webp new file mode 100644 index 0000000..33576d3 Binary files /dev/null and b/resource/ship-library/assets/portraits/222.webp differ diff --git a/resource/ship-library/assets/portraits/223.webp b/resource/ship-library/assets/portraits/223.webp new file mode 100644 index 0000000..08eb706 Binary files /dev/null and b/resource/ship-library/assets/portraits/223.webp differ diff --git a/resource/ship-library/assets/portraits/224.webp b/resource/ship-library/assets/portraits/224.webp new file mode 100644 index 0000000..942eae5 Binary files /dev/null and b/resource/ship-library/assets/portraits/224.webp differ diff --git a/resource/ship-library/assets/portraits/225.webp b/resource/ship-library/assets/portraits/225.webp new file mode 100644 index 0000000..24f532a Binary files /dev/null and b/resource/ship-library/assets/portraits/225.webp differ diff --git a/resource/ship-library/assets/portraits/226.webp b/resource/ship-library/assets/portraits/226.webp new file mode 100644 index 0000000..ee3958c Binary files /dev/null and b/resource/ship-library/assets/portraits/226.webp differ diff --git a/resource/ship-library/assets/portraits/227.webp b/resource/ship-library/assets/portraits/227.webp new file mode 100644 index 0000000..b1e90f5 Binary files /dev/null and b/resource/ship-library/assets/portraits/227.webp differ diff --git a/resource/ship-library/assets/portraits/228.webp b/resource/ship-library/assets/portraits/228.webp new file mode 100644 index 0000000..5af15be Binary files /dev/null and b/resource/ship-library/assets/portraits/228.webp differ diff --git a/resource/ship-library/assets/portraits/229.webp b/resource/ship-library/assets/portraits/229.webp new file mode 100644 index 0000000..1519af4 Binary files /dev/null and b/resource/ship-library/assets/portraits/229.webp differ diff --git a/resource/ship-library/assets/portraits/23.webp b/resource/ship-library/assets/portraits/23.webp new file mode 100644 index 0000000..26f767d Binary files /dev/null and b/resource/ship-library/assets/portraits/23.webp differ diff --git a/resource/ship-library/assets/portraits/230.webp b/resource/ship-library/assets/portraits/230.webp new file mode 100644 index 0000000..fa30bba Binary files /dev/null and b/resource/ship-library/assets/portraits/230.webp differ diff --git a/resource/ship-library/assets/portraits/231.webp b/resource/ship-library/assets/portraits/231.webp new file mode 100644 index 0000000..d4967e8 Binary files /dev/null and b/resource/ship-library/assets/portraits/231.webp differ diff --git a/resource/ship-library/assets/portraits/232.webp b/resource/ship-library/assets/portraits/232.webp new file mode 100644 index 0000000..c412421 Binary files /dev/null and b/resource/ship-library/assets/portraits/232.webp differ diff --git a/resource/ship-library/assets/portraits/233.webp b/resource/ship-library/assets/portraits/233.webp new file mode 100644 index 0000000..8029a0f Binary files /dev/null and b/resource/ship-library/assets/portraits/233.webp differ diff --git a/resource/ship-library/assets/portraits/234.webp b/resource/ship-library/assets/portraits/234.webp new file mode 100644 index 0000000..15eabc4 Binary files /dev/null and b/resource/ship-library/assets/portraits/234.webp differ diff --git a/resource/ship-library/assets/portraits/235.webp b/resource/ship-library/assets/portraits/235.webp new file mode 100644 index 0000000..0339f1d Binary files /dev/null and b/resource/ship-library/assets/portraits/235.webp differ diff --git a/resource/ship-library/assets/portraits/236.webp b/resource/ship-library/assets/portraits/236.webp new file mode 100644 index 0000000..b6ebec0 Binary files /dev/null and b/resource/ship-library/assets/portraits/236.webp differ diff --git a/resource/ship-library/assets/portraits/237.webp b/resource/ship-library/assets/portraits/237.webp new file mode 100644 index 0000000..c592ebc Binary files /dev/null and b/resource/ship-library/assets/portraits/237.webp differ diff --git a/resource/ship-library/assets/portraits/238.webp b/resource/ship-library/assets/portraits/238.webp new file mode 100644 index 0000000..d5fcd20 Binary files /dev/null and b/resource/ship-library/assets/portraits/238.webp differ diff --git a/resource/ship-library/assets/portraits/239.webp b/resource/ship-library/assets/portraits/239.webp new file mode 100644 index 0000000..c973bbf Binary files /dev/null and b/resource/ship-library/assets/portraits/239.webp differ diff --git a/resource/ship-library/assets/portraits/24.webp b/resource/ship-library/assets/portraits/24.webp new file mode 100644 index 0000000..ddf49df Binary files /dev/null and b/resource/ship-library/assets/portraits/24.webp differ diff --git a/resource/ship-library/assets/portraits/240.webp b/resource/ship-library/assets/portraits/240.webp new file mode 100644 index 0000000..47ce90e Binary files /dev/null and b/resource/ship-library/assets/portraits/240.webp differ diff --git a/resource/ship-library/assets/portraits/241.webp b/resource/ship-library/assets/portraits/241.webp new file mode 100644 index 0000000..9bbe28c Binary files /dev/null and b/resource/ship-library/assets/portraits/241.webp differ diff --git a/resource/ship-library/assets/portraits/242.webp b/resource/ship-library/assets/portraits/242.webp new file mode 100644 index 0000000..e8cff44 Binary files /dev/null and b/resource/ship-library/assets/portraits/242.webp differ diff --git a/resource/ship-library/assets/portraits/243.webp b/resource/ship-library/assets/portraits/243.webp new file mode 100644 index 0000000..41387bb Binary files /dev/null and b/resource/ship-library/assets/portraits/243.webp differ diff --git a/resource/ship-library/assets/portraits/244.webp b/resource/ship-library/assets/portraits/244.webp new file mode 100644 index 0000000..5746488 Binary files /dev/null and b/resource/ship-library/assets/portraits/244.webp differ diff --git a/resource/ship-library/assets/portraits/245.webp b/resource/ship-library/assets/portraits/245.webp new file mode 100644 index 0000000..935b043 Binary files /dev/null and b/resource/ship-library/assets/portraits/245.webp differ diff --git a/resource/ship-library/assets/portraits/246.webp b/resource/ship-library/assets/portraits/246.webp new file mode 100644 index 0000000..be95435 Binary files /dev/null and b/resource/ship-library/assets/portraits/246.webp differ diff --git a/resource/ship-library/assets/portraits/247.webp b/resource/ship-library/assets/portraits/247.webp new file mode 100644 index 0000000..0f7cfa6 Binary files /dev/null and b/resource/ship-library/assets/portraits/247.webp differ diff --git a/resource/ship-library/assets/portraits/248.webp b/resource/ship-library/assets/portraits/248.webp new file mode 100644 index 0000000..a55cb52 Binary files /dev/null and b/resource/ship-library/assets/portraits/248.webp differ diff --git a/resource/ship-library/assets/portraits/249.webp b/resource/ship-library/assets/portraits/249.webp new file mode 100644 index 0000000..fa597de Binary files /dev/null and b/resource/ship-library/assets/portraits/249.webp differ diff --git a/resource/ship-library/assets/portraits/25.webp b/resource/ship-library/assets/portraits/25.webp new file mode 100644 index 0000000..5450406 Binary files /dev/null and b/resource/ship-library/assets/portraits/25.webp differ diff --git a/resource/ship-library/assets/portraits/250.webp b/resource/ship-library/assets/portraits/250.webp new file mode 100644 index 0000000..584a16f Binary files /dev/null and b/resource/ship-library/assets/portraits/250.webp differ diff --git a/resource/ship-library/assets/portraits/251.webp b/resource/ship-library/assets/portraits/251.webp new file mode 100644 index 0000000..2c2bacb Binary files /dev/null and b/resource/ship-library/assets/portraits/251.webp differ diff --git a/resource/ship-library/assets/portraits/252.webp b/resource/ship-library/assets/portraits/252.webp new file mode 100644 index 0000000..4e32597 Binary files /dev/null and b/resource/ship-library/assets/portraits/252.webp differ diff --git a/resource/ship-library/assets/portraits/253.webp b/resource/ship-library/assets/portraits/253.webp new file mode 100644 index 0000000..786b6aa Binary files /dev/null and b/resource/ship-library/assets/portraits/253.webp differ diff --git a/resource/ship-library/assets/portraits/254.webp b/resource/ship-library/assets/portraits/254.webp new file mode 100644 index 0000000..78ce681 Binary files /dev/null and b/resource/ship-library/assets/portraits/254.webp differ diff --git a/resource/ship-library/assets/portraits/255.webp b/resource/ship-library/assets/portraits/255.webp new file mode 100644 index 0000000..8f53bee Binary files /dev/null and b/resource/ship-library/assets/portraits/255.webp differ diff --git a/resource/ship-library/assets/portraits/256.webp b/resource/ship-library/assets/portraits/256.webp new file mode 100644 index 0000000..26d72ef Binary files /dev/null and b/resource/ship-library/assets/portraits/256.webp differ diff --git a/resource/ship-library/assets/portraits/257.webp b/resource/ship-library/assets/portraits/257.webp new file mode 100644 index 0000000..9031220 Binary files /dev/null and b/resource/ship-library/assets/portraits/257.webp differ diff --git a/resource/ship-library/assets/portraits/258.webp b/resource/ship-library/assets/portraits/258.webp new file mode 100644 index 0000000..b44aef3 Binary files /dev/null and b/resource/ship-library/assets/portraits/258.webp differ diff --git a/resource/ship-library/assets/portraits/259.webp b/resource/ship-library/assets/portraits/259.webp new file mode 100644 index 0000000..a84b337 Binary files /dev/null and b/resource/ship-library/assets/portraits/259.webp differ diff --git a/resource/ship-library/assets/portraits/26.webp b/resource/ship-library/assets/portraits/26.webp new file mode 100644 index 0000000..29d48cc Binary files /dev/null and b/resource/ship-library/assets/portraits/26.webp differ diff --git a/resource/ship-library/assets/portraits/260.webp b/resource/ship-library/assets/portraits/260.webp new file mode 100644 index 0000000..9586dbe Binary files /dev/null and b/resource/ship-library/assets/portraits/260.webp differ diff --git a/resource/ship-library/assets/portraits/261.webp b/resource/ship-library/assets/portraits/261.webp new file mode 100644 index 0000000..3f9c499 Binary files /dev/null and b/resource/ship-library/assets/portraits/261.webp differ diff --git a/resource/ship-library/assets/portraits/262.webp b/resource/ship-library/assets/portraits/262.webp new file mode 100644 index 0000000..280b2e0 Binary files /dev/null and b/resource/ship-library/assets/portraits/262.webp differ diff --git a/resource/ship-library/assets/portraits/263.webp b/resource/ship-library/assets/portraits/263.webp new file mode 100644 index 0000000..465ea29 Binary files /dev/null and b/resource/ship-library/assets/portraits/263.webp differ diff --git a/resource/ship-library/assets/portraits/264.webp b/resource/ship-library/assets/portraits/264.webp new file mode 100644 index 0000000..fec12b7 Binary files /dev/null and b/resource/ship-library/assets/portraits/264.webp differ diff --git a/resource/ship-library/assets/portraits/265.webp b/resource/ship-library/assets/portraits/265.webp new file mode 100644 index 0000000..ed449c5 Binary files /dev/null and b/resource/ship-library/assets/portraits/265.webp differ diff --git a/resource/ship-library/assets/portraits/266.webp b/resource/ship-library/assets/portraits/266.webp new file mode 100644 index 0000000..286b00c Binary files /dev/null and b/resource/ship-library/assets/portraits/266.webp differ diff --git a/resource/ship-library/assets/portraits/267.webp b/resource/ship-library/assets/portraits/267.webp new file mode 100644 index 0000000..0c46053 Binary files /dev/null and b/resource/ship-library/assets/portraits/267.webp differ diff --git a/resource/ship-library/assets/portraits/268.webp b/resource/ship-library/assets/portraits/268.webp new file mode 100644 index 0000000..095b715 Binary files /dev/null and b/resource/ship-library/assets/portraits/268.webp differ diff --git a/resource/ship-library/assets/portraits/269.webp b/resource/ship-library/assets/portraits/269.webp new file mode 100644 index 0000000..a69afb9 Binary files /dev/null and b/resource/ship-library/assets/portraits/269.webp differ diff --git a/resource/ship-library/assets/portraits/27.webp b/resource/ship-library/assets/portraits/27.webp new file mode 100644 index 0000000..6abeb26 Binary files /dev/null and b/resource/ship-library/assets/portraits/27.webp differ diff --git a/resource/ship-library/assets/portraits/270.webp b/resource/ship-library/assets/portraits/270.webp new file mode 100644 index 0000000..d53baef Binary files /dev/null and b/resource/ship-library/assets/portraits/270.webp differ diff --git a/resource/ship-library/assets/portraits/271.webp b/resource/ship-library/assets/portraits/271.webp new file mode 100644 index 0000000..fc9ab98 Binary files /dev/null and b/resource/ship-library/assets/portraits/271.webp differ diff --git a/resource/ship-library/assets/portraits/272.webp b/resource/ship-library/assets/portraits/272.webp new file mode 100644 index 0000000..702a379 Binary files /dev/null and b/resource/ship-library/assets/portraits/272.webp differ diff --git a/resource/ship-library/assets/portraits/273.webp b/resource/ship-library/assets/portraits/273.webp new file mode 100644 index 0000000..cd8bae5 Binary files /dev/null and b/resource/ship-library/assets/portraits/273.webp differ diff --git a/resource/ship-library/assets/portraits/274.webp b/resource/ship-library/assets/portraits/274.webp new file mode 100644 index 0000000..e5cdf8c Binary files /dev/null and b/resource/ship-library/assets/portraits/274.webp differ diff --git a/resource/ship-library/assets/portraits/275.webp b/resource/ship-library/assets/portraits/275.webp new file mode 100644 index 0000000..8f12aea Binary files /dev/null and b/resource/ship-library/assets/portraits/275.webp differ diff --git a/resource/ship-library/assets/portraits/276.webp b/resource/ship-library/assets/portraits/276.webp new file mode 100644 index 0000000..cc2b027 Binary files /dev/null and b/resource/ship-library/assets/portraits/276.webp differ diff --git a/resource/ship-library/assets/portraits/277.webp b/resource/ship-library/assets/portraits/277.webp new file mode 100644 index 0000000..01e457e Binary files /dev/null and b/resource/ship-library/assets/portraits/277.webp differ diff --git a/resource/ship-library/assets/portraits/278.webp b/resource/ship-library/assets/portraits/278.webp new file mode 100644 index 0000000..1a27921 Binary files /dev/null and b/resource/ship-library/assets/portraits/278.webp differ diff --git a/resource/ship-library/assets/portraits/279.webp b/resource/ship-library/assets/portraits/279.webp new file mode 100644 index 0000000..67dcc5a Binary files /dev/null and b/resource/ship-library/assets/portraits/279.webp differ diff --git a/resource/ship-library/assets/portraits/28.webp b/resource/ship-library/assets/portraits/28.webp new file mode 100644 index 0000000..37f589a Binary files /dev/null and b/resource/ship-library/assets/portraits/28.webp differ diff --git a/resource/ship-library/assets/portraits/280.webp b/resource/ship-library/assets/portraits/280.webp new file mode 100644 index 0000000..e416986 Binary files /dev/null and b/resource/ship-library/assets/portraits/280.webp differ diff --git a/resource/ship-library/assets/portraits/281.webp b/resource/ship-library/assets/portraits/281.webp new file mode 100644 index 0000000..13fd126 Binary files /dev/null and b/resource/ship-library/assets/portraits/281.webp differ diff --git a/resource/ship-library/assets/portraits/282.webp b/resource/ship-library/assets/portraits/282.webp new file mode 100644 index 0000000..f8df04f Binary files /dev/null and b/resource/ship-library/assets/portraits/282.webp differ diff --git a/resource/ship-library/assets/portraits/283.webp b/resource/ship-library/assets/portraits/283.webp new file mode 100644 index 0000000..4532d9f Binary files /dev/null and b/resource/ship-library/assets/portraits/283.webp differ diff --git a/resource/ship-library/assets/portraits/284.webp b/resource/ship-library/assets/portraits/284.webp new file mode 100644 index 0000000..d606c56 Binary files /dev/null and b/resource/ship-library/assets/portraits/284.webp differ diff --git a/resource/ship-library/assets/portraits/285.webp b/resource/ship-library/assets/portraits/285.webp new file mode 100644 index 0000000..0c23699 Binary files /dev/null and b/resource/ship-library/assets/portraits/285.webp differ diff --git a/resource/ship-library/assets/portraits/286.webp b/resource/ship-library/assets/portraits/286.webp new file mode 100644 index 0000000..3701b42 Binary files /dev/null and b/resource/ship-library/assets/portraits/286.webp differ diff --git a/resource/ship-library/assets/portraits/287.webp b/resource/ship-library/assets/portraits/287.webp new file mode 100644 index 0000000..b6aaca4 Binary files /dev/null and b/resource/ship-library/assets/portraits/287.webp differ diff --git a/resource/ship-library/assets/portraits/288.webp b/resource/ship-library/assets/portraits/288.webp new file mode 100644 index 0000000..f4b18a6 Binary files /dev/null and b/resource/ship-library/assets/portraits/288.webp differ diff --git a/resource/ship-library/assets/portraits/289.webp b/resource/ship-library/assets/portraits/289.webp new file mode 100644 index 0000000..e602817 Binary files /dev/null and b/resource/ship-library/assets/portraits/289.webp differ diff --git a/resource/ship-library/assets/portraits/29.webp b/resource/ship-library/assets/portraits/29.webp new file mode 100644 index 0000000..a5f12f3 Binary files /dev/null and b/resource/ship-library/assets/portraits/29.webp differ diff --git a/resource/ship-library/assets/portraits/290.webp b/resource/ship-library/assets/portraits/290.webp new file mode 100644 index 0000000..123e610 Binary files /dev/null and b/resource/ship-library/assets/portraits/290.webp differ diff --git a/resource/ship-library/assets/portraits/291.webp b/resource/ship-library/assets/portraits/291.webp new file mode 100644 index 0000000..599d175 Binary files /dev/null and b/resource/ship-library/assets/portraits/291.webp differ diff --git a/resource/ship-library/assets/portraits/292.webp b/resource/ship-library/assets/portraits/292.webp new file mode 100644 index 0000000..ae260d0 Binary files /dev/null and b/resource/ship-library/assets/portraits/292.webp differ diff --git a/resource/ship-library/assets/portraits/293.webp b/resource/ship-library/assets/portraits/293.webp new file mode 100644 index 0000000..671e571 Binary files /dev/null and b/resource/ship-library/assets/portraits/293.webp differ diff --git a/resource/ship-library/assets/portraits/294.webp b/resource/ship-library/assets/portraits/294.webp new file mode 100644 index 0000000..6ac942f Binary files /dev/null and b/resource/ship-library/assets/portraits/294.webp differ diff --git a/resource/ship-library/assets/portraits/295.webp b/resource/ship-library/assets/portraits/295.webp new file mode 100644 index 0000000..b575e42 Binary files /dev/null and b/resource/ship-library/assets/portraits/295.webp differ diff --git a/resource/ship-library/assets/portraits/296.webp b/resource/ship-library/assets/portraits/296.webp new file mode 100644 index 0000000..15ee8e7 Binary files /dev/null and b/resource/ship-library/assets/portraits/296.webp differ diff --git a/resource/ship-library/assets/portraits/297.webp b/resource/ship-library/assets/portraits/297.webp new file mode 100644 index 0000000..db06e38 Binary files /dev/null and b/resource/ship-library/assets/portraits/297.webp differ diff --git a/resource/ship-library/assets/portraits/298.webp b/resource/ship-library/assets/portraits/298.webp new file mode 100644 index 0000000..1506884 Binary files /dev/null and b/resource/ship-library/assets/portraits/298.webp differ diff --git a/resource/ship-library/assets/portraits/299.webp b/resource/ship-library/assets/portraits/299.webp new file mode 100644 index 0000000..66a9715 Binary files /dev/null and b/resource/ship-library/assets/portraits/299.webp differ diff --git a/resource/ship-library/assets/portraits/3.webp b/resource/ship-library/assets/portraits/3.webp new file mode 100644 index 0000000..6420753 Binary files /dev/null and b/resource/ship-library/assets/portraits/3.webp differ diff --git a/resource/ship-library/assets/portraits/30.webp b/resource/ship-library/assets/portraits/30.webp new file mode 100644 index 0000000..7e8fb20 Binary files /dev/null and b/resource/ship-library/assets/portraits/30.webp differ diff --git a/resource/ship-library/assets/portraits/300.webp b/resource/ship-library/assets/portraits/300.webp new file mode 100644 index 0000000..e506d29 Binary files /dev/null and b/resource/ship-library/assets/portraits/300.webp differ diff --git a/resource/ship-library/assets/portraits/301.webp b/resource/ship-library/assets/portraits/301.webp new file mode 100644 index 0000000..40ec21b Binary files /dev/null and b/resource/ship-library/assets/portraits/301.webp differ diff --git a/resource/ship-library/assets/portraits/302.webp b/resource/ship-library/assets/portraits/302.webp new file mode 100644 index 0000000..1ec21ba Binary files /dev/null and b/resource/ship-library/assets/portraits/302.webp differ diff --git a/resource/ship-library/assets/portraits/303.webp b/resource/ship-library/assets/portraits/303.webp new file mode 100644 index 0000000..4a5de20 Binary files /dev/null and b/resource/ship-library/assets/portraits/303.webp differ diff --git a/resource/ship-library/assets/portraits/304.webp b/resource/ship-library/assets/portraits/304.webp new file mode 100644 index 0000000..8fabba5 Binary files /dev/null and b/resource/ship-library/assets/portraits/304.webp differ diff --git a/resource/ship-library/assets/portraits/305.webp b/resource/ship-library/assets/portraits/305.webp new file mode 100644 index 0000000..72491c4 Binary files /dev/null and b/resource/ship-library/assets/portraits/305.webp differ diff --git a/resource/ship-library/assets/portraits/306.webp b/resource/ship-library/assets/portraits/306.webp new file mode 100644 index 0000000..5f332dc Binary files /dev/null and b/resource/ship-library/assets/portraits/306.webp differ diff --git a/resource/ship-library/assets/portraits/307.webp b/resource/ship-library/assets/portraits/307.webp new file mode 100644 index 0000000..42a02bc Binary files /dev/null and b/resource/ship-library/assets/portraits/307.webp differ diff --git a/resource/ship-library/assets/portraits/308.webp b/resource/ship-library/assets/portraits/308.webp new file mode 100644 index 0000000..b2d4e28 Binary files /dev/null and b/resource/ship-library/assets/portraits/308.webp differ diff --git a/resource/ship-library/assets/portraits/309.webp b/resource/ship-library/assets/portraits/309.webp new file mode 100644 index 0000000..6068316 Binary files /dev/null and b/resource/ship-library/assets/portraits/309.webp differ diff --git a/resource/ship-library/assets/portraits/31.webp b/resource/ship-library/assets/portraits/31.webp new file mode 100644 index 0000000..4daf467 Binary files /dev/null and b/resource/ship-library/assets/portraits/31.webp differ diff --git a/resource/ship-library/assets/portraits/310.webp b/resource/ship-library/assets/portraits/310.webp new file mode 100644 index 0000000..f9b6d77 Binary files /dev/null and b/resource/ship-library/assets/portraits/310.webp differ diff --git a/resource/ship-library/assets/portraits/311.webp b/resource/ship-library/assets/portraits/311.webp new file mode 100644 index 0000000..22e55da Binary files /dev/null and b/resource/ship-library/assets/portraits/311.webp differ diff --git a/resource/ship-library/assets/portraits/312.webp b/resource/ship-library/assets/portraits/312.webp new file mode 100644 index 0000000..1562a76 Binary files /dev/null and b/resource/ship-library/assets/portraits/312.webp differ diff --git a/resource/ship-library/assets/portraits/313.webp b/resource/ship-library/assets/portraits/313.webp new file mode 100644 index 0000000..393beeb Binary files /dev/null and b/resource/ship-library/assets/portraits/313.webp differ diff --git a/resource/ship-library/assets/portraits/314.webp b/resource/ship-library/assets/portraits/314.webp new file mode 100644 index 0000000..9dedb54 Binary files /dev/null and b/resource/ship-library/assets/portraits/314.webp differ diff --git a/resource/ship-library/assets/portraits/315.webp b/resource/ship-library/assets/portraits/315.webp new file mode 100644 index 0000000..f8c29aa Binary files /dev/null and b/resource/ship-library/assets/portraits/315.webp differ diff --git a/resource/ship-library/assets/portraits/316.webp b/resource/ship-library/assets/portraits/316.webp new file mode 100644 index 0000000..4ec7d12 Binary files /dev/null and b/resource/ship-library/assets/portraits/316.webp differ diff --git a/resource/ship-library/assets/portraits/317.webp b/resource/ship-library/assets/portraits/317.webp new file mode 100644 index 0000000..9f05ef9 Binary files /dev/null and b/resource/ship-library/assets/portraits/317.webp differ diff --git a/resource/ship-library/assets/portraits/318.webp b/resource/ship-library/assets/portraits/318.webp new file mode 100644 index 0000000..ac954ac Binary files /dev/null and b/resource/ship-library/assets/portraits/318.webp differ diff --git a/resource/ship-library/assets/portraits/319.webp b/resource/ship-library/assets/portraits/319.webp new file mode 100644 index 0000000..76bf86c Binary files /dev/null and b/resource/ship-library/assets/portraits/319.webp differ diff --git a/resource/ship-library/assets/portraits/32.webp b/resource/ship-library/assets/portraits/32.webp new file mode 100644 index 0000000..68d71de Binary files /dev/null and b/resource/ship-library/assets/portraits/32.webp differ diff --git a/resource/ship-library/assets/portraits/320.webp b/resource/ship-library/assets/portraits/320.webp new file mode 100644 index 0000000..4dcbb05 Binary files /dev/null and b/resource/ship-library/assets/portraits/320.webp differ diff --git a/resource/ship-library/assets/portraits/321.webp b/resource/ship-library/assets/portraits/321.webp new file mode 100644 index 0000000..4388547 Binary files /dev/null and b/resource/ship-library/assets/portraits/321.webp differ diff --git a/resource/ship-library/assets/portraits/322.webp b/resource/ship-library/assets/portraits/322.webp new file mode 100644 index 0000000..1825b12 Binary files /dev/null and b/resource/ship-library/assets/portraits/322.webp differ diff --git a/resource/ship-library/assets/portraits/323.webp b/resource/ship-library/assets/portraits/323.webp new file mode 100644 index 0000000..4bf920a Binary files /dev/null and b/resource/ship-library/assets/portraits/323.webp differ diff --git a/resource/ship-library/assets/portraits/324.webp b/resource/ship-library/assets/portraits/324.webp new file mode 100644 index 0000000..4d2a8b8 Binary files /dev/null and b/resource/ship-library/assets/portraits/324.webp differ diff --git a/resource/ship-library/assets/portraits/325.webp b/resource/ship-library/assets/portraits/325.webp new file mode 100644 index 0000000..48d9c35 Binary files /dev/null and b/resource/ship-library/assets/portraits/325.webp differ diff --git a/resource/ship-library/assets/portraits/326.webp b/resource/ship-library/assets/portraits/326.webp new file mode 100644 index 0000000..b746ee6 Binary files /dev/null and b/resource/ship-library/assets/portraits/326.webp differ diff --git a/resource/ship-library/assets/portraits/327.webp b/resource/ship-library/assets/portraits/327.webp new file mode 100644 index 0000000..5a7688d Binary files /dev/null and b/resource/ship-library/assets/portraits/327.webp differ diff --git a/resource/ship-library/assets/portraits/328.webp b/resource/ship-library/assets/portraits/328.webp new file mode 100644 index 0000000..c83b192 Binary files /dev/null and b/resource/ship-library/assets/portraits/328.webp differ diff --git a/resource/ship-library/assets/portraits/329.webp b/resource/ship-library/assets/portraits/329.webp new file mode 100644 index 0000000..914fc57 Binary files /dev/null and b/resource/ship-library/assets/portraits/329.webp differ diff --git a/resource/ship-library/assets/portraits/33.webp b/resource/ship-library/assets/portraits/33.webp new file mode 100644 index 0000000..f1f2ad5 Binary files /dev/null and b/resource/ship-library/assets/portraits/33.webp differ diff --git a/resource/ship-library/assets/portraits/330.webp b/resource/ship-library/assets/portraits/330.webp new file mode 100644 index 0000000..34929e4 Binary files /dev/null and b/resource/ship-library/assets/portraits/330.webp differ diff --git a/resource/ship-library/assets/portraits/331.webp b/resource/ship-library/assets/portraits/331.webp new file mode 100644 index 0000000..29f61e6 Binary files /dev/null and b/resource/ship-library/assets/portraits/331.webp differ diff --git a/resource/ship-library/assets/portraits/332.webp b/resource/ship-library/assets/portraits/332.webp new file mode 100644 index 0000000..cb3fb4e Binary files /dev/null and b/resource/ship-library/assets/portraits/332.webp differ diff --git a/resource/ship-library/assets/portraits/333.webp b/resource/ship-library/assets/portraits/333.webp new file mode 100644 index 0000000..649669e Binary files /dev/null and b/resource/ship-library/assets/portraits/333.webp differ diff --git a/resource/ship-library/assets/portraits/334.webp b/resource/ship-library/assets/portraits/334.webp new file mode 100644 index 0000000..1af11bf Binary files /dev/null and b/resource/ship-library/assets/portraits/334.webp differ diff --git a/resource/ship-library/assets/portraits/335.webp b/resource/ship-library/assets/portraits/335.webp new file mode 100644 index 0000000..f9bb5dd Binary files /dev/null and b/resource/ship-library/assets/portraits/335.webp differ diff --git a/resource/ship-library/assets/portraits/336.webp b/resource/ship-library/assets/portraits/336.webp new file mode 100644 index 0000000..3c30953 Binary files /dev/null and b/resource/ship-library/assets/portraits/336.webp differ diff --git a/resource/ship-library/assets/portraits/337.webp b/resource/ship-library/assets/portraits/337.webp new file mode 100644 index 0000000..8f295d5 Binary files /dev/null and b/resource/ship-library/assets/portraits/337.webp differ diff --git a/resource/ship-library/assets/portraits/338.webp b/resource/ship-library/assets/portraits/338.webp new file mode 100644 index 0000000..eaed6d0 Binary files /dev/null and b/resource/ship-library/assets/portraits/338.webp differ diff --git a/resource/ship-library/assets/portraits/339.webp b/resource/ship-library/assets/portraits/339.webp new file mode 100644 index 0000000..8ad8d8b Binary files /dev/null and b/resource/ship-library/assets/portraits/339.webp differ diff --git a/resource/ship-library/assets/portraits/34.webp b/resource/ship-library/assets/portraits/34.webp new file mode 100644 index 0000000..e2469a1 Binary files /dev/null and b/resource/ship-library/assets/portraits/34.webp differ diff --git a/resource/ship-library/assets/portraits/340.webp b/resource/ship-library/assets/portraits/340.webp new file mode 100644 index 0000000..4e39480 Binary files /dev/null and b/resource/ship-library/assets/portraits/340.webp differ diff --git a/resource/ship-library/assets/portraits/341.webp b/resource/ship-library/assets/portraits/341.webp new file mode 100644 index 0000000..5e3fa0e Binary files /dev/null and b/resource/ship-library/assets/portraits/341.webp differ diff --git a/resource/ship-library/assets/portraits/342.webp b/resource/ship-library/assets/portraits/342.webp new file mode 100644 index 0000000..8770a9d Binary files /dev/null and b/resource/ship-library/assets/portraits/342.webp differ diff --git a/resource/ship-library/assets/portraits/343.webp b/resource/ship-library/assets/portraits/343.webp new file mode 100644 index 0000000..5a124a5 Binary files /dev/null and b/resource/ship-library/assets/portraits/343.webp differ diff --git a/resource/ship-library/assets/portraits/344.webp b/resource/ship-library/assets/portraits/344.webp new file mode 100644 index 0000000..0a17735 Binary files /dev/null and b/resource/ship-library/assets/portraits/344.webp differ diff --git a/resource/ship-library/assets/portraits/345.webp b/resource/ship-library/assets/portraits/345.webp new file mode 100644 index 0000000..f938be2 Binary files /dev/null and b/resource/ship-library/assets/portraits/345.webp differ diff --git a/resource/ship-library/assets/portraits/346.webp b/resource/ship-library/assets/portraits/346.webp new file mode 100644 index 0000000..fe55305 Binary files /dev/null and b/resource/ship-library/assets/portraits/346.webp differ diff --git a/resource/ship-library/assets/portraits/347.webp b/resource/ship-library/assets/portraits/347.webp new file mode 100644 index 0000000..d73dad1 Binary files /dev/null and b/resource/ship-library/assets/portraits/347.webp differ diff --git a/resource/ship-library/assets/portraits/348.webp b/resource/ship-library/assets/portraits/348.webp new file mode 100644 index 0000000..243a742 Binary files /dev/null and b/resource/ship-library/assets/portraits/348.webp differ diff --git a/resource/ship-library/assets/portraits/349.webp b/resource/ship-library/assets/portraits/349.webp new file mode 100644 index 0000000..f166ce7 Binary files /dev/null and b/resource/ship-library/assets/portraits/349.webp differ diff --git a/resource/ship-library/assets/portraits/35.webp b/resource/ship-library/assets/portraits/35.webp new file mode 100644 index 0000000..80b252b Binary files /dev/null and b/resource/ship-library/assets/portraits/35.webp differ diff --git a/resource/ship-library/assets/portraits/350.webp b/resource/ship-library/assets/portraits/350.webp new file mode 100644 index 0000000..51d4772 Binary files /dev/null and b/resource/ship-library/assets/portraits/350.webp differ diff --git a/resource/ship-library/assets/portraits/351.webp b/resource/ship-library/assets/portraits/351.webp new file mode 100644 index 0000000..e2f81a2 Binary files /dev/null and b/resource/ship-library/assets/portraits/351.webp differ diff --git a/resource/ship-library/assets/portraits/352.webp b/resource/ship-library/assets/portraits/352.webp new file mode 100644 index 0000000..d58c462 Binary files /dev/null and b/resource/ship-library/assets/portraits/352.webp differ diff --git a/resource/ship-library/assets/portraits/353.webp b/resource/ship-library/assets/portraits/353.webp new file mode 100644 index 0000000..daa6f93 Binary files /dev/null and b/resource/ship-library/assets/portraits/353.webp differ diff --git a/resource/ship-library/assets/portraits/354.webp b/resource/ship-library/assets/portraits/354.webp new file mode 100644 index 0000000..eb4ae85 Binary files /dev/null and b/resource/ship-library/assets/portraits/354.webp differ diff --git a/resource/ship-library/assets/portraits/355.webp b/resource/ship-library/assets/portraits/355.webp new file mode 100644 index 0000000..6e0cc21 Binary files /dev/null and b/resource/ship-library/assets/portraits/355.webp differ diff --git a/resource/ship-library/assets/portraits/356.webp b/resource/ship-library/assets/portraits/356.webp new file mode 100644 index 0000000..d20952d Binary files /dev/null and b/resource/ship-library/assets/portraits/356.webp differ diff --git a/resource/ship-library/assets/portraits/357.webp b/resource/ship-library/assets/portraits/357.webp new file mode 100644 index 0000000..8ebf7ef Binary files /dev/null and b/resource/ship-library/assets/portraits/357.webp differ diff --git a/resource/ship-library/assets/portraits/358.webp b/resource/ship-library/assets/portraits/358.webp new file mode 100644 index 0000000..880dae4 Binary files /dev/null and b/resource/ship-library/assets/portraits/358.webp differ diff --git a/resource/ship-library/assets/portraits/359.webp b/resource/ship-library/assets/portraits/359.webp new file mode 100644 index 0000000..8ff75bc Binary files /dev/null and b/resource/ship-library/assets/portraits/359.webp differ diff --git a/resource/ship-library/assets/portraits/36.webp b/resource/ship-library/assets/portraits/36.webp new file mode 100644 index 0000000..67a1546 Binary files /dev/null and b/resource/ship-library/assets/portraits/36.webp differ diff --git a/resource/ship-library/assets/portraits/360.webp b/resource/ship-library/assets/portraits/360.webp new file mode 100644 index 0000000..ac481ef Binary files /dev/null and b/resource/ship-library/assets/portraits/360.webp differ diff --git a/resource/ship-library/assets/portraits/361.webp b/resource/ship-library/assets/portraits/361.webp new file mode 100644 index 0000000..bdc0a78 Binary files /dev/null and b/resource/ship-library/assets/portraits/361.webp differ diff --git a/resource/ship-library/assets/portraits/362.webp b/resource/ship-library/assets/portraits/362.webp new file mode 100644 index 0000000..dff2592 Binary files /dev/null and b/resource/ship-library/assets/portraits/362.webp differ diff --git a/resource/ship-library/assets/portraits/363.webp b/resource/ship-library/assets/portraits/363.webp new file mode 100644 index 0000000..b2c7881 Binary files /dev/null and b/resource/ship-library/assets/portraits/363.webp differ diff --git a/resource/ship-library/assets/portraits/364.webp b/resource/ship-library/assets/portraits/364.webp new file mode 100644 index 0000000..1c4ff22 Binary files /dev/null and b/resource/ship-library/assets/portraits/364.webp differ diff --git a/resource/ship-library/assets/portraits/365.webp b/resource/ship-library/assets/portraits/365.webp new file mode 100644 index 0000000..26339aa Binary files /dev/null and b/resource/ship-library/assets/portraits/365.webp differ diff --git a/resource/ship-library/assets/portraits/366.webp b/resource/ship-library/assets/portraits/366.webp new file mode 100644 index 0000000..5c1b5f6 Binary files /dev/null and b/resource/ship-library/assets/portraits/366.webp differ diff --git a/resource/ship-library/assets/portraits/367.webp b/resource/ship-library/assets/portraits/367.webp new file mode 100644 index 0000000..926c1ea Binary files /dev/null and b/resource/ship-library/assets/portraits/367.webp differ diff --git a/resource/ship-library/assets/portraits/368.webp b/resource/ship-library/assets/portraits/368.webp new file mode 100644 index 0000000..68733cc Binary files /dev/null and b/resource/ship-library/assets/portraits/368.webp differ diff --git a/resource/ship-library/assets/portraits/369.webp b/resource/ship-library/assets/portraits/369.webp new file mode 100644 index 0000000..ca07808 Binary files /dev/null and b/resource/ship-library/assets/portraits/369.webp differ diff --git a/resource/ship-library/assets/portraits/37.webp b/resource/ship-library/assets/portraits/37.webp new file mode 100644 index 0000000..1de4e0b Binary files /dev/null and b/resource/ship-library/assets/portraits/37.webp differ diff --git a/resource/ship-library/assets/portraits/370.webp b/resource/ship-library/assets/portraits/370.webp new file mode 100644 index 0000000..fc57c25 Binary files /dev/null and b/resource/ship-library/assets/portraits/370.webp differ diff --git a/resource/ship-library/assets/portraits/371.webp b/resource/ship-library/assets/portraits/371.webp new file mode 100644 index 0000000..199f483 Binary files /dev/null and b/resource/ship-library/assets/portraits/371.webp differ diff --git a/resource/ship-library/assets/portraits/372.webp b/resource/ship-library/assets/portraits/372.webp new file mode 100644 index 0000000..50d46c5 Binary files /dev/null and b/resource/ship-library/assets/portraits/372.webp differ diff --git a/resource/ship-library/assets/portraits/373.webp b/resource/ship-library/assets/portraits/373.webp new file mode 100644 index 0000000..51fe32a Binary files /dev/null and b/resource/ship-library/assets/portraits/373.webp differ diff --git a/resource/ship-library/assets/portraits/374.webp b/resource/ship-library/assets/portraits/374.webp new file mode 100644 index 0000000..647e476 Binary files /dev/null and b/resource/ship-library/assets/portraits/374.webp differ diff --git a/resource/ship-library/assets/portraits/375.webp b/resource/ship-library/assets/portraits/375.webp new file mode 100644 index 0000000..91b6863 Binary files /dev/null and b/resource/ship-library/assets/portraits/375.webp differ diff --git a/resource/ship-library/assets/portraits/376.webp b/resource/ship-library/assets/portraits/376.webp new file mode 100644 index 0000000..8aa0fe7 Binary files /dev/null and b/resource/ship-library/assets/portraits/376.webp differ diff --git a/resource/ship-library/assets/portraits/377.webp b/resource/ship-library/assets/portraits/377.webp new file mode 100644 index 0000000..2e4a606 Binary files /dev/null and b/resource/ship-library/assets/portraits/377.webp differ diff --git a/resource/ship-library/assets/portraits/378.webp b/resource/ship-library/assets/portraits/378.webp new file mode 100644 index 0000000..3482f65 Binary files /dev/null and b/resource/ship-library/assets/portraits/378.webp differ diff --git a/resource/ship-library/assets/portraits/379.webp b/resource/ship-library/assets/portraits/379.webp new file mode 100644 index 0000000..0044b65 Binary files /dev/null and b/resource/ship-library/assets/portraits/379.webp differ diff --git a/resource/ship-library/assets/portraits/38.webp b/resource/ship-library/assets/portraits/38.webp new file mode 100644 index 0000000..3a21a51 Binary files /dev/null and b/resource/ship-library/assets/portraits/38.webp differ diff --git a/resource/ship-library/assets/portraits/380.webp b/resource/ship-library/assets/portraits/380.webp new file mode 100644 index 0000000..16cea47 Binary files /dev/null and b/resource/ship-library/assets/portraits/380.webp differ diff --git a/resource/ship-library/assets/portraits/381.webp b/resource/ship-library/assets/portraits/381.webp new file mode 100644 index 0000000..23fda80 Binary files /dev/null and b/resource/ship-library/assets/portraits/381.webp differ diff --git a/resource/ship-library/assets/portraits/382.webp b/resource/ship-library/assets/portraits/382.webp new file mode 100644 index 0000000..e3b7390 Binary files /dev/null and b/resource/ship-library/assets/portraits/382.webp differ diff --git a/resource/ship-library/assets/portraits/383.webp b/resource/ship-library/assets/portraits/383.webp new file mode 100644 index 0000000..c303e91 Binary files /dev/null and b/resource/ship-library/assets/portraits/383.webp differ diff --git a/resource/ship-library/assets/portraits/384.webp b/resource/ship-library/assets/portraits/384.webp new file mode 100644 index 0000000..f58cb07 Binary files /dev/null and b/resource/ship-library/assets/portraits/384.webp differ diff --git a/resource/ship-library/assets/portraits/385.webp b/resource/ship-library/assets/portraits/385.webp new file mode 100644 index 0000000..5448258 Binary files /dev/null and b/resource/ship-library/assets/portraits/385.webp differ diff --git a/resource/ship-library/assets/portraits/386.webp b/resource/ship-library/assets/portraits/386.webp new file mode 100644 index 0000000..25a3a2f Binary files /dev/null and b/resource/ship-library/assets/portraits/386.webp differ diff --git a/resource/ship-library/assets/portraits/387.webp b/resource/ship-library/assets/portraits/387.webp new file mode 100644 index 0000000..ac2b850 Binary files /dev/null and b/resource/ship-library/assets/portraits/387.webp differ diff --git a/resource/ship-library/assets/portraits/388.webp b/resource/ship-library/assets/portraits/388.webp new file mode 100644 index 0000000..cbd392a Binary files /dev/null and b/resource/ship-library/assets/portraits/388.webp differ diff --git a/resource/ship-library/assets/portraits/389.webp b/resource/ship-library/assets/portraits/389.webp new file mode 100644 index 0000000..4b12039 Binary files /dev/null and b/resource/ship-library/assets/portraits/389.webp differ diff --git a/resource/ship-library/assets/portraits/39.webp b/resource/ship-library/assets/portraits/39.webp new file mode 100644 index 0000000..008589f Binary files /dev/null and b/resource/ship-library/assets/portraits/39.webp differ diff --git a/resource/ship-library/assets/portraits/390.webp b/resource/ship-library/assets/portraits/390.webp new file mode 100644 index 0000000..7a7b3c4 Binary files /dev/null and b/resource/ship-library/assets/portraits/390.webp differ diff --git a/resource/ship-library/assets/portraits/391.webp b/resource/ship-library/assets/portraits/391.webp new file mode 100644 index 0000000..c74d8a2 Binary files /dev/null and b/resource/ship-library/assets/portraits/391.webp differ diff --git a/resource/ship-library/assets/portraits/392.webp b/resource/ship-library/assets/portraits/392.webp new file mode 100644 index 0000000..2c3204b Binary files /dev/null and b/resource/ship-library/assets/portraits/392.webp differ diff --git a/resource/ship-library/assets/portraits/393.webp b/resource/ship-library/assets/portraits/393.webp new file mode 100644 index 0000000..627b66b Binary files /dev/null and b/resource/ship-library/assets/portraits/393.webp differ diff --git a/resource/ship-library/assets/portraits/394.webp b/resource/ship-library/assets/portraits/394.webp new file mode 100644 index 0000000..7b0b8da Binary files /dev/null and b/resource/ship-library/assets/portraits/394.webp differ diff --git a/resource/ship-library/assets/portraits/395.webp b/resource/ship-library/assets/portraits/395.webp new file mode 100644 index 0000000..984d7c1 Binary files /dev/null and b/resource/ship-library/assets/portraits/395.webp differ diff --git a/resource/ship-library/assets/portraits/396.webp b/resource/ship-library/assets/portraits/396.webp new file mode 100644 index 0000000..a2adec1 Binary files /dev/null and b/resource/ship-library/assets/portraits/396.webp differ diff --git a/resource/ship-library/assets/portraits/397.webp b/resource/ship-library/assets/portraits/397.webp new file mode 100644 index 0000000..4824f79 Binary files /dev/null and b/resource/ship-library/assets/portraits/397.webp differ diff --git a/resource/ship-library/assets/portraits/398.webp b/resource/ship-library/assets/portraits/398.webp new file mode 100644 index 0000000..2c69f62 Binary files /dev/null and b/resource/ship-library/assets/portraits/398.webp differ diff --git a/resource/ship-library/assets/portraits/399.webp b/resource/ship-library/assets/portraits/399.webp new file mode 100644 index 0000000..912c1bf Binary files /dev/null and b/resource/ship-library/assets/portraits/399.webp differ diff --git a/resource/ship-library/assets/portraits/4.webp b/resource/ship-library/assets/portraits/4.webp new file mode 100644 index 0000000..8e706f1 Binary files /dev/null and b/resource/ship-library/assets/portraits/4.webp differ diff --git a/resource/ship-library/assets/portraits/40.webp b/resource/ship-library/assets/portraits/40.webp new file mode 100644 index 0000000..f186138 Binary files /dev/null and b/resource/ship-library/assets/portraits/40.webp differ diff --git a/resource/ship-library/assets/portraits/400.webp b/resource/ship-library/assets/portraits/400.webp new file mode 100644 index 0000000..69ad733 Binary files /dev/null and b/resource/ship-library/assets/portraits/400.webp differ diff --git a/resource/ship-library/assets/portraits/401.webp b/resource/ship-library/assets/portraits/401.webp new file mode 100644 index 0000000..8c69d1c Binary files /dev/null and b/resource/ship-library/assets/portraits/401.webp differ diff --git a/resource/ship-library/assets/portraits/402.webp b/resource/ship-library/assets/portraits/402.webp new file mode 100644 index 0000000..b046e0d Binary files /dev/null and b/resource/ship-library/assets/portraits/402.webp differ diff --git a/resource/ship-library/assets/portraits/403.webp b/resource/ship-library/assets/portraits/403.webp new file mode 100644 index 0000000..6ad4af7 Binary files /dev/null and b/resource/ship-library/assets/portraits/403.webp differ diff --git a/resource/ship-library/assets/portraits/404.webp b/resource/ship-library/assets/portraits/404.webp new file mode 100644 index 0000000..89746e0 Binary files /dev/null and b/resource/ship-library/assets/portraits/404.webp differ diff --git a/resource/ship-library/assets/portraits/405.webp b/resource/ship-library/assets/portraits/405.webp new file mode 100644 index 0000000..22dc5bd Binary files /dev/null and b/resource/ship-library/assets/portraits/405.webp differ diff --git a/resource/ship-library/assets/portraits/406.webp b/resource/ship-library/assets/portraits/406.webp new file mode 100644 index 0000000..eb1e289 Binary files /dev/null and b/resource/ship-library/assets/portraits/406.webp differ diff --git a/resource/ship-library/assets/portraits/407.webp b/resource/ship-library/assets/portraits/407.webp new file mode 100644 index 0000000..fe31f3f Binary files /dev/null and b/resource/ship-library/assets/portraits/407.webp differ diff --git a/resource/ship-library/assets/portraits/408.webp b/resource/ship-library/assets/portraits/408.webp new file mode 100644 index 0000000..1c1f73b Binary files /dev/null and b/resource/ship-library/assets/portraits/408.webp differ diff --git a/resource/ship-library/assets/portraits/409.webp b/resource/ship-library/assets/portraits/409.webp new file mode 100644 index 0000000..72aa536 Binary files /dev/null and b/resource/ship-library/assets/portraits/409.webp differ diff --git a/resource/ship-library/assets/portraits/41.webp b/resource/ship-library/assets/portraits/41.webp new file mode 100644 index 0000000..9a8ecea Binary files /dev/null and b/resource/ship-library/assets/portraits/41.webp differ diff --git a/resource/ship-library/assets/portraits/410.webp b/resource/ship-library/assets/portraits/410.webp new file mode 100644 index 0000000..a703a85 Binary files /dev/null and b/resource/ship-library/assets/portraits/410.webp differ diff --git a/resource/ship-library/assets/portraits/411.webp b/resource/ship-library/assets/portraits/411.webp new file mode 100644 index 0000000..7ffcbb2 Binary files /dev/null and b/resource/ship-library/assets/portraits/411.webp differ diff --git a/resource/ship-library/assets/portraits/412.webp b/resource/ship-library/assets/portraits/412.webp new file mode 100644 index 0000000..505c3a0 Binary files /dev/null and b/resource/ship-library/assets/portraits/412.webp differ diff --git a/resource/ship-library/assets/portraits/413.webp b/resource/ship-library/assets/portraits/413.webp new file mode 100644 index 0000000..d4f903e Binary files /dev/null and b/resource/ship-library/assets/portraits/413.webp differ diff --git a/resource/ship-library/assets/portraits/414.webp b/resource/ship-library/assets/portraits/414.webp new file mode 100644 index 0000000..a01dfe8 Binary files /dev/null and b/resource/ship-library/assets/portraits/414.webp differ diff --git a/resource/ship-library/assets/portraits/415.webp b/resource/ship-library/assets/portraits/415.webp new file mode 100644 index 0000000..70066a2 Binary files /dev/null and b/resource/ship-library/assets/portraits/415.webp differ diff --git a/resource/ship-library/assets/portraits/416.webp b/resource/ship-library/assets/portraits/416.webp new file mode 100644 index 0000000..d321480 Binary files /dev/null and b/resource/ship-library/assets/portraits/416.webp differ diff --git a/resource/ship-library/assets/portraits/417.webp b/resource/ship-library/assets/portraits/417.webp new file mode 100644 index 0000000..7390faf Binary files /dev/null and b/resource/ship-library/assets/portraits/417.webp differ diff --git a/resource/ship-library/assets/portraits/418.webp b/resource/ship-library/assets/portraits/418.webp new file mode 100644 index 0000000..ce915f5 Binary files /dev/null and b/resource/ship-library/assets/portraits/418.webp differ diff --git a/resource/ship-library/assets/portraits/419.webp b/resource/ship-library/assets/portraits/419.webp new file mode 100644 index 0000000..0f332ff Binary files /dev/null and b/resource/ship-library/assets/portraits/419.webp differ diff --git a/resource/ship-library/assets/portraits/42.webp b/resource/ship-library/assets/portraits/42.webp new file mode 100644 index 0000000..6136df0 Binary files /dev/null and b/resource/ship-library/assets/portraits/42.webp differ diff --git a/resource/ship-library/assets/portraits/420.webp b/resource/ship-library/assets/portraits/420.webp new file mode 100644 index 0000000..907fde9 Binary files /dev/null and b/resource/ship-library/assets/portraits/420.webp differ diff --git a/resource/ship-library/assets/portraits/421.webp b/resource/ship-library/assets/portraits/421.webp new file mode 100644 index 0000000..751f6c6 Binary files /dev/null and b/resource/ship-library/assets/portraits/421.webp differ diff --git a/resource/ship-library/assets/portraits/422.webp b/resource/ship-library/assets/portraits/422.webp new file mode 100644 index 0000000..923d441 Binary files /dev/null and b/resource/ship-library/assets/portraits/422.webp differ diff --git a/resource/ship-library/assets/portraits/423.webp b/resource/ship-library/assets/portraits/423.webp new file mode 100644 index 0000000..3f0a182 Binary files /dev/null and b/resource/ship-library/assets/portraits/423.webp differ diff --git a/resource/ship-library/assets/portraits/424.webp b/resource/ship-library/assets/portraits/424.webp new file mode 100644 index 0000000..44b0d50 Binary files /dev/null and b/resource/ship-library/assets/portraits/424.webp differ diff --git a/resource/ship-library/assets/portraits/425.webp b/resource/ship-library/assets/portraits/425.webp new file mode 100644 index 0000000..7a1c8df Binary files /dev/null and b/resource/ship-library/assets/portraits/425.webp differ diff --git a/resource/ship-library/assets/portraits/426.webp b/resource/ship-library/assets/portraits/426.webp new file mode 100644 index 0000000..ea8ac17 Binary files /dev/null and b/resource/ship-library/assets/portraits/426.webp differ diff --git a/resource/ship-library/assets/portraits/427.webp b/resource/ship-library/assets/portraits/427.webp new file mode 100644 index 0000000..ccbd52a Binary files /dev/null and b/resource/ship-library/assets/portraits/427.webp differ diff --git a/resource/ship-library/assets/portraits/428.webp b/resource/ship-library/assets/portraits/428.webp new file mode 100644 index 0000000..5ca9e36 Binary files /dev/null and b/resource/ship-library/assets/portraits/428.webp differ diff --git a/resource/ship-library/assets/portraits/429.webp b/resource/ship-library/assets/portraits/429.webp new file mode 100644 index 0000000..ff7df57 Binary files /dev/null and b/resource/ship-library/assets/portraits/429.webp differ diff --git a/resource/ship-library/assets/portraits/43.webp b/resource/ship-library/assets/portraits/43.webp new file mode 100644 index 0000000..c118d57 Binary files /dev/null and b/resource/ship-library/assets/portraits/43.webp differ diff --git a/resource/ship-library/assets/portraits/430.webp b/resource/ship-library/assets/portraits/430.webp new file mode 100644 index 0000000..7eb8b21 Binary files /dev/null and b/resource/ship-library/assets/portraits/430.webp differ diff --git a/resource/ship-library/assets/portraits/431.webp b/resource/ship-library/assets/portraits/431.webp new file mode 100644 index 0000000..faf7887 Binary files /dev/null and b/resource/ship-library/assets/portraits/431.webp differ diff --git a/resource/ship-library/assets/portraits/432.webp b/resource/ship-library/assets/portraits/432.webp new file mode 100644 index 0000000..78ad07f Binary files /dev/null and b/resource/ship-library/assets/portraits/432.webp differ diff --git a/resource/ship-library/assets/portraits/433.webp b/resource/ship-library/assets/portraits/433.webp new file mode 100644 index 0000000..60d82b1 Binary files /dev/null and b/resource/ship-library/assets/portraits/433.webp differ diff --git a/resource/ship-library/assets/portraits/434.webp b/resource/ship-library/assets/portraits/434.webp new file mode 100644 index 0000000..c20f2ff Binary files /dev/null and b/resource/ship-library/assets/portraits/434.webp differ diff --git a/resource/ship-library/assets/portraits/435.webp b/resource/ship-library/assets/portraits/435.webp new file mode 100644 index 0000000..79bf048 Binary files /dev/null and b/resource/ship-library/assets/portraits/435.webp differ diff --git a/resource/ship-library/assets/portraits/436.webp b/resource/ship-library/assets/portraits/436.webp new file mode 100644 index 0000000..92afee0 Binary files /dev/null and b/resource/ship-library/assets/portraits/436.webp differ diff --git a/resource/ship-library/assets/portraits/437.webp b/resource/ship-library/assets/portraits/437.webp new file mode 100644 index 0000000..fe8c785 Binary files /dev/null and b/resource/ship-library/assets/portraits/437.webp differ diff --git a/resource/ship-library/assets/portraits/438.webp b/resource/ship-library/assets/portraits/438.webp new file mode 100644 index 0000000..7224c33 Binary files /dev/null and b/resource/ship-library/assets/portraits/438.webp differ diff --git a/resource/ship-library/assets/portraits/439.webp b/resource/ship-library/assets/portraits/439.webp new file mode 100644 index 0000000..74e853b Binary files /dev/null and b/resource/ship-library/assets/portraits/439.webp differ diff --git a/resource/ship-library/assets/portraits/44.webp b/resource/ship-library/assets/portraits/44.webp new file mode 100644 index 0000000..d390c1f Binary files /dev/null and b/resource/ship-library/assets/portraits/44.webp differ diff --git a/resource/ship-library/assets/portraits/440.webp b/resource/ship-library/assets/portraits/440.webp new file mode 100644 index 0000000..980846e Binary files /dev/null and b/resource/ship-library/assets/portraits/440.webp differ diff --git a/resource/ship-library/assets/portraits/441.webp b/resource/ship-library/assets/portraits/441.webp new file mode 100644 index 0000000..c6f7ff9 Binary files /dev/null and b/resource/ship-library/assets/portraits/441.webp differ diff --git a/resource/ship-library/assets/portraits/442.webp b/resource/ship-library/assets/portraits/442.webp new file mode 100644 index 0000000..13bcf0a Binary files /dev/null and b/resource/ship-library/assets/portraits/442.webp differ diff --git a/resource/ship-library/assets/portraits/443.webp b/resource/ship-library/assets/portraits/443.webp new file mode 100644 index 0000000..2efb9cf Binary files /dev/null and b/resource/ship-library/assets/portraits/443.webp differ diff --git a/resource/ship-library/assets/portraits/444.webp b/resource/ship-library/assets/portraits/444.webp new file mode 100644 index 0000000..0ab58b5 Binary files /dev/null and b/resource/ship-library/assets/portraits/444.webp differ diff --git a/resource/ship-library/assets/portraits/445.webp b/resource/ship-library/assets/portraits/445.webp new file mode 100644 index 0000000..0f09ef9 Binary files /dev/null and b/resource/ship-library/assets/portraits/445.webp differ diff --git a/resource/ship-library/assets/portraits/446.webp b/resource/ship-library/assets/portraits/446.webp new file mode 100644 index 0000000..d07b283 Binary files /dev/null and b/resource/ship-library/assets/portraits/446.webp differ diff --git a/resource/ship-library/assets/portraits/447.webp b/resource/ship-library/assets/portraits/447.webp new file mode 100644 index 0000000..a048545 Binary files /dev/null and b/resource/ship-library/assets/portraits/447.webp differ diff --git a/resource/ship-library/assets/portraits/448.webp b/resource/ship-library/assets/portraits/448.webp new file mode 100644 index 0000000..e37f03b Binary files /dev/null and b/resource/ship-library/assets/portraits/448.webp differ diff --git a/resource/ship-library/assets/portraits/449.webp b/resource/ship-library/assets/portraits/449.webp new file mode 100644 index 0000000..ca1333f Binary files /dev/null and b/resource/ship-library/assets/portraits/449.webp differ diff --git a/resource/ship-library/assets/portraits/45.webp b/resource/ship-library/assets/portraits/45.webp new file mode 100644 index 0000000..a2816f2 Binary files /dev/null and b/resource/ship-library/assets/portraits/45.webp differ diff --git a/resource/ship-library/assets/portraits/450.webp b/resource/ship-library/assets/portraits/450.webp new file mode 100644 index 0000000..98d0c97 Binary files /dev/null and b/resource/ship-library/assets/portraits/450.webp differ diff --git a/resource/ship-library/assets/portraits/451.webp b/resource/ship-library/assets/portraits/451.webp new file mode 100644 index 0000000..0d2a717 Binary files /dev/null and b/resource/ship-library/assets/portraits/451.webp differ diff --git a/resource/ship-library/assets/portraits/452.webp b/resource/ship-library/assets/portraits/452.webp new file mode 100644 index 0000000..60307d6 Binary files /dev/null and b/resource/ship-library/assets/portraits/452.webp differ diff --git a/resource/ship-library/assets/portraits/453.webp b/resource/ship-library/assets/portraits/453.webp new file mode 100644 index 0000000..95edf13 Binary files /dev/null and b/resource/ship-library/assets/portraits/453.webp differ diff --git a/resource/ship-library/assets/portraits/454.webp b/resource/ship-library/assets/portraits/454.webp new file mode 100644 index 0000000..45f7ab1 Binary files /dev/null and b/resource/ship-library/assets/portraits/454.webp differ diff --git a/resource/ship-library/assets/portraits/455.webp b/resource/ship-library/assets/portraits/455.webp new file mode 100644 index 0000000..950ab3a Binary files /dev/null and b/resource/ship-library/assets/portraits/455.webp differ diff --git a/resource/ship-library/assets/portraits/456.webp b/resource/ship-library/assets/portraits/456.webp new file mode 100644 index 0000000..9661324 Binary files /dev/null and b/resource/ship-library/assets/portraits/456.webp differ diff --git a/resource/ship-library/assets/portraits/457.webp b/resource/ship-library/assets/portraits/457.webp new file mode 100644 index 0000000..0c1b5bd Binary files /dev/null and b/resource/ship-library/assets/portraits/457.webp differ diff --git a/resource/ship-library/assets/portraits/458.webp b/resource/ship-library/assets/portraits/458.webp new file mode 100644 index 0000000..926a5e6 Binary files /dev/null and b/resource/ship-library/assets/portraits/458.webp differ diff --git a/resource/ship-library/assets/portraits/459.webp b/resource/ship-library/assets/portraits/459.webp new file mode 100644 index 0000000..fd37d53 Binary files /dev/null and b/resource/ship-library/assets/portraits/459.webp differ diff --git a/resource/ship-library/assets/portraits/46.webp b/resource/ship-library/assets/portraits/46.webp new file mode 100644 index 0000000..83a3eb7 Binary files /dev/null and b/resource/ship-library/assets/portraits/46.webp differ diff --git a/resource/ship-library/assets/portraits/460.webp b/resource/ship-library/assets/portraits/460.webp new file mode 100644 index 0000000..6b9f211 Binary files /dev/null and b/resource/ship-library/assets/portraits/460.webp differ diff --git a/resource/ship-library/assets/portraits/461.webp b/resource/ship-library/assets/portraits/461.webp new file mode 100644 index 0000000..fe26c34 Binary files /dev/null and b/resource/ship-library/assets/portraits/461.webp differ diff --git a/resource/ship-library/assets/portraits/462.webp b/resource/ship-library/assets/portraits/462.webp new file mode 100644 index 0000000..4734262 Binary files /dev/null and b/resource/ship-library/assets/portraits/462.webp differ diff --git a/resource/ship-library/assets/portraits/463.webp b/resource/ship-library/assets/portraits/463.webp new file mode 100644 index 0000000..a432d64 Binary files /dev/null and b/resource/ship-library/assets/portraits/463.webp differ diff --git a/resource/ship-library/assets/portraits/464.webp b/resource/ship-library/assets/portraits/464.webp new file mode 100644 index 0000000..7f28269 Binary files /dev/null and b/resource/ship-library/assets/portraits/464.webp differ diff --git a/resource/ship-library/assets/portraits/465.webp b/resource/ship-library/assets/portraits/465.webp new file mode 100644 index 0000000..48bb5c6 Binary files /dev/null and b/resource/ship-library/assets/portraits/465.webp differ diff --git a/resource/ship-library/assets/portraits/466.webp b/resource/ship-library/assets/portraits/466.webp new file mode 100644 index 0000000..d953728 Binary files /dev/null and b/resource/ship-library/assets/portraits/466.webp differ diff --git a/resource/ship-library/assets/portraits/467.webp b/resource/ship-library/assets/portraits/467.webp new file mode 100644 index 0000000..8a43ca3 Binary files /dev/null and b/resource/ship-library/assets/portraits/467.webp differ diff --git a/resource/ship-library/assets/portraits/468.webp b/resource/ship-library/assets/portraits/468.webp new file mode 100644 index 0000000..f007ed5 Binary files /dev/null and b/resource/ship-library/assets/portraits/468.webp differ diff --git a/resource/ship-library/assets/portraits/469.webp b/resource/ship-library/assets/portraits/469.webp new file mode 100644 index 0000000..bdc7cba Binary files /dev/null and b/resource/ship-library/assets/portraits/469.webp differ diff --git a/resource/ship-library/assets/portraits/47.webp b/resource/ship-library/assets/portraits/47.webp new file mode 100644 index 0000000..e252c22 Binary files /dev/null and b/resource/ship-library/assets/portraits/47.webp differ diff --git a/resource/ship-library/assets/portraits/470.webp b/resource/ship-library/assets/portraits/470.webp new file mode 100644 index 0000000..903e8dc Binary files /dev/null and b/resource/ship-library/assets/portraits/470.webp differ diff --git a/resource/ship-library/assets/portraits/471.webp b/resource/ship-library/assets/portraits/471.webp new file mode 100644 index 0000000..51fc030 Binary files /dev/null and b/resource/ship-library/assets/portraits/471.webp differ diff --git a/resource/ship-library/assets/portraits/472.webp b/resource/ship-library/assets/portraits/472.webp new file mode 100644 index 0000000..5c1553d Binary files /dev/null and b/resource/ship-library/assets/portraits/472.webp differ diff --git a/resource/ship-library/assets/portraits/473.webp b/resource/ship-library/assets/portraits/473.webp new file mode 100644 index 0000000..ae9c4b0 Binary files /dev/null and b/resource/ship-library/assets/portraits/473.webp differ diff --git a/resource/ship-library/assets/portraits/474.webp b/resource/ship-library/assets/portraits/474.webp new file mode 100644 index 0000000..4c7d109 Binary files /dev/null and b/resource/ship-library/assets/portraits/474.webp differ diff --git a/resource/ship-library/assets/portraits/475.webp b/resource/ship-library/assets/portraits/475.webp new file mode 100644 index 0000000..8beb7fa Binary files /dev/null and b/resource/ship-library/assets/portraits/475.webp differ diff --git a/resource/ship-library/assets/portraits/476.webp b/resource/ship-library/assets/portraits/476.webp new file mode 100644 index 0000000..c53c8a6 Binary files /dev/null and b/resource/ship-library/assets/portraits/476.webp differ diff --git a/resource/ship-library/assets/portraits/477.webp b/resource/ship-library/assets/portraits/477.webp new file mode 100644 index 0000000..69ce212 Binary files /dev/null and b/resource/ship-library/assets/portraits/477.webp differ diff --git a/resource/ship-library/assets/portraits/478.webp b/resource/ship-library/assets/portraits/478.webp new file mode 100644 index 0000000..449aadd Binary files /dev/null and b/resource/ship-library/assets/portraits/478.webp differ diff --git a/resource/ship-library/assets/portraits/479.webp b/resource/ship-library/assets/portraits/479.webp new file mode 100644 index 0000000..5c1907b Binary files /dev/null and b/resource/ship-library/assets/portraits/479.webp differ diff --git a/resource/ship-library/assets/portraits/48.webp b/resource/ship-library/assets/portraits/48.webp new file mode 100644 index 0000000..d7acb9f Binary files /dev/null and b/resource/ship-library/assets/portraits/48.webp differ diff --git a/resource/ship-library/assets/portraits/480.webp b/resource/ship-library/assets/portraits/480.webp new file mode 100644 index 0000000..a0e71be Binary files /dev/null and b/resource/ship-library/assets/portraits/480.webp differ diff --git a/resource/ship-library/assets/portraits/481.webp b/resource/ship-library/assets/portraits/481.webp new file mode 100644 index 0000000..6ba3d02 Binary files /dev/null and b/resource/ship-library/assets/portraits/481.webp differ diff --git a/resource/ship-library/assets/portraits/482.webp b/resource/ship-library/assets/portraits/482.webp new file mode 100644 index 0000000..1123c3f Binary files /dev/null and b/resource/ship-library/assets/portraits/482.webp differ diff --git a/resource/ship-library/assets/portraits/483.webp b/resource/ship-library/assets/portraits/483.webp new file mode 100644 index 0000000..56b554b Binary files /dev/null and b/resource/ship-library/assets/portraits/483.webp differ diff --git a/resource/ship-library/assets/portraits/484.webp b/resource/ship-library/assets/portraits/484.webp new file mode 100644 index 0000000..962e196 Binary files /dev/null and b/resource/ship-library/assets/portraits/484.webp differ diff --git a/resource/ship-library/assets/portraits/485.webp b/resource/ship-library/assets/portraits/485.webp new file mode 100644 index 0000000..0c4f103 Binary files /dev/null and b/resource/ship-library/assets/portraits/485.webp differ diff --git a/resource/ship-library/assets/portraits/486.webp b/resource/ship-library/assets/portraits/486.webp new file mode 100644 index 0000000..defde16 Binary files /dev/null and b/resource/ship-library/assets/portraits/486.webp differ diff --git a/resource/ship-library/assets/portraits/487.webp b/resource/ship-library/assets/portraits/487.webp new file mode 100644 index 0000000..a8b542c Binary files /dev/null and b/resource/ship-library/assets/portraits/487.webp differ diff --git a/resource/ship-library/assets/portraits/488.webp b/resource/ship-library/assets/portraits/488.webp new file mode 100644 index 0000000..bca44a3 Binary files /dev/null and b/resource/ship-library/assets/portraits/488.webp differ diff --git a/resource/ship-library/assets/portraits/489.webp b/resource/ship-library/assets/portraits/489.webp new file mode 100644 index 0000000..8057125 Binary files /dev/null and b/resource/ship-library/assets/portraits/489.webp differ diff --git a/resource/ship-library/assets/portraits/49.webp b/resource/ship-library/assets/portraits/49.webp new file mode 100644 index 0000000..8a17eac Binary files /dev/null and b/resource/ship-library/assets/portraits/49.webp differ diff --git a/resource/ship-library/assets/portraits/490.webp b/resource/ship-library/assets/portraits/490.webp new file mode 100644 index 0000000..c1f5c28 Binary files /dev/null and b/resource/ship-library/assets/portraits/490.webp differ diff --git a/resource/ship-library/assets/portraits/491.webp b/resource/ship-library/assets/portraits/491.webp new file mode 100644 index 0000000..fd02b5f Binary files /dev/null and b/resource/ship-library/assets/portraits/491.webp differ diff --git a/resource/ship-library/assets/portraits/492.webp b/resource/ship-library/assets/portraits/492.webp new file mode 100644 index 0000000..5a97ae0 Binary files /dev/null and b/resource/ship-library/assets/portraits/492.webp differ diff --git a/resource/ship-library/assets/portraits/493.webp b/resource/ship-library/assets/portraits/493.webp new file mode 100644 index 0000000..fa7539a Binary files /dev/null and b/resource/ship-library/assets/portraits/493.webp differ diff --git a/resource/ship-library/assets/portraits/494.webp b/resource/ship-library/assets/portraits/494.webp new file mode 100644 index 0000000..327210b Binary files /dev/null and b/resource/ship-library/assets/portraits/494.webp differ diff --git a/resource/ship-library/assets/portraits/495.webp b/resource/ship-library/assets/portraits/495.webp new file mode 100644 index 0000000..6e19693 Binary files /dev/null and b/resource/ship-library/assets/portraits/495.webp differ diff --git a/resource/ship-library/assets/portraits/496.webp b/resource/ship-library/assets/portraits/496.webp new file mode 100644 index 0000000..c6a5e6b Binary files /dev/null and b/resource/ship-library/assets/portraits/496.webp differ diff --git a/resource/ship-library/assets/portraits/497.webp b/resource/ship-library/assets/portraits/497.webp new file mode 100644 index 0000000..fba4e01 Binary files /dev/null and b/resource/ship-library/assets/portraits/497.webp differ diff --git a/resource/ship-library/assets/portraits/498.webp b/resource/ship-library/assets/portraits/498.webp new file mode 100644 index 0000000..ed286a5 Binary files /dev/null and b/resource/ship-library/assets/portraits/498.webp differ diff --git a/resource/ship-library/assets/portraits/499.webp b/resource/ship-library/assets/portraits/499.webp new file mode 100644 index 0000000..fb663db Binary files /dev/null and b/resource/ship-library/assets/portraits/499.webp differ diff --git a/resource/ship-library/assets/portraits/5.webp b/resource/ship-library/assets/portraits/5.webp new file mode 100644 index 0000000..3e63e36 Binary files /dev/null and b/resource/ship-library/assets/portraits/5.webp differ diff --git a/resource/ship-library/assets/portraits/50.webp b/resource/ship-library/assets/portraits/50.webp new file mode 100644 index 0000000..dc2a751 Binary files /dev/null and b/resource/ship-library/assets/portraits/50.webp differ diff --git a/resource/ship-library/assets/portraits/500.webp b/resource/ship-library/assets/portraits/500.webp new file mode 100644 index 0000000..4398381 Binary files /dev/null and b/resource/ship-library/assets/portraits/500.webp differ diff --git a/resource/ship-library/assets/portraits/501.webp b/resource/ship-library/assets/portraits/501.webp new file mode 100644 index 0000000..ec24cde Binary files /dev/null and b/resource/ship-library/assets/portraits/501.webp differ diff --git a/resource/ship-library/assets/portraits/502.webp b/resource/ship-library/assets/portraits/502.webp new file mode 100644 index 0000000..63bfde1 Binary files /dev/null and b/resource/ship-library/assets/portraits/502.webp differ diff --git a/resource/ship-library/assets/portraits/503.webp b/resource/ship-library/assets/portraits/503.webp new file mode 100644 index 0000000..19cd03f Binary files /dev/null and b/resource/ship-library/assets/portraits/503.webp differ diff --git a/resource/ship-library/assets/portraits/504.webp b/resource/ship-library/assets/portraits/504.webp new file mode 100644 index 0000000..02493c2 Binary files /dev/null and b/resource/ship-library/assets/portraits/504.webp differ diff --git a/resource/ship-library/assets/portraits/505.webp b/resource/ship-library/assets/portraits/505.webp new file mode 100644 index 0000000..e0a40bb Binary files /dev/null and b/resource/ship-library/assets/portraits/505.webp differ diff --git a/resource/ship-library/assets/portraits/506.webp b/resource/ship-library/assets/portraits/506.webp new file mode 100644 index 0000000..434928d Binary files /dev/null and b/resource/ship-library/assets/portraits/506.webp differ diff --git a/resource/ship-library/assets/portraits/507.webp b/resource/ship-library/assets/portraits/507.webp new file mode 100644 index 0000000..31e31e9 Binary files /dev/null and b/resource/ship-library/assets/portraits/507.webp differ diff --git a/resource/ship-library/assets/portraits/508.webp b/resource/ship-library/assets/portraits/508.webp new file mode 100644 index 0000000..5503644 Binary files /dev/null and b/resource/ship-library/assets/portraits/508.webp differ diff --git a/resource/ship-library/assets/portraits/509.webp b/resource/ship-library/assets/portraits/509.webp new file mode 100644 index 0000000..5240c45 Binary files /dev/null and b/resource/ship-library/assets/portraits/509.webp differ diff --git a/resource/ship-library/assets/portraits/51.webp b/resource/ship-library/assets/portraits/51.webp new file mode 100644 index 0000000..9577db3 Binary files /dev/null and b/resource/ship-library/assets/portraits/51.webp differ diff --git a/resource/ship-library/assets/portraits/510.webp b/resource/ship-library/assets/portraits/510.webp new file mode 100644 index 0000000..8443505 Binary files /dev/null and b/resource/ship-library/assets/portraits/510.webp differ diff --git a/resource/ship-library/assets/portraits/511.webp b/resource/ship-library/assets/portraits/511.webp new file mode 100644 index 0000000..fba87cb Binary files /dev/null and b/resource/ship-library/assets/portraits/511.webp differ diff --git a/resource/ship-library/assets/portraits/512.webp b/resource/ship-library/assets/portraits/512.webp new file mode 100644 index 0000000..a779671 Binary files /dev/null and b/resource/ship-library/assets/portraits/512.webp differ diff --git a/resource/ship-library/assets/portraits/513.webp b/resource/ship-library/assets/portraits/513.webp new file mode 100644 index 0000000..2ee7102 Binary files /dev/null and b/resource/ship-library/assets/portraits/513.webp differ diff --git a/resource/ship-library/assets/portraits/514.webp b/resource/ship-library/assets/portraits/514.webp new file mode 100644 index 0000000..2d56de8 Binary files /dev/null and b/resource/ship-library/assets/portraits/514.webp differ diff --git a/resource/ship-library/assets/portraits/515.webp b/resource/ship-library/assets/portraits/515.webp new file mode 100644 index 0000000..891b3b6 Binary files /dev/null and b/resource/ship-library/assets/portraits/515.webp differ diff --git a/resource/ship-library/assets/portraits/516.webp b/resource/ship-library/assets/portraits/516.webp new file mode 100644 index 0000000..5a90b0f Binary files /dev/null and b/resource/ship-library/assets/portraits/516.webp differ diff --git a/resource/ship-library/assets/portraits/517.webp b/resource/ship-library/assets/portraits/517.webp new file mode 100644 index 0000000..ec3cdeb Binary files /dev/null and b/resource/ship-library/assets/portraits/517.webp differ diff --git a/resource/ship-library/assets/portraits/518.webp b/resource/ship-library/assets/portraits/518.webp new file mode 100644 index 0000000..8d2c1a9 Binary files /dev/null and b/resource/ship-library/assets/portraits/518.webp differ diff --git a/resource/ship-library/assets/portraits/519.webp b/resource/ship-library/assets/portraits/519.webp new file mode 100644 index 0000000..adbdcfb Binary files /dev/null and b/resource/ship-library/assets/portraits/519.webp differ diff --git a/resource/ship-library/assets/portraits/52.webp b/resource/ship-library/assets/portraits/52.webp new file mode 100644 index 0000000..1e3b201 Binary files /dev/null and b/resource/ship-library/assets/portraits/52.webp differ diff --git a/resource/ship-library/assets/portraits/520.webp b/resource/ship-library/assets/portraits/520.webp new file mode 100644 index 0000000..439e3c9 Binary files /dev/null and b/resource/ship-library/assets/portraits/520.webp differ diff --git a/resource/ship-library/assets/portraits/521.webp b/resource/ship-library/assets/portraits/521.webp new file mode 100644 index 0000000..53a3914 Binary files /dev/null and b/resource/ship-library/assets/portraits/521.webp differ diff --git a/resource/ship-library/assets/portraits/522.webp b/resource/ship-library/assets/portraits/522.webp new file mode 100644 index 0000000..cd21069 Binary files /dev/null and b/resource/ship-library/assets/portraits/522.webp differ diff --git a/resource/ship-library/assets/portraits/523.webp b/resource/ship-library/assets/portraits/523.webp new file mode 100644 index 0000000..955cad2 Binary files /dev/null and b/resource/ship-library/assets/portraits/523.webp differ diff --git a/resource/ship-library/assets/portraits/524.webp b/resource/ship-library/assets/portraits/524.webp new file mode 100644 index 0000000..7314b58 Binary files /dev/null and b/resource/ship-library/assets/portraits/524.webp differ diff --git a/resource/ship-library/assets/portraits/525.webp b/resource/ship-library/assets/portraits/525.webp new file mode 100644 index 0000000..08e6798 Binary files /dev/null and b/resource/ship-library/assets/portraits/525.webp differ diff --git a/resource/ship-library/assets/portraits/526.webp b/resource/ship-library/assets/portraits/526.webp new file mode 100644 index 0000000..3593d2b Binary files /dev/null and b/resource/ship-library/assets/portraits/526.webp differ diff --git a/resource/ship-library/assets/portraits/527.webp b/resource/ship-library/assets/portraits/527.webp new file mode 100644 index 0000000..ce620d9 Binary files /dev/null and b/resource/ship-library/assets/portraits/527.webp differ diff --git a/resource/ship-library/assets/portraits/528.webp b/resource/ship-library/assets/portraits/528.webp new file mode 100644 index 0000000..b8441c9 Binary files /dev/null and b/resource/ship-library/assets/portraits/528.webp differ diff --git a/resource/ship-library/assets/portraits/529.webp b/resource/ship-library/assets/portraits/529.webp new file mode 100644 index 0000000..381c6c3 Binary files /dev/null and b/resource/ship-library/assets/portraits/529.webp differ diff --git a/resource/ship-library/assets/portraits/53.webp b/resource/ship-library/assets/portraits/53.webp new file mode 100644 index 0000000..e7e240d Binary files /dev/null and b/resource/ship-library/assets/portraits/53.webp differ diff --git a/resource/ship-library/assets/portraits/530.webp b/resource/ship-library/assets/portraits/530.webp new file mode 100644 index 0000000..3d1b28a Binary files /dev/null and b/resource/ship-library/assets/portraits/530.webp differ diff --git a/resource/ship-library/assets/portraits/531.webp b/resource/ship-library/assets/portraits/531.webp new file mode 100644 index 0000000..4287ef4 Binary files /dev/null and b/resource/ship-library/assets/portraits/531.webp differ diff --git a/resource/ship-library/assets/portraits/532.webp b/resource/ship-library/assets/portraits/532.webp new file mode 100644 index 0000000..b80e9cf Binary files /dev/null and b/resource/ship-library/assets/portraits/532.webp differ diff --git a/resource/ship-library/assets/portraits/533.webp b/resource/ship-library/assets/portraits/533.webp new file mode 100644 index 0000000..291a3fe Binary files /dev/null and b/resource/ship-library/assets/portraits/533.webp differ diff --git a/resource/ship-library/assets/portraits/534.webp b/resource/ship-library/assets/portraits/534.webp new file mode 100644 index 0000000..55f0d11 Binary files /dev/null and b/resource/ship-library/assets/portraits/534.webp differ diff --git a/resource/ship-library/assets/portraits/535.webp b/resource/ship-library/assets/portraits/535.webp new file mode 100644 index 0000000..46ba764 Binary files /dev/null and b/resource/ship-library/assets/portraits/535.webp differ diff --git a/resource/ship-library/assets/portraits/536.webp b/resource/ship-library/assets/portraits/536.webp new file mode 100644 index 0000000..4f9e891 Binary files /dev/null and b/resource/ship-library/assets/portraits/536.webp differ diff --git a/resource/ship-library/assets/portraits/537.webp b/resource/ship-library/assets/portraits/537.webp new file mode 100644 index 0000000..d4d6288 Binary files /dev/null and b/resource/ship-library/assets/portraits/537.webp differ diff --git a/resource/ship-library/assets/portraits/538.webp b/resource/ship-library/assets/portraits/538.webp new file mode 100644 index 0000000..8578d29 Binary files /dev/null and b/resource/ship-library/assets/portraits/538.webp differ diff --git a/resource/ship-library/assets/portraits/539.webp b/resource/ship-library/assets/portraits/539.webp new file mode 100644 index 0000000..6e1fdc0 Binary files /dev/null and b/resource/ship-library/assets/portraits/539.webp differ diff --git a/resource/ship-library/assets/portraits/54.webp b/resource/ship-library/assets/portraits/54.webp new file mode 100644 index 0000000..7f72ed3 Binary files /dev/null and b/resource/ship-library/assets/portraits/54.webp differ diff --git a/resource/ship-library/assets/portraits/540.webp b/resource/ship-library/assets/portraits/540.webp new file mode 100644 index 0000000..f16b067 Binary files /dev/null and b/resource/ship-library/assets/portraits/540.webp differ diff --git a/resource/ship-library/assets/portraits/541.webp b/resource/ship-library/assets/portraits/541.webp new file mode 100644 index 0000000..7a1757a Binary files /dev/null and b/resource/ship-library/assets/portraits/541.webp differ diff --git a/resource/ship-library/assets/portraits/542.webp b/resource/ship-library/assets/portraits/542.webp new file mode 100644 index 0000000..8b06037 Binary files /dev/null and b/resource/ship-library/assets/portraits/542.webp differ diff --git a/resource/ship-library/assets/portraits/543.webp b/resource/ship-library/assets/portraits/543.webp new file mode 100644 index 0000000..5a39182 Binary files /dev/null and b/resource/ship-library/assets/portraits/543.webp differ diff --git a/resource/ship-library/assets/portraits/544.webp b/resource/ship-library/assets/portraits/544.webp new file mode 100644 index 0000000..e1ecf08 Binary files /dev/null and b/resource/ship-library/assets/portraits/544.webp differ diff --git a/resource/ship-library/assets/portraits/545.webp b/resource/ship-library/assets/portraits/545.webp new file mode 100644 index 0000000..bfc06ac Binary files /dev/null and b/resource/ship-library/assets/portraits/545.webp differ diff --git a/resource/ship-library/assets/portraits/546.webp b/resource/ship-library/assets/portraits/546.webp new file mode 100644 index 0000000..9cf8447 Binary files /dev/null and b/resource/ship-library/assets/portraits/546.webp differ diff --git a/resource/ship-library/assets/portraits/547.webp b/resource/ship-library/assets/portraits/547.webp new file mode 100644 index 0000000..f695259 Binary files /dev/null and b/resource/ship-library/assets/portraits/547.webp differ diff --git a/resource/ship-library/assets/portraits/548.webp b/resource/ship-library/assets/portraits/548.webp new file mode 100644 index 0000000..bd9a288 Binary files /dev/null and b/resource/ship-library/assets/portraits/548.webp differ diff --git a/resource/ship-library/assets/portraits/549.webp b/resource/ship-library/assets/portraits/549.webp new file mode 100644 index 0000000..4041dee Binary files /dev/null and b/resource/ship-library/assets/portraits/549.webp differ diff --git a/resource/ship-library/assets/portraits/55.webp b/resource/ship-library/assets/portraits/55.webp new file mode 100644 index 0000000..c1e1e00 Binary files /dev/null and b/resource/ship-library/assets/portraits/55.webp differ diff --git a/resource/ship-library/assets/portraits/550.webp b/resource/ship-library/assets/portraits/550.webp new file mode 100644 index 0000000..b0bb6ba Binary files /dev/null and b/resource/ship-library/assets/portraits/550.webp differ diff --git a/resource/ship-library/assets/portraits/551.webp b/resource/ship-library/assets/portraits/551.webp new file mode 100644 index 0000000..d2bff43 Binary files /dev/null and b/resource/ship-library/assets/portraits/551.webp differ diff --git a/resource/ship-library/assets/portraits/552.webp b/resource/ship-library/assets/portraits/552.webp new file mode 100644 index 0000000..6bd3325 Binary files /dev/null and b/resource/ship-library/assets/portraits/552.webp differ diff --git a/resource/ship-library/assets/portraits/553.webp b/resource/ship-library/assets/portraits/553.webp new file mode 100644 index 0000000..0b008a9 Binary files /dev/null and b/resource/ship-library/assets/portraits/553.webp differ diff --git a/resource/ship-library/assets/portraits/554.webp b/resource/ship-library/assets/portraits/554.webp new file mode 100644 index 0000000..a3acf27 Binary files /dev/null and b/resource/ship-library/assets/portraits/554.webp differ diff --git a/resource/ship-library/assets/portraits/555.webp b/resource/ship-library/assets/portraits/555.webp new file mode 100644 index 0000000..48a5f14 Binary files /dev/null and b/resource/ship-library/assets/portraits/555.webp differ diff --git a/resource/ship-library/assets/portraits/556.webp b/resource/ship-library/assets/portraits/556.webp new file mode 100644 index 0000000..28a7527 Binary files /dev/null and b/resource/ship-library/assets/portraits/556.webp differ diff --git a/resource/ship-library/assets/portraits/557.webp b/resource/ship-library/assets/portraits/557.webp new file mode 100644 index 0000000..03bf7f8 Binary files /dev/null and b/resource/ship-library/assets/portraits/557.webp differ diff --git a/resource/ship-library/assets/portraits/558.webp b/resource/ship-library/assets/portraits/558.webp new file mode 100644 index 0000000..3b861f7 Binary files /dev/null and b/resource/ship-library/assets/portraits/558.webp differ diff --git a/resource/ship-library/assets/portraits/559.webp b/resource/ship-library/assets/portraits/559.webp new file mode 100644 index 0000000..ac091e5 Binary files /dev/null and b/resource/ship-library/assets/portraits/559.webp differ diff --git a/resource/ship-library/assets/portraits/56.webp b/resource/ship-library/assets/portraits/56.webp new file mode 100644 index 0000000..88f616d Binary files /dev/null and b/resource/ship-library/assets/portraits/56.webp differ diff --git a/resource/ship-library/assets/portraits/560.webp b/resource/ship-library/assets/portraits/560.webp new file mode 100644 index 0000000..e8f1c3d Binary files /dev/null and b/resource/ship-library/assets/portraits/560.webp differ diff --git a/resource/ship-library/assets/portraits/561.webp b/resource/ship-library/assets/portraits/561.webp new file mode 100644 index 0000000..edb3532 Binary files /dev/null and b/resource/ship-library/assets/portraits/561.webp differ diff --git a/resource/ship-library/assets/portraits/562.webp b/resource/ship-library/assets/portraits/562.webp new file mode 100644 index 0000000..edc648f Binary files /dev/null and b/resource/ship-library/assets/portraits/562.webp differ diff --git a/resource/ship-library/assets/portraits/563.webp b/resource/ship-library/assets/portraits/563.webp new file mode 100644 index 0000000..cf25708 Binary files /dev/null and b/resource/ship-library/assets/portraits/563.webp differ diff --git a/resource/ship-library/assets/portraits/564.webp b/resource/ship-library/assets/portraits/564.webp new file mode 100644 index 0000000..49307a8 Binary files /dev/null and b/resource/ship-library/assets/portraits/564.webp differ diff --git a/resource/ship-library/assets/portraits/565.webp b/resource/ship-library/assets/portraits/565.webp new file mode 100644 index 0000000..db5a383 Binary files /dev/null and b/resource/ship-library/assets/portraits/565.webp differ diff --git a/resource/ship-library/assets/portraits/566.webp b/resource/ship-library/assets/portraits/566.webp new file mode 100644 index 0000000..ffd4ed5 Binary files /dev/null and b/resource/ship-library/assets/portraits/566.webp differ diff --git a/resource/ship-library/assets/portraits/567.webp b/resource/ship-library/assets/portraits/567.webp new file mode 100644 index 0000000..e79efa0 Binary files /dev/null and b/resource/ship-library/assets/portraits/567.webp differ diff --git a/resource/ship-library/assets/portraits/568.webp b/resource/ship-library/assets/portraits/568.webp new file mode 100644 index 0000000..1585b97 Binary files /dev/null and b/resource/ship-library/assets/portraits/568.webp differ diff --git a/resource/ship-library/assets/portraits/569.webp b/resource/ship-library/assets/portraits/569.webp new file mode 100644 index 0000000..e0dd5a4 Binary files /dev/null and b/resource/ship-library/assets/portraits/569.webp differ diff --git a/resource/ship-library/assets/portraits/57.webp b/resource/ship-library/assets/portraits/57.webp new file mode 100644 index 0000000..1f44f2a Binary files /dev/null and b/resource/ship-library/assets/portraits/57.webp differ diff --git a/resource/ship-library/assets/portraits/570.webp b/resource/ship-library/assets/portraits/570.webp new file mode 100644 index 0000000..49e2e5c Binary files /dev/null and b/resource/ship-library/assets/portraits/570.webp differ diff --git a/resource/ship-library/assets/portraits/571.webp b/resource/ship-library/assets/portraits/571.webp new file mode 100644 index 0000000..c6bf7e3 Binary files /dev/null and b/resource/ship-library/assets/portraits/571.webp differ diff --git a/resource/ship-library/assets/portraits/572.webp b/resource/ship-library/assets/portraits/572.webp new file mode 100644 index 0000000..a419e26 Binary files /dev/null and b/resource/ship-library/assets/portraits/572.webp differ diff --git a/resource/ship-library/assets/portraits/573.webp b/resource/ship-library/assets/portraits/573.webp new file mode 100644 index 0000000..babd412 Binary files /dev/null and b/resource/ship-library/assets/portraits/573.webp differ diff --git a/resource/ship-library/assets/portraits/574.webp b/resource/ship-library/assets/portraits/574.webp new file mode 100644 index 0000000..998b9ec Binary files /dev/null and b/resource/ship-library/assets/portraits/574.webp differ diff --git a/resource/ship-library/assets/portraits/575.webp b/resource/ship-library/assets/portraits/575.webp new file mode 100644 index 0000000..4c5b771 Binary files /dev/null and b/resource/ship-library/assets/portraits/575.webp differ diff --git a/resource/ship-library/assets/portraits/576.webp b/resource/ship-library/assets/portraits/576.webp new file mode 100644 index 0000000..1798e70 Binary files /dev/null and b/resource/ship-library/assets/portraits/576.webp differ diff --git a/resource/ship-library/assets/portraits/577.webp b/resource/ship-library/assets/portraits/577.webp new file mode 100644 index 0000000..733f5fd Binary files /dev/null and b/resource/ship-library/assets/portraits/577.webp differ diff --git a/resource/ship-library/assets/portraits/578.webp b/resource/ship-library/assets/portraits/578.webp new file mode 100644 index 0000000..0dda6a7 Binary files /dev/null and b/resource/ship-library/assets/portraits/578.webp differ diff --git a/resource/ship-library/assets/portraits/579.webp b/resource/ship-library/assets/portraits/579.webp new file mode 100644 index 0000000..8f247fa Binary files /dev/null and b/resource/ship-library/assets/portraits/579.webp differ diff --git a/resource/ship-library/assets/portraits/58.webp b/resource/ship-library/assets/portraits/58.webp new file mode 100644 index 0000000..ebf725e Binary files /dev/null and b/resource/ship-library/assets/portraits/58.webp differ diff --git a/resource/ship-library/assets/portraits/580.webp b/resource/ship-library/assets/portraits/580.webp new file mode 100644 index 0000000..0637faf Binary files /dev/null and b/resource/ship-library/assets/portraits/580.webp differ diff --git a/resource/ship-library/assets/portraits/581.webp b/resource/ship-library/assets/portraits/581.webp new file mode 100644 index 0000000..61653cc Binary files /dev/null and b/resource/ship-library/assets/portraits/581.webp differ diff --git a/resource/ship-library/assets/portraits/582.webp b/resource/ship-library/assets/portraits/582.webp new file mode 100644 index 0000000..d86d023 Binary files /dev/null and b/resource/ship-library/assets/portraits/582.webp differ diff --git a/resource/ship-library/assets/portraits/583.webp b/resource/ship-library/assets/portraits/583.webp new file mode 100644 index 0000000..10194c7 Binary files /dev/null and b/resource/ship-library/assets/portraits/583.webp differ diff --git a/resource/ship-library/assets/portraits/584.webp b/resource/ship-library/assets/portraits/584.webp new file mode 100644 index 0000000..833247c Binary files /dev/null and b/resource/ship-library/assets/portraits/584.webp differ diff --git a/resource/ship-library/assets/portraits/585.webp b/resource/ship-library/assets/portraits/585.webp new file mode 100644 index 0000000..e15a569 Binary files /dev/null and b/resource/ship-library/assets/portraits/585.webp differ diff --git a/resource/ship-library/assets/portraits/586.webp b/resource/ship-library/assets/portraits/586.webp new file mode 100644 index 0000000..db785a4 Binary files /dev/null and b/resource/ship-library/assets/portraits/586.webp differ diff --git a/resource/ship-library/assets/portraits/587.webp b/resource/ship-library/assets/portraits/587.webp new file mode 100644 index 0000000..81f683e Binary files /dev/null and b/resource/ship-library/assets/portraits/587.webp differ diff --git a/resource/ship-library/assets/portraits/588.webp b/resource/ship-library/assets/portraits/588.webp new file mode 100644 index 0000000..14b06b3 Binary files /dev/null and b/resource/ship-library/assets/portraits/588.webp differ diff --git a/resource/ship-library/assets/portraits/589.webp b/resource/ship-library/assets/portraits/589.webp new file mode 100644 index 0000000..66f9d7a Binary files /dev/null and b/resource/ship-library/assets/portraits/589.webp differ diff --git a/resource/ship-library/assets/portraits/59.webp b/resource/ship-library/assets/portraits/59.webp new file mode 100644 index 0000000..ab431f8 Binary files /dev/null and b/resource/ship-library/assets/portraits/59.webp differ diff --git a/resource/ship-library/assets/portraits/590.webp b/resource/ship-library/assets/portraits/590.webp new file mode 100644 index 0000000..fba1895 Binary files /dev/null and b/resource/ship-library/assets/portraits/590.webp differ diff --git a/resource/ship-library/assets/portraits/591.webp b/resource/ship-library/assets/portraits/591.webp new file mode 100644 index 0000000..7c1181e Binary files /dev/null and b/resource/ship-library/assets/portraits/591.webp differ diff --git a/resource/ship-library/assets/portraits/592.webp b/resource/ship-library/assets/portraits/592.webp new file mode 100644 index 0000000..537c03e Binary files /dev/null and b/resource/ship-library/assets/portraits/592.webp differ diff --git a/resource/ship-library/assets/portraits/593.webp b/resource/ship-library/assets/portraits/593.webp new file mode 100644 index 0000000..5e5f9cd Binary files /dev/null and b/resource/ship-library/assets/portraits/593.webp differ diff --git a/resource/ship-library/assets/portraits/594.webp b/resource/ship-library/assets/portraits/594.webp new file mode 100644 index 0000000..6084c45 Binary files /dev/null and b/resource/ship-library/assets/portraits/594.webp differ diff --git a/resource/ship-library/assets/portraits/595.webp b/resource/ship-library/assets/portraits/595.webp new file mode 100644 index 0000000..369ac0a Binary files /dev/null and b/resource/ship-library/assets/portraits/595.webp differ diff --git a/resource/ship-library/assets/portraits/596.webp b/resource/ship-library/assets/portraits/596.webp new file mode 100644 index 0000000..108177e Binary files /dev/null and b/resource/ship-library/assets/portraits/596.webp differ diff --git a/resource/ship-library/assets/portraits/597.webp b/resource/ship-library/assets/portraits/597.webp new file mode 100644 index 0000000..a08ec35 Binary files /dev/null and b/resource/ship-library/assets/portraits/597.webp differ diff --git a/resource/ship-library/assets/portraits/598.webp b/resource/ship-library/assets/portraits/598.webp new file mode 100644 index 0000000..f3c0550 Binary files /dev/null and b/resource/ship-library/assets/portraits/598.webp differ diff --git a/resource/ship-library/assets/portraits/599.webp b/resource/ship-library/assets/portraits/599.webp new file mode 100644 index 0000000..e2af089 Binary files /dev/null and b/resource/ship-library/assets/portraits/599.webp differ diff --git a/resource/ship-library/assets/portraits/6.webp b/resource/ship-library/assets/portraits/6.webp new file mode 100644 index 0000000..e7d8a32 Binary files /dev/null and b/resource/ship-library/assets/portraits/6.webp differ diff --git a/resource/ship-library/assets/portraits/60.webp b/resource/ship-library/assets/portraits/60.webp new file mode 100644 index 0000000..212c3e8 Binary files /dev/null and b/resource/ship-library/assets/portraits/60.webp differ diff --git a/resource/ship-library/assets/portraits/600.webp b/resource/ship-library/assets/portraits/600.webp new file mode 100644 index 0000000..f4f651f Binary files /dev/null and b/resource/ship-library/assets/portraits/600.webp differ diff --git a/resource/ship-library/assets/portraits/601.webp b/resource/ship-library/assets/portraits/601.webp new file mode 100644 index 0000000..af0f610 Binary files /dev/null and b/resource/ship-library/assets/portraits/601.webp differ diff --git a/resource/ship-library/assets/portraits/602.webp b/resource/ship-library/assets/portraits/602.webp new file mode 100644 index 0000000..1e85476 Binary files /dev/null and b/resource/ship-library/assets/portraits/602.webp differ diff --git a/resource/ship-library/assets/portraits/603.webp b/resource/ship-library/assets/portraits/603.webp new file mode 100644 index 0000000..675331c Binary files /dev/null and b/resource/ship-library/assets/portraits/603.webp differ diff --git a/resource/ship-library/assets/portraits/604.webp b/resource/ship-library/assets/portraits/604.webp new file mode 100644 index 0000000..2973005 Binary files /dev/null and b/resource/ship-library/assets/portraits/604.webp differ diff --git a/resource/ship-library/assets/portraits/605.webp b/resource/ship-library/assets/portraits/605.webp new file mode 100644 index 0000000..1ba135f Binary files /dev/null and b/resource/ship-library/assets/portraits/605.webp differ diff --git a/resource/ship-library/assets/portraits/606.webp b/resource/ship-library/assets/portraits/606.webp new file mode 100644 index 0000000..b4395c9 Binary files /dev/null and b/resource/ship-library/assets/portraits/606.webp differ diff --git a/resource/ship-library/assets/portraits/607.webp b/resource/ship-library/assets/portraits/607.webp new file mode 100644 index 0000000..f76a411 Binary files /dev/null and b/resource/ship-library/assets/portraits/607.webp differ diff --git a/resource/ship-library/assets/portraits/608.webp b/resource/ship-library/assets/portraits/608.webp new file mode 100644 index 0000000..d9ec650 Binary files /dev/null and b/resource/ship-library/assets/portraits/608.webp differ diff --git a/resource/ship-library/assets/portraits/609.webp b/resource/ship-library/assets/portraits/609.webp new file mode 100644 index 0000000..8f95a46 Binary files /dev/null and b/resource/ship-library/assets/portraits/609.webp differ diff --git a/resource/ship-library/assets/portraits/61.webp b/resource/ship-library/assets/portraits/61.webp new file mode 100644 index 0000000..8cfb101 Binary files /dev/null and b/resource/ship-library/assets/portraits/61.webp differ diff --git a/resource/ship-library/assets/portraits/610.webp b/resource/ship-library/assets/portraits/610.webp new file mode 100644 index 0000000..4863bd8 Binary files /dev/null and b/resource/ship-library/assets/portraits/610.webp differ diff --git a/resource/ship-library/assets/portraits/611.webp b/resource/ship-library/assets/portraits/611.webp new file mode 100644 index 0000000..36fcb98 Binary files /dev/null and b/resource/ship-library/assets/portraits/611.webp differ diff --git a/resource/ship-library/assets/portraits/612.webp b/resource/ship-library/assets/portraits/612.webp new file mode 100644 index 0000000..6ba5274 Binary files /dev/null and b/resource/ship-library/assets/portraits/612.webp differ diff --git a/resource/ship-library/assets/portraits/613.webp b/resource/ship-library/assets/portraits/613.webp new file mode 100644 index 0000000..ee7a43a Binary files /dev/null and b/resource/ship-library/assets/portraits/613.webp differ diff --git a/resource/ship-library/assets/portraits/614.webp b/resource/ship-library/assets/portraits/614.webp new file mode 100644 index 0000000..ce1f676 Binary files /dev/null and b/resource/ship-library/assets/portraits/614.webp differ diff --git a/resource/ship-library/assets/portraits/615.webp b/resource/ship-library/assets/portraits/615.webp new file mode 100644 index 0000000..373f974 Binary files /dev/null and b/resource/ship-library/assets/portraits/615.webp differ diff --git a/resource/ship-library/assets/portraits/616.webp b/resource/ship-library/assets/portraits/616.webp new file mode 100644 index 0000000..349d146 Binary files /dev/null and b/resource/ship-library/assets/portraits/616.webp differ diff --git a/resource/ship-library/assets/portraits/617.webp b/resource/ship-library/assets/portraits/617.webp new file mode 100644 index 0000000..afdff29 Binary files /dev/null and b/resource/ship-library/assets/portraits/617.webp differ diff --git a/resource/ship-library/assets/portraits/618.webp b/resource/ship-library/assets/portraits/618.webp new file mode 100644 index 0000000..729fbc8 Binary files /dev/null and b/resource/ship-library/assets/portraits/618.webp differ diff --git a/resource/ship-library/assets/portraits/619.webp b/resource/ship-library/assets/portraits/619.webp new file mode 100644 index 0000000..ff71029 Binary files /dev/null and b/resource/ship-library/assets/portraits/619.webp differ diff --git a/resource/ship-library/assets/portraits/62.webp b/resource/ship-library/assets/portraits/62.webp new file mode 100644 index 0000000..0f0b6b6 Binary files /dev/null and b/resource/ship-library/assets/portraits/62.webp differ diff --git a/resource/ship-library/assets/portraits/620.webp b/resource/ship-library/assets/portraits/620.webp new file mode 100644 index 0000000..c2db700 Binary files /dev/null and b/resource/ship-library/assets/portraits/620.webp differ diff --git a/resource/ship-library/assets/portraits/621.webp b/resource/ship-library/assets/portraits/621.webp new file mode 100644 index 0000000..e48cb49 Binary files /dev/null and b/resource/ship-library/assets/portraits/621.webp differ diff --git a/resource/ship-library/assets/portraits/622.webp b/resource/ship-library/assets/portraits/622.webp new file mode 100644 index 0000000..33a8319 Binary files /dev/null and b/resource/ship-library/assets/portraits/622.webp differ diff --git a/resource/ship-library/assets/portraits/623.webp b/resource/ship-library/assets/portraits/623.webp new file mode 100644 index 0000000..5f88ad5 Binary files /dev/null and b/resource/ship-library/assets/portraits/623.webp differ diff --git a/resource/ship-library/assets/portraits/624.webp b/resource/ship-library/assets/portraits/624.webp new file mode 100644 index 0000000..b32a122 Binary files /dev/null and b/resource/ship-library/assets/portraits/624.webp differ diff --git a/resource/ship-library/assets/portraits/625.webp b/resource/ship-library/assets/portraits/625.webp new file mode 100644 index 0000000..1e6d44f Binary files /dev/null and b/resource/ship-library/assets/portraits/625.webp differ diff --git a/resource/ship-library/assets/portraits/626.webp b/resource/ship-library/assets/portraits/626.webp new file mode 100644 index 0000000..2795235 Binary files /dev/null and b/resource/ship-library/assets/portraits/626.webp differ diff --git a/resource/ship-library/assets/portraits/627.webp b/resource/ship-library/assets/portraits/627.webp new file mode 100644 index 0000000..01ce17c Binary files /dev/null and b/resource/ship-library/assets/portraits/627.webp differ diff --git a/resource/ship-library/assets/portraits/628.webp b/resource/ship-library/assets/portraits/628.webp new file mode 100644 index 0000000..d345cc3 Binary files /dev/null and b/resource/ship-library/assets/portraits/628.webp differ diff --git a/resource/ship-library/assets/portraits/629.webp b/resource/ship-library/assets/portraits/629.webp new file mode 100644 index 0000000..abadede Binary files /dev/null and b/resource/ship-library/assets/portraits/629.webp differ diff --git a/resource/ship-library/assets/portraits/63.webp b/resource/ship-library/assets/portraits/63.webp new file mode 100644 index 0000000..982a807 Binary files /dev/null and b/resource/ship-library/assets/portraits/63.webp differ diff --git a/resource/ship-library/assets/portraits/630.webp b/resource/ship-library/assets/portraits/630.webp new file mode 100644 index 0000000..4db6c96 Binary files /dev/null and b/resource/ship-library/assets/portraits/630.webp differ diff --git a/resource/ship-library/assets/portraits/631.webp b/resource/ship-library/assets/portraits/631.webp new file mode 100644 index 0000000..58236d5 Binary files /dev/null and b/resource/ship-library/assets/portraits/631.webp differ diff --git a/resource/ship-library/assets/portraits/632.webp b/resource/ship-library/assets/portraits/632.webp new file mode 100644 index 0000000..56857fd Binary files /dev/null and b/resource/ship-library/assets/portraits/632.webp differ diff --git a/resource/ship-library/assets/portraits/633.webp b/resource/ship-library/assets/portraits/633.webp new file mode 100644 index 0000000..fc1ba41 Binary files /dev/null and b/resource/ship-library/assets/portraits/633.webp differ diff --git a/resource/ship-library/assets/portraits/634.webp b/resource/ship-library/assets/portraits/634.webp new file mode 100644 index 0000000..69519ab Binary files /dev/null and b/resource/ship-library/assets/portraits/634.webp differ diff --git a/resource/ship-library/assets/portraits/635.webp b/resource/ship-library/assets/portraits/635.webp new file mode 100644 index 0000000..0a90b22 Binary files /dev/null and b/resource/ship-library/assets/portraits/635.webp differ diff --git a/resource/ship-library/assets/portraits/636.webp b/resource/ship-library/assets/portraits/636.webp new file mode 100644 index 0000000..2262d8e Binary files /dev/null and b/resource/ship-library/assets/portraits/636.webp differ diff --git a/resource/ship-library/assets/portraits/637.webp b/resource/ship-library/assets/portraits/637.webp new file mode 100644 index 0000000..88fa09a Binary files /dev/null and b/resource/ship-library/assets/portraits/637.webp differ diff --git a/resource/ship-library/assets/portraits/638.webp b/resource/ship-library/assets/portraits/638.webp new file mode 100644 index 0000000..228bc15 Binary files /dev/null and b/resource/ship-library/assets/portraits/638.webp differ diff --git a/resource/ship-library/assets/portraits/639.webp b/resource/ship-library/assets/portraits/639.webp new file mode 100644 index 0000000..ae2dcd7 Binary files /dev/null and b/resource/ship-library/assets/portraits/639.webp differ diff --git a/resource/ship-library/assets/portraits/64.webp b/resource/ship-library/assets/portraits/64.webp new file mode 100644 index 0000000..7927372 Binary files /dev/null and b/resource/ship-library/assets/portraits/64.webp differ diff --git a/resource/ship-library/assets/portraits/640.webp b/resource/ship-library/assets/portraits/640.webp new file mode 100644 index 0000000..3143e55 Binary files /dev/null and b/resource/ship-library/assets/portraits/640.webp differ diff --git a/resource/ship-library/assets/portraits/641.webp b/resource/ship-library/assets/portraits/641.webp new file mode 100644 index 0000000..c561af4 Binary files /dev/null and b/resource/ship-library/assets/portraits/641.webp differ diff --git a/resource/ship-library/assets/portraits/642.webp b/resource/ship-library/assets/portraits/642.webp new file mode 100644 index 0000000..3ef1e9e Binary files /dev/null and b/resource/ship-library/assets/portraits/642.webp differ diff --git a/resource/ship-library/assets/portraits/643.webp b/resource/ship-library/assets/portraits/643.webp new file mode 100644 index 0000000..bff788f Binary files /dev/null and b/resource/ship-library/assets/portraits/643.webp differ diff --git a/resource/ship-library/assets/portraits/644.webp b/resource/ship-library/assets/portraits/644.webp new file mode 100644 index 0000000..8124691 Binary files /dev/null and b/resource/ship-library/assets/portraits/644.webp differ diff --git a/resource/ship-library/assets/portraits/645.webp b/resource/ship-library/assets/portraits/645.webp new file mode 100644 index 0000000..13a253c Binary files /dev/null and b/resource/ship-library/assets/portraits/645.webp differ diff --git a/resource/ship-library/assets/portraits/646.webp b/resource/ship-library/assets/portraits/646.webp new file mode 100644 index 0000000..74d48a0 Binary files /dev/null and b/resource/ship-library/assets/portraits/646.webp differ diff --git a/resource/ship-library/assets/portraits/647.webp b/resource/ship-library/assets/portraits/647.webp new file mode 100644 index 0000000..78a3e1f Binary files /dev/null and b/resource/ship-library/assets/portraits/647.webp differ diff --git a/resource/ship-library/assets/portraits/648.webp b/resource/ship-library/assets/portraits/648.webp new file mode 100644 index 0000000..4ddc425 Binary files /dev/null and b/resource/ship-library/assets/portraits/648.webp differ diff --git a/resource/ship-library/assets/portraits/649.webp b/resource/ship-library/assets/portraits/649.webp new file mode 100644 index 0000000..b1b1ebc Binary files /dev/null and b/resource/ship-library/assets/portraits/649.webp differ diff --git a/resource/ship-library/assets/portraits/65.webp b/resource/ship-library/assets/portraits/65.webp new file mode 100644 index 0000000..f0c3a8e Binary files /dev/null and b/resource/ship-library/assets/portraits/65.webp differ diff --git a/resource/ship-library/assets/portraits/650.webp b/resource/ship-library/assets/portraits/650.webp new file mode 100644 index 0000000..6d17028 Binary files /dev/null and b/resource/ship-library/assets/portraits/650.webp differ diff --git a/resource/ship-library/assets/portraits/651.webp b/resource/ship-library/assets/portraits/651.webp new file mode 100644 index 0000000..9969e36 Binary files /dev/null and b/resource/ship-library/assets/portraits/651.webp differ diff --git a/resource/ship-library/assets/portraits/66.webp b/resource/ship-library/assets/portraits/66.webp new file mode 100644 index 0000000..548cea0 Binary files /dev/null and b/resource/ship-library/assets/portraits/66.webp differ diff --git a/resource/ship-library/assets/portraits/67.webp b/resource/ship-library/assets/portraits/67.webp new file mode 100644 index 0000000..6b9c75a Binary files /dev/null and b/resource/ship-library/assets/portraits/67.webp differ diff --git a/resource/ship-library/assets/portraits/68.webp b/resource/ship-library/assets/portraits/68.webp new file mode 100644 index 0000000..70f59f9 Binary files /dev/null and b/resource/ship-library/assets/portraits/68.webp differ diff --git a/resource/ship-library/assets/portraits/69.webp b/resource/ship-library/assets/portraits/69.webp new file mode 100644 index 0000000..f4ba13a Binary files /dev/null and b/resource/ship-library/assets/portraits/69.webp differ diff --git a/resource/ship-library/assets/portraits/7.webp b/resource/ship-library/assets/portraits/7.webp new file mode 100644 index 0000000..1f64b9a Binary files /dev/null and b/resource/ship-library/assets/portraits/7.webp differ diff --git a/resource/ship-library/assets/portraits/70.webp b/resource/ship-library/assets/portraits/70.webp new file mode 100644 index 0000000..52bf287 Binary files /dev/null and b/resource/ship-library/assets/portraits/70.webp differ diff --git a/resource/ship-library/assets/portraits/71.webp b/resource/ship-library/assets/portraits/71.webp new file mode 100644 index 0000000..963d64d Binary files /dev/null and b/resource/ship-library/assets/portraits/71.webp differ diff --git a/resource/ship-library/assets/portraits/72.webp b/resource/ship-library/assets/portraits/72.webp new file mode 100644 index 0000000..a94ba55 Binary files /dev/null and b/resource/ship-library/assets/portraits/72.webp differ diff --git a/resource/ship-library/assets/portraits/73.webp b/resource/ship-library/assets/portraits/73.webp new file mode 100644 index 0000000..8ad4aec Binary files /dev/null and b/resource/ship-library/assets/portraits/73.webp differ diff --git a/resource/ship-library/assets/portraits/74.webp b/resource/ship-library/assets/portraits/74.webp new file mode 100644 index 0000000..c38ccf4 Binary files /dev/null and b/resource/ship-library/assets/portraits/74.webp differ diff --git a/resource/ship-library/assets/portraits/75.webp b/resource/ship-library/assets/portraits/75.webp new file mode 100644 index 0000000..3099e01 Binary files /dev/null and b/resource/ship-library/assets/portraits/75.webp differ diff --git a/resource/ship-library/assets/portraits/76.webp b/resource/ship-library/assets/portraits/76.webp new file mode 100644 index 0000000..447bd5a Binary files /dev/null and b/resource/ship-library/assets/portraits/76.webp differ diff --git a/resource/ship-library/assets/portraits/77.webp b/resource/ship-library/assets/portraits/77.webp new file mode 100644 index 0000000..d0e28d9 Binary files /dev/null and b/resource/ship-library/assets/portraits/77.webp differ diff --git a/resource/ship-library/assets/portraits/78.webp b/resource/ship-library/assets/portraits/78.webp new file mode 100644 index 0000000..26b0e79 Binary files /dev/null and b/resource/ship-library/assets/portraits/78.webp differ diff --git a/resource/ship-library/assets/portraits/79.webp b/resource/ship-library/assets/portraits/79.webp new file mode 100644 index 0000000..dfd9143 Binary files /dev/null and b/resource/ship-library/assets/portraits/79.webp differ diff --git a/resource/ship-library/assets/portraits/8.webp b/resource/ship-library/assets/portraits/8.webp new file mode 100644 index 0000000..3a74848 Binary files /dev/null and b/resource/ship-library/assets/portraits/8.webp differ diff --git a/resource/ship-library/assets/portraits/80.webp b/resource/ship-library/assets/portraits/80.webp new file mode 100644 index 0000000..e5bb7eb Binary files /dev/null and b/resource/ship-library/assets/portraits/80.webp differ diff --git a/resource/ship-library/assets/portraits/8007.webp b/resource/ship-library/assets/portraits/8007.webp new file mode 100644 index 0000000..11410d0 Binary files /dev/null and b/resource/ship-library/assets/portraits/8007.webp differ diff --git a/resource/ship-library/assets/portraits/8009.webp b/resource/ship-library/assets/portraits/8009.webp new file mode 100644 index 0000000..305a9e6 Binary files /dev/null and b/resource/ship-library/assets/portraits/8009.webp differ diff --git a/resource/ship-library/assets/portraits/81.webp b/resource/ship-library/assets/portraits/81.webp new file mode 100644 index 0000000..d3ed27a Binary files /dev/null and b/resource/ship-library/assets/portraits/81.webp differ diff --git a/resource/ship-library/assets/portraits/8111.webp b/resource/ship-library/assets/portraits/8111.webp new file mode 100644 index 0000000..c27e521 Binary files /dev/null and b/resource/ship-library/assets/portraits/8111.webp differ diff --git a/resource/ship-library/assets/portraits/8116.webp b/resource/ship-library/assets/portraits/8116.webp new file mode 100644 index 0000000..cce2f3c Binary files /dev/null and b/resource/ship-library/assets/portraits/8116.webp differ diff --git a/resource/ship-library/assets/portraits/82.webp b/resource/ship-library/assets/portraits/82.webp new file mode 100644 index 0000000..dc312f8 Binary files /dev/null and b/resource/ship-library/assets/portraits/82.webp differ diff --git a/resource/ship-library/assets/portraits/83.webp b/resource/ship-library/assets/portraits/83.webp new file mode 100644 index 0000000..2522243 Binary files /dev/null and b/resource/ship-library/assets/portraits/83.webp differ diff --git a/resource/ship-library/assets/portraits/84.webp b/resource/ship-library/assets/portraits/84.webp new file mode 100644 index 0000000..eadaa6d Binary files /dev/null and b/resource/ship-library/assets/portraits/84.webp differ diff --git a/resource/ship-library/assets/portraits/85.webp b/resource/ship-library/assets/portraits/85.webp new file mode 100644 index 0000000..bfb7f0f Binary files /dev/null and b/resource/ship-library/assets/portraits/85.webp differ diff --git a/resource/ship-library/assets/portraits/86.webp b/resource/ship-library/assets/portraits/86.webp new file mode 100644 index 0000000..51c23f3 Binary files /dev/null and b/resource/ship-library/assets/portraits/86.webp differ diff --git a/resource/ship-library/assets/portraits/87.webp b/resource/ship-library/assets/portraits/87.webp new file mode 100644 index 0000000..b345d07 Binary files /dev/null and b/resource/ship-library/assets/portraits/87.webp differ diff --git a/resource/ship-library/assets/portraits/88.webp b/resource/ship-library/assets/portraits/88.webp new file mode 100644 index 0000000..787ddc0 Binary files /dev/null and b/resource/ship-library/assets/portraits/88.webp differ diff --git a/resource/ship-library/assets/portraits/89.webp b/resource/ship-library/assets/portraits/89.webp new file mode 100644 index 0000000..bf44a77 Binary files /dev/null and b/resource/ship-library/assets/portraits/89.webp differ diff --git a/resource/ship-library/assets/portraits/9.webp b/resource/ship-library/assets/portraits/9.webp new file mode 100644 index 0000000..0fc2a45 Binary files /dev/null and b/resource/ship-library/assets/portraits/9.webp differ diff --git a/resource/ship-library/assets/portraits/90.webp b/resource/ship-library/assets/portraits/90.webp new file mode 100644 index 0000000..b853264 Binary files /dev/null and b/resource/ship-library/assets/portraits/90.webp differ diff --git a/resource/ship-library/assets/portraits/91.webp b/resource/ship-library/assets/portraits/91.webp new file mode 100644 index 0000000..6322915 Binary files /dev/null and b/resource/ship-library/assets/portraits/91.webp differ diff --git a/resource/ship-library/assets/portraits/92.webp b/resource/ship-library/assets/portraits/92.webp new file mode 100644 index 0000000..920e0a0 Binary files /dev/null and b/resource/ship-library/assets/portraits/92.webp differ diff --git a/resource/ship-library/assets/portraits/93.webp b/resource/ship-library/assets/portraits/93.webp new file mode 100644 index 0000000..df53c28 Binary files /dev/null and b/resource/ship-library/assets/portraits/93.webp differ diff --git a/resource/ship-library/assets/portraits/94.webp b/resource/ship-library/assets/portraits/94.webp new file mode 100644 index 0000000..c30312d Binary files /dev/null and b/resource/ship-library/assets/portraits/94.webp differ diff --git a/resource/ship-library/assets/portraits/95.webp b/resource/ship-library/assets/portraits/95.webp new file mode 100644 index 0000000..647a88c Binary files /dev/null and b/resource/ship-library/assets/portraits/95.webp differ diff --git a/resource/ship-library/assets/portraits/96.webp b/resource/ship-library/assets/portraits/96.webp new file mode 100644 index 0000000..304237d Binary files /dev/null and b/resource/ship-library/assets/portraits/96.webp differ diff --git a/resource/ship-library/assets/portraits/97.webp b/resource/ship-library/assets/portraits/97.webp new file mode 100644 index 0000000..9607e7e Binary files /dev/null and b/resource/ship-library/assets/portraits/97.webp differ diff --git a/resource/ship-library/assets/portraits/98.webp b/resource/ship-library/assets/portraits/98.webp new file mode 100644 index 0000000..71896be Binary files /dev/null and b/resource/ship-library/assets/portraits/98.webp differ diff --git a/resource/ship-library/assets/portraits/99.webp b/resource/ship-library/assets/portraits/99.webp new file mode 100644 index 0000000..279a8a3 Binary files /dev/null and b/resource/ship-library/assets/portraits/99.webp differ diff --git a/resource/ship-library/assets/type-icons/ap.webp b/resource/ship-library/assets/type-icons/ap.webp new file mode 100644 index 0000000..6e7f785 Binary files /dev/null and b/resource/ship-library/assets/type-icons/ap.webp differ diff --git a/resource/ship-library/assets/type-icons/av.webp b/resource/ship-library/assets/type-icons/av.webp new file mode 100644 index 0000000..86a710a Binary files /dev/null and b/resource/ship-library/assets/type-icons/av.webp differ diff --git a/resource/ship-library/assets/type-icons/bb.webp b/resource/ship-library/assets/type-icons/bb.webp new file mode 100644 index 0000000..81fd732 Binary files /dev/null and b/resource/ship-library/assets/type-icons/bb.webp differ diff --git a/resource/ship-library/assets/type-icons/bbg.webp b/resource/ship-library/assets/type-icons/bbg.webp new file mode 100644 index 0000000..f6d0442 Binary files /dev/null and b/resource/ship-library/assets/type-icons/bbg.webp differ diff --git a/resource/ship-library/assets/type-icons/bbv.webp b/resource/ship-library/assets/type-icons/bbv.webp new file mode 100644 index 0000000..02c51a4 Binary files /dev/null and b/resource/ship-library/assets/type-icons/bbv.webp differ diff --git a/resource/ship-library/assets/type-icons/bc.webp b/resource/ship-library/assets/type-icons/bc.webp new file mode 100644 index 0000000..6e6b903 Binary files /dev/null and b/resource/ship-library/assets/type-icons/bc.webp differ diff --git a/resource/ship-library/assets/type-icons/bm.webp b/resource/ship-library/assets/type-icons/bm.webp new file mode 100644 index 0000000..a721450 Binary files /dev/null and b/resource/ship-library/assets/type-icons/bm.webp differ diff --git a/resource/ship-library/assets/type-icons/ca.webp b/resource/ship-library/assets/type-icons/ca.webp new file mode 100644 index 0000000..ec5bc9a Binary files /dev/null and b/resource/ship-library/assets/type-icons/ca.webp differ diff --git a/resource/ship-library/assets/type-icons/cav.webp b/resource/ship-library/assets/type-icons/cav.webp new file mode 100644 index 0000000..9616554 Binary files /dev/null and b/resource/ship-library/assets/type-icons/cav.webp differ diff --git a/resource/ship-library/assets/type-icons/cbg.webp b/resource/ship-library/assets/type-icons/cbg.webp new file mode 100644 index 0000000..d05ea74 Binary files /dev/null and b/resource/ship-library/assets/type-icons/cbg.webp differ diff --git a/resource/ship-library/assets/type-icons/cf.webp b/resource/ship-library/assets/type-icons/cf.webp new file mode 100644 index 0000000..8e4390e Binary files /dev/null and b/resource/ship-library/assets/type-icons/cf.webp differ diff --git a/resource/ship-library/assets/type-icons/cg.webp b/resource/ship-library/assets/type-icons/cg.webp new file mode 100644 index 0000000..18a8f14 Binary files /dev/null and b/resource/ship-library/assets/type-icons/cg.webp differ diff --git a/resource/ship-library/assets/type-icons/cgaa.webp b/resource/ship-library/assets/type-icons/cgaa.webp new file mode 100644 index 0000000..b6623b5 Binary files /dev/null and b/resource/ship-library/assets/type-icons/cgaa.webp differ diff --git a/resource/ship-library/assets/type-icons/cl.webp b/resource/ship-library/assets/type-icons/cl.webp new file mode 100644 index 0000000..00c8bcd Binary files /dev/null and b/resource/ship-library/assets/type-icons/cl.webp differ diff --git a/resource/ship-library/assets/type-icons/clt.webp b/resource/ship-library/assets/type-icons/clt.webp new file mode 100644 index 0000000..c6b7e22 Binary files /dev/null and b/resource/ship-library/assets/type-icons/clt.webp differ diff --git a/resource/ship-library/assets/type-icons/cv.webp b/resource/ship-library/assets/type-icons/cv.webp new file mode 100644 index 0000000..14d38b6 Binary files /dev/null and b/resource/ship-library/assets/type-icons/cv.webp differ diff --git a/resource/ship-library/assets/type-icons/cvl.webp b/resource/ship-library/assets/type-icons/cvl.webp new file mode 100644 index 0000000..39f188b Binary files /dev/null and b/resource/ship-library/assets/type-icons/cvl.webp differ diff --git a/resource/ship-library/assets/type-icons/dd.webp b/resource/ship-library/assets/type-icons/dd.webp new file mode 100644 index 0000000..b165169 Binary files /dev/null and b/resource/ship-library/assets/type-icons/dd.webp differ diff --git a/resource/ship-library/assets/type-icons/ddg.webp b/resource/ship-library/assets/type-icons/ddg.webp new file mode 100644 index 0000000..0f2b01d Binary files /dev/null and b/resource/ship-library/assets/type-icons/ddg.webp differ diff --git a/resource/ship-library/assets/type-icons/ddgaa.webp b/resource/ship-library/assets/type-icons/ddgaa.webp new file mode 100644 index 0000000..0984320 Binary files /dev/null and b/resource/ship-library/assets/type-icons/ddgaa.webp differ diff --git a/resource/ship-library/assets/type-icons/sc.webp b/resource/ship-library/assets/type-icons/sc.webp new file mode 100644 index 0000000..7235f9b Binary files /dev/null and b/resource/ship-library/assets/type-icons/sc.webp differ diff --git a/resource/ship-library/assets/type-icons/ss.webp b/resource/ship-library/assets/type-icons/ss.webp new file mode 100644 index 0000000..fd5b3a5 Binary files /dev/null and b/resource/ship-library/assets/type-icons/ss.webp differ diff --git a/resource/ship-library/assets/type-icons/ssg.webp b/resource/ship-library/assets/type-icons/ssg.webp new file mode 100644 index 0000000..7434957 Binary files /dev/null and b/resource/ship-library/assets/type-icons/ssg.webp differ diff --git a/resource/ship-library/database/ships.sqlite3 b/resource/ship-library/database/ships.sqlite3 new file mode 100644 index 0000000..fa176ef Binary files /dev/null and b/resource/ship-library/database/ships.sqlite3 differ diff --git a/resource/ship-library/labels.zh-CN.json b/resource/ship-library/labels.zh-CN.json new file mode 100644 index 0000000..b721e10 --- /dev/null +++ b/resource/ship-library/labels.zh-CN.json @@ -0,0 +1,52 @@ +{ + "locale": "zh-CN", + "ship_types": { + "aadg": "防驱", + "ap": "补给", + "asdg": "导驱", + "av": "装母", + "bb": "战列", + "bbg": "导战", + "bbv": "航战", + "bc": "战巡", + "bg": "大巡", + "bm": "重炮", + "ca": "重巡", + "cav": "航巡", + "cg": "防巡", + "cl": "轻巡", + "clt": "雷巡", + "cv": "航母", + "cvl": "轻母", + "dd": "驱逐", + "kp": "导巡", + "sc": "炮潜", + "ss": "潜艇", + "ssg": "导潜" + }, + "size_classes": { + "large": "大型舰", + "medium": "中型舰", + "small": "小型舰" + }, + "role_classes": { + "main_force": "主力舰", + "escort": "护卫舰" + }, + "countries": { + "china": "中国", + "france": "法国", + "germany": "德国", + "italy": "意大利", + "japan": "日本", + "other": "其他", + "soviet_union": "苏联", + "united_kingdom": "英国", + "united_states": "美国" + }, + "variants": { + "normal": "普通", + "refit": "改造", + "special": "特殊" + } +} diff --git a/resource/ship-library/manifest.json b/resource/ship-library/manifest.json new file mode 100644 index 0000000..e812921 --- /dev/null +++ b/resource/ship-library/manifest.json @@ -0,0 +1,14434 @@ +{ + "schema_version": 4, + "generated_at": "2026-08-04T07:16:55+00:00", + "source": { + "wiki": "https://www.zjsnrwiki.com", + "index_url": "https://www.zjsnrwiki.com/wiki/%E8%88%B0%E5%A8%98%E5%9B%BE%E9%89%B4", + "database_revision": 122519, + "patch_revision": 121763, + "ship_type_schema": "legacy" + }, + "counts": { + "ships": 894, + "assets": 929, + "missing_assets": 0 + }, + "labels": { + "locale": "zh-CN", + "ship_types": { + "aadg": "防驱", + "ap": "补给", + "asdg": "导驱", + "av": "装母", + "bb": "战列", + "bbg": "导战", + "bbv": "航战", + "bc": "战巡", + "bg": "大巡", + "bm": "重炮", + "ca": "重巡", + "cav": "航巡", + "cg": "防巡", + "cl": "轻巡", + "clt": "雷巡", + "cv": "航母", + "cvl": "轻母", + "dd": "驱逐", + "kp": "导巡", + "sc": "炮潜", + "ss": "潜艇", + "ssg": "导潜" + }, + "size_classes": { + "large": "大型舰", + "medium": "中型舰", + "small": "小型舰" + }, + "role_classes": { + "main_force": "主力舰", + "escort": "护卫舰" + }, + "countries": { + "china": "中国", + "france": "法国", + "germany": "德国", + "italy": "意大利", + "japan": "日本", + "other": "其他", + "soviet_union": "苏联", + "united_kingdom": "英国", + "united_states": "美国" + }, + "variants": { + "normal": "普通", + "refit": "改造", + "special": "特殊" + } + }, + "type_groups": { + "size_classes": { + "large": [ + "cv", + "av", + "bb", + "bbv", + "bc", + "bg", + "bbg" + ], + "medium": [ + "cvl", + "ca", + "cav", + "cl", + "clt", + "kp", + "cg" + ], + "small": [ + "dd", + "asdg", + "aadg", + "bm", + "ss", + "sc", + "ssg", + "ap" + ] + }, + "role_classes": { + "main_force": [ + "cv", + "av", + "bb", + "bbv", + "bc", + "bg", + "bbg", + "kp", + "asdg", + "ssg" + ], + "escort": [ + "cvl", + "ca", + "cav", + "cl", + "clt", + "cg", + "dd", + "aadg", + "bm", + "ss", + "sc", + "ap" + ] + } + }, + "ships": [ + { + "id": 1, + "name": "胡德", + "search_name": "胡德", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%83%A1%E5%BE%B7" + }, + { + "id": 2, + "name": "扶桑", + "search_name": "扶桑", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/2.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%89%B6%E6%A1%91" + }, + { + "id": 3, + "name": "山城", + "search_name": "山城", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/3.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B1%B1%E5%9F%8E" + }, + { + "id": 4, + "name": "伊势", + "search_name": "伊势", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/4.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E5%8A%BF" + }, + { + "id": 5, + "name": "日向", + "search_name": "日向", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/5.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A5%E5%90%91" + }, + { + "id": 6, + "name": "俾斯麦", + "search_name": "俾斯麦", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/6.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BF%BE%E6%96%AF%E9%BA%A6" + }, + { + "id": 7, + "name": "提尔比茨", + "search_name": "提尔比茨", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/7.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8F%90%E5%B0%94%E6%AF%94%E8%8C%A8" + }, + { + "id": 8, + "name": "纳尔逊", + "search_name": "纳尔逊", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/8.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%B3%E5%B0%94%E9%80%8A" + }, + { + "id": 9, + "name": "罗德尼", + "search_name": "罗德尼", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/9.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BD%97%E5%BE%B7%E5%B0%BC" + }, + { + "id": 10, + "name": "威尔士亲王", + "search_name": "威尔士亲王", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/10.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E5%B0%94%E5%A3%AB%E4%BA%B2%E7%8E%8B" + }, + { + "id": 11, + "name": "内华达", + "search_name": "内华达", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/11.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%86%85%E5%8D%8E%E8%BE%BE" + }, + { + "id": 12, + "name": "俄克拉荷马", + "search_name": "俄克拉荷马", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/12.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BF%84%E5%85%8B%E6%8B%89%E8%8D%B7%E9%A9%AC" + }, + { + "id": 13, + "name": "安德烈亚·多利亚", + "search_name": "安德烈亚·多利亚", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/13.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E5%BE%B7%E7%83%88%E4%BA%9A%C2%B7%E5%A4%9A%E5%88%A9%E4%BA%9A" + }, + { + "id": 14, + "name": "金刚", + "search_name": "金刚", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/14.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%91%E5%88%9A" + }, + { + "id": 15, + "name": "比睿", + "search_name": "比睿", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/15.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%AF%94%E7%9D%BF" + }, + { + "id": 16, + "name": "榛名", + "search_name": "榛名", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/16.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A6%9B%E5%90%8D" + }, + { + "id": 17, + "name": "雾岛", + "search_name": "雾岛", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/17.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%BE%E5%B2%9B" + }, + { + "id": 18, + "name": "声望", + "search_name": "声望", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/18.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A3%B0%E6%9C%9B" + }, + { + "id": 19, + "name": "反击", + "search_name": "反击", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/19.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%8D%E5%87%BB" + }, + { + "id": 20, + "name": "阿拉斯加", + "search_name": "阿拉斯加", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/20.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E6%8B%89%E6%96%AF%E5%8A%A0" + }, + { + "id": 21, + "name": "关岛", + "search_name": "关岛", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/21.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%B3%E5%B2%9B" + }, + { + "id": 22, + "name": "赤城", + "search_name": "赤城", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/22.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B5%A4%E5%9F%8E" + }, + { + "id": 23, + "name": "加贺", + "search_name": "加贺", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/23.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E8%B4%BA" + }, + { + "id": 24, + "name": "祥凤", + "search_name": "祥凤", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/24.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A5%A5%E5%87%A4" + }, + { + "id": 25, + "name": "瑞凤", + "search_name": "瑞凤", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/25.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%91%9E%E5%87%A4" + }, + { + "id": 26, + "name": "百眼巨人", + "search_name": "百眼巨人", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/26.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%99%BE%E7%9C%BC%E5%B7%A8%E4%BA%BA" + }, + { + "id": 27, + "name": "兰利", + "search_name": "兰利", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/27.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%B0%E5%88%A9" + }, + { + "id": 28, + "name": "突击者", + "search_name": "突击者", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/28.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%AA%81%E5%87%BB%E8%80%85" + }, + { + "id": 29, + "name": "列克星敦(CV-2)", + "search_name": "列克星敦", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/29.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%97%E5%85%8B%E6%98%9F%E6%95%A6(CV-2)" + }, + { + "id": 30, + "name": "萨拉托加", + "search_name": "萨拉托加", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/30.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A8%E6%8B%89%E6%89%98%E5%8A%A0" + }, + { + "id": 31, + "name": "大黄蜂", + "search_name": "大黄蜂", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/31.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E9%BB%84%E8%9C%82" + }, + { + "id": 32, + "name": "高雄", + "search_name": "高雄", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/32.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%AB%98%E9%9B%84" + }, + { + "id": 33, + "name": "爱宕", + "search_name": "爱宕", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/33.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%B1%E5%AE%95" + }, + { + "id": 34, + "name": "摩耶", + "search_name": "摩耶", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/34.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%91%A9%E8%80%B6" + }, + { + "id": 35, + "name": "鸟海", + "search_name": "鸟海", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/35.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B8%9F%E6%B5%B7" + }, + { + "id": 36, + "name": "希佩尔海军上将", + "search_name": "希佩尔海军上将", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/36.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%8C%E4%BD%A9%E5%B0%94%E6%B5%B7%E5%86%9B%E4%B8%8A%E5%B0%86" + }, + { + "id": 37, + "name": "布吕歇尔(重巡洋舰)", + "search_name": "布吕歇尔", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/37.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E5%90%95%E6%AD%87%E5%B0%94(%E9%87%8D%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 38, + "name": "欧根亲王", + "search_name": "欧根亲王", + "variant": "normal", + "rarity": 5, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/38.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%AC%A7%E6%A0%B9%E4%BA%B2%E7%8E%8B" + }, + { + "id": 39, + "name": "威奇塔", + "search_name": "威奇塔", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/39.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E5%A5%87%E5%A1%94" + }, + { + "id": 40, + "name": "昆西", + "search_name": "昆西", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/40.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%98%86%E8%A5%BF" + }, + { + "id": 41, + "name": "天龙", + "search_name": "天龙", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/41.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E9%BE%99" + }, + { + "id": 42, + "name": "龙田", + "search_name": "龙田", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/42.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BE%99%E7%94%B0" + }, + { + "id": 43, + "name": "北上", + "search_name": "北上", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/43.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8C%97%E4%B8%8A" + }, + { + "id": 44, + "name": "大井", + "search_name": "大井", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/44.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E4%BA%95" + }, + { + "id": 45, + "name": "五十铃", + "search_name": "五十铃", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/45.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%94%E5%8D%81%E9%93%83" + }, + { + "id": 46, + "name": "夕张", + "search_name": "夕张", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/46.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%95%E5%BC%A0" + }, + { + "id": 47, + "name": "柯尼斯堡", + "search_name": "柯尼斯堡", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/47.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9F%AF%E5%B0%BC%E6%96%AF%E5%A0%A1" + }, + { + "id": 48, + "name": "卡尔斯鲁厄", + "search_name": "卡尔斯鲁厄", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/48.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E5%B0%94%E6%96%AF%E9%B2%81%E5%8E%84" + }, + { + "id": 49, + "name": "科隆", + "search_name": "科隆", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/49.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E9%9A%86" + }, + { + "id": 50, + "name": "天狼星", + "search_name": "天狼星", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/50.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E7%8B%BC%E6%98%9F" + }, + { + "id": 51, + "name": "林仙", + "search_name": "林仙", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/51.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9E%97%E4%BB%99" + }, + { + "id": 52, + "name": "加拉蒂亚", + "search_name": "加拉蒂亚", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/52.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E6%8B%89%E8%92%82%E4%BA%9A" + }, + { + "id": 53, + "name": "佩内洛珀", + "search_name": "佩内洛珀", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/53.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BD%A9%E5%86%85%E6%B4%9B%E7%8F%80" + }, + { + "id": 54, + "name": "曙光女神", + "search_name": "曙光女神", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/54.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9B%99%E5%85%89%E5%A5%B3%E7%A5%9E" + }, + { + "id": 55, + "name": "奥马哈", + "search_name": "奥马哈", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/55.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E9%A9%AC%E5%93%88" + }, + { + "id": 56, + "name": "亚特兰大", + "search_name": "亚特兰大", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/56.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%9A%E7%89%B9%E5%85%B0%E5%A4%A7" + }, + { + "id": 57, + "name": "朱诺(CL-52)", + "search_name": "朱诺", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/57.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%B1%E8%AF%BA(CL-52)" + }, + { + "id": 58, + "name": "布鲁克林", + "search_name": "布鲁克林", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/58.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E9%B2%81%E5%85%8B%E6%9E%97" + }, + { + "id": 59, + "name": "海伦娜", + "search_name": "海伦娜", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/59.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E4%BC%A6%E5%A8%9C" + }, + { + "id": 60, + "name": "宁海", + "search_name": "宁海", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/60.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%81%E6%B5%B7" + }, + { + "id": 61, + "name": "平海", + "search_name": "平海", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/61.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B9%B3%E6%B5%B7" + }, + { + "id": 62, + "name": "罗伯茨", + "search_name": "罗伯茨", + "variant": "normal", + "rarity": 3, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/62.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BD%97%E4%BC%AF%E8%8C%A8" + }, + { + "id": 63, + "name": "阿贝克隆比", + "search_name": "阿贝克隆比", + "variant": "normal", + "rarity": 3, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/63.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E8%B4%9D%E5%85%8B%E9%9A%86%E6%AF%94" + }, + { + "id": 64, + "name": "吹雪", + "search_name": "吹雪", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/64.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%B9%E9%9B%AA" + }, + { + "id": 65, + "name": "白雪", + "search_name": "白雪", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/65.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%99%BD%E9%9B%AA" + }, + { + "id": 66, + "name": "初雪", + "search_name": "初雪", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/66.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%9D%E9%9B%AA" + }, + { + "id": 67, + "name": "深雪", + "search_name": "深雪", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/67.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B7%B1%E9%9B%AA" + }, + { + "id": 68, + "name": "晓", + "search_name": "晓", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/68.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%99%93" + }, + { + "id": 69, + "name": "响", + "search_name": "响", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/69.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%8D" + }, + { + "id": 70, + "name": "雷", + "search_name": "雷", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/70.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%B7" + }, + { + "id": 71, + "name": "电", + "search_name": "电", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/71.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%94%B5" + }, + { + "id": 72, + "name": "绫波", + "search_name": "绫波", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/72.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%AB%E6%B3%A2" + }, + { + "id": 73, + "name": "敷波", + "search_name": "敷波", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/73.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%95%B7%E6%B3%A2" + }, + { + "id": 74, + "name": "Z1", + "search_name": "Z1", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/74.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z1" + }, + { + "id": 75, + "name": "Z16", + "search_name": "Z16", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/75.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z16" + }, + { + "id": 76, + "name": "Z21", + "search_name": "Z21", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/76.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z21" + }, + { + "id": 77, + "name": "Z22", + "search_name": "Z22", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/77.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z22" + }, + { + "id": 78, + "name": "Z24", + "search_name": "Z24", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/78.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z24" + }, + { + "id": 79, + "name": "Z28", + "search_name": "Z28", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/79.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z28" + }, + { + "id": 80, + "name": "Z31", + "search_name": "Z31", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/80.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z31" + }, + { + "id": 81, + "name": "紫石英", + "search_name": "紫石英", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/81.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B4%AB%E7%9F%B3%E8%8B%B1" + }, + { + "id": 82, + "name": "萤火虫", + "search_name": "萤火虫", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/82.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A4%E7%81%AB%E8%99%AB" + }, + { + "id": 83, + "name": "标枪", + "search_name": "标枪", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/83.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A0%87%E6%9E%AA" + }, + { + "id": 84, + "name": "天后", + "search_name": "天后", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/84.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E5%90%8E" + }, + { + "id": 85, + "name": "黑背豺", + "search_name": "黑背豺", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/85.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%91%E8%83%8C%E8%B1%BA" + }, + { + "id": 86, + "name": "哥萨克人", + "search_name": "哥萨克人", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/86.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%A5%E8%90%A8%E5%85%8B%E4%BA%BA" + }, + { + "id": 87, + "name": "爱斯基摩人", + "search_name": "爱斯基摩人", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/87.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%B1%E6%96%AF%E5%9F%BA%E6%91%A9%E4%BA%BA" + }, + { + "id": 88, + "name": "旁遮普人", + "search_name": "旁遮普人", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/88.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%81%E9%81%AE%E6%99%AE%E4%BA%BA" + }, + { + "id": 89, + "name": "弗莱彻", + "search_name": "弗莱彻", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/89.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BC%97%E8%8E%B1%E5%BD%BB" + }, + { + "id": 90, + "name": "卡辛杨", + "search_name": "卡辛杨", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/90.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E8%BE%9B%E6%9D%A8" + }, + { + "id": 91, + "name": "安东尼", + "search_name": "安东尼", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/91.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E4%B8%9C%E5%B0%BC" + }, + { + "id": 92, + "name": "布雷恩", + "search_name": "布雷恩", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/92.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E9%9B%B7%E6%81%A9" + }, + { + "id": 93, + "name": "基林", + "search_name": "基林", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/93.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E6%9E%97" + }, + { + "id": 94, + "name": "基阿特", + "search_name": "基阿特", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/94.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E9%98%BF%E7%89%B9" + }, + { + "id": 95, + "name": "弗兰克·诺克斯", + "search_name": "弗兰克·诺克斯", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/95.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BC%97%E5%85%B0%E5%85%8B%C2%B7%E8%AF%BA%E5%85%8B%E6%96%AF" + }, + { + "id": 96, + "name": "鲍尔", + "search_name": "鲍尔", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/96.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B2%8D%E5%B0%94" + }, + { + "id": 97, + "name": "果敢", + "search_name": "果敢", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/97.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9E%9C%E6%95%A2" + }, + { + "id": 98, + "name": "沃克兰", + "search_name": "沃克兰", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/98.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B2%83%E5%85%8B%E5%85%B0" + }, + { + "id": 99, + "name": "空想", + "search_name": "空想", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/99.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A9%BA%E6%83%B3" + }, + { + "id": 100, + "name": "狮(战列舰)", + "search_name": "狮", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/100.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8B%AE(%E6%88%98%E5%88%97%E8%88%B0)" + }, + { + "id": 101, + "name": "长门", + "search_name": "长门", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/101.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%95%BF%E9%97%A8" + }, + { + "id": 102, + "name": "陆奥", + "search_name": "陆奥", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/102.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%99%86%E5%A5%A5" + }, + { + "id": 103, + "name": "皇家橡树", + "search_name": "皇家橡树", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/103.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9A%87%E5%AE%B6%E6%A9%A1%E6%A0%91" + }, + { + "id": 104, + "name": "厌战", + "search_name": "厌战", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/104.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8E%8C%E6%88%98" + }, + { + "id": 105, + "name": "前卫", + "search_name": "前卫", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/105.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%89%8D%E5%8D%AB" + }, + { + "id": 106, + "name": "田纳西", + "search_name": "田纳西", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/106.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%94%B0%E7%BA%B3%E8%A5%BF" + }, + { + "id": 107, + "name": "加利福尼亚", + "search_name": "加利福尼亚", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/107.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E5%88%A9%E7%A6%8F%E5%B0%BC%E4%BA%9A" + }, + { + "id": 108, + "name": "科罗拉多", + "search_name": "科罗拉多", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/108.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E7%BD%97%E6%8B%89%E5%A4%9A" + }, + { + "id": 109, + "name": "马里兰", + "search_name": "马里兰", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/109.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E9%87%8C%E5%85%B0" + }, + { + "id": 110, + "name": "西弗吉尼亚", + "search_name": "西弗吉尼亚", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/110.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%A5%BF%E5%BC%97%E5%90%89%E5%B0%BC%E4%BA%9A" + }, + { + "id": 111, + "name": "华盛顿", + "search_name": "华盛顿", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/111.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%8E%E7%9B%9B%E9%A1%BF" + }, + { + "id": 112, + "name": "维托里奥·维内托", + "search_name": "维托里奥·维内托", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/112.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%B4%E6%89%98%E9%87%8C%E5%A5%A5%C2%B7%E7%BB%B4%E5%86%85%E6%89%98" + }, + { + "id": 113, + "name": "黎塞留", + "search_name": "黎塞留", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/113.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%8E%E5%A1%9E%E7%95%99" + }, + { + "id": 114, + "name": "沙恩霍斯特", + "search_name": "沙恩霍斯特", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/114.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B2%99%E6%81%A9%E9%9C%8D%E6%96%AF%E7%89%B9" + }, + { + "id": 115, + "name": "格奈森瑙", + "search_name": "格奈森瑙", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/115.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A0%BC%E5%A5%88%E6%A3%AE%E7%91%99" + }, + { + "id": 117, + "name": "大凤", + "search_name": "大凤", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/117.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E5%87%A4" + }, + { + "id": 118, + "name": "齐柏林伯爵", + "search_name": "齐柏林伯爵", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/118.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BD%90%E6%9F%8F%E6%9E%97%E4%BC%AF%E7%88%B5" + }, + { + "id": 119, + "name": "皇家方舟(91)", + "search_name": "皇家方舟", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/119.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9A%87%E5%AE%B6%E6%96%B9%E8%88%9F(91)" + }, + { + "id": 120, + "name": "约克城", + "search_name": "约克城", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/120.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E5%85%8B%E5%9F%8E" + }, + { + "id": 121, + "name": "企业", + "search_name": "企业", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/121.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%81%E4%B8%9A" + }, + { + "id": 122, + "name": "龙骧", + "search_name": "龙骧", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/122.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BE%99%E9%AA%A7" + }, + { + "id": 123, + "name": "博格", + "search_name": "博格", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/123.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%9A%E6%A0%BC" + }, + { + "id": 124, + "name": "追赶者", + "search_name": "追赶者", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/124.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%BD%E8%B5%B6%E8%80%85" + }, + { + "id": 125, + "name": "巨像", + "search_name": "巨像", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/125.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%A8%E5%83%8F" + }, + { + "id": 126, + "name": "普林斯顿", + "search_name": "普林斯顿", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/126.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%99%AE%E6%9E%97%E6%96%AF%E9%A1%BF" + }, + { + "id": 127, + "name": "独角兽", + "search_name": "独角兽", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/127.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8B%AC%E8%A7%92%E5%85%BD" + }, + { + "id": 128, + "name": "德意志(装甲舰)", + "search_name": "德意志", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/128.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E6%84%8F%E5%BF%97(%E8%A3%85%E7%94%B2%E8%88%B0)" + }, + { + "id": 129, + "name": "舍尔海军上将", + "search_name": "舍尔海军上将", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/129.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%88%8D%E5%B0%94%E6%B5%B7%E5%86%9B%E4%B8%8A%E5%B0%86" + }, + { + "id": 130, + "name": "斯佩伯爵海军上将", + "search_name": "斯佩伯爵海军上将", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/130.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E4%BD%A9%E4%BC%AF%E7%88%B5%E6%B5%B7%E5%86%9B%E4%B8%8A%E5%B0%86" + }, + { + "id": 131, + "name": "古鹰", + "search_name": "古鹰", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/131.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%A4%E9%B9%B0" + }, + { + "id": 132, + "name": "加古", + "search_name": "加古", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/132.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E5%8F%A4" + }, + { + "id": 133, + "name": "青叶", + "search_name": "青叶", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/133.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9D%92%E5%8F%B6" + }, + { + "id": 134, + "name": "衣笠", + "search_name": "衣笠", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/134.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%A1%A3%E7%AC%A0" + }, + { + "id": 135, + "name": "伦敦", + "search_name": "伦敦", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/135.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%A6%E6%95%A6" + }, + { + "id": 136, + "name": "肯特", + "search_name": "肯特", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/136.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%82%AF%E7%89%B9" + }, + { + "id": 137, + "name": "波特兰", + "search_name": "波特兰", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/137.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E7%89%B9%E5%85%B0" + }, + { + "id": 138, + "name": "印第安纳波利斯", + "search_name": "印第安纳波利斯", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/138.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%B0%E7%AC%AC%E5%AE%89%E7%BA%B3%E6%B3%A2%E5%88%A9%E6%96%AF" + }, + { + "id": 139, + "name": "彭萨科拉", + "search_name": "彭萨科拉", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/139.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BD%AD%E8%90%A8%E7%A7%91%E6%8B%89" + }, + { + "id": 140, + "name": "盐湖城", + "search_name": "盐湖城", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/140.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9B%90%E6%B9%96%E5%9F%8E" + }, + { + "id": 141, + "name": "北安普顿", + "search_name": "北安普顿", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/141.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8C%97%E5%AE%89%E6%99%AE%E9%A1%BF" + }, + { + "id": 142, + "name": "休斯顿", + "search_name": "休斯顿", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/142.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%91%E6%96%AF%E9%A1%BF" + }, + { + "id": 143, + "name": "新奥尔良", + "search_name": "新奥尔良", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/143.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%B0%E5%A5%A5%E5%B0%94%E8%89%AF" + }, + { + "id": 144, + "name": "基洛夫", + "search_name": "基洛夫", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/144.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E6%B4%9B%E5%A4%AB" + }, + { + "id": 145, + "name": "川内", + "search_name": "川内", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/145.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%9D%E5%86%85" + }, + { + "id": 146, + "name": "神通", + "search_name": "神通", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/146.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A5%9E%E9%80%9A" + }, + { + "id": 147, + "name": "那珂", + "search_name": "那珂", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/147.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%82%A3%E7%8F%82" + }, + { + "id": 148, + "name": "M计划", + "search_name": "M计划", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/148.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/M%E8%AE%A1%E5%88%92" + }, + { + "id": 149, + "name": "埃姆登", + "search_name": "埃姆登", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/149.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%83%E5%A7%86%E7%99%BB" + }, + { + "id": 150, + "name": "翡翠", + "search_name": "翡翠", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/150.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BF%A1%E7%BF%A0" + }, + { + "id": 151, + "name": "进取", + "search_name": "进取", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/151.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%9B%E5%8F%96" + }, + { + "id": 152, + "name": "爱丁堡", + "search_name": "爱丁堡", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/152.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%B1%E4%B8%81%E5%A0%A1" + }, + { + "id": 153, + "name": "贝尔法斯特", + "search_name": "贝尔法斯特", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/153.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B4%9D%E5%B0%94%E6%B3%95%E6%96%AF%E7%89%B9" + }, + { + "id": 154, + "name": "勇敢", + "search_name": "勇敢", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/154.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8B%87%E6%95%A2" + }, + { + "id": 155, + "name": "光荣(大型轻巡洋舰)", + "search_name": "光荣", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/155.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%89%E8%8D%A3(%E5%A4%A7%E5%9E%8B%E8%BD%BB%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 156, + "name": "暴怒", + "search_name": "暴怒", + "variant": "normal", + "rarity": 4, + "ship_type": "bbv", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/156.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bbv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9A%B4%E6%80%92" + }, + { + "id": 157, + "name": "圣地亚哥", + "search_name": "圣地亚哥", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/157.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E5%9C%B0%E4%BA%9A%E5%93%A5" + }, + { + "id": 158, + "name": "圣胡安", + "search_name": "圣胡安", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/158.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E8%83%A1%E5%AE%89" + }, + { + "id": 159, + "name": "索莫斯", + "search_name": "索莫斯", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/159.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B4%A2%E8%8E%AB%E6%96%AF" + }, + { + "id": 160, + "name": "摩尔曼斯克", + "search_name": "摩尔曼斯克", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/160.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%91%A9%E5%B0%94%E6%9B%BC%E6%96%AF%E5%85%8B" + }, + { + "id": 161, + "name": "德·鲁伊特", + "search_name": "德·鲁伊特", + "variant": "normal", + "rarity": 2, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/161.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%C2%B7%E9%B2%81%E4%BC%8A%E7%89%B9" + }, + { + "id": 162, + "name": "逸仙", + "search_name": "逸仙", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/162.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%80%B8%E4%BB%99" + }, + { + "id": 163, + "name": "维那莫依嫩", + "search_name": "维那莫依嫩", + "variant": "normal", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/163.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%B4%E9%82%A3%E8%8E%AB%E4%BE%9D%E5%AB%A9" + }, + { + "id": 164, + "name": "秋月", + "search_name": "秋月", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/164.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%8B%E6%9C%88" + }, + { + "id": 165, + "name": "凉月", + "search_name": "凉月", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/165.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%89%E6%9C%88" + }, + { + "id": 166, + "name": "阳炎", + "search_name": "阳炎", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/166.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%B3%E7%82%8E" + }, + { + "id": 167, + "name": "不知火", + "search_name": "不知火", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/167.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E7%9F%A5%E7%81%AB" + }, + { + "id": 168, + "name": "黑潮", + "search_name": "黑潮", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/168.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%91%E6%BD%AE" + }, + { + "id": 169, + "name": "雪风", + "search_name": "雪风", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/169.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%AA%E9%A3%8E" + }, + { + "id": 170, + "name": "Z3", + "search_name": "Z3", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/170.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z3" + }, + { + "id": 171, + "name": "Z46", + "search_name": "Z46", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/171.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z46" + }, + { + "id": 172, + "name": "热心", + "search_name": "热心", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/172.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%83%AD%E5%BF%83" + }, + { + "id": 173, + "name": "阿卡司塔", + "search_name": "阿卡司塔", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/173.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%8D%A1%E5%8F%B8%E5%A1%94" + }, + { + "id": 174, + "name": "索玛雷兹", + "search_name": "索玛雷兹", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/174.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B4%A2%E7%8E%9B%E9%9B%B7%E5%85%B9" + }, + { + "id": 175, + "name": "维纳斯", + "search_name": "维纳斯", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/175.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%B4%E7%BA%B3%E6%96%AF" + }, + { + "id": 176, + "name": "军团", + "search_name": "军团", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/176.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%86%9B%E5%9B%A2" + }, + { + "id": 177, + "name": "撒切尔", + "search_name": "撒切尔", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/177.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%92%92%E5%88%87%E5%B0%94" + }, + { + "id": 178, + "name": "沙利文", + "search_name": "沙利文", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/178.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B2%99%E5%88%A9%E6%96%87" + }, + { + "id": 179, + "name": "西格斯比", + "search_name": "西格斯比", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/179.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%A5%BF%E6%A0%BC%E6%96%AF%E6%AF%94" + }, + { + "id": 180, + "name": "奥班农", + "search_name": "奥班农", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/180.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E7%8F%AD%E5%86%9C" + }, + { + "id": 181, + "name": "威廉·D·波特", + "search_name": "威廉·D·波特", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/181.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E5%BB%89%C2%B7D%C2%B7%E6%B3%A2%E7%89%B9" + }, + { + "id": 182, + "name": "波特", + "search_name": "波特", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/182.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E7%89%B9" + }, + { + "id": 183, + "name": "拉菲(DD-724)", + "search_name": "拉菲", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/183.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8B%89%E8%8F%B2(DD-724)" + }, + { + "id": 184, + "name": "拉菲(DD-459)", + "search_name": "拉菲", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/184.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8B%89%E8%8F%B2(DD-459)" + }, + { + "id": 185, + "name": "卡米契亚·内拉", + "search_name": "卡米契亚·内拉", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/185.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E7%B1%B3%E5%A5%91%E4%BA%9A%C2%B7%E5%86%85%E6%8B%89" + }, + { + "id": 186, + "name": "阿维埃尔", + "search_name": "阿维埃尔", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/186.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E7%BB%B4%E5%9F%83%E5%B0%94" + }, + { + "id": 187, + "name": "安东尼奥·达诺利", + "search_name": "安东尼奥·达诺利", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/187.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E4%B8%9C%E5%B0%BC%E5%A5%A5%C2%B7%E8%BE%BE%E8%AF%BA%E5%88%A9" + }, + { + "id": 188, + "name": "乌戈里尼·维瓦尔迪", + "search_name": "乌戈里尼·维瓦尔迪", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/188.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B9%8C%E6%88%88%E9%87%8C%E5%B0%BC%C2%B7%E7%BB%B4%E7%93%A6%E5%B0%94%E8%BF%AA" + }, + { + "id": 189, + "name": "天城", + "search_name": "天城", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/189.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E5%9F%8E" + }, + { + "id": 190, + "name": "土佐", + "search_name": "土佐", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/190.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%9F%E4%BD%90" + }, + { + "id": 191, + "name": "A150", + "search_name": "A150", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/191.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/A150" + }, + { + "id": 192, + "name": "塔什干", + "search_name": "塔什干", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/192.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%94%E4%BB%80%E5%B9%B2" + }, + { + "id": 193, + "name": "恩格斯", + "search_name": "恩格斯", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/193.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%81%A9%E6%A0%BC%E6%96%AF" + }, + { + "id": 194, + "name": "大青花鱼", + "search_name": "大青花鱼", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/194.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E9%9D%92%E8%8A%B1%E9%B1%BC" + }, + { + "id": 195, + "name": "射水鱼", + "search_name": "射水鱼", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/195.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B0%84%E6%B0%B4%E9%B1%BC" + }, + { + "id": 196, + "name": "M1", + "search_name": "M1", + "variant": "normal", + "rarity": 3, + "ship_type": "sc", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/196.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/sc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/M1" + }, + { + "id": 197, + "name": "U-47", + "search_name": "U-47", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/197.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-47" + }, + { + "id": 198, + "name": "U-505", + "search_name": "U-505", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/198.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-505" + }, + { + "id": 199, + "name": "絮库夫", + "search_name": "絮库夫", + "variant": "normal", + "rarity": 3, + "ship_type": "sc", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/199.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/sc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B5%AE%E5%BA%93%E5%A4%AB" + }, + { + "id": 200, + "name": "兴登堡", + "search_name": "兴登堡", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/200.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%B4%E7%99%BB%E5%A0%A1" + }, + { + "id": 201, + "name": "V方案", + "search_name": "V方案", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/201.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/V%E6%96%B9%E6%A1%88" + }, + { + "id": 202, + "name": "伊利诺伊", + "search_name": "伊利诺伊", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/202.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E5%88%A9%E8%AF%BA%E4%BC%8A" + }, + { + "id": 203, + "name": "伊丽莎白女王(00)", + "search_name": "伊丽莎白女王", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/203.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E4%B8%BD%E8%8E%8E%E7%99%BD%E5%A5%B3%E7%8E%8B(00)" + }, + { + "id": 205, + "name": "约克公爵", + "search_name": "约克公爵", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/205.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E5%85%8B%E5%85%AC%E7%88%B5" + }, + { + "id": 206, + "name": "北卡罗来纳", + "search_name": "北卡罗来纳", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/206.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8C%97%E5%8D%A1%E7%BD%97%E6%9D%A5%E7%BA%B3" + }, + { + "id": 207, + "name": "南达科他(BB-57)", + "search_name": "南达科他", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/207.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%97%E8%BE%BE%E7%A7%91%E4%BB%96(BB-57)" + }, + { + "id": 208, + "name": "马萨诸塞", + "search_name": "马萨诸塞", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/208.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E8%90%A8%E8%AF%B8%E5%A1%9E" + }, + { + "id": 209, + "name": "密苏里", + "search_name": "密苏里", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/209.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AF%86%E8%8B%8F%E9%87%8C" + }, + { + "id": 210, + "name": "衣阿华", + "search_name": "衣阿华", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/210.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%A1%A3%E9%98%BF%E5%8D%8E" + }, + { + "id": 211, + "name": "卡约·杜伊里奥", + "search_name": "卡约·杜伊里奥", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/211.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E7%BA%A6%C2%B7%E6%9D%9C%E4%BC%8A%E9%87%8C%E5%A5%A5" + }, + { + "id": 212, + "name": "利托里奥", + "search_name": "利托里奥", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/212.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%A9%E6%89%98%E9%87%8C%E5%A5%A5" + }, + { + "id": 213, + "name": "罗马", + "search_name": "罗马", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/213.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BD%97%E9%A9%AC" + }, + { + "id": 214, + "name": "苏联", + "search_name": "苏联", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/214.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%8F%E8%81%94" + }, + { + "id": 215, + "name": "玛丽亚皇后", + "search_name": "玛丽亚皇后", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/215.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8E%9B%E4%B8%BD%E4%BA%9A%E7%9A%87%E5%90%8E" + }, + { + "id": 216, + "name": "敦刻尔克", + "search_name": "敦刻尔克", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/216.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%95%A6%E5%88%BB%E5%B0%94%E5%85%8B" + }, + { + "id": 217, + "name": "斯特拉斯堡", + "search_name": "斯特拉斯堡", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/217.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E7%89%B9%E6%8B%89%E6%96%AF%E5%A0%A1" + }, + { + "id": 218, + "name": "古斯塔夫五世", + "search_name": "古斯塔夫五世", + "variant": "normal", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/218.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%A4%E6%96%AF%E5%A1%94%E5%A4%AB%E4%BA%94%E4%B8%96" + }, + { + "id": 219, + "name": "瑞鹤", + "search_name": "瑞鹤", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/219.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%91%9E%E9%B9%A4" + }, + { + "id": 220, + "name": "翔鹤", + "search_name": "翔鹤", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/220.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BF%94%E9%B9%A4" + }, + { + "id": 221, + "name": "飞龙", + "search_name": "飞龙", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/221.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A3%9E%E9%BE%99" + }, + { + "id": 222, + "name": "苍龙", + "search_name": "苍龙", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/222.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%8D%E9%BE%99" + }, + { + "id": 223, + "name": "信浓", + "search_name": "信浓", + "variant": "normal", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/223.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BF%A1%E6%B5%93" + }, + { + "id": 224, + "name": "可畏", + "search_name": "可畏", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/224.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%AF%E7%95%8F" + }, + { + "id": 225, + "name": "黄蜂(CV-7)", + "search_name": "黄蜂", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/225.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%84%E8%9C%82(CV-7)" + }, + { + "id": 226, + "name": "埃塞克斯", + "search_name": "埃塞克斯", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/226.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%83%E5%A1%9E%E5%85%8B%E6%96%AF" + }, + { + "id": 227, + "name": "飞鹰", + "search_name": "飞鹰", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/227.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A3%9E%E9%B9%B0" + }, + { + "id": 228, + "name": "隼鹰", + "search_name": "隼鹰", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/228.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9A%BC%E9%B9%B0" + }, + { + "id": 229, + "name": "凤翔", + "search_name": "凤翔", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/229.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%A4%E7%BF%94" + }, + { + "id": 230, + "name": "竞技神", + "search_name": "竞技神", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/230.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%AB%9E%E6%8A%80%E7%A5%9E" + }, + { + "id": 231, + "name": "伦道夫", + "search_name": "伦道夫", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/231.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%A6%E9%81%93%E5%A4%AB" + }, + { + "id": 232, + "name": "贝亚恩", + "search_name": "贝亚恩", + "variant": "normal", + "rarity": 3, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/232.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B4%9D%E4%BA%9A%E6%81%A9" + }, + { + "id": 233, + "name": "最上", + "search_name": "最上", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/233.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%80%E4%B8%8A" + }, + { + "id": 234, + "name": "三隈", + "search_name": "三隈", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/234.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%89%E9%9A%88" + }, + { + "id": 235, + "name": "铃谷", + "search_name": "铃谷", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/235.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%93%83%E8%B0%B7" + }, + { + "id": 236, + "name": "熊野", + "search_name": "熊野", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/236.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%86%8A%E9%87%8E" + }, + { + "id": 237, + "name": "萨福克", + "search_name": "萨福克", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/237.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A8%E7%A6%8F%E5%85%8B" + }, + { + "id": 238, + "name": "约克", + "search_name": "约克", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/238.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E5%85%8B" + }, + { + "id": 239, + "name": "埃克赛特", + "search_name": "埃克赛特", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/239.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%83%E5%85%8B%E8%B5%9B%E7%89%B9" + }, + { + "id": 240, + "name": "旧金山", + "search_name": "旧金山", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/240.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A7%E9%87%91%E5%B1%B1" + }, + { + "id": 241, + "name": "巴尔的摩", + "search_name": "巴尔的摩", + "variant": "normal", + "rarity": 5, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/241.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%B4%E5%B0%94%E7%9A%84%E6%91%A9" + }, + { + "id": 242, + "name": "卡德", + "search_name": "卡德", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/242.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E5%BE%B7" + }, + { + "id": 243, + "name": "德梅因", + "search_name": "德梅因", + "variant": "normal", + "rarity": 6, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/243.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E6%A2%85%E5%9B%A0" + }, + { + "id": 244, + "name": "灵敏", + "search_name": "灵敏", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/244.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%81%B5%E6%95%8F" + }, + { + "id": 245, + "name": "扎拉", + "search_name": "扎拉", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/245.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%89%8E%E6%8B%89" + }, + { + "id": 246, + "name": "阿尔及利亚", + "search_name": "阿尔及利亚", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/246.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B0%94%E5%8F%8A%E5%88%A9%E4%BA%9A" + }, + { + "id": 247, + "name": "香取", + "search_name": "香取", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/247.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A6%99%E5%8F%96" + }, + { + "id": 248, + "name": "大淀", + "search_name": "大淀", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/248.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E6%B7%80" + }, + { + "id": 249, + "name": "莱比锡", + "search_name": "莱比锡", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/249.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8E%B1%E6%AF%94%E9%94%A1" + }, + { + "id": 250, + "name": "伍斯特", + "search_name": "伍斯特", + "variant": "normal", + "rarity": 6, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/250.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8D%E6%96%AF%E7%89%B9" + }, + { + "id": 251, + "name": "奥克兰", + "search_name": "奥克兰", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/251.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E5%85%8B%E5%85%B0" + }, + { + "id": 252, + "name": "克利夫兰", + "search_name": "克利夫兰", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/252.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E5%88%A9%E5%A4%AB%E5%85%B0" + }, + { + "id": 253, + "name": "应瑞", + "search_name": "应瑞", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/253.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%94%E7%91%9E" + }, + { + "id": 254, + "name": "肇和", + "search_name": "肇和", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/254.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%82%87%E5%92%8C" + }, + { + "id": 255, + "name": "加罗林", + "search_name": "加罗林", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/255.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E7%BD%97%E6%9E%97" + }, + { + "id": 256, + "name": "布莱克", + "search_name": "布莱克", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/256.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E8%8E%B1%E5%85%8B" + }, + { + "id": 257, + "name": "阿尔比翁", + "search_name": "阿尔比翁", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/257.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B0%94%E6%AF%94%E7%BF%81" + }, + { + "id": 258, + "name": "高波", + "search_name": "高波", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/258.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%AB%98%E6%B3%A2" + }, + { + "id": 259, + "name": "吞武里", + "search_name": "吞武里", + "variant": "normal", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/259.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%9E%E6%AD%A6%E9%87%8C" + }, + { + "id": 260, + "name": "睦月", + "search_name": "睦月", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/260.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9D%A6%E6%9C%88" + }, + { + "id": 261, + "name": "白露", + "search_name": "白露", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/261.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%99%BD%E9%9C%B2" + }, + { + "id": 262, + "name": "时雨", + "search_name": "时雨", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/262.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%B6%E9%9B%A8" + }, + { + "id": 263, + "name": "村雨", + "search_name": "村雨", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/263.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9D%91%E9%9B%A8" + }, + { + "id": 264, + "name": "夕立", + "search_name": "夕立", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/264.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%95%E7%AB%8B" + }, + { + "id": 265, + "name": "岚", + "search_name": "岚", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/265.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B2%9A" + }, + { + "id": 266, + "name": "初夏", + "search_name": "初夏", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/266.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%9D%E5%A4%8F" + }, + { + "id": 267, + "name": "岛风(岛风型驱逐舰)", + "search_name": "岛风", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/267.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B2%9B%E9%A3%8E(%E5%B2%9B%E9%A3%8E%E5%9E%8B%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 268, + "name": "峰风", + "search_name": "峰风", + "variant": "normal", + "rarity": 1, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/268.webp", + "background": "assets/backgrounds/rarity-1.webp", + "frame": "assets/frames/rarity-1.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B3%B0%E9%A3%8E" + }, + { + "id": 269, + "name": "Z17", + "search_name": "Z17", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/269.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z17" + }, + { + "id": 270, + "name": "Z18", + "search_name": "Z18", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/270.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z18" + }, + { + "id": 271, + "name": "吸血鬼", + "search_name": "吸血鬼", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/271.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%B8%E8%A1%80%E9%AC%BC" + }, + { + "id": 272, + "name": "海达人", + "search_name": "海达人", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/272.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E8%BE%BE%E4%BA%BA" + }, + { + "id": 273, + "name": "塞尔弗里奇", + "search_name": "塞尔弗里奇", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/273.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E5%B0%94%E5%BC%97%E9%87%8C%E5%A5%87" + }, + { + "id": 274, + "name": "X1", + "search_name": "X1", + "variant": "normal", + "rarity": 3, + "ship_type": "sc", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/274.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/sc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/X1" + }, + { + "id": 275, + "name": "康弗斯", + "search_name": "康弗斯", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/275.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%B7%E5%BC%97%E6%96%AF" + }, + { + "id": 276, + "name": "戴森", + "search_name": "戴森", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/276.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%88%B4%E6%A3%AE" + }, + { + "id": 277, + "name": "克拉克斯顿", + "search_name": "克拉克斯顿", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/277.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E6%8B%89%E5%85%8B%E6%96%AF%E9%A1%BF" + }, + { + "id": 278, + "name": "霍埃尔", + "search_name": "霍埃尔", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/278.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9C%8D%E5%9F%83%E5%B0%94" + }, + { + "id": 279, + "name": "塞缪尔·B·罗伯茨", + "search_name": "塞缪尔·B·罗伯茨", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/279.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E7%BC%AA%E5%B0%94%C2%B7B%C2%B7%E7%BD%97%E4%BC%AF%E8%8C%A8" + }, + { + "id": 280, + "name": "约翰斯顿", + "search_name": "约翰斯顿", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/280.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E7%BF%B0%E6%96%AF%E9%A1%BF" + }, + { + "id": 281, + "name": "雷鸣", + "search_name": "雷鸣", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/281.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%B7%E9%B8%A3" + }, + { + "id": 282, + "name": "潜甲", + "search_name": "潜甲", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/282.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%BD%9C%E7%94%B2" + }, + { + "id": 283, + "name": "潜乙", + "search_name": "潜乙", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/283.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%BD%9C%E4%B9%99" + }, + { + "id": 284, + "name": "布吕歇尔(装甲巡洋舰)", + "search_name": "布吕歇尔", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/284.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E5%90%95%E6%AD%87%E5%B0%94(%E8%A3%85%E7%94%B2%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 285, + "name": "85工程", + "search_name": "85工程", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/285.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/85%E5%B7%A5%E7%A8%8B" + }, + { + "id": 286, + "name": "不挠(战列巡洋舰)", + "search_name": "不挠", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/286.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E6%8C%A0(%E6%88%98%E5%88%97%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 287, + "name": "鹰潭", + "search_name": "鹰潭", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/287.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B9%B0%E6%BD%AD" + }, + { + "id": 288, + "name": "U-552", + "search_name": "U-552", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/288.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-552" + }, + { + "id": 289, + "name": "U-81", + "search_name": "U-81", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/289.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-81" + }, + { + "id": 290, + "name": "U-96", + "search_name": "U-96", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/290.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-96" + }, + { + "id": 291, + "name": "U-2540", + "search_name": "U-2540", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/291.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-2540" + }, + { + "id": 292, + "name": "U-156", + "search_name": "U-156", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/292.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-156" + }, + { + "id": 293, + "name": "U-1206", + "search_name": "U-1206", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/293.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-1206" + }, + { + "id": 294, + "name": "刺尾鱼", + "search_name": "刺尾鱼", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/294.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%BA%E5%B0%BE%E9%B1%BC" + }, + { + "id": 295, + "name": "S-56", + "search_name": "S-56", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/295.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/S-56" + }, + { + "id": 296, + "name": "贝劳伍德", + "search_name": "贝劳伍德", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/296.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B4%9D%E5%8A%B3%E4%BC%8D%E5%BE%B7" + }, + { + "id": 297, + "name": "乔治·埃夫洛夫", + "search_name": "乔治·埃夫洛夫", + "variant": "normal", + "rarity": 5, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/297.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B9%94%E6%B2%BB%C2%B7%E5%9F%83%E5%A4%AB%E6%B4%9B%E5%A4%AB" + }, + { + "id": 298, + "name": "苏赫巴托尔", + "search_name": "苏赫巴托尔", + "variant": "normal", + "rarity": 6, + "ship_type": "ap", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/298.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ap.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%8F%E8%B5%AB%E5%B7%B4%E6%89%98%E5%B0%94" + }, + { + "id": 299, + "name": "让巴尔", + "search_name": "让巴尔", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/299.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%AE%A9%E5%B7%B4%E5%B0%94" + }, + { + "id": 300, + "name": "马耳他", + "search_name": "马耳他", + "variant": "normal", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/300.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E8%80%B3%E4%BB%96" + }, + { + "id": 301, + "name": "马汉", + "search_name": "马汉", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/301.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E6%B1%89" + }, + { + "id": 302, + "name": "圣路易斯", + "search_name": "圣路易斯", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/302.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E8%B7%AF%E6%98%93%E6%96%AF" + }, + { + "id": 303, + "name": "奥丁", + "search_name": "奥丁", + "variant": "normal", + "rarity": 5, + "ship_type": "ap", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/303.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ap.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E4%B8%81" + }, + { + "id": 304, + "name": "圣女贞德", + "search_name": "圣女贞德", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/304.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E5%A5%B3%E8%B4%9E%E5%BE%B7" + }, + { + "id": 305, + "name": "英王乔治五世", + "search_name": "英王乔治五世", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/305.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%B1%E7%8E%8B%E4%B9%94%E6%B2%BB%E4%BA%94%E4%B8%96" + }, + { + "id": 306, + "name": "巴夫勒尔", + "search_name": "巴夫勒尔", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/306.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%B4%E5%A4%AB%E5%8B%92%E5%B0%94" + }, + { + "id": 307, + "name": "女灶神", + "search_name": "女灶神", + "variant": "normal", + "rarity": 5, + "ship_type": "ap", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/307.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ap.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%B3%E7%81%B6%E7%A5%9E" + }, + { + "id": 308, + "name": "哥特兰", + "search_name": "哥特兰", + "variant": "normal", + "rarity": 4, + "ship_type": "cav", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/308.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cav.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%A5%E7%89%B9%E5%85%B0" + }, + { + "id": 309, + "name": "诺夫哥罗德", + "search_name": "诺夫哥罗德", + "variant": "normal", + "rarity": 6, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/309.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%AF%BA%E5%A4%AB%E5%93%A5%E7%BD%97%E5%BE%B7" + }, + { + "id": 310, + "name": "库欣", + "search_name": "库欣", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/310.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%93%E6%AC%A3" + }, + { + "id": 311, + "name": "瓜达卡纳尔", + "search_name": "瓜达卡纳尔", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/311.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%93%9C%E8%BE%BE%E5%8D%A1%E7%BA%B3%E5%B0%94" + }, + { + "id": 312, + "name": "纽伦堡", + "search_name": "纽伦堡", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/312.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%BD%E4%BC%A6%E5%A0%A1" + }, + { + "id": 313, + "name": "阿贾克斯", + "search_name": "阿贾克斯", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/313.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E8%B4%BE%E5%85%8B%E6%96%AF" + }, + { + "id": 314, + "name": "哥特雄狮", + "search_name": "哥特雄狮", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/314.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%A5%E7%89%B9%E9%9B%84%E7%8B%AE" + }, + { + "id": 315, + "name": "宵月", + "search_name": "宵月", + "variant": "normal", + "rarity": 2, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/315.webp", + "background": "assets/backgrounds/rarity-2.webp", + "frame": "assets/frames/rarity-2.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%B5%E6%9C%88" + }, + { + "id": 316, + "name": "明斯克", + "search_name": "明斯克", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/316.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%98%8E%E6%96%AF%E5%85%8B" + }, + { + "id": 317, + "name": "M2", + "search_name": "M2", + "variant": "normal", + "rarity": 3, + "ship_type": "sc", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/317.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/sc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/M2" + }, + { + "id": 318, + "name": "毛奇", + "search_name": "毛奇", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/318.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%AF%9B%E5%A5%87" + }, + { + "id": 319, + "name": "不挠(装甲航母)", + "search_name": "不挠", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/319.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E6%8C%A0(%E8%A3%85%E7%94%B2%E8%88%AA%E6%AF%8D)" + }, + { + "id": 320, + "name": "鹞鹰", + "search_name": "鹞鹰", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/320.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B9%9E%E9%B9%B0" + }, + { + "id": 321, + "name": "费拉迪D", + "search_name": "费拉迪D", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/321.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B4%B9%E6%8B%89%E8%BF%AAD" + }, + { + "id": 322, + "name": "希尔曼", + "search_name": "希尔曼", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/322.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%8C%E5%B0%94%E6%9B%BC" + }, + { + "id": 323, + "name": "基辅", + "search_name": "基辅", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/323.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E8%BE%85" + }, + { + "id": 324, + "name": "塔斯卡卢萨", + "search_name": "塔斯卡卢萨", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/324.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%94%E6%96%AF%E5%8D%A1%E5%8D%A2%E8%90%A8" + }, + { + "id": 325, + "name": "列克星敦(CV-16)", + "search_name": "列克星敦", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/325.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%97%E5%85%8B%E6%98%9F%E6%95%A6(CV-16)" + }, + { + "id": 326, + "name": "诺福克(78)", + "search_name": "诺福克", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/326.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%AF%BA%E7%A6%8F%E5%85%8B(78)" + }, + { + "id": 327, + "name": "斯特雷特", + "search_name": "斯特雷特", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/327.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E7%89%B9%E9%9B%B7%E7%89%B9" + }, + { + "id": 328, + "name": "妙高", + "search_name": "妙高", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/328.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A6%99%E9%AB%98" + }, + { + "id": 329, + "name": "奥古斯塔", + "search_name": "奥古斯塔", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/329.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E5%8F%A4%E6%96%AF%E5%A1%94" + }, + { + "id": 330, + "name": "卫士", + "search_name": "卫士", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/330.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%AB%E5%A3%AB" + }, + { + "id": 331, + "name": "无比", + "search_name": "无比", + "variant": "normal", + "rarity": 6, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/331.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A0%E6%AF%94" + }, + { + "id": 332, + "name": "神鹰", + "search_name": "神鹰", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/332.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A5%9E%E9%B9%B0" + }, + { + "id": 333, + "name": "德格拉斯", + "search_name": "德格拉斯", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/333.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E6%A0%BC%E6%8B%89%E6%96%AF" + }, + { + "id": 334, + "name": "不惧(驱逐舰)", + "search_name": "不惧", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/334.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E6%83%A7(%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 335, + "name": "伏尔塔", + "search_name": "伏尔塔", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/335.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8F%E5%B0%94%E5%A1%94" + }, + { + "id": 336, + "name": "竹", + "search_name": "竹", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/336.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%AB%B9" + }, + { + "id": 337, + "name": "伟大的庞贝", + "search_name": "伟大的庞贝", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/337.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%9F%E5%A4%A7%E7%9A%84%E5%BA%9E%E8%B4%9D" + }, + { + "id": 338, + "name": "天鹰", + "search_name": "天鹰", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/338.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E9%B9%B0" + }, + { + "id": 339, + "name": "帝国", + "search_name": "帝国", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/339.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%9D%E5%9B%BD" + }, + { + "id": 340, + "name": "波尔扎诺", + "search_name": "波尔扎诺", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/340.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E5%B0%94%E6%89%8E%E8%AF%BA" + }, + { + "id": 341, + "name": "龙骑兵", + "search_name": "龙骑兵", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/341.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BE%99%E9%AA%91%E5%85%B5" + }, + { + "id": 342, + "name": "江原", + "search_name": "江原", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/342.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B1%9F%E5%8E%9F" + }, + { + "id": 343, + "name": "蔚山", + "search_name": "蔚山", + "variant": "normal", + "rarity": 5, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/343.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%94%9A%E5%B1%B1" + }, + { + "id": 344, + "name": "忠武", + "search_name": "忠武", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/344.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BF%A0%E6%AD%A6" + }, + { + "id": 345, + "name": "威斯康星", + "search_name": "威斯康星", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/345.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E6%96%AF%E5%BA%B7%E6%98%9F" + }, + { + "id": 346, + "name": "谢菲尔德", + "search_name": "谢菲尔德", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/346.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B0%A2%E8%8F%B2%E5%B0%94%E5%BE%B7" + }, + { + "id": 347, + "name": "蒙彼利埃", + "search_name": "蒙彼利埃", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/347.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%92%99%E5%BD%BC%E5%88%A9%E5%9F%83" + }, + { + "id": 348, + "name": "丹佛", + "search_name": "丹佛", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/348.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%B9%E4%BD%9B" + }, + { + "id": 349, + "name": "初月", + "search_name": "初月", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/349.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%9D%E6%9C%88" + }, + { + "id": 350, + "name": "菲尔普斯", + "search_name": "菲尔普斯", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/350.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8F%B2%E5%B0%94%E6%99%AE%E6%96%AF" + }, + { + "id": 351, + "name": "U-1405", + "search_name": "U-1405", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/351.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-1405" + }, + { + "id": 352, + "name": "莫斯科", + "search_name": "莫斯科", + "variant": "normal", + "rarity": 6, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/352.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8E%AB%E6%96%AF%E7%A7%91" + }, + { + "id": 353, + "name": "斯维尔德洛夫", + "search_name": "斯维尔德洛夫", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/353.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E7%BB%B4%E5%B0%94%E5%BE%B7%E6%B4%9B%E5%A4%AB" + }, + { + "id": 354, + "name": "阿尔贝托·迪·朱桑诺", + "search_name": "阿尔贝托·迪·朱桑诺", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/354.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B0%94%E8%B4%9D%E6%89%98%C2%B7%E8%BF%AA%C2%B7%E6%9C%B1%E6%A1%91%E8%AF%BA" + }, + { + "id": 355, + "name": "联合力量", + "search_name": "联合力量", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/355.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%81%94%E5%90%88%E5%8A%9B%E9%87%8F" + }, + { + "id": 356, + "name": "爱丽", + "search_name": "爱丽", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/356.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%B1%E4%B8%BD" + }, + { + "id": 357, + "name": "羽黑", + "search_name": "羽黑", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/357.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BE%BD%E9%BB%91" + }, + { + "id": 358, + "name": "天津风", + "search_name": "天津风", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/358.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E6%B4%A5%E9%A3%8E" + }, + { + "id": 359, + "name": "吕-34", + "search_name": "吕-34", + "variant": "normal", + "rarity": 3, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/359.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%95-34" + }, + { + "id": 360, + "name": "虎(轻巡洋舰)", + "search_name": "虎", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/360.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%99%8E(%E8%BD%BB%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 361, + "name": "K1", + "search_name": "K1", + "variant": "normal", + "rarity": 3, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/361.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/K1" + }, + { + "id": 362, + "name": "星座", + "search_name": "星座", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/362.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%98%9F%E5%BA%A7" + }, + { + "id": 363, + "name": "炽热", + "search_name": "炽热", + "variant": "normal", + "rarity": 4, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/363.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%82%BD%E7%83%AD" + }, + { + "id": 364, + "name": "加纳里亚斯", + "search_name": "加纳里亚斯", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/364.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E7%BA%B3%E9%87%8C%E4%BA%9A%E6%96%AF" + }, + { + "id": 365, + "name": "门德斯·努涅斯", + "search_name": "门德斯·努涅斯", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/365.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%97%A8%E5%BE%B7%E6%96%AF%C2%B7%E5%8A%AA%E6%B6%85%E6%96%AF" + }, + { + "id": 366, + "name": "鹦鹉螺", + "search_name": "鹦鹉螺", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/366.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B9%A6%E9%B9%89%E8%9E%BA" + }, + { + "id": 367, + "name": "乌尔里希·冯·胡滕", + "search_name": "乌尔里希·冯·胡滕", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/367.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B9%8C%E5%B0%94%E9%87%8C%E5%B8%8C%C2%B7%E5%86%AF%C2%B7%E8%83%A1%E6%BB%95" + }, + { + "id": 368, + "name": "塞德利茨", + "search_name": "塞德利茨", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/368.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E5%BE%B7%E5%88%A9%E8%8C%A8" + }, + { + "id": 369, + "name": "克莱夫勋爵", + "search_name": "克莱夫勋爵", + "variant": "normal", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/369.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E8%8E%B1%E5%A4%AB%E5%8B%8B%E7%88%B5" + }, + { + "id": 370, + "name": "光辉", + "search_name": "光辉", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/370.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%89%E8%BE%89" + }, + { + "id": 371, + "name": "海王星", + "search_name": "海王星", + "variant": "normal", + "rarity": 6, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/371.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E7%8E%8B%E6%98%9F" + }, + { + "id": 372, + "name": "早春", + "search_name": "早春", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/372.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A9%E6%98%A5" + }, + { + "id": 373, + "name": "莱昂纳多·达·芬奇", + "search_name": "莱昂纳多·达·芬奇", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/373.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8E%B1%E6%98%82%E7%BA%B3%E5%A4%9A%C2%B7%E8%BE%BE%C2%B7%E8%8A%AC%E5%A5%87" + }, + { + "id": 374, + "name": "汉考克", + "search_name": "汉考克", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/374.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B1%89%E8%80%83%E5%85%8B" + }, + { + "id": 375, + "name": "圣哈辛托", + "search_name": "圣哈辛托", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/375.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E5%93%88%E8%BE%9B%E6%89%98" + }, + { + "id": 376, + "name": "埃德加·居内", + "search_name": "埃德加·居内", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/376.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%83%E5%BE%B7%E5%8A%A0%C2%B7%E5%B1%85%E5%86%85" + }, + { + "id": 377, + "name": "木曾", + "search_name": "木曾", + "variant": "normal", + "rarity": 4, + "ship_type": "clt", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/377.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/clt.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%A8%E6%9B%BE" + }, + { + "id": 378, + "name": "斯普利特", + "search_name": "斯普利特", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/378.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E6%99%AE%E5%88%A9%E7%89%B9" + }, + { + "id": 379, + "name": "哥伦比亚", + "search_name": "哥伦比亚", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/379.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%A5%E4%BC%A6%E6%AF%94%E4%BA%9A" + }, + { + "id": 380, + "name": "圣乔治", + "search_name": "圣乔治", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/380.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E4%B9%94%E6%B2%BB" + }, + { + "id": 381, + "name": "印第安纳", + "search_name": "印第安纳", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/381.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%B0%E7%AC%AC%E5%AE%89%E7%BA%B3" + }, + { + "id": 382, + "name": "坎伯兰", + "search_name": "坎伯兰", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/382.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9D%8E%E4%BC%AF%E5%85%B0" + }, + { + "id": 383, + "name": "奥斯塔公爵", + "search_name": "奥斯塔公爵", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/383.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E6%96%AF%E5%A1%94%E5%85%AC%E7%88%B5" + }, + { + "id": 384, + "name": "法迪布鲁诺", + "search_name": "法迪布鲁诺", + "variant": "normal", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/384.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%95%E8%BF%AA%E5%B8%83%E9%B2%81%E8%AF%BA" + }, + { + "id": 385, + "name": "S113", + "search_name": "S113", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/385.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/S113" + }, + { + "id": 386, + "name": "莉安夕", + "search_name": "莉安夕", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/386.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8E%89%E5%AE%89%E5%A4%95" + }, + { + "id": 387, + "name": "海圻", + "search_name": "海圻", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/387.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E5%9C%BB" + }, + { + "id": 388, + "name": "纽波特纽斯", + "search_name": "纽波特纽斯", + "variant": "normal", + "rarity": 6, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/388.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%BD%E6%B3%A2%E7%89%B9%E7%BA%BD%E6%96%AF" + }, + { + "id": 389, + "name": "留里克", + "search_name": "留里克", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/389.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%95%99%E9%87%8C%E5%85%8B" + }, + { + "id": 390, + "name": "库图佐夫", + "search_name": "库图佐夫", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/390.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%93%E5%9B%BE%E4%BD%90%E5%A4%AB" + }, + { + "id": 391, + "name": "查尔斯·F·亚当斯", + "search_name": "查尔斯·F·亚当斯", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/391.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9F%A5%E5%B0%94%E6%96%AF%C2%B7F%C2%B7%E4%BA%9A%E5%BD%93%E6%96%AF" + }, + { + "id": 392, + "name": "天雾", + "search_name": "天雾", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/392.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E9%9B%BE" + }, + { + "id": 393, + "name": "查尔斯·奥斯本", + "search_name": "查尔斯·奥斯本", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/393.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9F%A5%E5%B0%94%E6%96%AF%C2%B7%E5%A5%A5%E6%96%AF%E6%9C%AC" + }, + { + "id": 394, + "name": "泰勒", + "search_name": "泰勒", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/394.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%B0%E5%8B%92" + }, + { + "id": 395, + "name": "闪电", + "search_name": "闪电", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/395.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%97%AA%E7%94%B5" + }, + { + "id": 396, + "name": "新墨西哥", + "search_name": "新墨西哥", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/396.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%B0%E5%A2%A8%E8%A5%BF%E5%93%A5" + }, + { + "id": 397, + "name": "斯大林格勒", + "search_name": "斯大林格勒", + "variant": "normal", + "rarity": 6, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/397.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E5%A4%A7%E6%9E%97%E6%A0%BC%E5%8B%92" + }, + { + "id": 398, + "name": "德里", + "search_name": "德里", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/398.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E9%87%8C" + }, + { + "id": 399, + "name": "阿布鲁奇公爵", + "search_name": "阿布鲁奇公爵", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/399.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B8%83%E9%B2%81%E5%A5%87%E5%85%AC%E7%88%B5" + }, + { + "id": 400, + "name": "七省联盟", + "search_name": "七省联盟", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/400.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%83%E7%9C%81%E8%81%94%E7%9B%9F" + }, + { + "id": 401, + "name": "胆大", + "search_name": "胆大", + "variant": "normal", + "rarity": 4, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/401.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%83%86%E5%A4%A7" + }, + { + "id": 402, + "name": "U-35", + "search_name": "U-35", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/402.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-35" + }, + { + "id": 403, + "name": "格拉摩根", + "search_name": "格拉摩根", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/403.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A0%BC%E6%8B%89%E6%91%A9%E6%A0%B9" + }, + { + "id": 404, + "name": "怨仇", + "search_name": "怨仇", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/404.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%80%A8%E4%BB%87" + }, + { + "id": 405, + "name": "乔治·莱格", + "search_name": "乔治·莱格", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/405.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B9%94%E6%B2%BB%C2%B7%E8%8E%B1%E6%A0%BC" + }, + { + "id": 406, + "name": "哈曼", + "search_name": "哈曼", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/406.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%88%E6%9B%BC" + }, + { + "id": 407, + "name": "吕特晏斯", + "search_name": "吕特晏斯", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/407.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%95%E7%89%B9%E6%99%8F%E6%96%AF" + }, + { + "id": 408, + "name": "鲃鱼", + "search_name": "鲃鱼", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/408.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B2%83%E9%B1%BC" + }, + { + "id": 409, + "name": "新泽西", + "search_name": "新泽西", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/409.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%B0%E6%B3%BD%E8%A5%BF" + }, + { + "id": 410, + "name": "萨勒姆", + "search_name": "萨勒姆", + "variant": "normal", + "rarity": 6, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/410.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A8%E5%8B%92%E5%A7%86" + }, + { + "id": 411, + "name": "黛朵", + "search_name": "黛朵", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/411.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%9B%E6%9C%B5" + }, + { + "id": 412, + "name": "堪培拉", + "search_name": "堪培拉", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/412.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A0%AA%E5%9F%B9%E6%8B%89" + }, + { + "id": 413, + "name": "加里波第", + "search_name": "加里波第", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/413.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E9%87%8C%E6%B3%A2%E7%AC%AC" + }, + { + "id": 414, + "name": "凯利", + "search_name": "凯利", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/414.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%AF%E5%88%A9" + }, + { + "id": 415, + "name": "英格兰", + "search_name": "英格兰", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/415.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%B1%E6%A0%BC%E5%85%B0" + }, + { + "id": 416, + "name": "47工程", + "search_name": "47工程", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/416.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/47%E5%B7%A5%E7%A8%8B" + }, + { + "id": 417, + "name": "可怖", + "search_name": "可怖", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/417.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%AF%E6%80%96" + }, + { + "id": 418, + "name": "阿金库尔", + "search_name": "阿金库尔", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/418.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E9%87%91%E5%BA%93%E5%B0%94" + }, + { + "id": 419, + "name": "桑提", + "search_name": "桑提", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/419.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A1%91%E6%8F%90" + }, + { + "id": 420, + "name": "亚尔古水手", + "search_name": "亚尔古水手", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/420.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%9A%E5%B0%94%E5%8F%A4%E6%B0%B4%E6%89%8B" + }, + { + "id": 421, + "name": "萨凡纳", + "search_name": "萨凡纳", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/421.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A8%E5%87%A1%E7%BA%B3" + }, + { + "id": 422, + "name": "凤凰城", + "search_name": "凤凰城", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/422.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%A4%E5%87%B0%E5%9F%8E" + }, + { + "id": 423, + "name": "燕八哥", + "search_name": "燕八哥", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/423.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%87%95%E5%85%AB%E5%93%A5" + }, + { + "id": 424, + "name": "蒂默曼", + "search_name": "蒂默曼", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/424.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%92%82%E9%BB%98%E6%9B%BC" + }, + { + "id": 425, + "name": "史密斯", + "search_name": "史密斯", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/425.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%B2%E5%AF%86%E6%96%AF" + }, + { + "id": 426, + "name": "彼得·施特拉塞尔", + "search_name": "彼得·施特拉塞尔", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/426.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BD%BC%E5%BE%97%C2%B7%E6%96%BD%E7%89%B9%E6%8B%89%E5%A1%9E%E5%B0%94" + }, + { + "id": 427, + "name": "塞班", + "search_name": "塞班", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/427.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E7%8F%AD" + }, + { + "id": 428, + "name": "尼古拉斯", + "search_name": "尼古拉斯", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/428.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B0%BC%E5%8F%A4%E6%8B%89%E6%96%AF" + }, + { + "id": 429, + "name": "马伊·布雷泽", + "search_name": "马伊·布雷泽", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/429.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E4%BC%8A%C2%B7%E5%B8%83%E9%9B%B7%E6%B3%BD" + }, + { + "id": 430, + "name": "捷尔任斯基", + "search_name": "捷尔任斯基", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/430.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8D%B7%E5%B0%94%E4%BB%BB%E6%96%AF%E5%9F%BA" + }, + { + "id": 431, + "name": "什罗普郡", + "search_name": "什罗普郡", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/431.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BB%80%E7%BD%97%E6%99%AE%E9%83%A1" + }, + { + "id": 432, + "name": "波士顿", + "search_name": "波士顿", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/432.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E5%A3%AB%E9%A1%BF" + }, + { + "id": 433, + "name": "提康德罗加", + "search_name": "提康德罗加", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/433.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8F%90%E5%BA%B7%E5%BE%B7%E7%BD%97%E5%8A%A0" + }, + { + "id": 434, + "name": "亨廷顿", + "search_name": "亨廷顿", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/434.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%A8%E5%BB%B7%E9%A1%BF" + }, + { + "id": 435, + "name": "B65", + "search_name": "B65", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/435.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/B65" + }, + { + "id": 436, + "name": "十三号战舰", + "search_name": "十三号战舰", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/436.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%81%E4%B8%89%E5%8F%B7%E6%88%98%E8%88%B0" + }, + { + "id": 437, + "name": "鞍马", + "search_name": "鞍马", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/437.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9E%8D%E9%A9%AC" + }, + { + "id": 438, + "name": "梅肯", + "search_name": "梅肯", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/438.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A2%85%E8%82%AF" + }, + { + "id": 439, + "name": "冬月", + "search_name": "冬月", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/439.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%86%AC%E6%9C%88" + }, + { + "id": 440, + "name": "阿尔维塞·达莫斯托", + "search_name": "阿尔维塞·达莫斯托", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/440.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B0%94%E7%BB%B4%E5%A1%9E%C2%B7%E8%BE%BE%E8%8E%AB%E6%96%AF%E6%89%98" + }, + { + "id": 441, + "name": "托戈", + "search_name": "托戈", + "variant": "normal", + "rarity": 4, + "ship_type": "ap", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/441.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ap.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%89%98%E6%88%88" + }, + { + "id": 442, + "name": "里昂", + "search_name": "里昂", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/442.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%8C%E6%98%82" + }, + { + "id": 443, + "name": "阿贺野", + "search_name": "阿贺野", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/443.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E8%B4%BA%E9%87%8E" + }, + { + "id": 444, + "name": "红色高加索", + "search_name": "红色高加索", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/444.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A2%E8%89%B2%E9%AB%98%E5%8A%A0%E7%B4%A2" + }, + { + "id": 445, + "name": "哈尔福德", + "search_name": "哈尔福德", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/445.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%88%E5%B0%94%E7%A6%8F%E5%BE%B7" + }, + { + "id": 446, + "name": "狮(战列巡洋舰)", + "search_name": "狮", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/446.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8B%AE(%E6%88%98%E5%88%97%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 447, + "name": "迪凯纳", + "search_name": "迪凯纳", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/447.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%AA%E5%87%AF%E7%BA%B3" + }, + { + "id": 448, + "name": "弗兰德尔", + "search_name": "弗兰德尔", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/448.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BC%97%E5%85%B0%E5%BE%B7%E5%B0%94" + }, + { + "id": 449, + "name": "莫加多尔", + "search_name": "莫加多尔", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/449.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8E%AB%E5%8A%A0%E5%A4%9A%E5%B0%94" + }, + { + "id": 450, + "name": "L20", + "search_name": "L20", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/450.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/L20" + }, + { + "id": 451, + "name": "伊吹", + "search_name": "伊吹", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/451.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E5%90%B9" + }, + { + "id": 452, + "name": "萨里", + "search_name": "萨里", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/452.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A8%E9%87%8C" + }, + { + "id": 453, + "name": "野分", + "search_name": "野分", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/453.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%8E%E5%88%86" + }, + { + "id": 454, + "name": "征服者", + "search_name": "征服者", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/454.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%81%E6%9C%8D%E8%80%85" + }, + { + "id": 455, + "name": "皇家方舟(R09)", + "search_name": "皇家方舟", + "variant": "normal", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/455.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9A%87%E5%AE%B6%E6%96%B9%E8%88%9F(R09)" + }, + { + "id": 456, + "name": "彼得罗巴甫洛夫斯克", + "search_name": "彼得罗巴甫洛夫斯克", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/456.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BD%BC%E5%BE%97%E7%BD%97%E5%B7%B4%E7%94%AB%E6%B4%9B%E5%A4%AB%E6%96%AF%E5%85%8B" + }, + { + "id": 457, + "name": "潮", + "search_name": "潮", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/457.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%BD%AE" + }, + { + "id": 458, + "name": "U-14", + "search_name": "U-14", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/458.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-14" + }, + { + "id": 459, + "name": "底特律", + "search_name": "底特律", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/459.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%95%E7%89%B9%E5%BE%8B" + }, + { + "id": 460, + "name": "宾夕法尼亚", + "search_name": "宾夕法尼亚", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/460.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%BE%E5%A4%95%E6%B3%95%E5%B0%BC%E4%BA%9A" + }, + { + "id": 461, + "name": "无敌", + "search_name": "无敌", + "variant": "normal", + "rarity": 6, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/461.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A0%E6%95%8C" + }, + { + "id": 462, + "name": "多摩", + "search_name": "多摩", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/462.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%9A%E6%91%A9" + }, + { + "id": 463, + "name": "鹰", + "search_name": "鹰", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/463.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B9%B0" + }, + { + "id": 464, + "name": "南达科他(BB-49)", + "search_name": "南达科他", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/464.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%97%E8%BE%BE%E7%A7%91%E4%BB%96(BB-49)" + }, + { + "id": 465, + "name": "爪哇", + "search_name": "爪哇", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/465.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%AA%E5%93%87" + }, + { + "id": 466, + "name": "1939战列舰", + "search_name": "1939战列舰", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/466.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/1939%E6%88%98%E5%88%97%E8%88%B0" + }, + { + "id": 467, + "name": "鞍山", + "search_name": "鞍山", + "variant": "normal", + "rarity": 4, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/467.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9E%8D%E5%B1%B1" + }, + { + "id": 468, + "name": "猎户座", + "search_name": "猎户座", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/468.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8C%8E%E6%88%B7%E5%BA%A7" + }, + { + "id": 469, + "name": "本宁顿", + "search_name": "本宁顿", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/469.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%AC%E5%AE%81%E9%A1%BF" + }, + { + "id": 470, + "name": "甘比尔湾", + "search_name": "甘比尔湾", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/470.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%94%98%E6%AF%94%E5%B0%94%E6%B9%BE" + }, + { + "id": 471, + "name": "欧罗巴", + "search_name": "欧罗巴", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/471.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%AC%A7%E7%BD%97%E5%B7%B4" + }, + { + "id": 472, + "name": "霞飞", + "search_name": "霞飞", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/472.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9C%9E%E9%A3%9E" + }, + { + "id": 473, + "name": "阿非利加征服者西庇阿", + "search_name": "阿非利加征服者西庇阿", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/473.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E9%9D%9E%E5%88%A9%E5%8A%A0%E5%BE%81%E6%9C%8D%E8%80%85%E8%A5%BF%E5%BA%87%E9%98%BF" + }, + { + "id": 474, + "name": "72工程", + "search_name": "72工程", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/474.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/72%E5%B7%A5%E7%A8%8B" + }, + { + "id": 475, + "name": "U-2511", + "search_name": "U-2511", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/475.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-2511" + }, + { + "id": 476, + "name": "科顿艾尔", + "search_name": "科顿艾尔", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/476.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E9%A1%BF%E8%89%BE%E5%B0%94" + }, + { + "id": 477, + "name": "马戈迪尼", + "search_name": "马戈迪尼", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/477.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E6%88%88%E8%BF%AA%E5%B0%BC" + }, + { + "id": 478, + "name": "阿方索十三世", + "search_name": "阿方索十三世", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/478.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E6%96%B9%E7%B4%A2%E5%8D%81%E4%B8%89%E4%B8%96" + }, + { + "id": 479, + "name": "桑普森", + "search_name": "桑普森", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/479.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A1%91%E6%99%AE%E6%A3%AE" + }, + { + "id": 480, + "name": "U-459", + "search_name": "U-459", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/480.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-459" + }, + { + "id": 481, + "name": "仁淀", + "search_name": "仁淀", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/481.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BB%81%E6%B7%80" + }, + { + "id": 482, + "name": "大淀(苍青幻影)", + "search_name": "大淀", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/482.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E6%B7%80(%E8%8B%8D%E9%9D%92%E5%B9%BB%E5%BD%B1)" + }, + { + "id": 483, + "name": "胜利", + "search_name": "胜利", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/483.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%83%9C%E5%88%A9" + }, + { + "id": 484, + "name": "安森", + "search_name": "安森", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/484.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E6%A3%AE" + }, + { + "id": 485, + "name": "英格拉姆", + "search_name": "英格拉姆", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/485.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%B1%E6%A0%BC%E6%8B%89%E5%A7%86" + }, + { + "id": 486, + "name": "G15", + "search_name": "G15", + "variant": "normal", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/486.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/G15" + }, + { + "id": 487, + "name": "五月二十五", + "search_name": "五月二十五", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/487.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%94%E6%9C%88%E4%BA%8C%E5%8D%81%E4%BA%94" + }, + { + "id": 488, + "name": "U-556", + "search_name": "U-556", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/488.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-556" + }, + { + "id": 489, + "name": "历战", + "search_name": "历战", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/489.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8E%86%E6%88%98" + }, + { + "id": 490, + "name": "克劳塞维茨", + "search_name": "克劳塞维茨", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/490.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E5%8A%B3%E5%A1%9E%E7%BB%B4%E8%8C%A8" + }, + { + "id": 491, + "name": "夏伯阳", + "search_name": "夏伯阳", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/491.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%8F%E4%BC%AF%E9%98%B3" + }, + { + "id": 492, + "name": "壮丽", + "search_name": "壮丽", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/492.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A3%AE%E4%B8%BD" + }, + { + "id": 493, + "name": "K-21", + "search_name": "K-21", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/493.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/K-21" + }, + { + "id": 494, + "name": "G6", + "search_name": "G6", + "variant": "normal", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/494.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/G6" + }, + { + "id": 495, + "name": "光荣(轻巡洋舰)", + "search_name": "光荣", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/495.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%89%E8%8D%A3(%E8%BD%BB%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 496, + "name": "诺福克(DL-1)", + "search_name": "诺福克", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/496.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%AF%BA%E7%A6%8F%E5%85%8B(DL-1)" + }, + { + "id": 497, + "name": "U-441", + "search_name": "U-441", + "variant": "normal", + "rarity": 3, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/497.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-441" + }, + { + "id": 498, + "name": "复仇", + "search_name": "复仇", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/498.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%8D%E4%BB%87" + }, + { + "id": 499, + "name": "克里蒙梭", + "search_name": "克里蒙梭", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/499.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E9%87%8C%E8%92%99%E6%A2%AD" + }, + { + "id": 500, + "name": "伏尔铿", + "search_name": "伏尔铿", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/500.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8F%E5%B0%94%E9%93%BF" + }, + { + "id": 501, + "name": "拉弗雷", + "search_name": "拉弗雷", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/501.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8B%89%E5%BC%97%E9%9B%B7" + }, + { + "id": 502, + "name": "济南", + "search_name": "济南", + "variant": "normal", + "rarity": 5, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/502.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%8E%E5%8D%97" + }, + { + "id": 503, + "name": "T-23", + "search_name": "T-23", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/503.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/T-23" + }, + { + "id": 504, + "name": "费城", + "search_name": "费城", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/504.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B4%B9%E5%9F%8E" + }, + { + "id": 505, + "name": "雉", + "search_name": "雉", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/505.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%89" + }, + { + "id": 506, + "name": "1938(I)", + "search_name": "1938", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/506.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/1938(I)" + }, + { + "id": 507, + "name": "艾斯沃尔德", + "search_name": "艾斯沃尔德", + "variant": "normal", + "rarity": 3, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/507.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%89%BE%E6%96%AF%E6%B2%83%E5%B0%94%E5%BE%B7" + }, + { + "id": 508, + "name": "安萨尔多", + "search_name": "安萨尔多", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/508.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E8%90%A8%E5%B0%94%E5%A4%9A" + }, + { + "id": 509, + "name": "谦逊", + "search_name": "谦逊", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/509.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B0%A6%E9%80%8A" + }, + { + "id": 510, + "name": "G14", + "search_name": "G14", + "variant": "normal", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/510.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/G14" + }, + { + "id": 511, + "name": "德文郡", + "search_name": "德文郡", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/511.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E6%96%87%E9%83%A1" + }, + { + "id": 512, + "name": "科伦坡", + "search_name": "科伦坡", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/512.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E4%BC%A6%E5%9D%A1" + }, + { + "id": 513, + "name": "勇猛", + "search_name": "勇猛", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/513.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8B%87%E7%8C%9B" + }, + { + "id": 514, + "name": "近江", + "search_name": "近江", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/514.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%91%E6%B1%9F" + }, + { + "id": 515, + "name": "伏罗希洛夫", + "search_name": "伏罗希洛夫", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/515.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8F%E7%BD%97%E5%B8%8C%E6%B4%9B%E5%A4%AB" + }, + { + "id": 516, + "name": "日丹诺夫", + "search_name": "日丹诺夫", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/516.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A5%E4%B8%B9%E8%AF%BA%E5%A4%AB" + }, + { + "id": 517, + "name": "吕贝克", + "search_name": "吕贝克", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/517.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%95%E8%B4%9D%E5%85%8B" + }, + { + "id": 518, + "name": "IIIA", + "search_name": "IIIA", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/518.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/IIIA" + }, + { + "id": 519, + "name": "T.995", + "search_name": "T.995", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/519.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/T.995" + }, + { + "id": 520, + "name": "蒙大拿", + "search_name": "蒙大拿", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/520.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%92%99%E5%A4%A7%E6%8B%BF" + }, + { + "id": 521, + "name": "1913战巡", + "search_name": "1913战巡", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/521.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/1913%E6%88%98%E5%B7%A1" + }, + { + "id": 522, + "name": "法戈", + "search_name": "法戈", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/522.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%95%E6%88%88" + }, + { + "id": 523, + "name": "亚拉巴马", + "search_name": "亚拉巴马", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/523.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%9A%E6%8B%89%E5%B7%B4%E9%A9%AC" + }, + { + "id": 524, + "name": "夏威夷", + "search_name": "夏威夷", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/524.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%8F%E5%A8%81%E5%A4%B7" + }, + { + "id": 525, + "name": "长波", + "search_name": "长波", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/525.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%95%BF%E6%B3%A2" + }, + { + "id": 526, + "name": "朱诺(CL-119)", + "search_name": "朱诺", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/526.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%B1%E8%AF%BA(CL-119)" + }, + { + "id": 527, + "name": "CNT巡洋舰", + "search_name": "CNT巡洋舰", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/527.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/CNT%E5%B7%A1%E6%B4%8B%E8%88%B0" + }, + { + "id": 528, + "name": "芝加哥(CA-136)", + "search_name": "芝加哥", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/528.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8A%9D%E5%8A%A0%E5%93%A5(CA-136)" + }, + { + "id": 529, + "name": "花信风", + "search_name": "花信风", + "variant": "normal", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/529.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8A%B1%E4%BF%A1%E9%A3%8E" + }, + { + "id": 530, + "name": "M-296", + "search_name": "M-296", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/530.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/M-296" + }, + { + "id": 531, + "name": "墨尔波墨涅", + "search_name": "墨尔波墨涅", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/531.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A2%A8%E5%B0%94%E6%B3%A2%E5%A2%A8%E6%B6%85" + }, + { + "id": 532, + "name": "但丁", + "search_name": "但丁", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/532.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BD%86%E4%B8%81" + }, + { + "id": 533, + "name": "格罗兹尼", + "search_name": "格罗兹尼", + "variant": "normal", + "rarity": 5, + "ship_type": "kp", + "size_class": "medium", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/533.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A0%BC%E7%BD%97%E5%85%B9%E5%B0%BC" + }, + { + "id": 534, + "name": "24工程", + "search_name": "24工程", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/534.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/24%E5%B7%A5%E7%A8%8B" + }, + { + "id": 535, + "name": "Z32", + "search_name": "Z32", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/535.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z32" + }, + { + "id": 536, + "name": "卡伯特", + "search_name": "卡伯特", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/536.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E4%BC%AF%E7%89%B9" + }, + { + "id": 537, + "name": "波拉", + "search_name": "波拉", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/537.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E6%8B%89" + }, + { + "id": 538, + "name": "伊-25", + "search_name": "伊-25", + "variant": "normal", + "rarity": 3, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/538.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A-25" + }, + { + "id": 539, + "name": "SKR-6", + "search_name": "SKR-6", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/539.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/SKR-6" + }, + { + "id": 540, + "name": "伊兹梅尔", + "search_name": "伊兹梅尔", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/540.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E5%85%B9%E6%A2%85%E5%B0%94" + }, + { + "id": 541, + "name": "火力", + "search_name": "火力", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/541.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%81%AB%E5%8A%9B" + }, + { + "id": 542, + "name": "卡律布狄斯", + "search_name": "卡律布狄斯", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/542.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E5%BE%8B%E5%B8%83%E7%8B%84%E6%96%AF" + }, + { + "id": 543, + "name": "Z2", + "search_name": "Z2", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/543.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z2" + }, + { + "id": 544, + "name": "丛云", + "search_name": "丛云", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/544.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%9B%E4%BA%91" + }, + { + "id": 545, + "name": "威悉河", + "search_name": "威悉河", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/545.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E6%82%89%E6%B2%B3" + }, + { + "id": 546, + "name": "德意志(训练巡洋舰)", + "search_name": "德意志", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/546.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E6%84%8F%E5%BF%97(%E8%AE%AD%E7%BB%83%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 547, + "name": "勃艮第", + "search_name": "勃艮第", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/547.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8B%83%E8%89%AE%E7%AC%AC" + }, + { + "id": 548, + "name": "龙凤", + "search_name": "龙凤", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/548.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BE%99%E5%87%A4" + }, + { + "id": 549, + "name": "岛风(峰风型驱逐舰)", + "search_name": "岛风", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/549.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B2%9B%E9%A3%8E(%E5%B3%B0%E9%A3%8E%E5%9E%8B%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 550, + "name": "赫尔", + "search_name": "赫尔", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/550.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B5%AB%E5%B0%94" + }, + { + "id": 551, + "name": "本明", + "search_name": "本明", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/551.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%AC%E6%98%8E" + }, + { + "id": 552, + "name": "絮弗伦", + "search_name": "絮弗伦", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/552.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B5%AE%E5%BC%97%E4%BC%A6" + }, + { + "id": 553, + "name": "长田", + "search_name": "长田", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/553.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%95%BF%E7%94%B0" + }, + { + "id": 554, + "name": "戈里齐亚", + "search_name": "戈里齐亚", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/554.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%88%88%E9%87%8C%E9%BD%90%E4%BA%9A" + }, + { + "id": 555, + "name": "玛丽女王", + "search_name": "玛丽女王", + "variant": "normal", + "rarity": 3, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/555.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8E%9B%E4%B8%BD%E5%A5%B3%E7%8E%8B" + }, + { + "id": 556, + "name": "若月", + "search_name": "若月", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/556.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%A5%E6%9C%88" + }, + { + "id": 557, + "name": "初春", + "search_name": "初春", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/557.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%9D%E6%98%A5" + }, + { + "id": 558, + "name": "芝加哥(CA-29)", + "search_name": "芝加哥", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/558.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8A%9D%E5%8A%A0%E5%93%A5(CA-29)" + }, + { + "id": 559, + "name": "不惧(防空导弹驱逐舰)", + "search_name": "不惧", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/559.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E6%83%A7(%E9%98%B2%E7%A9%BA%E5%AF%BC%E5%BC%B9%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 560, + "name": "石勒苏益格-荷尔施泰因", + "search_name": "石勒苏益格-荷尔施泰因", + "variant": "normal", + "rarity": 3, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/560.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9F%B3%E5%8B%92%E8%8B%8F%E7%9B%8A%E6%A0%BC-%E8%8D%B7%E5%B0%94%E6%96%BD%E6%B3%B0%E5%9B%A0" + }, + { + "id": 561, + "name": "朱利奥·格马尼库斯", + "search_name": "朱利奥·格马尼库斯", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/561.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%B1%E5%88%A9%E5%A5%A5%C2%B7%E6%A0%BC%E9%A9%AC%E5%B0%BC%E5%BA%93%E6%96%AF" + }, + { + "id": 562, + "name": "不屈", + "search_name": "不屈", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/562.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E5%B1%88" + }, + { + "id": 563, + "name": "康沃尔", + "search_name": "康沃尔", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/563.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%B7%E6%B2%83%E5%B0%94" + }, + { + "id": 564, + "name": "Z60", + "search_name": "Z60", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/564.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z60" + }, + { + "id": 565, + "name": "塞瓦斯托波尔(战列巡洋舰)", + "search_name": "塞瓦斯托波尔", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/565.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E7%93%A6%E6%96%AF%E6%89%98%E6%B3%A2%E5%B0%94(%E6%88%98%E5%88%97%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 566, + "name": "风暴", + "search_name": "风暴", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/566.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A3%8E%E6%9A%B4" + }, + { + "id": 567, + "name": "科芒斯曼特湾", + "search_name": "科芒斯曼特湾", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/567.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E8%8A%92%E6%96%AF%E6%9B%BC%E7%89%B9%E6%B9%BE" + }, + { + "id": 568, + "name": "苏尔特元帅", + "search_name": "苏尔特元帅", + "variant": "normal", + "rarity": 3, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/568.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%8F%E5%B0%94%E7%89%B9%E5%85%83%E5%B8%85" + }, + { + "id": 569, + "name": "布勃诺夫方案", + "search_name": "布勃诺夫方案", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/569.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E5%8B%83%E8%AF%BA%E5%A4%AB%E6%96%B9%E6%A1%88" + }, + { + "id": 570, + "name": "千岁", + "search_name": "千岁", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/570.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%83%E5%B2%81" + }, + { + "id": 571, + "name": "文森斯", + "search_name": "文森斯", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/571.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%87%E6%A3%AE%E6%96%AF" + }, + { + "id": 572, + "name": "骏河", + "search_name": "骏河", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/572.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%AA%8F%E6%B2%B3" + }, + { + "id": 573, + "name": "71工程", + "search_name": "71工程", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/573.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/71%E5%B7%A5%E7%A8%8B" + }, + { + "id": 574, + "name": "德克萨斯", + "search_name": "德克萨斯", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/574.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BE%B7%E5%85%8B%E8%90%A8%E6%96%AF" + }, + { + "id": 575, + "name": "威武", + "search_name": "威武", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/575.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E6%AD%A6" + }, + { + "id": 576, + "name": "中途岛", + "search_name": "中途岛", + "variant": "normal", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/576.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%AD%E9%80%94%E5%B2%9B" + }, + { + "id": 577, + "name": "水中仙女", + "search_name": "水中仙女", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/577.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B0%B4%E4%B8%AD%E4%BB%99%E5%A5%B3" + }, + { + "id": 578, + "name": "里维斯", + "search_name": "里维斯", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/578.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%8C%E7%BB%B4%E6%96%AF" + }, + { + "id": 579, + "name": "伊-201", + "search_name": "伊-201", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/579.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A-201" + }, + { + "id": 580, + "name": "特伦托", + "search_name": "特伦托", + "variant": "normal", + "rarity": 3, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/580.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%89%B9%E4%BC%A6%E6%89%98" + }, + { + "id": 581, + "name": "塞瓦斯托波尔(导弹巡洋舰)", + "search_name": "塞瓦斯托波尔", + "variant": "normal", + "rarity": 5, + "ship_type": "kp", + "size_class": "medium", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/581.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E7%93%A6%E6%96%AF%E6%89%98%E6%B3%A2%E5%B0%94(%E5%AF%BC%E5%BC%B9%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 582, + "name": "H43", + "search_name": "H43", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/582.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/H43" + }, + { + "id": 583, + "name": "卡拉乔洛", + "search_name": "卡拉乔洛", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/583.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E6%8B%89%E4%B9%94%E6%B4%9B" + }, + { + "id": 584, + "name": "IVS轻巡洋舰", + "search_name": "IVS轻巡洋舰", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/584.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/IVS%E8%BD%BB%E5%B7%A1%E6%B4%8B%E8%88%B0" + }, + { + "id": 585, + "name": "鳞鲀", + "search_name": "鳞鲀", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/585.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B3%9E%E9%B2%80" + }, + { + "id": 586, + "name": "AIII", + "search_name": "AIII", + "variant": "normal", + "rarity": 6, + "ship_type": "bbv", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/586.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bbv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/AIII" + }, + { + "id": 587, + "name": "考彭斯", + "search_name": "考彭斯", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/587.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%80%83%E5%BD%AD%E6%96%AF" + }, + { + "id": 588, + "name": "422", + "search_name": "422", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/588.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/422" + }, + { + "id": 589, + "name": "休·W·哈德利", + "search_name": "休·W·哈德利", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/589.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%91%C2%B7W%C2%B7%E5%93%88%E5%BE%B7%E5%88%A9" + }, + { + "id": 590, + "name": "金伯恩", + "search_name": "金伯恩", + "variant": "normal", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/590.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%91%E4%BC%AF%E6%81%A9" + }, + { + "id": 591, + "name": "加尔各答", + "search_name": "加尔各答", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/591.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E5%B0%94%E5%90%84%E7%AD%94" + }, + { + "id": 592, + "name": "豹", + "search_name": "豹", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/592.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B1%B9" + }, + { + "id": 593, + "name": "夕雾", + "search_name": "夕雾", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/593.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%95%E9%9B%BE" + }, + { + "id": 594, + "name": "黄蜂(CV-18)", + "search_name": "黄蜂", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/594.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%84%E8%9C%82(CV-18)" + }, + { + "id": 595, + "name": "东风", + "search_name": "东风", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/595.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%9C%E9%A3%8E" + }, + { + "id": 596, + "name": "九江", + "search_name": "九江", + "variant": "normal", + "rarity": 4, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/596.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B9%9D%E6%B1%9F" + }, + { + "id": 597, + "name": "海洋", + "search_name": "海洋", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/597.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E6%B4%8B" + }, + { + "id": 598, + "name": "梅雷迪斯", + "search_name": "梅雷迪斯", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/598.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A2%85%E9%9B%B7%E8%BF%AA%E6%96%AF" + }, + { + "id": 599, + "name": "303", + "search_name": "303", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/599.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/303" + }, + { + "id": 600, + "name": "U-4501", + "search_name": "U-4501", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/600.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-4501" + }, + { + "id": 601, + "name": "351", + "search_name": "351", + "variant": "normal", + "rarity": 5, + "ship_type": "ssg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/601.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ssg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/351" + }, + { + "id": 602, + "name": "密西西比", + "search_name": "密西西比", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/602.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AF%86%E8%A5%BF%E8%A5%BF%E6%AF%94" + }, + { + "id": 603, + "name": "凯尔森", + "search_name": "凯尔森", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/603.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%AF%E5%B0%94%E6%A3%AE" + }, + { + "id": 604, + "name": "大胆", + "search_name": "大胆", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/604.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E8%83%86" + }, + { + "id": 605, + "name": "卡萨诺方案", + "search_name": "卡萨诺方案", + "variant": "normal", + "rarity": 6, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/605.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E8%90%A8%E8%AF%BA%E6%96%B9%E6%A1%88" + }, + { + "id": 606, + "name": "汉堡", + "search_name": "汉堡", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/606.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B1%89%E5%A0%A1" + }, + { + "id": 607, + "name": "航空兵", + "search_name": "航空兵", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/607.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%88%AA%E7%A9%BA%E5%85%B5" + }, + { + "id": 608, + "name": "支持者", + "search_name": "支持者", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/608.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%94%AF%E6%8C%81%E8%80%85" + }, + { + "id": 609, + "name": "香格里拉", + "search_name": "香格里拉", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/609.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A6%99%E6%A0%BC%E9%87%8C%E6%8B%89" + }, + { + "id": 610, + "name": "激流", + "search_name": "激流", + "variant": "normal", + "rarity": 3, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/610.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%BF%80%E6%B5%81" + }, + { + "id": 611, + "name": "迪盖·特鲁安", + "search_name": "迪盖·特鲁安", + "variant": "normal", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/611.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%AA%E7%9B%96%C2%B7%E7%89%B9%E9%B2%81%E5%AE%89" + }, + { + "id": 612, + "name": "巴格利", + "search_name": "巴格利", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/612.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%B4%E6%A0%BC%E5%88%A9" + }, + { + "id": 613, + "name": "武藏", + "search_name": "武藏", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/613.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%AD%A6%E8%97%8F" + }, + { + "id": 614, + "name": "布列塔尼", + "search_name": "布列塔尼", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/614.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E5%88%97%E5%A1%94%E5%B0%BC" + }, + { + "id": 615, + "name": "冲锋", + "search_name": "冲锋", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/615.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%86%B2%E9%94%8B" + }, + { + "id": 616, + "name": "狭雾", + "search_name": "狭雾", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/616.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8B%AD%E9%9B%BE" + }, + { + "id": 617, + "name": "南京", + "search_name": "南京", + "variant": "normal", + "rarity": 5, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/617.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%97%E4%BA%AC" + }, + { + "id": 618, + "name": "米切尔", + "search_name": "米切尔", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/618.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B1%B3%E5%88%87%E5%B0%94" + }, + { + "id": 619, + "name": "罗斯福", + "search_name": "罗斯福", + "variant": "normal", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/619.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BD%97%E6%96%AF%E7%A6%8F" + }, + { + "id": 620, + "name": "铁公爵", + "search_name": "铁公爵", + "variant": "normal", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/620.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%93%81%E5%85%AC%E7%88%B5" + }, + { + "id": 621, + "name": "长矛", + "search_name": "长矛", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/621.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%95%BF%E7%9F%9B" + }, + { + "id": 622, + "name": "杜布雷", + "search_name": "杜布雷", + "variant": "normal", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/622.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9D%9C%E5%B8%83%E9%9B%B7" + }, + { + "id": 623, + "name": "鲁莽", + "search_name": "鲁莽", + "variant": "normal", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/623.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B2%81%E8%8E%BD" + }, + { + "id": 624, + "name": "堡垒", + "search_name": "堡垒", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/624.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A0%A1%E5%9E%92" + }, + { + "id": 625, + "name": "卡萨尔", + "search_name": "卡萨尔", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/625.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E8%90%A8%E5%B0%94" + }, + { + "id": 626, + "name": "伊-17", + "search_name": "伊-17", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/626.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A-17" + }, + { + "id": 627, + "name": "奥里斯坎尼", + "search_name": "奥里斯坎尼", + "variant": "normal", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/627.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E9%87%8C%E6%96%AF%E5%9D%8E%E5%B0%BC" + }, + { + "id": 628, + "name": "长岛", + "search_name": "长岛", + "variant": "normal", + "rarity": 3, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/628.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%95%BF%E5%B2%9B" + }, + { + "id": 629, + "name": "桑加蒙", + "search_name": "桑加蒙", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/629.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A1%91%E5%8A%A0%E8%92%99" + }, + { + "id": 630, + "name": "U-3008", + "search_name": "U-3008", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/630.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-3008" + }, + { + "id": 631, + "name": "Kw45", + "search_name": "Kw45", + "variant": "normal", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/631.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Kw45" + }, + { + "id": 632, + "name": "海梅一世", + "search_name": "海梅一世", + "variant": "normal", + "rarity": 3, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/632.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E6%A2%85%E4%B8%80%E4%B8%96" + }, + { + "id": 633, + "name": "S-49", + "search_name": "S-49", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/633.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/S-49" + }, + { + "id": 634, + "name": "自豪", + "search_name": "自豪", + "variant": "normal", + "rarity": 4, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/634.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%87%AA%E8%B1%AA" + }, + { + "id": 635, + "name": "10581工程", + "search_name": "10581工程", + "variant": "normal", + "rarity": 6, + "ship_type": "bbv", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/635.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bbv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/10581%E5%B7%A5%E7%A8%8B" + }, + { + "id": 636, + "name": "活泼(导弹驱逐舰)", + "search_name": "活泼", + "variant": "normal", + "rarity": 4, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/636.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B4%BB%E6%B3%BC(%E5%AF%BC%E5%BC%B9%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 637, + "name": "伊-400", + "search_name": "伊-400", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/637.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A-400" + }, + { + "id": 638, + "name": "虎(驱逐舰)", + "search_name": "虎", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/638.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%99%8E(%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 639, + "name": "阿尔戈", + "search_name": "阿尔戈", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/639.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B0%94%E6%88%88" + }, + { + "id": 640, + "name": "考文垂", + "search_name": "考文垂", + "variant": "normal", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/640.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%80%83%E6%96%87%E5%9E%82" + }, + { + "id": 641, + "name": "21工程", + "search_name": "21工程", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/641.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/21%E5%B7%A5%E7%A8%8B" + }, + { + "id": 642, + "name": "圣洛", + "search_name": "圣洛", + "variant": "normal", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/642.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E6%B4%9B" + }, + { + "id": 643, + "name": "活泼(驱逐舰)", + "search_name": "活泼", + "variant": "normal", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/643.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B4%BB%E6%B3%BC(%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 644, + "name": "伊丽莎白女王(CVA-01)", + "search_name": "伊丽莎白女王", + "variant": "normal", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/644.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E4%B8%BD%E8%8E%8E%E7%99%BD%E5%A5%B3%E7%8E%8B(CVA-01)" + }, + { + "id": 645, + "name": "U-103", + "search_name": "U-103", + "variant": "normal", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/645.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-103" + }, + { + "id": 646, + "name": "阿蒂利奥·雷戈洛", + "search_name": "阿蒂利奥·雷戈洛", + "variant": "normal", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/646.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E8%92%82%E5%88%A9%E5%A5%A5%C2%B7%E9%9B%B7%E6%88%88%E6%B4%9B" + }, + { + "id": 647, + "name": "矢矧", + "search_name": "矢矧", + "variant": "normal", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/647.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9F%A2%E7%9F%A7" + }, + { + "id": 648, + "name": "阿尔汉格尔斯克", + "search_name": "阿尔汉格尔斯克", + "variant": "normal", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/648.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E5%B0%94%E6%B1%89%E6%A0%BC%E5%B0%94%E6%96%AF%E5%85%8B" + }, + { + "id": 649, + "name": "U-177", + "search_name": "U-177", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/649.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-177" + }, + { + "id": 650, + "name": "马丁", + "search_name": "马丁", + "variant": "normal", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/650.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E4%B8%81" + }, + { + "id": 651, + "name": "卡萨比安卡", + "search_name": "卡萨比安卡", + "variant": "normal", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/651.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E8%90%A8%E6%AF%94%E5%AE%89%E5%8D%A1" + }, + { + "id": 1001, + "name": "胡德·改", + "search_name": "胡德", + "variant": "refit", + "rarity": 6, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1001.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%83%A1%E5%BE%B7" + }, + { + "id": 1002, + "name": "扶桑·改", + "search_name": "扶桑", + "variant": "refit", + "rarity": 4, + "ship_type": "bbv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1002.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bbv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%89%B6%E6%A1%91" + }, + { + "id": 1003, + "name": "山城·改", + "search_name": "山城", + "variant": "refit", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1003.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B1%B1%E5%9F%8E" + }, + { + "id": 1004, + "name": "伊势·改", + "search_name": "伊势", + "variant": "refit", + "rarity": 4, + "ship_type": "bbv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1004.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bbv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A%E5%8A%BF" + }, + { + "id": 1005, + "name": "日向·改", + "search_name": "日向", + "variant": "refit", + "rarity": 4, + "ship_type": "bbv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1005.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bbv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A5%E5%90%91" + }, + { + "id": 1006, + "name": "俾斯麦·改", + "search_name": "俾斯麦", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1006.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BF%BE%E6%96%AF%E9%BA%A6" + }, + { + "id": 1007, + "name": "提尔比茨·改", + "search_name": "提尔比茨", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1007.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8F%90%E5%B0%94%E6%AF%94%E8%8C%A8" + }, + { + "id": 1008, + "name": "纳尔逊·改", + "search_name": "纳尔逊", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1008.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%B3%E5%B0%94%E9%80%8A" + }, + { + "id": 1009, + "name": "罗德尼·改", + "search_name": "罗德尼", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1009.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BD%97%E5%BE%B7%E5%B0%BC" + }, + { + "id": 1010, + "name": "威尔士亲王·改", + "search_name": "威尔士亲王", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1010.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E5%B0%94%E5%A3%AB%E4%BA%B2%E7%8E%8B" + }, + { + "id": 1011, + "name": "内华达·改", + "search_name": "内华达", + "variant": "refit", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1011.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%86%85%E5%8D%8E%E8%BE%BE" + }, + { + "id": 1012, + "name": "俄克拉荷马·改", + "search_name": "俄克拉荷马", + "variant": "refit", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1012.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BF%84%E5%85%8B%E6%8B%89%E8%8D%B7%E9%A9%AC" + }, + { + "id": 1013, + "name": "安德烈亚·多利亚·改", + "search_name": "安德烈亚·多利亚", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/1013.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E5%BE%B7%E7%83%88%E4%BA%9A%C2%B7%E5%A4%9A%E5%88%A9%E4%BA%9A" + }, + { + "id": 1014, + "name": "金刚·改", + "search_name": "金刚", + "variant": "refit", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1014.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%91%E5%88%9A" + }, + { + "id": 1018, + "name": "声望·改", + "search_name": "声望", + "variant": "refit", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1018.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A3%B0%E6%9C%9B" + }, + { + "id": 1019, + "name": "反击·改", + "search_name": "反击", + "variant": "refit", + "rarity": 4, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1019.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%8D%E5%87%BB" + }, + { + "id": 1020, + "name": "阿拉斯加·改", + "search_name": "阿拉斯加", + "variant": "refit", + "rarity": 6, + "ship_type": "bg", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1020.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cbg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E6%8B%89%E6%96%AF%E5%8A%A0" + }, + { + "id": 1021, + "name": "关岛·改", + "search_name": "关岛", + "variant": "refit", + "rarity": 6, + "ship_type": "bg", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1021.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cbg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%B3%E5%B2%9B" + }, + { + "id": 1022, + "name": "赤城·改", + "search_name": "赤城", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1022.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B5%A4%E5%9F%8E" + }, + { + "id": 1023, + "name": "加贺·改", + "search_name": "加贺", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1023.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E8%B4%BA" + }, + { + "id": 1024, + "name": "祥凤·改", + "search_name": "祥凤", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1024.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A5%A5%E5%87%A4" + }, + { + "id": 1025, + "name": "瑞凤·改", + "search_name": "瑞凤", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1025.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%91%9E%E5%87%A4" + }, + { + "id": 1026, + "name": "百眼巨人·改", + "search_name": "百眼巨人", + "variant": "refit", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1026.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%99%BE%E7%9C%BC%E5%B7%A8%E4%BA%BA" + }, + { + "id": 1027, + "name": "兰利·改", + "search_name": "兰利", + "variant": "refit", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1027.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%B0%E5%88%A9" + }, + { + "id": 1028, + "name": "突击者·改", + "search_name": "突击者", + "variant": "refit", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1028.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%AA%81%E5%87%BB%E8%80%85" + }, + { + "id": 1029, + "name": "列克星敦(CV-2)·改", + "search_name": "列克星敦", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1029.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%97%E5%85%8B%E6%98%9F%E6%95%A6(CV-2)" + }, + { + "id": 1030, + "name": "萨拉托加·改", + "search_name": "萨拉托加", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1030.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A8%E6%8B%89%E6%89%98%E5%8A%A0" + }, + { + "id": 1031, + "name": "大黄蜂·改", + "search_name": "大黄蜂", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1031.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E9%BB%84%E8%9C%82" + }, + { + "id": 1032, + "name": "高雄·改", + "search_name": "高雄", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1032.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%AB%98%E9%9B%84" + }, + { + "id": 1033, + "name": "爱宕·改", + "search_name": "爱宕", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1033.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%B1%E5%AE%95" + }, + { + "id": 1034, + "name": "摩耶·改", + "search_name": "摩耶", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1034.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%91%A9%E8%80%B6" + }, + { + "id": 1035, + "name": "鸟海·改", + "search_name": "鸟海", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1035.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B8%9F%E6%B5%B7" + }, + { + "id": 1036, + "name": "希佩尔海军上将·改", + "search_name": "希佩尔海军上将", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1036.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%8C%E4%BD%A9%E5%B0%94%E6%B5%B7%E5%86%9B%E4%B8%8A%E5%B0%86" + }, + { + "id": 1037, + "name": "布吕歇尔(重巡洋舰)·改", + "search_name": "布吕歇尔", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1037.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E5%90%95%E6%AD%87%E5%B0%94(%E9%87%8D%E5%B7%A1%E6%B4%8B%E8%88%B0)" + }, + { + "id": 1038, + "name": "欧根亲王·改", + "search_name": "欧根亲王", + "variant": "refit", + "rarity": 6, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1038.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%AC%A7%E6%A0%B9%E4%BA%B2%E7%8E%8B" + }, + { + "id": 1039, + "name": "威奇塔·改", + "search_name": "威奇塔", + "variant": "refit", + "rarity": 5, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1039.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E5%A5%87%E5%A1%94" + }, + { + "id": 1040, + "name": "昆西·改", + "search_name": "昆西", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1040.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%98%86%E8%A5%BF" + }, + { + "id": 1041, + "name": "天龙·改", + "search_name": "天龙", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1041.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E9%BE%99" + }, + { + "id": 1042, + "name": "龙田·改", + "search_name": "龙田", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1042.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BE%99%E7%94%B0" + }, + { + "id": 1043, + "name": "北上·改", + "search_name": "北上", + "variant": "refit", + "rarity": 5, + "ship_type": "clt", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1043.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/clt.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8C%97%E4%B8%8A" + }, + { + "id": 1044, + "name": "大井·改", + "search_name": "大井", + "variant": "refit", + "rarity": 5, + "ship_type": "clt", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1044.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/clt.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E4%BA%95" + }, + { + "id": 1045, + "name": "五十铃·改", + "search_name": "五十铃", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1045.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%94%E5%8D%81%E9%93%83" + }, + { + "id": 1046, + "name": "夕张·改", + "search_name": "夕张", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1046.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%95%E5%BC%A0" + }, + { + "id": 1047, + "name": "柯尼斯堡·改", + "search_name": "柯尼斯堡", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1047.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9F%AF%E5%B0%BC%E6%96%AF%E5%A0%A1" + }, + { + "id": 1048, + "name": "卡尔斯鲁厄·改", + "search_name": "卡尔斯鲁厄", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1048.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E5%B0%94%E6%96%AF%E9%B2%81%E5%8E%84" + }, + { + "id": 1049, + "name": "科隆·改", + "search_name": "科隆", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1049.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E9%9A%86" + }, + { + "id": 1050, + "name": "天狼星·改", + "search_name": "天狼星", + "variant": "refit", + "rarity": 6, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1050.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E7%8B%BC%E6%98%9F" + }, + { + "id": 1054, + "name": "重庆", + "search_name": "重庆", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1054.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9B%99%E5%85%89%E5%A5%B3%E7%A5%9E" + }, + { + "id": 1055, + "name": "奥马哈·改", + "search_name": "奥马哈", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1055.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A5%A5%E9%A9%AC%E5%93%88" + }, + { + "id": 1056, + "name": "亚特兰大·改", + "search_name": "亚特兰大", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1056.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BA%9A%E7%89%B9%E5%85%B0%E5%A4%A7" + }, + { + "id": 1057, + "name": "朱诺(CL-52)·改", + "search_name": "朱诺", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1057.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%B1%E8%AF%BA(CL-52)" + }, + { + "id": 1058, + "name": "奥希金斯", + "search_name": "奥希金斯", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/1058.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E9%B2%81%E5%85%8B%E6%9E%97" + }, + { + "id": 1059, + "name": "海伦娜·改", + "search_name": "海伦娜", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1059.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B5%B7%E4%BC%A6%E5%A8%9C" + }, + { + "id": 1060, + "name": "宁海·改", + "search_name": "宁海", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1060.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%81%E6%B5%B7" + }, + { + "id": 1061, + "name": "平海·改", + "search_name": "平海", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1061.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B9%B3%E6%B5%B7" + }, + { + "id": 1062, + "name": "罗伯茨·改", + "search_name": "罗伯茨", + "variant": "refit", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1062.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BD%97%E4%BC%AF%E8%8C%A8" + }, + { + "id": 1063, + "name": "阿贝克隆比·改", + "search_name": "阿贝克隆比", + "variant": "refit", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1063.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%BF%E8%B4%9D%E5%85%8B%E9%9A%86%E6%AF%94" + }, + { + "id": 1064, + "name": "吹雪·改", + "search_name": "吹雪", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1064.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%B9%E9%9B%AA" + }, + { + "id": 1065, + "name": "白雪·改", + "search_name": "白雪", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1065.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%99%BD%E9%9B%AA" + }, + { + "id": 1066, + "name": "初雪·改", + "search_name": "初雪", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1066.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%9D%E9%9B%AA" + }, + { + "id": 1067, + "name": "深雪·改", + "search_name": "深雪", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1067.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B7%B1%E9%9B%AA" + }, + { + "id": 1068, + "name": "晓·改", + "search_name": "晓", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1068.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%99%93" + }, + { + "id": 1069, + "name": "信赖", + "search_name": "信赖", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1069.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%8D" + }, + { + "id": 1070, + "name": "雷·改", + "search_name": "雷", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1070.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%B7" + }, + { + "id": 1071, + "name": "电·改", + "search_name": "电", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1071.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%94%B5" + }, + { + "id": 1072, + "name": "绫波·改", + "search_name": "绫波", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1072.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%AB%E6%B3%A2" + }, + { + "id": 1073, + "name": "敷波·改", + "search_name": "敷波", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1073.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%95%B7%E6%B3%A2" + }, + { + "id": 1074, + "name": "Z1·改", + "search_name": "Z1", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1074.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z1" + }, + { + "id": 1075, + "name": "Z16·改", + "search_name": "Z16", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1075.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z16" + }, + { + "id": 1076, + "name": "Z21·改", + "search_name": "Z21", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1076.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z21" + }, + { + "id": 1077, + "name": "Z22·改", + "search_name": "Z22", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1077.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z22" + }, + { + "id": 1079, + "name": "Z28·改", + "search_name": "Z28", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1079.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z28" + }, + { + "id": 1080, + "name": "Z31·改", + "search_name": "Z31", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1080.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z31" + }, + { + "id": 1081, + "name": "紫石英·改", + "search_name": "紫石英", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1081.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B4%AB%E7%9F%B3%E8%8B%B1" + }, + { + "id": 1082, + "name": "萤火虫·改", + "search_name": "萤火虫", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1082.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%A4%E7%81%AB%E8%99%AB" + }, + { + "id": 1083, + "name": "标枪·改", + "search_name": "标枪", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1083.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%A0%87%E6%9E%AA" + }, + { + "id": 1084, + "name": "天后·改", + "search_name": "天后", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1084.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A9%E5%90%8E" + }, + { + "id": 1085, + "name": "黑背豺·改", + "search_name": "黑背豺", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1085.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%91%E8%83%8C%E8%B1%BA" + }, + { + "id": 1086, + "name": "哥萨克人·改", + "search_name": "哥萨克人", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1086.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%93%A5%E8%90%A8%E5%85%8B%E4%BA%BA" + }, + { + "id": 1087, + "name": "爱斯基摩人·改", + "search_name": "爱斯基摩人", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1087.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%88%B1%E6%96%AF%E5%9F%BA%E6%91%A9%E4%BA%BA" + }, + { + "id": 1088, + "name": "旁遮普人·改", + "search_name": "旁遮普人", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1088.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%81%E9%81%AE%E6%99%AE%E4%BA%BA" + }, + { + "id": 1089, + "name": "弗莱彻·改", + "search_name": "弗莱彻", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1089.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BC%97%E8%8E%B1%E5%BD%BB" + }, + { + "id": 1092, + "name": "布雷恩·改", + "search_name": "布雷恩", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1092.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%83%E9%9B%B7%E6%81%A9" + }, + { + "id": 1093, + "name": "基林·改", + "search_name": "基林", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1093.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E6%9E%97" + }, + { + "id": 1094, + "name": "基阿特·改", + "search_name": "基阿特", + "variant": "refit", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1094.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E9%98%BF%E7%89%B9" + }, + { + "id": 1097, + "name": "长春", + "search_name": "长春", + "variant": "refit", + "rarity": 5, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/1097.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9E%9C%E6%95%A2" + }, + { + "id": 1098, + "name": "沃克兰·改", + "search_name": "沃克兰", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/1098.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B2%83%E5%85%8B%E5%85%B0" + }, + { + "id": 1099, + "name": "空想·改", + "search_name": "空想", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/1099.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A9%BA%E6%83%B3" + }, + { + "id": 1100, + "name": "狮(战列舰)·改", + "search_name": "狮", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1100.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8B%AE(%E6%88%98%E5%88%97%E8%88%B0)" + }, + { + "id": 1101, + "name": "长门·改", + "search_name": "长门", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1101.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%95%BF%E9%97%A8" + }, + { + "id": 1102, + "name": "陆奥·改", + "search_name": "陆奥", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1102.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%99%86%E5%A5%A5" + }, + { + "id": 1105, + "name": "前卫·改", + "search_name": "前卫", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1105.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%89%8D%E5%8D%AB" + }, + { + "id": 1106, + "name": "田纳西·改", + "search_name": "田纳西", + "variant": "refit", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1106.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%94%B0%E7%BA%B3%E8%A5%BF" + }, + { + "id": 1107, + "name": "加利福尼亚·改", + "search_name": "加利福尼亚", + "variant": "refit", + "rarity": 4, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1107.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E5%88%A9%E7%A6%8F%E5%B0%BC%E4%BA%9A" + }, + { + "id": 1108, + "name": "科罗拉多·改", + "search_name": "科罗拉多", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1108.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%91%E7%BD%97%E6%8B%89%E5%A4%9A" + }, + { + "id": 1109, + "name": "马里兰·改", + "search_name": "马里兰", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1109.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E9%87%8C%E5%85%B0" + }, + { + "id": 1110, + "name": "西弗吉尼亚·改", + "search_name": "西弗吉尼亚", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1110.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%A5%BF%E5%BC%97%E5%90%89%E5%B0%BC%E4%BA%9A" + }, + { + "id": 1111, + "name": "华盛顿·改", + "search_name": "华盛顿", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1111.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%8E%E7%9B%9B%E9%A1%BF" + }, + { + "id": 1112, + "name": "维托里奥·维内托·改", + "search_name": "维托里奥·维内托", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/1112.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%B4%E6%89%98%E9%87%8C%E5%A5%A5%C2%B7%E7%BB%B4%E5%86%85%E6%89%98" + }, + { + "id": 1113, + "name": "黎塞留·改", + "search_name": "黎塞留", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/1113.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%8E%E5%A1%9E%E7%95%99" + }, + { + "id": 1117, + "name": "大凤·改", + "search_name": "大凤", + "variant": "refit", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1117.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E5%87%A4" + }, + { + "id": 1118, + "name": "齐柏林伯爵·改", + "search_name": "齐柏林伯爵", + "variant": "refit", + "rarity": 5, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1118.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BD%90%E6%9F%8F%E6%9E%97%E4%BC%AF%E7%88%B5" + }, + { + "id": 1120, + "name": "约克城·改", + "search_name": "约克城", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1120.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E5%85%8B%E5%9F%8E" + }, + { + "id": 1121, + "name": "企业·改", + "search_name": "企业", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1121.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%81%E4%B8%9A" + }, + { + "id": 1123, + "name": "博格·改", + "search_name": "博格", + "variant": "refit", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1123.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%9A%E6%A0%BC" + }, + { + "id": 1124, + "name": "追赶者·改", + "search_name": "追赶者", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1124.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%BD%E8%B5%B6%E8%80%85" + }, + { + "id": 1125, + "name": "埃罗芒什", + "search_name": "埃罗芒什", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/1125.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%A8%E5%83%8F" + }, + { + "id": 1126, + "name": "普林斯顿·改", + "search_name": "普林斯顿", + "variant": "refit", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1126.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%99%AE%E6%9E%97%E6%96%AF%E9%A1%BF" + }, + { + "id": 1130, + "name": "斯佩伯爵海军上将·改", + "search_name": "斯佩伯爵海军上将", + "variant": "refit", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1130.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%AF%E4%BD%A9%E4%BC%AF%E7%88%B5%E6%B5%B7%E5%86%9B%E4%B8%8A%E5%B0%86" + }, + { + "id": 1131, + "name": "古鹰·改", + "search_name": "古鹰", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1131.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%A4%E9%B9%B0" + }, + { + "id": 1132, + "name": "加古·改", + "search_name": "加古", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1132.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E5%8F%A4" + }, + { + "id": 1133, + "name": "青叶·改", + "search_name": "青叶", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1133.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9D%92%E5%8F%B6" + }, + { + "id": 1135, + "name": "伦敦·改", + "search_name": "伦敦", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1135.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%A6%E6%95%A6" + }, + { + "id": 1136, + "name": "肯特·改", + "search_name": "肯特", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1136.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%82%AF%E7%89%B9" + }, + { + "id": 1137, + "name": "波特兰·改", + "search_name": "波特兰", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1137.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E7%89%B9%E5%85%B0" + }, + { + "id": 1139, + "name": "彭萨科拉·改", + "search_name": "彭萨科拉", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1139.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BD%AD%E8%90%A8%E7%A7%91%E6%8B%89" + }, + { + "id": 1141, + "name": "北安普顿·改", + "search_name": "北安普顿", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1141.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8C%97%E5%AE%89%E6%99%AE%E9%A1%BF" + }, + { + "id": 1142, + "name": "休斯顿·改", + "search_name": "休斯顿", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1142.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%91%E6%96%AF%E9%A1%BF" + }, + { + "id": 1143, + "name": "新奥尔良·改", + "search_name": "新奥尔良", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1143.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%96%B0%E5%A5%A5%E5%B0%94%E8%89%AF" + }, + { + "id": 1145, + "name": "川内·改", + "search_name": "川内", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1145.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%9D%E5%86%85" + }, + { + "id": 1149, + "name": "埃姆登·改", + "search_name": "埃姆登", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1149.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%83%E5%A7%86%E7%99%BB" + }, + { + "id": 1150, + "name": "翡翠·改", + "search_name": "翡翠", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1150.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BF%A1%E7%BF%A0" + }, + { + "id": 1151, + "name": "进取·改", + "search_name": "进取", + "variant": "refit", + "rarity": 3, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1151.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%BF%9B%E5%8F%96" + }, + { + "id": 1158, + "name": "圣胡安·改", + "search_name": "圣胡安", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1158.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E8%83%A1%E5%AE%89" + }, + { + "id": 1160, + "name": "摩尔曼斯克·改", + "search_name": "摩尔曼斯克", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1160.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%91%A9%E5%B0%94%E6%9B%BC%E6%96%AF%E5%85%8B" + }, + { + "id": 1162, + "name": "逸仙·改", + "search_name": "逸仙", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1162.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%80%B8%E4%BB%99" + }, + { + "id": 1164, + "name": "秋月·改", + "search_name": "秋月", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1164.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A7%8B%E6%9C%88" + }, + { + "id": 1165, + "name": "凉月·改", + "search_name": "凉月", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1165.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%89%E6%9C%88" + }, + { + "id": 1166, + "name": "阳炎·改", + "search_name": "阳炎", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1166.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%98%B3%E7%82%8E" + }, + { + "id": 1167, + "name": "不知火·改", + "search_name": "不知火", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1167.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%8D%E7%9F%A5%E7%81%AB" + }, + { + "id": 1168, + "name": "黑潮·改", + "search_name": "黑潮", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1168.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%91%E6%BD%AE" + }, + { + "id": 1169, + "name": "丹阳", + "search_name": "丹阳", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1169.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9B%AA%E9%A3%8E" + }, + { + "id": 1171, + "name": "Z46·改", + "search_name": "Z46", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1171.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z46" + }, + { + "id": 1175, + "name": "维纳斯·改", + "search_name": "维纳斯", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1175.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BB%B4%E7%BA%B3%E6%96%AF" + }, + { + "id": 1178, + "name": "沙利文·改", + "search_name": "沙利文", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1178.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B2%99%E5%88%A9%E6%96%87" + }, + { + "id": 1181, + "name": "威廉·D·波特·改", + "search_name": "威廉·D·波特", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1181.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E5%BB%89%C2%B7D%C2%B7%E6%B3%A2%E7%89%B9" + }, + { + "id": 1182, + "name": "波特·改", + "search_name": "波特", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1182.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E7%89%B9" + }, + { + "id": 1183, + "name": "拉菲(DD-724)·改", + "search_name": "拉菲", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1183.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8B%89%E8%8F%B2(DD-724)" + }, + { + "id": 1185, + "name": "机灵", + "search_name": "机灵", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1185.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E7%B1%B3%E5%A5%91%E4%BA%9A%C2%B7%E5%86%85%E6%8B%89" + }, + { + "id": 1187, + "name": "安东尼奥·达诺利·改", + "search_name": "安东尼奥·达诺利", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/1187.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AE%89%E4%B8%9C%E5%B0%BC%E5%A5%A5%C2%B7%E8%BE%BE%E8%AF%BA%E5%88%A9" + }, + { + "id": 1188, + "name": "乌戈里尼·维瓦尔迪·改", + "search_name": "乌戈里尼·维瓦尔迪", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/1188.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B9%8C%E6%88%88%E9%87%8C%E5%B0%BC%C2%B7%E7%BB%B4%E7%93%A6%E5%B0%94%E8%BF%AA" + }, + { + "id": 1192, + "name": "塔什干·改", + "search_name": "塔什干", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1192.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%94%E4%BB%80%E5%B9%B2" + }, + { + "id": 1194, + "name": "大青花鱼·改", + "search_name": "大青花鱼", + "variant": "refit", + "rarity": 6, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1194.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E9%9D%92%E8%8A%B1%E9%B1%BC" + }, + { + "id": 1195, + "name": "射水鱼·改", + "search_name": "射水鱼", + "variant": "refit", + "rarity": 6, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1195.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B0%84%E6%B0%B4%E9%B1%BC" + }, + { + "id": 1197, + "name": "U-47·改", + "search_name": "U-47", + "variant": "refit", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1197.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-47" + }, + { + "id": 1198, + "name": "U-505·改", + "search_name": "U-505", + "variant": "refit", + "rarity": 6, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1198.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-505" + }, + { + "id": 1199, + "name": "絮库夫·改", + "search_name": "絮库夫", + "variant": "refit", + "rarity": 4, + "ship_type": "sc", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/1199.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/sc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B5%AE%E5%BA%93%E5%A4%AB" + }, + { + "id": 1205, + "name": "约克公爵·改", + "search_name": "约克公爵", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1205.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E5%85%8B%E5%85%AC%E7%88%B5" + }, + { + "id": 1206, + "name": "北卡罗来纳·改", + "search_name": "北卡罗来纳", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1206.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8C%97%E5%8D%A1%E7%BD%97%E6%9D%A5%E7%BA%B3" + }, + { + "id": 1207, + "name": "南达科他(BB-57)·改", + "search_name": "南达科他", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1207.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%97%E8%BE%BE%E7%A7%91%E4%BB%96(BB-57)" + }, + { + "id": 1209, + "name": "密苏里·改", + "search_name": "密苏里", + "variant": "refit", + "rarity": 6, + "ship_type": "bbg", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1209.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bbg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%AF%86%E8%8B%8F%E9%87%8C" + }, + { + "id": 1211, + "name": "卡约·杜伊里奥·改", + "search_name": "卡约·杜伊里奥", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/1211.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E7%BA%A6%C2%B7%E6%9D%9C%E4%BC%8A%E9%87%8C%E5%A5%A5" + }, + { + "id": 1215, + "name": "玛丽亚皇后·改", + "search_name": "玛丽亚皇后", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/1215.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%8E%9B%E4%B8%BD%E4%BA%9A%E7%9A%87%E5%90%8E" + }, + { + "id": 1219, + "name": "瑞鹤·改", + "search_name": "瑞鹤", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1219.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%91%9E%E9%B9%A4" + }, + { + "id": 1220, + "name": "翔鹤·改", + "search_name": "翔鹤", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1220.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BF%94%E9%B9%A4" + }, + { + "id": 1221, + "name": "飞龙·改", + "search_name": "飞龙", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1221.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A3%9E%E9%BE%99" + }, + { + "id": 1222, + "name": "苍龙·改", + "search_name": "苍龙", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1222.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8B%8D%E9%BE%99" + }, + { + "id": 1223, + "name": "信浓·改", + "search_name": "信浓", + "variant": "refit", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "japan", + "portrait": "assets/portraits/1223.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BF%A1%E6%B5%93" + }, + { + "id": 1224, + "name": "可畏·改", + "search_name": "可畏", + "variant": "refit", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1224.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%AF%E7%95%8F" + }, + { + "id": 1225, + "name": "黄蜂(CV-7)·改", + "search_name": "黄蜂", + "variant": "refit", + "rarity": 5, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1225.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%BB%84%E8%9C%82(CV-7)" + }, + { + "id": 1227, + "name": "飞鹰·改", + "search_name": "飞鹰", + "variant": "refit", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1227.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A3%9E%E9%B9%B0" + }, + { + "id": 1228, + "name": "隼鹰·改", + "search_name": "隼鹰", + "variant": "refit", + "rarity": 4, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1228.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9A%BC%E9%B9%B0" + }, + { + "id": 1232, + "name": "贝亚恩·改", + "search_name": "贝亚恩", + "variant": "refit", + "rarity": 4, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/1232.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B4%9D%E4%BA%9A%E6%81%A9" + }, + { + "id": 1233, + "name": "最上·改", + "search_name": "最上", + "variant": "refit", + "rarity": 5, + "ship_type": "cav", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1233.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cav.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%9C%80%E4%B8%8A" + }, + { + "id": 1238, + "name": "约克·改", + "search_name": "约克", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1238.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E5%85%8B" + }, + { + "id": 1239, + "name": "埃克赛特·改", + "search_name": "埃克赛特", + "variant": "refit", + "rarity": 4, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1239.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%83%E5%85%8B%E8%B5%9B%E7%89%B9" + }, + { + "id": 1240, + "name": "旧金山·改", + "search_name": "旧金山", + "variant": "refit", + "rarity": 5, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1240.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A7%E9%87%91%E5%B1%B1" + }, + { + "id": 1241, + "name": "巴尔的摩·改", + "search_name": "巴尔的摩", + "variant": "refit", + "rarity": 6, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1241.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%B4%E5%B0%94%E7%9A%84%E6%91%A9" + }, + { + "id": 1244, + "name": "灵敏·改", + "search_name": "灵敏", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1244.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%81%B5%E6%95%8F" + }, + { + "id": 1247, + "name": "香取·改", + "search_name": "香取", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1247.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A6%99%E5%8F%96" + }, + { + "id": 1248, + "name": "大淀·改", + "search_name": "大淀", + "variant": "refit", + "rarity": 6, + "ship_type": "cav", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1248.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cf.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A4%A7%E6%B7%80" + }, + { + "id": 1249, + "name": "莱比锡·改", + "search_name": "莱比锡", + "variant": "refit", + "rarity": 4, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1249.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8E%B1%E6%AF%94%E9%94%A1" + }, + { + "id": 1258, + "name": "高波·改", + "search_name": "高波", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1258.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%AB%98%E6%B3%A2" + }, + { + "id": 1265, + "name": "岚·改", + "search_name": "岚", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1265.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B2%9A" + }, + { + "id": 1266, + "name": "初夏·改", + "search_name": "初夏", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1266.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%9D%E5%A4%8F" + }, + { + "id": 1267, + "name": "岛风(岛风型驱逐舰)·改", + "search_name": "岛风", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1267.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B2%9B%E9%A3%8E(%E5%B2%9B%E9%A3%8E%E5%9E%8B%E9%A9%B1%E9%80%90%E8%88%B0)" + }, + { + "id": 1269, + "name": "Z17·改", + "search_name": "Z17", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1269.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z17" + }, + { + "id": 1270, + "name": "Z18·改", + "search_name": "Z18", + "variant": "refit", + "rarity": 3, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1270.webp", + "background": "assets/backgrounds/rarity-3.webp", + "frame": "assets/frames/rarity-3.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/Z18" + }, + { + "id": 1273, + "name": "塞尔弗里奇·改", + "search_name": "塞尔弗里奇", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1273.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E5%B0%94%E5%BC%97%E9%87%8C%E5%A5%87" + }, + { + "id": 1275, + "name": "康弗斯·改", + "search_name": "康弗斯", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1275.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BA%B7%E5%BC%97%E6%96%AF" + }, + { + "id": 1277, + "name": "克拉克斯顿·改", + "search_name": "克拉克斯顿", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1277.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E6%8B%89%E5%85%8B%E6%96%AF%E9%A1%BF" + }, + { + "id": 1278, + "name": "霍埃尔·改", + "search_name": "霍埃尔", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1278.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9C%8D%E5%9F%83%E5%B0%94" + }, + { + "id": 1279, + "name": "塞缪尔·B·罗伯茨·改", + "search_name": "塞缪尔·B·罗伯茨", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1279.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A1%9E%E7%BC%AA%E5%B0%94%C2%B7B%C2%B7%E7%BD%97%E4%BC%AF%E8%8C%A8" + }, + { + "id": 1280, + "name": "约翰斯顿·改", + "search_name": "约翰斯顿", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1280.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%BA%A6%E7%BF%B0%E6%96%AF%E9%A1%BF" + }, + { + "id": 1289, + "name": "U-81·改", + "search_name": "U-81", + "variant": "refit", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1289.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-81" + }, + { + "id": 1290, + "name": "U-96·改", + "search_name": "U-96", + "variant": "refit", + "rarity": 6, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1290.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-96" + }, + { + "id": 1291, + "name": "U-2540·改", + "search_name": "U-2540", + "variant": "refit", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1291.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-2540" + }, + { + "id": 1293, + "name": "U-1206·改", + "search_name": "U-1206", + "variant": "refit", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1293.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-1206" + }, + { + "id": 1299, + "name": "让巴尔·改", + "search_name": "让巴尔", + "variant": "refit", + "rarity": 6, + "ship_type": "bbg", + "size_class": "large", + "role_class": "main_force", + "country": "france", + "portrait": "assets/portraits/1299.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bbg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%AE%A9%E5%B7%B4%E5%B0%94" + }, + { + "id": 1301, + "name": "马汉·改", + "search_name": "马汉", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1301.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E6%B1%89" + }, + { + "id": 1306, + "name": "巴夫勒尔·改", + "search_name": "巴夫勒尔", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1306.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B7%B4%E5%A4%AB%E5%8B%92%E5%B0%94" + }, + { + "id": 1311, + "name": "瓜达卡纳尔·改", + "search_name": "瓜达卡纳尔", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1311.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%93%9C%E8%BE%BE%E5%8D%A1%E7%BA%B3%E5%B0%94" + }, + { + "id": 1316, + "name": "明斯克·改", + "search_name": "明斯克", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1316.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%98%8E%E6%96%AF%E5%85%8B" + }, + { + "id": 1323, + "name": "基辅·改", + "search_name": "基辅", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1323.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9F%BA%E8%BE%85" + }, + { + "id": 1325, + "name": "列克星敦(CV-16)·改", + "search_name": "列克星敦", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1325.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%88%97%E5%85%8B%E6%98%9F%E6%95%A6(CV-16)" + }, + { + "id": 1332, + "name": "神鹰·改", + "search_name": "神鹰", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1332.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%A5%9E%E9%B9%B0" + }, + { + "id": 1335, + "name": "伏尔塔·改", + "search_name": "伏尔塔", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/1335.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8F%E5%B0%94%E5%A1%94" + }, + { + "id": 1339, + "name": "帝国·改", + "search_name": "帝国", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/1339.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%B8%9D%E5%9B%BD" + }, + { + "id": 1340, + "name": "波尔扎诺·改", + "search_name": "波尔扎诺", + "variant": "refit", + "rarity": 5, + "ship_type": "cav", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/1340.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cav.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B3%A2%E5%B0%94%E6%89%8E%E8%AF%BA" + }, + { + "id": 1342, + "name": "江原·改", + "search_name": "江原", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/1342.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B1%9F%E5%8E%9F" + }, + { + "id": 1343, + "name": "蔚山·改", + "search_name": "蔚山", + "variant": "refit", + "rarity": 6, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/1343.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%94%9A%E5%B1%B1" + }, + { + "id": 1344, + "name": "忠武·改", + "search_name": "忠武", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/1344.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BF%A0%E6%AD%A6" + }, + { + "id": 1351, + "name": "U-1405·改", + "search_name": "U-1405", + "variant": "refit", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1351.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/U-1405" + }, + { + "id": 1362, + "name": "星座·改", + "search_name": "星座", + "variant": "refit", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/1362.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%98%9F%E5%BA%A7" + }, + { + "id": 1363, + "name": "炽热·改", + "search_name": "炽热", + "variant": "refit", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1363.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%82%BD%E7%83%AD" + }, + { + "id": 1366, + "name": "鹦鹉螺·改", + "search_name": "鹦鹉螺", + "variant": "refit", + "rarity": 6, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1366.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B9%A6%E9%B9%89%E8%9E%BA" + }, + { + "id": 1372, + "name": "早春·改", + "search_name": "早春", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1372.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A9%E6%98%A5" + }, + { + "id": 1380, + "name": "圣乔治·改", + "search_name": "圣乔治", + "variant": "refit", + "rarity": 6, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1380.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%9C%A3%E4%B9%94%E6%B2%BB" + }, + { + "id": 1400, + "name": "七省联盟·改", + "search_name": "七省联盟", + "variant": "refit", + "rarity": 5, + "ship_type": "cg", + "size_class": "medium", + "role_class": "escort", + "country": "other", + "portrait": "assets/portraits/1400.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%B8%83%E7%9C%81%E8%81%94%E7%9B%9F" + }, + { + "id": 1408, + "name": "鲃鱼·改", + "search_name": "鲃鱼", + "variant": "refit", + "rarity": 6, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1408.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%B2%83%E9%B1%BC" + }, + { + "id": 1413, + "name": "加里波第·改", + "search_name": "加里波第", + "variant": "refit", + "rarity": 5, + "ship_type": "cg", + "size_class": "medium", + "role_class": "escort", + "country": "italy", + "portrait": "assets/portraits/1413.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8A%A0%E9%87%8C%E6%B3%A2%E7%AC%AC" + }, + { + "id": 1414, + "name": "凯利·改", + "search_name": "凯利", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1414.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%87%AF%E5%88%A9" + }, + { + "id": 1416, + "name": "47工程·改", + "search_name": "47工程", + "variant": "refit", + "rarity": 6, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1416.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/47%E5%B7%A5%E7%A8%8B" + }, + { + "id": 1425, + "name": "史密斯·改", + "search_name": "史密斯", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1425.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8F%B2%E5%AF%86%E6%96%AF" + }, + { + "id": 1426, + "name": "彼得·施特拉塞尔·改", + "search_name": "彼得·施特拉塞尔", + "variant": "refit", + "rarity": 6, + "ship_type": "cv", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1426.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/cv.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BD%BC%E5%BE%97%C2%B7%E6%96%BD%E7%89%B9%E6%8B%89%E5%A1%9E%E5%B0%94" + }, + { + "id": 1429, + "name": "马伊·布雷泽·改", + "search_name": "马伊·布雷泽", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "france", + "portrait": "assets/portraits/1429.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%A9%AC%E4%BC%8A%C2%B7%E5%B8%83%E9%9B%B7%E6%B3%BD" + }, + { + "id": 1430, + "name": "捷尔任斯基·改", + "search_name": "捷尔任斯基", + "variant": "refit", + "rarity": 5, + "ship_type": "cg", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1430.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8D%B7%E5%B0%94%E4%BB%BB%E6%96%AF%E5%9F%BA" + }, + { + "id": 1431, + "name": "什罗普郡·改", + "search_name": "什罗普郡", + "variant": "refit", + "rarity": 5, + "ship_type": "ca", + "size_class": "medium", + "role_class": "escort", + "country": "united_kingdom", + "portrait": "assets/portraits/1431.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ca.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BB%80%E7%BD%97%E6%99%AE%E9%83%A1" + }, + { + "id": 1456, + "name": "塔林", + "search_name": "塔林", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1456.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%BD%BC%E5%BE%97%E7%BD%97%E5%B7%B4%E7%94%AB%E6%B4%9B%E5%A4%AB%E6%96%AF%E5%85%8B" + }, + { + "id": 1467, + "name": "鞍山·改", + "search_name": "鞍山", + "variant": "refit", + "rarity": 5, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "china", + "portrait": "assets/portraits/1467.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%9E%8D%E5%B1%B1" + }, + { + "id": 1483, + "name": "胜利·改", + "search_name": "胜利", + "variant": "refit", + "rarity": 6, + "ship_type": "av", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/1483.webp", + "background": "assets/backgrounds/rarity-6.webp", + "frame": "assets/frames/rarity-6.webp", + "type_icon": "assets/type-icons/av.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%83%9C%E5%88%A9" + }, + { + "id": 1490, + "name": "克劳塞维茨·改", + "search_name": "克劳塞维茨", + "variant": "refit", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1490.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%85%8B%E5%8A%B3%E5%A1%9E%E7%BB%B4%E8%8C%A8" + }, + { + "id": 1496, + "name": "诺福克(DL-1)·改", + "search_name": "诺福克", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1496.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%AF%BA%E7%A6%8F%E5%85%8B(DL-1)" + }, + { + "id": 1503, + "name": "T-23·改", + "search_name": "T-23", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1503.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/T-23" + }, + { + "id": 1509, + "name": "谦逊·改", + "search_name": "谦逊", + "variant": "refit", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1509.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%B0%A6%E9%80%8A" + }, + { + "id": 1516, + "name": "日丹诺夫·改", + "search_name": "日丹诺夫", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1516.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%97%A5%E4%B8%B9%E8%AF%BA%E5%A4%AB" + }, + { + "id": 1517, + "name": "吕贝克·改", + "search_name": "吕贝克", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1517.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%90%95%E8%B4%9D%E5%85%8B" + }, + { + "id": 1519, + "name": "T.995·改", + "search_name": "T.995", + "variant": "refit", + "rarity": 4, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1519.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/T.995" + }, + { + "id": 1527, + "name": "CNT巡洋舰·改", + "search_name": "CNT巡洋舰", + "variant": "refit", + "rarity": 5, + "ship_type": "cl", + "size_class": "medium", + "role_class": "escort", + "country": "china", + "portrait": "assets/portraits/1527.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/CNT%E5%B7%A1%E6%B4%8B%E8%88%B0" + }, + { + "id": 1528, + "name": "芝加哥(CA-136)·改", + "search_name": "芝加哥", + "variant": "refit", + "rarity": 5, + "ship_type": "cg", + "size_class": "medium", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1528.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%8A%9D%E5%8A%A0%E5%93%A5(CA-136)" + }, + { + "id": 1530, + "name": "M-296·改", + "search_name": "M-296", + "variant": "refit", + "rarity": 5, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1530.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/M-296" + }, + { + "id": 1532, + "name": "但丁·改", + "search_name": "但丁", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/1532.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BD%86%E4%B8%81" + }, + { + "id": 1538, + "name": "伊-25·改", + "search_name": "伊-25", + "variant": "refit", + "rarity": 4, + "ship_type": "ss", + "size_class": "small", + "role_class": "escort", + "country": "japan", + "portrait": "assets/portraits/1538.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/ss.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%8A-25" + }, + { + "id": 1545, + "name": "威悉河·改", + "search_name": "威悉河", + "variant": "refit", + "rarity": 5, + "ship_type": "cvl", + "size_class": "medium", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1545.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/cvl.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E6%82%89%E6%B2%B3" + }, + { + "id": 1560, + "name": "石勒苏益格-荷尔施泰因·改", + "search_name": "石勒苏益格-荷尔施泰因", + "variant": "refit", + "rarity": 4, + "ship_type": "bm", + "size_class": "small", + "role_class": "escort", + "country": "germany", + "portrait": "assets/portraits/1560.webp", + "background": "assets/backgrounds/rarity-4.webp", + "frame": "assets/frames/rarity-4.webp", + "type_icon": "assets/type-icons/bm.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%9F%B3%E5%8B%92%E8%8B%8F%E7%9B%8A%E6%A0%BC-%E8%8D%B7%E5%B0%94%E6%96%BD%E6%B3%B0%E5%9B%A0" + }, + { + "id": 1575, + "name": "威武·改", + "search_name": "威武", + "variant": "refit", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "soviet_union", + "portrait": "assets/portraits/1575.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%A8%81%E6%AD%A6" + }, + { + "id": 1583, + "name": "卡拉乔洛·改", + "search_name": "卡拉乔洛", + "variant": "refit", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "italy", + "portrait": "assets/portraits/1583.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%A1%E6%8B%89%E4%B9%94%E6%B4%9B" + }, + { + "id": 1589, + "name": "休·W·哈德利·改", + "search_name": "休·W·哈德利", + "variant": "refit", + "rarity": 5, + "ship_type": "dd", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1589.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/dd.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E4%BC%91%C2%B7W%C2%B7%E5%93%88%E5%BE%B7%E5%88%A9" + }, + { + "id": 1590, + "name": "金伯恩·改", + "search_name": "金伯恩", + "variant": "refit", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "soviet_union", + "portrait": "assets/portraits/1590.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E9%87%91%E4%BC%AF%E6%81%A9" + }, + { + "id": 1606, + "name": "汉堡·改", + "search_name": "汉堡", + "variant": "refit", + "rarity": 5, + "ship_type": "asdg", + "size_class": "small", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/1606.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddg.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%B1%89%E5%A0%A1" + }, + { + "id": 1618, + "name": "米切尔·改", + "search_name": "米切尔", + "variant": "refit", + "rarity": 5, + "ship_type": "aadg", + "size_class": "small", + "role_class": "escort", + "country": "united_states", + "portrait": "assets/portraits/1618.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/ddgaa.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E7%B1%B3%E5%88%87%E5%B0%94" + }, + { + "id": 8007, + "name": "提尔比茨(儿童节)", + "search_name": "提尔比茨", + "variant": "special", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "germany", + "portrait": "assets/portraits/8007.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%8F%90%E5%B0%94%E6%AF%94%E8%8C%A8" + }, + { + "id": 8009, + "name": "萝德尼", + "search_name": "萝德尼", + "variant": "special", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_kingdom", + "portrait": "assets/portraits/8009.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E8%90%9D%E5%BE%B7%E5%B0%BC" + }, + { + "id": 8111, + "name": "华盛顿(儿童节)", + "search_name": "华盛顿", + "variant": "special", + "rarity": 5, + "ship_type": "bb", + "size_class": "large", + "role_class": "main_force", + "country": "united_states", + "portrait": "assets/portraits/8111.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bb.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E5%8D%8E%E7%9B%9B%E9%A1%BF" + }, + { + "id": 8116, + "name": "戈本", + "search_name": "戈本", + "variant": "special", + "rarity": 5, + "ship_type": "bc", + "size_class": "large", + "role_class": "main_force", + "country": "other", + "portrait": "assets/portraits/8116.webp", + "background": "assets/backgrounds/rarity-5.webp", + "frame": "assets/frames/rarity-5.webp", + "type_icon": "assets/type-icons/bc.webp", + "wiki_url": "https://www.zjsnrwiki.com/wiki/%E6%88%88%E6%9C%AC" + } + ] +} diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-1-1.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-1-1.yaml" new file mode 100644 index 0000000..ea6e661 --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-1-1.yaml" @@ -0,0 +1,98 @@ +chapter: 1 +map: 1 +selected_nodes: ['0', A, B] +endpoint_nodes: [B] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + B: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} + A: {formation: 2, night: false, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 4日驱周常队 + ships: + - name: 岛风(岛风型驱逐舰)·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 岛风 + ship_type: [dd] + min_level: 100 + - name: 初夏·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 初夏 + ship_type: [dd] + min_level: 100 + - name: 高波·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 高波 + ship_type: [dd] + min_level: 100 + - name: 阳炎·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 阳炎 + ship_type: [dd] + min_level: 100 + - name: 4德驱周常队 + ships: + - name: Z28·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: Z28 + ship_type: [dd] + min_level: 100 + - name: T-23·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: T-23 + ship_type: [dd] + min_level: 100 + - name: 吕贝克·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 + - name: Z46·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: Z46 + ship_type: [dd] + min_level: 100 diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-10-1.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-10-1.yaml" new file mode 100644 index 0000000..ce85e44 --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-10-1.yaml" @@ -0,0 +1,84 @@ +# 周常第10章 — 地图 10-1 +# 路线: B-D-F-G-H-L-M-O (L 迂回) +chapter: 10 +map: 1 +selected_nodes: ['0', O, A, I, K, N] +endpoint_nodes: [O] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + O: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} + A: {formation: 2, night: false, long_missile_support: false, proceed: true, detour: true} + I: {formation: 2, night: false, long_missile_support: false, proceed: true, detour: false} + K: {formation: 2, night: false, long_missile_support: false, proceed: true, detour: false} + N: {formation: 2, night: false, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 重巡周常队 + ships: + - name: 巴尔的摩·改 + candidates: + - {name: 旧金山·改, search_name: 旧金山, ship_type: [ca], min_level: 100} + - {name: 彭萨科拉·改, search_name: 彭萨科拉, ship_type: [ca], min_level: 100} + - {name: 昆西·改, search_name: 昆西, ship_type: [ca], min_level: 100} + - {name: 威奇塔·改, search_name: 威奇塔, ship_type: [ca], min_level: 100} + - {name: 萨勒姆, ship_type: [ca], min_level: 100} + - {name: 德梅因, ship_type: [ca], min_level: 100} + - {name: 什罗普郡·改, search_name: 什罗普郡, ship_type: [ca], min_level: 100} + - {name: 埃克赛特·改, search_name: 埃克赛特, ship_type: [ca], min_level: 100} + - {name: 约克·改, search_name: 约克, ship_type: [ca], min_level: 100} + - {name: 欧根亲王·改, search_name: 欧根亲王, ship_type: [ca], min_level: 100} + search_name: 巴尔的摩 + ship_type: [ca] + min_level: 100 + - name: 昆西·改 + candidates: + - {name: 旧金山·改, search_name: 旧金山, ship_type: [ca], min_level: 100} + - {name: 彭萨科拉·改, search_name: 彭萨科拉, ship_type: [ca], min_level: 100} + - {name: 昆西·改, search_name: 昆西, ship_type: [ca], min_level: 100} + - {name: 威奇塔·改, search_name: 威奇塔, ship_type: [ca], min_level: 100} + - {name: 萨勒姆, ship_type: [ca], min_level: 100} + - {name: 德梅因, ship_type: [ca], min_level: 100} + - {name: 什罗普郡·改, search_name: 什罗普郡, ship_type: [ca], min_level: 100} + - {name: 埃克赛特·改, search_name: 埃克赛特, ship_type: [ca], min_level: 100} + - {name: 约克·改, search_name: 约克, ship_type: [ca], min_level: 100} + - {name: 欧根亲王·改, search_name: 欧根亲王, ship_type: [ca], min_level: 100} + search_name: 昆西 + ship_type: [ca] + min_level: 100 + - name: 海伦娜·改 + candidates: + - {name: 圣胡安·改, search_name: 圣胡安, ship_type: [cl], min_level: 100} + - {name: 朱诺(CL-52)·改, search_name: 朱诺, ship_type: [cl], min_level: 100} + - {name: 亚特兰大·改, search_name: 亚特兰大, ship_type: [cl], min_level: 100} + search_name: 海伦娜 + ship_type: [cl] + min_level: 100 + - name: 欧根亲王·改 + candidates: + - {name: 旧金山·改, search_name: 旧金山, ship_type: [ca], min_level: 100} + - {name: 彭萨科拉·改, search_name: 彭萨科拉, ship_type: [ca], min_level: 100} + - {name: 昆西·改, search_name: 昆西, ship_type: [ca], min_level: 100} + - {name: 威奇塔·改, search_name: 威奇塔, ship_type: [ca], min_level: 100} + - {name: 萨勒姆, ship_type: [ca], min_level: 100} + - {name: 德梅因, ship_type: [ca], min_level: 100} + - {name: 什罗普郡·改, search_name: 什罗普郡, ship_type: [ca], min_level: 100} + - {name: 埃克赛特·改, search_name: 埃克赛特, ship_type: [ca], min_level: 100} + - {name: 约克·改, search_name: 约克, ship_type: [ca], min_level: 100} + - {name: 欧根亲王·改, search_name: 欧根亲王, ship_type: [ca], min_level: 100} + search_name: 欧根亲王 + ship_type: [ca] + min_level: 100 + - name: 塔林 + candidates: + - {name: 圣胡安·改, search_name: 圣胡安, ship_type: [cl], min_level: 100} + - {name: 朱诺(CL-52)·改, search_name: 朱诺, ship_type: [cl], min_level: 100} + - {name: 亚特兰大·改, search_name: 亚特兰大, ship_type: [cl], min_level: 100} + ship_type: [cl] + min_level: 100 + - name: 85工程 + ship_type: [cv] + min_level: 100 diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-2-1.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-2-1.yaml" new file mode 100644 index 0000000..335a3a6 --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-2-1.yaml" @@ -0,0 +1,99 @@ +# 周常第2章 — 地图 2-1 +# 默认复纵阵 +chapter: 2 +map: 1 +selected_nodes: ['0', B, D, F] +endpoint_nodes: [F] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + F: {formation: 2, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +result: S +fleet_id: 3 +fleet_presets: + - name: 4日驱周常队 + ships: + - name: 岛风(岛风型驱逐舰)·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 岛风 + ship_type: [dd] + min_level: 100 + - name: 初夏·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 初夏 + ship_type: [dd] + min_level: 100 + - name: 高波·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 高波 + ship_type: [dd] + min_level: 100 + - name: 阳炎·改 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + search_name: 阳炎 + ship_type: [dd] + min_level: 100 + - name: 4德驱周常队 + ships: + - name: Z28·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: Z28 + ship_type: [dd] + min_level: 100 + - name: T-23·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: T-23 + ship_type: [dd] + min_level: 100 + - name: 吕贝克·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 + - name: Z46·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: Z46 + ship_type: [dd] + min_level: 100 diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-3-1.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-3-1.yaml" new file mode 100644 index 0000000..9626d2a --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-3-1.yaml" @@ -0,0 +1,89 @@ +# 周常第3章 — 地图 3-1 +# 默认复纵阵 +chapter: 3 +map: 1 +selected_nodes: ['0', A, B, C, E, F, H, I] +endpoint_nodes: [I] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + I: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 6日驱队 + ships: + - name: 岛风(岛风型驱逐舰)·改 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + search_name: 岛风 + ship_type: [dd] + min_level: 100 + - name: 初夏·改 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + search_name: 初夏 + ship_type: [dd] + min_level: 100 + - name: 高波·改 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + search_name: 高波 + ship_type: [dd] + min_level: 100 + - name: 早春·改 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + search_name: 早春 + ship_type: [dd] + min_level: 100 + - name: 吹雪·改 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + search_name: 吹雪 + ship_type: [dd] + min_level: 100 + - name: 阳炎·改 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + search_name: 阳炎 + ship_type: [dd] + min_level: 100 diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-4-1.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-4-1.yaml" new file mode 100644 index 0000000..fc5d8fa --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-4-1.yaml" @@ -0,0 +1,53 @@ +# 周常第4章 — 地图 4-1 +# 默认复纵阵 +chapter: 4 +map: 1 +selected_nodes: ['0', A, B, C, D, F, G, I] +endpoint_nodes: [I] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + I: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 85工程+3德驱队 + ships: + - name: 85工程 + ship_type: [cv] + min_level: 100 + - name: Z28·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + search_name: Z28 + ship_type: [dd] + min_level: 100 + - name: T-23·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + search_name: T-23 + ship_type: [dd] + min_level: 100 + - name: 吕贝克·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-5-5.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-5-5.yaml" new file mode 100644 index 0000000..dfb48de --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-5-5.yaml" @@ -0,0 +1,95 @@ +# 周常第5章 — 地图 5-5 +# 默认复纵阵 +chapter: 5 +map: 5 +selected_nodes: ['0', C, F, H, I] +endpoint_nodes: [I] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + I: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 6潜艇队 + ships: + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-6-4.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-6-4.yaml" new file mode 100644 index 0000000..784b981 --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-6-4.yaml" @@ -0,0 +1,68 @@ +# 周常第6章 — 地图 6-4 +# 默认复纵阵 +chapter: 6 +map: 4 +selected_nodes: ['0', A, C, F, H, J, K] +endpoint_nodes: [K] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + K: {formation: 2, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 3驱克劳85工程队 + ships: + - name: Z28·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: Z28 + ship_type: [dd] + min_level: 100 + - name: T-23·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: T-23 + ship_type: [dd] + min_level: 100 + - name: 吕贝克·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 + - name: 克劳塞维茨·改 + search_name: 克劳塞维茨 + ship_type: [bc] + min_level: 100 + - name: 塞尔弗里奇·改 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + search_name: 塞尔弗里奇 + ship_type: [dd] + min_level: 100 + - name: 85工程 + ship_type: [cv] + min_level: 100 diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-7-4.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-7-4.yaml" new file mode 100644 index 0000000..4bbe5d9 --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-7-4.yaml" @@ -0,0 +1,95 @@ +# 周常第7章 — 地图 7-4 +# 默认复纵阵,Boss(M点)梯形阵+夜战 +chapter: 7 +map: 4 +selected_nodes: ['0', B, D, E, G, H, L, K, M] +endpoint_nodes: [M] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + M: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 6潜艇队 + ships: + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-8-2.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-8-2.yaml" new file mode 100644 index 0000000..7dbb0cb --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-8-2.yaml" @@ -0,0 +1,45 @@ +# 周常第8章 — 地图 8-2 +# 默认复纵阵 +chapter: 8 +map: 2 +selected_nodes: ['0', B, J, M] +endpoint_nodes: [M] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + M: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +result: S +fleet_id: 3 +fleet_presets: + - name: 大淀85AIII队 + ships: + - name: 大淀(苍青幻影) + candidates: + - {name: 海伦娜·改, search_name: 海伦娜, ship_type: [cl], min_level: 100} + - {name: 圣胡安·改, search_name: 圣胡安, ship_type: [cl], min_level: 100} + - {name: 朱诺(CL-52)·改, search_name: 朱诺, ship_type: [cl], min_level: 100} + - {name: 亚特兰大·改, search_name: 亚特兰大, ship_type: [cl], min_level: 100} + search_name: 大淀 + ship_type: [cl] + min_level: 100 + - name: 85工程 + ship_type: [cv] + min_level: 100 + - name: AIII + ship_type: [bbv] + min_level: 100 + - min_level: 100 + candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} diff --git "a/resource/system_battle_plans/bettle-\345\221\250\345\270\270-9-2.yaml" "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-9-2.yaml" new file mode 100644 index 0000000..b99147b --- /dev/null +++ "b/resource/system_battle_plans/bettle-\345\221\250\345\270\270-9-2.yaml" @@ -0,0 +1,78 @@ +# 周常第9章 — 地图 9-2 +# 默认复纵阵 +chapter: 9 +map: 2 +selected_nodes: ['0', A, D, G, H, M, O] +endpoint_nodes: [O] +fight_condition: 1 +repair_mode: 1 +node_defaults: {formation: 2, night: false, proceed: true} +node_args: + A: {sl_when_detour_fails: true, detour: true} + D: {sl_when_detour_fails: true, detour: true} + G: {sl_when_detour_fails: true, detour: true} + H: {sl_when_detour_fails: true, detour: true} + M: {sl_when_detour_fails: true, detour: true} + O: {formation: 4, night: true, long_missile_support: false, proceed: true, detour: false} +times: 3 +fleet_id: 3 +result: S +fleet_presets: + - name: 85AIII4防驱队 + ships: + - name: 85工程 + ship_type: [cv] + min_level: 100 + - name: AIII + ship_type: [bbv] + min_level: 100 + - name: 炽热·改 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + search_name: 炽热 + ship_type: [aadg] + min_level: 100 + - name: 考文垂 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + ship_type: [aadg] + min_level: 100 + - name: 絮弗伦 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + ship_type: [aadg] + min_level: 100 + - name: 基阿特·改 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + search_name: 基阿特 + ship_type: [aadg] + min_level: 100 diff --git "a/resource/system_daily_plans/campaign-\345\233\260\351\232\276\345\267\241\346\264\213.yaml" "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\345\267\241\346\264\213.yaml" new file mode 100644 index 0000000..d6b17aa --- /dev/null +++ "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\345\267\241\346\264\213.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 困难巡洋 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\345\233\260\351\232\276\346\210\230\345\210\227.yaml" "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\346\210\230\345\210\227.yaml" new file mode 100644 index 0000000..017b05a --- /dev/null +++ "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\346\210\230\345\210\227.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 困难战列 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\345\233\260\351\232\276\346\275\234\350\211\207.yaml" "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\346\275\234\350\211\207.yaml" new file mode 100644 index 0000000..61fc617 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\346\275\234\350\211\207.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 困难潜艇 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\345\233\260\351\232\276\350\210\252\346\257\215.yaml" "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\350\210\252\346\257\215.yaml" new file mode 100644 index 0000000..356b187 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\350\210\252\346\257\215.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 困难航母 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\345\233\260\351\232\276\351\251\261\351\200\220.yaml" "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\351\251\261\351\200\220.yaml" new file mode 100644 index 0000000..217bc8d --- /dev/null +++ "b/resource/system_daily_plans/campaign-\345\233\260\351\232\276\351\251\261\351\200\220.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 困难驱逐 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\346\231\256\351\200\232\345\267\241\346\264\213.yaml" "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\345\267\241\346\264\213.yaml" new file mode 100644 index 0000000..10f4142 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\345\267\241\346\264\213.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 简单巡洋 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\346\231\256\351\200\232\346\210\230\345\210\227.yaml" "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\346\210\230\345\210\227.yaml" new file mode 100644 index 0000000..5856790 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\346\210\230\345\210\227.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 简单战列 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\346\231\256\351\200\232\346\275\234\350\211\207.yaml" "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\346\275\234\350\211\207.yaml" new file mode 100644 index 0000000..18b1045 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\346\275\234\350\211\207.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 简单潜艇 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\346\231\256\351\200\232\350\210\252\346\257\215.yaml" "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\350\210\252\346\257\215.yaml" new file mode 100644 index 0000000..b45cd14 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\350\210\252\346\257\215.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 简单航母 +times: 1 diff --git "a/resource/system_daily_plans/campaign-\346\231\256\351\200\232\351\251\261\351\200\220.yaml" "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\351\251\261\351\200\220.yaml" new file mode 100644 index 0000000..b41e064 --- /dev/null +++ "b/resource/system_daily_plans/campaign-\346\231\256\351\200\232\351\251\261\351\200\220.yaml" @@ -0,0 +1,3 @@ +task_type: campaign +campaign_name: 简单驱逐 +times: 1 diff --git "a/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2541\347\253\240.yaml" "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2541\347\253\240.yaml" new file mode 100644 index 0000000..2c1ec11 --- /dev/null +++ "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2541\347\253\240.yaml" @@ -0,0 +1,33 @@ +task_type: decisive +chapter: 1 +times: 1 +use_quick_repair: true +level1: + - U-47 + - U-1405 + - U-1206 + - U-2540 + - U-81 + - U-96 +level2: + - U-505 + - 射水鱼 + - 大青花鱼 + - M-296 + - 鹦鹉螺 + - S-49 + - IIIA + - K-21 + - U-441 + - 潜甲 + - 潜乙 + - 伊-201 + - 伊-25 + - 鲃鱼 + - 伊-400 + - 激流 + - U-4501 + - U-459 + - U-14 + - U-35 + - K1 diff --git "a/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2542\347\253\240.yaml" "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2542\347\253\240.yaml" new file mode 100644 index 0000000..1916e86 --- /dev/null +++ "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2542\347\253\240.yaml" @@ -0,0 +1,33 @@ +task_type: decisive +chapter: 2 +times: 1 +use_quick_repair: true +level1: + - U-47 + - U-1405 + - U-1206 + - U-2540 + - U-81 + - U-96 +level2: + - U-505 + - 射水鱼 + - 大青花鱼 + - M-296 + - 鹦鹉螺 + - S-49 + - IIIA + - K-21 + - U-441 + - 潜甲 + - 潜乙 + - 伊-201 + - 伊-25 + - 鲃鱼 + - 伊-400 + - 激流 + - U-4501 + - U-459 + - U-14 + - U-35 + - K1 diff --git "a/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2543\347\253\240.yaml" "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2543\347\253\240.yaml" new file mode 100644 index 0000000..044d4ea --- /dev/null +++ "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2543\347\253\240.yaml" @@ -0,0 +1,33 @@ +task_type: decisive +chapter: 3 +times: 1 +use_quick_repair: true +level1: + - U-47 + - U-1405 + - U-1206 + - U-2540 + - U-81 + - U-96 +level2: + - U-505 + - 射水鱼 + - 大青花鱼 + - M-296 + - 鹦鹉螺 + - S-49 + - IIIA + - K-21 + - U-441 + - 潜甲 + - 潜乙 + - 伊-201 + - 伊-25 + - 鲃鱼 + - 伊-400 + - 激流 + - U-4501 + - U-459 + - U-14 + - U-35 + - K1 diff --git "a/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2544\347\253\240.yaml" "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2544\347\253\240.yaml" new file mode 100644 index 0000000..6d09e28 --- /dev/null +++ "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2544\347\253\240.yaml" @@ -0,0 +1,33 @@ +task_type: decisive +chapter: 4 +times: 1 +use_quick_repair: true +level1: + - U-47 + - U-1405 + - U-1206 + - U-2540 + - U-81 + - U-96 +level2: + - U-505 + - 射水鱼 + - 大青花鱼 + - M-296 + - 鹦鹉螺 + - S-49 + - IIIA + - K-21 + - U-441 + - 潜甲 + - 潜乙 + - 伊-201 + - 伊-25 + - 鲃鱼 + - 伊-400 + - 激流 + - U-4501 + - U-459 + - U-14 + - U-35 + - K1 diff --git "a/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2545\347\253\240.yaml" "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2545\347\253\240.yaml" new file mode 100644 index 0000000..cb06364 --- /dev/null +++ "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2545\347\253\240.yaml" @@ -0,0 +1,33 @@ +task_type: decisive +chapter: 5 +times: 1 +use_quick_repair: true +level1: + - U-47 + - U-1405 + - U-1206 + - U-2540 + - U-81 + - U-96 +level2: + - U-505 + - 射水鱼 + - 大青花鱼 + - M-296 + - 鹦鹉螺 + - S-49 + - IIIA + - K-21 + - U-441 + - 潜甲 + - 潜乙 + - 伊-201 + - 伊-25 + - 鲃鱼 + - 伊-400 + - 激流 + - U-4501 + - U-459 + - U-14 + - U-35 + - K1 diff --git "a/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2546\347\253\240.yaml" "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2546\347\253\240.yaml" new file mode 100644 index 0000000..262b2b2 --- /dev/null +++ "b/resource/system_daily_plans/decisive-\345\206\263\346\210\230\347\254\2546\347\253\240.yaml" @@ -0,0 +1,33 @@ +task_type: decisive +chapter: 6 +times: 1 +use_quick_repair: true +level1: + - U-47 + - U-1405 + - U-1206 + - U-2540 + - U-81 + - U-96 +level2: + - U-505 + - 射水鱼 + - 大青花鱼 + - M-296 + - 鹦鹉螺 + - S-49 + - IIIA + - K-21 + - U-441 + - 潜甲 + - 潜乙 + - 伊-201 + - 伊-25 + - 鲃鱼 + - 伊-400 + - 激流 + - U-4501 + - U-459 + - U-14 + - U-35 + - K1 diff --git "a/resource/system_daily_plans/exercise-\351\230\237\344\274\2151\346\274\224\344\271\240.yaml" "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2151\346\274\224\344\271\240.yaml" new file mode 100644 index 0000000..5688756 --- /dev/null +++ "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2151\346\274\224\344\271\240.yaml" @@ -0,0 +1,3 @@ +task_type: exercise +fleet_id: 1 +times: 1 diff --git "a/resource/system_daily_plans/exercise-\351\230\237\344\274\2152\346\274\224\344\271\240.yaml" "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2152\346\274\224\344\271\240.yaml" new file mode 100644 index 0000000..350a85a --- /dev/null +++ "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2152\346\274\224\344\271\240.yaml" @@ -0,0 +1,3 @@ +task_type: exercise +fleet_id: 2 +times: 1 diff --git "a/resource/system_daily_plans/exercise-\351\230\237\344\274\2153\346\274\224\344\271\240.yaml" "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2153\346\274\224\344\271\240.yaml" new file mode 100644 index 0000000..a9a53f1 --- /dev/null +++ "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2153\346\274\224\344\271\240.yaml" @@ -0,0 +1,3 @@ +task_type: exercise +fleet_id: 3 +times: 1 diff --git "a/resource/system_daily_plans/exercise-\351\230\237\344\274\2154\346\274\224\344\271\240.yaml" "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2154\346\274\224\344\271\240.yaml" new file mode 100644 index 0000000..50b0a6d --- /dev/null +++ "b/resource/system_daily_plans/exercise-\351\230\237\344\274\2154\346\274\224\344\271\240.yaml" @@ -0,0 +1,3 @@ +task_type: exercise +fleet_id: 4 +times: 1 diff --git a/resource/system_team_plans/.gitkeep b/resource/system_team_plans/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/resource/system_team_plans/.gitkeep @@ -0,0 +1 @@ + diff --git "a/resource/system_team_plans/team-3\351\251\261\345\205\213\345\212\26385\345\267\245\347\250\213\351\230\237.yaml" "b/resource/system_team_plans/team-3\351\251\261\345\205\213\345\212\26385\345\267\245\347\250\213\351\230\237.yaml" new file mode 100644 index 0000000..e7c4bd1 --- /dev/null +++ "b/resource/system_team_plans/team-3\351\251\261\345\205\213\345\212\26385\345\267\245\347\250\213\351\230\237.yaml" @@ -0,0 +1,53 @@ +name: 3驱克劳85工程队 +ships: + - name: Z28·改 + search_name: Z28 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: T-23·改 + search_name: T-23 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: 吕贝克·改 + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: 克劳塞维茨·改 + search_name: 克劳塞维茨 + ship_type: [bc] + min_level: 100 + - name: 塞尔弗里奇·改 + search_name: 塞尔弗里奇 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: 85工程 + ship_type: [cv] + min_level: 100 diff --git "a/resource/system_team_plans/team-4\345\276\267\351\251\261\345\221\250\345\270\270\351\230\237.yaml" "b/resource/system_team_plans/team-4\345\276\267\351\251\261\345\221\250\345\270\270\351\230\237.yaml" new file mode 100644 index 0000000..fb3c463 --- /dev/null +++ "b/resource/system_team_plans/team-4\345\276\267\351\251\261\345\221\250\345\270\270\351\230\237.yaml" @@ -0,0 +1,42 @@ +name: 4德驱周常队 +ships: + - name: Z28·改 + search_name: Z28 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: T-23·改 + search_name: T-23 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: 吕贝克·改 + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - name: Z46·改 + search_name: Z46 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} diff --git "a/resource/system_team_plans/team-4\346\227\245\351\251\261\345\221\250\345\270\270\351\230\237.yaml" "b/resource/system_team_plans/team-4\346\227\245\351\251\261\345\221\250\345\270\270\351\230\237.yaml" new file mode 100644 index 0000000..965f719 --- /dev/null +++ "b/resource/system_team_plans/team-4\346\227\245\351\251\261\345\221\250\345\270\270\351\230\237.yaml" @@ -0,0 +1,42 @@ +name: 4日驱周常队 +ships: + - name: 岛风(岛风型驱逐舰)·改 + search_name: 岛风 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - name: 初夏·改 + search_name: 初夏 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - name: 高波·改 + search_name: 高波 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - name: 阳炎·改 + search_name: 阳炎 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 吹雪·改, search_name: 吹雪, ship_type: [dd], min_level: 100} + - {name: 早春·改, search_name: 早春, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} diff --git "a/resource/system_team_plans/team-6\346\227\245\351\251\261\351\230\237.yaml" "b/resource/system_team_plans/team-6\346\227\245\351\251\261\351\230\237.yaml" new file mode 100644 index 0000000..0d33d51 --- /dev/null +++ "b/resource/system_team_plans/team-6\346\227\245\351\251\261\351\230\237.yaml" @@ -0,0 +1,74 @@ +name: 6日驱队 +ships: + - name: 岛风(岛风型驱逐舰)·改 + search_name: 岛风 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + - name: 初夏·改 + search_name: 初夏 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + - name: 高波·改 + search_name: 高波 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + - name: 早春·改 + search_name: 早春 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + - name: 吹雪·改 + search_name: 吹雪 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} + - name: 阳炎·改 + search_name: 阳炎 + ship_type: [dd] + min_level: 100 + candidates: + - {name: 黑潮·改, search_name: 黑潮, ship_type: [dd], min_level: 100} + - {name: 不知火·改, search_name: 不知火, ship_type: [dd], min_level: 100} + - {name: 敷波·改, search_name: 敷波, ship_type: [dd], min_level: 100} + - {name: 绫波·改, search_name: 绫波, ship_type: [dd], min_level: 100} + - {name: 初雪·改, search_name: 初雪, ship_type: [dd], min_level: 100} + - {name: 深雪·改, search_name: 深雪, ship_type: [dd], min_level: 100} + - {name: 白雪·改, search_name: 白雪, ship_type: [dd], min_level: 100} diff --git "a/resource/system_team_plans/team-6\346\275\234\350\211\207\351\230\237.yaml" "b/resource/system_team_plans/team-6\346\275\234\350\211\207\351\230\237.yaml" new file mode 100644 index 0000000..e9b0957 --- /dev/null +++ "b/resource/system_team_plans/team-6\346\275\234\350\211\207\351\230\237.yaml" @@ -0,0 +1,74 @@ +name: 6潜艇队 +ships: + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} diff --git "a/resource/system_team_plans/team-85AIII4\351\230\262\351\251\261\351\230\237.yaml" "b/resource/system_team_plans/team-85AIII4\351\230\262\351\251\261\351\230\237.yaml" new file mode 100644 index 0000000..d0fe89c --- /dev/null +++ "b/resource/system_team_plans/team-85AIII4\351\230\262\351\251\261\351\230\237.yaml" @@ -0,0 +1,58 @@ +name: 85AIII4防驱队 +ships: + - name: 85工程 + ship_type: [cv] + min_level: 100 + - name: AIII + ship_type: [bbv] + min_level: 100 + - name: 炽热·改 + search_name: 炽热 + ship_type: [aadg] + min_level: 100 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + - name: 考文垂 + ship_type: [aadg] + min_level: 100 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + - name: 絮弗伦 + ship_type: [aadg] + min_level: 100 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} + - name: 基阿特·改 + search_name: 基阿特 + ship_type: [aadg] + min_level: 100 + candidates: + - {name: 米切尔·改, search_name: 米切尔, ship_type: [aadg], min_level: 100} + - {name: 威武·改, search_name: 威武, ship_type: [aadg], min_level: 100} + - {name: 谦逊·改, search_name: 谦逊, ship_type: [aadg], min_level: 100} + - {name: 炽热·改, search_name: 炽热, ship_type: [aadg], min_level: 100} + - {name: 基阿特·改, search_name: 基阿特, ship_type: [aadg], min_level: 100} + - {name: 考文垂, ship_type: [aadg], min_level: 100} + - {name: 不惧(防空导弹驱逐舰), search_name: 不惧, ship_type: [aadg], min_level: 100} + - {name: 絮弗伦, ship_type: [aadg], min_level: 100} diff --git "a/resource/system_team_plans/team-85\345\267\245\347\250\213+3\345\276\267\351\251\261\351\230\237.yaml" "b/resource/system_team_plans/team-85\345\267\245\347\250\213+3\345\276\267\351\251\261\351\230\237.yaml" new file mode 100644 index 0000000..ca8f6a1 --- /dev/null +++ "b/resource/system_team_plans/team-85\345\267\245\347\250\213+3\345\276\267\351\251\261\351\230\237.yaml" @@ -0,0 +1,38 @@ +name: 85工程+3德驱队 +ships: + - name: 85工程 + ship_type: [cv] + min_level: 100 + - name: Z28·改 + search_name: Z28 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - name: T-23·改 + search_name: T-23 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} + - name: 吕贝克·改 + search_name: 吕贝克 + ship_type: [dd] + min_level: 100 + candidates: + - {name: Z1·改, search_name: Z1, ship_type: [dd], min_level: 100} + - {name: Z16·改, search_name: Z16, ship_type: [dd], min_level: 100} + - {name: Z21·改, search_name: Z21, ship_type: [dd], min_level: 100} + - {name: Z22·改, search_name: Z22, ship_type: [dd], min_level: 100} + - {name: Z31·改, search_name: Z31, ship_type: [dd], min_level: 100} + - {name: Z46·改, search_name: Z46, ship_type: [dd], min_level: 100} diff --git "a/resource/system_team_plans/team-\345\244\247\346\267\20085AIII\351\230\237.yaml" "b/resource/system_team_plans/team-\345\244\247\346\267\20085AIII\351\230\237.yaml" new file mode 100644 index 0000000..ad90d43 --- /dev/null +++ "b/resource/system_team_plans/team-\345\244\247\346\267\20085AIII\351\230\237.yaml" @@ -0,0 +1,29 @@ +name: 大淀85AIII队 +ships: + - name: 大淀(苍青幻影) + search_name: 大淀 + ship_type: [cl] + min_level: 100 + candidates: + - {name: 海伦娜·改, search_name: 海伦娜, ship_type: [cl], min_level: 100} + - {name: 圣胡安·改, search_name: 圣胡安, ship_type: [cl], min_level: 100} + - {name: 朱诺(CL-52)·改, search_name: 朱诺, ship_type: [cl], min_level: 100} + - {name: 亚特兰大·改, search_name: 亚特兰大, ship_type: [cl], min_level: 100} + - name: 85工程 + ship_type: [cv] + min_level: 100 + - name: AIII + ship_type: [bbv] + min_level: 100 + - candidates: + - {name: U-47·改, search_name: U-47, ship_type: [ss], min_level: 100} + - {name: U-81·改, search_name: U-81, ship_type: [ss], min_level: 100} + - {name: U-96·改, search_name: U-96, ship_type: [ss], min_level: 100} + - {name: U-2540·改, search_name: U-2540, ship_type: [ss], min_level: 100} + - {name: U-1206·改, search_name: U-1206, ship_type: [ss], min_level: 100} + - {name: U-1405·改, search_name: U-1405, ship_type: [ss], min_level: 100} + - {name: U-505·改, search_name: U-505, ship_type: [ss], min_level: 100} + - {name: 鹦鹉螺·改, search_name: 鹦鹉螺, ship_type: [ss], min_level: 100} + - {name: 射水鱼·改, search_name: 射水鱼, ship_type: [ss], min_level: 100} + - {name: 大青花鱼·改, search_name: 大青花鱼, ship_type: [ss], min_level: 100} + - {name: 鲃鱼·改, search_name: 鲃鱼, ship_type: [ss], min_level: 100} diff --git "a/resource/system_team_plans/team-\351\207\215\345\267\241\345\221\250\345\270\270\351\230\237.yaml" "b/resource/system_team_plans/team-\351\207\215\345\267\241\345\221\250\345\270\270\351\230\237.yaml" new file mode 100644 index 0000000..1f7813b --- /dev/null +++ "b/resource/system_team_plans/team-\351\207\215\345\267\241\345\221\250\345\270\270\351\230\237.yaml" @@ -0,0 +1,65 @@ +name: 重巡周常队 +ships: + - name: 巴尔的摩·改 + search_name: 巴尔的摩 + ship_type: [ca] + min_level: 100 + candidates: + - {name: 旧金山·改, search_name: 旧金山, ship_type: [ca], min_level: 100} + - {name: 彭萨科拉·改, search_name: 彭萨科拉, ship_type: [ca], min_level: 100} + - {name: 昆西·改, search_name: 昆西, ship_type: [ca], min_level: 100} + - {name: 威奇塔·改, search_name: 威奇塔, ship_type: [ca], min_level: 100} + - {name: 萨勒姆, ship_type: [ca], min_level: 100} + - {name: 德梅因, ship_type: [ca], min_level: 100} + - {name: 什罗普郡·改, search_name: 什罗普郡, ship_type: [ca], min_level: 100} + - {name: 埃克赛特·改, search_name: 埃克赛特, ship_type: [ca], min_level: 100} + - {name: 约克·改, search_name: 约克, ship_type: [ca], min_level: 100} + - {name: 欧根亲王·改, search_name: 欧根亲王, ship_type: [ca], min_level: 100} + - name: 昆西·改 + search_name: 昆西 + ship_type: [ca] + min_level: 100 + candidates: + - {name: 旧金山·改, search_name: 旧金山, ship_type: [ca], min_level: 100} + - {name: 彭萨科拉·改, search_name: 彭萨科拉, ship_type: [ca], min_level: 100} + - {name: 昆西·改, search_name: 昆西, ship_type: [ca], min_level: 100} + - {name: 威奇塔·改, search_name: 威奇塔, ship_type: [ca], min_level: 100} + - {name: 萨勒姆, ship_type: [ca], min_level: 100} + - {name: 德梅因, ship_type: [ca], min_level: 100} + - {name: 什罗普郡·改, search_name: 什罗普郡, ship_type: [ca], min_level: 100} + - {name: 埃克赛特·改, search_name: 埃克赛特, ship_type: [ca], min_level: 100} + - {name: 约克·改, search_name: 约克, ship_type: [ca], min_level: 100} + - {name: 欧根亲王·改, search_name: 欧根亲王, ship_type: [ca], min_level: 100} + - name: 海伦娜·改 + search_name: 海伦娜 + ship_type: [cl] + min_level: 100 + candidates: + - {name: 圣胡安·改, search_name: 圣胡安, ship_type: [cl], min_level: 100} + - {name: 朱诺(CL-52)·改, search_name: 朱诺, ship_type: [cl], min_level: 100} + - {name: 亚特兰大·改, search_name: 亚特兰大, ship_type: [cl], min_level: 100} + - name: 欧根亲王·改 + search_name: 欧根亲王 + ship_type: [ca] + min_level: 100 + candidates: + - {name: 旧金山·改, search_name: 旧金山, ship_type: [ca], min_level: 100} + - {name: 彭萨科拉·改, search_name: 彭萨科拉, ship_type: [ca], min_level: 100} + - {name: 昆西·改, search_name: 昆西, ship_type: [ca], min_level: 100} + - {name: 威奇塔·改, search_name: 威奇塔, ship_type: [ca], min_level: 100} + - {name: 萨勒姆, ship_type: [ca], min_level: 100} + - {name: 德梅因, ship_type: [ca], min_level: 100} + - {name: 什罗普郡·改, search_name: 什罗普郡, ship_type: [ca], min_level: 100} + - {name: 埃克赛特·改, search_name: 埃克赛特, ship_type: [ca], min_level: 100} + - {name: 约克·改, search_name: 约克, ship_type: [ca], min_level: 100} + - {name: 欧根亲王·改, search_name: 欧根亲王, ship_type: [ca], min_level: 100} + - name: 塔林 + ship_type: [cl] + min_level: 100 + candidates: + - {name: 圣胡安·改, search_name: 圣胡安, ship_type: [cl], min_level: 100} + - {name: 朱诺(CL-52)·改, search_name: 朱诺, ship_type: [cl], min_level: 100} + - {name: 亚特兰大·改, search_name: 亚特兰大, ship_type: [cl], min_level: 100} + - name: 85工程 + ship_type: [cv] + min_level: 100 diff --git a/scripts/backend-fleet-contract.py b/scripts/backend-fleet-contract.py new file mode 100644 index 0000000..8c97dbc --- /dev/null +++ b/scripts/backend-fleet-contract.py @@ -0,0 +1,107 @@ +"""Evaluate GUI fleet fixtures with the local AutoWSGR backend. + +The Node.js contract test writes JSON cases to stdin. Each case contains the +original YAML, the YAML saved by the GUI, and the HTTP request built by the +GUI scheduler. This helper loads the real backend package from the repository +path supplied on the command line. + +The three inputs pass through their production boundaries: +1. Original YAML goes through CombatPlan.from_yaml(). +2. GUI YAML goes through CombatPlan.from_yaml(). +3. HTTP JSON goes through NormalFightRequest and build_fleet_selection(). + +Only canonical FleetSlotRule data is written to stdout. Backend logs remain on +stderr, so the Node.js caller can parse the result as one JSON document. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any + + +def _selector_contract(selector: Any) -> dict[str, Any] | None: + """Convert one backend ShipSelector into stable JSON contract data.""" + if selector is None: + return None + return { + 'name': selector.name, + 'search_name': selector.search_name, + 'ship_type': [value.value for value in selector.ship_types], + 'min_level': selector.min_level, + 'max_level': selector.max_level, + } + + +def _slot_contract(slot: Any) -> dict[str, Any]: + """Convert one backend FleetSlotRule into stable JSON contract data.""" + return { + 'primary': _selector_contract(slot.primary), + 'candidates': [ + _selector_contract(candidate) + for candidate in slot.candidates + ], + } + + +def _plan_contract(plan: Any) -> list[dict[str, Any]]: + """Return the first fleet preset as canonical backend slot rules.""" + if not plan.fleet_presets: + raise AssertionError('contract plan has no fleet_presets') + return [_slot_contract(slot) for slot in plan.fleet_presets[0].slots] + + +def main() -> None: + """Load contract cases from stdin and write backend results to stdout.""" + if len(sys.argv) != 2: + raise SystemExit('usage: backend-fleet-contract.py ') + + backend_root = Path(sys.argv[1]).resolve() + if not (backend_root / 'autowsgr' / 'server' / 'schemas.py').is_file(): + raise FileNotFoundError(f'invalid AutoWSGR repository: {backend_root}') + sys.path.insert(0, str(backend_root)) + + from autowsgr.combat import CombatPlan + from autowsgr.server.schemas import NormalFightRequest + from autowsgr.server.serializers import build_combat_plan, build_fleet_selection + + cases = json.load(sys.stdin) + results: list[dict[str, Any]] = [] + with TemporaryDirectory(prefix='autowsgr-gui-contract-') as temp: + temp_root = Path(temp) + for index, case in enumerate(cases): + source_path = temp_root / f'{index}-source.yaml' + gui_path = temp_root / f'{index}-gui.yaml' + source_path.write_text(case['source_yaml'], encoding='utf-8') + gui_path.write_text(case['gui_yaml'], encoding='utf-8') + + request = NormalFightRequest.model_validate(case['request']) + if request.plan is None: + raise AssertionError('contract request has no plan') + api_plan = build_combat_plan(request.plan) + selection = build_fleet_selection(api_plan, request.plan) + if selection.slot_rules is None: + raise AssertionError('contract request produced no fleet rules') + + results.append({ + 'name': case['name'], + 'source_yaml': _plan_contract( + CombatPlan.from_yaml(source_path), + ), + 'gui_yaml': _plan_contract( + CombatPlan.from_yaml(gui_path), + ), + 'api': [ + _slot_contract(slot) + for slot in selection.slot_rules + ], + }) + + print(json.dumps(results, ensure_ascii=False)) + + +if __name__ == '__main__': + main() diff --git a/scripts/fixtures/task-groups/v1-path-form/task_groups.json b/scripts/fixtures/task-groups/v1-path-form/task_groups.json new file mode 100644 index 0000000..79a379a --- /dev/null +++ b/scripts/fixtures/task-groups/v1-path-form/task_groups.json @@ -0,0 +1,151 @@ +{ + "activeGroup": "决战", + "groups": [ + { + "name": "默认", + "items": [ + { + "path": "D:\\AutoWSGR-GUI\\plans\\9-4胖次6SS.yaml", + "kind": "plan", + "times": 500, + "label": "9-4胖次6SS" + } + ] + }, + { + "name": "周常列表", + "items": [ + { + "path": "resource/builtin_plans/周常1章-1-2.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常1章-1-2 · 岛风日驱队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常2章-2-1.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常2章-2-1 · 岛风日驱队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常3章-3-1.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常3章-3-1 · 契卡洛夫日驱队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常4章-4-1.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常4章-4-1 · 契卡洛夫日驱队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常5章-5-5.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常5章-5-5 · SS*6)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常6章-6-4.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常6章-6-4 · 克劳塞维茨德系队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": true + }, + { + "path": "resource/builtin_plans/周常7章-7-4.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常7章-7-4 · SS*6)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常8章-8-2.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常8章-8-2 · 大淀AIII队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常9章-9-2.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常9章-9-2 · 三响岛风)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "path": "resource/builtin_plans/周常10章-10-1.yaml", + "kind": "plan", + "times": 3, + "label": "周常任务 (周常10章-10-1 · 空想351潜艇队)", + "fleetPresetIndex": 0, + "fleet_id": 3, + "autoFleetFallback": false, + "forceRetry": false + }, + { + "templateId": "builtin_decisive_6", + "kind": "template", + "times": 5, + "label": "决战 (第 6 章)", + "chapter": 6 + } + ] + }, + { + "name": "练级", + "items": [ + { + "path": "C:\\ShiinaKuroko\\01.Project\\AutoWSGR-GUI\\plans\\9-3练级-min航速≤27.yaml", + "kind": "plan", + "times": 6, + "label": "9-3练级-min航速≤27" + } + ] + }, + { + "name": "决战", + "items": [ + { + "templateId": "builtin_decisive_6", + "kind": "template", + "times": 999, + "label": "决战 (第 6 章)", + "chapter": 6 + } + ] + } + ] +} diff --git a/scripts/main-services/test-combat-plan.js b/scripts/main-services/test-combat-plan.js new file mode 100644 index 0000000..d4763db --- /dev/null +++ b/scripts/main-services/test-combat-plan.js @@ -0,0 +1,566 @@ +/** + * 作战计划 Codec、Repository 和 Service 测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + TaskPresetCodec, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; + +/** 验证出征计划格式、运行时展开和管理流程保持既有语义。 */ +function testCombatPlanServices() { + const projectRoot = path.join(temporaryDirectory, 'combat-project'); + const userData = path.join(temporaryDirectory, 'combat-user-data'); + const tempDirectory = path.join(temporaryDirectory, 'combat-temp'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const atomicFiles = new AtomicFileStore(); + const settings = new GuiSettingsStore( + () => path.join(userData, 'gui_settings.json'), + ); + const teamCodec = new TeamPlanCodec(); + const teamRepository = new TeamPlanRepository( + appPaths, + atomicFiles, + teamCodec, + ); + const combatRepository = new CombatPlanRepository( + appPaths, + atomicFiles, + ); + const combatCodec = new CombatPlanCodec( + teamCodec, + teamRepository, + ); + const runtimePlans = new RuntimePlanService( + combatCodec, + combatRepository, + atomicFiles, + { + getTempDirectory: () => tempDirectory, + processId: 42, + now: () => 123456, + }, + ); + const management = new PlanManagementService( + combatCodec, + combatRepository, + runtimePlans, + teamRepository, + settings, + new TaskPresetCodec(), + ); + combatRepository.initializeSystemDirectory(); + combatRepository.initializeUserDirectory(); + teamRepository.initializeSystemDirectory(); + teamRepository.initializeUserDirectory(); + + const split = combatCodec.normalizeFleetPresets({ + chapter: 1, + map: 2, + rootExtension: { preserved: true }, + fleet_presets: [{ + name: '内嵌舰队', + ships: [{ + candidates: [{ name: 'U-47', customCandidate: true }], + }], + }], + }, 'user', true); + assert.equal(split.mapRoot.rootExtension.preserved, true); + assert.deepEqual(split.mapRoot.fleet_presets, [{ + name: '内嵌舰队', + }]); + assert.equal(split.teams[0].ships[0].name, undefined); + assert.equal( + split.teams[0].ships[0].candidates[0].customCandidate, + true, + ); + + const serialized = combatCodec.serialize( + split.mapRoot, + '# 保留注释\nchapter: 1\n', + ); + assert.match(serialized, /^# 保留注释\n/); + assert.equal(yaml.load(serialized).rootExtension.preserved, true); + assert.equal(combatCodec.safeBaseName('bettle-测试?.yaml'), '测试_'); + + assert.equal( + combatRepository.safeUserPath('../outside.yaml'), + null, + ); + assert.equal( + combatRepository.safeManagedPath('user', 'not-yaml.txt'), + null, + ); + assert.throws( + () => runtimePlans.write( + 'chapter: 1\nmap: 2\nfleet_presets:\n - name: 未展开\n', + 'unexpanded', + ), + /运行时出征计划包含尚未展开的舰队引用/, + ); + + const editableContent = [ + '# 编辑器注释', + 'chapter: 1', + 'map: 2', + 'times: 3', + 'selected_nodes: [A, B]', + 'customRoot: keep', + 'fleet_presets:', + ' - name: 测试舰队', + ' ships:', + ' - name: 重庆', + ' candidates:', + ' - name: U-47', + '', + ].join('\n'); + const saved = management.saveManaged( + '测试计划', + editableContent, + false, + ); + assert.equal(saved.success, true); + assert.deepEqual(saved.teamFiles, ['team-测试舰队.yaml']); + const savedMap = combatRepository.read(saved.path); + assert.match(savedMap, /^# 编辑器注释\n/); + assert.equal(yaml.load(savedMap).customRoot, 'keep'); + assert.deepEqual(yaml.load(savedMap).fleet_presets, [{ + name: '测试舰队', + }]); + + // 旧 renderer 即使继续多传 system,也只能保存为用户计划。 + const readonlyBoundary = management.saveManaged( + '只读边界', + 'chapter: 1\nmap: 1\n', + false, + undefined, + 'system', + ); + assert.equal(readonlyBoundary.success, true); + assert.equal(readonlyBoundary.source, 'user'); + assert.equal( + readonlyBoundary.path, + path.join( + appPaths.userBattlePlansDir(), + 'bettle-只读边界.yaml', + ), + ); + assert.equal( + fs.existsSync(path.join( + appPaths.systemBattlePlansDir(), + 'bettle-只读边界.yaml', + )), + false, + ); + assert.deepEqual( + management.deleteUserCombat('bettle-只读边界.yaml'), + { success: true }, + ); + + const prepared = management.readManaged( + 'user', + 'bettle-测试计划.yaml', + ); + assert.equal(prepared.success, true); + assert.equal(prepared.sourcePath, saved.path); + assert.match( + prepared.runtimePath, + /测试计划-123456-1\.yaml$/, + ); + assert.equal( + yaml.load(prepared.content).fleet_presets[0].ships[0].name, + '重庆', + ); + + const duplicate = management.saveManaged( + '测试计划', + editableContent.replace('重庆', '长春'), + false, + ); + assert.equal(duplicate.success, false); + assert.equal(duplicate.exists, true); + assert.equal(duplicate.error, '存在同名配置'); + assert.deepEqual(duplicate.conflicts, [ + '地图:bettle-测试计划.yaml', + '舰队:测试舰队', + ]); + + const localLegacyPlan = path.join( + temporaryDirectory, + 'local-legacy-plan.yaml', + ); + const legacyContent = [ + '# 本地旧计划', + 'chapter: 2', + 'map: 3', + 'customRoot: keep', + 'fleet_presets:', + ' - name: 旧版导入舰队', + ' ships:', + ' - U-47', + ' - priority: [U-96, U-81]', + ' ship_type: [ss]', + ' - ship_type: ss', + ' min_level: 100', + '', + ].join('\n'); + fs.writeFileSync(localLegacyPlan, legacyContent, 'utf8'); + const imported = management.importLocal(localLegacyPlan); + assert.equal(imported.success, true); + assert.equal(imported.file, 'bettle-local-legacy-plan.yaml'); + assert.equal(fs.readFileSync(localLegacyPlan, 'utf8'), legacyContent); + assert.deepEqual( + yaml.load(fs.readFileSync(imported.path, 'utf8')).fleet_presets, + [{ name: '旧版导入舰队' }], + ); + const importedTeamPath = path.join( + appPaths.userTeamPlansDir(), + 'team-旧版导入舰队.yaml', + ); + assert.deepEqual( + yaml.load(fs.readFileSync(importedTeamPath, 'utf8')).ships, + [ + { name: 'U-47' }, + { + ship_type: ['ss'], + candidates: [ + { name: 'U-96', ship_type: ['ss'] }, + { name: 'U-81', ship_type: ['ss'] }, + ], + }, + { + ship_type: ['ss'], + min_level: 100, + }, + ], + ); + + fs.writeFileSync( + localLegacyPlan, + legacyContent.replace('U-47', 'U-505'), + 'utf8', + ); + const importConflict = management.importLocal(localLegacyPlan); + assert.equal(importConflict.success, false); + assert.equal(importConflict.exists, true); + assert.deepEqual(importConflict.conflicts, [ + '地图:bettle-local-legacy-plan.yaml', + '舰队:旧版导入舰队', + ]); + assert.equal( + yaml.load(fs.readFileSync(importedTeamPath, 'utf8')).ships[0].name, + 'U-47', + ); + const importedOverwrite = management.importLocal( + localLegacyPlan, + true, + ); + assert.equal(importedOverwrite.success, true); + assert.equal( + yaml.load(fs.readFileSync(importedTeamPath, 'utf8')).ships[0].name, + 'U-505', + ); + + const dailyTaskFixtures = [ + { + file: '战役.yaml', + type: 'campaign', + content: 'task_type: campaign\ncampaign_name: 困难航母\ntimes: 8\n', + }, + { + file: '自动演习.yaml', + type: 'exercise', + content: 'task_type: exercise\nfleet_id: 4\n', + }, + { + file: '决战.yaml', + type: 'decisive', + content: [ + 'task_type: decisive', + 'chapter: 6', + 'level1: [鲃鱼]', + 'level2: [巧言]', + '', + ].join('\n'), + }, + ]; + dailyTaskFixtures.forEach((fixture) => { + const sourcePath = path.join(temporaryDirectory, fixture.file); + fs.writeFileSync(sourcePath, fixture.content, 'utf8'); + const result = management.importLocal(sourcePath); + assert.equal(result.success, false); + assert.equal( + result.error, + '演习、战役和决战配置请使用“加载日常任务”管理', + ); + assert.equal(fs.readFileSync(sourcePath, 'utf8'), fixture.content); + }); + const normalFightPreset = path.join( + temporaryDirectory, + '普通出征任务.yaml', + ); + fs.writeFileSync( + normalFightPreset, + 'task_type: normal_fight\nplan_id: 1-1\ntimes: 2\n', + 'utf8', + ); + const importedPreset = management.importLocal(normalFightPreset); + assert.equal(importedPreset.success, true); + assert.equal(importedPreset.kind, 'preset'); + const managedPreset = management.readManaged( + 'user', + importedPreset.file, + ); + assert.equal(managedPreset.success, true); + assert.equal(managedPreset.kind, 'preset'); + assert.equal(managedPreset.runtimePath, undefined); + assert.deepEqual( + management.deleteUserCombat(importedPreset.file), + { success: true }, + ); + + assert.deepEqual( + management.importLocal(path.join(temporaryDirectory, 'plan.txt')), + { success: false, error: '本地出征计划路径不合法' }, + ); + assert.deepEqual( + management.deleteUserCombat('bettle-local-legacy-plan.yaml'), + { success: true }, + ); + assert.deepEqual( + management.deleteUserTeam('team-旧版导入舰队.yaml'), + { success: true }, + ); + + assert.deepEqual( + management.setUnlinkedIgnored( + 'battle', + 'user', + 'bettle-测试计划.yaml', + true, + ), + ['battle/user/bettle-测试计划.yaml'], + ); + const renamed = management.saveManaged( + '重命名计划', + [ + 'chapter: 1', + 'map: 2', + 'fleet_presets:', + ' - name: 测试舰队', + '', + ].join('\n'), + false, + 'bettle-测试计划.yaml', + ); + assert.equal(renamed.success, true); + assert.equal(combatRepository.exists(saved.path), false); + assert.deepEqual( + settings.read().plan_management_ignored_unlinked, + ['battle/user/bettle-重命名计划.yaml'], + ); + + teamRepository.write( + path.join( + appPaths.systemTeamPlansDir(), + 'team-系统舰队.yaml', + ), + [ + 'name: 系统舰队', + 'ships:', + ' - name: 系统舰', + '', + ].join('\n'), + ); + combatRepository.write( + path.join( + appPaths.systemBattlePlansDir(), + 'bettle-系统计划.yaml', + ), + [ + 'chapter: 3', + 'map: 4', + 'fleet_presets:', + ' - name: 系统舰队', + '', + ].join('\n'), + ); + const summary = management.get(); + assert.equal(summary.battlePlans.length, 2); + const userSummary = summary.battlePlans.find( + plan => plan.source === 'user', + ); + const systemSummary = summary.battlePlans.find( + plan => plan.source === 'system', + ); + assert.equal(userSummary.name, '重命名计划'); + assert.equal(userSummary.chapter, 1); + assert.equal(userSummary.map, 2); + assert.equal(userSummary.fleetCount, 1); + assert.equal(userSummary.fleets[0].primaryCount, 1); + assert.equal(userSummary.fleets[0].backupCount, 1); + assert.equal(systemSummary.name, '系统计划'); + assert.equal(systemSummary.chapter, 3); + assert.equal(systemSummary.map, 4); + assert.equal( + summary.teamPlans.some(plan => plan.source === 'system'), + true, + ); + const preparedSystem = management.readManaged( + 'system', + 'bettle-系统计划.yaml', + ); + assert.equal(preparedSystem.success, true); + assert.equal( + yaml.load(preparedSystem.content).fleet_presets[0].ships[0].name, + '系统舰', + ); + + const legacyEventPlans = [ + { + file: 'E1炸鱼.yaml', + content: [ + '# 20260730 激斗漩涡 E-1 alpha 入口示例', + 'event: "20260730"', + 'chapter: E', + 'map: 1a', + 'selected_nodes: [B]', + 'node_defaults:', + ' proceed: False', + ' formation: 5', + '', + ].join('\n'), + expected: ['E', '1a', ['B']], + }, + { + file: 'E5夜战.yaml', + content: [ + '# 20260730 激斗漩涡 E-5 alpha 入口示例', + 'event: "20260730"', + 'chapter: E', + 'map: 5a', + 'selected_nodes: [A, B, C, D, F]', + 'node_defaults:', + ' night: True', + ' proceed: True', + ' formation: 4', + 'node_args:', + ' C:', + ' proceed: False', + ' D:', + ' proceed: False', + ' F:', + ' proceed: False', + '', + ].join('\n'), + expected: ['E', '5a', ['A', 'B', 'C', 'D', 'F']], + }, + { + file: 'H1炸鱼.yaml', + content: [ + '# 20260730 激斗漩涡 H-1 alpha 入口示例', + 'event: "20260730"', + 'chapter: H', + 'map: 1a', + 'selected_nodes: [B]', + 'node_defaults:', + ' proceed: False', + ' formation: 5', + '', + ].join('\n'), + expected: ['H', '1a', ['B']], + }, + { + file: 'H5夜战.yaml', + content: [ + '# 20260730 激斗漩涡 H-5 alpha 入口示例', + 'event: "20260730"', + 'chapter: H', + 'map: 5a', + 'selected_nodes: [A, B, C, D, F]', + 'node_defaults:', + ' night: True', + ' proceed: True', + ' formation: 4', + 'node_args:', + ' C:', + ' proceed: False', + ' D:', + ' proceed: False', + ' F:', + ' proceed: False', + '', + ].join('\n'), + expected: ['H', '5a', ['A', 'B', 'C', 'D', 'F']], + }, + ]; + legacyEventPlans.forEach((fixture) => { + const systemFile = `bettle-${fixture.file}`; + combatRepository.write( + path.join(appPaths.systemBattlePlansDir(), systemFile), + fixture.content, + ); + const preparedEvent = management.readManaged( + 'system', + systemFile, + ); + assert.equal(preparedEvent.success, true); + assert.equal(fs.existsSync(preparedEvent.runtimePath), true); + const eventPlan = yaml.load(preparedEvent.content); + assert.equal(eventPlan.event, '20260730'); + assert.deepEqual( + [eventPlan.chapter, eventPlan.map, eventPlan.selected_nodes], + fixture.expected, + ); + }); + + assert.deepEqual( + management.deleteUserCombat('bettle-重命名计划.yaml'), + { success: true }, + ); + assert.deepEqual( + management.deleteUserTeam('team-测试舰队.yaml'), + { success: true }, + ); +} + +module.exports = { + testCombatPlanServices, +}; diff --git a/scripts/main-services/test-configuration-and-window.js b/scripts/main-services/test-configuration-and-window.js new file mode 100644 index 0000000..31f1344 --- /dev/null +++ b/scripts/main-services/test-configuration-and-window.js @@ -0,0 +1,573 @@ +/** + * 窗口、设置存储和 GUI 配置服务测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; +const { + DEFAULT_LOOT_PLANS, +} = require('../../dist/src/shared/lootPlans.js'); + +/** 验证窗口偏好、创建参数和唯一窗口状态。 */ +function testWindowService() { + const settingsPath = path.join(temporaryDirectory, 'window-settings.json'); + const settings = new GuiSettingsStore(() => settingsPath); + settings.write({ + default_window_width: 400, + default_window_height: 'invalid', + remember_window_bounds: true, + window_bounds: { + x: 20, + y: 30, + width: 1400, + height: 800, + }, + }); + + let createdOptions = null; + let loadedFile = null; + let headersHandler = null; + let normalBounds = { x: 20, y: 30, width: 1400, height: 800 }; + const windowHandlers = new Map(); + const webContentsHandlers = new Map(); + const fakeWindow = { + isDestroyed: () => false, + getNormalBounds: () => normalBounds, + webContents: { + session: { + webRequest: { + onHeadersReceived: handler => { + headersHandler = handler; + }, + }, + }, + on: (event, handler) => { + webContentsHandlers.set(event, handler); + }, + }, + loadFile: filePath => { + loadedFile = filePath; + return Promise.resolve(); + }, + on: (event, handler) => { + windowHandlers.set(event, handler); + }, + }; + const service = new WindowService(settings, { + backendPort: 18438, + moduleDirectory: path.join(temporaryDirectory, 'dist', 'electron'), + createBrowserWindow: options => { + createdOptions = options; + return fakeWindow; + }, + getDisplays: () => [{ + workArea: { x: 0, y: 0, width: 1920, height: 1080 }, + }], + getAppPath: () => path.join(temporaryDirectory, 'app'), + isPackaged: () => false, + resourceRoot: () => path.join(temporaryDirectory, 'resources'), + showMessageBox: () => {}, + }); + + assert.deepEqual(service.getPreferences(), { + defaultWidth: 854, + defaultHeight: 720, + rememberBounds: true, + }); + service.createWindow(); + assert.equal(createdOptions.width, 1400); + assert.equal(createdOptions.height, 800); + assert.equal(createdOptions.x, 20); + assert.equal(createdOptions.y, 30); + assert.equal(createdOptions.center, false); + assert.equal( + createdOptions.webPreferences.preload, + path.join(temporaryDirectory, 'dist', 'electron', 'preload.js'), + ); + assert.equal( + loadedFile, + path.join(temporaryDirectory, 'app', 'src', 'view', 'index.html'), + ); + assert.equal(service.getMainWindow(), fakeWindow); + + let responseHeaders = null; + headersHandler( + { responseHeaders: { existing: ['value'] } }, + response => { + responseHeaders = response.responseHeaders; + }, + ); + assert.match( + responseHeaders['Content-Security-Policy'][0], + /localhost:18438/, + ); + + normalBounds = { x: 40, y: 50, width: 1500, height: 900 }; + windowHandlers.get('close')(); + assert.deepEqual(settings.read().window_bounds, normalBounds); + windowHandlers.get('closed')(); + assert.equal(service.getMainWindow(), null); + + assert.deepEqual(service.setPreferences({ + defaultWidth: 1440, + defaultHeight: 810, + rememberBounds: false, + }), { + defaultWidth: 1440, + defaultHeight: 810, + rememberBounds: false, + }); + assert.equal(webContentsHandlers.has('did-fail-load'), true); +} + +/** 验证 GUI 设置读取、损坏回退和浅合并格式。 */ +function testGuiSettingsStore() { + const settingsPath = path.join(temporaryDirectory, 'gui_settings.json'); + const store = new GuiSettingsStore(() => settingsPath); + + assert.deepEqual(store.read(), {}); + fs.writeFileSync(settingsPath, '{invalid', 'utf8'); + assert.deepEqual(store.read(), {}); + + fs.writeFileSync( + settingsPath, + JSON.stringify({ + preserved: 'keep', + nested: { old: true }, + }), + 'utf8', + ); + store.write({ + backend_port: 18438, + nested: { current: true }, + }); + assert.equal( + fs.readFileSync(settingsPath, 'utf8'), + JSON.stringify({ + preserved: 'keep', + nested: { current: true }, + backend_port: 18438, + }, null, 2), + ); +} + +/** 验证 GUI 配置字段保持既有默认值、边界和迁移规则。 */ +function testGuiConfigurationService() { + const settingsPath = path.join( + temporaryDirectory, + 'gui_configuration.json', + ); + const store = new GuiSettingsStore(() => settingsPath); + const environmentPort = { value: undefined }; + let clearPythonCacheCalls = 0; + const service = new GuiConfigurationService(store, { + clearPythonCache: () => { + clearPythonCacheCalls += 1; + }, + normalizeCudaPath: candidate => candidate.replaceAll('/', '\\'), + environmentPort: () => environmentPort.value, + }); + + assert.deepEqual(service.legacyDecisiveAutomation(), { + exists: false, + settings: {}, + }); + assert.equal(service.backendPort(), 8438); + service.setBackendPort(18438.9); + assert.equal(service.backendPort(), 18438); + service.setBackendPort(0); + service.setBackendPort(Number.NaN); + assert.equal(service.backendPort(), 18438); + environmentPort.value = '28438'; + assert.equal(service.backendPort(), 28438); + environmentPort.value = undefined; + + assert.equal(service.configuredPythonPath(), null); + service.setPythonPath('C:\\Python313\\python.exe'); + assert.equal( + service.configuredPythonPath(), + 'C:\\Python313\\python.exe', + ); + service.setPythonPath(null); + assert.equal(service.configuredPythonPath(), null); + assert.equal(clearPythonCacheCalls, 2); + + assert.equal(service.updateMode(), 'auto'); + service.setUpdateMode('manual'); + assert.equal(service.updateMode(), 'manual'); + service.setUpdateMode('invalid'); + assert.equal(service.updateMode(), 'auto'); + + assert.equal(service.backendStartupMode(), 'managed'); + service.setBackendStartupMode('external'); + assert.equal(service.backendStartupMode(), 'external'); + service.setBackendStartupMode('invalid'); + assert.equal(service.backendStartupMode(), 'managed'); + + service.setBackendRepoPath(' C:\\AutoWSGR '); + assert.equal(service.backendRepoPath(), 'C:\\AutoWSGR'); + service.setBackendRepoPath(null); + assert.equal(service.backendRepoPath(), ''); + + assert.equal(service.ocrGpuMode(), 'auto'); + service.setOcrGpuMode('cuda'); + assert.equal(service.ocrGpuMode(), 'cuda'); + service.setOcrGpuMode('invalid'); + assert.equal(service.ocrGpuMode(), 'auto'); + + service.setCudaPath(' C:/CUDA/v12.8 '); + assert.equal(service.cudaPath(), 'C:\\CUDA\\v12.8'); + service.setCudaPath(null); + assert.equal(service.cudaPath(), ''); + + assert.equal(service.saveBackendScreenshots(), false); + service.setSaveBackendScreenshots(true); + assert.equal(service.saveBackendScreenshots(), true); + service.setSaveBackendScreenshots('true'); + assert.equal(service.saveBackendScreenshots(), false); + + assert.deepEqual(service.automation(), { + exists: false, + settings: {}, + }); + store.write({ + automation: { + expeditionInterval: 20, + }, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + expeditionInterval: 20, + }, + }); + store.write({ + automation: { + expeditionInterval: 20, + battleTimes: 4, + autoLoot: true, + lootPlanIndex: 2, + lootStopCount: 16, + }, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + expeditionInterval: 20, + battleTimes: 4, + autoLoot: true, + lootPlanSource: 'system', + lootPlanId: 'bettle-周常-8-2.yaml', + lootStopCount: 16, + }, + }); + assert.equal( + Object.hasOwn(store.read().automation, 'lootPlanIndex'), + false, + ); + store.write({ + automation: { + autoLoot: true, + lootPlanId: 'bettle-不存在.yaml', + }, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + autoLoot: false, + }, + }); + assert.deepEqual(store.read().automation, { + autoLoot: false, + lootPlanId: 'bettle-不存在.yaml', + }); + for (const invalidIndex of [99, null, '', false, 2.5]) { + store.write({ + automation: { + autoLoot: true, + lootPlanIndex: invalidIndex, + }, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + autoLoot: false, + }, + }); + assert.equal( + Object.hasOwn(store.read().automation, 'lootPlanIndex'), + false, + ); + } + store.write({ automation: { autoLoot: true } }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + autoLoot: true, + }, + }); + const userLootPlans = [ + { + source: 'system', + file: 'bettle-周常-8-2.yaml', + name: '周常 8-2', + }, + { + source: 'user', + file: 'bettle-用户胖次测试.yaml', + name: '用户胖次测试', + }, + ]; + assert.deepEqual(service.setAutomation({ + expeditionInterval: 20, + battleTimes: 4, + autoDecisive: false, + decisiveTemplateId: 'user_plan', + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: 'bettle-用户胖次测试.yaml', + lootPlans: userLootPlans, + lootStopCount: 16, + }), { + expeditionInterval: 20, + battleTimes: 4, + autoDecisive: false, + decisiveTemplateId: 'user_plan', + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: 'bettle-用户胖次测试.yaml', + lootPlans: userLootPlans, + lootStopCount: 16, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + expeditionInterval: 20, + battleTimes: 4, + autoDecisive: false, + decisiveTemplateId: 'user_plan', + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: 'bettle-用户胖次测试.yaml', + lootPlans: userLootPlans, + lootStopCount: 16, + }, + }); + assert.deepEqual(service.setAutomation({ + expeditionInterval: 20, + battleTimes: 4, + autoDecisive: false, + decisiveTemplateId: 'user_plan', + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: 'bettle-用户胖次测试.yaml', + lootPlans: [], + lootStopCount: 16, + }), { + expeditionInterval: 20, + battleTimes: 4, + autoDecisive: false, + decisiveTemplateId: 'user_plan', + autoLoot: false, + lootPlanSource: 'system', + lootPlanId: 'bettle-old-9-2ADGHM速刷胖次.yaml', + lootPlans: [], + lootStopCount: 16, + }); + assert.deepEqual(service.setAutomation({ + expeditionInterval: 999, + battleTimes: 0, + autoDecisive: true, + decisiveTemplateId: ' builtin_decisive_6 ', + autoLoot: true, + lootPlanId: 'bettle-捞胖次-8-5.yaml', + lootStopCount: 0, + }), { + expeditionInterval: 120, + battleTimes: 3, + autoDecisive: true, + decisiveTemplateId: 'system_preset', + autoLoot: true, + lootPlanSource: 'system', + lootPlanId: 'bettle-old-8-5AI六潜胖次.yaml', + lootPlans: DEFAULT_LOOT_PLANS, + lootStopCount: 50, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + expeditionInterval: 120, + battleTimes: 3, + autoDecisive: true, + decisiveTemplateId: 'system_preset', + autoLoot: true, + lootPlanSource: 'system', + lootPlanId: 'bettle-old-8-5AI六潜胖次.yaml', + lootPlans: DEFAULT_LOOT_PLANS, + lootStopCount: 50, + }, + }); + + const automationWithoutLegacyDecisive = { + ...store.read().automation, + }; + delete automationWithoutLegacyDecisive.autoDecisive; + delete automationWithoutLegacyDecisive.decisiveTemplateId; + store.write({ automation: automationWithoutLegacyDecisive }); + store.write({ + preserved: 'keep', + legacy_decisive_automation: { + preserved_extension: 'keep', + }, + decisive_plan: { + chapter: 9, + use_quick_repair: false, + level1: [' A ', 'B', 'C', 'D', 'E', 'F', 'G'], + level2: ['B', 'H'], + level3: ['I', 'H'], + }, + }); + assert.deepEqual(service.legacyDecisiveAutomation(), { + exists: true, + settings: {}, + }); + const decisivePlanBeforeLegacyMigration = structuredClone( + store.read().decisive_plan, + ); + const legacyDecisive = { + autoDecisive: true, + ticketReserve: 0, + templateId: 'builtin_decisive_6', + }; + assert.deepEqual( + service.migrateLegacyDecisiveAutomation(legacyDecisive), + legacyDecisive, + ); + assert.deepEqual(service.legacyDecisiveAutomation(), { + exists: true, + settings: legacyDecisive, + }); + assert.deepEqual(service.automation(), { + exists: true, + settings: { + expeditionInterval: 120, + battleTimes: 3, + autoDecisive: true, + decisiveTemplateId: 'system_preset', + autoLoot: true, + lootPlanSource: 'system', + lootPlanId: 'bettle-old-8-5AI六潜胖次.yaml', + lootPlans: DEFAULT_LOOT_PLANS, + lootStopCount: 50, + }, + }); + assert.deepEqual( + store.read().legacy_decisive_automation, + { + preserved_extension: 'keep', + auto_decisive: true, + decisive_ticket_reserve: 0, + decisive_template_id: 'builtin_decisive_6', + }, + ); + assert.deepEqual( + store.read().decisive_plan, + decisivePlanBeforeLegacyMigration, + '旧版决战字段不能覆盖当前决战计划', + ); + assert.deepEqual( + service.migrateLegacyDecisiveAutomation(legacyDecisive), + legacyDecisive, + '重复迁移必须保持原值', + ); + assert.throws( + () => service.migrateLegacyDecisiveAutomation({ + ticketReserve: Number.NaN, + }), + /decisive_ticket_reserve/, + ); + assert.deepEqual(service.decisivePlan(), { + chapter: 6, + useQuickRepair: false, + level1: ['A', 'B', 'C', 'D', 'E', 'F'], + level2: ['G', 'H', 'I'], + }); + assert.deepEqual(store.read(), { + backend_port: 18438, + python_path: '', + update_mode: 'auto', + backend_startup_mode: 'managed', + backend_repo_path: '', + ocr_gpu_mode: 'auto', + cuda_path: '', + save_backend_screenshots: false, + automation: { + expeditionInterval: 120, + battleTimes: 3, + autoDecisive: true, + decisiveTemplateId: 'system_preset', + autoLoot: true, + lootPlanSource: 'system', + lootPlanId: 'bettle-old-8-5AI六潜胖次.yaml', + lootPlans: DEFAULT_LOOT_PLANS, + lootStopCount: 50, + }, + preserved: 'keep', + legacy_decisive_automation: { + preserved_extension: 'keep', + auto_decisive: true, + decisive_ticket_reserve: 0, + decisive_template_id: 'builtin_decisive_6', + }, + decisive_plan: { + chapter: 6, + use_quick_repair: false, + level1: ['A', 'B', 'C', 'D', 'E', 'F'], + level2: ['G', 'H', 'I'], + }, + }); + + fs.rmSync(settingsPath, { force: true }); + const defaults = service.decisivePlan(); + assert.equal(defaults.chapter, 6); + assert.equal(defaults.useQuickRepair, true); + assert.equal(defaults.level1.length, 6); + assert.ok(defaults.level2.length > 0); +} + +module.exports = { + testWindowService, + testGuiSettingsStore, + testGuiConfigurationService, +}; diff --git a/scripts/main-services/test-context.js b/scripts/main-services/test-context.js new file mode 100644 index 0000000..18560e9 --- /dev/null +++ b/scripts/main-services/test-context.js @@ -0,0 +1,119 @@ +/** + * 主进程服务测试共享上下文。 + * + * 只集中测试依赖和单次临时目录,不包含业务断言。 + */ +const assert = require('node:assert/strict'); +const { EventEmitter } = require('node:events'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { PassThrough } = require('node:stream'); +const yaml = require('js-yaml'); +const { + AppPaths, +} = require('../../dist/electron/services/AppPaths.js'); +const { + AtomicFileStore, +} = require('../../dist/electron/services/AtomicFileStore.js'); +const { + GuiSettingsStore, +} = require('../../dist/electron/services/GuiSettingsStore.js'); +const { + SafePathService, +} = require('../../dist/electron/services/SafePathService.js'); +const { + SecureFileService, +} = require('../../dist/electron/services/SecureFileService.js'); +const { + WindowService, +} = require('../../dist/electron/services/WindowService.js'); +const { + UserDataMigrationService, +} = require('../../dist/electron/services/UserDataMigrationService.js'); +const { + LegacyPlanMigration, +} = require('../../dist/electron/services/LegacyPlanMigration.js'); +const { + MigrationConflictService, +} = require('../../dist/electron/services/MigrationConflictService.js'); +const { + TeamPlanCodec, +} = require('../../dist/electron/services/TeamPlanCodec.js'); +const { + TeamPlanRepository, +} = require('../../dist/electron/services/TeamPlanRepository.js'); +const { + TeamPlanService, +} = require('../../dist/electron/services/TeamPlanService.js'); +const { + CombatPlanCodec, +} = require('../../dist/electron/services/CombatPlanCodec.js'); +const { + CombatPlanRepository, +} = require('../../dist/electron/services/CombatPlanRepository.js'); +const { + RuntimePlanService, +} = require('../../dist/electron/services/RuntimePlanService.js'); +const { + PlanManagementService, +} = require('../../dist/electron/services/PlanManagementService.js'); +const { + TaskPresetCodec, +} = require('../../dist/electron/services/TaskPresetCodec.js'); +const { + ShipLibraryService, +} = require('../../dist/electron/services/ShipLibraryService.js'); +const { + ShipLibraryUpdater, +} = require('../../dist/electron/services/ShipLibraryUpdater.js'); +const { + AdbService, +} = require('../../dist/electron/services/AdbService.js'); +const { + CudaEnvironmentService, +} = require('../../dist/electron/services/CudaEnvironmentService.js'); +const { + GuiConfigurationService, +} = require('../../dist/electron/services/GuiConfigurationService.js'); +const { + PythonEnvironmentService, +} = require('../../dist/electron/services/PythonEnvironmentService.js'); + +const temporaryDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'autowsgr-main-services-'), +); + +module.exports = { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + MigrationConflictService, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + TaskPresetCodec, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +}; diff --git a/scripts/main-services/test-environment-and-device.js b/scripts/main-services/test-environment-and-device.js new file mode 100644 index 0000000..b0e7f74 --- /dev/null +++ b/scripts/main-services/test-environment-and-device.js @@ -0,0 +1,536 @@ +/** + * Python、ADB 和 CUDA 环境服务测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; +const { + applyBackendRuntimeSettings, + buildBackendBootstrap, + buildBackendCapabilityProbe, +} = require( + '../../dist/electron/services/BackendRuntimeContract.js', +); + +/** 验证 Python 校验、检查和安装入口的原有返回语义。 */ +async function testPythonEnvironmentService() { + const state = { + exists: false, + version: 'Python 3.13.5', + allowed: true, + pythonPath: null, + versionError: null, + }; + const calls = { + check: 0, + install: [], + portable: 0, + }; + const service = new PythonEnvironmentService({ + fileExists: () => state.exists, + readVersion: async () => { + if (state.versionError) throw state.versionError; + return state.version; + }, + isAllowedVersion: () => state.allowed, + findPython: async () => state.pythonPath, + checkEnvironment: async () => { + calls.check += 1; + return { ready: true, pythonCmd: 'python.exe' }; + }, + installDependencies: async pythonPath => { + calls.install.push(pythonPath); + return { success: true, output: 'installed' }; + }, + installPortablePython: async () => { + calls.portable += 1; + return { success: true }; + }, + }); + + assert.deepEqual(await service.validate(''), { + valid: false, + version: null, + error: '路径为空', + }); + assert.deepEqual(await service.validate('missing.exe'), { + valid: false, + version: null, + error: '文件不存在', + }); + + state.exists = true; + assert.deepEqual(await service.validate('python.exe'), { + valid: true, + version: 'Python 3.13.5', + }); + state.allowed = false; + assert.deepEqual(await service.validate('python.exe'), { + valid: false, + version: 'Python 3.13.5', + error: '版本不兼容: Python 3.13.5(需要 3.12 或 3.13)', + }); + state.versionError = new Error('cannot execute'); + assert.deepEqual(await service.validate('python.exe'), { + valid: false, + version: null, + error: '执行失败: cannot execute', + }); + + assert.deepEqual(await service.check(), { + ready: true, + pythonCmd: 'python.exe', + }); + assert.equal(calls.check, 1); + + assert.deepEqual(await service.installDependencies(), { + success: false, + output: '找不到 Python', + }); + state.pythonPath = 'C:\\Python313\\python.exe'; + assert.deepEqual(await service.installDependencies(), { + success: true, + output: 'installed', + }); + assert.deepEqual(calls.install, ['C:\\Python313\\python.exe']); + + assert.deepEqual(await service.installPortablePython(), { + success: true, + }); + assert.equal(calls.portable, 1); +} + +/** 验证 ADB 命令参数、设备解析和状态确认保持不变。 */ +async function testAdbService() { + const projectRoot = path.join(temporaryDirectory, 'adb-project'); + const userData = path.join(temporaryDirectory, 'adb-user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const responses = []; + const calls = []; + const service = new AdbService(appPaths, { + execute: async (executable, args, options) => { + calls.push({ executable, args, options }); + const response = responses.shift(); + if (response instanceof Error) throw response; + return response; + }, + }); + + assert.equal(service.executable(), 'adb'); + const bundledAdb = path.join(projectRoot, 'adb', 'adb.exe'); + fs.mkdirSync(path.dirname(bundledAdb), { recursive: true }); + fs.writeFileSync(bundledAdb, 'adb', 'utf8'); + assert.equal(service.executable(), bundledAdb); + + responses.push({ + stdout: [ + 'List of devices attached', + '127.0.0.1:16384\tdevice', + 'emulator-5554', + '', + ].join('\r\n'), + stderr: '', + }); + assert.deepEqual(await service.listDevices(), [ + { serial: '127.0.0.1:16384', status: 'device' }, + { serial: 'emulator-5554', status: 'unknown' }, + ]); + assert.deepEqual(calls[0], { + executable: bundledAdb, + args: ['devices'], + options: { + windowsHide: true, + timeout: 5000, + encoding: 'utf8', + }, + }); + + assert.deepEqual( + await service.runDeviceCommand('connect', 'bad address!'), + { + success: false, + serial: 'bad address!', + status: 'invalid', + message: 'ADB 地址格式不正确', + }, + ); + assert.equal(calls.length, 1); + + responses.push( + { stdout: 'connected', stderr: '' }, + { + stdout: [ + 'List of devices attached', + '127.0.0.1:16384\tdevice', + '', + ].join('\n'), + stderr: '', + }, + ); + assert.deepEqual( + await service.runDeviceCommand( + 'connect', + ' 127.0.0.1:16384 ', + ), + { + success: true, + serial: '127.0.0.1:16384', + status: 'device', + message: 'connected', + }, + ); + assert.deepEqual(calls[1].args, [ + 'connect', + '127.0.0.1:16384', + ]); + assert.equal(calls[1].options.timeout, 10000); + + responses.push( + { stdout: '', stderr: '' }, + { + stdout: 'List of devices attached\n', + stderr: '', + }, + ); + assert.deepEqual( + await service.runDeviceCommand( + 'disconnect', + '127.0.0.1:16384', + ), + { + success: true, + serial: '127.0.0.1:16384', + status: 'disconnected', + message: '操作成功', + }, + ); + + const commandError = new Error('command failed'); + commandError.stdout = 'stdout details'; + commandError.stderr = 'stderr details'; + responses.push(commandError); + assert.deepEqual( + await service.runDeviceCommand( + 'connect', + '127.0.0.1:16384', + ), + { + success: false, + serial: '127.0.0.1:16384', + status: 'error', + message: 'stderr details', + }, + ); +} + +/** 验证 CUDA 路径说明和实际 PyTorch 检测保持既有语义。 */ +async function testCudaEnvironmentService() { + const cudaRoot = path.join( + temporaryDirectory, + 'cuda', + 'CUDA-v12.4', + ); + const cudaBin = path.join(cudaRoot, 'bin'); + const runtimeDirectory = path.join( + temporaryDirectory, + 'cuda-runtime', + ); + const invalidDirectory = path.join( + temporaryDirectory, + 'cuda-invalid', + ); + fs.mkdirSync(cudaBin, { recursive: true }); + fs.mkdirSync(runtimeDirectory, { recursive: true }); + fs.mkdirSync(invalidDirectory, { recursive: true }); + fs.writeFileSync(path.join(cudaBin, 'nvcc.exe'), 'nvcc', 'utf8'); + fs.writeFileSync( + path.join(cudaRoot, 'version.json'), + JSON.stringify({ cuda: { version: '12.4' } }), + 'utf8', + ); + fs.writeFileSync( + path.join(runtimeDirectory, 'cudart64_121.dll'), + 'cudart', + 'utf8', + ); + fs.writeFileSync( + path.join(runtimeDirectory, 'cublas64_12.dll'), + 'cublas', + 'utf8', + ); + const contractEnvironment = { + pythonCmd: 'python.exe', + startupMode: 'external', + backendRoot: temporaryDirectory, + localSite: path.join(temporaryDirectory, 'site-packages'), + useLocalSite: false, + installTarget: null, + identity: 'external:test', + }; + const runtimeSettings = applyBackendRuntimeSettings( + { PYTHONPATH: 'shared-python-path' }, + { ocrGpuMode: 'cpu', saveImages: false }, + ); + assert.equal(runtimeSettings.PYTHONPATH, 'shared-python-path'); + assert.equal(runtimeSettings.AUTOWSGR_OCR_GPU_MODE, 'cpu'); + assert.equal(runtimeSettings.AUTOWSGR_SAVE_IMAGES, 'false'); + const capabilityProbe = buildBackendCapabilityProbe( + contractEnvironment, + ); + assert.match(capabilityProbe, /gui-runtime-env-v1/); + assert.match(capabilityProbe, /AUTOWSGR_SAVE_IMAGES/); + assert.match(capabilityProbe, /AUTOWSGR_OCR_GPU_MODE/); + const backendBootstrap = buildBackendBootstrap( + contractEnvironment, + 16710, + ); + assert.match(backendBootstrap, /port=16710/); + assert.doesNotMatch(backendBootstrap, /__func__|_patched_|_image_dir/); + + let findPythonCalls = 0; + const noPython = new CudaEnvironmentService({ + findPython: async () => { + findPythonCalls += 1; + return null; + }, + buildRuntimeEnvironment: () => { + throw new Error('must not build runtime environment'); + }, + execute: async () => { + throw new Error('must not execute'); + }, + }); + assert.deepEqual(noPython.validatePath(''), { + valid: false, + path: '', + version: null, + error: '路径为空', + }); + const missingDirectory = path.join( + temporaryDirectory, + 'missing-cuda', + ); + assert.deepEqual(noPython.validatePath(missingDirectory), { + valid: false, + path: path.resolve(missingDirectory), + version: null, + error: '目录不存在', + }); + assert.deepEqual(noPython.validatePath(invalidDirectory), { + valid: false, + path: path.resolve(invalidDirectory), + version: null, + error: '未找到 CUDA Toolkit(bin\\nvcc.exe)或 PyTorch CUDA Runtime DLL', + }); + assert.equal(noPython.normalizePath(cudaBin), cudaRoot); + assert.deepEqual(noPython.validatePath(cudaRoot), { + valid: true, + path: cudaRoot, + version: '12.4', + kind: 'toolkit', + }); + assert.deepEqual(noPython.validatePath(runtimeDirectory), { + valid: true, + path: runtimeDirectory, + version: '12.1', + kind: 'runtime', + }); + assert.deepEqual(await noPython.detect(cudaRoot), { + valid: false, + path: cudaRoot, + version: '12.4', + kind: 'toolkit', + error: '未找到可用的 Python 3.12 或 3.13', + }); + assert.equal(findPythonCalls, 1); + assert.deepEqual(await noPython.detect(invalidDirectory), { + valid: false, + path: invalidDirectory, + version: null, + error: '未找到 CUDA Toolkit(bin\\nvcc.exe)或 PyTorch CUDA Runtime DLL', + }); + assert.equal(findPythonCalls, 1); + + let commandCall = null; + const runtimeEnvironmentCalls = []; + const buildRuntimeEnvironment = ( + pythonCommand, + configuredCudaRoot, + ) => { + runtimeEnvironmentCalls.push({ + pythonCommand, + configuredCudaRoot, + }); + const cudaDirectory = configuredCudaRoot + ? ( + fs.existsSync(path.join(configuredCudaRoot, 'bin')) + ? path.join(configuredCudaRoot, 'bin') + : configuredCudaRoot + ) + : null; + return { + PATH: [ + ...(cudaDirectory ? [cudaDirectory] : []), + 'base-path', + ].join(path.delimiter), + PYTHONPATH: 'shared-python-path', + PYTHONUSERBASE: 'shared-python-user-base', + ...(configuredCudaRoot + ? { + CUDA_PATH: configuredCudaRoot, + CUDA_HOME: configuredCudaRoot, + } + : {}), + }; + }; + const detected = new CudaEnvironmentService({ + findPython: async () => 'python.exe', + buildRuntimeEnvironment, + execute: async (executable, args, options) => { + commandCall = { executable, args, options }; + return { + stdout: [ + 'diagnostic', + JSON.stringify({ + available: true, + torch_version: '2.5.1', + cuda_version: '12.4', + device: 'Test GPU', + }), + ].join('\n'), + }; + }, + }); + assert.deepEqual(await detected.detect(cudaRoot), { + valid: true, + path: cudaRoot, + version: '12.4', + kind: 'toolkit', + torchVersion: '2.5.1', + device: 'Test GPU', + }); + assert.equal(commandCall.executable, 'python.exe'); + assert.equal(commandCall.args[0], '-c'); + assert.match(commandCall.args[1], /torch\.cuda\.is_available/); + assert.equal(commandCall.options.windowsHide, true); + assert.equal(commandCall.options.timeout, 20000); + assert.equal(commandCall.options.encoding, 'utf8'); + assert.equal(commandCall.options.env.CUDA_PATH, cudaRoot); + assert.equal(commandCall.options.env.CUDA_HOME, cudaRoot); + assert.equal( + commandCall.options.env.PYTHONPATH, + 'shared-python-path', + ); + assert.equal( + commandCall.options.env.PYTHONUSERBASE, + 'shared-python-user-base', + ); + assert.equal( + commandCall.options.env.PATH.split(path.delimiter)[0], + cudaBin, + ); + assert.deepEqual(runtimeEnvironmentCalls[0], { + pythonCommand: 'python.exe', + configuredCudaRoot: cudaRoot, + }); + + const unavailable = new CudaEnvironmentService({ + findPython: async () => 'python.exe', + buildRuntimeEnvironment, + execute: async () => ({ + stdout: JSON.stringify({ + available: false, + torch_version: '2.5.1', + cuda_version: null, + }), + }), + }); + assert.deepEqual(await unavailable.detect(''), { + valid: false, + path: '', + version: null, + kind: undefined, + torchVersion: '2.5.1', + device: null, + error: 'PyTorch 2.5.1 未检测到可用 CUDA', + }); + + const importFailure = new CudaEnvironmentService({ + findPython: async () => 'python.exe', + buildRuntimeEnvironment, + execute: async () => ({ + stdout: JSON.stringify({ + available: false, + error: 'No module named torch', + }), + }), + }); + assert.deepEqual(await importFailure.detect(''), { + valid: false, + path: '', + version: null, + kind: undefined, + torchVersion: null, + device: null, + error: 'PyTorch 检测失败:No module named torch', + }); + + const executionFailure = new CudaEnvironmentService({ + findPython: async () => 'python.exe', + buildRuntimeEnvironment, + execute: async () => { + throw new Error('execution failed'); + }, + }); + assert.deepEqual(await executionFailure.detect(runtimeDirectory), { + valid: false, + path: runtimeDirectory, + version: '12.1', + kind: 'runtime', + error: '硬件检测失败:execution failed', + }); +} + +module.exports = { + testPythonEnvironmentService, + testAdbService, + testCudaEnvironmentService, +}; diff --git a/scripts/main-services/test-migration.js b/scripts/main-services/test-migration.js new file mode 100644 index 0000000..b9c93e8 --- /dev/null +++ b/scripts/main-services/test-migration.js @@ -0,0 +1,1542 @@ +/** + * 用户数据和旧计划迁移服务测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + buildLegacyMigrationNotice, +} = require('../../dist/electron/services/LegacyMigrationNotice.js'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + MigrationConflictService, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + TaskPresetCodec, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; + +/** 验证旧配置、旧计划和任务组引用迁移保持幂等。 */ +function testUserDataMigration() { + const migrationRoot = path.join(temporaryDirectory, 'migration'); + const projectRoot = path.join(migrationRoot, 'project'); + const moduleDirectory = path.join(projectRoot, 'dist', 'electron'); + const userData = path.join(migrationRoot, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory, + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const atomicFiles = new AtomicFileStore(); + const userDataMigration = new UserDataMigrationService( + appPaths, + atomicFiles, + ); + + fs.mkdirSync(path.join(projectRoot, 'templates'), { recursive: true }); + fs.mkdirSync(path.join(projectRoot, 'plans'), { recursive: true }); + fs.mkdirSync( + path.join(projectRoot, 'resource', 'user_battle_plans'), + { recursive: true }, + ); + fs.mkdirSync( + path.join(projectRoot, 'resource', 'user_team_plans'), + { recursive: true }, + ); + fs.mkdirSync(appPaths.userBattlePlansDir(), { recursive: true }); + fs.mkdirSync(appPaths.userTeamPlansDir(), { recursive: true }); + fs.writeFileSync( + path.join(userData, 'usersettings.yaml'), + [ + 'legacy: false', + 'new_setting: keep', + 'nested:', + ' new_only: 1', + '', + ].join('\n'), + 'utf8', + ); + fs.writeFileSync( + path.join(userData, 'gui_settings.json'), + JSON.stringify({ + legacy: false, + new_setting: 'keep', + nested: { new_only: 1 }, + }), + 'utf8', + ); + fs.writeFileSync( + path.join(projectRoot, 'usersettings.yaml'), + [ + 'legacy: true', + 'nested:', + ' old_value: 2', + '', + ].join('\n'), + 'utf8', + ); + fs.writeFileSync( + path.join(projectRoot, 'gui_settings.json'), + JSON.stringify({ + legacy: true, + nested: { old_value: 2 }, + decisive_plan: { + chapter: 3, + use_quick_repair: false, + level1: ['U-47', 'U-96'], + level2: ['U-505'], + }, + }), + 'utf8', + ); + fs.writeFileSync( + path.join(projectRoot, 'templates', 'legacy.json'), + '{"template":true}', + 'utf8', + ); + fs.writeFileSync( + path.join(projectRoot, 'task_groups.json'), + JSON.stringify({ + rootExtension: { preserved: true }, + groups: [{ + name: 'legacy', + groupExtension: 'keep', + items: [{ + path: 'plans/legacy.yaml', + itemExtension: { preserved: true }, + }, { + path: [ + 'python', + 'site-packages', + 'autowsgr', + 'data', + 'plan', + '自动演习.yaml', + ].join(path.sep), + label: '旧自动演习', + times: 1, + }, { + path: 'resource/user_battle_plans/bettle-普通驱逐.yaml', + managedSource: 'user', + managedFile: 'bettle-普通驱逐.yaml', + kind: 'preset', + label: '旧普通驱逐战役', + times: 3, + }], + }], + }), + 'utf8', + ); + fs.writeFileSync( + path.join(projectRoot, 'plans', 'legacy.yaml'), + [ + 'chapter: 1', + 'map: 2', + 'fleet_presets:', + ' - name: Legacy Team', + ' ships:', + ' - U-47', + ' - priority: [U-96, U-81]', + ' ship_type: [ss]', + '', + ].join('\n'), + 'utf8', + ); + fs.writeFileSync( + path.join( + projectRoot, + 'resource', + 'user_team_plans', + 'old-reference.yaml', + ), + [ + 'name: Referenced Team', + 'ships:', + ' - candidates:', + ' - name: U-505', + '', + ].join('\n'), + 'utf8', + ); + fs.writeFileSync( + path.join( + projectRoot, + 'resource', + 'user_battle_plans', + 'weekly.yml', + ), + [ + 'chapter: 2', + 'map: 3', + 'fleet_presets:', + ' - name: Referenced Team', + '', + ].join('\n'), + 'utf8', + ); + const recursivePreset = path.join( + projectRoot, + 'python', + 'site-packages', + 'autowsgr', + 'data', + 'plan', + '自动演习.yaml', + ); + fs.mkdirSync(path.dirname(recursivePreset), { recursive: true }); + fs.writeFileSync( + recursivePreset, + 'task_type: exercise\nfleet_id: 4\ntimes: 1\n', + 'utf8', + ); + const misplacedCampaign = path.join( + appPaths.userBattlePlansDir(), + 'bettle-普通驱逐.yaml', + ); + fs.writeFileSync( + misplacedCampaign, + 'task_type: campaign\ncampaign_name: 普通驱逐\ntimes: 3\n', + 'utf8', + ); + const unrelatedYaml = path.join( + projectRoot, + 'python', + 'unrelated.yaml', + ); + fs.mkdirSync(path.dirname(unrelatedYaml), { recursive: true }); + fs.writeFileSync(unrelatedYaml, 'package: metadata\n', 'utf8'); + + const userDataResult = ( + userDataMigration.migrateLegacyUserDataFiles() + ); + assert.deepEqual( + { + total: userDataResult.total, + succeeded: userDataResult.succeeded, + failed: userDataResult.failed, + }, + { total: 4, succeeded: 4, failed: 0 }, + ); + assert.deepEqual( + yaml.load( + fs.readFileSync(path.join(userData, 'usersettings.yaml'), 'utf8'), + ), + { + legacy: true, + new_setting: 'keep', + nested: { + new_only: 1, + old_value: 2, + }, + }, + ); + assert.deepEqual( + JSON.parse( + fs.readFileSync(path.join(userData, 'gui_settings.json'), 'utf8'), + ), + { + legacy: true, + new_setting: 'keep', + nested: { + new_only: 1, + old_value: 2, + }, + decisive_plan: { + chapter: 3, + use_quick_repair: false, + level1: ['U-47', 'U-96'], + level2: ['U-505'], + }, + }, + ); + assert.equal( + fs.readFileSync( + path.join(userData, 'templates', 'legacy.json'), + 'utf8', + ), + '{"template":true}', + ); + assert.equal( + userDataMigration.migrateLegacyUserDataFiles().total, + 0, + ); + + userDataMigration.writeState({ + version: 2, + completed: [ + `plan:${path.join(projectRoot, 'plans')}:legacy.yaml`, + ], + }); + + const teamCodec = new TeamPlanCodec(); + const teamRepository = new TeamPlanRepository( + appPaths, + atomicFiles, + teamCodec, + ); + const combatCodec = new CombatPlanCodec( + teamCodec, + teamRepository, + ); + const combatRepository = new CombatPlanRepository( + appPaths, + atomicFiles, + ); + const taskPresetCodec = new TaskPresetCodec(); + const legacyMigration = new LegacyPlanMigration( + appPaths, + atomicFiles, + userDataMigration, + { + yamlFiles: directory => combatRepository.yamlFiles(directory), + safePlanBaseName: value => combatCodec.safeBaseName(value), + normalizeUserTeamPlan: raw => teamCodec.normalize(raw), + teamPlanMatches: (filePath, team) => ( + teamRepository.matches(filePath, team) + ), + teamName: team => team.name, + renameTeam: (team, name) => ({ + ...structuredClone(team), + name, + }), + normalizeCombatPlanFleetPresets: ( + root, + source, + requireEmbeddedShips, + ) => combatCodec.normalizeFleetPresets( + root, + source, + requireEmbeddedShips, + ), + buildTeamPlanWrites: (teams, directory) => ( + teamRepository.buildWrites(teams, directory) + ), + serializeCombatPlan: (root, originalContent) => ( + combatCodec.serialize(root, originalContent) + ), + isStandaloneTaskPreset: root => ( + taskPresetCodec.isStandalone(root) + ), + normalizeTaskPreset: root => taskPresetCodec.normalize(root), + }, + ); + assert.equal(userDataMigration.migratePresetInventory().failed, 0); + assert.equal(userDataMigration.readState().version, 6); + const planResult = legacyMigration.migrate(); + assert.deepEqual( + { + total: planResult.total, + succeeded: planResult.succeeded, + failed: planResult.failed, + }, + { total: 6, succeeded: 6, failed: 0 }, + ); + + const migratedPlanPath = path.join( + appPaths.userBattlePlansDir(), + 'bettle-legacy.yaml', + ); + const migratedTeamPath = path.join( + appPaths.userTeamPlansDir(), + 'team-Legacy Team.yaml', + ); + const referencedPlanPath = path.join( + appPaths.userBattlePlansDir(), + 'bettle-weekly.yaml', + ); + const referencedTeamPath = path.join( + appPaths.userTeamPlansDir(), + 'team-Referenced Team.yaml', + ); + assert.equal(fs.existsSync(migratedPlanPath), true); + assert.equal(fs.existsSync(migratedTeamPath), true); + assert.equal(fs.existsSync(referencedPlanPath), true); + assert.equal(fs.existsSync(referencedTeamPath), true); + assert.equal( + fs.existsSync(path.join( + appPaths.userDailyPlansDir(), + 'exercise-队伍4演习.yaml', + )), + true, + ); + assert.equal( + fs.existsSync(path.join( + appPaths.userBattlePlansDir(), + 'bettle-自动演习.yaml', + )), + false, + ); + const migratedDecisive = yaml.load(fs.readFileSync(path.join( + appPaths.userDailyPlansDir(), + 'decisive-决战第3章.yaml', + ), 'utf8')); + assert.equal(migratedDecisive.chapter, 3); + assert.equal(migratedDecisive.use_quick_repair, false); + assert.deepEqual(migratedDecisive.level1, ['U-47', 'U-96']); + assert.deepEqual(migratedDecisive.level2, ['U-505']); + const migratedCampaign = yaml.load(fs.readFileSync(path.join( + appPaths.userDailyPlansDir(), + 'campaign-普通驱逐.yaml', + ), 'utf8')); + assert.equal(migratedCampaign.campaign_name, '简单驱逐'); + assert.equal(migratedCampaign.times, 3); + assert.equal(fs.existsSync(misplacedCampaign), true); + assert.equal( + fs.existsSync(path.join( + appPaths.userBattlePlansDir(), + 'bettle-unrelated.yaml', + )), + false, + ); + assert.deepEqual( + yaml.load(fs.readFileSync(migratedPlanPath, 'utf8')).fleet_presets, + [{ name: 'Legacy Team' }], + ); + assert.deepEqual( + yaml.load(fs.readFileSync(migratedTeamPath, 'utf8')).ships, + [ + { name: 'U-47' }, + { + ship_type: ['ss'], + candidates: [ + { name: 'U-96', ship_type: ['ss'] }, + { name: 'U-81', ship_type: ['ss'] }, + ], + }, + ], + ); + assert.deepEqual( + yaml.load(fs.readFileSync(referencedPlanPath, 'utf8')).fleet_presets, + [{ name: 'Referenced Team' }], + ); + assert.equal( + fs.existsSync(path.join(projectRoot, 'plans', 'legacy.yaml')), + true, + ); + assert.equal( + fs.existsSync(path.join( + projectRoot, + 'resource', + 'user_battle_plans', + 'weekly.yml', + )), + true, + ); + const taskGroups = JSON.parse( + fs.readFileSync(path.join(userData, 'task_groups.json'), 'utf8'), + ); + assert.equal(taskGroups.version, 4); + assert.equal(taskGroups.rootExtension.preserved, true); + assert.equal(taskGroups.groups[0].groupExtension, 'keep'); + assert.equal( + taskGroups.groups[0].items[0].itemExtension.preserved, + true, + ); + assert.equal(taskGroups.groups[0].items[0].path, 'plans/legacy.yaml'); + assert.equal(taskGroups.groups[0].items[0].managedSource, 'user'); + assert.equal( + taskGroups.groups[0].items[0].managedFile, + 'bettle-legacy.yaml', + ); + assert.equal(taskGroups.groups[0].items[1].kind, 'daily'); + assert.equal( + taskGroups.groups[0].items[1].dailySource, + 'user', + ); + assert.equal( + taskGroups.groups[0].items[1].dailyFile, + 'exercise-队伍4演习.yaml', + ); + assert.equal( + taskGroups.groups[0].items[1].dailyTaskType, + 'exercise', + ); + assert.equal(taskGroups.groups[0].items[1].managedFile, undefined); + assert.equal(taskGroups.groups[0].items[2].kind, 'daily'); + assert.equal( + taskGroups.groups[0].items[2].dailyFile, + 'campaign-普通驱逐.yaml', + ); + assert.equal( + taskGroups.groups[0].items[2].dailyTaskType, + 'campaign', + ); + assert.equal(taskGroups.groups[0].items[2].managedFile, undefined); + assert.equal(userDataMigration.readState().version, 7); + assert.equal( + userDataMigration.readState().version, + 7, + 'v6 库存升级后必须继续执行 v7 日常任务分类迁移', + ); + assert.equal(userDataMigration.migratePresetInventory().total, 0); + + const planBeforeSecondRun = fs.readFileSync(migratedPlanPath, 'utf8'); + assert.equal(legacyMigration.migrate().total, 0); + assert.equal( + fs.readFileSync(migratedPlanPath, 'utf8'), + planBeforeSecondRun, + ); + testLegacyMigrationNotice(); + testLegacyPlanConflictRetry(); + testExistingUserDataCompatibility(); + testLegacyLootPlanIndexMigration(); + testPresetInventoryMigration(); + testMigrationConflictResolution(); +} + +/** 验证不同旧版本的数字索引都迁移为原地图的稳定文件名。 */ +function testLegacyLootPlanIndexMigration() { + const legacyFivePlanPaths = [ + 'resource/builtin_plans/9-4胖次6SS.yaml', + 'resource/builtin_plans/周常9章-9-2.yaml', + 'resource/builtin_plans/周常7章-7-4.yaml', + 'resource/builtin_plans/8-5胖次.yaml', + 'resource/builtin_plans/周常2章-2-1.yaml', + ]; + const cases = [ + { + name: 'fallback-four-item-layout', + index: 2, + expected: 'bettle-old-8-5AI六潜胖次.yaml', + }, + { + name: 'installed-five-item-index-zero', + index: 0, + planPaths: legacyFivePlanPaths, + expected: 'bettle-old-9-4六潜练级.yaml', + }, + { + name: 'installed-five-item-index-two', + index: 2, + planPaths: legacyFivePlanPaths, + expected: 'bettle-周常-7-4.yaml', + }, + { + name: 'installed-template-with-unknown-path', + index: 0, + planPaths: ['resource/builtin_plans/未知地图.yaml'], + expected: 'bettle-周常-9-2.yaml', + expectedAutoLoot: false, + }, + { + name: 'invalid-null-index', + index: null, + expected: 'bettle-周常-9-2.yaml', + expectedAutoLoot: false, + }, + ]; + + for (const migrationCase of cases) { + const root = path.join( + temporaryDirectory, + `loot-index-${migrationCase.name}`, + ); + const projectRoot = path.join(root, 'project'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const source = path.join(projectRoot, 'usersettings.yaml'); + fs.mkdirSync(projectRoot, { recursive: true }); + fs.writeFileSync(source, [ + 'daily_automation:', + ' auto_loot: true', + ` loot_plan_index: ${migrationCase.index}`, + '', + ].join('\n'), 'utf8'); + + if (migrationCase.planPaths) { + const templateFile = path.join( + projectRoot, + 'resources', + 'resource', + 'builtin_templates.json', + ); + fs.mkdirSync(path.dirname(templateFile), { recursive: true }); + fs.writeFileSync(templateFile, JSON.stringify([{ + id: 'builtin_farm_loot', + planPaths: migrationCase.planPaths, + }]), 'utf8'); + } + + const migration = new UserDataMigrationService( + appPaths, + new AtomicFileStore(), + ); + const result = migration.migrateLegacyUserDataFiles(); + assert.equal(result.failed, 0); + const migrated = yaml.load(fs.readFileSync( + path.join(userData, 'usersettings.yaml'), + 'utf8', + )); + assert.equal( + migrated.daily_automation.loot_plan_id, + migrationCase.expected, + ); + assert.equal( + migrated.daily_automation.auto_loot, + migrationCase.expectedAutoLoot ?? true, + ); + assert.equal( + Object.hasOwn( + migrated.daily_automation, + 'loot_plan_index', + ), + false, + ); + assert.equal( + yaml.load(fs.readFileSync(source, 'utf8')) + .daily_automation.loot_plan_index, + migrationCase.index, + '旧安装源配置不应被修改', + ); + } + + const root = path.join( + temporaryDirectory, + 'loot-index-already-moved-to-gui-json', + ); + const projectRoot = path.join(root, 'project'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const source = path.join(projectRoot, 'usersettings.yaml'); + const templateFile = path.join( + projectRoot, + 'resources', + 'resource', + 'builtin_templates.json', + ); + const guiSettingsFile = path.join(userData, 'gui_settings.json'); + fs.mkdirSync(path.dirname(templateFile), { recursive: true }); + fs.mkdirSync(userData, { recursive: true }); + fs.writeFileSync(source, [ + 'daily_automation:', + ' auto_loot: true', + ' loot_plan_index: 2', + '', + ].join('\n'), 'utf8'); + fs.writeFileSync(templateFile, JSON.stringify([{ + id: 'builtin_farm_loot', + planPaths: legacyFivePlanPaths, + }]), 'utf8'); + fs.writeFileSync(guiSettingsFile, JSON.stringify({ + automation: { + autoLoot: true, + lootPlanIndex: 2, + }, + }), 'utf8'); + + const migration = new UserDataMigrationService( + appPaths, + new AtomicFileStore(), + ); + assert.equal(migration.migrateLegacyUserDataFiles().failed, 0); + const migratedGui = JSON.parse(fs.readFileSync(guiSettingsFile, 'utf8')); + assert.equal( + migratedGui.automation.lootPlanId, + 'bettle-周常-7-4.yaml', + '已搬到 GUI JSON 的旧五项索引没有恢复原地图', + ); + assert.equal( + Object.hasOwn(migratedGui.automation, 'lootPlanIndex'), + false, + ); + migration.migrateLegacyUserDataFiles(); + assert.equal( + JSON.parse(fs.readFileSync(guiSettingsFile, 'utf8')) + .automation.lootPlanId, + 'bettle-周常-7-4.yaml', + '稳定标识不应在再次启动时被重复解释', + ); + + const retryRoot = path.join( + temporaryDirectory, + 'loot-index-reconcile-retry', + ); + const retryProjectRoot = path.join(retryRoot, 'project'); + const retryUserData = path.join(retryRoot, 'user-data'); + const retryAppPaths = new AppPaths({ + moduleDirectory: path.join(retryProjectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(retryProjectRoot, 'AutoWSGR.exe') + : retryUserData, + getResourcesPath: () => path.join(retryProjectRoot, 'resources'), + }); + const retryGuiSettings = path.join( + retryUserData, + 'gui_settings.json', + ); + const retryTemplate = path.join( + retryProjectRoot, + 'resources', + 'resource', + 'builtin_templates.json', + ); + fs.mkdirSync(path.dirname(retryTemplate), { recursive: true }); + fs.mkdirSync(retryUserData, { recursive: true }); + fs.writeFileSync( + path.join(retryProjectRoot, 'usersettings.yaml'), + 'daily_automation:\n loot_plan_index: 2\n', + 'utf8', + ); + fs.writeFileSync(retryTemplate, JSON.stringify([{ + id: 'builtin_farm_loot', + planPaths: legacyFivePlanPaths, + }]), 'utf8'); + fs.writeFileSync(retryGuiSettings, JSON.stringify({ + automation: { autoLoot: true, lootPlanIndex: 2 }, + }), 'utf8'); + + const realAtomicFiles = new AtomicFileStore(); + let failReconcileWrite = true; + const retryMigration = new UserDataMigrationService( + retryAppPaths, + { + write(file, content) { + if ( + failReconcileWrite + && file === retryGuiSettings + && content.includes('"lootPlanId"') + ) { + failReconcileWrite = false; + throw new Error('模拟 GUI 索引纠正写入失败'); + } + realAtomicFiles.write(file, content); + }, + }, + ); + const originalConsoleError = console.error; + let migrationFailureLogged = false; + let failed; + console.error = (...args) => { + migrationFailureLogged = String(args[0]).startsWith('[Migration]'); + }; + try { + failed = retryMigration.migrateLegacyUserDataFiles(); + } finally { + console.error = originalConsoleError; + } + assert.equal(failed.failed, 1); + assert.equal(migrationFailureLogged, true); + assert.equal(failed.failedFiles.includes(retryGuiSettings), true); + assert.equal( + JSON.parse(fs.readFileSync(retryGuiSettings, 'utf8')) + .automation.lootPlanIndex, + 2, + ); + + const retried = retryMigration.migrateLegacyUserDataFiles(); + assert.equal(retried.failed, 0); + assert.equal( + JSON.parse(fs.readFileSync(retryGuiSettings, 'utf8')) + .automation.lootPlanId, + 'bettle-周常-7-4.yaml', + '纠正失败后必须在下次启动重试', + ); +} + +/** 验证 v6 淘汰预设转为个人计划,并在失败后稳定重试。 */ +function testPresetInventoryMigration() { + const root = path.join(temporaryDirectory, 'preset-inventory-v6'); + const projectRoot = path.join(root, 'project'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const sourceResources = path.join( + __dirname, + '..', + '..', + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + ); + const migrationResources = path.join( + appPaths.resourceRoot(), + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + ); + fs.mkdirSync(path.dirname(migrationResources), { recursive: true }); + fs.cpSync(sourceResources, migrationResources, { recursive: true }); + fs.mkdirSync(path.join(userData, 'templates'), { recursive: true }); + fs.mkdirSync(appPaths.userBattlePlansDir(), { recursive: true }); + + const guiSettings = path.join(userData, 'gui_settings.json'); + const userSettings = path.join(userData, 'usersettings.yaml'); + const taskGroups = path.join(userData, 'task_groups.json'); + const templates = path.join(userData, 'templates', 'templates.json'); + fs.writeFileSync(guiSettings, JSON.stringify({ + automation: { + autoLoot: true, + lootPlanId: 'bettle-捞胖次-8-5.yaml', + }, + }), 'utf8'); + fs.writeFileSync(userSettings, [ + 'daily_automation:', + ' auto_loot: true', + ' loot_plan_id: bettle-捞胖次-9-4-6SS.yaml', + '', + ].join('\n'), 'utf8'); + fs.writeFileSync(taskGroups, JSON.stringify({ + version: 3, + activeGroup: '旧预设', + groups: [{ + name: '旧预设', + items: [ + { + path: 'resource/system_battle_plans/bettle-E1炸鱼.yaml', + managedSource: 'system', + managedFile: 'bettle-E1炸鱼.yaml', + kind: 'plan', + times: 1, + label: 'E1 炸鱼', + }, + { + path: 'resource/builtin_plans/周常3章-3-3.yaml', + kind: 'plan', + times: 1, + label: '旧周常 3-3', + }, + { + path: 'resource/system_battle_plans/bettle-周常-2-1.yaml', + managedSource: 'system', + managedFile: 'bettle-周常-2-1.yaml', + kind: 'plan', + times: 1, + label: '当前周常 2-1', + }, + ], + }], + }), 'utf8'); + fs.writeFileSync(templates, JSON.stringify([ + { + id: 'legacy-event', + planPath: 'resource/system_battle_plans/bettle-E5夜战.yaml', + }, + { + id: 'legacy-weekly', + planPaths: [ + 'resource/builtin_plans/周常6章-6-3.yaml', + 'resource/system_battle_plans/bettle-周常-2-1.yaml', + ], + }, + ]), 'utf8'); + + const e1Source = path.join(migrationResources, 'bettle-E1炸鱼.yaml'); + const e5Source = path.join(migrationResources, 'bettle-E5夜战.yaml'); + const e1Target = path.join( + appPaths.userBattlePlansDir(), + 'bettle-E1炸鱼.yaml', + ); + const e1LegacyTarget = path.join( + appPaths.userBattlePlansDir(), + 'bettle-E1炸鱼(旧版).yaml', + ); + fs.writeFileSync(e1Target, 'chapter: 99\nmap: 99\n', 'utf8'); + fs.copyFileSync( + e5Source, + path.join(appPaths.userBattlePlansDir(), 'bettle-E5夜战.yaml'), + ); + + const realAtomicFiles = new AtomicFileStore(); + let failTaskGroupWrite = true; + const migration = new UserDataMigrationService(appPaths, { + write(file, content) { + if (failTaskGroupWrite && file === taskGroups) { + failTaskGroupWrite = false; + throw new Error('模拟 v6 任务组写入失败'); + } + realAtomicFiles.write(file, content); + }, + }); + migration.writeState({ version: 5, completed: ['v5-complete'] }); + + const originalConsoleError = console.error; + let firstResult; + console.error = () => {}; + try { + firstResult = migration.migratePresetInventory(); + } finally { + console.error = originalConsoleError; + } + assert.equal(firstResult.failed, 1); + assert.equal(firstResult.failedFiles.includes(taskGroups), true); + assert.equal(migration.readState().version, 5); + assert.equal( + fs.readFileSync(e1LegacyTarget, 'utf8'), + fs.readFileSync(e1Source, 'utf8'), + '同名不同内容的个人计划必须保留为旧版副本', + ); + + const retried = migration.migratePresetInventory(); + assert.deepEqual( + { + total: retried.total, + succeeded: retried.succeeded, + failed: retried.failed, + }, + { total: 1, succeeded: 1, failed: 0 }, + ); + assert.equal(migration.readState().version, 6); + assert.equal(migration.readState().completed.includes('v5-complete'), true); + + const migratedGroups = JSON.parse(fs.readFileSync(taskGroups, 'utf8')); + const [eventItem, weeklyItem, currentItem] = ( + migratedGroups.groups[0].items + ); + assert.equal(eventItem.managedSource, 'user'); + assert.equal(eventItem.managedFile, 'bettle-E1炸鱼(旧版).yaml'); + assert.equal(weeklyItem.managedSource, 'user'); + assert.equal(weeklyItem.managedFile, 'bettle-周常-3-3-v1.yaml'); + assert.equal(currentItem.managedSource, 'system'); + assert.equal(currentItem.managedFile, 'bettle-周常-2-1.yaml'); + + const migratedTemplates = JSON.parse(fs.readFileSync(templates, 'utf8')); + assert.equal( + migratedTemplates[0].planPath, + 'user_battle_plans/bettle-E5夜战.yaml', + '同名同内容的个人计划必须直接复用', + ); + assert.deepEqual(migratedTemplates[1].planPaths, [ + 'user_battle_plans/bettle-周常-6-3-v1.yaml', + 'resource/system_battle_plans/bettle-周常-2-1.yaml', + ]); + assert.equal( + JSON.parse(fs.readFileSync(guiSettings, 'utf8')) + .automation.lootPlanId, + 'bettle-old-8-5AI六潜胖次.yaml', + ); + assert.equal( + yaml.load(fs.readFileSync(userSettings, 'utf8')) + .daily_automation.loot_plan_id, + 'bettle-old-9-4六潜练级.yaml', + ); + assert.equal(migration.migratePresetInventory().total, 0); + assert.equal( + fs.readdirSync(appPaths.userBattlePlansDir()) + .filter(file => file.startsWith('bettle-E1炸鱼(旧版')) + .length, + 1, + '失败重试不能生成重复旧版副本', + ); +} + +/** 验证迁移提示展示真实计数、失败文件和源文件保留说明。 */ +function testLegacyMigrationNotice() { + assert.equal( + buildLegacyMigrationNotice({ + detected: false, + total: 1, + succeeded: 1, + failed: 0, + failedFiles: [], + }), + null, + ); + assert.equal( + buildLegacyMigrationNotice({ + detected: true, + total: 0, + succeeded: 0, + failed: 0, + failedFiles: [], + }), + null, + ); + const successNotice = buildLegacyMigrationNotice({ + detected: true, + total: 3, + succeeded: 3, + failed: 0, + failedFiles: [], + }); + assert.ok(successNotice); + assert.equal(successNotice.type, 'info'); + assert.match(successNotice.message, /成功:3 项/); + assert.match(successNotice.message, /失败:0 项/); + const notice = buildLegacyMigrationNotice({ + detected: true, + total: 5, + succeeded: 4, + failed: 1, + failedFiles: ['C:\\old\\broken.yaml'], + }); + assert.ok(notice); + assert.equal(notice.type, 'warning'); + assert.match(notice.message, /当前已迁移旧版数据:5 项/); + assert.match(notice.message, /成功:4 项/); + assert.match(notice.message, /失败:1 项/); + assert.match(notice.detail, /旧版本原始目录/); + assert.match(notice.detail, /broken\.yaml/); +} + +/** 验证同名计划保留为旧版副本,并在下次启动恢复实际引用。 */ +function testLegacyPlanConflictRetry() { + const root = path.join(temporaryDirectory, 'migration-conflict'); + const projectRoot = path.join(root, 'project'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const atomicFiles = new AtomicFileStore(); + const userDataMigration = new UserDataMigrationService( + appPaths, + atomicFiles, + ); + const teamCodec = new TeamPlanCodec(); + const teamRepository = new TeamPlanRepository( + appPaths, + atomicFiles, + teamCodec, + ); + const combatRepository = new CombatPlanRepository( + appPaths, + atomicFiles, + ); + const combatCodec = new CombatPlanCodec( + teamCodec, + teamRepository, + ); + const migration = new LegacyPlanMigration( + appPaths, + atomicFiles, + userDataMigration, + { + yamlFiles: directory => combatRepository.yamlFiles(directory), + safePlanBaseName: value => combatCodec.safeBaseName(value), + normalizeUserTeamPlan: raw => teamCodec.normalize(raw), + teamPlanMatches: (filePath, team) => ( + teamRepository.matches(filePath, team) + ), + teamName: team => team.name, + renameTeam: (team, name) => ({ + ...structuredClone(team), + name, + }), + normalizeCombatPlanFleetPresets: ( + planRoot, + source, + requireEmbeddedShips, + ) => combatCodec.normalizeFleetPresets( + planRoot, + source, + requireEmbeddedShips, + ), + buildTeamPlanWrites: (teams, directory) => ( + teamRepository.buildWrites(teams, directory) + ), + serializeCombatPlan: (planRoot, originalContent) => ( + combatCodec.serialize(planRoot, originalContent) + ), + }, + ); + const source = path.join(projectRoot, 'plans', 'conflict.yaml'); + const target = path.join( + appPaths.userBattlePlansDir(), + 'bettle-conflict.yaml', + ); + fs.mkdirSync(path.dirname(source), { recursive: true }); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(source, 'chapter: 1\nmap: 1\n', 'utf8'); + fs.writeFileSync(target, 'chapter: 9\nmap: 9\n', 'utf8'); + fs.writeFileSync( + path.join(userData, 'task_groups.json'), + JSON.stringify({ + groups: [{ + name: '旧任务', + items: [{ path: 'plans/conflict.yaml' }], + }], + }), + 'utf8', + ); + + const result = migration.migrate(); + const legacyTarget = path.join( + appPaths.userBattlePlansDir(), + 'bettle-conflict(旧版).yaml', + ); + assert.deepEqual( + { + total: result.total, + succeeded: result.succeeded, + failed: result.failed, + }, + { total: 1, succeeded: 1, failed: 0 }, + ); + assert.equal(fs.readFileSync(target, 'utf8'), 'chapter: 9\nmap: 9\n'); + assert.equal(fs.existsSync(source), true); + assert.equal(yaml.load(fs.readFileSync(legacyTarget, 'utf8')).chapter, 1); + assert.equal(userDataMigration.readState().version, 7); + assert.equal( + userDataMigration.readState().completed.some( + value => value.startsWith('plan-output-v7:'), + ), + true, + ); + let taskGroups = JSON.parse( + fs.readFileSync(path.join(userData, 'task_groups.json'), 'utf8'), + ); + assert.equal( + taskGroups.groups[0].items[0].managedFile, + 'bettle-conflict(旧版).yaml', + ); + + assert.equal(migration.migrate().total, 0); + taskGroups = JSON.parse( + fs.readFileSync(path.join(userData, 'task_groups.json'), 'utf8'), + ); + assert.equal( + taskGroups.groups[0].items[0].managedFile, + 'bettle-conflict(旧版).yaml', + ); +} + +/** 验证已有 userData 时仍会合并另一安装目录的旧队列和计划。 */ +function testExistingUserDataCompatibility() { + const root = path.join(temporaryDirectory, 'migration-existing-data'); + const projectRoot = path.join(root, 'old-install'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const atomicFiles = new AtomicFileStore(); + const userDataMigration = new UserDataMigrationService( + appPaths, + atomicFiles, + ); + const teamCodec = new TeamPlanCodec(); + const teamRepository = new TeamPlanRepository( + appPaths, + atomicFiles, + teamCodec, + ); + const combatRepository = new CombatPlanRepository( + appPaths, + atomicFiles, + ); + const combatCodec = new CombatPlanCodec( + teamCodec, + teamRepository, + ); + const sourceTaskGroups = path.join(projectRoot, 'task_groups.json'); + const targetTaskGroups = path.join(userData, 'task_groups.json'); + const sourcePlan = path.join(projectRoot, 'plans', 'old.yaml'); + const migratedPlan = path.join( + appPaths.userBattlePlansDir(), + 'bettle-old.yaml', + ); + const sourcePlanContent = [ + 'chapter: 9', + 'map: 3', + 'fleet_presets:', + ' - name: Legacy Team', + ' ships:', + ' - Old Ship', + '', + ].join('\n'); + + fs.mkdirSync(path.dirname(sourcePlan), { recursive: true }); + fs.mkdirSync(userData, { recursive: true }); + fs.mkdirSync(appPaths.userBattlePlansDir(), { recursive: true }); + fs.mkdirSync(appPaths.userTeamPlansDir(), { recursive: true }); + fs.writeFileSync(sourcePlan, sourcePlanContent, 'utf8'); + const split = combatCodec.normalizeFleetPresets( + yaml.load(sourcePlanContent), + 'user', + false, + ); + fs.writeFileSync( + migratedPlan, + combatCodec.serialize(split.mapRoot, sourcePlanContent), + 'utf8', + ); + const [existingTeam] = teamRepository.buildWrites( + [{ + name: 'Legacy Team', + ships: [{ name: 'Current Ship' }], + }], + appPaths.userTeamPlansDir(), + ); + assert.ok(existingTeam); + fs.writeFileSync(existingTeam.path, existingTeam.content, 'utf8'); + fs.writeFileSync( + sourceTaskGroups, + JSON.stringify({ + activeGroup: '默认', + groups: [ + { + name: '默认', + items: [{ + path: 'plans/old.yaml', + kind: 'plan', + times: 2, + label: '旧计划', + }], + }, + { name: '决战', items: [] }, + ], + }), + 'utf8', + ); + fs.writeFileSync( + targetTaskGroups, + JSON.stringify({ + version: 2, + activeGroup: '默认', + groups: [{ + name: '默认', + items: [{ + managedSource: 'system', + managedFile: 'weekly.yaml', + kind: 'plan', + times: 1, + label: '当前计划', + }], + }], + }), + 'utf8', + ); + userDataMigration.writeState({ + version: 3, + completed: ['existing-install-complete'], + }); + + userDataMigration.migrateLegacyUserDataFiles(); + let taskGroups = JSON.parse( + fs.readFileSync(targetTaskGroups, 'utf8'), + ); + assert.deepEqual( + taskGroups.groups.map(group => group.name), + ['默认', '默认(旧版)', '决战'], + ); + assert.equal(taskGroups.activeGroup, '默认'); + + const migration = new LegacyPlanMigration( + appPaths, + atomicFiles, + userDataMigration, + { + yamlFiles: directory => combatRepository.yamlFiles(directory), + safePlanBaseName: value => combatCodec.safeBaseName(value), + normalizeUserTeamPlan: raw => teamCodec.normalize(raw), + teamPlanMatches: (filePath, team) => ( + teamRepository.matches(filePath, team) + ), + teamName: team => team.name, + renameTeam: (team, name) => ({ + ...structuredClone(team), + name, + }), + normalizeCombatPlanFleetPresets: ( + planRoot, + source, + requireEmbeddedShips, + ) => combatCodec.normalizeFleetPresets( + planRoot, + source, + requireEmbeddedShips, + ), + buildTeamPlanWrites: (teams, directory) => ( + teamRepository.buildWrites(teams, directory) + ), + serializeCombatPlan: (planRoot, originalContent) => ( + combatCodec.serialize(planRoot, originalContent) + ), + }, + ); + userDataMigration.migratePresetInventory(); + assert.equal(userDataMigration.readState().version, 6); + migration.migrate(); + + taskGroups = JSON.parse(fs.readFileSync(targetTaskGroups, 'utf8')); + const legacyGroup = taskGroups.groups.find( + group => group.name === '默认(旧版)', + ); + assert.equal(fs.existsSync(migratedPlan), true); + assert.equal(fs.existsSync(sourcePlan), true); + assert.equal( + legacyGroup.items[0].managedFile, + 'bettle-old.yaml', + ); + assert.equal(legacyGroup.items[0].managedSource, 'user'); + assert.equal( + fs.readFileSync(existingTeam.path, 'utf8'), + existingTeam.content, + ); + assert.equal(userDataMigration.readState().version, 7); + + userDataMigration.migrateLegacyUserDataFiles(); + migration.migrate(); + taskGroups = JSON.parse(fs.readFileSync(targetTaskGroups, 'utf8')); + assert.equal(taskGroups.groups.length, 3); +} + +/** 验证迁移冲突识别、保留、删除和任务列表引用重写。 */ +function testMigrationConflictResolution() { + const root = path.join(temporaryDirectory, 'migration-conflicts'); + const projectRoot = path.join(root, 'project'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const atomicFiles = new AtomicFileStore(); + const conflicts = new MigrationConflictService(appPaths, atomicFiles); + for (const directory of [ + appPaths.systemBattlePlansDir(), + appPaths.userBattlePlansDir(), + appPaths.systemDailyPlansDir(), + appPaths.userDailyPlansDir(), + ]) { + fs.mkdirSync(directory, { recursive: true }); + } + + const systemBattleFile = 'bettle-系统相同.yaml'; + const userBattleFile = 'bettle-迁移副本.yaml'; + fs.writeFileSync( + path.join(appPaths.systemBattlePlansDir(), systemBattleFile), + 'chapter: 1\nmap: 2\nnode_defaults:\n formation: 2\n', + 'utf8', + ); + fs.writeFileSync( + path.join(appPaths.userBattlePlansDir(), userBattleFile), + 'node_defaults: { formation: 2 }\nmap: 2\nchapter: 1\n', + 'utf8', + ); + + const dailyFile = 'campaign-普通驱逐.yaml'; + fs.writeFileSync( + path.join(appPaths.systemDailyPlansDir(), dailyFile), + 'task_type: campaign\ncampaign_name: 简单驱逐\ntimes: 1\n', + 'utf8', + ); + fs.writeFileSync( + path.join(appPaths.userDailyPlansDir(), dailyFile), + 'task_type: campaign\ncampaign_name: 简单驱逐\ntimes: 3\n', + 'utf8', + ); + + fs.writeFileSync( + path.join(appPaths.userBattlePlansDir(), 'bettle-重名.yaml'), + 'chapter: 2\nmap: 1\n', + 'utf8', + ); + const legacyCopyFile = 'bettle-重名(旧版).yaml'; + fs.writeFileSync( + path.join(appPaths.userBattlePlansDir(), legacyCopyFile), + 'chapter: 3\nmap: 1\n', + 'utf8', + ); + const taskGroupsPath = path.join(userData, 'task_groups.json'); + fs.mkdirSync(userData, { recursive: true }); + fs.writeFileSync( + taskGroupsPath, + JSON.stringify({ + version: 4, + groups: [{ + name: '默认', + items: [{ + kind: 'plan', + managedSource: 'user', + managedFile: userBattleFile, + label: '迁移副本', + }], + }], + }), + 'utf8', + ); + const guiSettingsPath = path.join(userData, 'gui_settings.json'); + fs.writeFileSync( + guiSettingsPath, + JSON.stringify({ + automation: { + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: userBattleFile, + lootPlans: [ + { + source: 'system', + file: systemBattleFile, + name: '系统相同', + }, + { + source: 'user', + file: userBattleFile, + name: '迁移副本', + }, + ], + }, + }), + 'utf8', + ); + + conflicts.prepareAfterMigration(false); + const pending = conflicts.pending(); + assert.equal(pending.pending, true); + assert.equal(pending.conflicts.length, 3); + const sameContent = pending.conflicts.find(conflict => ( + conflict.file === userBattleFile + )); + assert.ok(sameContent); + assert.equal( + sameContent.reasons[0].reasonCode, + 'same_as_system_preset', + ); + const sameName = pending.conflicts.find(conflict => ( + conflict.file === dailyFile + )); + assert.ok(sameName); + assert.equal( + sameName.reasons[0].reasonCode, + 'same_name_as_system_preset', + ); + const legacyCopy = pending.conflicts.find(conflict => ( + conflict.file === legacyCopyFile + )); + assert.ok(legacyCopy); + assert.equal( + legacyCopy.reasons[0].reasonCode, + 'legacy_copy_name_conflict', + ); + + const result = conflicts.resolve([ + sameName.id, + legacyCopy.id, + ]); + assert.equal(result.success, true); + assert.equal(result.deleted, 1); + assert.equal(result.kept, 2); + assert.equal( + fs.existsSync(path.join( + appPaths.userBattlePlansDir(), + userBattleFile, + )), + false, + ); + assert.equal( + fs.existsSync(path.join(appPaths.userDailyPlansDir(), dailyFile)), + true, + ); + const migratedGroups = JSON.parse( + fs.readFileSync(taskGroupsPath, 'utf8'), + ); + assert.equal( + migratedGroups.groups[0].items[0].managedSource, + 'system', + ); + assert.equal( + migratedGroups.groups[0].items[0].managedFile, + systemBattleFile, + ); + const migratedGuiSettings = JSON.parse( + fs.readFileSync(guiSettingsPath, 'utf8'), + ); + assert.equal( + migratedGuiSettings.automation.lootPlanSource, + 'system', + ); + assert.equal( + migratedGuiSettings.automation.lootPlanId, + systemBattleFile, + ); + assert.deepEqual( + migratedGuiSettings.automation.lootPlans, + [{ + source: 'system', + file: systemBattleFile, + name: '系统相同', + }], + ); + assert.equal(conflicts.pending().pending, false); + + conflicts.prepareAfterMigration(true); + assert.equal( + conflicts.pending().pending, + false, + '已确认保留且内容未变化的冲突不应重复提示', + ); + const newCopyFile = 'bettle-新增迁移副本.yaml'; + fs.writeFileSync( + path.join(appPaths.userBattlePlansDir(), newCopyFile), + 'chapter: 1\nmap: 2\nnode_defaults:\n formation: 2\n', + 'utf8', + ); + conflicts.prepareAfterMigration(true); + const rescanned = conflicts.pending(); + assert.equal(rescanned.conflicts.length, 1); + assert.equal(rescanned.conflicts[0].file, newCopyFile); + const keepAll = conflicts.resolve( + rescanned.conflicts.map(conflict => conflict.id), + ); + assert.equal(keepAll.success, true); + assert.equal(keepAll.deleted, 0); + assert.equal(keepAll.kept, 1); + assert.equal(conflicts.pending().pending, false); +} + +module.exports = { + testUserDataMigration, +}; diff --git a/scripts/main-services/test-path-and-file.js b/scripts/main-services/test-path-and-file.js new file mode 100644 index 0000000..c7d0ce2 --- /dev/null +++ b/scripts/main-services/test-path-and-file.js @@ -0,0 +1,339 @@ +/** + * 路径、文件安全和原子写入服务测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; + +/** 验证 AppPaths 不依赖真实 Electron app 对象。 */ +function testAppPaths() { + const projectRoot = path.join(temporaryDirectory, 'project'); + const moduleDirectory = path.join(projectRoot, 'dist', 'electron'); + const userData = path.join(temporaryDirectory, 'user-data'); + const executable = path.join(temporaryDirectory, 'install', 'AutoWSGR.exe'); + const resources = path.join(temporaryDirectory, 'install', 'resources'); + const packaged = { value: false }; + const paths = new AppPaths({ + moduleDirectory, + isPackaged: () => packaged.value, + getPath: name => name === 'exe' ? executable : userData, + getResourcesPath: () => resources, + }); + const safePaths = new SafePathService(paths); + + assert.equal(paths.appRoot(), projectRoot); + assert.equal(paths.resourceRoot(), projectRoot); + assert.equal(paths.userDataRoot(), userData); + assert.equal( + paths.systemBattlePlansDir(), + path.join(projectRoot, 'resource', 'system_battle_plans'), + ); + assert.equal( + paths.userBattlePlansDir(), + path.join(userData, 'user_battle_plans'), + ); + assert.equal( + paths.systemTeamPlansDir(), + path.join(projectRoot, 'resource', 'system_team_plans'), + ); + assert.equal( + paths.userTeamPlansDir(), + path.join(userData, 'user_team_plans'), + ); + assert.equal( + safePaths.resolveAppPath('usersettings.yaml'), + path.join(userData, 'usersettings.yaml'), + ); + assert.equal( + safePaths.resolveAppPath(path.join(userData, 'absolute.yaml')), + path.join(userData, 'absolute.yaml'), + ); + assert.equal( + safePaths.resolveAppPath(path.join('resource', 'maps', '1.json')), + path.join(projectRoot, 'resource', 'maps', '1.json'), + ); + assert.throws( + () => safePaths.resolveAppPath(''), + /文件路径不能为空/, + ); + assert.throws( + () => safePaths.resolveAppPath(path.join(temporaryDirectory, 'outside.yaml')), + /文件路径超出应用允许目录/, + ); + assert.throws( + () => safePaths.resolveAppPath('nested/../settings.json'), + /文件路径不允许包含 \.\./, + ); + assert.throws( + () => safePaths.resolveAppPath('C:relative.yaml'), + /不允许使用盘符相对路径/, + ); + assert.throws( + () => safePaths.resolveAppPath('Z:\\outside\\plan.yaml'), + /不允许切换路径根目录|文件路径超出应用允许目录/, + ); + assert.throws( + () => safePaths.resolveAppPath('\\\\server\\share\\plan.yaml'), + /不允许使用 UNC 路径/, + ); + + packaged.value = true; + assert.equal(paths.appRoot(), path.dirname(executable)); + assert.equal(paths.resourceRoot(), resources); +} + +/** 验证文件服务只在允许目录内保存、读取和追加。 */ +function testSecureFileService() { + const userData = path.join(temporaryDirectory, 'secure-user-data'); + const resources = path.join(temporaryDirectory, 'secure-resources'); + const bundledResources = path.join(resources, 'resource'); + const outside = path.join(temporaryDirectory, 'secure-outside'); + fs.mkdirSync(path.join(bundledResources, 'maps'), { recursive: true }); + fs.mkdirSync(outside, { recursive: true }); + fs.writeFileSync( + path.join(bundledResources, 'maps', '1.json'), + '{"map":1}', + 'utf8', + ); + const appPaths = new AppPaths({ + moduleDirectory: path.join(temporaryDirectory, 'dist', 'electron'), + isPackaged: () => true, + getPath: name => name === 'exe' + ? path.join(temporaryDirectory, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => resources, + }); + const service = new SecureFileService(new SafePathService(appPaths)); + + service.save(path.join('nested', 'settings.txt'), 'first'); + assert.equal( + service.read(path.join('nested', 'settings.txt')), + 'first', + ); + service.append(path.join('nested', 'settings.txt'), '-second'); + assert.equal( + service.read(path.join('nested', 'settings.txt')), + 'first-second', + ); + assert.equal(service.read('missing.txt'), ''); + assert.equal(service.read('resource/maps/1.json'), '{"map":1}'); + assert.throws( + () => service.save( + path.join(temporaryDirectory, 'outside.txt'), + 'rejected', + ), + /文件路径超出应用允许目录/, + ); + assert.throws( + () => service.append( + path.join(temporaryDirectory, 'outside.log'), + 'rejected', + ), + /文件路径超出应用允许目录/, + ); + assert.throws( + () => service.save('resource/maps/1.json', 'changed'), + /安装资源目录为只读/, + ); + assert.throws( + () => service.save( + path.join(bundledResources, 'maps', '1.json'), + 'changed', + ), + /文件路径超出应用允许目录/, + ); + assert.equal( + fs.readFileSync(path.join(bundledResources, 'maps', '1.json'), 'utf8'), + '{"map":1}', + ); + + const linkedDirectory = path.join(userData, 'outside-link'); + fs.symlinkSync( + outside, + linkedDirectory, + process.platform === 'win32' ? 'junction' : 'dir', + ); + assert.throws( + () => service.save( + path.join('outside-link', 'escaped.txt'), + 'rejected', + ), + /文件路径超出应用允许目录/, + ); + assert.throws( + () => service.read(path.join('outside-link', 'secret.txt')), + /文件路径超出应用允许目录/, + ); + assert.equal(fs.existsSync(path.join(outside, 'escaped.txt')), false); + + const danglingTarget = path.join( + temporaryDirectory, + 'secure-dangling-target', + ); + const danglingLink = path.join(userData, 'dangling-link'); + fs.mkdirSync(danglingTarget, { recursive: true }); + fs.symlinkSync( + danglingTarget, + danglingLink, + process.platform === 'win32' ? 'junction' : 'dir', + ); + fs.rmSync(danglingTarget, { recursive: true, force: true }); + assert.throws( + () => service.save( + path.join('dangling-link', 'escaped.txt'), + 'rejected', + ), + /文件路径包含无法解析的符号链接/, + ); +} + +/** 验证普通写入、覆盖写入和替换失败时保留旧文件。 */ +function testAtomicFileStore() { + const store = new AtomicFileStore(); + const target = path.join(temporaryDirectory, 'atomic.txt'); + const temporaryArtifacts = filePath => ( + fs.readdirSync(path.dirname(filePath)) + .filter(name => ( + name.startsWith(`${path.basename(filePath)}.`) + && /\.(?:tmp|bak)$/.test(name) + )) + ); + const assertRenameFailurePreservesTarget = (name, code) => { + const failureTarget = path.join(temporaryDirectory, name); + const oldContent = `old-${code}`; + fs.writeFileSync(failureTarget, oldContent, 'utf8'); + const originalRename = fs.renameSync; + let renameCall = 0; + fs.renameSync = (source, destination) => { + if (destination === failureTarget) { + renameCall += 1; + const error = new Error(`simulated ${code} rename failure`); + error.code = code; + throw error; + } + return originalRename(source, destination); + }; + try { + assert.throws( + () => store.write(failureTarget, `new-${code}`), + { code }, + ); + } finally { + fs.renameSync = originalRename; + } + const transient = ['EACCES', 'EBUSY', 'EPERM'].includes(code); + const expectedAttempts = process.platform === 'win32' && transient + ? 4 + : 1; + assert.equal(renameCall, expectedAttempts); + assert.equal(fs.readFileSync(failureTarget, 'utf8'), oldContent); + assert.deepEqual(temporaryArtifacts(failureTarget), []); + }; + + store.write(target, 'first'); + assert.equal(fs.readFileSync(target, 'utf8'), 'first'); + store.write(target, 'second'); + assert.equal(fs.readFileSync(target, 'utf8'), 'second'); + + if (process.platform === 'win32') { + const retryTarget = path.join(temporaryDirectory, 'atomic-retry.txt'); + const originalWrite = fs.writeFileSync; + let writeCall = 0; + fs.writeFileSync = (...args) => { + if ( + String(args[0]).startsWith(`${retryTarget}.`) + && writeCall === 0 + ) { + writeCall += 1; + const error = new Error('simulated temporary file lock'); + error.code = 'EPERM'; + throw error; + } + writeCall += 1; + return originalWrite(...args); + }; + try { + store.write(retryTarget, 'retry-success'); + } finally { + fs.writeFileSync = originalWrite; + } + assert.equal(writeCall, 2); + assert.equal(fs.readFileSync(retryTarget, 'utf8'), 'retry-success'); + + const renameRetryTarget = path.join( + temporaryDirectory, + 'atomic-rename-retry.txt', + ); + fs.writeFileSync(renameRetryTarget, 'old-before-retry', 'utf8'); + const originalRename = fs.renameSync; + let renameCall = 0; + fs.renameSync = (source, destination) => { + if (destination === renameRetryTarget) { + renameCall += 1; + } + if (destination === renameRetryTarget && renameCall <= 2) { + const error = new Error('simulated occupied target'); + error.code = 'EPERM'; + throw error; + } + return originalRename(source, destination); + }; + try { + store.write(renameRetryTarget, 'new-after-retry'); + } finally { + fs.renameSync = originalRename; + } + assert.equal(renameCall, 3); + assert.equal( + fs.readFileSync(renameRetryTarget, 'utf8'), + 'new-after-retry', + ); + assert.deepEqual(temporaryArtifacts(renameRetryTarget), []); + } + + assertRenameFailurePreservesTarget('atomic-occupied.txt', 'EBUSY'); + assertRenameFailurePreservesTarget('atomic-permission.txt', 'EACCES'); + assertRenameFailurePreservesTarget('atomic-cross-volume.txt', 'EXDEV'); + assertRenameFailurePreservesTarget('atomic-repeated-rename.txt', 'EPERM'); + assert.deepEqual( + temporaryArtifacts(target), + [], + ); +} + +module.exports = { + testAppPaths, + testSecureFileService, + testAtomicFileStore, +}; diff --git a/scripts/main-services/test-plan-export.js b/scripts/main-services/test-plan-export.js new file mode 100644 index 0000000..7a722cb --- /dev/null +++ b/scripts/main-services/test-plan-export.js @@ -0,0 +1,97 @@ +/** + * 用户计划批量导出服务测试。 + * + * 使用真实临时文件验证 ZIP 目录、原始 YAML 内容、去重和路径边界。 + */ +const assert = require('node:assert/strict'); +const path = require('node:path'); +const JSZip = require('jszip'); +const { + fs, + temporaryDirectory, +} = require('./test-context'); +const { + PlanExportService, +} = require('../../dist/electron/services/PlanExportService.js'); + +async function testPlanExportService() { + const root = path.join(temporaryDirectory, 'plan-export'); + const userBattleDirectory = path.join(root, 'user_battle_plans'); + const userTeamDirectory = path.join(root, 'user_team_plans'); + fs.mkdirSync(userBattleDirectory, { recursive: true }); + fs.mkdirSync(userTeamDirectory, { recursive: true }); + fs.writeFileSync( + path.join(userBattleDirectory, 'bettle-test.yaml'), + 'name: 出征测试\n', + 'utf8', + ); + fs.writeFileSync( + path.join(userTeamDirectory, 'team-test.yaml'), + 'name: 舰队测试\n', + 'utf8', + ); + + const service = new PlanExportService( + { + directory: source => { + assert.equal(source, 'user'); + return userBattleDirectory; + }, + }, + { + directory: source => { + assert.equal(source, 'user'); + return userTeamDirectory; + }, + }, + ); + + assert.equal( + service.archiveFileName(new Date(2026, 7, 4)), + '2026-08-04-plans.zip', + ); + + const archive = await service.createArchive([ + { kind: 'battle', file: 'bettle-test.yaml' }, + { kind: 'team', file: 'team-test.yaml' }, + { kind: 'team', file: 'TEAM-TEST.YAML' }, + ]); + assert.equal(archive.count, 2); + + const zip = await JSZip.loadAsync(archive.content); + assert.equal(zip.files['user_battle_plans/']?.dir, true); + assert.equal(zip.files['user_team_plans/']?.dir, true); + assert.equal( + await zip.file( + 'user_battle_plans/bettle-test.yaml', + ).async('string'), + 'name: 出征测试\n', + ); + assert.equal( + await zip.file('user_team_plans/team-test.yaml').async('string'), + 'name: 舰队测试\n', + ); + + const outputPath = path.join(root, '2026-08-04-plans.zip'); + service.writeArchive(outputPath, archive); + assert.deepEqual(fs.readFileSync(outputPath), archive.content); + + await assert.rejects( + service.createArchive([ + { kind: 'battle', file: '../system-plan.yaml' }, + ]), + /导出配置包含非法文件名/, + ); + await assert.rejects( + service.createArchive([ + { kind: 'battle', file: 'system-plan.yaml' }, + ]), + /用户配置不存在/, + ); + await assert.rejects( + service.createArchive([]), + /请至少选择一个用户配置/, + ); +} + +module.exports = { testPlanExportService }; diff --git a/scripts/main-services/test-ship-library.js b/scripts/main-services/test-ship-library.js new file mode 100644 index 0000000..263c8ab --- /dev/null +++ b/scripts/main-services/test-ship-library.js @@ -0,0 +1,552 @@ +/** + * 舰船资料库同步和更新器测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; + +/** 验证舰船资料库目录同步、清单读取和失败恢复。 */ +function testShipLibraryService() { + const projectRoot = path.join(temporaryDirectory, 'ship-library-project'); + const userData = path.join(temporaryDirectory, 'ship-library-user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const service = new ShipLibraryService(appPaths, { + processId: 77, + now: () => 12345, + }); + const bundledDirectory = path.join( + projectRoot, + 'resource', + 'ship-library', + ); + const bundledManifestPath = path.join( + bundledDirectory, + 'manifest.json', + ); + const userManifestPath = path.join( + service.directory(), + 'manifest.json', + ); + const writeBundledManifest = ( + schemaVersion, + generatedAt, + marker, + ) => { + fs.mkdirSync( + path.join(bundledDirectory, 'assets'), + { recursive: true }, + ); + fs.writeFileSync( + bundledManifestPath, + JSON.stringify({ + schema_version: schemaVersion, + generated_at: generatedAt, + labels: { nationality: '国籍' }, + type_groups: { surface: ['BB'] }, + counts: { + ships: 1, + assets: 2, + missing_assets: 0, + }, + ships: [{ + name: '测试舰', + portrait: 'assets/portrait.png', + background: '../outside.png', + frame: '', + type_icon: 'assets/type.png', + }], + }), + 'utf8', + ); + fs.writeFileSync( + path.join(bundledDirectory, 'bundled-marker.txt'), + marker, + 'utf8', + ); + fs.writeFileSync( + path.join(bundledDirectory, 'assets', 'portrait.png'), + 'portrait', + 'utf8', + ); + fs.writeFileSync( + path.join(bundledDirectory, 'assets', 'type.png'), + 'type', + 'utf8', + ); + }; + + service.initialize(); + assert.deepEqual(service.getStatus(), { + exists: false, + path: service.directory(), + shipCount: 0, + assetCount: 0, + missingAssets: 0, + }); + assert.throws( + () => service.getManifest(), + /舰船资料库尚未建立,请先在配置页更新舰船数据库/, + ); + + writeBundledManifest(1, '2026-01-01T00:00:00Z', 'version-1'); + service.initialize(); + assert.equal( + fs.readFileSync( + path.join(service.directory(), 'bundled-marker.txt'), + 'utf8', + ), + 'version-1', + ); + assert.deepEqual(service.getStatus(), { + exists: true, + path: service.directory(), + generatedAt: '2026-01-01T00:00:00Z', + shipCount: 1, + assetCount: 2, + missingAssets: 0, + }); + const manifest = service.getManifest(); + assert.equal(manifest.schemaVersion, 1); + assert.equal(manifest.generatedAt, '2026-01-01T00:00:00Z'); + assert.deepEqual(manifest.labels, { nationality: '国籍' }); + assert.deepEqual(manifest.typeGroups, { surface: ['bb'] }); + assert.match(manifest.ships[0].portraitUrl, /^file:/); + assert.match(manifest.ships[0].portraitUrl, /portrait\.png$/); + assert.equal(manifest.ships[0].backgroundUrl, ''); + assert.equal(manifest.ships[0].frameUrl, ''); + assert.match(manifest.ships[0].typeIconUrl, /type\.png$/); + assert.equal(service.assetUrl('../outside.png'), ''); + assert.equal(service.assetUrl(''), ''); + + fs.writeFileSync( + path.join(service.directory(), 'user-only.txt'), + 'remove-on-upgrade', + 'utf8', + ); + writeBundledManifest(2, '2026-02-01T00:00:00Z', 'version-2'); + service.initialize(); + assert.equal( + fs.existsSync(path.join(service.directory(), 'user-only.txt')), + false, + ); + assert.equal( + JSON.parse(fs.readFileSync(userManifestPath, 'utf8')).schema_version, + 2, + ); + + const legacyManifest = JSON.parse( + fs.readFileSync(userManifestPath, 'utf8'), + ); + legacyManifest.labels = { + ship_types: { + cav: '航空巡洋舰', + cg: '反舰导弹巡洋舰', + cgaa: '防空导弹巡洋舰', + cbg: '导弹大型巡洋舰', + ddg: '反舰导弹驱逐舰', + ddgaa: '防空导弹驱逐舰', + cf: '旗舰', + }, + }; + legacyManifest.type_groups = { + size_classes: { + medium: ['cav', 'cg', 'cgaa', 'cf'], + large: ['cbg'], + small: ['ddg', 'ddgaa'], + }, + }; + legacyManifest.ships = [ + 'cg', + 'cgaa', + 'cbg', + 'ddg', + 'ddgaa', + 'cf', + ].map((shipType, index) => ({ + id: index + 1, + name: `旧资料库舰船${index + 1}`, + ship_type: shipType, + portrait: 'assets/portrait.png', + background: '', + frame: '', + type_icon: 'assets/type.png', + })); + fs.writeFileSync( + userManifestPath, + JSON.stringify(legacyManifest), + 'utf8', + ); + const normalizedLegacyManifest = service.getManifest(); + assert.deepEqual( + normalizedLegacyManifest.ships.map(ship => ship.ship_type), + ['kp', 'cg', 'bg', 'asdg', 'aadg', 'cav'], + ); + assert.deepEqual(normalizedLegacyManifest.labels.ship_types, { + cav: '航空巡洋舰', + kp: '反舰导弹巡洋舰', + cg: '防空导弹巡洋舰', + bg: '导弹大型巡洋舰', + asdg: '反舰导弹驱逐舰', + aadg: '防空导弹驱逐舰', + }); + assert.deepEqual(normalizedLegacyManifest.typeGroups, { + size_classes: { + medium: ['cav', 'kp', 'cg'], + large: ['bg'], + small: ['asdg', 'aadg'], + }, + }); + assert.equal( + normalizedLegacyManifest.ships.every( + ship => /assets\/type\.png$/.test(ship.typeIconUrl), + ), + true, + ); + + legacyManifest.schema_version = 3; + legacyManifest.labels.ship_types = { + cav: '航空巡洋舰', + cf: '旗舰', + cg: '防空导弹巡洋舰', + }; + legacyManifest.type_groups = { + size_classes: { + medium: ['cav', 'cf', 'cg'], + }, + }; + legacyManifest.ships = [ + { name: 'canonical 防巡', ship_type: 'cg' }, + { name: '旧旗舰类型', ship_type: 'cf' }, + ].map((ship, index) => ({ + id: index + 1, + ...ship, + portrait: 'assets/portrait.png', + background: '', + frame: '', + type_icon: 'assets/type.png', + })); + fs.writeFileSync( + userManifestPath, + JSON.stringify(legacyManifest), + 'utf8', + ); + const normalizedSchemaThree = service.getManifest(); + assert.deepEqual( + normalizedSchemaThree.ships.map(ship => ship.ship_type), + ['cg', 'cav'], + ); + assert.deepEqual(normalizedSchemaThree.labels.ship_types, { + cav: '航空巡洋舰', + cg: '防空导弹巡洋舰', + }); + assert.deepEqual(normalizedSchemaThree.typeGroups, { + size_classes: { + medium: ['cav', 'cg'], + }, + }); + + fs.writeFileSync( + userManifestPath, + JSON.stringify({ + schema_version: 99, + generated_at: '2099-01-01T00:00:00Z', + ships: [], + }), + 'utf8', + ); + fs.writeFileSync( + path.join(service.directory(), 'newer-user.txt'), + 'preserve', + 'utf8', + ); + writeBundledManifest(4, '2026-03-01T00:00:00Z', 'version-4'); + service.initialize(); + assert.equal( + fs.readFileSync( + path.join(service.directory(), 'newer-user.txt'), + 'utf8', + ), + 'preserve', + ); + + fs.writeFileSync(userManifestPath, '{invalid', 'utf8'); + assert.match( + service.getStatus().error, + /^资料库清单读取失败:/, + ); + service.initialize(); + assert.equal( + JSON.parse(fs.readFileSync(userManifestPath, 'utf8')).schema_version, + 4, + ); + assert.equal( + fs.existsSync(path.join(service.directory(), 'newer-user.txt')), + false, + ); + + fs.writeFileSync( + path.join(service.directory(), 'old-version.txt'), + 'must-survive', + 'utf8', + ); + writeBundledManifest(5, '2026-04-01T00:00:00Z', 'version-5'); + const originalRename = fs.renameSync; + const originalConsoleError = console.error; + let renameCall = 0; + fs.renameSync = (source, destination) => { + renameCall += 1; + if (renameCall === 2) { + throw new Error('simulated ship library replacement failure'); + } + return originalRename(source, destination); + }; + console.error = () => {}; + try { + service.initialize(); + } finally { + fs.renameSync = originalRename; + console.error = originalConsoleError; + } + assert.equal( + fs.readFileSync( + path.join(service.directory(), 'old-version.txt'), + 'utf8', + ), + 'must-survive', + ); + assert.equal( + JSON.parse(fs.readFileSync(userManifestPath, 'utf8')).schema_version, + 4, + ); + assert.deepEqual( + fs.readdirSync(userData) + .filter(name => name.startsWith('ship-library.')), + [], + ); +} + +/** 创建可由更新服务监听的最小子进程替身。 */ +function createFakeChild() { + const child = new EventEmitter(); + child.stdout = new PassThrough(); + child.stderr = new PassThrough(); + return child; +} + +/** 验证资料库更新器保持原有进度、结果和互斥语义。 */ +async function testShipLibraryUpdater() { + const projectRoot = path.join(temporaryDirectory, 'ship-updater-project'); + const userData = path.join(temporaryDirectory, 'ship-updater-user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const library = new ShipLibraryService(appPaths, { + processId: 88, + }); + const missingPython = new ShipLibraryUpdater(library, { + findPython: async () => null, + appRoot: () => projectRoot, + sendProgress: () => {}, + }); + assert.deepEqual(await missingPython.update(), { + success: false, + error: '找不到可用的 Python 3.12 或 3.13', + }); + + const missingScript = new ShipLibraryUpdater(library, { + findPython: async () => 'python.exe', + appRoot: () => projectRoot, + sendProgress: () => {}, + }); + assert.deepEqual(await missingScript.update(), { + success: false, + error: `找不到舰船资料库更新程序: ${library.updaterPath()}`, + }); + + fs.mkdirSync(path.dirname(library.updaterPath()), { recursive: true }); + fs.writeFileSync(library.updaterPath(), '# updater', 'utf8'); + const progress = []; + const spawnCalls = []; + let spawnCount = 0; + const updater = new ShipLibraryUpdater(library, { + findPython: async () => 'python.exe', + appRoot: () => projectRoot, + sendProgress: message => progress.push(message), + spawnProcess: (command, args, options) => { + spawnCount += 1; + spawnCalls.push({ command, args, options }); + const child = createFakeChild(); + setImmediate(() => { + child.stdout.write([ + 'PROGRESS sources started', + 'PROGRESS records parsed=12', + 'PROGRESS assets 1/2 down', + ].join('\n')); + child.stdout.write([ + 'loaded=1 failed=0', + `RESULT_JSON=${JSON.stringify({ + success: true, + ship_count: 12, + })}`, + '', + ].join('\n')); + child.emit('close', 0); + }); + return child; + }, + }); + + const firstUpdate = updater.update(); + assert.deepEqual(await updater.update(), { + success: false, + error: '舰船资料库正在更新,请稍候', + }); + assert.deepEqual(await firstUpdate, { + success: true, + ship_count: 12, + }); + assert.deepEqual(progress, [ + '正在获取舰R百科数据…', + '已读取 12 艘舰船,正在检查本地资源…', + '正在检查资源 1/2,已下载 1,失败 0', + ]); + assert.equal(spawnCalls[0].command, 'python.exe'); + assert.equal(spawnCalls[0].args[0], '-c'); + assert.match( + spawnCalls[0].args[1], + /sys\.path\.insert\(0,.+runpy\.run_path/, + ); + assert.deepEqual(spawnCalls[0].args.slice(2), [ + library.updaterPath(), + '--output', + library.directory(), + '--workers', + '8', + '--force-assets', + ]); + assert.deepEqual(spawnCalls[0].options, { + cwd: projectRoot, + windowsHide: true, + }); + assert.deepEqual(await updater.update(), { + success: true, + ship_count: 12, + }); + assert.equal(spawnCount, 2); + + const invalidResult = new ShipLibraryUpdater(library, { + findPython: async () => 'python.exe', + appRoot: () => projectRoot, + sendProgress: () => {}, + spawnProcess: () => { + const child = createFakeChild(); + setImmediate(() => { + child.stdout.write('RESULT_JSON={invalid\n'); + child.emit('close', 0); + }); + return child; + }, + }); + assert.deepEqual(await invalidResult.update(), { + success: false, + error: '更新程序返回了无效结果', + }); + + const stderrFailure = new ShipLibraryUpdater(library, { + findPython: async () => 'python.exe', + appRoot: () => projectRoot, + sendProgress: () => {}, + spawnProcess: () => { + const child = createFakeChild(); + setImmediate(() => { + child.stderr.write('updater failed'); + child.emit('close', 7); + }); + return child; + }, + }); + assert.deepEqual(await stderrFailure.update(), { + success: false, + error: 'updater failed', + }); + + const exitFailure = new ShipLibraryUpdater(library, { + findPython: async () => 'python.exe', + appRoot: () => projectRoot, + sendProgress: () => {}, + spawnProcess: () => { + const child = createFakeChild(); + setImmediate(() => child.emit('close', 9)); + return child; + }, + }); + assert.deepEqual(await exitFailure.update(), { + success: false, + error: '更新程序异常退出(代码 9)', + }); + + const startFailure = new ShipLibraryUpdater(library, { + findPython: async () => 'python.exe', + appRoot: () => projectRoot, + sendProgress: () => {}, + spawnProcess: () => { + const child = createFakeChild(); + setImmediate(() => { + child.emit('error', new Error('cannot start')); + }); + return child; + }, + }); + assert.deepEqual(await startFailure.update(), { + success: false, + error: '更新程序启动失败: cannot start', + }); +} + +module.exports = { + testShipLibraryService, + testShipLibraryUpdater, +}; diff --git a/scripts/main-services/test-team-plan.js b/scripts/main-services/test-team-plan.js new file mode 100644 index 0000000..d19a822 --- /dev/null +++ b/scripts/main-services/test-team-plan.js @@ -0,0 +1,327 @@ +/** + * 编队 Codec、Repository 和 Service 测试。 + * + * 复用同一隔离临时目录,不读取或修改真实用户数据。 + */ +const context = require('./test-context'); +const { + assert, + EventEmitter, + fs, + os, + path, + PassThrough, + yaml, + AppPaths, + AtomicFileStore, + GuiSettingsStore, + SafePathService, + SecureFileService, + WindowService, + UserDataMigrationService, + LegacyPlanMigration, + TeamPlanCodec, + TeamPlanRepository, + TeamPlanService, + CombatPlanCodec, + CombatPlanRepository, + RuntimePlanService, + PlanManagementService, + ShipLibraryService, + ShipLibraryUpdater, + AdbService, + CudaEnvironmentService, + GuiConfigurationService, + PythonEnvironmentService, + temporaryDirectory, +} = context; + +/** 验证编队格式、仓储和保存流程保持原有业务语义。 */ +function testTeamPlanServices() { + const projectRoot = path.join(temporaryDirectory, 'team-project'); + const userData = path.join(temporaryDirectory, 'team-user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const codec = new TeamPlanCodec(); + const repository = new TeamPlanRepository( + appPaths, + new AtomicFileStore(), + codec, + ); + const service = new TeamPlanService(codec, repository); + repository.initializeSystemDirectory(); + repository.initializeUserDirectory(); + + const candidateOnly = codec.normalize({ + name: '备选位置', + rootExtension: { preserved: true }, + ships: [{ + slotExtension: 'keep', + candidates: [{ + name: 'U-47', + ship_type: ['SS'], + customCandidate: true, + }], + }], + }); + assert.equal(candidateOnly.ships[0].name, undefined); + assert.deepEqual(candidateOnly.ships[0].ship_type, undefined); + assert.deepEqual( + candidateOnly.ships[0].candidates[0].ship_type, + ['ss'], + ); + + const serialized = codec.serialize(candidateOnly); + const serializedObject = yaml.load(serialized); + assert.equal(serializedObject.rootExtension.preserved, true); + assert.equal(serializedObject.ships[0].name, undefined); + assert.equal(serializedObject.ships[0].slotExtension, 'keep'); + assert.equal( + serializedObject.ships[0].candidates[0].customCandidate, + true, + ); + const legacyCandidateOnly = codec.normalize({ + name: '旧版纯候选', + ships: [ + { + search_name: '契卡洛夫', + ship_type: ['CV'], + min_level: 90, + max_level: 110, + candidates: ['85工程', '岛风'], + }, + { + priority: ['胡德', '扶桑'], + }, + { + search_name: '大凤', + ship_type: ['CV'], + min_level: 80, + max_level: 100, + candidates: [ + '大凤·改', + { + name: '岛风', + search_name: '岛风', + ship_type: ['DD'], + min_level: 20, + max_level: 30, + }, + ], + }, + ], + }); + assert.equal(legacyCandidateOnly.ships[0].name, undefined); + assert.equal(legacyCandidateOnly.ships[0].search_name, undefined); + assert.deepEqual( + legacyCandidateOnly.ships[0].candidates.map(rule => ({ + name: rule.name, + search_name: rule.search_name, + ship_type: rule.ship_type, + min_level: rule.min_level, + max_level: rule.max_level, + })), + [ + { + name: '85工程', + search_name: '契卡洛夫', + ship_type: ['cv'], + min_level: 90, + max_level: 110, + }, + { + name: '岛风', + search_name: '契卡洛夫', + ship_type: ['cv'], + min_level: 90, + max_level: 110, + }, + ], + ); + assert.equal(legacyCandidateOnly.ships[1].name, undefined); + assert.deepEqual( + legacyCandidateOnly.ships[1].candidates.map(rule => rule.name), + ['胡德', '扶桑'], + ); + assert.equal(legacyCandidateOnly.ships[2].name, undefined); + assert.equal(legacyCandidateOnly.ships[2].search_name, undefined); + assert.deepEqual(legacyCandidateOnly.ships[2].candidates, [ + { + name: '大凤·改', + search_name: '大凤', + ship_type: ['cv'], + min_level: 80, + max_level: 100, + }, + { + name: '岛风', + search_name: '岛风', + ship_type: ['dd'], + min_level: 20, + max_level: 30, + }, + ]); + const serializedLegacy = yaml.load(codec.serialize(legacyCandidateOnly)); + assert.equal(serializedLegacy.ships[0].name, undefined); + assert.equal(serializedLegacy.ships[0].search_name, undefined); + assert.equal(serializedLegacy.ships[1].priority, undefined); + assert.deepEqual( + serializedLegacy.ships[1].candidates.map(rule => rule.name), + ['胡德', '扶桑'], + ); + assert.equal(serializedLegacy.ships[2].search_name, undefined); + assert.deepEqual( + serializedLegacy.ships[2].candidates, + legacyCandidateOnly.ships[2].candidates, + ); + const nativeShipTypes = [ + 'aadg', 'ap', 'asdg', 'av', 'bb', 'bbg', 'bbv', 'bc', 'bg', 'bm', + 'ca', 'cav', 'cg', 'cl', 'clt', 'cv', 'cvl', 'dd', 'kp', 'sc', + 'ss', 'ssg', 'ss_or_ssg', + ]; + for (const shipType of nativeShipTypes) { + const plan = codec.normalize({ + name: `舰种-${shipType}`, + ships: [{ name: '测试舰船', ship_type: [shipType] }], + }); + assert.deepEqual(plan.ships[0].ship_type, [shipType]); + } + for (const invalidShipType of ['cf', 'cgaa', 'cbg', 'ddg', 'ddgaa']) { + assert.throws( + () => codec.normalize({ + name: '非 canonical 舰种', + ships: [{ name: '测试舰船', ship_type: [invalidShipType] }], + }), + new RegExp(`不符合后端接口: ${invalidShipType}`), + ); + } + assert.equal(codec.fileName('测试/编队'), 'team-测试_编队.yaml'); + + const systemPath = path.join( + appPaths.systemTeamPlansDir(), + 'team-同名.yaml', + ); + const userPath = path.join( + appPaths.userTeamPlansDir(), + 'team-同名.yaml', + ); + repository.write(systemPath, codec.serialize(codec.normalize({ + name: '同名', + ships: [{ name: '系统舰' }], + }))); + repository.write(userPath, codec.serialize(codec.normalize({ + name: '同名', + ships: [{ name: '用户舰' }], + }))); + fs.writeFileSync( + path.join(appPaths.userTeamPlansDir(), 'invalid.yaml'), + 'name: 无效命名\nships:\n - name: 测试舰\n', + 'utf8', + ); + + const listed = repository.list(); + assert.equal(listed.plans.length, 2); + assert.equal(listed.errors.length, 1); + assert.equal( + repository.find('同名', 'user', listed.plans).ships[0].name, + '用户舰', + ); + assert.equal( + repository.find('同名', 'system', listed.plans).ships[0].name, + '系统舰', + ); + + const systemCopyFile = 'team-系统另存.yaml'; + const systemCopyPath = path.join( + appPaths.systemTeamPlansDir(), + systemCopyFile, + ); + const systemCopyPlan = codec.normalize({ + name: '系统另存', + ships: [{ name: '系统原版' }], + }); + repository.write(systemCopyPath, codec.serialize(systemCopyPlan)); + const unchangedSystemCopy = service.save( + systemCopyPlan, + false, + systemCopyFile, + 'system', + ); + assert.equal(unchangedSystemCopy.success, true); + assert.equal(unchangedSystemCopy.plan.source, 'user'); + assert.deepEqual( + repository.read(path.join( + appPaths.userTeamPlansDir(), + systemCopyFile, + )), + systemCopyPlan, + ); + const systemCopy = service.save({ + name: '系统另存', + ships: [{ name: '用户修改版' }], + }, true, systemCopyFile, 'system'); + assert.equal(systemCopy.success, true); + assert.equal(systemCopy.plan.source, 'user'); + assert.equal(repository.read(systemCopyPath).ships[0].name, '系统原版'); + assert.equal( + repository.read(path.join( + appPaths.userTeamPlansDir(), + systemCopyFile, + )).ships[0].name, + '用户修改版', + ); + + const firstSave = service.save({ + name: '可重命名', + ships: [{ name: '重庆' }], + }, false); + assert.equal(firstSave.success, true); + assert.equal(firstSave.file, 'team-可重命名.yaml'); + + const duplicate = service.save({ + name: '可重命名', + ships: [{ name: '重庆' }], + }, false); + assert.deepEqual(duplicate, { + success: false, + exists: true, + file: 'team-可重命名.yaml', + error: '存在同名配置', + }); + + const renamed = service.save({ + name: '重命名完成', + ships: [{ name: '重庆' }], + }, false, 'team-可重命名.yaml'); + assert.equal(renamed.success, true); + assert.equal( + fs.existsSync(path.join( + appPaths.userTeamPlansDir(), + 'team-可重命名.yaml', + )), + false, + ); + const renamedPath = path.join( + appPaths.userTeamPlansDir(), + 'team-重命名完成.yaml', + ); + assert.equal(fs.existsSync(renamedPath), true); + assert.equal(service.loadSelected(renamedPath).success, true); + assert.deepEqual( + service.loadSelected(path.join(temporaryDirectory, 'outside.yaml')), + { + success: false, + error: '当前yaml格式不符合规则', + }, + ); +} + +module.exports = { + testTeamPlanServices, +}; diff --git a/scripts/main-services/test-updater-and-shutdown.js b/scripts/main-services/test-updater-and-shutdown.js new file mode 100644 index 0000000..ba17557 --- /dev/null +++ b/scripts/main-services/test-updater-and-shutdown.js @@ -0,0 +1,381 @@ +/** + * GUI 更新策略和后端安全关闭测试。 + * + * 覆盖稳定、预发布、开发三个版本频道,确保无更新、检查失败和发现更新 + * 不会互相混淆。后端关闭测试验证系统停止接口一定先于进程终止调用。 + * + * Windows 环境还会创建父子进程树:子进程独占锁定临时文件,父进程作为 + * 后端进程交给关闭服务。关闭完成后必须能立即删除锁文件,证明子进程和 + * 操作系统文件锁都已释放。 + */ +const { execFileSync, spawn } = require('node:child_process'); +const http = require('node:http'); +const { + assert, + fs, + path, + temporaryDirectory, + yaml, +} = require('./test-context'); +const { + classifyGuiUpdateCheck, + resolveGuiReleasePolicy, + validateGuiUpdateCandidate, +} = require('../../dist/electron/services/GuiUpdatePolicy.js'); +const { + requestBackendSystemStop, + shutdownBackendProcess, +} = require( + '../../dist/electron/services/BackendShutdownService.js' +); + +function delay(milliseconds) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + +async function waitForFile(filePath, timeoutMs = 5000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (fs.existsSync(filePath)) return; + await delay(50); + } + throw new Error(`等待文件超时: ${filePath}`); +} + +async function deleteLockedFile(filePath, timeoutMs = 5000) { + const deadline = Date.now() + timeoutMs; + let lastError; + while (Date.now() < deadline) { + try { + fs.rmSync(filePath, { force: true }); + return; + } catch (error) { + lastError = error; + await delay(50); + } + } + throw lastError ?? new Error(`无法删除文件: ${filePath}`); +} + +function testGuiUpdatePolicy() { + assert.deepEqual(resolveGuiReleasePolicy('2.0.3'), { + channel: 'latest', + stage: 'stable', + allowPrerelease: false, + }); + assert.deepEqual(resolveGuiReleasePolicy('2.0.3-beta.2'), { + channel: 'beta', + stage: 'prerelease', + allowPrerelease: true, + }); + const development = resolveGuiReleasePolicy('2.0.3-dev'); + assert.deepEqual(development, { + channel: 'dev', + stage: 'development', + allowPrerelease: true, + }); + assert.deepEqual( + resolveGuiReleasePolicy('2.0.3-dev.0'), + development, + ); + assert.throws( + () => resolveGuiReleasePolicy('2.0.3-rc.1'), + /不符合规范/, + ); + + assert.deepEqual( + classifyGuiUpdateCheck(development, { + isUpdateAvailable: false, + updateInfo: { version: '2.0.3-dev.0' }, + }), + { status: 'up-to-date' }, + ); + assert.deepEqual( + classifyGuiUpdateCheck(development, null), + { + status: 'error', + message: '当前运行环境未启用 GUI 自动更新', + }, + ); + assert.deepEqual( + classifyGuiUpdateCheck(development, { + isUpdateAvailable: true, + updateInfo: { version: '2.0.3-dev.1' }, + }), + { status: 'available', version: '2.0.3-dev.1' }, + ); + assert.match( + validateGuiUpdateCandidate(development, '2.0.3-beta.1'), + /只允许 dev 频道/, + ); + assert.equal( + validateGuiUpdateCandidate(development, '2.0.3-dev.1'), + null, + ); + + const root = path.join(__dirname, '..', '..'); + const packageJson = JSON.parse( + fs.readFileSync(path.join(root, 'package.json'), 'utf8'), + ); + const packagePolicy = resolveGuiReleasePolicy(packageJson.version); + assert.equal(packageJson.build.publish.channel, packagePolicy.channel); + + const workflow = fs.readFileSync( + path.join(root, '.github', 'workflows', 'release.yml'), + 'utf8', + ); + assert.ok(yaml.load(workflow).jobs.build); + assert.match(workflow, /X\.Y\.Z-beta\.N/); + assert.match(workflow, /X\.Y\.Z-dev\.N/); + assert.match( + workflow, + /release\/\$\{\{ steps\.version\.outputs\.CHANNEL \}\}\.yml/, + ); + assert.doesNotMatch(workflow, /^\s+release\/latest\.yml\s*$/m); +} + +async function testSystemStopRequest() { + let receivedPath = ''; + let receivedMethod = ''; + let shouldFail = false; + const server = http.createServer((request, response) => { + receivedPath = request.url ?? ''; + receivedMethod = request.method ?? ''; + response.writeHead(200, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify( + shouldFail + ? { success: false, error: '运行中任务停止超时' } + : { success: true }, + )); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + try { + const address = server.address(); + assert.ok(address && typeof address === 'object'); + await requestBackendSystemStop(address.port, 1000); + assert.equal(receivedPath, '/api/system/stop'); + assert.equal(receivedMethod, 'POST'); + shouldFail = true; + await assert.rejects( + requestBackendSystemStop(address.port, 1000), + /运行中任务停止超时/, + ); + } finally { + await new Promise(resolve => server.close(resolve)); + } +} + +async function testRunningTaskStopsBeforeUpdateInstall() { + const events = []; + const fakeProcess = { + exitCode: null, + signalCode: null, + pid: 12345, + }; + await shutdownBackendProcess( + fakeProcess, + { + backendPort: 14433, + terminateTimeoutMs: 10, + forceTerminateTimeoutMs: 100, + }, + { + platform: 'win32', + requestSystemStop: async () => { + events.push('system-stop'); + }, + terminateProcessTree: async (_process, force) => { + events.push(force ? 'force-tree' : 'terminate-tree'); + fakeProcess.exitCode = 0; + }, + waitForProcessClose: async () => { + events.push('wait-close'); + return true; + }, + }, + ); + events.push('quit-install'); + assert.deepEqual(events, [ + 'system-stop', + 'terminate-tree', + 'wait-close', + 'quit-install', + ]); +} + +async function testForceShutdownWaitsForClose() { + const events = []; + const fakeProcess = { + exitCode: null, + signalCode: null, + pid: 12346, + }; + let waitCount = 0; + await shutdownBackendProcess( + fakeProcess, + { + backendPort: 14433, + terminateTimeoutMs: 10, + forceTerminateTimeoutMs: 10, + }, + { + platform: 'win32', + requestSystemStop: async () => { + events.push('system-stop'); + throw new Error('模拟运行中任务停止超时'); + }, + terminateProcessTree: async (_process, force) => { + events.push(force ? 'force-tree' : 'terminate-tree'); + }, + waitForProcessClose: async () => { + waitCount += 1; + events.push(`wait-${waitCount}`); + return waitCount === 2; + }, + warn: message => events.push(`warning:${message}`), + }, + ); + assert.deepEqual( + events.filter(event => !event.startsWith('warning:')), + [ + 'system-stop', + 'terminate-tree', + 'wait-1', + 'force-tree', + 'wait-2', + ], + ); +} + +async function testUnclosedProcessBlocksUpdateInstall() { + const fakeProcess = { + exitCode: null, + signalCode: null, + pid: 12347, + }; + let installed = false; + await assert.rejects( + shutdownBackendProcess( + fakeProcess, + { + backendPort: 14433, + terminateTimeoutMs: 10, + forceTerminateTimeoutMs: 10, + }, + { + platform: 'win32', + requestSystemStop: async () => {}, + terminateProcessTree: async () => {}, + waitForProcessClose: async () => false, + warn: () => {}, + }, + ).then(() => { + installed = true; + }), + /无法确认后端进程树已经退出/, + ); + assert.equal(installed, false); +} + +async function testWindowsProcessTreeAndFileLock() { + if (process.platform !== 'win32') return; + + const testDirectory = path.join( + temporaryDirectory, + 'backend-shutdown-windows', + ); + fs.mkdirSync(testDirectory, { recursive: true }); + const lockFile = path.join(testDirectory, 'backend.lock'); + const childReady = path.join(testDirectory, 'child-ready.txt'); + const parentScript = path.join(testDirectory, 'parent.js'); + const lockScript = path.join(testDirectory, 'lock.ps1'); + + fs.writeFileSync(lockScript, [ + 'param([string]$LockPath, [string]$ReadyPath)', + '$stream = [System.IO.File]::Open(', + ' $LockPath,', + ' [System.IO.FileMode]::OpenOrCreate,', + ' [System.IO.FileAccess]::ReadWrite,', + ' [System.IO.FileShare]::None', + ')', + 'try {', + ' [System.IO.File]::WriteAllText($ReadyPath, $PID.ToString())', + ' while ($true) { Start-Sleep -Milliseconds 100 }', + '} finally {', + ' $stream.Dispose()', + '}', + ].join('\r\n')); + fs.writeFileSync(parentScript, [ + "const { spawn } = require('node:child_process');", + 'const child = spawn(', + " 'powershell.exe',", + ' [', + " '-NoProfile',", + " '-NonInteractive',", + " '-ExecutionPolicy',", + " 'Bypass',", + " '-File',", + ' process.argv[2],', + ' process.argv[3],', + ' process.argv[4],', + ' ],', + " { windowsHide: true, stdio: 'ignore' },", + ');', + "child.on('error', error => { throw error; });", + 'setInterval(() => {}, 1000);', + ].join('\n')); + + const parentProcess = spawn( + process.execPath, + [parentScript, lockScript, lockFile, childReady], + { + windowsHide: true, + stdio: 'ignore', + }, + ); + try { + await waitForFile(childReady); + assert.throws(() => fs.rmSync(lockFile), /EPERM|EBUSY|UNKNOWN/); + + await shutdownBackendProcess( + parentProcess, + { + backendPort: 14433, + terminateTimeoutMs: 200, + forceTerminateTimeoutMs: 5000, + }, + { + requestSystemStop: async () => {}, + warn: () => {}, + }, + ); + assert.notEqual(parentProcess.exitCode, null); + await deleteLockedFile(lockFile); + assert.equal(fs.existsSync(lockFile), false); + } finally { + if (parentProcess.exitCode === null) { + try { + execFileSync( + 'taskkill.exe', + ['/PID', String(parentProcess.pid), '/T', '/F'], + { windowsHide: true, stdio: 'ignore' }, + ); + } catch { + // 测试进程已经退出。 + } + } + } +} + +async function testUpdaterAndBackendShutdown() { + testGuiUpdatePolicy(); + await testSystemStopRequest(); + await testRunningTaskStopsBeforeUpdateInstall(); + await testForceShutdownWaitsForClose(); + await testUnclosedProcessBlocksUpdateInstall(); + await testWindowsProcessTreeAndFileLock(); +} + +module.exports = { + testUpdaterAndBackendShutdown, +}; diff --git a/scripts/prepare-python.js b/scripts/prepare-python.js index e054948..6215ae6 100644 --- a/scripts/prepare-python.js +++ b/scripts/prepare-python.js @@ -21,12 +21,34 @@ function run(cmd, opts = {}) { execSync(cmd, { stdio: 'inherit', windowsHide: true, timeout: 600000, ...opts }); } +function ensureVCRedist() { + console.log('[5/5] 检查 VC++ Redistributable…'); + const redistDir = path.join(ROOT, 'redist'); + const redistExe = path.join(redistDir, 'vc_redist.x64.exe'); + if (fs.existsSync(redistExe)) { + console.log('✓ vc_redist.x64.exe 已就绪'); + return; + } + + console.log(' 下载 vc_redist.x64.exe…'); + if (!fs.existsSync(redistDir)) fs.mkdirSync(redistDir, { recursive: true }); + try { + run(`curl.exe -L -o "${redistExe}" "https://aka.ms/vs/17/release/vc_redist.x64.exe"`); + console.log('✓ vc_redist.x64.exe 下载完成'); + } catch { + console.error('✗ vc_redist.x64.exe 下载失败,请手动下载到 redist/ 目录'); + console.error(' URL: https://aka.ms/vs/17/release/vc_redist.x64.exe'); + process.exit(1); + } +} + async function main() { - // 如果已有完整的 python.exe + pip,跳过 + // 如果已有完整的 python.exe + pip,只跳过 Python 下载。 if (fs.existsSync(PYTHON_EXE)) { try { execSync(`"${PYTHON_EXE}" -m pip --version`, { windowsHide: true }); console.log(`✓ Python ${PYTHON_VERSION} 已就绪,跳过下载`); + ensureVCRedist(); return; } catch { /* pip missing, re-setup */ } } @@ -89,24 +111,7 @@ async function main() { process.exit(1); } - // VC++ Redistributable - console.log('[5/5] 检查 VC++ Redistributable…'); - const REDIST_DIR = path.join(ROOT, 'redist'); - const redistExe = path.join(REDIST_DIR, 'vc_redist.x64.exe'); - if (fs.existsSync(redistExe)) { - console.log('✓ vc_redist.x64.exe 已就绪'); - } else { - console.log(' 下载 vc_redist.x64.exe…'); - if (!fs.existsSync(REDIST_DIR)) fs.mkdirSync(REDIST_DIR, { recursive: true }); - try { - run(`curl.exe -L -o "${redistExe}" "https://aka.ms/vs/17/release/vc_redist.x64.exe"`); - console.log('✓ vc_redist.x64.exe 下载完成'); - } catch { - console.error('✗ vc_redist.x64.exe 下载失败,请手动下载到 redist/ 目录'); - console.error(' URL: https://aka.ms/vs/17/release/vc_redist.x64.exe'); - process.exit(1); - } - } + ensureVCRedist(); } main().catch(err => { diff --git a/scripts/sync-fleet-ship-types.js b/scripts/sync-fleet-ship-types.js new file mode 100644 index 0000000..6af8572 --- /dev/null +++ b/scripts/sync-fleet-ship-types.js @@ -0,0 +1,87 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const projectRoot = path.resolve(__dirname, '..'); +const exporter = path.join( + projectRoot, + 'tools', + 'ship_library', + 'native_fleet_types.py', +); +const outputPath = path.join( + projectRoot, + 'src', + 'shared', + 'nativeFleetShipTypes.generated.ts', +); + +function exportContract() { + const candidates = [ + process.env.AUTOWSGR_PYTHON, + path.join(projectRoot, 'python', 'python.exe'), + process.platform === 'win32' ? 'python' : 'python3', + ].filter(Boolean); + + const failures = []; + for (const python of candidates) { + const result = spawnSync(python, [exporter], { + cwd: projectRoot, + encoding: 'utf8', + windowsHide: true, + }); + if (result.status === 0) { + return JSON.parse(result.stdout.trim()); + } + failures.push(`${python}: ${(result.stderr || result.error || '').toString().trim()}`); + } + throw new Error( + `无法从 autowsgr_native 导出舰种契约:\n${failures.join('\n')}`, + ); +} + +function renderContract(contract) { + if ( + contract.schema_version !== 1 + || contract.source !== 'autowsgr_native.vessel_type.VesselType' + || !Array.isArray(contract.ship_types) + ) { + throw new Error('native 舰种契约格式无效'); + } + const entries = contract.ship_types.map(({ code, label }) => { + if (typeof code !== 'string' || typeof label !== 'string') { + throw new Error('native 舰种契约包含无效条目'); + } + return ` ${JSON.stringify(code)}: ${JSON.stringify(label)},`; + }); + return [ + '/** 保存由 autowsgr_native 生成的舰种代码,供前端漂移检查。 */', + '/* 此文件由 scripts/sync-fleet-ship-types.js 生成,禁止手工修改。 */', + 'export const NATIVE_FLEET_SHIP_TYPE_LABELS: Readonly<', + ' Record', + '> = Object.freeze({', + ...entries, + '});', + '', + 'export const NATIVE_FLEET_SHIP_TYPE_CODES: readonly string[] =', + ' Object.freeze(Object.keys(NATIVE_FLEET_SHIP_TYPE_LABELS));', + '', + ].join('\n'); +} + +const expected = renderContract(exportContract()); +const checkOnly = process.argv.includes('--check'); +if (checkOnly) { + const actual = fs.existsSync(outputPath) + ? fs.readFileSync(outputPath, 'utf8') + : ''; + if (actual !== expected) { + throw new Error( + 'GUI 舰种生成文件与 autowsgr_native 不一致,请运行 npm run sync:fleet-types', + ); + } + console.log('native fleet ship type contract is up to date'); +} else { + fs.writeFileSync(outputPath, expected, 'utf8'); + console.log(`updated ${path.relative(projectRoot, outputPath)}`); +} diff --git a/scripts/sync-map-resources.js b/scripts/sync-map-resources.js new file mode 100644 index 0000000..5e56e99 --- /dev/null +++ b/scripts/sync-map-resources.js @@ -0,0 +1,313 @@ +/** + * 按 AutoWSGR 主库目录同步地图,并生成 GUI 活动地图清单。 + * normal/event 转为 GUI JSON,decisive_battle 数据保持主库 YAML 原文。 + * 目标目录采用严格镜像,删除主库契约外的旧命名和重复资源。 + * + * 用法: + * node scripts/sync-map-resources.js + * node scripts/sync-map-resources.js --backend C:\path\to\AutoWSGR + * node scripts/sync-map-resources.js --check + */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const guiRoot = path.resolve(__dirname, '..'); +const args = process.argv.slice(2); +const backendArgIndex = args.indexOf('--backend'); +const backendRoot = backendArgIndex >= 0 + ? path.resolve(args[backendArgIndex + 1] ?? '') + : path.resolve(guiRoot, '..', 'AutoWSGR'); +const checkOnly = args.includes('--check'); +const sourceMapRoot = path.join( + backendRoot, + 'autowsgr', + 'data', + 'map', +); +const sourceNormalRoot = path.join(sourceMapRoot, 'normal'); +const sourceEventRoot = path.join(sourceMapRoot, 'event'); +const sourceDecisiveRoot = path.join(sourceMapRoot, 'decisive_battle'); +const targetMapRoot = path.join(guiRoot, 'resource', 'maps'); +const targetNormalRoot = path.join(targetMapRoot, 'normal'); +const targetEventRoot = path.join(targetMapRoot, 'event'); +const targetDecisiveRoot = path.join(targetMapRoot, 'decisive_battle'); + +assert.equal( + fs.existsSync(sourceNormalRoot) + && fs.existsSync(sourceEventRoot) + && fs.existsSync(sourceDecisiveRoot), + true, + `AutoWSGR 地图目录不完整: ${sourceMapRoot}`, +); + +function writeOrCheck(filePath, content) { + if (checkOnly) { + assert.equal( + fs.existsSync(filePath), + true, + `缺少同步文件: ${path.relative(guiRoot, filePath)}`, + ); + assert.equal( + fs.readFileSync(filePath, 'utf8'), + content, + `地图资源不是 AutoWSGR 主库最新数据: ${path.relative(guiRoot, filePath)}`, + ); + return; + } + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, 'utf8'); +} + +function parseSourceFileName(fileName) { + const oldMatch = fileName.match(/^([EH])-(\d+)\.ya?ml$/i); + if (oldMatch) { + return { + chapter: oldMatch[1].toUpperCase(), + map: oldMatch[2], + targetName: fileName.replace(/\.ya?ml$/i, '.json'), + }; + } + + const newMatch = fileName.match(/Ex-(\d+)-(α|β)\.ya?ml$/i); + if (!newMatch) return null; + const prefix = fileName.slice(0, newMatch.index); + return { + chapter: prefix.endsWith('H-') ? 'H' : 'E', + map: `${newMatch[1]}${newMatch[2] === 'β' ? 'b' : 'a'}`, + targetName: fileName.replace(/\.ya?ml$/i, '.json'), + }; +} + +function readJson(filePath) { + if (!fs.existsSync(filePath)) return {}; + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function convertMap(source, existing) { + const output = {}; + for (const [nodeId, rawPoint] of Object.entries(source)) { + const positionOnly = Array.isArray(rawPoint); + const point = ( + rawPoint + && typeof rawPoint === 'object' + && !positionOnly + ) + ? rawPoint + : {}; + const position = ( + positionOnly + ? rawPoint + : Array.isArray(point.position) + ? point.position + : [0, 0] + ).map(Number); + assert.equal(position.length, 2, `节点 ${nodeId} 缺少坐标`); + assert.ok(position.every(Number.isFinite), `节点 ${nodeId} 坐标无效`); + + const previous = existing[nodeId] ?? {}; + const next = Array.isArray(point.next) ? point.next.map(String) : []; + output[nodeId] = { + type: typeof previous.type === 'string' + ? previous.type + : nodeId === '0' || nodeId === 'α' || nodeId === 'β' + ? 'Start' + : !positionOnly && next.length === 0 + ? 'Boss' + : 'Normal', + detour: previous.detour === true, + night: previous.night === true, + position, + next, + }; + } + return output; +} + +function mapSort(left, right) { + const leftMatch = left.match(/^(\d+)([ab])?$/); + const rightMatch = right.match(/^(\d+)([ab])?$/); + if (!leftMatch || !rightMatch) return left.localeCompare(right); + const stageDiff = Number(leftMatch[1]) - Number(rightMatch[1]); + return stageDiff || (leftMatch[2] ?? '').localeCompare(rightMatch[2] ?? ''); +} + +function loadSourceMap(filePath) { + const source = yaml.load(fs.readFileSync(filePath, 'utf8')); + assert.ok(source && typeof source === 'object', `地图 YAML 无效: ${filePath}`); + return source; +} + +function syncNormalMaps() { + let count = 0; + const files = []; + for (const file of fs.readdirSync(sourceNormalRoot).sort()) { + if (!/^\d+-\d+\.ya?ml$/i.test(file)) continue; + const targetName = file.replace(/\.ya?ml$/i, '.json'); + const targetPath = path.join(targetNormalRoot, targetName); + const converted = convertMap( + loadSourceMap(path.join(sourceNormalRoot, file)), + readJson(targetPath), + ); + writeOrCheck(targetPath, `${JSON.stringify(converted, null, 2)}\n`); + files.push(targetPath); + count += 1; + } + return { count, files }; +} + +function syncEventMaps() { + const events = []; + const targetFiles = []; + let count = 0; + const eventNames = fs.readdirSync(sourceEventRoot, { withFileTypes: true }) + .filter(entry => entry.isDirectory() && /^\d{8}$/.test(entry.name)) + .map(entry => entry.name) + .sort(); + assert.ok(eventNames.length > 0, 'AutoWSGR 主库没有活动地图数据'); + + for (const eventName of eventNames) { + const sourceDirectory = path.join(sourceEventRoot, eventName); + const targetDirectory = path.join(targetEventRoot, eventName); + const chapters = { E: [], H: [] }; + const files = { E: {}, H: {} }; + + for (const file of fs.readdirSync(sourceDirectory).sort()) { + const parsedName = parseSourceFileName(file); + if (!parsedName) continue; + const { chapter, map, targetName } = parsedName; + assert.equal( + files[chapter][map], + undefined, + `活动地图命名匹配歧义: ${eventName}/${chapter}-${map}`, + ); + const targetPath = path.join(targetDirectory, targetName); + const converted = convertMap( + loadSourceMap(path.join(sourceDirectory, file)), + readJson(targetPath), + ); + writeOrCheck(targetPath, `${JSON.stringify(converted, null, 2)}\n`); + targetFiles.push(targetPath); + chapters[chapter].push(map); + files[chapter][map] = targetName; + count += 1; + } + + chapters.E.sort(mapSort); + chapters.H.sort(mapSort); + if (chapters.E.length > 0 || chapters.H.length > 0) { + events.push({ + event: eventName, + chapters, + files, + }); + } + } + + events.sort((left, right) => right.event.localeCompare(left.event)); + return { count, events, files: targetFiles }; +} + +function syncDecisiveBattleData() { + const sourcePath = path.join(sourceDecisiveRoot, 'enemy_spec.yaml'); + const targetPath = path.join(targetDecisiveRoot, 'enemy_spec.yaml'); + assert.equal( + fs.existsSync(sourcePath), + true, + `缺少决战地图数据: ${sourcePath}`, + ); + writeOrCheck(targetPath, fs.readFileSync(sourcePath, 'utf8')); + return targetPath; +} + +function recursiveFiles(root) { + if (!fs.existsSync(root)) return []; + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const value = path.join(root, entry.name); + return entry.isDirectory() ? recursiveFiles(value) : [value]; + }); +} + +function recursiveDirectories(root) { + if (!fs.existsSync(root)) return []; + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + if (!entry.isDirectory()) return []; + const value = path.join(root, entry.name); + return [value, ...recursiveDirectories(value)]; + }); +} + +function pathKey(value) { + const resolved = path.resolve(value); + return process.platform === 'win32' ? resolved.toLowerCase() : resolved; +} + +function expectedDirectories(files) { + const directories = new Set([pathKey(targetMapRoot)]); + for (const file of files) { + let directory = path.dirname(file); + while (pathKey(directory).startsWith(pathKey(targetMapRoot))) { + directories.add(pathKey(directory)); + if (pathKey(directory) === pathKey(targetMapRoot)) break; + directory = path.dirname(directory); + } + } + return directories; +} + +function cleanTarget(expectedFiles) { + const expectedFileKeys = new Set(expectedFiles.map(pathKey)); + const extraFiles = recursiveFiles(targetMapRoot) + .filter(file => !expectedFileKeys.has(pathKey(file))); + const expectedDirectoryKeys = expectedDirectories(expectedFiles); + const extraDirectories = recursiveDirectories(targetMapRoot) + .filter(directory => !expectedDirectoryKeys.has(pathKey(directory))) + .sort((left, right) => right.length - left.length); + + if (checkOnly) { + assert.deepEqual( + extraFiles.map(file => path.relative(guiRoot, file)).sort(), + [], + 'GUI 地图目录存在主库契约外的文件', + ); + assert.deepEqual( + extraDirectories.map(directory => path.relative(guiRoot, directory)).sort(), + [], + 'GUI 地图目录存在主库契约外的目录', + ); + return { files: 0, directories: 0 }; + } + + for (const file of extraFiles) fs.rmSync(file, { force: true }); + for (const directory of extraDirectories) { + if (fs.existsSync(directory)) fs.rmdirSync(directory); + } + return { + files: extraFiles.length, + directories: extraDirectories.length, + }; +} + +const normalResult = syncNormalMaps(); +const eventResult = syncEventMaps(); +const catalog = `${JSON.stringify({ + schema_version: 2, + events: eventResult.events, +}, null, 2)}\n`; +const catalogPath = path.join(targetEventRoot, 'index.json'); +writeOrCheck(catalogPath, catalog); +const decisivePath = syncDecisiveBattleData(); +const cleaned = cleanTarget([ + ...normalResult.files, + ...eventResult.files, + catalogPath, + decisivePath, +]); + +console.log( + `${checkOnly ? 'checked' : 'synced'} ` + + `${normalResult.count} normal maps, ${eventResult.count} event maps, ` + + 'and decisive_battle/enemy_spec.yaml; ' + + `removed ${cleaned.files} files and ${cleaned.directories} directories`, +); diff --git a/scripts/test-api-contract.js b/scripts/test-api-contract.js new file mode 100644 index 0000000..a6c14f5 --- /dev/null +++ b/scripts/test-api-contract.js @@ -0,0 +1,358 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const { PlanModel } = require('../dist/src/model/PlanModel.js'); +const { TaskQueue } = require('../dist/src/model/scheduler/TaskQueue.js'); +const { ALL_SHIPS } = require('../dist/src/model/fleet/ShipCatalog.js'); +const { + FLEET_SHIP_TYPE_CODES, + NATIVE_FLEET_SHIP_TYPE_CODES, + NATIVE_FLEET_SHIP_TYPE_LABELS, + normalizeFleetShipTypeCode, + SHIP_TYPE_FILTER_ORDER, + TYPE_LABELS, +} = require('../dist/src/shared/fleetShipTypes.js'); + +function buildFleetContractCase(name, sourceYaml) { + const contractPlan = PlanModel.fromYaml(sourceYaml, `${name}.yaml`); + const contractRequest = { + type: 'normal_fight', + times: 1, + plan: { + chapter: contractPlan.data.chapter, + map: contractPlan.data.map, + fleet_id: 1, + }, + }; + new TaskQueue().switchTaskPreset({ + request: contractRequest, + fleetId: 1, + fleetPresets: contractPlan.data.fleet_presets, + currentPresetIndex: -1, + }, 0); + return { + name, + source_yaml: sourceYaml, + gui_yaml: contractPlan.toYaml(), + request: contractRequest, + }; +} + +function runBackendFleetContract(cases) { + const projectRoot = path.resolve(__dirname, '..'); + const backendRoot = path.resolve( + process.env.AUTOWSGR_REPO + || path.join(projectRoot, '..', 'AutoWSGR'), + ); + const venvPython = process.platform === 'win32' + ? path.join(backendRoot, '.venv', 'Scripts', 'python.exe') + : path.join(backendRoot, '.venv', 'bin', 'python'); + const python = process.env.AUTOWSGR_PYTHON + || (fs.existsSync(venvPython) + ? venvPython + : process.platform === 'win32' ? 'python' : 'python3'); + const helper = path.join(__dirname, 'backend-fleet-contract.py'); + + assert.equal( + fs.existsSync(path.join( + backendRoot, + 'autowsgr', + 'server', + 'schemas.py', + )), + true, + `找不到本地 AutoWSGR 后端仓库: ${backendRoot}`, + ); + const result = spawnSync(python, [helper, backendRoot], { + cwd: backendRoot, + input: JSON.stringify(cases), + encoding: 'utf8', + env: { ...process.env, PYTHONUTF8: '1' }, + maxBuffer: 10 * 1024 * 1024, + windowsHide: true, + }); + assert.equal( + result.status, + 0, + [ + '本地 AutoWSGR 跨仓契约测试失败', + result.error?.message, + result.stdout, + result.stderr, + ].filter(Boolean).join('\n'), + ); + return JSON.parse(result.stdout); +} + +const plan = PlanModel.fromYaml([ + 'chapter: 1', + 'map: 1', + 'fleet_presets:', + ' - name: 候选契约', + ' ships:', + ' - candidates:', + ' - name: 胡德', + ' ship_type: [bc]', + ' min_level: 20', + ' - name: 扶桑', + ' ship_type: [bb]', + ' max_level: 90', + ' - name: 重庆', + ' ship_type: [kp, cg, bg, bbg, asdg, aadg, ap]', + ' candidates:', + ' - name: 长春', + ' - ship_type: [ss]', + ' min_level: 100', + '', +].join('\n'), 'candidate-only.yaml'); + +const request = { + type: 'normal_fight', + times: 2, + plan: { + chapter: plan.data.chapter, + map: plan.data.map, + fleet_id: 1, + node_defaults: { + formation: 4, + night: true, + long_missile_support: true, + proceed_stop: [1, 2], + }, + }, +}; + +const task = { + request, + fleetId: 1, + fleetPresets: plan.data.fleet_presets, + currentPresetIndex: -1, +}; +new TaskQueue().switchTaskPreset(task, 0); + +const [candidateOnly, strictPrimary, anonymousFilter] = + request.plan.fleet_rules; +assert.equal( + Object.prototype.hasOwnProperty.call(candidateOnly, 'name'), + false, +); +assert.deepEqual(candidateOnly.candidates, [ + { name: '胡德', ship_type: ['bc'], min_level: 20 }, + { name: '扶桑', ship_type: ['bb'], max_level: 90 }, +]); +assert.equal(strictPrimary.name, '重庆'); +assert.deepEqual( + strictPrimary.ship_type, + ['kp', 'cg', 'bg', 'bbg', 'asdg', 'aadg', 'ap'], +); +assert.deepEqual(strictPrimary.candidates, [{ name: '长春' }]); +assert.equal(typeof anonymousFilter.name, 'string'); +assert.ok(anonymousFilter.name.length > 0); +assert.equal(anonymousFilter.search_name, anonymousFilter.name); +assert.deepEqual(anonymousFilter.ship_type, ['ss']); +assert.equal(anonymousFilter.min_level, 100); +assert.equal( + Object.prototype.hasOwnProperty.call(request, 'fleet_id'), + false, +); +assert.equal(request.plan.node_defaults.long_missile_support, true); +assert.deepEqual(request.plan.node_defaults.proceed_stop, [1, 2]); +assert.equal(TYPE_LABELS.kp, '导巡'); +assert.equal(TYPE_LABELS.cg, '防巡'); +assert.deepEqual( + new Set(NATIVE_FLEET_SHIP_TYPE_CODES), + new Set(Object.keys(NATIVE_FLEET_SHIP_TYPE_LABELS)), +); +assert.deepEqual( + new Set(SHIP_TYPE_FILTER_ORDER), + new Set(NATIVE_FLEET_SHIP_TYPE_CODES), +); +assert.deepEqual( + new Set(FLEET_SHIP_TYPE_CODES), + new Set([...NATIVE_FLEET_SHIP_TYPE_CODES, 'ss_or_ssg']), +); +const nativeShipTypes = new Set(NATIVE_FLEET_SHIP_TYPE_CODES); +assert.ok(ALL_SHIPS.length >= 875); +for (const ship of ALL_SHIPS) { + assert.equal( + nativeShipTypes.has(ship.ship_type), + true, + `${ship.name} 使用了非 canonical 舰种 ${ship.ship_type}`, + ); +} +for (const shipType of ['cg', 'bg', 'asdg', 'kp']) { + assert.equal(normalizeFleetShipTypeCode(shipType), shipType); +} +for (const shipType of ['cgaa', 'cbg', 'ddg', 'ddgaa', 'cf']) { + assert.equal(normalizeFleetShipTypeCode(shipType), null); +} + +const crossRepoCases = [ + buildFleetContractCase('structured-candidate-only', [ + 'chapter: 1', + 'map: 1', + 'fleet_presets:', + ' - name: 结构化纯候选', + ' ships:', + ' - candidates:', + ' - name: 胡德', + ' ship_type: [bc]', + ' min_level: 20', + ' - name: 扶桑', + ' ship_type: [bb]', + ' max_level: 90', + ' - candidates:', + ' - name: 胡德', + '', + ].join('\n')), + buildFleetContractCase('legacy-string-candidate-only', [ + 'chapter: 1', + 'map: 1', + 'fleet_presets:', + ' - name: 旧字符串纯候选', + ' ships:', + ' - search_name: 契卡洛夫', + ' ship_type: [cv]', + ' min_level: 90', + ' max_level: 110', + ' candidates: [85工程, 岛风]', + ' - candidates: [胡德, 扶桑]', + ' - candidates: [胡德]', + '', + ].join('\n')), + buildFleetContractCase('mixed-candidate-only', [ + 'chapter: 1', + 'map: 1', + 'fleet_presets:', + ' - name: 混合纯候选', + ' ships:', + ' - search_name: 大凤', + ' ship_type: [cv]', + ' min_level: 80', + ' max_level: 100', + ' candidates:', + ' - 大凤·改', + ' - name: 岛风', + ' search_name: 岛风', + ' ship_type: [dd]', + ' min_level: 20', + ' max_level: 30', + '', + ].join('\n')), + buildFleetContractCase('strict-primary', [ + 'chapter: 1', + 'map: 1', + 'fleet_presets:', + ' - name: 严格主选', + ' ships:', + ' - name: 胡德', + ' search_name: 胡德', + ' ship_type: [bc]', + ' min_level: 20', + ' candidates:', + ' - name: 扶桑', + ' ship_type: [bb]', + ' max_level: 90', + '', + ].join('\n')), +]; + +const legacyApiRules = crossRepoCases[1].request.plan.fleet_rules; +assert.equal(legacyApiRules.length, 3); +for (const rule of legacyApiRules) { + assert.equal( + Object.prototype.hasOwnProperty.call(rule, 'name'), + false, + ); +} +assert.equal( + Object.prototype.hasOwnProperty.call(legacyApiRules[0], 'search_name'), + false, +); +assert.deepEqual( + legacyApiRules[0].candidates.map(rule => ({ + name: rule.name, + search_name: rule.search_name, + ship_type: rule.ship_type, + min_level: rule.min_level, + max_level: rule.max_level, + })), + [ + { + name: '85工程', + search_name: '契卡洛夫', + ship_type: ['cv'], + min_level: 90, + max_level: 110, + }, + { + name: '岛风', + search_name: '契卡洛夫', + ship_type: ['cv'], + min_level: 90, + max_level: 110, + }, + ], +); +const mixedApiRule = crossRepoCases[2].request.plan.fleet_rules[0]; +assert.equal( + Object.prototype.hasOwnProperty.call(mixedApiRule, 'name'), + false, +); +assert.equal( + Object.prototype.hasOwnProperty.call(mixedApiRule, 'search_name'), + false, +); +assert.deepEqual(mixedApiRule.candidates, [ + { + name: '大凤·改', + search_name: '大凤', + ship_type: ['cv'], + min_level: 80, + max_level: 100, + }, + { + name: '岛风', + search_name: '岛风', + ship_type: ['dd'], + min_level: 20, + max_level: 30, + }, +]); + +const backendContracts = runBackendFleetContract(crossRepoCases); +assert.equal(backendContracts.length, crossRepoCases.length); +for (const contract of backendContracts) { + assert.deepEqual( + contract.api, + contract.source_yaml, + `${contract.name}: GUI API 与后端原始 YAML 语义不一致`, + ); + assert.deepEqual( + contract.gui_yaml, + contract.source_yaml, + `${contract.name}: GUI 保存 YAML 改变了后端语义`, + ); +} +for (const contract of backendContracts.slice(0, 3)) { + for (const slot of contract.api) { + assert.equal(slot.primary, null); + } +} +assert.equal(backendContracts[3].api[0].primary.name, '胡德'); +assert.equal(backendContracts[3].api[0].primary.search_name, '胡德'); +assert.equal(backendContracts[3].api[0].primary.min_level, 20); +assert.deepEqual( + backendContracts[3].api[0].candidates.map(rule => rule.name), + ['扶桑'], +); +assert.deepEqual( + backendContracts[1].api[1].candidates.map(rule => rule.name), + ['胡德', '扶桑'], +); +assert.deepEqual( + backendContracts[1].api[2].candidates.map(rule => rule.name), + ['胡德'], +); +console.log('GUI/AutoWSGR API contract tests passed'); diff --git a/scripts/test-daily-sortie-stats.js b/scripts/test-daily-sortie-stats.js new file mode 100644 index 0000000..587e88f --- /dev/null +++ b/scripts/test-daily-sortie-stats.js @@ -0,0 +1,136 @@ +import assert from 'node:assert/strict'; +import esbuild from 'esbuild'; + +const result = await esbuild.build({ + entryPoints: ['src/model/statistics/DailySortieStats.ts'], + bundle: true, + platform: 'node', + format: 'esm', + write: false, +}); +const moduleUrl = `data:text/javascript;base64,${ + Buffer.from(result.outputFiles[0].text).toString('base64') +}`; +const { DailySortieStats } = await import(moduleUrl); + +const values = new Map(); +const storage = { + get: key => values.get(key) ?? null, + set: (key, value) => values.set(key, value), + remove: key => values.delete(key), +}; + +const start = new Date(2026, 7, 4, 12, 0, 0).getTime(); +let now = start; +const stats = new DailySortieStats(storage, () => now); + +const battleLogs = [ + '[Combat] 战果: MVP=1 评价=SS 节点: A', + '[Combat] 战果: MVP=2 评价=S 节点: B', + '[Combat] 战果: MVP=1 评价=A 节点: C', + '[Combat] 战果: MVP=4 评价=B 节点: D', + '[Combat] 战果: MVP=3 评价=C 节点: E', + '[Combat] 战果: MVP=5 评价=D 节点: F', +]; +for (const log of battleLogs) { + assert.equal(stats.consume(log, now), true); + now += 10; +} + +// WebSocket 和 stdout 紧接着传入同一日志时只统计一次。 +const duplicateBattle = battleLogs[0]; +assert.equal(stats.consume(duplicateBattle, now), false); +now += 1_300; +assert.equal(stats.consume(duplicateBattle, now), true); + +now += 10; +assert.equal( + stats.consume('[UI] 修理位置: [1, 3] (策略: moderate)', now), + true, +); +assert.equal( + stats.consume('[UI] 修理位置: [1, 3] (策略: moderate)', now + 50), + false, +); + +now += 100; +assert.equal( + stats.consume('[OPS] 浴室修理派单成功 (120s, 本轮已派 1 艘)', now), + true, +); +now += 100; +stats.consume('[UI] 战利品数量: 12/50', now); +now += 100; +stats.consume('[UI] 舰船数量: 27/500', now); + +now += 100; +stats.consume('[Combat] 获得舰船: 昆西', now); +let snapshot = stats.getSnapshot(now); +assert.equal(snapshot.shipCount, 28); +assert.deepEqual(snapshot.shipDrops, [{ name: '昆西', count: 1 }]); +assert.deepEqual(snapshot.dropNotice, { + shipName: '昆西', + dailyIndex: 28, + visibleUntil: now + 60_000, +}); + +now += 2_000; +stats.consume('[Combat] 获得舰船: 海伦娜', now); +now += 2_000; +stats.consume('[Combat] 获得舰船: 昆西', now); +const latestDropAt = now; +now += 100; +stats.consume('[UI] 远征收取: 2 支', now); + +snapshot = stats.getSnapshot(now); +assert.equal(snapshot.battleCount, 7); +assert.deepEqual(snapshot.grades, { + SS: 2, + S: 1, + A: 1, + B: 1, + C: 1, + D: 1, +}); +assert.equal(snapshot.quickRepairCount, 2); +assert.equal(snapshot.bathRepairCount, 1); +assert.equal(snapshot.lootCount, 12); +assert.equal(snapshot.lootLimit, 50); +assert.equal(snapshot.shipCount, 30); +assert.equal(snapshot.shipLimit, 500); +assert.equal(snapshot.expeditionCount, 2); +assert.deepEqual(snapshot.shipDrops, [ + { name: '昆西', count: 2 }, + { name: '海伦娜', count: 1 }, +]); +assert.deepEqual(snapshot.dropNotice, { + shipName: '昆西', + dailyIndex: 30, + visibleUntil: latestDropAt + 60_000, +}); + +// 重启恢复累计值,但旧掉落提示不重复出现。 +const restored = new DailySortieStats(storage, () => now); +const restoredSnapshot = restored.getSnapshot(now); +assert.equal(restoredSnapshot.battleCount, 7); +assert.equal(restoredSnapshot.shipCount, 30); +assert.equal(restoredSnapshot.dropNotice, null); + +// 当前会话中的提示满 60 秒后消失。 +assert.equal( + stats.getSnapshot(latestDropAt + 60_001).dropNotice, + null, +); + +// 跨到本地时间下一天后,所有每日数据归零。 +const nextDay = new Date(2026, 7, 5, 0, 0, 1).getTime(); +const nextDaySnapshot = restored.getSnapshot(nextDay); +assert.equal(nextDaySnapshot.battleCount, 0); +assert.equal(nextDaySnapshot.quickRepairCount, 0); +assert.equal(nextDaySnapshot.bathRepairCount, 0); +assert.equal(nextDaySnapshot.lootCount, 0); +assert.equal(nextDaySnapshot.shipCount, 0); +assert.equal(nextDaySnapshot.expeditionCount, 0); +assert.deepEqual(nextDaySnapshot.shipDrops, []); + +console.log('daily sortie stats tests passed'); diff --git a/scripts/test-event-20260730-resources.js b/scripts/test-event-20260730-resources.js new file mode 100644 index 0000000..dc52f0b --- /dev/null +++ b/scripts/test-event-20260730-resources.js @@ -0,0 +1,177 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); + +const root = path.resolve(__dirname, '..'); +const expectedMaps = [ + '1a', + '2a', + '3a', + '3b', + '4a', + '4b', + '5a', + '6a', + '6b', +]; + +function canonicalEventMapFile(chapter, map) { + const match = map.match(/^(\d+)([ab])$/); + assert.ok(match, `invalid 20260730 map code: ${map}`); + const entrance = match[2] === 'b' ? 'β' : 'α'; + const hard = chapter === 'H' ? 'H' : ''; + return `激斗漩涡${hard}-Ex-${match[1]}-${entrance}.json`; +} + +const planFiles = [ + 'bettle-E1炸鱼.yaml', + 'bettle-E5夜战.yaml', + 'bettle-H1炸鱼.yaml', + 'bettle-H5夜战.yaml', +]; +const expected = new Map([ + ['bettle-E1炸鱼.yaml', { + event: '20260730', + chapter: 'E', + map: '1a', + selected_nodes: ['B'], + node_defaults: { proceed: false, formation: 5 }, + }], + ['bettle-E5夜战.yaml', { + event: '20260730', + chapter: 'E', + map: '5a', + selected_nodes: ['A', 'B', 'C', 'D', 'F'], + node_defaults: { night: true, proceed: true, formation: 4 }, + node_args: { + C: { proceed: false }, + D: { proceed: false }, + F: { proceed: false }, + }, + }], + ['bettle-H1炸鱼.yaml', { + event: '20260730', + chapter: 'H', + map: '1a', + selected_nodes: ['B'], + node_defaults: { proceed: false, formation: 5 }, + }], + ['bettle-H5夜战.yaml', { + event: '20260730', + chapter: 'H', + map: '5a', + selected_nodes: ['A', 'B', 'C', 'D', 'F'], + node_defaults: { night: true, proceed: true, formation: 4 }, + node_args: { + C: { proceed: false }, + D: { proceed: false }, + F: { proceed: false }, + }, + }], +]); + +for (const file of planFiles) { + const systemPath = path.join( + root, + 'resource', + 'system_battle_plans', + file, + ); + const migrationPath = path.join( + root, + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + file, + ); + assert.equal( + fs.existsSync(systemPath), + false, + `obsolete activity plan must not remain a system preset: ${file}`, + ); + assert.equal( + fs.existsSync(migrationPath), + true, + `missing v6 activity migration resource: ${file}`, + ); + const plan = yaml.load(fs.readFileSync(migrationPath, 'utf8')); + assert.deepEqual(plan, expected.get(file)); +} + +const templates = JSON.parse(fs.readFileSync( + path.join(root, 'resource', 'builtin_templates.json'), + 'utf8', +)); +const template = templates.find(item => item.id === 'builtin_event_20260730'); +assert.equal( + template, + undefined, + 'retired activity plans must not remain in the builtin template list', +); + +const eventMapRoot = path.join(root, 'resource', 'maps', 'event'); +const catalog = JSON.parse(fs.readFileSync( + path.join(eventMapRoot, 'index.json'), + 'utf8', +)); +assert.equal(catalog.schema_version, 2); +const event = catalog.events.find(item => item.event === '20260730'); +assert.ok(event, 'missing 20260730 event map catalog'); +assert.deepEqual(event.chapters.E, expectedMaps); +assert.deepEqual(event.chapters.H, expectedMaps); +assert.equal(event.chapters.E.includes('1b'), false); +assert.equal(event.chapters.H.includes('1b'), false); + +for (const chapter of ['E', 'H']) { + for (const map of expectedMaps) { + const file = canonicalEventMapFile(chapter, map); + assert.equal(event.files[chapter][map], file); + const mapPath = path.join( + eventMapRoot, + '20260730', + file, + ); + assert.equal(fs.existsSync(mapPath), true, `missing event map: ${mapPath}`); + const mapData = JSON.parse(fs.readFileSync(mapPath, 'utf8')); + const entrance = map.endsWith('b') ? 'β' : 'α'; + assert.ok(mapData[entrance], `${chapter}-${map} missing ${entrance} entrance`); + } +} + +assert.equal( + fs.existsSync(path.join(eventMapRoot, '20260730', 'E-1a.json')), + false, + 'legacy activity map aliases must be removed', +); +assert.equal( + fs.existsSync(path.join(eventMapRoot, '20260212', 'E-1.json')), + true, + 'historical AutoWSGR activity maps must remain available', +); +assert.equal( + fs.existsSync(path.join(root, 'resource', 'maps', 'normal', '1-1.json')), + true, + 'normal maps must follow the AutoWSGR directory contract', +); +assert.deepEqual( + fs.readdirSync(path.join(root, 'resource', 'maps'), { + withFileTypes: true, + }) + .filter(entry => entry.isFile()) + .map(entry => entry.name), + [], + 'map files must be stored in AutoWSGR category directories', +); + +const decisiveData = yaml.load(fs.readFileSync( + path.join(root, 'resource', 'maps', 'decisive_battle', 'enemy_spec.yaml'), + 'utf8', +)); +assert.ok( + decisiveData?.key_points && decisiveData?.map_end && decisiveData?.enemy, + 'decisive battle data must keep the AutoWSGR schema', +); + +console.log('20260730 activity resources test passed'); diff --git a/scripts/test-event-map-loader.js b/scripts/test-event-map-loader.js new file mode 100644 index 0000000..af0fd9b --- /dev/null +++ b/scripts/test-event-map-loader.js @@ -0,0 +1,209 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('js-yaml'); +const esbuild = require('esbuild'); + +const root = path.resolve(__dirname, '..'); +const readPaths = []; + +global.window = { + electronBridge: { + readFile: async (relativePath) => { + readPaths.push(relativePath); + return fs.promises.readFile(path.join(root, relativePath), 'utf8'); + }, + }, +}; +global.document = { + createElement: () => ({ + value: '', + textContent: '', + title: '', + dataset: {}, + }), +}; + +async function loadModules() { + const result = await esbuild.build({ + stdin: { + contents: [ + "export * from './src/model/MapDataLoader.ts';", + "export { PlanModel } from './src/model/PlanModel.ts';", + "export { PlanPreviewView } from './src/view/plan/PlanPreviewView.ts';", + ].join('\n'), + resolveDir: root, + sourcefile: 'event-map-test-entry.ts', + }, + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + }); + const testModule = { exports: {} }; + const execute = new Function( + 'require', + 'module', + 'exports', + result.outputFiles[0].text, + ); + execute(require, testModule, testModule.exports); + return testModule.exports; +} + +async function main() { + const { + loadEventMapCatalog, + loadEventMapData, + loadMapData, + PlanModel, + PlanPreviewView, + } = await loadModules(); + + const catalog = await loadEventMapCatalog(); + const event = catalog.find(entry => entry.event === '20260730'); + assert.ok(event); + assert.deepEqual(event.chapters.E, [ + '1a', + '2a', + '3a', + '3b', + '4a', + '4b', + '5a', + '6a', + '6b', + ]); + assert.equal(event.chapters.E.includes('1b'), false); + assert.equal( + event.files.E['1a'], + '激斗漩涡-Ex-1-α.json', + ); + assert.equal( + event.files.H['3b'], + '激斗漩涡H-Ex-3-β.json', + ); + + const view = Object.create(PlanPreviewView.prototype); + view.eventMapCatalog = catalog; + view.chapterSelect = { + value: '', + options: [{ value: '1', dataset: {} }], + querySelectorAll() { + return this.options.filter(option => option.dataset.eventMap); + }, + appendChild(option) { + this.options.push(option); + }, + }; + view.mapSelect = { + disabled: false, + value: '', + options: [], + replaceChildren(...options) { + this.options = options; + this.value = options[0]?.value ?? ''; + }, + }; + view.renderEventChapterOptions({ + event: '20260730', + chapter: 'E', + map: '1a', + }); + const eventChapterOptions = view.chapterSelect.options + .filter(option => option.dataset.eventMap) + .map(option => option.value); + assert.deepEqual( + eventChapterOptions.slice(0, 2), + ['event:20260730:E', 'event:20260730:H'], + ); + assert.equal(eventChapterOptions.includes('event:20260212:E'), true); + view.updateMapOptions('event:20260730:E'); + assert.equal(view.mapSelect.value, '1a'); + assert.equal( + view.mapSelect.options.some(option => option.value === '1'), + false, + ); + view.updateMapOptions('event:20260730:E', '1a'); + assert.deepEqual( + view.mapSelect.options.map(option => option.value), + event.chapters.E, + ); + let mapChange; + view.onMapChange = (chapter, map) => { + mapChange = { chapter, map }; + }; + view.chapterSelect.value = 'event:20260730:E'; + view.mapSelect.value = '3b'; + view.emitMapChange(); + assert.deepEqual(mapChange, { + chapter: 'event:20260730:E', + map: '3b', + }); + + let readStart = readPaths.length; + const map1a = await loadEventMapData('20260730', 'E', '1a'); + assert.ok(map1a); + assert.ok(map1a['0']); + assert.equal(Object.hasOwn(map1a, 'α'), false); + assert.deepEqual(map1a['0'].next, ['A', 'B', 'C']); + assert.deepEqual( + readPaths.slice(readStart), + ['resource/maps/event/20260730/激斗漩涡-Ex-1-α.json'], + ); + + const map3b = await loadEventMapData('20260730', 'H', '3b'); + assert.ok(map3b); + assert.ok(map3b['0']); + assert.equal(Object.hasOwn(map3b, 'β'), false); + + readStart = readPaths.length; + assert.equal(await loadEventMapData('20260730', 'E', '1'), null); + assert.deepEqual(readPaths.slice(readStart), []); + + readStart = readPaths.length; + const historicalEventMap = await loadEventMapData( + '20260212', + 'E', + 1, + ); + assert.ok(historicalEventMap); + assert.deepEqual( + readPaths.slice(readStart), + ['resource/maps/event/20260212/E-1.json'], + ); + + readStart = readPaths.length; + const normalMap = await loadMapData(1, 1); + assert.ok(normalMap); + assert.equal(readPaths[readStart], 'resource/maps/normal/1-1.json'); + + const plan = PlanModel.fromYaml([ + "event: '20260730'", + 'chapter: E', + 'map: 1a', + 'selected_nodes: [B]', + '', + ].join('\n'), 'event-plan.yaml'); + const roundTrip = yaml.load(plan.toYaml()); + assert.equal(roundTrip.event, '20260730'); + assert.equal(roundTrip.chapter, 'E'); + assert.equal(roundTrip.map, '1a'); + assert.equal(plan.mapName, 'E-Ex-1-α'); + + const betaPlan = PlanModel.create('H', '3b', ['0', 'A'], '20260730'); + const betaRoundTrip = yaml.load(betaPlan.toYaml()); + assert.equal(betaRoundTrip.event, '20260730'); + assert.equal(betaRoundTrip.chapter, 'H'); + assert.equal(betaRoundTrip.map, '3b'); + assert.equal(betaPlan.mapName, 'H-Ex-3-β'); + + const legacyPlan = PlanModel.create('E', 1, ['0'], '20200101'); + assert.equal(legacyPlan.mapName, 'E-Ex-1'); + console.log('event map loader and plan round-trip tests passed'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/test-fleet-domain.js b/scripts/test-fleet-domain.js new file mode 100644 index 0000000..05bab48 --- /dev/null +++ b/scripts/test-fleet-domain.js @@ -0,0 +1,628 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import esbuild from 'esbuild'; + +const require = createRequire(import.meta.url); +const result = esbuild.buildSync({ + entryPoints: ['src/model/fleet/index.ts'], + bundle: true, + platform: 'node', + format: 'cjs', + write: false, + loader: { '.json': 'json' }, +}); +const module = { exports: {} }; +new Function('require', 'module', 'exports', result.outputFiles[0].text)(require, module, module.exports); +const { + DecisiveFleetDraft, + compactFleetDraftSlots, + createFleetCandidateDraft, + createFleetDraft, + fleetDraftFromTeamPlan, + fleetDraftToTeamPlan, + fleetPresetIdentityKey, + fleetPresetRuleKey, + hasOtherPrimaryShip, + insertFleetCandidate, + insertFleetPrimary, + isFleetSlotEmpty, + moveFleetPrimary, + removeFleetPrimary, + resolveFleetSlotPosition, + resolveGalleryFormationAssignment, + resolveGalleryFormationDropTarget, + resolveFleetPresetRules, + resolveFleetPreset, + toBackendName, +} = module.exports; + +assert.equal(toBackendName('岛风(岛风型驱逐舰)·改'), '岛风'); + +const testShip = (id, name) => ({ + id, + name, + search_name: name, +}); +const createFollowModeDraft = () => { + const draft = createFleetDraft(); + draft.slots[0].primary = testShip(1, '主选A'); + draft.slots[0].candidates[0] = createFleetCandidateDraft( + testShip(11, '备选A'), + ); + draft.slots[1].primary = testShip(2, '主选B'); + draft.slots[1].candidates[0] = createFleetCandidateDraft( + testShip(12, '备选B'), + ); + return draft; +}; + +const duplicatePrimaryDraft = createFollowModeDraft(); +assert.equal( + hasOtherPrimaryShip( + duplicatePrimaryDraft.slots, + '主选A', + 1, + ), + true, +); +assert.equal( + hasOtherPrimaryShip( + duplicatePrimaryDraft.slots, + '主选A', + 0, + ), + false, +); + +const preservedFocusDraft = createFleetDraft(); +preservedFocusDraft.slots[0].primary = testShip(31, '删除目标'); +preservedFocusDraft.slots[1].primary = testShip(32, '中间主选'); +preservedFocusDraft.slots[2].primary = testShip(33, '当前焦点'); +const focusedSlot = preservedFocusDraft.slots[2]; +const focusedPosition = removeFleetPrimary( + preservedFocusDraft.slots, + 0, + 2, +); +assert.equal(focusedPosition, 1); +assert.equal(preservedFocusDraft.slots[focusedPosition], focusedSlot); +assert.equal( + preservedFocusDraft.slots[focusedPosition].primary.name, + '当前焦点', +); + +const rightRemovalDraft = createFleetDraft(); +rightRemovalDraft.slots[0].primary = testShip(34, '当前焦点'); +rightRemovalDraft.slots[1].primary = testShip(35, '删除目标'); +const rightFocusedSlot = rightRemovalDraft.slots[0]; +const rightFocusedPosition = removeFleetPrimary( + rightRemovalDraft.slots, + 1, + 0, +); +assert.equal(rightFocusedPosition, 0); +assert.equal(rightRemovalDraft.slots[0], rightFocusedSlot); + +const emptyFormationDraft = createFleetDraft(); +assert.deepEqual( + resolveGalleryFormationAssignment( + emptyFormationDraft.slots, + 0, + '连续新增A', + ), + { targetPosition: 0, activePosition: 1 }, +); + +const continuousFormationDraft = createFleetDraft(); +continuousFormationDraft.slots[0].primary = testShip(41, '已有主选'); +assert.deepEqual( + resolveGalleryFormationAssignment( + continuousFormationDraft.slots, + 0, + '连续新增B', + ), + { targetPosition: 1, activePosition: 1 }, +); + +const replacementFormationDraft = createFleetDraft(); +replacementFormationDraft.slots[0].primary = testShip(42, '当前主选'); +replacementFormationDraft.slots[1].primary = testShip(43, '右侧主选'); +assert.deepEqual( + resolveGalleryFormationAssignment( + replacementFormationDraft.slots, + 0, + '替换主选', + ), + { targetPosition: 0, activePosition: 0 }, +); +assert.deepEqual( + resolveGalleryFormationAssignment( + replacementFormationDraft.slots, + 0, + '右侧主选', + ), + { targetPosition: 1, activePosition: 0 }, +); + +const skippedEmptyDraft = createFleetDraft(); +assert.deepEqual( + resolveGalleryFormationAssignment( + skippedEmptyDraft.slots, + 3, + '从左侧新增', + ), + { targetPosition: 0, activePosition: 1 }, +); + +const insertedFormationDraft = createFleetDraft(); +insertedFormationDraft.slots.slice(0, 5).forEach((slot, index) => { + slot.primary = testShip(70 + index, `原编队${index + 1}`); +}); +assert.equal( + resolveGalleryFormationDropTarget(insertedFormationDraft.slots, 2), + 2, +); +const leftEmptyFormationDraft = createFleetDraft(); +leftEmptyFormationDraft.slots[2].primary = testShip(69, '右侧已有舰船'); +assert.equal( + resolveGalleryFormationDropTarget(leftEmptyFormationDraft.slots, 2), + 0, +); +const leftFormationSlots = insertedFormationDraft.slots.slice(0, 2); +const shiftedFormationSlots = insertedFormationDraft.slots.slice(2, 5); +const insertedFormationSlot = createFleetDraft().slots[0]; +insertedFormationSlot.primary = testShip(80, '插入编队'); +assert.equal( + insertFleetPrimary( + insertedFormationDraft.slots, + 2, + insertedFormationSlot, + 'ship', + ), + insertedFormationSlot, +); +assert.deepEqual( + insertedFormationDraft.slots.slice(0, 2), + leftFormationSlots, +); +assert.equal(insertedFormationDraft.slots[2], insertedFormationSlot); +assert.deepEqual( + insertedFormationDraft.slots.slice(3, 6), + shiftedFormationSlots, +); + +const fullFormationDraft = createFleetDraft(); +fullFormationDraft.slots.forEach((slot, index) => { + slot.primary = testShip(90 + index, `满编${index + 1}`); +}); +const fullFormationSnapshot = [...fullFormationDraft.slots]; +const rejectedFormationSlot = createFleetDraft().slots[0]; +rejectedFormationSlot.primary = testShip(99, '无空位插入'); +assert.equal( + insertFleetPrimary( + fullFormationDraft.slots, + 2, + rejectedFormationSlot, + 'ship', + ), + null, +); +assert.deepEqual(fullFormationDraft.slots, fullFormationSnapshot); + +const insertedCandidates = Array.from( + { length: 6 }, + (_, index) => createFleetCandidateDraft( + index < 5 ? testShip(100 + index, `原备选${index + 1}`) : null, + ), +); +const leftCandidates = insertedCandidates.slice(0, 2); +const shiftedCandidates = insertedCandidates.slice(2, 5); +const insertedCandidate = createFleetCandidateDraft( + testShip(110, '插入备选'), +); +assert.equal( + insertFleetCandidate(insertedCandidates, 2, insertedCandidate), + 2, +); +assert.deepEqual(insertedCandidates.slice(0, 2), leftCandidates); +assert.equal(insertedCandidates[2], insertedCandidate); +assert.deepEqual(insertedCandidates.slice(3, 6), shiftedCandidates); + +const dragFocusDraft = createFollowModeDraft(); +dragFocusDraft.slots[2].primary = testShip(44, '保持焦点'); +const dragFocusedSlot = dragFocusDraft.slots[2]; +moveFleetPrimary(dragFocusDraft.slots, 0, 1, 'ship'); +assert.equal( + resolveFleetSlotPosition(dragFocusDraft.slots, dragFocusedSlot, 2), + 2, +); + +const shipFollowDraft = createFollowModeDraft(); +moveFleetPrimary( + shipFollowDraft.slots, + 0, + 1, + 'ship', +); +assert.equal(shipFollowDraft.slots[0].primary.name, '主选B'); +assert.equal(shipFollowDraft.slots[0].candidates[0].ship.name, '备选B'); + +const positionFollowDraft = createFollowModeDraft(); +moveFleetPrimary( + positionFollowDraft.slots, + 0, + 1, + 'position', +); +assert.equal(positionFollowDraft.slots[0].primary.name, '主选B'); +assert.equal(positionFollowDraft.slots[0].candidates[0].ship.name, '备选A'); +assert.equal(positionFollowDraft.slots[1].primary.name, '主选A'); +assert.equal(positionFollowDraft.slots[1].candidates[0].ship.name, '备选B'); + +const switchedModeDraft = createFollowModeDraft(); +moveFleetPrimary( + switchedModeDraft.slots, + 0, + 1, + 'position', +); +assert.equal(switchedModeDraft.slots[0].primary.name, '主选B'); +assert.equal(switchedModeDraft.slots[0].candidates[0].ship.name, '备选A'); +moveFleetPrimary( + switchedModeDraft.slots, + 0, + 1, + 'ship', +); +assert.equal(switchedModeDraft.slots[0].primary.name, '主选A'); +assert.equal(switchedModeDraft.slots[0].candidates[0].ship.name, '备选B'); +assert.equal(switchedModeDraft.slots[1].primary.name, '主选B'); +assert.equal(switchedModeDraft.slots[1].candidates[0].ship.name, '备选A'); + +const reservedPositionDraft = createFleetDraft(); +reservedPositionDraft.slots[0].primary = testShip(3, '主选C'); +reservedPositionDraft.slots[0].candidates[0] = createFleetCandidateDraft( + testShip(13, '备选C'), +); +reservedPositionDraft.slots[1].primary = testShip(4, '主选D'); +reservedPositionDraft.slots[0].primary = null; +compactFleetDraftSlots(reservedPositionDraft.slots); +assert.equal(reservedPositionDraft.slots[0].primary, null); +assert.equal( + reservedPositionDraft.slots[0].candidates[0].ship.name, + '备选C', +); +assert.equal(reservedPositionDraft.slots[1].primary.name, '主选D'); +reservedPositionDraft.slots[0].candidates[0].ship = null; +compactFleetDraftSlots(reservedPositionDraft.slots); +assert.equal(reservedPositionDraft.slots[0].primary.name, '主选D'); + +const shiftedCandidateOnlyDraft = createFleetDraft(); +shiftedCandidateOnlyDraft.slots[0].primary = testShip(5, '主选E'); +shiftedCandidateOnlyDraft.slots[1].primary = testShip(6, '主选F'); +shiftedCandidateOnlyDraft.slots[1].candidates[0] = + createFleetCandidateDraft(testShip(16, '备选F')); +shiftedCandidateOnlyDraft.slots[2].candidates[0] = + createFleetCandidateDraft(testShip(17, '纯备选G')); +shiftedCandidateOnlyDraft.slots[0].primary = null; +compactFleetDraftSlots(shiftedCandidateOnlyDraft.slots); +assert.equal(shiftedCandidateOnlyDraft.slots[0].primary.name, '主选F'); +assert.equal( + shiftedCandidateOnlyDraft.slots[0].candidates[0].ship.name, + '备选F', +); +assert.equal(shiftedCandidateOnlyDraft.slots[1].primary, null); +assert.equal( + shiftedCandidateOnlyDraft.slots[1].candidates[0].ship.name, + '纯备选G', +); + +const delayedBindingDraft = createFleetDraft(); +delayedBindingDraft.slots[0].primary = testShip(7, '主选H'); +const waitingSlot = delayedBindingDraft.slots[3]; +waitingSlot.candidates[0] = + createFleetCandidateDraft(testShip(18, '待绑定备选')); +assert.equal(waitingSlot.primary, null); +assert.equal(isFleetSlotEmpty(waitingSlot), false); +waitingSlot.primary = testShip(8, '新加入主选'); +moveFleetPrimary(delayedBindingDraft.slots, 3, 0, 'ship'); +assert.equal(delayedBindingDraft.slots[0].primary.name, '新加入主选'); +assert.equal( + delayedBindingDraft.slots[0].candidates[0].ship.name, + '待绑定备选', +); + +const draggedBindingDraft = createFleetDraft(); +const draggedSource = draggedBindingDraft.slots[0]; +draggedSource.primary = testShip(9, '拖入主选'); +draggedSource.candidates[0] = + createFleetCandidateDraft(testShip(20, '来源位置备选')); +const draggedTarget = draggedBindingDraft.slots[1]; +draggedTarget.candidates[0] = + createFleetCandidateDraft(testShip(19, '目标位置备选')); +moveFleetPrimary(draggedBindingDraft.slots, 0, 1, 'ship'); +assert.equal(draggedBindingDraft.slots.indexOf(draggedSource), 1); +assert.equal(draggedSource.primary.name, '拖入主选'); +assert.equal(draggedSource.candidates[0].ship.name, '来源位置备选'); +assert.equal(draggedBindingDraft.slots.indexOf(draggedTarget), 0); +assert.equal(draggedBindingDraft.slots[0].primary, null); +assert.equal( + draggedBindingDraft.slots[0].candidates[0].ship.name, + '目标位置备选', +); + +const positionBindingDraft = createFleetDraft(); +positionBindingDraft.slots[0].primary = testShip(10, '位置模式主选'); +positionBindingDraft.slots[0].candidates[0] = + createFleetCandidateDraft(testShip(21, '来源位置保留备选')); +const positionTarget = positionBindingDraft.slots[1]; +positionTarget.candidates[0] = + createFleetCandidateDraft(testShip(22, '目标位置备选')); +moveFleetPrimary(positionBindingDraft.slots, 0, 1, 'position'); +assert.equal(positionTarget.primary.name, '位置模式主选'); +assert.equal(positionTarget.candidates[0].ship.name, '目标位置备选'); +assert.equal(positionBindingDraft.slots[0].primary, null); +assert.equal( + positionBindingDraft.slots[0].candidates[0].ship.name, + '来源位置保留备选', +); + +const candidateOnlyReorderDraft = createFleetDraft(); +const candidateOnlySlots = candidateOnlyReorderDraft.slots.slice(0, 3); +candidateOnlySlots.forEach((slot, index) => { + slot.candidates[0] = createFleetCandidateDraft( + testShip(51 + index, `纯备选位置${index + 1}`), + ); +}); +assert.equal( + moveFleetPrimary(candidateOnlyReorderDraft.slots, 0, 2, 'position'), + candidateOnlySlots[0], +); +assert.equal(candidateOnlyReorderDraft.slots[0], candidateOnlySlots[2]); +assert.equal(candidateOnlyReorderDraft.slots[1], candidateOnlySlots[1]); +assert.equal(candidateOnlyReorderDraft.slots[2], candidateOnlySlots[0]); +assert.equal( + moveFleetPrimary(candidateOnlyReorderDraft.slots, 2, 0, 'ship'), + null, +); + +const candidateOnlyMoveToEndDraft = createFleetDraft(); +const candidateOnlyMoveSlots = candidateOnlyMoveToEndDraft.slots.slice(0, 3); +candidateOnlyMoveSlots.forEach((slot, index) => { + slot.candidates[0] = createFleetCandidateDraft( + testShip(61 + index, `移动纯备选${index + 1}`), + ); +}); +moveFleetPrimary(candidateOnlyMoveToEndDraft.slots, 0, 5, 'position'); +assert.equal( + candidateOnlyMoveToEndDraft.slots[0], + candidateOnlyMoveSlots[1], +); +assert.equal( + candidateOnlyMoveToEndDraft.slots[1], + candidateOnlyMoveSlots[2], +); +assert.equal( + candidateOnlyMoveToEndDraft.slots[2], + candidateOnlyMoveSlots[0], +); + +const persistedDraft = createFleetDraft(); +persistedDraft.slots[0].primary = testShip(201, '主选持久化'); +persistedDraft.slots[0].shipTypes = ['cl']; +persistedDraft.slots[0].levelEnabled = true; +persistedDraft.slots[0].minLevel = 20; +persistedDraft.slots[0].candidates[0] = createFleetCandidateDraft( + testShip(202, '主选的备选'), +); +persistedDraft.slots[0].candidates[0].levelEnabled = true; +persistedDraft.slots[0].candidates[0].maxLevel = 90; +persistedDraft.slots[1].candidates[0] = createFleetCandidateDraft( + testShip(203, '纯候选'), +); +persistedDraft.slots[1].shipTypes = ['dd']; +persistedDraft.slots[1].levelEnabled = true; +persistedDraft.slots[1].minLevel = 30; +persistedDraft.slots[1].maxLevel = 80; + +const persistedPlan = fleetDraftToTeamPlan( + persistedDraft, + ' 持久化测试 ', +); +assert.equal(persistedPlan.name, '持久化测试'); +assert.deepEqual(persistedPlan.ships[0], { + name: '主选持久化', + ship_type: ['cl'], + min_level: 20, + candidates: [{ + name: '主选的备选', + max_level: 90, + }], +}); +assert.equal('name' in persistedPlan.ships[1], false); +assert.deepEqual(persistedPlan.ships[1], { + ship_type: ['dd'], + min_level: 30, + max_level: 80, + candidates: [{ name: '纯候选' }], +}); + +const restoredDraft = fleetDraftFromTeamPlan({ + ...persistedPlan, + file: 'team-持久化测试.yaml', + source: 'system', +}, [ + testShip(201, '主选持久化'), + testShip(202, '主选的备选'), + testShip(203, '纯候选'), +]); +assert.equal(restoredDraft.file, 'team-持久化测试.yaml'); +assert.equal(restoredDraft.source, 'system'); +assert.equal(restoredDraft.slots[1].primary, null); +assert.equal(restoredDraft.slots[1].candidates[0].ship.name, '纯候选'); +assert.deepEqual(restoredDraft.slots[1].shipTypes, ['dd']); +assert.equal(restoredDraft.slots[1].minLevel, 30); +assert.equal(restoredDraft.slots[1].maxLevel, 80); +assert.deepEqual( + fleetDraftToTeamPlan(restoredDraft, restoredDraft.name), + persistedPlan, +); + +assert.throws( + () => fleetDraftToTeamPlan(createFleetDraft(), '空舰队'), + /至少需要一艘/, +); +assert.throws( + () => fleetDraftToTeamPlan(persistedDraft, ' '), + /请输入舰队预设名称/, +); + +const invalidRuleDraft = () => { + const draft = createFleetDraft(); + draft.slots[0].primary = testShip(204, '非法规则'); + return draft; +}; +const invalidShipTypeDraft = invalidRuleDraft(); +invalidShipTypeDraft.slots[0].shipTypes = ['not-a-ship-type']; +assert.throws( + () => fleetDraftToTeamPlan(invalidShipTypeDraft, '非法舰种'), + /舰种不符合后端接口/, +); +const invalidMinLevelDraft = invalidRuleDraft(); +invalidMinLevelDraft.slots[0].levelEnabled = true; +invalidMinLevelDraft.slots[0].minLevel = 0; +assert.throws( + () => fleetDraftToTeamPlan(invalidMinLevelDraft, '非法最小等级'), + /最小等级不合法/, +); +const invalidMaxLevelDraft = invalidRuleDraft(); +invalidMaxLevelDraft.slots[0].levelEnabled = true; +invalidMaxLevelDraft.slots[0].maxLevel = 1.5; +assert.throws( + () => fleetDraftToTeamPlan(invalidMaxLevelDraft, '非法最大等级'), + /最大等级不合法/, +); +const reversedLevelDraft = invalidRuleDraft(); +reversedLevelDraft.slots[0].levelEnabled = true; +reversedLevelDraft.slots[0].minLevel = 80; +reversedLevelDraft.slots[0].maxLevel = 20; +assert.throws( + () => fleetDraftToTeamPlan(reversedLevelDraft, '反向等级'), + /最大等级不能小于最小等级/, +); + +const candidateOnly = resolveFleetPresetRules([{ + candidates: [{ name: '海伦娜' }, { name: '克利夫兰' }], + ship_type: ['cl'], + min_level: 10, + max_level: 80, +}]); +assert.equal(candidateOnly.length, 1); +assert.equal('name' in candidateOnly[0], false); +assert.deepEqual(candidateOnly[0].candidates?.map(rule => rule.name), ['海伦娜', '克利夫兰']); +assert.deepEqual(candidateOnly[0].ship_type, ['cl']); +assert.equal(candidateOnly[0].min_level, 10); +assert.equal(candidateOnly[0].max_level, 80); + +const resolved = resolveFleetPreset(['海伦娜', { ship_type: ['cl'] }]); +assert.equal(resolved[0], '海伦娜'); +assert.equal(resolved.length, 2); +assert.notEqual(resolved[1], '海伦娜'); + +const existingFleetPresets = [{ + name: '已有编队', + ships: [ + '海伦娜', + { + candidates: [ + { name: '昆西', ship_type: ['cl'] }, + { name: '克利夫兰' }, + ], + ship_type: ['cl', 'ca'], + min_level: 10, + }, + ], +}]; +const sameRulesDifferentName = { + name: '不同名称但内容相同', + ships: [ + { name: '海伦娜' }, + { + candidates: [ + { name: '昆西', ship_type: ['cl'] }, + { name: '克利夫兰' }, + ], + ship_type: ['ca', 'cl'], + min_level: 10, + }, + ], +}; +assert.equal( + fleetPresetRuleKey(existingFleetPresets[0]), + fleetPresetRuleKey(sameRulesDifferentName), +); +assert.notEqual( + fleetPresetIdentityKey(existingFleetPresets[0]), + fleetPresetIdentityKey(sameRulesDifferentName), +); +assert.equal( + fleetPresetIdentityKey(existingFleetPresets[0]), + fleetPresetIdentityKey({ + ...existingFleetPresets[0], + name: ' 已有编队 ', + }), +); +assert.notEqual( + fleetPresetRuleKey(existingFleetPresets[0]), + fleetPresetRuleKey({ + name: '候选顺序不同', + ships: [ + '海伦娜', + { + candidates: [ + { name: '克利夫兰' }, + { name: '昆西', ship_type: ['cl'] }, + ], + ship_type: ['cl', 'ca'], + min_level: 10, + }, + ], + }), +); +assert.notEqual( + fleetPresetIdentityKey(existingFleetPresets[0]), + fleetPresetIdentityKey({ + name: '等级条件不同', + ships: [ + '海伦娜', + { + candidates: [ + { name: '昆西', ship_type: ['cl'] }, + { name: '克利夫兰' }, + ], + ship_type: ['cl', 'ca'], + min_level: 20, + }, + ], + }), +); + +const decisive = new DecisiveFleetDraft({ + chapter: 6, + useQuickRepair: true, + level1: ['U-47', 'U-81'], + level2: ['U-96'], +}); +assert.equal(decisive.dirty, false); +assert.equal(decisive.place('U-1206', 'level1', 1, 5), 1); +assert.deepEqual(decisive.queue('level1'), ['U-47', 'U-1206']); +assert.equal(decisive.dirty, true); +assert.equal(decisive.move('level1', 0, 'level2', 1), 1); +assert.deepEqual(decisive.queue('level1'), ['U-1206']); +assert.deepEqual(decisive.queue('level2'), ['U-96', 'U-47']); +assert.equal(decisive.remove('level2', 0), true); +assert.deepEqual(decisive.queue('level2'), ['U-47']); +decisive.load(decisive.toSettings()); +assert.equal(decisive.dirty, false); + +console.log('fleet domain tests passed'); diff --git a/scripts/test-legacy-config-upgrade.js b/scripts/test-legacy-config-upgrade.js new file mode 100644 index 0000000..e0c5ab5 --- /dev/null +++ b/scripts/test-legacy-config-upgrade.js @@ -0,0 +1,19 @@ +/** + * 旧安装目录配置升级兼容测试入口。 + * + * 使用隔离临时目录,不读取或修改真实用户数据。 + */ +const { + fs, + temporaryDirectory, +} = require('./main-services/test-context'); +const { + testUserDataMigration, +} = require('./main-services/test-migration'); + +try { + testUserDataMigration(); + console.log('legacy configuration upgrade compatibility test passed'); +} finally { + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); +} diff --git a/scripts/test-legacy-plan-migration.js b/scripts/test-legacy-plan-migration.js new file mode 100644 index 0000000..5bd5e63 --- /dev/null +++ b/scripts/test-legacy-plan-migration.js @@ -0,0 +1,19 @@ +/** + * 旧计划自动升级迁移测试入口。 + * + * 复用主进程迁移测试,仅在系统临时目录中读写。 + */ +const { + fs, + temporaryDirectory, +} = require('./main-services/test-context'); +const { + testUserDataMigration, +} = require('./main-services/test-migration'); + +try { + testUserDataMigration(); + console.log('legacy plan migration test passed'); +} finally { + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); +} diff --git a/scripts/test-main-ipc.js b/scripts/test-main-ipc.js new file mode 100644 index 0000000..b522f67 --- /dev/null +++ b/scripts/test-main-ipc.js @@ -0,0 +1,294 @@ +/** + * Electron 主进程 IPC 契约测试。 + * + * 测试流程: + * 1. 加载不依赖 Electron 运行时的 IPC Adapter。 + * 2. 使用内存注册器收集 handle 和 on 通道。 + * 3. 拒绝 Adapter 之间的重复通道注册。 + * 4. 从 UpdaterIpc 源码收集三个更新通道。 + * 5. 从 preload 源码收集 invoke 和 sendSync 通道。 + * 6. 验证所有 sendSync 通道仍由 ipcMain.on 注册。 + * 7. 验证所有启用的 invoke 通道仍由 ipcMain.handle 注册。 + * 8. 验证主进程没有 preload 未暴露的额外业务通道。 + * 9. 测试不启动 Electron,也不访问真实用户数据。 + */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { + registerBackendIpc, +} = require('../dist/electron/ipc/BackendIpc.js'); +const { + registerCombatPlanIpc, +} = require('../dist/electron/ipc/CombatPlanIpc.js'); +const { + registerConfigurationIpc, +} = require('../dist/electron/ipc/ConfigurationIpc.js'); +const { + registerDeviceIpc, +} = require('../dist/electron/ipc/DeviceIpc.js'); +const { + registerDailyPlanIpc, +} = require('../dist/electron/ipc/DailyPlanIpc.js'); +const { + registerEnvironmentIpc, +} = require('../dist/electron/ipc/EnvironmentIpc.js'); +const { + registerFileIpc, +} = require('../dist/electron/ipc/FileIpc.js'); +const { + registerMigrationConflictIpc, +} = require('../dist/electron/ipc/MigrationConflictIpc.js'); +const { + registerShipLibraryIpc, +} = require('../dist/electron/ipc/ShipLibraryIpc.js'); +const { + registerTeamPlanIpc, +} = require('../dist/electron/ipc/TeamPlanIpc.js'); + +class MemoryIpcRegistrar { + constructor() { + this.handles = new Map(); + this.listeners = new Map(); + } + + handle(channel, listener) { + assert.equal( + this.handles.has(channel) || this.listeners.has(channel), + false, + `重复 IPC 通道: ${channel}`, + ); + this.handles.set(channel, listener); + } + + on(channel, listener) { + assert.equal( + this.handles.has(channel) || this.listeners.has(channel), + false, + `重复 IPC 通道: ${channel}`, + ); + this.listeners.set(channel, listener); + return this; + } +} + +/** 从 TypeScript 源码提取指定调用的字符串通道。 */ +function sourceChannels(source, expression) { + const channels = []; + const pattern = new RegExp( + `${expression}\\(\\s*['"]([^'"]+)['"]`, + 'g', + ); + for (const match of source.matchAll(pattern)) { + channels.push(match[1]); + } + return channels; +} + +/** 返回排序后的集合差异。 */ +function difference(left, right) { + return [...left] + .filter(value => !right.has(value)) + .sort(); +} + +const ipc = new MemoryIpcRegistrar(); +registerFileIpc(ipc, {}); +registerMigrationConflictIpc(ipc, {}); +registerDeviceIpc(ipc, {}); +registerDailyPlanIpc(ipc, {}); +registerConfigurationIpc(ipc, {}); +registerEnvironmentIpc(ipc, {}); +registerTeamPlanIpc(ipc, {}); +registerCombatPlanIpc(ipc, {}); +registerShipLibraryIpc(ipc, {}); +registerBackendIpc(ipc, {}); + +const projectRoot = path.resolve(__dirname, '..'); +const preloadSource = fs.readFileSync( + path.join(projectRoot, 'electron', 'preload.ts'), + 'utf8', +); +const updaterSource = fs.readFileSync( + path.join(projectRoot, 'electron', 'ipc', 'UpdaterIpc.ts'), + 'utf8', +); + +const preloadInvoke = new Set(sourceChannels( + preloadSource, + 'ipcRenderer\\.invoke', +)); +const preloadSync = new Set(sourceChannels( + preloadSource, + 'ipcRenderer\\.sendSync', +)); +const updaterHandles = sourceChannels( + updaterSource, + 'ipc\\.handle', +); +const mainHandles = new Set([ + ...ipc.handles.keys(), + ...updaterHandles, +]); +const mainSync = new Set(ipc.listeners.keys()); + +assert.deepEqual( + difference(preloadSync, mainSync), + [], + 'preload 存在未注册的同步通道', +); +assert.deepEqual( + difference(mainSync, preloadSync), + [], + '主进程存在 preload 未暴露的同步通道', +); +assert.deepEqual( + difference(preloadInvoke, mainHandles), + [], + 'preload 异步通道与主进程注册不一致', +); +assert.deepEqual( + difference(mainHandles, preloadInvoke), + [], + '主进程存在 preload 未暴露的异步通道', +); + +/** 验证本地计划导入取消、冲突拒绝和确认覆盖流程。 */ +async function testLocalCombatPlanImport() { + const importIpc = new MemoryIpcRegistrar(); + const importCalls = []; + let selectionCanceled = true; + let confirmationResponse = 0; + registerCombatPlanIpc(importIpc, { + dialog: { + showOpenDialog: async () => ({ + canceled: selectionCanceled, + filePaths: selectionCanceled ? [] : ['C:\\plans\\legacy.yaml'], + }), + showMessageBox: async (options) => { + assert.equal(options.title, '覆盖用户配置'); + assert.equal(options.detail, '地图:bettle-legacy.yaml\n舰队:旧舰队'); + return { response: confirmationResponse }; + }, + }, + plans: { + importLocal: (filePath, overwrite) => { + importCalls.push([filePath, overwrite]); + return overwrite + ? { success: true, file: 'bettle-legacy.yaml' } + : { + success: false, + exists: true, + conflicts: [ + '地图:bettle-legacy.yaml', + '舰队:旧舰队', + ], + }; + }, + }, + }); + const importHandler = importIpc.handles.get( + 'import-local-combat-plan', + ); + + assert.deepEqual( + await importHandler({}), + { success: false, canceled: true }, + ); + assert.deepEqual(importCalls, []); + + selectionCanceled = false; + assert.deepEqual( + await importHandler({}), + { success: false, canceled: true }, + ); + assert.deepEqual(importCalls, [ + ['C:\\plans\\legacy.yaml', false], + ]); + + confirmationResponse = 1; + assert.deepEqual( + await importHandler({}), + { success: true, file: 'bettle-legacy.yaml' }, + ); + assert.deepEqual(importCalls, [ + ['C:\\plans\\legacy.yaml', false], + ['C:\\plans\\legacy.yaml', false], + ['C:\\plans\\legacy.yaml', true], + ]); +} + +/** 验证批量导出取消、默认文件名和 ZIP 写入流程。 */ +async function testUserPlanExport() { + const exportIpc = new MemoryIpcRegistrar(); + const createCalls = []; + const writeCalls = []; + let selectionCanceled = true; + registerCombatPlanIpc(exportIpc, { + dialog: { + showSaveDialog: async (options) => { + assert.equal(options.title, '批量导出用户配置'); + assert.match(options.defaultPath, /^\d{4}-\d{2}-\d{2}-plans\.zip$/); + assert.deepEqual(options.filters, [{ + name: 'ZIP 压缩包', + extensions: ['zip'], + }]); + return { + canceled: selectionCanceled, + filePath: selectionCanceled + ? undefined + : 'C:\\exports\\2026-08-04-plans.zip', + }; + }, + }, + planExports: { + createArchive: async selections => { + createCalls.push(selections); + return { content: Buffer.from('zip'), count: selections.length }; + }, + archiveFileName: () => '2026-08-04-plans.zip', + writeArchive: (filePath, archive) => { + writeCalls.push([filePath, archive.count]); + }, + }, + }); + const handler = exportIpc.handles.get('export-user-plans'); + const selections = [ + { kind: 'battle', file: 'bettle-test.yaml' }, + { kind: 'team', file: 'team-test.yaml' }, + ]; + + assert.deepEqual( + await handler({}, selections), + { success: false, canceled: true }, + ); + assert.deepEqual(writeCalls, []); + + selectionCanceled = false; + assert.deepEqual( + await handler({}, selections), + { + success: true, + path: 'C:\\exports\\2026-08-04-plans.zip', + count: 2, + }, + ); + assert.deepEqual(createCalls, [selections, selections]); + assert.deepEqual(writeCalls, [[ + 'C:\\exports\\2026-08-04-plans.zip', + 2, + ]]); +} + +async function main() { + await testLocalCombatPlanImport(); + await testUserPlanExport(); +} + +main() + .then(() => console.log('main IPC contract tests passed')) + .catch((error) => { + console.error(error); + process.exitCode = 1; + }); diff --git a/scripts/test-main-services.js b/scripts/test-main-services.js new file mode 100644 index 0000000..bf6782f --- /dev/null +++ b/scripts/test-main-services.js @@ -0,0 +1,43 @@ +/** + * Electron 主进程服务测试入口。 + * + * 各领域测试共享一个隔离临时目录并按固定顺序执行。 + * Electron 与真实用户数据不会被加载。 + */ +const { fs, temporaryDirectory } = require('./main-services/test-context'); +const { testAppPaths, testSecureFileService, testAtomicFileStore } = require('./main-services/test-path-and-file'); +const { testWindowService, testGuiSettingsStore, testGuiConfigurationService } = require('./main-services/test-configuration-and-window'); +const { testUserDataMigration } = require('./main-services/test-migration'); +const { testTeamPlanServices } = require('./main-services/test-team-plan'); +const { testCombatPlanServices } = require('./main-services/test-combat-plan'); +const { testPlanExportService } = require('./main-services/test-plan-export'); +const { testPythonEnvironmentService, testAdbService, testCudaEnvironmentService } = require('./main-services/test-environment-and-device'); +const { testShipLibraryService, testShipLibraryUpdater } = require('./main-services/test-ship-library'); +const { testUpdaterAndBackendShutdown } = require('./main-services/test-updater-and-shutdown'); + +async function main() { + testAppPaths(); + testSecureFileService(); + testWindowService(); + testUserDataMigration(); + testGuiSettingsStore(); + testGuiConfigurationService(); + await testPythonEnvironmentService(); + testAtomicFileStore(); + testTeamPlanServices(); + testCombatPlanServices(); + await testPlanExportService(); + await testAdbService(); + await testCudaEnvironmentService(); + testShipLibraryService(); + await testShipLibraryUpdater(); + await testUpdaterAndBackendShutdown(); + console.log('main services tests passed'); +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}).finally(() => { + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); +}); diff --git a/scripts/test-python-environment.js b/scripts/test-python-environment.js new file mode 100644 index 0000000..bc09830 --- /dev/null +++ b/scripts/test-python-environment.js @@ -0,0 +1,265 @@ +/** + * Python 环境一致性回归测试。 + * + * 测试流程: + * 1. 在系统临时目录创建隔离应用根目录。 + * 2. 创建最小 external AutoWSGR 仓库结构。 + * 3. 注入可切换的 Python 环境上下文。 + * 4. 验证 managed 模式使用 GUI site-packages。 + * 5. 验证 managed pip 参数包含 --target。 + * 6. 切换到 external 仓库和外部解释器。 + * 7. 验证 external pip 参数不包含 --target。 + * 8. 验证 external 安装目标是所选解释器自身环境。 + * 9. 验证 external 运行环境移除 GUI site-packages。 + * 10. 验证用户的其他 PYTHONPATH 条目仍然保留。 + * 11. 验证 CUDA 环境叠加后不会重新混入 GUI 包目录。 + * 12. 验证 external 仓库包目录会归一化到仓库根目录。 + * 13. 验证 external + 内置 Python 继续使用 GUI 包目录。 + * 14. 验证无效 external 仓库会明确失败。 + * 15. 测试结束后恢复环境变量并删除全部临时文件。 + */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { + initPythonEnv, +} = require('../dist/electron/pythonEnv/context.js'); +const { + buildPythonProcessEnv, + resolvePythonEnvironment, +} = require('../dist/electron/pythonEnv/environment.js'); +const { + buildBackendRuntimeEnvironment, + resolveConfiguredCudaRoot, +} = require('../dist/electron/pythonEnv/cuda.js'); +const { + buildDependencyInstallPlan, +} = require('../dist/electron/pythonEnv/installer.js'); +const { + MANAGED_AUTOWSGR_COMMIT, + MANAGED_AUTOWSGR_REQUIREMENT, +} = require('../dist/electron/pythonEnv/backendRequirement.js'); +const { + buildManagedAutowsgrUpdateArgs, +} = require('../dist/electron/pythonEnv/updater.js'); +const { + buildBackendRuntimeContractProbeLines, +} = require('../dist/electron/pythonEnv/backendContractProbe.js'); +const { + applyBackendRuntimeSettings, + buildBackendBootstrap, + buildBackendCapabilityProbe, + selectBackendOcrGpuMode, +} = require( + '../dist/electron/services/BackendRuntimeContract.js', +); + +const temporaryDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'autowsgr-python-environment-'), +); +const appRoot = path.join(temporaryDirectory, 'app'); +const repoRoot = path.join(temporaryDirectory, 'external-repo'); +const packageRoot = path.join(repoRoot, 'autowsgr'); +const serverRoot = path.join(packageRoot, 'server'); +const localPython = path.join(appRoot, 'python', 'python.exe'); +const externalPython = path.join( + temporaryDirectory, + 'external-venv', + 'Scripts', + 'python.exe', +); +const localSite = path.join(appRoot, 'python', 'site-packages'); +const externalPackages = path.join( + temporaryDirectory, + 'external-venv', + 'Lib', + 'site-packages', +); +const state = { + mode: 'managed', + repoPath: repoRoot, +}; +const previousRepoOverride = process.env.AUTOWSGR_BACKEND_REPO; + +fs.mkdirSync(serverRoot, { recursive: true }); +fs.mkdirSync(path.dirname(localPython), { recursive: true }); +fs.mkdirSync(path.dirname(externalPython), { recursive: true }); +fs.writeFileSync(path.join(packageRoot, '__init__.py'), '', 'utf8'); +fs.writeFileSync(path.join(serverRoot, 'main.py'), '', 'utf8'); + +initPythonEnv({ + appRoot: () => appRoot, + sendProgress: () => {}, + getConfiguredPythonPath: () => null, + getUpdateMode: () => 'manual', + getBackendStartupMode: () => state.mode, + getBackendRepoPath: () => state.repoPath, + getTempDir: () => temporaryDirectory, +}); + +try { + delete process.env.AUTOWSGR_BACKEND_REPO; + + const managed = resolvePythonEnvironment(localPython); + assert.equal(managed.startupMode, 'managed'); + assert.equal(managed.backendRoot, null); + assert.equal(managed.useLocalSite, true); + assert.equal(managed.installTarget, localSite); + const managedPlan = buildDependencyInstallPlan(managed, 'autowsgr'); + assert.equal(managedPlan.backendArgs.includes('--target'), true); + assert.equal(managedPlan.backendArgs.includes(localSite), true); + assert.equal(managedPlan.toolArgs.includes('--target'), true); + assert.equal(managedPlan.toolArgs.includes(localSite), true); + assert.equal( + managedPlan.toolArgs.includes('requests>=2.32.5'), + true, + ); + assert.equal( + managedPlan.toolArgs.includes('beautifulsoup4>=4.12.0'), + true, + ); + assert.equal( + MANAGED_AUTOWSGR_COMMIT, + 'b0f473fb1ec5318c2c4cff4795a804a3d2dd25bd', + ); + assert.equal( + MANAGED_AUTOWSGR_REQUIREMENT.endsWith( + `${MANAGED_AUTOWSGR_COMMIT}.zip`, + ), + true, + ); + const managedUpdateArgs = buildManagedAutowsgrUpdateArgs(localSite); + assert.equal(managedUpdateArgs.at(-1), MANAGED_AUTOWSGR_REQUIREMENT); + assert.equal(managedUpdateArgs.includes('autowsgr'), false); + assert.equal(managedUpdateArgs.includes(localSite), true); + const contractProbeLines = buildBackendRuntimeContractProbeLines(); + assert.equal( + contractProbeLines.some( + line => line.includes('unittest.mock'), + ), + true, + ); + assert.equal( + contractProbeLines.some( + line => line.includes("_save_values != [True, False]"), + ), + true, + ); + assert.equal( + contractProbeLines.some( + line => line.includes("_ocr_values != [True, False]"), + ), + true, + ); + + state.mode = 'external'; + const external = resolvePythonEnvironment(externalPython); + assert.equal(external.backendRoot, repoRoot); + assert.equal(external.useLocalSite, false); + assert.equal(external.installTarget, null); + assert.notEqual(external.identity, managed.identity); + + const externalPlan = buildDependencyInstallPlan(external, repoRoot); + assert.equal(externalPlan.buildArgs.includes('--target'), false); + assert.equal(externalPlan.toolArgs.includes('--target'), false); + assert.equal(externalPlan.backendArgs.includes('--target'), false); + assert.equal(externalPlan.backendArgs.at(-1), repoRoot); + + const baseEnv = { + PATH: 'base-path', + PYTHONPATH: [localSite, externalPackages].join(path.delimiter), + PYTHONUSERBASE: path.join(appRoot, 'python'), + }; + const installEnv = buildPythonProcessEnv(external, baseEnv); + assert.equal(installEnv.PYTHONPATH, externalPackages); + assert.equal(installEnv.PYTHONUSERBASE, undefined); + + const runtimeEnv = buildBackendRuntimeEnvironment( + external, + null, + baseEnv, + ); + assert.equal(runtimeEnv.PYTHONPATH, externalPackages); + assert.equal(runtimeEnv.PYTHONUSERBASE, undefined); + + const cudaRoot = path.join(temporaryDirectory, 'cuda', 'v12.8'); + const cudaBin = path.join(cudaRoot, 'bin'); + fs.mkdirSync(cudaBin, { recursive: true }); + fs.writeFileSync(path.join(cudaBin, 'nvcc.exe'), '', 'utf8'); + fs.writeFileSync( + path.join(cudaRoot, 'version.json'), + JSON.stringify({ cuda: { version: '12.8.0' } }), + 'utf8', + ); + const configuredCudaRoot = resolveConfiguredCudaRoot(cudaRoot); + assert.equal(configuredCudaRoot, cudaRoot); + const cudaRuntimeEnv = buildBackendRuntimeEnvironment( + external, + configuredCudaRoot, + baseEnv, + ); + assert.equal(cudaRuntimeEnv.PYTHONPATH, externalPackages); + assert.equal( + cudaRuntimeEnv.PATH.split(path.delimiter)[0], + cudaBin, + ); + + state.repoPath = packageRoot; + const packageDirectoryInput = resolvePythonEnvironment(externalPython); + assert.equal(packageDirectoryInput.backendRoot, repoRoot); + + state.repoPath = repoRoot; + const externalWithLocalPython = resolvePythonEnvironment(localPython); + assert.equal(externalWithLocalPython.useLocalSite, true); + assert.equal(externalWithLocalPython.installTarget, localSite); + + const settingsEnv = applyBackendRuntimeSettings( + cudaRuntimeEnv, + { + ocrGpuMode: 'cuda', + saveImages: true, + }, + ); + assert.equal(settingsEnv.AUTOWSGR_OCR_GPU_MODE, 'cuda'); + assert.equal(settingsEnv.AUTOWSGR_SAVE_IMAGES, 'true'); + assert.equal(selectBackendOcrGpuMode('auto', true), 'cuda'); + assert.equal(selectBackendOcrGpuMode('auto', false), 'cpu'); + assert.equal(selectBackendOcrGpuMode('cpu', true), 'cpu'); + assert.throws( + () => selectBackendOcrGpuMode('cuda', false), + /未检测到可用 CUDA/, + ); + + const capabilityProbe = buildBackendCapabilityProbe(external); + assert.match(capabilityProbe, /AUTOWSGR_SAVE_IMAGES/); + assert.match(capabilityProbe, /AUTOWSGR_OCR_GPU_MODE/); + assert.match(capabilityProbe, /autowsgr\.server\.main/); + assert.match(capabilityProbe, /GUI 后端来源错误/); + assert.match(capabilityProbe, /gui-runtime-env-v1/); + assert.match(capabilityProbe, /unittest\.mock/); + assert.match(capabilityProbe, /iscoroutinefunction/); + + const bootstrap = buildBackendBootstrap(external, 16710); + assert.match(bootstrap, /AUTOWSGR_SAVE_IMAGES/); + assert.match(bootstrap, /AUTOWSGR_OCR_GPU_MODE/); + assert.match(bootstrap, /port=16710/); + assert.doesNotMatch(bootstrap, /__func__/); + assert.doesNotMatch(bootstrap, /_image_dir/); + assert.doesNotMatch(bootstrap, /_patched_/); + + state.repoPath = path.join(temporaryDirectory, 'missing-repo'); + assert.throws( + () => resolvePythonEnvironment(externalPython), + /external 后端仓库路径不存在/, + ); + + console.log('python environment tests passed'); +} finally { + if (previousRepoOverride === undefined) { + delete process.env.AUTOWSGR_BACKEND_REPO; + } else { + process.env.AUTOWSGR_BACKEND_REPO = previousRepoOverride; + } + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); +} diff --git a/scripts/test-real-plan-import.js b/scripts/test-real-plan-import.js new file mode 100644 index 0000000..c4f9ace --- /dev/null +++ b/scripts/test-real-plan-import.js @@ -0,0 +1,380 @@ +/** + * 使用主库和旧 GUI 的真实 YAML 验证本地导入能力。 + * + * 默认只写临时目录;传入 --real 后会先备份,再写入真实 Electron userData。 + */ +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const yaml = require('js-yaml'); +const { + AppPaths, +} = require('../dist/electron/services/AppPaths.js'); +const { + AtomicFileStore, +} = require('../dist/electron/services/AtomicFileStore.js'); +const { + CombatPlanCodec, +} = require('../dist/electron/services/CombatPlanCodec.js'); +const { + CombatPlanRepository, +} = require('../dist/electron/services/CombatPlanRepository.js'); +const { + GuiSettingsStore, +} = require('../dist/electron/services/GuiSettingsStore.js'); +const { + PlanExportService, +} = require('../dist/electron/services/PlanExportService.js'); +const { + PlanManagementService, +} = require('../dist/electron/services/PlanManagementService.js'); +const { + RuntimePlanService, +} = require('../dist/electron/services/RuntimePlanService.js'); +const { + TaskPresetCodec, +} = require('../dist/electron/services/TaskPresetCodec.js'); +const { + TeamPlanCodec, +} = require('../dist/electron/services/TeamPlanCodec.js'); +const { + TeamPlanRepository, +} = require('../dist/electron/services/TeamPlanRepository.js'); + +const projectRoot = path.resolve(__dirname, '..'); +const workspaceRoot = path.resolve(projectRoot, '..'); +const corpusRoots = [ + { + group: '主库计划', + directory: path.join( + workspaceRoot, + 'AutoWSGR', + 'autowsgr', + 'data', + 'plan', + ), + }, + { + group: '旧GUI用户计划', + directory: path.join(workspaceRoot, 'AutoWSGR-GUI-old', 'plans'), + }, + { + group: '旧GUI内置计划', + directory: path.join( + workspaceRoot, + 'AutoWSGR-GUI-old', + 'resources', + 'resource', + 'builtin_plans', + ), + }, + { + group: '旧GUI内置后端计划', + directory: path.join( + workspaceRoot, + 'AutoWSGR-GUI-old', + 'python', + 'site-packages', + 'autowsgr', + 'data', + 'plan', + ), + }, +]; + +function yamlFiles(directory) { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const candidate = path.join(directory, entry.name); + if (entry.isDirectory()) return yamlFiles(candidate); + return /\.ya?ml$/i.test(entry.name) ? [candidate] : []; + }); +} + +function corpus() { + return corpusRoots.flatMap(root => yamlFiles(root.directory).map(file => ({ + group: root.group, + file, + relativePath: path.relative(root.directory, file), + }))); +} + +function createServices(userData, tempRoot) { + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR-GUI.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const atomicFiles = new AtomicFileStore(); + const teamCodec = new TeamPlanCodec(); + const teamRepository = new TeamPlanRepository( + appPaths, + atomicFiles, + teamCodec, + ); + const combatRepository = new CombatPlanRepository(appPaths, atomicFiles); + const combatCodec = new CombatPlanCodec(teamCodec, teamRepository); + const runtimePlans = new RuntimePlanService( + combatCodec, + combatRepository, + atomicFiles, + { + getTempDirectory: () => tempRoot, + processId: process.pid, + }, + ); + const management = new PlanManagementService( + combatCodec, + combatRepository, + runtimePlans, + teamRepository, + new GuiSettingsStore(() => path.join(userData, 'gui_settings.json')), + new TaskPresetCodec(), + ); + return { + appPaths, + combatRepository, + management, + teamRepository, + exports: new PlanExportService(combatRepository, teamRepository), + }; +} + +function hashFile(file) { + return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +} + +function isObject(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +function fieldDifferences(source, output, currentPath = '$') { + const missing = []; + const normalized = []; + if (Array.isArray(source)) { + if (!Array.isArray(output)) { + missing.push(currentPath); + return { missing, normalized }; + } + source.forEach((value, index) => { + if (typeof value !== 'object' || value === null) return; + const result = fieldDifferences( + value, + output[index], + `${currentPath}[${index}]`, + ); + missing.push(...result.missing); + normalized.push(...result.normalized); + }); + return { missing, normalized }; + } + if (!isObject(source)) return { missing, normalized }; + if (!isObject(output)) { + missing.push(currentPath); + return { missing, normalized }; + } + + Object.entries(source).forEach(([key, value]) => { + const fieldPath = `${currentPath}.${key}`; + if (key === 'priority' && !Object.hasOwn(output, key)) { + normalized.push(`${fieldPath} -> name/candidates`); + return; + } + if (!Object.hasOwn(output, key)) { + missing.push(fieldPath); + return; + } + const result = fieldDifferences(value, output[key], fieldPath); + missing.push(...result.missing); + normalized.push(...result.normalized); + }); + return { missing, normalized }; +} + +function validateOne(entry, index) { + const root = fs.mkdtempSync( + path.join(os.tmpdir(), `autowsgr-real-plan-${index}-`), + ); + try { + const services = createServices( + path.join(root, 'userData'), + path.join(root, 'temp'), + ); + const sourceHash = hashFile(entry.file); + const imported = services.management.importLocal(entry.file); + if (!imported.success) { + throw new Error(imported.error || '导入失败'); + } + const managed = services.management.readManaged('user', imported.file); + if (!managed.success || typeof managed.content !== 'string') { + throw new Error(managed.error || '受管读取失败'); + } + const sourceRoot = yaml.load(fs.readFileSync(entry.file, 'utf8')); + const managedRoot = yaml.load(managed.content); + const differences = fieldDifferences(sourceRoot, managedRoot); + return { + ...entry, + kind: imported.kind, + importedFile: imported.file, + teamFiles: imported.teamFiles ?? [], + sourceUnchanged: hashFile(entry.file) === sourceHash, + missingFields: [...new Set(differences.missing)], + normalizedFields: [...new Set(differences.normalized)], + }; + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +function backupRealPlans(userData) { + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupRoot = path.join(userData, 'import_backups', stamp); + for (const directory of ['user_battle_plans', 'user_team_plans']) { + const source = path.join(userData, directory); + if (fs.existsSync(source)) { + fs.cpSync(source, path.join(backupRoot, directory), { + recursive: true, + }); + } + } + return backupRoot; +} + +function fileNames(directory) { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory) + .filter(file => /\.ya?ml$/i.test(file)) + .sort((left, right) => left.localeCompare(right, 'zh-CN')); +} + +async function importReal(entries, validationRecords, userData) { + const backupRoot = backupRealPlans(userData); + const services = createServices( + userData, + path.join(os.tmpdir(), 'AutoWSGR-GUI-real-import'), + ); + const writes = []; + for (const entry of entries) { + const result = services.management.importLocal(entry.file, true); + if (!result.success) { + throw new Error(`${entry.group}/${entry.relativePath}: ${result.error}`); + } + writes.push({ + group: entry.group, + source: entry.relativePath, + kind: result.kind, + importedFile: result.file, + teamFiles: result.teamFiles ?? [], + }); + } + + const battleFiles = fileNames(services.appPaths.userBattlePlansDir()); + const teamFiles = fileNames(services.appPaths.userTeamPlansDir()); + const selections = [ + ...battleFiles.map(file => ({ kind: 'battle', file })), + ...teamFiles.map(file => ({ kind: 'team', file })), + ]; + const archive = await services.exports.createArchive(selections); + const exportDirectory = path.join(userData, 'exports'); + fs.mkdirSync(exportDirectory, { recursive: true }); + const exportPath = path.join( + exportDirectory, + `real-plan-corpus-${services.exports.archiveFileName()}`, + ); + services.exports.writeArchive(exportPath, archive); + + const report = { + generatedAt: new Date().toISOString(), + userData, + backupRoot, + exportPath, + corpusCount: entries.length, + importedCount: writes.length, + finalBattleFileCount: battleFiles.length, + finalTeamFileCount: teamFiles.length, + overwrittenBattleNames: duplicateNames( + writes.map(item => item.importedFile), + ), + overwrittenTeamNames: duplicateNames( + writes.flatMap(item => item.teamFiles), + ), + battleFiles, + teamFiles, + writes, + validationRecords, + }; + const reportPath = path.join(userData, 'real-plan-import-report.json'); + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + return { reportPath, ...report }; +} + +function duplicateNames(names) { + const counts = new Map(); + names.forEach(name => counts.set(name, (counts.get(name) ?? 0) + 1)); + return [...counts.entries()] + .filter(([, count]) => count > 1) + .map(([name, count]) => ({ name, count })); +} + +async function main() { + const entries = corpus(); + const records = []; + const failures = []; + entries.forEach((entry, index) => { + try { + records.push(validateOne(entry, index)); + } catch (error) { + failures.push({ + group: entry.group, + source: entry.relativePath, + error: error instanceof Error ? error.message : String(error), + }); + } + }); + const summary = { + corpus: Object.fromEntries(corpusRoots.map(root => [ + root.group, + entries.filter(entry => entry.group === root.group).length, + ])), + total: entries.length, + passed: records.length, + failed: failures.length, + sourceChanged: records.filter(item => !item.sourceUnchanged).length, + filesWithMissingFields: records.filter( + item => item.missingFields.length > 0, + ).length, + failures, + }; + if (failures.length > 0) { + console.log(JSON.stringify(summary, null, 2)); + process.exitCode = 1; + return; + } + + const real = process.argv.includes('--real') + ? await importReal( + entries, + records, + path.join(process.env.APPDATA, 'wsgrgui'), + ) + : null; + console.log(JSON.stringify({ + ...summary, + real: real && { + reportPath: real.reportPath, + backupRoot: real.backupRoot, + exportPath: real.exportPath, + importedCount: real.importedCount, + finalBattleFileCount: real.finalBattleFileCount, + finalTeamFileCount: real.finalTeamFileCount, + overwrittenBattleNameCount: real.overwrittenBattleNames.length, + overwrittenTeamNameCount: real.overwrittenTeamNames.length, + }, + }, null, 2)); +} + +void main(); diff --git a/scripts/test-scheduler-domain.js b/scripts/test-scheduler-domain.js new file mode 100644 index 0000000..fb4b4eb --- /dev/null +++ b/scripts/test-scheduler-domain.js @@ -0,0 +1,880 @@ +import assert from 'node:assert/strict'; +import esbuild from 'esbuild'; + +const entries = [ + 'src/model/scheduler/SchedulerTaskPolicy.ts', + 'src/model/scheduler/SchedulerRepairPolicy.ts', + 'src/model/scheduler/CronScheduler.ts', + 'src/controller/app/AutomaticDecisiveTask.ts', + 'src/controller/app/SchedulerBinder.ts', + 'src/model/scheduler/RepairManager.ts', + 'src/model/scheduler/Scheduler.ts', +]; +const modules = await Promise.all(entries.map(async entry => { + const result = await esbuild.build({ + entryPoints: [entry], + bundle: true, + platform: 'node', + format: 'esm', + write: false, + loader: { '.json': 'json' }, + }); + return import(`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`); +})); +const taskPolicy = modules[0]; +const repairPolicy = modules[1]; +const cronModule = modules[2]; +const automaticDecisive = modules[3]; +const schedulerBinderModule = modules[4]; +const repairModule = modules[5]; +const schedulerModule = modules[6]; + +const task = taskPolicy.createSchedulerTask({ + id: 'task-1', name: 'test', type: 'normal_fight', request: { type: 'normal_fight' }, + priority: 10, times: 3, sortKey: 2, +}); +const lower = { ...task, id: 'lower', priority: 0 }; +const same = { ...task, id: 'same', sortKey: 3 }; +assert.equal(taskPolicy.findPriorityInsertionIndex([lower, same], task), 1); +assert.equal(taskPolicy.findPriorityInsertionIndex([lower, same], task, true), 1); +const followUp = taskPolicy.buildFollowUpTask(task, 2, 'task-2'); +assert.equal(followUp.logicalId, task.logicalId); +assert.equal(followUp.remainingTimes, 2); +assert.equal(followUp.retryCount, 0); +assert.equal(repairPolicy.calculateRepairWaitMs(new Map([['a', { repairEndTime: 110_000 }]]), 100_000), 15_000); +assert.equal(repairPolicy.calculateRepairWaitMs(new Map([['a', { repairEndTime: 0 }]]), 100_000), -1); + +const values = new Map(); +const storage = { + get: key => values.get(key) ?? null, + set: (key, value) => values.set(key, value), + remove: key => values.delete(key), +}; +const cronConfig = { + autoExercise: false, exerciseFleetId: 1, autoBattle: false, battleType: '1-1', battleTimes: 1, + autoNormalFight: false, + autoDecisive: false, decisiveTemplateId: 'system_preset', + autoLoot: false, + lootPlanSource: 'system', + lootPlanId: 'bettle-周常-9-2.yaml', lootStopCount: 0, +}; +const cron = new cronModule.CronScheduler(cronConfig, storage); +cron.markBattleCompleted(); +const restoredCron = new cronModule.CronScheduler(cronConfig, storage); +restoredCron.start(); +restoredCron.stop(); +assert.ok(values.has('cron_lastBattleRun')); + +const decisiveTriggers = []; +const decisiveCron = new cronModule.CronScheduler({ + ...cronConfig, + autoDecisive: true, +}, storage); +decisiveCron.setCallbacks({ + onDecisiveDue: templateId => decisiveTriggers.push(templateId), +}); +decisiveCron.start(); +decisiveCron.stop(); +assert.deepEqual(decisiveTriggers, ['system_preset']); +decisiveCron.start(); +decisiveCron.stop(); +assert.equal( + decisiveTriggers.length, + 1, + '同一天存在 pending 时不得重复触发自动决战', +); +decisiveCron.clearDecisivePending(); +decisiveCron.start(); +decisiveCron.stop(); +assert.equal( + decisiveTriggers.length, + 2, + '任务入队前失败并清除 pending 后应允许重试', +); +decisiveCron.markDecisiveHandled(); +assert.ok(values.has('cron_lastDecisiveRun')); + +let restoredDecisiveTriggers = 0; +const restoredDecisiveCron = new cronModule.CronScheduler({ + ...cronConfig, + autoDecisive: true, +}, storage); +restoredDecisiveCron.setCallbacks({ + onDecisiveDue: () => { + restoredDecisiveTriggers += 1; + }, +}); +restoredDecisiveCron.start(); +restoredDecisiveCron.stop(); +assert.equal( + restoredDecisiveTriggers, + 0, + '自动决战已处理日期必须跨实例持久化', +); + +const automationValues = new Map(); +const automationStorage = { + get: key => automationValues.get(key) ?? null, + set: (key, value) => automationValues.set(key, value), + remove: key => automationValues.delete(key), +}; +const automationTriggers = { + exercise: [], + campaign: [], + normalFight: 0, + decisive: [], + loot: [], +}; +const automationCron = new cronModule.CronScheduler({ + ...cronConfig, + autoExercise: true, + exerciseFleetId: 3, + autoBattle: true, + battleType: '困难航母', + battleTimes: 4, + autoNormalFight: true, + autoDecisive: true, + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: 'bettle-old-9-2ADGHM速刷胖次.yaml', + lootStopCount: 21, +}, automationStorage); +automationCron.setCallbacks({ + onExerciseDue: fleetId => automationTriggers.exercise.push(fleetId), + onCampaignDue: (name, times) => { + automationTriggers.campaign.push([name, times]); + }, + onNormalFightDue: () => { + automationTriggers.normalFight += 1; + }, + onDecisiveDue: templateId => { + automationTriggers.decisive.push(templateId); + }, + onLootDue: (source, planId, stopCount) => { + automationTriggers.loot.push([source, planId, stopCount]); + }, +}); +automationCron.start(); +automationCron.stop(); +assert.deepEqual(automationTriggers, { + exercise: [3], + campaign: [['困难航母', 4]], + normalFight: 1, + decisive: ['system_preset'], + loot: [[ + 'user', + 'bettle-old-9-2ADGHM速刷胖次.yaml', + 21, + ]], +}); +automationCron.markExerciseCompleted(); +automationCron.markBattleHandled(); +automationCron.markNormalFightHandled(); +automationCron.markDecisiveHandled(); +automationCron.markLootHandled(); + +const restoredAutomationTriggers = []; +const restoredAutomationCron = new cronModule.CronScheduler({ + ...cronConfig, + autoExercise: true, + autoBattle: true, + autoNormalFight: true, + autoDecisive: true, + autoLoot: true, +}, automationStorage); +restoredAutomationCron.setCallbacks({ + onExerciseDue: () => restoredAutomationTriggers.push('exercise'), + onCampaignDue: () => restoredAutomationTriggers.push('campaign'), + onNormalFightDue: () => restoredAutomationTriggers.push('normal'), + onDecisiveDue: () => restoredAutomationTriggers.push('decisive'), + onLootDue: () => restoredAutomationTriggers.push('loot'), +}); +restoredAutomationCron.start(); +restoredAutomationCron.stop(); +assert.deepEqual( + restoredAutomationTriggers, + [], + '已处理的日常自动任务不得在同一天跨实例重复触发', +); + +const userDecisiveRequest = + automaticDecisive.buildAutomaticDecisivePlanRequest( + { + chapter: 5, + useQuickRepair: false, + level1: [' 当前主力 ', ''], + level2: ['当前替补'], + }, + ); +assert.deepEqual(userDecisiveRequest, { + type: 'decisive', + chapter: 5, + decisive_rounds: 1, + use_quick_repair: false, + level1: ['当前主力'], + level2: ['当前替补'], +}); + +const presetDecisiveRequest = + automaticDecisive.buildAutomaticDecisivePresetRequest( + { + id: 'custom-decisive', + name: '自定义决战', + type: 'decisive', + chapter: 4, + level1: ['模板主力'], + level2: ['模板替补'], + flagship_priority: ['旗舰优先'], + use_quick_repair: true, + }, + ); +assert.deepEqual(presetDecisiveRequest, { + type: 'decisive', + chapter: 4, + decisive_rounds: 1, + use_quick_repair: true, + level1: ['模板主力'], + level2: ['模板替补'], + flagship_priority: ['旗舰优先'], +}); + +const repairData = JSON.stringify([{ key: 'ship', name: 'Ship', startTime: Date.now(), repairEndTime: Date.now() + 60_000, requestSent: true }]); +storage.set('autowsgr_bathing_ships', repairData); +const repair = new repairModule.RepairManager({}, storage); +assert.equal(repair.getBathingShips().size, 1); +assert.equal(repair.getBathingShips().get('ship').name, 'Ship'); + +globalThis.localStorage = { + getItem: key => values.get(key) ?? null, + setItem: (key, value) => values.set(key, value), + removeItem: key => values.delete(key), +}; + +let cronCallbacks = {}; +let schedulerCallbacks = {}; +let decisiveTemplate = { + id: 'builtin_decisive_6', + name: '决战', + type: 'decisive', + chapter: 6, + level1: ['系统主力'], + level2: ['系统替补'], + use_quick_repair: true, +}; +const decisiveQueueCalls = []; +let decisivePlanReads = 0; +let decisiveStartCalls = 0; +let decisivePendingClears = 0; +let decisiveHandledCalls = 0; +let normalFightPendingClears = 0; +let normalFightHandledCalls = 0; +let lootPendingClears = 0; +let lootHandledCalls = 0; +let binderRunningTask = null; +const managedPlanReads = []; +let managedPlanWrites = 0; +const managedLootYaml = [ + 'chapter: 9', + 'map: 2', + 'selected_nodes: [A]', + 'fleet_id: 3', + 'gap: 4', + 'stop_condition:', + ' loot_count_ge: 48', + ' ship_count_ge: 7', + '', +].join('\n'); +globalThis.window = { + electronBridge: { + getDecisivePlanSettings: async () => { + decisivePlanReads += 1; + return { + chapter: 5, + useQuickRepair: false, + level1: ['当前主力'], + level2: ['当前替补'], + }; + }, + readManagedCombatPlan: async (source, file) => { + managedPlanReads.push([source, file]); + return { + success: true, + path: `managed://${source}/${file}`, + runtimePath: `runtime://${source}/${file}`, + content: managedLootYaml, + }; + }, + saveFile: async () => { + managedPlanWrites += 1; + }, + }, +}; +const binder = new schedulerBinderModule.SchedulerBinder({ + scheduler: { + setCallbacks: callbacks => { + schedulerCallbacks = callbacks; + }, + addTask: (...args) => { + decisiveQueueCalls.push(args); + return 'automatic-decisive-task'; + }, + startConsuming: () => { + decisiveStartCalls += 1; + }, + get currentRunningTask() { + return binderRunningTask; + }, + taskQueue: [], + waitingTaskList: [], + }, + cronScheduler: { + setCallbacks: callbacks => { + cronCallbacks = callbacks; + }, + clearDecisivePending: () => { + decisivePendingClears += 1; + }, + markDecisiveHandled: () => { + decisiveHandledCalls += 1; + }, + clearNormalFightPending: () => { + normalFightPendingClears += 1; + }, + markNormalFightHandled: () => { + normalFightHandledCalls += 1; + }, + clearLootPending: () => { + lootPendingClears += 1; + }, + markLootHandled: () => { + lootHandledCalls += 1; + }, + }, + api: {}, + templateModel: { + get: () => decisiveTemplate, + }, + configModel: { + current: { + daily_automation: { + normal_fight_tasks: [], + }, + }, + }, + renderMain: () => {}, + updateOpsAvailability: () => {}, +}); +binder.bindCronCallbacks(); +binder.bindSchedulerCallbacks(); +cronCallbacks.onDecisiveDue('user_plan'); +await new Promise(resolve => setTimeout(resolve, 0)); +assert.equal(decisiveQueueCalls.length, 1); +assert.deepEqual(decisiveQueueCalls[0].slice(0, 6), [ + '自动决战·用户计划', + 'decisive', + { + type: 'decisive', + chapter: 5, + decisive_rounds: 1, + use_quick_repair: false, + level1: ['当前主力'], + level2: ['当前替补'], + }, + 20, + 1, +]); +assert.equal(decisivePlanReads, 1); +assert.equal(decisiveStartCalls, 1); +schedulerCallbacks.onLogicalTaskCompleted( + 'automatic-decisive-task', + false, +); +assert.equal( + decisiveHandledCalls, + 1, + '自动决战实际任务结束后,无论结果均应标记当天已处理', +); + +cronCallbacks.onDecisiveDue('system_preset'); +await new Promise(resolve => setTimeout(resolve, 0)); +assert.deepEqual(decisiveQueueCalls[1].slice(0, 6), [ + '自动决战·系统预设', + 'decisive', + { + type: 'decisive', + chapter: 6, + decisive_rounds: 1, + use_quick_repair: true, + level1: ['系统主力'], + level2: ['系统替补'], + }, + 20, + 1, +]); +assert.equal( + decisivePlanReads, + 1, + '系统预设不得读取或混入用户决战计划', +); +schedulerCallbacks.onLogicalTaskCanceled( + 'automatic-decisive-task', + 'queue_cleared', +); +assert.equal( + decisiveHandledCalls, + 2, + '用户清空自动任务后应结算父任务,避免同日立即重复加入', +); + +cronCallbacks.onDecisiveDue('system_preset'); +await new Promise(resolve => setTimeout(resolve, 0)); +schedulerCallbacks.onLogicalTaskCanceled( + 'automatic-decisive-task', + 'system_stopped', +); +assert.equal( + decisivePendingClears, + 1, + '系统停止只释放 pending,重启后仍应允许自动任务再次触发', +); + +decisiveTemplate = undefined; +cronCallbacks.onDecisiveDue('system_preset'); +await new Promise(resolve => setTimeout(resolve, 0)); +assert.equal(decisiveQueueCalls.length, 3); +assert.equal( + decisivePendingClears, + 2, + '自动决战入队前失败必须清除 pending', +); + +cronCallbacks.onLootDue( + 'user', + 'bettle-用户胖次测试.yaml', + 23, +); +await new Promise(resolve => setTimeout(resolve, 0)); +assert.deepEqual(managedPlanReads, [[ + 'user', + 'bettle-用户胖次测试.yaml', +]]); +assert.equal( + managedPlanWrites, + 0, + '自动胖次只能读取受管 YAML,不得修改原文件', +); +assert.equal(decisiveQueueCalls.length, 4); +const automaticLootCall = decisiveQueueCalls[3]; +assert.deepEqual(automaticLootCall.slice(0, 6), [ + '自动刷胖次·9-2', + 'normal_fight', + { + type: 'normal_fight', + plan_id: 'runtime://user/bettle-用户胖次测试.yaml', + times: 1, + gap: 4, + plan: { + selected_nodes: ['A', '0'], + fleet_id: 3, + }, + }, + 20, + 99, + { loot_count_ge: 23 }, +]); +assert.equal( + Object.hasOwn(automaticLootCall[5], 'ship_count_ge'), + false, + '自动胖次默认不得启用舰船掉落停止检测', +); +schedulerCallbacks.onLogicalTaskCompleted( + 'automatic-decisive-task', + true, +); +assert.equal(lootHandledCalls, 1); +assert.equal(lootPendingClears, 0); + +binder.pendingNormalFightTaskIds.add('normal-parent-cleared'); +schedulerCallbacks.onLogicalTaskCanceled( + 'normal-parent-cleared', + 'queue_cleared', +); +assert.equal( + normalFightHandledCalls, + 1, + '用户清空无限自动出征后,cron 不得保留 pending 或立即重新加入', +); + +binder.pendingNormalFightTaskIds.add('normal-parent-system-stop'); +schedulerCallbacks.onLogicalTaskCanceled( + 'normal-parent-system-stop', + 'system_stopped', +); +assert.equal( + normalFightPendingClears, + 1, + '系统停止无限自动出征后应释放 cron pending,允许重启后重试', +); +binder.currentProgress = '2/5'; +binderRunningTask = { + id: 'running-round', + logicalId: 'running-parent', +}; +schedulerCallbacks.onLogicalTaskCanceled( + 'other-queued-parent', + 'removed', +); +assert.equal( + binder.currentProgress, + '2/5', + '取消其他排队任务不得清空当前运行任务的进度', +); +binderRunningTask = null; +clearTimeout(binder.dailyStatsRefreshTimer); + +function createSchedulerApi(overrides = {}) { + let callbacks = {}; + return { + setCallbacks(next) { + callbacks = next; + }, + get callbacks() { + return callbacks; + }, + systemStart: async () => ({ success: true }), + systemStop: async () => ({ success: true }), + connectWebSockets() {}, + disconnectWebSockets() {}, + expeditionCheck: async () => ({ success: true }), + taskStart: async () => ({ + success: true, + data: { task_id: 'backend-task-1', status: 'running' }, + }), + taskStop: async () => ({ success: true }), + taskStatus: async () => ({ + success: true, + data: { + task_id: 'backend-task-1', + status: 'running', + progress: null, + result: null, + }, + }), + ...overrides, + }; +} + +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + +// 无限任务每轮使用新物理 ID,但父 logicalId 必须稳定。 +let gapTaskStarts = 0; +const gapApi = createSchedulerApi({ + taskStart: async () => { + gapTaskStarts += 1; + return { + success: true, + data: { + task_id: `gap-backend-${gapTaskStarts}`, + status: 'running', + }, + }; + }, +}); +const gapScheduler = new schedulerModule.Scheduler(gapApi); +gapScheduler.setAutoExpedition(false); +const gapRoundEvents = []; +const gapLogicalEvents = []; +const gapCanceledEvents = []; +gapScheduler.setCallbacks({ + onTaskCompleted: taskId => gapRoundEvents.push(taskId), + onLogicalTaskCompleted: logicalId => { + gapLogicalEvents.push(logicalId); + }, + onLogicalTaskCanceled: (logicalId, reason) => { + gapCanceledEvents.push([logicalId, reason]); + }, +}); +assert.equal(await gapScheduler.start(), true); +const gapParentId = gapScheduler.addTask( + '无限间隔测试', + 'normal_fight', + { type: 'normal_fight', gap: 0.08 }, + 10, + Number.POSITIVE_INFINITY, +); +gapScheduler.startConsuming(); +await wait(0); +gapApi.callbacks.onTaskCompleted({ + type: 'task_completed', + success: true, + result: null, + error: null, +}); +await wait(0); +assert.deepEqual(gapRoundEvents, [gapParentId]); +assert.deepEqual( + gapLogicalEvents, + [], + '无限任务单轮完成不得报告整个父任务完成', +); +assert.equal(gapScheduler.currentRunningTask, null); +assert.equal(gapScheduler.taskQueue.length, 0); +assert.equal(gapScheduler.waitingTaskList.length, 1); +assert.equal(gapScheduler.waitingTaskList[0].reason, 'gap'); +assert.notEqual(gapScheduler.waitingTaskList[0].task.id, gapParentId); +assert.equal( + gapScheduler.waitingTaskList[0].task.logicalId, + gapParentId, +); +assert.equal(gapScheduler.status, 'idle'); +const gapFollowUpId = gapScheduler.waitingTaskList[0].task.id; +assert.equal(gapScheduler.removeTask(gapFollowUpId), true); +assert.deepEqual( + gapCanceledEvents, + [[gapParentId, 'removed']], + '删除 gap 等待项必须取消其父任务', +); +await wait(100); +assert.equal( + gapTaskStarts, + 1, + '被删除的 gap 等待项不得在计时结束后重新启动', +); +await gapScheduler.stop(); + +// 启动失败后的重试倒计时同样必须可见、可删除。 +const retryApi = createSchedulerApi({ + taskStart: async () => ({ + success: false, + error: '模拟启动失败', + }), +}); +const retryScheduler = new schedulerModule.Scheduler(retryApi); +retryScheduler.setAutoExpedition(false); +const retryCanceledEvents = []; +retryScheduler.setCallbacks({ + onLogicalTaskCanceled: (logicalId, reason) => { + retryCanceledEvents.push([logicalId, reason]); + }, +}); +assert.equal(await retryScheduler.start(), true); +const retryParentId = retryScheduler.addTask( + '失败重试等待测试', + 'normal_fight', + { type: 'normal_fight' }, +); +retryScheduler.startConsuming(); +await wait(0); +assert.equal(retryScheduler.waitingTaskList.length, 1); +assert.equal(retryScheduler.waitingTaskList[0].reason, 'retry'); +assert.equal( + retryScheduler.waitingTaskList[0].task.logicalId, + retryParentId, +); +const retryWaitingId = + retryScheduler.waitingTaskList[0].task.id; +assert.equal(retryScheduler.removeTask(retryWaitingId), true); +assert.deepEqual( + retryCanceledEvents, + [[retryParentId, 'removed']], + '删除失败重试等待项必须取消其父任务', +); +await retryScheduler.stop(); + +// 清空和系统停止必须给每个父任务发送明确的取消原因。 +const clearApi = createSchedulerApi(); +const clearScheduler = new schedulerModule.Scheduler(clearApi); +clearScheduler.setAutoExpedition(false); +const clearCanceledEvents = []; +clearScheduler.setCallbacks({ + onLogicalTaskCanceled: (logicalId, reason) => { + clearCanceledEvents.push([logicalId, reason]); + }, +}); +assert.equal(await clearScheduler.start(), true); +const clearIdA = clearScheduler.addTask( + '清空测试 A', + 'normal_fight', + { type: 'normal_fight' }, +); +const clearIdB = clearScheduler.addTask( + '清空测试 B', + 'normal_fight', + { type: 'normal_fight' }, +); +clearScheduler.clearQueue(); +assert.deepEqual(clearCanceledEvents, [ + [clearIdA, 'queue_cleared'], + [clearIdB, 'queue_cleared'], +]); +const stopId = clearScheduler.addTask( + '系统停止测试', + 'normal_fight', + { type: 'normal_fight' }, +); +await clearScheduler.stop(); +assert.deepEqual( + clearCanceledEvents.at(-1), + [stopId, 'system_stopped'], +); + +// 实时停止条件结束父任务,不复用手动暂停的重新入队语义。 +let conditionStopCalls = 0; +const conditionApi = createSchedulerApi({ + taskStop: async () => { + conditionStopCalls += 1; + return { success: true }; + }, +}); +const conditionScheduler = new schedulerModule.Scheduler(conditionApi); +conditionScheduler.setAutoExpedition(false); +const conditionRoundEvents = []; +const conditionLogicalEvents = []; +conditionScheduler.setCallbacks({ + onTaskCompleted: taskId => conditionRoundEvents.push(taskId), + onLogicalTaskCompleted: (logicalId, success) => { + conditionLogicalEvents.push([logicalId, success]); + }, +}); +assert.equal(await conditionScheduler.start(), true); +const conditionParentId = conditionScheduler.addTask( + '实时停止条件测试', + 'normal_fight', + { type: 'normal_fight' }, + 10, + Number.POSITIVE_INFINITY, + { loot_count_ge: 5 }, +); +conditionScheduler.startConsuming(); +await wait(0); +conditionScheduler.processBackendLog('[UI] 战利品数量: 5/5'); +await wait(0); +assert.equal(conditionStopCalls, 1); +assert.equal(conditionScheduler.status, 'stopping'); +conditionApi.callbacks.onTaskCompleted({ + type: 'task_completed', + success: false, + result: null, + error: null, +}); +await wait(0); +assert.deepEqual( + conditionRoundEvents, + [], + '被停止条件中断的物理轮次不得伪报为单轮完成', +); +assert.deepEqual( + conditionLogicalEvents, + [[conditionParentId, true]], +); +assert.equal(conditionScheduler.currentRunningTask, null); +assert.equal(conditionScheduler.taskQueue.length, 0); +assert.equal(conditionScheduler.waitingTaskList.length, 0); +assert.equal(conditionScheduler.status, 'idle'); +await conditionScheduler.stop(); + +let expeditionChecks = 0; +const expeditionApi = createSchedulerApi({ + expeditionCheck: async () => { + expeditionChecks += 1; + return { success: true }; + }, +}); +const expeditionScheduler = new schedulerModule.Scheduler(expeditionApi); +expeditionScheduler.setAutoExpedition(true); +expeditionScheduler.setExpeditionInterval(1); +assert.equal(await expeditionScheduler.start(), true); +assert.equal( + expeditionChecks, + 1, + '系统启动后应立即执行一次远征检查', +); +expeditionScheduler.handleExpeditionTrigger(); +await new Promise(resolve => setTimeout(resolve, 0)); +assert.equal( + expeditionChecks, + 2, + '远征定时器触发后应通过 EXPEDITION 任务执行检查', +); +expeditionScheduler.setAutoExpedition(false); + +async function createRunningScheduler(api) { + const scheduler = new schedulerModule.Scheduler(api); + scheduler.setAutoExpedition(false); + assert.equal(await scheduler.start(), true); + const taskId = scheduler.addTask( + '停止测试', + 'normal_fight', + { type: 'normal_fight' }, + 10, + 1, + ); + scheduler.startConsuming(); + await new Promise(resolve => setTimeout(resolve, 0)); + assert.equal(scheduler.currentRunningTask?.id, taskId); + return { scheduler, taskId }; +} + +let workerRunning = true; +const stopApi = createSchedulerApi({ + taskStatus: async () => ({ + success: true, + data: { + task_id: workerRunning ? 'backend-task-1' : null, + status: workerRunning ? 'running' : 'stopped', + progress: null, + result: null, + }, + }), +}); +const { scheduler: stoppingScheduler, taskId: stoppedTaskId } = + await createRunningScheduler(stopApi); +let stopSettled = false; +const stopPromise = stoppingScheduler.stopRunning().finally(() => { + stopSettled = true; +}); +await new Promise(resolve => setTimeout(resolve, 20)); +assert.equal(stopSettled, false); +assert.equal(stoppingScheduler.status, 'stopping'); +assert.equal(stoppingScheduler.currentRunningTask?.id, stoppedTaskId); +assert.equal(stoppingScheduler.taskQueue.length, 0); + +workerRunning = false; +await stopPromise; +assert.equal(stoppingScheduler.status, 'idle'); +assert.equal(stoppingScheduler.currentRunningTask, null); +assert.equal(stoppingScheduler.taskQueue.length, 1); +assert.equal(stoppingScheduler.taskQueue[0].id, stoppedTaskId); +assert.equal(stoppingScheduler.taskQueue[0].backendTaskId, undefined); + +const wsStopApi = createSchedulerApi(); +const { scheduler: wsStoppingScheduler, taskId: wsStoppedTaskId } = + await createRunningScheduler(wsStopApi); +const wsStopPromise = wsStoppingScheduler.stopRunning(); +await new Promise(resolve => setTimeout(resolve, 20)); +wsStopApi.callbacks.onTaskCompleted({ + type: 'task_completed', + success: false, + result: null, + error: null, +}); +await wsStopPromise; +assert.equal(wsStoppingScheduler.status, 'idle'); +assert.equal(wsStoppingScheduler.currentRunningTask, null); +assert.equal(wsStoppingScheduler.taskQueue.length, 1); +assert.equal(wsStoppingScheduler.taskQueue[0].id, wsStoppedTaskId); + +const failedStopApi = createSchedulerApi({ + taskStop: async () => ({ success: false, error: '拒绝停止' }), +}); +const { scheduler: failedStopScheduler, taskId: failedTaskId } = + await createRunningScheduler(failedStopApi); +await assert.rejects( + failedStopScheduler.stopRunning(), + /拒绝停止/, +); +assert.equal(failedStopScheduler.status, 'running'); +assert.equal(failedStopScheduler.currentRunningTask?.id, failedTaskId); +assert.equal(failedStopScheduler.taskQueue.length, 0); + +console.log('scheduler domain tests passed'); diff --git a/scripts/test-settings-persistence.js b/scripts/test-settings-persistence.js new file mode 100644 index 0000000..92494f3 --- /dev/null +++ b/scripts/test-settings-persistence.js @@ -0,0 +1,1443 @@ +/** + * 设置页持久化隔离测试。 + * + * 测试流程: + * 1. 使用隐藏的 Electron BrowserWindow 加载真实设置页 HTML。 + * 2. 拦截正式 renderer bundle,避免启动应用和后端。 + * 3. 实例化真实 ConfigView、ConfigModel 和 ConfigController。 + * 4. 向所有可保存表单字段写入一组测试值。 + * 5. 验证 ConfigView 渲染和收集结果完全一致。 + * 6. 通过模拟 Electron Bridge 执行真实控制器保存逻辑。 + * 7. 将 YAML 和 GUI JSON 写入系统临时目录。 + * 8. 从磁盘重新读取并验证全部字段。 + * 9. 验证主题、主色调和调试模式的 localStorage 写入。 + * 10. 验证非法延迟区间会被表单校验拒绝。 + * 11. 测试过程不读取或修改项目中的真实用户配置。 + * 12. 完成后删除临时目录并清理测试会话数据。 + */ + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { + app, + BrowserWindow, + session, +} = require('electron'); + +app.commandLine.appendSwitch('disable-gpu'); + +const projectRoot = path.resolve(__dirname, '..'); +const temporaryDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'autowsgr-settings-test-'), +); + +/** + * 在真实设置页 DOM 中执行设置保存测试。 + * + * @param {string} root 项目根目录。 + * @param {string} tempDirectory 隔离配置输出目录。 + * @returns {Promise>} 测试覆盖统计。 + */ +async function runRendererTest(root, tempDirectory) { + const rendererAssert = require('node:assert/strict'); + const rendererFs = require('node:fs'); + const rendererPath = require('node:path'); + const yaml = require('js-yaml'); + const { + ConfigView, + } = require(rendererPath.join( + root, + 'dist/src/view/config/ConfigView.js', + )); + const { + ConfigModel, + } = require(rendererPath.join( + root, + 'dist/src/model/ConfigModel.js', + )); + const { + ConfigController, + } = require(rendererPath.join( + root, + 'dist/src/controller/app/ConfigController.js', + )); + const { + BattlePlanLoaderController, + } = require(rendererPath.join( + root, + 'dist/src/controller/plan/BattlePlanLoaderController.js', + )); + const { + BattlePlanLoaderView, + } = require(rendererPath.join( + root, + 'dist/src/view/plan/BattlePlanLoaderView.js', + )); + const { + DEFAULT_LOOT_PLANS, + } = require(rendererPath.join( + root, + 'dist/src/shared/lootPlans.js', + )); + + const shipNameAliases = { + 测试别名: 'U-47', + }; + const shipNameCorrections = { + 测试错字: 'U-81', + }; + const sample = { + emulatorType: '蓝叠', + emulatorPath: 'C:\\SettingsTest\\Emulator.exe', + emulatorSerial: '127.0.0.1:26555', + gameApp: '小米', + updateMode: 'manual', + autoExpedition: false, + expeditionInterval: 23, + autoBattle: true, + battleType: '困难航母', + autoExercise: true, + exerciseFleetId: 3, + battleTimes: 7, + autoNormalFight: true, + normalFightTasks: [ + { + name: 'C:\\SettingsTest\\battle.yaml', + fleet_id: 3, + fleet_preset_index: 1, + times: 2, + }, + ], + autoDecisive: true, + decisiveTemplateId: 'system_preset', + autoLoot: true, + lootPlanSource: 'user', + lootPlanId: 'bettle-用户胖次测试.yaml', + lootPlans: [ + { + source: 'system', + file: 'bettle-周常-8-2.yaml', + name: '周常 8-2', + }, + { + source: 'user', + file: 'bettle-用户胖次测试.yaml', + name: '用户胖次测试', + }, + ], + lootStopCount: 17, + logLevel: 'WARNING', + logRoot: 'C:\\SettingsTest\\logs', + themeMode: 'light', + accentColor: '#123456', + debugMode: true, + backendPort: 18438, + backendStartupMode: 'external', + backendRepoPath: 'C:\\SettingsTest\\AutoWSGR', + ocrGpuMode: 'cuda', + ocrGpu: true, + ocrMirror: 'github', + ocrConfidence: 0.73, + shipNameAliasesText: '测试别名: U-47', + shipNameCorrectionsText: '测试错字: U-81', + cudaPath: 'C:\\SettingsTest\\CUDA', + saveBackendScreenshots: true, + pythonPath: 'C:\\SettingsTest\\python.exe', + defaultWindowWidth: 1440, + defaultWindowHeight: 810, + rememberWindowBounds: true, + operationDelayMin: 0.4, + operationDelayMax: 1.6, + dockFullDestroy: false, + repairManually: true, + bathroomCount: 6, + destroyShipWorkMode: 2, + destroyShipTypes: ['驱逐', '潜艇'], + removeEquipmentMode: false, + planRoot: 'C:\\SettingsTest\\plans', + }; + + const sectionByTitle = title => Array.from( + document.querySelectorAll('.config-list-section'), + ).find(section => ( + section.querySelector('.config-section-heading h3')?.textContent === title + )); + const systemPanel = document.querySelector( + '[data-config-panel="system"]', + ); + const behaviorPanel = document.querySelector( + '[data-config-panel="behavior"]', + ); + const delaySection = sectionByTitle('全局延迟'); + const automationSection = sectionByTitle('自动化设置'); + const fleetSection = sectionByTitle('舰队设置'); + const ocrSection = sectionByTitle('OCR 设置'); + rendererAssert.equal( + automationSection?.parentElement, + behaviorPanel, + '自动化设置必须位于脚本行为面板', + ); + rendererAssert.equal( + systemPanel?.contains(automationSection), + false, + '系统设置面板不得再包含自动化设置', + ); + rendererAssert.ok( + delaySection.compareDocumentPosition(automationSection) + & Node.DOCUMENT_POSITION_FOLLOWING, + '自动化设置必须位于全局延迟设置之后', + ); + rendererAssert.equal( + fleetSection?.parentElement, + behaviorPanel, + '舰队设置必须位于脚本行为面板', + ); + rendererAssert.ok( + fleetSection.compareDocumentPosition(ocrSection) + & Node.DOCUMENT_POSITION_FOLLOWING, + '舰队设置必须位于 OCR 设置之前', + ); + + const view = new ConfigView(); + const decisiveOptions = Array.from( + document.getElementById('cfg-decisive-template').options, + ).map(option => [option.value, option.textContent]); + rendererAssert.deepStrictEqual(decisiveOptions, [ + ['user_plan', '用户计划'], + ['system_preset', '系统预设'], + ]); + const battleTypeStyle = getComputedStyle( + document.getElementById('cfg-battle-type'), + ); + const exerciseFleetStyle = getComputedStyle( + document.getElementById('cfg-exercise-fleet'), + ); + rendererAssert.equal( + document.getElementById('cfg-battle-type') + .dataset.animatedSelectWidth, + 'source', + '自动战役选项浮层必须跟随输入框宽度', + ); + rendererAssert.equal( + document.getElementById('cfg-exercise-fleet') + .dataset.animatedSelectWidth, + 'source', + '出征舰队选项浮层必须跟随输入框宽度', + ); + rendererAssert.equal( + document.getElementById('cfg-battle-type') + .dataset.configSelectWidth, + document.getElementById('cfg-exercise-fleet') + .dataset.configSelectWidth, + '自动战役与出征舰队必须使用同一个固定宽度', + ); + rendererAssert.equal( + document.getElementById('cfg-battle-type') + .style.getPropertyValue('--config-select-width'), + document.getElementById('cfg-exercise-fleet') + .style.getPropertyValue('--config-select-width'), + '自动战役与出征舰队渲染后的宽度必须一致', + ); + ['width', 'minWidth', 'maxWidth', 'flexBasis'].forEach((property) => { + rendererAssert.equal( + battleTypeStyle[property], + exerciseFleetStyle[property], + `自动战役与出征舰队下拉框的 ${property} 必须一致`, + ); + }); + view.render(sample); + + [ + ['cfg-auto-battle', 'cfg-auto-battle-body'], + ['cfg-auto-exercise', 'cfg-auto-exercise-body'], + ['cfg-auto-normal-fight', 'cfg-auto-normal-fight-body'], + ['cfg-auto-decisive', 'cfg-auto-decisive-body'], + ['cfg-auto-loot', 'cfg-auto-loot-body'], + ].forEach(([switchId, bodyId]) => { + const automationSwitch = document.getElementById(switchId); + const automationBody = document.getElementById(bodyId); + const originalChecked = automationSwitch.checked; + automationSwitch.checked = false; + automationSwitch.dispatchEvent(new Event('change')); + rendererAssert.notEqual( + getComputedStyle(automationBody).display, + 'none', + `关闭 ${switchId} 后不得收起内部配置`, + ); + automationSwitch.checked = originalChecked; + }); + + const collected = view.collect(); + rendererAssert.deepStrictEqual( + collected, + sample, + '设置页渲染后收集的数据与输入不一致', + ); + view.setLootPlans([ + { + source: 'system', + file: 'short.yaml', + name: '短', + }, + { + source: 'user', + file: 'long.yaml', + name: '这是一个较长的自动胖次计划名称', + }, + ]); + const lootPlanSelect = document.getElementById('cfg-loot-plan'); + const lootPlanWidth = Number.parseFloat( + lootPlanSelect.style.getPropertyValue('--config-select-width'), + ); + lootPlanSelect.selectedIndex = 1; + lootPlanSelect.dispatchEvent(new Event('change')); + rendererAssert.ok( + Number.isFinite(lootPlanWidth) && lootPlanWidth <= 320, + `自动胖次下拉框必须使用有效的分级宽度(${lootPlanWidth})`, + ); + rendererAssert.match( + lootPlanSelect.title, + /这是一个较长的自动胖次计划名称/, + '自动胖次下拉框必须保留完整文案提示', + ); + view.render(sample); + view.setLootPlans([]); + const emptyLootCollected = view.collect(); + rendererAssert.equal( + emptyLootCollected.autoLoot, + false, + '删除全部自动胖次计划后必须关闭开关', + ); + rendererAssert.deepStrictEqual( + emptyLootCollected.lootPlans, + [], + '显式空列表不得恢复默认计划', + ); + view.render(sample); + + const autoLootControl = document.querySelector( + '.config-auto-loot-control', + ); + rendererAssert.ok(autoLootControl, '自动胖次设置行不存在'); + rendererAssert.equal( + getComputedStyle(autoLootControl).flexWrap, + 'nowrap', + '自动胖次加载、下拉框、停止数量和开关必须保持同一行', + ); + [ + 'btn-load-loot-plans', + 'cfg-loot-plan', + 'cfg-loot-stop-count', + 'cfg-auto-loot', + ].forEach((id) => { + rendererAssert.equal( + document.getElementById(id)?.closest('.config-auto-loot-control'), + autoLootControl, + `自动胖次控件 ${id} 没有放在同一设置行`, + ); + }); + + const managedBattlePlans = [ + { + kind: 'battle', + file: 'bettle-周常-8-2.yaml', + name: '周常 8-2', + source: 'system', + modifiedAt: 1, + chapter: 8, + map: 2, + times: 1, + gap: 0, + fleetId: 1, + repairMode: 1, + result: 'S', + lootCountGe: 48, + shipCountGe: 7, + fleetCount: 0, + nodeCount: 1, + fleets: [], + }, + { + kind: 'battle', + file: 'bettle-用户胖次测试.yaml', + name: '用户胖次测试', + source: 'user', + modifiedAt: 2, + chapter: 9, + map: 2, + times: 1, + gap: 0, + fleetId: 1, + repairMode: 1, + result: 'S', + lootCountGe: 48, + shipCountGe: 7, + fleetCount: 0, + nodeCount: 1, + fleets: [], + }, + ]; + window.electronBridge = { + getPlanManagement: async () => ({ + bindings: [], + battlePlans: structuredClone(managedBattlePlans), + teamPlans: [], + errors: [], + ignoredUnlinkedPlans: [], + }), + }; + const loaderView = new BattlePlanLoaderView(); + const loaderController = new BattlePlanLoaderController( + loaderView, + { + getCurrentPlanIdentity: () => ({ + file: null, + source: 'user', + }), + openManagedPlan: async () => false, + }, + ); + loaderController.bindActions(); + const pickedLootPlansPromise = loaderController.pickLootPlans([ + { + source: 'system', + file: 'bettle-周常-8-2.yaml', + name: '周常 8-2', + }, + ]); + await new Promise(resolve => setTimeout(resolve, 0)); + + rendererAssert.equal( + document.getElementById('battle-plan-loader')?.style.display, + 'flex', + '加载自动胖次时没有打开复用的出征计划浮窗', + ); + rendererAssert.equal( + document.getElementById('battle-plan-loader-title')?.textContent, + '加载自动胖次计划', + ); + rendererAssert.equal( + document.getElementById('btn-confirm-battle-plan-loader')?.textContent, + '确认', + '浮窗底部按钮没有改为确认', + ); + const findAddButton = (file) => ( + Array.from(document.querySelectorAll('[data-loot-plan-add]')) + .find(button => button.dataset.lootPlanFile === file) + ); + const userAddButton = findAddButton('bettle-用户胖次测试.yaml'); + rendererAssert.ok(userAddButton, '用户计划卡片缺少加入列表按钮'); + rendererAssert.equal(userAddButton.textContent, '加入列表'); + rendererAssert.equal(userAddButton.disabled, false); + rendererAssert.equal( + getComputedStyle(userAddButton).backgroundColor, + 'rgb(37, 99, 235)', + '加入列表按钮必须使用蓝色背景', + ); + rendererAssert.equal( + getComputedStyle(userAddButton).position, + 'absolute', + '加入列表按钮没有嵌入任务卡片右下角', + ); + rendererAssert.equal(getComputedStyle(userAddButton).right, '7px'); + rendererAssert.equal(getComputedStyle(userAddButton).bottom, '7px'); + const addButtonBadge = userAddButton + .closest('.loot-automation-plan-card') + ?.querySelector('.fleet-team-source-badge'); + rendererAssert.ok( + Math.abs( + userAddButton.getBoundingClientRect().height + - (addButtonBadge?.getBoundingClientRect().height ?? 0), + ) < 0.1, + '加入列表按钮必须与配置标签同高', + ); + rendererAssert.ok( + Math.abs( + userAddButton.getBoundingClientRect().width + - (addButtonBadge?.getBoundingClientRect().width ?? 0), + ) < 0.1, + '加入列表按钮必须与配置标签同宽', + ); + rendererAssert.equal( + document.querySelector('.battle-plan-preview-stop-values'), + null, + '自动胖次列表浮窗不应展示 YAML 停止检测', + ); + const configPreviewTitle = document.getElementById( + 'battle-plan-loader-preview-title', + ); + const lootPreviewTitle = document.querySelector( + '.loot-plan-list-preview-heading h3', + ); + rendererAssert.match(lootPreviewTitle?.textContent ?? '', /^列表预览:/); + rendererAssert.equal( + lootPreviewTitle?.tagName, + configPreviewTitle?.tagName, + '列表预览与配置预览必须使用同级标题', + ); + rendererAssert.equal( + getComputedStyle(lootPreviewTitle).fontSize, + getComputedStyle(configPreviewTitle).fontSize, + '列表预览与配置预览标题字号必须一致', + ); + rendererAssert.equal( + getComputedStyle(lootPreviewTitle).lineHeight, + getComputedStyle(configPreviewTitle).lineHeight, + '列表预览与配置预览标题行高必须一致', + ); + rendererAssert.equal( + document.querySelector( + '.loot-plan-list-preview-heading span', + )?.textContent, + '设置自动胖次的下拉列表', + ); + const previewFrame = document.querySelector( + '.battle-plan-loader-preview', + ); + const previewFrameHeight = previewFrame.getBoundingClientRect().height; + + userAddButton.click(); + rendererAssert.equal( + document.querySelectorAll('.loot-plan-list-preview-card').length, + 2, + '加入列表后右侧预览没有立即更新', + ); + rendererAssert.equal( + findAddButton('bettle-用户胖次测试.yaml')?.textContent, + '已加入', + ); + rendererAssert.equal( + previewFrame.getBoundingClientRect().height, + previewFrameHeight, + '加入列表不得改变右侧配置预览框架高度', + ); + const lootPreview = document.querySelector('.loot-plan-list-preview'); + const lootPreviewCards = document.querySelector( + '.loot-plan-list-preview-cards', + ); + rendererAssert.equal( + lootPreview.classList.contains('battle-plan-preview-section'), + true, + '列表预览必须复用使用舰队的灰底分区样式', + ); + const fleetPreviewSection = document.querySelector( + '.battle-plan-preview-section:not(.loot-plan-list-preview)', + ); + rendererAssert.equal( + getComputedStyle(lootPreview).backgroundColor, + getComputedStyle(fleetPreviewSection).backgroundColor, + '列表预览与使用舰队必须使用相同灰色背景', + ); + rendererAssert.ok( + lootPreview.getBoundingClientRect().bottom + <= previewFrame.getBoundingClientRect().bottom, + '列表预览不得超出右侧配置预览框架', + ); + rendererAssert.equal( + getComputedStyle(lootPreview).maxHeight, + '150px', + '列表预览必须限制最大高度', + ); + rendererAssert.equal( + getComputedStyle(lootPreviewCards).overflowY, + 'auto', + '列表预览超高后必须在框架内滚动', + ); + const findDeleteButton = (file) => ( + Array.from(document.querySelectorAll('[data-loot-plan-delete]')) + .find(button => button.dataset.lootPlanFile === file) + ); + findDeleteButton('bettle-周常-8-2.yaml')?.click(); + rendererAssert.equal( + document.getElementById('generic-prompt-title')?.textContent, + '删除自动胖次计划', + '删除计划没有弹出二次确认', + ); + rendererAssert.match( + document.getElementById('generic-prompt-message')?.textContent ?? '', + /周常 8-2/, + ); + document.getElementById('generic-prompt-cancel')?.click(); + await Promise.resolve(); + rendererAssert.equal( + document.querySelectorAll('.loot-plan-list-preview-card').length, + 2, + '取消删除后不应修改列表草稿', + ); + + findDeleteButton('bettle-周常-8-2.yaml')?.click(); + document.getElementById('generic-prompt-ok')?.click(); + await Promise.resolve(); + rendererAssert.equal( + document.querySelectorAll('.loot-plan-list-preview-card').length, + 1, + '确认删除后右侧列表没有移除任务', + ); + const previewCard = document.querySelector( + '.loot-plan-list-preview-card', + ); + const currentLootPreviewCards = document.querySelector( + '.loot-plan-list-preview-cards', + ); + rendererAssert.equal( + previewCard?.children.length, + 2, + '列表卡片只能显示任务名称和删除按钮', + ); + rendererAssert.equal( + getComputedStyle(previewCard).paddingTop, + '3px', + '列表卡片顶部必须预留 3px', + ); + rendererAssert.equal( + getComputedStyle(previewCard).paddingBottom, + '3px', + '列表卡片底部必须预留 3px', + ); + rendererAssert.ok( + ( + (currentLootPreviewCards?.getBoundingClientRect().width ?? 0) + - (previewCard?.getBoundingClientRect().width ?? 0) + ) <= 12, + '列表卡片宽度必须铺满列表预览区域', + ); + rendererAssert.ok( + (previewCard?.getBoundingClientRect().height ?? 0) <= 24, + '列表预览卡片不得被删除按钮额外撑高', + ); + rendererAssert.ok( + Math.abs( + ( + previewCard?.querySelector('button') + ?.getBoundingClientRect().height ?? 0 + ) - ( + document.getElementById('battle-plan-loader-preview-source') + ?.getBoundingClientRect().height ?? 0 + ), + ) < 0.1, + '列表预览删除按钮必须与用户预设标签同高', + ); + document.getElementById('btn-confirm-battle-plan-loader')?.click(); + rendererAssert.deepStrictEqual( + await pickedLootPlansPromise, + [{ + source: 'user', + file: 'bettle-用户胖次测试.yaml', + name: '用户胖次测试', + }], + '确认后没有返回完整的自动胖次下拉列表草稿', + ); + + const canceledLootPlansPromise = loaderController.pickLootPlans([ + { + source: 'user', + file: 'bettle-用户胖次测试.yaml', + name: '用户胖次测试', + }, + ]); + await new Promise(resolve => setTimeout(resolve, 0)); + document.getElementById('btn-cancel-battle-plan-loader')?.click(); + rendererAssert.equal( + await canceledLootPlansPromise, + null, + '取消浮窗不应回填列表草稿', + ); + + const globalScrollButtonRule = Array.from(document.styleSheets) + .flatMap(sheet => Array.from(sheet.cssRules)) + .find(rule => rule.selectorText === '::-webkit-scrollbar-button'); + rendererAssert.equal( + globalScrollButtonRule?.style.display, + 'none', + '所有下拉框的滚动条顶部和底部按钮必须统一隐藏', + ); + + const delayMinimum = document.getElementById('cfg-delay-min'); + const delayMaximum = document.getElementById('cfg-delay-max'); + delayMinimum.value = '2'; + delayMaximum.value = '1'; + rendererAssert.throws( + () => view.collect(), + /最小值不能大于最大值/, + '非法延迟区间未被拦截', + ); + view.render(sample); + + const model = new ConfigModel(); + model.loadFromYaml([ + 'emulator:', + ' type: 雷电', + ' backend_options:', + ' transport:', + ' retry: 7', + 'account:', + ' game_app: 官服', + ' backend_identity:', + ' region:', + ' code: cn', + 'ocr:', + ' ship_name_corrections:', + ' stale_correction: stale', + ' backend_metadata:', + ' source: backend', + ' ship_name_aliases:', + ' stale_alias: stale', + ' backend_metadata:', + ' source: backend', + ' backend_options:', + ' detector:', + ' timeout: 30', + 'log:', + ' channels:', + ' stale.channel: DEBUG', + ' backend_metadata:', + ' sink:', + ' name: audit', + ' backend_options:', + ' rotation:', + ' compress: true', + 'daily_automation:', + ' auto_gain_bonus: true', + ' auto_bath_repair: true', + ' auto_decisive: true', + ' decisive_ticket_reserve: 0', + ' decisive_template_id: builtin_decisive_6', + ' loot_plan_index: 2', + ' backend_options:', + ' scheduler:', + ' jitter: 3', + 'custom_unknown:', + ' keep: true', + '', + ].join('\n')); + rendererAssert.equal( + model.migratedGuiAutomation.lootPlanId, + 'bettle-old-8-5AI六潜胖次.yaml', + '旧 usersettings.yaml 的索引没有保持 8-5 语义', + ); + rendererAssert.deepStrictEqual( + model.migratedLegacyDecisiveAutomation, + { + autoDecisive: true, + ticketReserve: 0, + templateId: 'builtin_decisive_6', + }, + '旧版决战自动化原值没有被逐字段读取', + ); + + const guiSettings = { + preserved_key: 'keep', + decisive_plan: { + chapter: 5, + use_quick_repair: false, + level1: ['当前主力'], + level2: ['当前替补'], + }, + }; + let failGuiAutomationMigration = false; + let failLegacyDecisiveMigration = false; + let legacyDecisiveMigrationCalls = 0; + const writeGuiSettings = patch => { + Object.assign(guiSettings, patch); + rendererFs.writeFileSync( + rendererPath.join(tempDirectory, 'gui_settings.json'), + JSON.stringify(guiSettings, null, 2), + 'utf8', + ); + }; + + window.electronBridge = { + readFile: async name => { + const file = rendererPath.join(tempDirectory, name); + return rendererFs.existsSync(file) + ? rendererFs.readFileSync(file, 'utf8') + : ''; + }, + saveFile: async (name, content) => { + rendererFs.writeFileSync( + rendererPath.join(tempDirectory, name), + content, + 'utf8', + ); + }, + getGuiAutomationSettings: async () => ({ + exists: Boolean(guiSettings.automation), + settings: structuredClone(guiSettings.automation ?? {}), + }), + setGuiAutomationSettings: async settings => { + if (failGuiAutomationMigration) { + throw new Error('模拟 GUI 自动化配置写入失败'); + } + writeGuiSettings({ + automation: structuredClone(settings), + }); + return settings; + }, + migrateLegacyDecisiveAutomation: async settings => { + legacyDecisiveMigrationCalls += 1; + if (failLegacyDecisiveMigration) { + throw new Error('模拟旧版决战配置写入失败'); + } + writeGuiSettings({ + legacy_decisive_automation: { + auto_decisive: settings.autoDecisive, + decisive_ticket_reserve: settings.ticketReserve, + decisive_template_id: settings.templateId, + }, + }); + return structuredClone(settings); + }, + setBackendPort: async backendPort => { + writeGuiSettings({ + backend_port: backendPort, + }); + }, + setBackendStartupMode: async mode => { + writeGuiSettings({ + backend_startup_mode: mode, + }); + }, + setBackendRepoPath: async repoPath => { + writeGuiSettings({ + backend_repo_path: repoPath ?? '', + }); + }, + setOcrGpuMode: async mode => { + writeGuiSettings({ + ocr_gpu_mode: mode, + }); + }, + setCudaPath: async cudaPath => { + writeGuiSettings({ + cuda_path: cudaPath ?? '', + }); + }, + setSaveBackendScreenshots: async enabled => { + writeGuiSettings({ + save_backend_screenshots: enabled === true, + }); + }, + setPythonPath: async pythonPath => { + writeGuiSettings({ + python_path: pythonPath ?? '', + }); + }, + setUpdateMode: async mode => { + writeGuiSettings({ + update_mode: mode, + }); + }, + setWindowPreferences: async preferences => { + writeGuiSettings({ + default_window_width: preferences.defaultWidth, + default_window_height: preferences.defaultHeight, + remember_window_bounds: preferences.rememberBounds, + }); + return preferences; + }, + }; + + const host = { + configModel: model, + configView: view, + setupView: {}, + mainView: { + setDebugMode: () => {}, + }, + scheduler: { + status: 'connected', + setAutoExpedition: () => {}, + setExpeditionInterval: () => {}, + }, + cronScheduler: { + updateConfig: () => {}, + }, + templateCtrl: {}, + startupCtrl: { + startSystem: () => {}, + }, + configDir: tempDirectory, + }; + + const partialYaml = [ + 'daily_automation:', + ' expedition_interval: 41', + ' battle_times: 6', + ' auto_loot: false', + ' loot_plan_id: bettle-捞胖次-8-5.yaml', + ' loot_stop_count: 12', + '', + ].join('\n'); + rendererFs.writeFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + partialYaml, + 'utf8', + ); + writeGuiSettings({ + automation: { + expeditionInterval: 29, + autoLoot: true, + }, + }); + const partialModel = new ConfigModel(); + const partialController = new ConfigController({ + ...host, + configModel: partialModel, + }); + + failGuiAutomationMigration = true; + await rendererAssert.rejects( + () => partialController.loadConfig(), + /模拟 GUI 自动化配置写入失败/, + ); + const yamlAfterFailedGuiMigration = yaml.load( + rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + 'utf8', + ), + ); + rendererAssert.equal( + yamlAfterFailedGuiMigration.daily_automation.battle_times, + 6, + 'GUI JSON 写入失败时不得删除 YAML 的缺失字段', + ); + rendererAssert.equal( + yamlAfterFailedGuiMigration.daily_automation.loot_plan_id, + 'bettle-捞胖次-8-5.yaml', + 'GUI JSON 写入失败时不得删除 YAML 的计划标识', + ); + + failGuiAutomationMigration = false; + await partialController.loadConfig(); + rendererAssert.deepStrictEqual( + partialModel.currentGuiAutomation, + { + expeditionInterval: 29, + battleTimes: 6, + autoDecisive: false, + decisiveTemplateId: 'system_preset', + autoLoot: true, + lootPlanSource: 'system', + lootPlanId: 'bettle-old-8-5AI六潜胖次.yaml', + lootPlans: structuredClone(DEFAULT_LOOT_PLANS), + lootStopCount: 12, + }, + '部分 GUI JSON 没有按字段优先级与 YAML 合并', + ); + rendererAssert.deepStrictEqual( + guiSettings.automation, + partialModel.currentGuiAutomation, + '逐字段合并结果没有完整写入 gui_settings.json', + ); + const yamlAfterSuccessfulGuiMigration = yaml.load( + rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + 'utf8', + ), + ); + for (const field of [ + 'expedition_interval', + 'battle_times', + 'auto_loot', + 'loot_plan_id', + 'loot_stop_count', + ]) { + rendererAssert.equal( + Object.hasOwn( + yamlAfterSuccessfulGuiMigration.daily_automation, + field, + ), + false, + `完整写入 GUI JSON 后未清理 ${field}`, + ); + } + + rendererFs.writeFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + [ + 'daily_automation:', + ' auto_loot: true', + ' loot_plan_id: bettle-不存在.yaml', + '', + ].join('\n'), + 'utf8', + ); + writeGuiSettings({ + automation: { + autoLoot: true, + }, + }); + const unsafeLootModel = new ConfigModel(); + const unsafeLootController = new ConfigController({ + ...host, + configModel: unsafeLootModel, + }); + await unsafeLootController.loadConfig(); + rendererAssert.equal( + unsafeLootModel.currentGuiAutomation.autoLoot, + false, + '缺少有效计划标识时必须关闭自动刷取', + ); + rendererAssert.equal( + unsafeLootModel.currentGuiAutomation.lootPlanId, + 'bettle-周常-9-2.yaml', + '损坏计划标识只能安全回退到默认计划', + ); + + const controller = new ConfigController(host); + await controller.saveConfig(); + + const savedYaml = yaml.load(rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + 'utf8', + )); + const savedGui = JSON.parse(rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'gui_settings.json'), + 'utf8', + )); + + rendererAssert.deepStrictEqual(savedYaml.emulator, { + type: sample.emulatorType, + path: sample.emulatorPath, + serial: sample.emulatorSerial, + backend_options: { + transport: { + retry: 7, + }, + }, + }); + rendererAssert.deepStrictEqual(savedYaml.account, { + game_app: sample.gameApp, + backend_identity: { + region: { + code: 'cn', + }, + }, + }); + rendererAssert.deepStrictEqual(savedYaml.daily_automation, { + auto_expedition: sample.autoExpedition, + auto_battle: sample.autoBattle, + battle_type: sample.battleType, + auto_exercise: sample.autoExercise, + exercise_fleet_id: sample.exerciseFleetId, + auto_normal_fight: sample.autoNormalFight, + auto_decisive: true, + decisive_ticket_reserve: 0, + decisive_template_id: 'builtin_decisive_6', + auto_gain_bonus: true, + auto_bath_repair: true, + auto_set_support: false, + bath_repair_blacklist: [], + normal_fight_tasks: sample.normalFightTasks, + stop_max_ship: false, + stop_max_loot: false, + backend_options: { + scheduler: { + jitter: 3, + }, + }, + }); + rendererAssert.deepStrictEqual(savedYaml.ocr, { + gpu: sample.ocrGpu, + mirror: sample.ocrMirror, + ship_name_match_confidence: sample.ocrConfidence, + ship_name_corrections: { + ...shipNameCorrections, + backend_metadata: { + source: 'backend', + }, + }, + ship_name_aliases: { + ...shipNameAliases, + backend_metadata: { + source: 'backend', + }, + }, + backend_options: { + detector: { + timeout: 30, + }, + }, + }); + rendererAssert.deepStrictEqual(savedYaml.log, { + level: sample.logLevel, + root: sample.logRoot, + channels: { + 'stale.channel': 'DEBUG', + backend_metadata: { + sink: { + name: 'audit', + }, + }, + }, + backend_options: { + rotation: { + compress: true, + }, + }, + }); + rendererAssert.equal( + savedYaml.operation_delay_min, + sample.operationDelayMin, + ); + rendererAssert.equal( + savedYaml.operation_delay_max, + sample.operationDelayMax, + ); + rendererAssert.equal( + savedYaml.dock_full_destroy, + sample.dockFullDestroy, + ); + rendererAssert.equal( + savedYaml.repair_manually, + sample.repairManually, + ); + rendererAssert.equal( + savedYaml.bathroom_count, + sample.bathroomCount, + ); + rendererAssert.equal( + savedYaml.destroy_ship_work_mode, + sample.destroyShipWorkMode, + ); + rendererAssert.deepStrictEqual( + savedYaml.destroy_ship_types, + sample.destroyShipTypes, + ); + rendererAssert.equal( + savedYaml.remove_equipment_mode, + sample.removeEquipmentMode, + ); + rendererAssert.equal(savedYaml.plan_root, sample.planRoot); + rendererAssert.deepStrictEqual(savedYaml.custom_unknown, { + keep: true, + }); + + rendererAssert.deepStrictEqual(savedGui.automation, { + expeditionInterval: sample.expeditionInterval, + battleTimes: sample.battleTimes, + autoDecisive: sample.autoDecisive, + decisiveTemplateId: sample.decisiveTemplateId, + autoLoot: sample.autoLoot, + lootPlanSource: sample.lootPlanSource, + lootPlanId: sample.lootPlanId, + lootPlans: sample.lootPlans, + lootStopCount: sample.lootStopCount, + }); + rendererAssert.equal(savedGui.backend_port, sample.backendPort); + rendererAssert.equal( + savedGui.backend_startup_mode, + sample.backendStartupMode, + ); + rendererAssert.equal( + savedGui.backend_repo_path, + sample.backendRepoPath, + ); + rendererAssert.equal(savedGui.ocr_gpu_mode, sample.ocrGpuMode); + rendererAssert.equal(savedGui.cuda_path, sample.cudaPath); + rendererAssert.equal( + savedGui.save_backend_screenshots, + sample.saveBackendScreenshots, + ); + rendererAssert.equal(savedGui.python_path, sample.pythonPath); + rendererAssert.equal(savedGui.update_mode, sample.updateMode); + rendererAssert.equal( + savedGui.default_window_width, + sample.defaultWindowWidth, + ); + rendererAssert.equal( + savedGui.default_window_height, + sample.defaultWindowHeight, + ); + rendererAssert.equal( + savedGui.remember_window_bounds, + sample.rememberWindowBounds, + ); + rendererAssert.equal(savedGui.preserved_key, 'keep'); + rendererAssert.deepStrictEqual(savedGui.decisive_plan, { + chapter: 5, + use_quick_repair: false, + level1: ['当前主力'], + level2: ['当前替补'], + }); + + const runWithAutoClosedAlert = async action => { + const notices = []; + const timer = window.setInterval(() => { + const overlay = document.getElementById('generic-prompt'); + if (!overlay || overlay.style.display === 'none') return; + const title = document.getElementById( + 'generic-prompt-title', + )?.textContent ?? ''; + const message = document.getElementById( + 'generic-prompt-message', + )?.textContent ?? ''; + if (!title) return; + notices.push({ title, message }); + document.getElementById('generic-prompt-ok')?.click(); + }, 5); + try { + await action(); + } finally { + window.clearInterval(timer); + } + return notices; + }; + + failLegacyDecisiveMigration = true; + const failedNotices = await runWithAutoClosedAlert( + () => controller.loadConfig(), + ); + const yamlAfterFailedMigration = yaml.load( + rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + 'utf8', + ), + ); + rendererAssert.equal( + yamlAfterFailedMigration.daily_automation.auto_decisive, + true, + '迁移失败时不得删除 auto_decisive', + ); + rendererAssert.equal( + yamlAfterFailedMigration.daily_automation + .decisive_ticket_reserve, + 0, + '迁移失败时不得删除 decisive_ticket_reserve', + ); + rendererAssert.equal( + yamlAfterFailedMigration.daily_automation + .decisive_template_id, + 'builtin_decisive_6', + '迁移失败时不得删除 decisive_template_id', + ); + rendererAssert.equal( + Object.hasOwn(guiSettings, 'legacy_decisive_automation'), + false, + ); + rendererAssert.match( + failedNotices[0]?.title ?? '', + /迁移失败/, + ); + + failLegacyDecisiveMigration = false; + const successNotices = await runWithAutoClosedAlert( + () => controller.loadConfig(), + ); + const yamlAfterSuccessfulMigration = yaml.load( + rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'usersettings.yaml'), + 'utf8', + ), + ); + for (const field of [ + 'auto_decisive', + 'decisive_ticket_reserve', + 'decisive_template_id', + ]) { + rendererAssert.equal( + Object.hasOwn( + yamlAfterSuccessfulMigration.daily_automation, + field, + ), + false, + `迁移成功后未清理 ${field}`, + ); + } + const guiAfterSuccessfulMigration = JSON.parse( + rendererFs.readFileSync( + rendererPath.join(tempDirectory, 'gui_settings.json'), + 'utf8', + ), + ); + rendererAssert.deepStrictEqual( + guiAfterSuccessfulMigration.legacy_decisive_automation, + { + auto_decisive: true, + decisive_ticket_reserve: 0, + decisive_template_id: 'builtin_decisive_6', + }, + ); + rendererAssert.deepStrictEqual( + guiAfterSuccessfulMigration.decisive_plan, + savedGui.decisive_plan, + '旧配置迁移不得覆盖当前决战计划', + ); + rendererAssert.match( + successNotices[0]?.message ?? '', + /自动决战:开启/, + ); + rendererAssert.match( + successNotices[0]?.message ?? '', + /决战票保留:0/, + ); + rendererAssert.match( + successNotices[0]?.message ?? '', + /决战模板:builtin_decisive_6/, + ); + rendererAssert.match( + successNotices[0]?.message ?? '', + /决战票保留仅无损保存,不参与执行轮数/, + ); + rendererAssert.equal(legacyDecisiveMigrationCalls, 2); + + await controller.loadConfig(); + rendererAssert.equal( + legacyDecisiveMigrationCalls, + 2, + '迁移成功后再次启动不应重复迁移或提示', + ); + + rendererAssert.equal( + localStorage.getItem('themeMode'), + sample.themeMode, + ); + rendererAssert.equal( + localStorage.getItem('accentColor'), + sample.accentColor, + ); + rendererAssert.equal( + localStorage.getItem('debugMode'), + String(sample.debugMode), + ); + rendererAssert.equal( + localStorage.getItem('updateMode'), + sample.updateMode, + ); + + const controlIds = Array.from(document.querySelectorAll( + '[data-config-panel] input[id], ' + + '[data-config-panel] select[id], ' + + '[data-config-panel] textarea[id]', + )) + .filter(element => !element.closest('[aria-hidden="true"]')) + .map(element => element.id); + const expectedControlIds = [ + 'cfg-emu-type', + 'cfg-emu-path', + 'cfg-emu-serial', + 'cfg-game-app', + 'cfg-auto-expedition', + 'cfg-auto-battle', + 'cfg-battle-type', + 'cfg-auto-exercise', + 'cfg-exercise-fleet', + 'cfg-auto-normal-fight', + 'cfg-auto-decisive', + 'cfg-decisive-template', + 'cfg-auto-loot', + 'cfg-loot-plan', + 'cfg-loot-stop-count', + 'cfg-log-level', + 'cfg-log-root', + 'cfg-debug-mode', + 'cfg-python-path', + 'cfg-backend-port', + 'cfg-save-backend-screenshots', + 'cfg-use-external-backend', + 'cfg-backend-repo-path', + 'cfg-window-width', + 'cfg-window-height', + 'cfg-remember-window-bounds', + 'cfg-update-mode', + 'cfg-theme-mode', + 'cfg-accent-color', + 'cfg-delay-min-range', + 'cfg-delay-min', + 'cfg-delay-max-range', + 'cfg-delay-max', + 'cfg-ocr-mirror', + 'cfg-ocr-gpu-mode', + 'cfg-cuda-path', + 'cfg-ocr-gpu', + 'cfg-ocr-confidence-range', + 'cfg-ocr-confidence', + 'cfg-ship-name-aliases', + 'cfg-ship-name-corrections', + 'cfg-dock-full-destroy', + 'cfg-repair-manually', + 'cfg-bathroom-count', + 'cfg-destroy-ship-mode', + 'cfg-remove-equipment-mode', + 'cfg-plan-root', + ]; + rendererAssert.deepStrictEqual( + [...controlIds].sort(), + [...expectedControlIds].sort(), + '设置页出现未纳入测试的表单控件', + ); + + return { + viewFields: Object.keys(collected).length, + visibleControls: controlIds.length, + yamlTopLevelFields: Object.keys(savedYaml).length, + guiTopLevelFields: Object.keys(savedGui).length, + }; +} + +async function main() { + await app.whenReady(); + + const testSession = session.fromPartition( + `settings-persistence-test-${Date.now()}`, + ); + testSession.webRequest.onBeforeRequest( + { + urls: ['file:///*'], + }, + (details, callback) => { + callback({ + cancel: details.url.endsWith('/dist/renderer.bundle.js'), + }); + }, + ); + + const window = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: false, + nodeIntegration: true, + session: testSession, + }, + }); + await window.loadFile( + path.join(projectRoot, 'src/view/index.html'), + ); + + const expression = `(${runRendererTest.toString()})(` + + `${JSON.stringify(projectRoot)},` + + `${JSON.stringify(temporaryDirectory)})`; + const result = await window.webContents.executeJavaScript(expression); + + assert.equal( + fs.existsSync(path.join(temporaryDirectory, 'usersettings.yaml')), + true, + ); + assert.equal( + fs.existsSync(path.join(temporaryDirectory, 'gui_settings.json')), + true, + ); + console.log(`设置页隔离持久化测试通过: ${JSON.stringify(result)}`); + + window.destroy(); + await testSession.clearStorageData(); +} + +main() + .then(() => { + fs.rmSync(temporaryDirectory, { + force: true, + recursive: true, + }); + app.exit(0); + }) + .catch(error => { + console.error(error); + fs.rmSync(temporaryDirectory, { + force: true, + recursive: true, + }); + app.exit(1); + }); diff --git a/scripts/test-ship-library-updater.js b/scripts/test-ship-library-updater.js new file mode 100644 index 0000000..ddc11c6 --- /dev/null +++ b/scripts/test-ship-library-updater.js @@ -0,0 +1,41 @@ +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const projectRoot = path.resolve(__dirname, '..'); +const candidates = [ + process.env.AUTOWSGR_PYTHON, + path.join(projectRoot, 'python', 'python.exe'), + process.platform === 'win32' ? 'python' : 'python3', +].filter(Boolean); + +const failures = []; +for (const python of candidates) { + const result = spawnSync( + python, + [ + '-m', + 'unittest', + 'discover', + '-s', + path.join('tools', 'ship_library'), + '-p', + 'test_*.py', + ], + { + cwd: projectRoot, + encoding: 'utf8', + windowsHide: true, + }, + ); + if (result.status === 0) { + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + console.log('ship library updater tests passed'); + process.exit(0); + } + failures.push( + `${python}:\n${result.stdout || ''}${result.stderr || result.error || ''}`, + ); +} + +throw new Error(`ship library updater tests failed:\n${failures.join('\n')}`); diff --git a/scripts/test-task-group-migration.js b/scripts/test-task-group-migration.js new file mode 100644 index 0000000..e8c7cf9 --- /dev/null +++ b/scripts/test-task-group-migration.js @@ -0,0 +1,467 @@ +/** + * 旧 path-form 任务组迁移回归测试。 + * + * 测试使用旧安装目录中实际保存过的 task_groups.json。 + * 第一步加载无版本号、只有 path 的旧任务组。 + * 第二步检查用户计划和系统计划都生成受管引用。 + * 第三步保存迁移后的 v4 数据。 + * 第四步把旧绝对路径视为已经删除。 + * 第五步通过正式队列加载函数执行一个旧任务。 + * 第六步确认执行只读取 managedSource 和 managedFile。 + * 第七步重新加载保存后的数据。 + * 第八步确认重载不会再次迁移或丢失字段。 + * 另外保留活动计划别名和 v2 中间格式的专项覆盖。 + * 任何旧路径读取都会让测试立即失败。 + */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const yaml = require('js-yaml'); +const { + AppPaths, +} = require('../dist/electron/services/AppPaths.js'); +const { + AtomicFileStore, +} = require('../dist/electron/services/AtomicFileStore.js'); +const { + UserDataMigrationService, +} = require('../dist/electron/services/UserDataMigrationService.js'); +const { TaskGroupModel } = require('../dist/src/model/TaskGroupModel.js'); +const { PlanModel } = require('../dist/src/model/PlanModel.js'); +const { + loadGroupToQueue, +} = require('../dist/src/controller/taskGroup/queueLoader.js'); + +const fixturePath = path.join( + __dirname, + 'fixtures', + 'task-groups', + 'v1-path-form', + 'task_groups.json', +); +const executablePlanPath = path.join( + __dirname, + '..', + 'resource', + 'system_battle_plans', + 'bettle-周常-1-1.yaml', +); +const executablePlanContent = fs.readFileSync(executablePlanPath, 'utf8'); +const weeklyPlans = [ + ['周常1章-1-2.yaml', 'bettle-周常-1-2-v1.yaml', 'user'], + ['周常2章-2-1.yaml', 'bettle-周常-2-1.yaml', 'system'], + ['周常3章-3-1.yaml', 'bettle-周常-3-1.yaml', 'system'], + ['周常4章-4-1.yaml', 'bettle-周常-4-1.yaml', 'system'], + ['周常5章-5-5.yaml', 'bettle-周常-5-5.yaml', 'system'], + ['周常6章-6-4.yaml', 'bettle-周常-6-4.yaml', 'system'], + ['周常7章-7-4.yaml', 'bettle-周常-7-4.yaml', 'system'], + ['周常8章-8-2.yaml', 'bettle-周常-8-2.yaml', 'system'], + ['周常9章-9-2.yaml', 'bettle-周常-9-2.yaml', 'system'], + ['周常10章-10-1.yaml', 'bettle-周常-10-1.yaml', 'system'], +]; +const activityPlans = [ + ['活动20260730-E1炸鱼.yaml', 'bettle-E1炸鱼.yaml'], + ['活动20260730-E5夜战.yaml', 'bettle-E5夜战.yaml'], + ['活动20260730-H1炸鱼.yaml', 'bettle-H1炸鱼.yaml'], + ['活动20260730-H5夜战.yaml', 'bettle-H5夜战.yaml'], +]; +const legacyWeeklyPlans = [ + ['周常1章-1-2.yaml', 'bettle-周常-1-2-v1.yaml', 1, 2], + ['周常3章-3-3.yaml', 'bettle-周常-3-3-v1.yaml', 3, 3], + ['周常6章-6-3.yaml', 'bettle-周常-6-3-v1.yaml', 6, 3], +]; +const compatibilityPlanResources = [ + ['bettle-周常-1-2-v1.yaml', 1, 2], + ['bettle-周常-3-3-v1.yaml', 3, 3], + ['bettle-周常-6-3-v1.yaml', 6, 3], + ['bettle-捞胖次-8-5.yaml', 8, 5], + ['bettle-捞胖次-9-4-6SS.yaml', 9, 4], +]; +const temporaryRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'autowsgr-task-group-v6-'), +); +let fixtureNumber = 0; + +process.on('exit', () => { + fs.rmSync(temporaryRoot, { recursive: true, force: true }); +}); + +function migratePresetInventory(content) { + fixtureNumber += 1; + const root = path.join(temporaryRoot, String(fixtureNumber)); + const projectRoot = path.join(root, 'project'); + const userData = path.join(root, 'user-data'); + const appPaths = new AppPaths({ + moduleDirectory: path.join(projectRoot, 'dist', 'electron'), + isPackaged: () => false, + getPath: name => name === 'exe' + ? path.join(projectRoot, 'AutoWSGR.exe') + : userData, + getResourcesPath: () => path.join(projectRoot, 'resources'), + }); + const migrationResources = path.join( + appPaths.resourceRoot(), + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + ); + fs.mkdirSync(path.dirname(migrationResources), { recursive: true }); + fs.cpSync( + path.join( + __dirname, + '..', + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + ), + migrationResources, + { recursive: true }, + ); + fs.mkdirSync(userData, { recursive: true }); + const taskGroups = path.join(userData, 'task_groups.json'); + fs.writeFileSync(taskGroups, content, 'utf8'); + const migration = new UserDataMigrationService( + appPaths, + new AtomicFileStore(), + ); + migration.writeState({ version: 5, completed: [] }); + const result = migration.migratePresetInventory(); + assert.equal(result.failed, 0); + assert.equal(migration.readState().version, 6); + return { + content: fs.readFileSync(taskGroups, 'utf8'), + userPlanDir: appPaths.userBattlePlansDir(), + }; +} + +function installBridge(initialContent) { + const state = { + stored: initialContent, + saves: 0, + managedReads: [], + legacyReads: [], + }; + global.window = { + electronBridge: { + readFile: async (file) => { + if (file === 'task_groups.json') return state.stored; + state.legacyReads.push(file); + throw new Error(`不应读取已迁走的旧路径: ${file}`); + }, + saveFile: async (_file, content) => { + state.stored = content; + state.saves += 1; + }, + readManagedCombatPlan: async (source, file) => { + state.managedReads.push({ source, file }); + const managedPath = path.join('C:\\managed-plans', source, file); + return { + success: true, + content: executablePlanContent, + path: managedPath, + runtimePath: managedPath, + }; + }, + readCombatPlanFile: async (file) => { + state.legacyReads.push(file); + return { + success: false, + error: `不应读取已迁走的旧路径: ${file}`, + }; + }, + }, + }; + return state; +} + +function assertManagedSystemPlan(item, legacyFile, managedFile) { + assert.equal(item.path, `resource/builtin_plans/${legacyFile}`); + assert.equal(item.managedSource, 'system'); + assert.equal(item.managedFile, managedFile); + assert.equal( + fs.existsSync(path.join( + __dirname, + '..', + 'resource', + 'system_battle_plans', + managedFile, + )), + true, + `迁移后的系统计划不存在: ${managedFile}`, + ); +} + +function assertManagedUserPlan( + item, + legacyFile, + managedFile, + userPlanDir, +) { + assert.equal(item.path, `resource/builtin_plans/${legacyFile}`); + assert.equal(item.managedSource, 'user'); + assert.equal(item.managedFile, managedFile); + assert.equal( + fs.existsSync(path.join(userPlanDir, managedFile)), + true, + `淘汰计划没有升级为个人计划: ${managedFile}`, + ); +} + +async function verifyRealLegacyFixtureLifecycle() { + const legacyFixture = fs.readFileSync(fixturePath, 'utf8'); + const inventory = migratePresetInventory(legacyFixture); + const state = installBridge(inventory.content); + const model = new TaskGroupModel(); + + await model.load(); + const migrated = model.toJSON(); + assert.equal(migrated.version, 4); + assert.equal(state.saves, 1); + + const defaultItem = migrated.groups[0].items[0]; + assert.equal(defaultItem.managedSource, 'user'); + assert.equal(defaultItem.managedFile, '9-4胖次6SS.yaml'); + assert.equal( + defaultItem.path, + 'D:\\AutoWSGR-GUI\\plans\\9-4胖次6SS.yaml', + ); + + const weeklyItems = migrated.groups[1].items; + weeklyPlans.forEach(([legacyFile, managedFile, source], index) => { + if (source === 'user') { + assertManagedUserPlan( + weeklyItems[index], + legacyFile, + managedFile, + inventory.userPlanDir, + ); + } else { + assertManagedSystemPlan( + weeklyItems[index], + legacyFile, + managedFile, + ); + } + }); + assert.equal(weeklyItems[10].kind, 'template'); + assert.equal(weeklyItems[10].templateId, 'builtin_decisive_6'); + + const trainingItem = migrated.groups[2].items[0]; + assert.equal(trainingItem.managedSource, 'user'); + assert.equal(trainingItem.managedFile, '9-3练级-min航速≤27.yaml'); + + model.setActiveGroup('默认'); + await model.save(); + assert.equal(state.saves, 2); + + const scheduledTasks = []; + let switchedPage = ''; + await loadGroupToQueue( + model, + { get: () => null }, + { + scheduler: { + addTask: (...args) => scheduledTasks.push(args), + }, + switchPage: (page) => { + switchedPage = page; + }, + renderMain: () => {}, + }, + ); + assert.deepEqual(state.managedReads, [{ + source: 'user', + file: '9-4胖次6SS.yaml', + }]); + assert.deepEqual(state.legacyReads, []); + assert.equal(scheduledTasks.length, 1); + assert.equal(scheduledTasks[0][4], 500); + assert.equal(switchedPage, 'main'); + + const reloaded = new TaskGroupModel(); + await reloaded.load(); + const reloadedItem = reloaded.toJSON().groups[0].items[0]; + assert.equal(reloadedItem.managedSource, 'user'); + assert.equal(reloadedItem.managedFile, '9-4胖次6SS.yaml'); + assert.equal(state.saves, 2); +} + +async function verifyActivityAliases() { + const legacy = JSON.stringify({ + activeGroup: '旧组', + rootExtension: { preserved: true }, + groups: [{ + name: '旧组', + groupExtension: 'keep', + items: activityPlans.map(([legacyFile], index) => ({ + path: `resource/builtin_plans/${legacyFile}`, + kind: 'plan', + times: index + 1, + label: `旧活动计划 ${index + 1}`, + forceRetry: true, + autoFleetFallback: true, + itemExtension: { preserved: true }, + })), + }], + }); + const inventory = migratePresetInventory(legacy); + const state = installBridge(inventory.content); + const model = new TaskGroupModel(); + + await model.load(); + const migrated = model.toJSON(); + assert.equal(migrated.rootExtension.preserved, true); + assert.equal(migrated.groups[0].groupExtension, 'keep'); + migrated.groups[0].items.forEach((item, index) => { + const [legacyFile, managedFile] = activityPlans[index]; + assertManagedUserPlan( + item, + legacyFile, + managedFile, + inventory.userPlanDir, + ); + assert.equal(item.itemExtension.preserved, true); + assert.equal(item.forceRetry, true); + assert.equal(item.autoFleetFallback, true); + }); + assert.equal(state.saves, 1); +} + +async function verifyLegacyWeeklyMapSemantics() { + const legacy = JSON.stringify({ + activeGroup: '旧周常', + groups: [{ + name: '旧周常', + items: legacyWeeklyPlans.map(([legacyFile]) => ({ + path: `resource/builtin_plans/${legacyFile}`, + kind: 'plan', + times: 1, + label: legacyFile, + })), + }], + }); + const inventory = migratePresetInventory(legacy); + const state = installBridge(inventory.content); + const model = new TaskGroupModel(); + + await model.load(); + const items = model.toJSON().groups[0].items; + legacyWeeklyPlans.forEach(( + [legacyFile, managedFile, chapter, map], + index, + ) => { + assertManagedUserPlan( + items[index], + legacyFile, + managedFile, + inventory.userPlanDir, + ); + const content = fs.readFileSync(path.join( + inventory.userPlanDir, + managedFile, + ), 'utf8'); + const plan = yaml.load(content); + assert.equal(plan.chapter, chapter); + assert.equal(plan.map, map); + }); + assert.equal(state.saves, 1); +} + +function verifyCompatibilityPlanResources() { + for (const [file, chapter, map] of compatibilityPlanResources) { + const migrationFile = path.join( + __dirname, + '..', + 'resource', + 'migrations', + 'v6', + 'system_battle_plans', + file, + ); + const systemFile = path.join( + __dirname, + '..', + 'resource', + 'system_battle_plans', + file, + ); + assert.equal(fs.existsSync(systemFile), false); + const content = fs.readFileSync(migrationFile, 'utf8'); + const model = PlanModel.fromYaml(content, migrationFile); + assert.equal(model.data.chapter, chapter); + assert.equal(model.data.map, map); + + const raw = yaml.load(content); + for (const preset of raw.fleet_presets ?? []) { + for (const slot of preset.ships ?? []) { + if (!Array.isArray(slot?.candidates)) continue; + assert.equal( + slot.candidates.every(candidate => ( + candidate + && typeof candidate === 'object' + && typeof candidate.name === 'string' + && candidate.name.length > 0 + )), + true, + `${file} 的 candidates 必须是带 name 的对象`, + ); + } + } + } +} + +async function verifyInterimVersion() { + const inventory = migratePresetInventory(JSON.stringify({ + version: 2, + activeGroup: '中间版本', + groups: [{ + name: '中间版本', + items: [{ + path: 'resource/system_battle_plans/E1炸鱼.yaml', + managedSource: 'system', + managedFile: 'E1炸鱼.yaml', + kind: 'plan', + times: 1, + label: 'E1炸鱼', + }], + }], + })); + const state = installBridge(inventory.content); + const model = new TaskGroupModel(); + + await model.load(); + assert.equal(model.toJSON().version, 4); + assert.equal( + model.toJSON().groups[0].items[0].managedFile, + 'bettle-E1炸鱼.yaml', + ); + assert.equal( + model.toJSON().groups[0].items[0].managedSource, + 'user', + ); + assert.equal( + fs.existsSync(path.join( + inventory.userPlanDir, + 'bettle-E1炸鱼.yaml', + )), + true, + ); + assert.equal(state.saves, 1); +} + +async function run() { + await verifyRealLegacyFixtureLifecycle(); + await verifyActivityAliases(); + await verifyLegacyWeeklyMapSemantics(); + verifyCompatibilityPlanResources(); + await verifyInterimVersion(); + console.log('任务组加载、保存、执行、再次加载兼容测试通过'); +} + +run().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/adapter/ApiAdapter.ts b/src/adapter/ApiAdapter.ts new file mode 100644 index 0000000..6bf44a3 --- /dev/null +++ b/src/adapter/ApiAdapter.ts @@ -0,0 +1,43 @@ +/** 封装后端 HTTP 请求和 WebSocket 连接,向 Model 提供传输能力。 */ +export interface HttpTransport { + request(method: string, path: string, body?: unknown, timeoutMs?: number): Promise; +} + +export function createHttpTransport(baseUrl: string): HttpTransport { + return { + async request(method: string, path: string, body?: unknown, timeoutMs?: number): Promise { + const init: RequestInit = { + method, + headers: body === undefined ? undefined : { 'Content-Type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + }; + if (timeoutMs) { + const controller = new AbortController(); + init.signal = controller.signal; + setTimeout(() => controller.abort(), timeoutMs); + } + const response = await fetch(`${baseUrl}${path}`, init); + return response.json() as Promise; + }, + }; +} + +export interface WebSocketTransport { + connect(url: string, handlers: { + onMessage: (data: string) => void; + onOpen?: () => void; + onClose?: () => void; + onError?: () => void; + }): WebSocket; +} + +export const webSocketTransport: WebSocketTransport = { + connect(url, handlers): WebSocket { + const socket = new WebSocket(url); + socket.onmessage = event => handlers.onMessage(String(event.data)); + socket.onopen = () => handlers.onOpen?.(); + socket.onclose = () => handlers.onClose?.(); + socket.onerror = () => handlers.onError?.(); + return socket; + }, +}; diff --git a/src/adapter/IpcAdapter.ts b/src/adapter/IpcAdapter.ts new file mode 100644 index 0000000..72e9b8b --- /dev/null +++ b/src/adapter/IpcAdapter.ts @@ -0,0 +1,148 @@ +/** 封装 Electron IPC 文件与计划操作,提供 Renderer 侧仓储接口。 */ +import type { + DecisivePlanSettings, + ElectronBridge, + ShipLibraryManifest, +} from '../types/ipc.js'; + +export interface RendererIpc { + readonly bridge: ElectronBridge; +} + +export function getRendererIpc(): RendererIpc { + const bridge = window.electronBridge; + if (!bridge) throw new Error('Electron IPC bridge 不可用'); + return { bridge }; +} + +export type FileRepository = Pick; + +export const rendererFileRepository: FileRepository = { + readFile(path: string): Promise { + return getRendererIpc().bridge.readFile(path); + }, + + saveFile(path: string, content: string): Promise { + return getRendererIpc().bridge.saveFile(path, content); + }, + + appendFile(path: string, content: string): Promise { + return getRendererIpc().bridge.appendFile(path, content); + }, +}; + +export interface MapDataRepository { + read(path: string): Promise; +} + +export function createMapDataRepository( + files: FileRepository = rendererFileRepository, +): MapDataRepository { + return { + read(path: string): Promise { + return files.readFile(path); + }, + }; +} + +export const mapDataRepository = createMapDataRepository(); + +export interface DecisivePlanRepository { + loadSettings(): Promise; + loadChapter(chapter: number): Promise; + loadSystemDefaults(chapter: number): Promise; + saveSettings( + settings: DecisivePlanSettings, + ): Promise; + loadShipLibrary(): Promise; +} + +export const decisivePlanRepository: DecisivePlanRepository = { + async loadSettings(): Promise { + const lastSaved = await getRendererIpc() + .bridge + .getDecisivePlanSettings(); + return getRendererIpc().bridge.getDailyDecisivePlan( + lastSaved.chapter, + ); + }, + + loadChapter(chapter: number): Promise { + return getRendererIpc().bridge.getDailyDecisivePlan(chapter); + }, + + loadSystemDefaults(chapter: number): Promise { + return getRendererIpc().bridge.getSystemDailyDecisivePlan(chapter); + }, + + saveSettings( + settings: DecisivePlanSettings, + ): Promise { + return getRendererIpc().bridge.saveDailyDecisivePlan(settings); + }, + + loadShipLibrary(): Promise { + return getRendererIpc().bridge.getShipLibraryManifest(); + }, +}; + +export type FleetPlannerRepository = Pick< + ElectronBridge, + | 'getShipLibraryManifest' + | 'saveUserTeamPlan' + | 'listTeamPlans' + | 'getPlanManagement' + | 'exportUserPlans' + | 'setPlanUnlinkedIgnored' + | 'renameUserCombatPlan' + | 'deleteUserCombatPlan' + | 'deleteUserTeamPlan' +>; + +export const fleetPlannerRepository: FleetPlannerRepository = { + getShipLibraryManifest() { + return getRendererIpc().bridge.getShipLibraryManifest(); + }, + + saveUserTeamPlan(plan, overwrite, currentFile, source) { + return getRendererIpc().bridge.saveUserTeamPlan( + plan, + overwrite, + currentFile, + source, + ); + }, + + listTeamPlans() { + return getRendererIpc().bridge.listTeamPlans(); + }, + + getPlanManagement() { + return getRendererIpc().bridge.getPlanManagement(); + }, + + exportUserPlans(selections) { + return getRendererIpc().bridge.exportUserPlans(selections); + }, + + setPlanUnlinkedIgnored(kind, source, file, ignored) { + return getRendererIpc().bridge.setPlanUnlinkedIgnored( + kind, + source, + file, + ignored, + ); + }, + + renameUserCombatPlan(file, newName) { + return getRendererIpc().bridge.renameUserCombatPlan(file, newName); + }, + + deleteUserCombatPlan(file) { + return getRendererIpc().bridge.deleteUserCombatPlan(file); + }, + + deleteUserTeamPlan(file) { + return getRendererIpc().bridge.deleteUserTeamPlan(file); + }, +}; diff --git a/src/adapter/JsonAdapter.ts b/src/adapter/JsonAdapter.ts new file mode 100644 index 0000000..fd9b8cb --- /dev/null +++ b/src/adapter/JsonAdapter.ts @@ -0,0 +1,15 @@ +/** 统一 JSON 序列化、解析和对象类型校验。 */ +export interface JsonCodec { + parse(content: string): T; + stringify(value: unknown): string; +} + +export const jsonCodec: JsonCodec = { + parse(content: string): T { + return JSON.parse(content) as T; + }, + + stringify(value: unknown): string { + return JSON.stringify(value, null, 2); + }, +}; diff --git a/src/adapter/StorageAdapter.ts b/src/adapter/StorageAdapter.ts new file mode 100644 index 0000000..a24c555 --- /dev/null +++ b/src/adapter/StorageAdapter.ts @@ -0,0 +1,20 @@ +/** 定义键值存储契约,并封装浏览器 localStorage 实现。 */ +export interface StorageStore { + get(key: string): string | null; + set(key: string, value: string): void; + remove(key: string): void; +} + +export const browserStorageStore: StorageStore = { + get(key: string): string | null { + return localStorage.getItem(key); + }, + + set(key: string, value: string): void { + localStorage.setItem(key, value); + }, + + remove(key: string): void { + localStorage.removeItem(key); + }, +}; diff --git a/src/adapter/YamlAdapter.ts b/src/adapter/YamlAdapter.ts new file mode 100644 index 0000000..d5c99c2 --- /dev/null +++ b/src/adapter/YamlAdapter.ts @@ -0,0 +1,48 @@ +/** 统一 YAML 编解码及作战方案元数据读取。 */ +import * as yaml from 'js-yaml'; + +export interface YamlCodec { + parse(content: string): T; + stringify(value: unknown, options?: yaml.DumpOptions): string; +} + +export const yamlCodec: YamlCodec = { + parse(content: string): T { + return yaml.load(content) as T; + }, + + stringify(value: unknown, options?: yaml.DumpOptions): string { + return yaml.dump(value, options); + }, +}; + +export interface PlanMetadata { + chapter: number | string; + map: number | string; + event?: string; + mode?: string; + times?: number; + gap?: number; + task_type?: string; + campaign_name?: string; +} + +export function readPlanMetadata(content: string): PlanMetadata { + const raw = yamlCodec.parse>(content); + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + throw new Error('无效的方案文件'); + } + + const chapterRaw = raw.chapter; + const mapRaw = raw.map; + return { + chapter: typeof chapterRaw === 'string' ? chapterRaw : Number(chapterRaw) || 0, + map: typeof mapRaw === 'string' ? mapRaw : Number(mapRaw) || 0, + event: typeof raw.event === 'string' ? raw.event : undefined, + mode: typeof raw.mode === 'string' ? raw.mode : undefined, + times: raw.times == null ? undefined : Number(raw.times), + gap: raw.gap == null ? undefined : Number(raw.gap), + task_type: typeof raw.task_type === 'string' ? raw.task_type : undefined, + campaign_name: typeof raw.campaign_name === 'string' ? raw.campaign_name : undefined, + }; +} diff --git a/src/adapter/index.ts b/src/adapter/index.ts new file mode 100644 index 0000000..3bcb0a0 --- /dev/null +++ b/src/adapter/index.ts @@ -0,0 +1,6 @@ +/** 集中导出 Renderer 使用的 Adapter 实例和边界类型。 */ +export * from './YamlAdapter.js'; +export * from './JsonAdapter.js'; +export * from './IpcAdapter.js'; +export * from './ApiAdapter.js'; +export * from './StorageAdapter.js'; diff --git a/src/controller/app/AppController.ts b/src/controller/app/AppController.ts index 0a10a1e..b367ddd 100644 --- a/src/controller/app/AppController.ts +++ b/src/controller/app/AppController.ts @@ -1,3 +1,4 @@ +/** 作为 Renderer 组合根,初始化并协调页面、模型和功能控制器。 */ /** * AppController —— 核心控制器(瘦身版)。 * 协调 Model 和 View,委托子控制器与独立函数处理细分逻辑。 @@ -7,29 +8,38 @@ import { PlanPreviewView } from '../../view/plan/PlanPreviewView'; import { ConfigView } from '../../view/config/ConfigView'; import { TaskGroupView } from '../../view/taskGroup/TaskGroupView'; import { SetupWizardView } from '../../view/setup/SetupWizardView'; -import type { MainViewObject, TaskQueueItemVO } from '../../types/view'; +import { initAnimatedSelects } from '../../view/shared/AnimatedSelect'; import { ConfigModel } from '../../model/ConfigModel'; import { ApiClient } from '../../model/ApiClient'; -import type { ApiResponse } from '../../types/api'; import { Scheduler, CronScheduler } from '../../model/scheduler'; import { TaskGroupModel } from '../../model/TaskGroupModel'; import { TemplateModel } from '../../model/TemplateModel'; import { Logger } from '../../utils/Logger'; -import { showPrompt, showConfirm, showAlert } from '../shared/DialogHelper'; +import { showAlert } from '../shared/DialogHelper'; import { TemplateController } from '../template/TemplateController'; import { TaskGroupController } from '../taskGroup/TaskGroupController'; +import { loadManagedPlanToQueue } from '../taskGroup/queueLoader'; import { PlanController } from '../plan/PlanController'; +import { DecisivePlanController } from '../plan/DecisivePlanController'; +import { FleetPlannerController } from '../plan/FleetPlannerController'; import { StartupController } from '../startup/StartupController'; +import { + MigrationConflictController, +} from '../migration/MigrationConflictController'; +import { fleetPlannerRepository } from '../../adapter/IpcAdapter'; import { SchedulerBinder } from './SchedulerBinder'; import { ConfigController } from './ConfigController'; +import { NavigationController } from './NavigationController'; +import { OperationsController } from './OperationsController'; +import { SettingsController } from './SettingsController'; import { applyTheme, getThemeMode } from './theme'; import { buildMainViewObject, type RenderingState } from './rendering'; -import { PRIORITY_LABELS, STATUS_TEXT } from './constants'; export class AppController { private mainView: MainView; private planView: PlanPreviewView; + private fleetPlannerCtrl: FleetPlannerController; private configView: ConfigView; private taskGroupView: TaskGroupView; private setupView: SetupWizardView; @@ -42,30 +52,40 @@ export class AppController { private scheduler: Scheduler; private cronScheduler: CronScheduler; private schedulerBinder: SchedulerBinder; - private configCtrl: ConfigController; + private configCtrl!: ConfigController; + private navigationCtrl: NavigationController; + private operationsCtrl: OperationsController; + private settingsCtrl!: SettingsController; private appRoot = ''; private plansDir = ''; private configDir = ''; - private heartbeatTimer: ReturnType | null = null; private templateCtrl!: TemplateController; private taskGroupCtrl!: TaskGroupController; private planCtrl!: PlanController; + private decisivePlanCtrl: DecisivePlanController; + private migrationConflictCtrl: MigrationConflictController; private startupCtrl!: StartupController; /** 待安装的 GUI 版本号 */ pendingGuiVersion: string | null = null; constructor() { - this.mainView = new MainView(); - this.planView = new PlanPreviewView(); + this.mainView = new MainView(fleetPlannerRepository); + this.planView = new PlanPreviewView(fleetPlannerRepository); + this.fleetPlannerCtrl = new FleetPlannerController(); + this.decisivePlanCtrl = new DecisivePlanController(); + this.migrationConflictCtrl = new MigrationConflictController(); this.configView = new ConfigView(); this.taskGroupView = new TaskGroupView(); this.setupView = new SetupWizardView(); this.configModel = new ConfigModel(); this.taskGroupModel = new TaskGroupModel(); this.templateModel = new TemplateModel(); + this.fleetPlannerCtrl.setTaskGroupsProvider( + () => this.taskGroupModel.groups, + ); const rawPort = window.electronBridge?.getBackendPort?.(); let port = Number(rawPort); @@ -74,18 +94,31 @@ export class AppController { } this.api = new ApiClient(`http://localhost:${port}`); this.scheduler = new Scheduler(this.api); + this.navigationCtrl = new NavigationController({ + fleetPlannerController: this.fleetPlannerCtrl, + getPlanController: () => this.planCtrl, + refreshAdbStatus: () => this.settingsCtrl.refreshAdbStatus(), + refreshShipLibraryStatus: () => ( + this.settingsCtrl.refreshShipLibraryStatus() + ), + }); + this.operationsCtrl = new OperationsController(this.api, this.mainView, Logger); const cfg = this.configModel.current.daily_automation; + const gui = this.configModel.currentGuiAutomation; this.cronScheduler = new CronScheduler({ autoExercise: cfg.auto_exercise, - exerciseFleetId: cfg.exercise_fleet_id, + exerciseFleetId: cfg.exercise_fleet_id ?? 1, autoBattle: cfg.auto_battle, battleType: cfg.battle_type, - battleTimes: cfg.battle_times, + battleTimes: gui.battleTimes, autoNormalFight: cfg.auto_normal_fight, - autoLoot: cfg.auto_loot, - lootPlanIndex: cfg.loot_plan_index, - lootStopCount: cfg.loot_stop_count, + autoDecisive: gui.autoDecisive, + decisiveTemplateId: gui.decisiveTemplateId, + autoLoot: gui.autoLoot, + lootPlanSource: gui.lootPlanSource, + lootPlanId: gui.lootPlanId, + lootStopCount: gui.lootStopCount, }); this.schedulerBinder = new SchedulerBinder({ @@ -93,28 +126,34 @@ export class AppController { cronScheduler: this.cronScheduler, api: this.api, templateModel: this.templateModel, + configModel: this.configModel, renderMain: () => this.renderMain(), - updateOpsAvailability: (c) => this.updateOpsAvailability(c), + updateOpsAvailability: (c) => this.operationsCtrl.updateOpsAvailability(c), }); - - // configCtrl 创建延迟到 init()(需要子控制器引用) - this.configCtrl = null!; } /** 初始化:绑定事件、渲染初始状态、自动连接后端 */ init(): void { applyTheme(); - this.bindNavigation(); - this.bindActions(); + initAnimatedSelects(); + this.navigationCtrl.bindNavigation(); + this.navigationCtrl.bindPlanNavigation(); + this.bindQueueActions(); this.schedulerBinder.bindSchedulerCallbacks(); this.schedulerBinder.bindCronCallbacks(); + this.decisivePlanCtrl.bindActions(); + void this.decisivePlanCtrl.load(); + this.planCtrl = new PlanController(this.planView, { scheduler: this.scheduler, plansDir: '', renderMain: () => this.renderMain(), - switchPage: (p) => this.switchPage(p), + switchPage: (p) => this.navigationCtrl.switchPage(p, p === 'plan' ? 'scheme' : undefined), }); + this.fleetPlannerCtrl.onOpenBattlePlan = async (file, source) => { + await this.planCtrl.openManagedPlan(file, source); + }; this.planCtrl.bindActions(); this.taskGroupCtrl = new TaskGroupController( @@ -123,7 +162,7 @@ export class AppController { scheduler: this.scheduler, plansDir: '', renderMain: () => this.renderMain(), - switchPage: (p) => this.switchPage(p), + switchPage: (p) => this.navigationCtrl.switchPage(p, p === 'plan' ? 'scheme' : undefined), importTaskPreset: (preset, fp) => this.planCtrl.importTaskPreset(preset, fp), getCurrentPlan: () => this.planCtrl.getCurrentPlan(), setCurrentPlan: (plan, mapData) => this.planCtrl.setCurrentPlan(plan, mapData), @@ -131,6 +170,10 @@ export class AppController { closePresetDetail: () => this.planCtrl.closePresetDetail(), executePreset: () => this.planCtrl.executePreset(), getCurrentPresetInfo: () => this.planCtrl.getCurrentPresetInfo(), + pickManagedBattlePlan: () => this.planCtrl.pickManagedBattlePlan(), + openManagedPlan: (file, source) => ( + this.planCtrl.openManagedPlan(file, source) + ), }, ); this.taskGroupCtrl.bindActions(); @@ -141,7 +184,7 @@ export class AppController { ); this.templateCtrl.bindActions(); - // 现在可以创建 configCtrl(依赖 templateCtrl / startupCtrl 后续会赋值) + // 现在可以创建 configCtrl(startupCtrl 后续赋值) this.configCtrl = new ConfigController({ configModel: this.configModel, configView: this.configView, @@ -149,12 +192,24 @@ export class AppController { mainView: this.mainView, scheduler: this.scheduler, cronScheduler: this.cronScheduler, - templateCtrl: this.templateCtrl, - startupCtrl: null!, // 在 startupCtrl 创建后回填 + startupCtrl: null, configDir: this.configDir, }); + this.settingsCtrl = new SettingsController({ + configView: this.configView, + getConfigDir: () => this.configDir, + saveConfig: () => this.configCtrl.saveConfig(), + pickAutomationPlan: () => ( + this.planCtrl.pickManagedBattlePlanForAutomation() + ), + pickLootAutomationPlans: currentPlans => ( + this.planCtrl.pickManagedLootPlans(currentPlans) + ), + reloadShipLibrary: () => this.fleetPlannerCtrl.load(true), + }); + this.settingsCtrl.bindActions(); - this.bindOpsActions(); + this.operationsCtrl.bindOpsActions(); this.renderMain(); this.planView.render(null); @@ -194,7 +249,7 @@ export class AppController { this.taskGroupCtrl.host.plansDir = plansDir; this.planCtrl.host.plansDir = plansDir; // 同步 configCtrl 的 configDir - (this.configCtrl as any).host.configDir = configDir; + this.configCtrl.setConfigDir(configDir); }, initLogger: (b) => { Logger.init({ @@ -210,29 +265,35 @@ export class AppController { loadConfigAndSync: async () => { await this.configCtrl.loadConfig(); const da = this.configModel.current.daily_automation; + const gui = this.configModel.currentGuiAutomation; this.cronScheduler.updateConfig({ autoExercise: da.auto_exercise, - exerciseFleetId: da.exercise_fleet_id, + exerciseFleetId: da.exercise_fleet_id ?? 1, autoBattle: da.auto_battle, battleType: da.battle_type, - battleTimes: da.battle_times, + battleTimes: gui.battleTimes, autoNormalFight: da.auto_normal_fight, - autoLoot: da.auto_loot, - lootPlanIndex: da.loot_plan_index, - lootStopCount: da.loot_stop_count, + autoDecisive: gui.autoDecisive, + decisiveTemplateId: gui.decisiveTemplateId, + autoLoot: gui.autoLoot, + lootPlanSource: gui.lootPlanSource, + lootPlanId: gui.lootPlanId, + lootStopCount: gui.lootStopCount, }); }, detectAndApplyEmulator: () => this.configCtrl.detectAndApplyEmulator(), showSetupWizard: () => this.configCtrl.showSetupWizard(), loadModelsAndRender: async (b) => { await this.templateModel.init(b); + this.templateCtrl.renderLibrary(); this.configCtrl.renderConfig(); this.mainView.setDebugMode(localStorage.getItem('debugMode') === 'true'); - this.templateCtrl.renderLibrary(); await this.taskGroupModel.load(); this.taskGroupCtrl.render(); - this.updatePlanEmptyHint(); }, + reviewMigrationConflicts: () => ( + this.migrationConflictCtrl.reviewPending() + ), bindBackendLog: (b) => { if (b.onBackendLog) { b.onBackendLog((line) => { @@ -250,11 +311,11 @@ export class AppController { } }, renderMain: () => this.renderMain(), - startHeartbeat: () => this.startHeartbeat(), + startHeartbeat: () => this.startupCtrl.startHeartbeat(), }); // 回填 startupCtrl 引用 - (this.configCtrl as any).host.startupCtrl = this.startupCtrl; + this.configCtrl.setStartupController(this.startupCtrl); this.startupCtrl.run().catch((e) => { console.error('初始化失败:', e); @@ -262,136 +323,45 @@ export class AppController { }); } - // ════════════════════════════════════════ - // 页面导航 - // ════════════════════════════════════════ - - private bindNavigation(): void { - document.querySelectorAll('.nav-tab').forEach((tab) => { - tab.addEventListener('click', () => { - const pageId = tab.dataset['page']; - if (pageId) this.switchPage(pageId); - }); - }); - } - - private switchPage(pageId: string): void { - document.querySelectorAll('.nav-tab').forEach((t) => t.classList.remove('active')); - document.querySelector(`.nav-tab[data-page="${pageId}"]`)?.classList.add('active'); - document.querySelectorAll('.page').forEach((p) => p.classList.remove('active')); - document.getElementById(`page-${pageId}`)?.classList.add('active'); - if (pageId === 'config') this.refreshAdbStatus(); - } - // ════════════════════════════════════════ // 用户操作绑定 // ════════════════════════════════════════ - private bindActions(): void { - document.getElementById('btn-save-config')?.addEventListener('click', () => this.configCtrl.saveConfig()); - document.getElementById('btn-open-plans-dir')?.addEventListener('click', () => this.openFolder(this.plansDir)); - document.getElementById('btn-open-config-dir')?.addEventListener('click', () => this.openFolder(this.configDir)); - - document.getElementById('btn-browse-emu')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge) return; - const dir = await bridge.openDirectoryDialog('选择模拟器安装目录'); - if (dir) this.configView.setEmulatorPath(dir); - }); - - document.getElementById('btn-browse-python')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge) return; - const result = await bridge.openFileDialog([{ name: 'Python', extensions: ['exe'] }]); - if (result) this.configView.setPythonPath(result.path); - }); - - document.getElementById('btn-browse-backend-repo')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge) return; - const dir = await bridge.openDirectoryDialog('选择本地后端仓库目录'); - if (dir) this.configView.setBackendRepoPath(dir); - }); - - document.getElementById('btn-browse-cuda')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge) return; - const dir = await bridge.openDirectoryDialog('选择 CUDA Toolkit 根目录/bin 或 PyTorch torch\\lib 目录'); - if (dir) this.configView.setCudaPath(dir); - }); - - document.getElementById('btn-validate-cuda')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge?.validateCudaPath) return; - const cudaPath = this.configView.getCudaPath(); - if (!cudaPath) { this.configView.setCudaStatus('留空将使用系统环境', 'unknown'); return; } - this.configView.setCudaValidateLoading(true); + private bindQueueActions(): void { + document.getElementById('btn-stop-task')?.addEventListener('click', async () => { + if (this.scheduler.status === 'stopping') return; + Logger.info('正在停止当前任务,请等待后端确认…'); try { - const result = await bridge.validateCudaPath(cudaPath); - if (result.valid) { - this.configView.setCudaPath(result.path); - const kind = result.kind === 'runtime' ? 'Runtime' : 'Toolkit'; - this.configView.setCudaStatus(`✓ CUDA ${result.version ?? ''} ${kind}`.replace(/\s+/g, ' ').trim(), 'ok'); - } else { - this.configView.setCudaStatus(result.error ?? '无效路径', 'error'); - } - } catch { - this.configView.setCudaStatus('检测失败', 'error'); - } finally { - this.configView.setCudaValidateLoading(false); + await this.scheduler.stopRunning(); + this.schedulerBinder.currentProgress = ''; + this.schedulerBinder.trackedLoot = ''; + this.schedulerBinder.trackedShip = ''; + this.renderMain(); + Logger.info('当前任务已停止(任务已保留在队列中)'); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + Logger.error(`停止任务失败:${message}`); + this.renderMain(); } }); - - document.getElementById('btn-validate-python')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge?.validatePython) return; - const pythonPath = this.configView.getPythonPath(); - if (!pythonPath) { this.configView.setPythonStatus('"留空"将自动检测', 'unknown'); return; } - this.configView.setPythonValidateLoading(true); - try { - const result = await bridge.validatePython(pythonPath); - this.configView.setPythonStatus(result.valid ? '✓ ' + result.version : (result.error ?? '不兼容'), result.valid ? 'ok' : 'error'); - } catch { this.configView.setPythonStatus('检测失败', 'error'); } - finally { this.configView.setPythonValidateLoading(false); } - }); - - document.getElementById('btn-check-updates')?.addEventListener('click', async () => { - await this.checkUpdatesManually(); - }); - - document.getElementById('btn-check-adb')?.addEventListener('click', async () => { - const bridge = window.electronBridge; - if (!bridge?.checkAdbDevices) return; - const btn = document.getElementById('btn-check-adb') as HTMLButtonElement; - btn.disabled = true; btn.textContent = '检测中…'; - try { - const devices = await bridge.checkAdbDevices(); - const online = devices.filter(d => d.status === 'device'); - if (online.length === 0) { - await showAlert('ADB 检测', '未发现在线设备。\n请确认模拟器已启动。'); - } else if (online.length === 1) { - this.configView.setEmulatorSerial(online[0].serial); - Logger.info(`ADB 检测到在线设备: ${online[0].serial},已自动填入`); - } else { - const list = online.map(d => d.serial).join('\n'); - const ok = await showConfirm('ADB 检测', `发现 ${online.length} 个在线设备:\n\n${list}\n\n是否将第一个设备填入 serial?`); - if (ok) this.configView.setEmulatorSerial(online[0].serial); - } - } catch (e: any) { await showAlert('ADB 检测失败', e.message || String(e)); } - finally { btn.disabled = false; btn.textContent = '检测 ADB'; } - }); - - document.getElementById('btn-stop-task')?.addEventListener('click', async () => { - await this.scheduler.stopRunning(); - this.schedulerBinder.currentProgress = ''; - this.schedulerBinder.trackedLoot = ''; - this.schedulerBinder.trackedShip = ''; - this.renderMain(); - Logger.info('已停止当前任务(任务已保留在队列中)'); - }); document.getElementById('btn-clear-queue')?.addEventListener('click', () => { this.scheduler.clearQueue(); this.renderMain(); }); + document.getElementById('btn-import-plan')?.addEventListener('click', async () => { + const selected = await this.planCtrl.pickManagedBattlePlanForQueue(); + if (!selected) return; + try { + await loadManagedPlanToQueue(selected, { + scheduler: this.scheduler, + renderMain: () => this.renderMain(), + }); + } catch (error) { + await showAlert( + '无法加载出征计划', + error instanceof Error ? error.message : String(error), + ); + } + }); document.getElementById('btn-start-queue')?.addEventListener('click', () => { this.scheduler.startConsuming(); this.renderMain(); }); @@ -399,53 +369,6 @@ export class AppController { this.mainView.onRemoveQueueItem = (taskId) => { this.scheduler.removeTask(taskId); this.renderMain(); }; this.mainView.onMoveQueueItem = (from, to) => { this.scheduler.moveTask(from, to); this.renderMain(); }; - document.getElementById('btn-reset-accent')?.addEventListener('click', () => { - this.configView.resetAccentColor('#0f7dff'); - localStorage.setItem('accentColor', '#0f7dff'); - applyTheme(); - }); - document.getElementById('cfg-theme-mode')?.addEventListener('change', (e) => { - localStorage.setItem('themeMode', (e.target as HTMLSelectElement).value); - applyTheme(); - }); - document.getElementById('cfg-accent-color')?.addEventListener('input', (e) => { - localStorage.setItem('accentColor', (e.target as HTMLInputElement).value); - applyTheme(); - }); - } - - // ════════════════════════════════════════ - // 日常操作按钮 - // ════════════════════════════════════════ - - private bindOpsActions(): void { - const wrap = (btnId: string, label: string, action: () => Promise) => { - document.getElementById(btnId)?.addEventListener('click', async () => { - const btn = document.getElementById(btnId) as HTMLButtonElement; - btn.disabled = true; - this.mainView.setOpsStatus(`${label}中…`); - try { - const res = await action(); - if (res.success) { Logger.info(`${label}完成`); this.mainView.setOpsStatus(`${label}完成`); } - else { Logger.warn(`${label}失败: ${res.message ?? '未知错误'}`); this.mainView.setOpsStatus(`${label}失败`); } - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - Logger.error(`${label}异常: ${msg}`); this.mainView.setOpsStatus(`${label}异常`); - } finally { - btn.disabled = false; - setTimeout(() => { this.mainView.setOpsStatus(''); }, 3000); - } - }); - }; - wrap('btn-ops-expedition', '收取远征', () => this.api.expeditionCheck()); - wrap('btn-ops-reward', '收取奖励', () => this.api.rewardCollect()); - wrap('btn-ops-build-collect', '收取建造', () => this.api.buildCollect()); - wrap('btn-ops-cook', '食堂烹饪', () => this.api.cook()); - wrap('btn-ops-repair', '浴室修理', () => this.api.repairBath()); - } - - private updateOpsAvailability(connected: boolean): void { - this.mainView.setOpsAvailability(connected); } // ════════════════════════════════════════ @@ -458,6 +381,7 @@ export class AppController { currentProgress: this.schedulerBinder.currentProgress, trackedLoot: this.schedulerBinder.trackedLoot, trackedShip: this.schedulerBinder.trackedShip, + dailySortieStats: this.schedulerBinder.dailyStatsSnapshot, wsConnected: this.schedulerBinder.wsConnected, expeditionTimerText: this.schedulerBinder.expeditionTimerText, }; @@ -465,141 +389,6 @@ export class AppController { this.mainView.render(vo); } - // ════════════════════════════════════════ - // ADB / 心跳 / 辅助 - // ════════════════════════════════════════ - - private async refreshAdbStatus(): Promise { - this.configView.setAdbStatus('检测中…', 'unknown'); - try { - const res = await this.api.emulatorDevices(); - if (res.success && Array.isArray(res.data)) { - const online = res.data.filter(d => d.status === 'device'); - if (online.length > 0) { - this.configView.setAdbStatus(`在线 (${online.map(d => d.serial).join(', ')})`, 'online'); - } else { - this.configView.setAdbStatus('未发现在线设备', 'offline'); - } - } else { - this.configView.setAdbStatus(res.error || '检测失败', 'offline'); - } - } catch { - this.configView.setAdbStatus('检测失败(后端未启动?)', 'offline'); - } - } - - private async checkUpdatesManually(): Promise { - const bridge = window.electronBridge; - if (!bridge) return; - const updateMode = bridge.getUpdateMode?.() ?? 'auto'; - - const btn = document.getElementById('btn-check-updates') as HTMLButtonElement | null; - if (btn) { - btn.disabled = true; - btn.textContent = '检查中…'; - } - - try { - /* - * 测试期接口(后端源码更新)已停用,逻辑保留便于回滚恢复。 - try { - const updates = await bridge.checkUpdates(); - if (updates.hasUpdates) { - const confirmed = await showConfirm( - '后端更新', - `发现后端可更新,是否立即拉取并更新?`, - ); - if (confirmed) { - const pull = await bridge.pullUpdates(); - if (pull.success) { - Logger.info('后端更新完成'); - } else { - Logger.warn(`后端更新失败: ${pull.output || '未知错误'}`); - } - } else { - Logger.info('已取消后端更新'); - } - } else { - Logger.info('后端已是最新版本'); - } - } catch { - Logger.warn('后端更新检查失败'); - } - */ - Logger.info('已跳过后端源码更新检查(测试接口已停用)'); - - try { - const guiUpdate = await bridge.checkGuiUpdates?.(); - if (updateMode === 'auto') { - if (guiUpdate?.version) { - Logger.info(`检测到 GUI 新版本 v${guiUpdate.version},自动模式下将自动下载`); - } else { - Logger.info('GUI 已是最新版本'); - } - return; - } - if (guiUpdate?.version) { - const confirmed = await showConfirm( - 'GUI 更新', - `发现 GUI 新版本 v${guiUpdate.version},是否立即下载?`, - ); - if (confirmed) { - const result = await bridge.downloadGuiUpdate?.(); - if (result?.success) { - Logger.info(`GUI 更新下载开始: v${guiUpdate.version}`); - } else { - Logger.warn(`GUI 更新下载失败: ${result?.message || '未知错误'}`); - } - } else { - Logger.info('已取消 GUI 更新下载'); - } - } else { - Logger.info('GUI 已是最新版本'); - } - } catch { - Logger.warn('GUI 更新检查失败'); - } - } finally { - if (btn) { - btn.disabled = false; - btn.textContent = '立即检查更新'; - } - } - } - - private startHeartbeat(): void { - this.stopHeartbeat(); - let consecutiveFails = 0; - this.heartbeatTimer = setInterval(async () => { - try { - const alive = await this.scheduler.ping(); - if (alive) { consecutiveFails = 0; } else { consecutiveFails++; } - } catch { consecutiveFails++; } - if (consecutiveFails >= 3) { - Logger.error('后端连续 3 次心跳失败,尝试自动重启…'); - this.stopHeartbeat(); - const bridge = window.electronBridge; - if (bridge?.startBackend) { - await bridge.startBackend(); - this.startupCtrl.waitForBackendAndConnect(); - } - } - }, 30_000); - } - - private stopHeartbeat(): void { - if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } - } - - private updatePlanEmptyHint(): void { - if (this.plansDir) this.planView.setPlansDir(this.plansDir); - } - - private openFolder(folderPath: string): void { - if (!folderPath) return; - const bridge = window.electronBridge; - if (bridge?.openFolder) bridge.openFolder(folderPath); - } } // ── 入口:实例化并初始化 ── diff --git a/src/controller/app/AutomaticDecisiveTask.ts b/src/controller/app/AutomaticDecisiveTask.ts new file mode 100644 index 0000000..9430f65 --- /dev/null +++ b/src/controller/app/AutomaticDecisiveTask.ts @@ -0,0 +1,67 @@ +/** 分别从用户计划和系统预设构造自动决战请求。 */ +import type { DecisiveReq } from '../../types/api.js'; +import type { DecisivePlanSettings } from '../../types/ipc.js'; +import type { TaskTemplate } from '../../types/model.js'; + +function nonEmptyNames(value: string[] | undefined): string[] { + return Array.isArray(value) + ? value.map(name => name.trim()).filter(Boolean) + : []; +} + +function buildRequest( + chapterValue: unknown, + useQuickRepair: boolean, + level1Value: string[] | undefined, + level2Value: string[] | undefined, + flagshipValue?: string[], +): DecisiveReq { + const chapter = Math.trunc(Number(chapterValue)); + if (!Number.isFinite(chapter) || chapter < 1 || chapter > 6) { + throw new Error(`决战章节无效: ${String(chapterValue)}`); + } + + const level1 = nonEmptyNames(level1Value); + const level2 = nonEmptyNames(level2Value); + const flagshipPriority = nonEmptyNames(flagshipValue); + const request: DecisiveReq = { + type: 'decisive', + chapter, + decisive_rounds: 1, + use_quick_repair: useQuickRepair, + }; + if (level1.length > 0) request.level1 = level1; + if (level2.length > 0) request.level2 = level2; + if (flagshipPriority.length > 0) { + request.flagship_priority = flagshipPriority; + } + return request; +} + +/** 使用计划页面当前保存的用户方案。 */ +export function buildAutomaticDecisivePlanRequest( + plan: DecisivePlanSettings, +): DecisiveReq { + return buildRequest( + plan.chapter, + plan.useQuickRepair, + plan.level1, + plan.level2, + ); +} + +/** 使用只读内置模板,不混入用户方案。 */ +export function buildAutomaticDecisivePresetRequest( + template: TaskTemplate, +): DecisiveReq { + if (template.type !== 'decisive') { + throw new Error(`模板「${template.name}」不是决战模板`); + } + return buildRequest( + template.chapter, + template.use_quick_repair ?? true, + template.level1, + template.level2, + template.flagship_priority, + ); +} diff --git a/src/controller/app/ConfigController.ts b/src/controller/app/ConfigController.ts index 5de0114..37e93af 100644 --- a/src/controller/app/ConfigController.ts +++ b/src/controller/app/ConfigController.ts @@ -1,3 +1,4 @@ +/** 编排设置加载、环境检测、表单保存和配置持久化。 */ /** * ConfigController —— 配置管理子控制器。 * 负责 loadConfig / saveConfig / renderConfig / detectAndApplyEmulator / showSetupWizard @@ -7,12 +8,45 @@ import type { ConfigView } from '../../view/config/ConfigView'; import type { SetupWizardView } from '../../view/setup/SetupWizardView'; import type { MainView } from '../../view/main/MainView'; import type { Scheduler, CronScheduler } from '../../model/scheduler'; -import type { TemplateController } from '../template/TemplateController'; import type { StartupController } from '../startup/StartupController'; -import type { ConfigViewObject } from '../../types/view'; +import type { + EmulatorConfig, + GuiAutomationSettings, +} from '../../types/model.js'; +import type { ConfigViewObject } from '../../types/view.js'; +import type { + LegacyDecisiveAutomationSettings, +} from '../../shared/legacyDecisiveAutomation.js'; +import { + normalizeDecisiveAutomationSource, +} from '../../shared/decisiveAutomation.js'; +import { yamlCodec } from '../../adapter/YamlAdapter'; import { Logger } from '../../utils/Logger'; import { getThemeMode, getAccentColor, applyTheme } from './theme'; -import { showAlert } from '../shared/DialogHelper'; +import { showAlert, showSaveSuccess } from '../shared/DialogHelper'; + +const GUI_AUTOMATION_FIELDS = [ + 'expeditionInterval', + 'battleTimes', + 'autoDecisive', + 'decisiveTemplateId', + 'autoLoot', + 'lootPlanSource', + 'lootPlanId', + 'lootPlans', + 'lootStopCount', +] as const satisfies readonly (keyof GuiAutomationSettings)[]; + +function guiAutomationFieldMatches( + left: GuiAutomationSettings, + right: GuiAutomationSettings, + field: typeof GUI_AUTOMATION_FIELDS[number], +): boolean { + if (field === 'lootPlans') { + return JSON.stringify(left.lootPlans) === JSON.stringify(right.lootPlans); + } + return left[field] === right[field]; +} export interface ConfigControllerHost { readonly configModel: ConfigModel; @@ -21,33 +55,241 @@ export interface ConfigControllerHost { readonly mainView: MainView; readonly scheduler: Scheduler; readonly cronScheduler: CronScheduler; - templateCtrl: TemplateController; - startupCtrl: StartupController; + startupCtrl: StartupController | null; configDir: string; } export class ConfigController { constructor(private readonly host: ConfigControllerHost) {} + setConfigDir(configDir: string): void { + this.host.configDir = configDir; + } + + setStartupController(startupCtrl: StartupController): void { + this.host.startupCtrl = startupCtrl; + } + /** 从磁盘加载 usersettings.yaml */ async loadConfig(): Promise { const bridge = window.electronBridge; if (!bridge) return; try { const yamlStr = await bridge.readFile('usersettings.yaml'); - this.host.configModel.loadFromYaml(yamlStr); - Logger.debug('usersettings.yaml 已加载'); + if (!yamlStr.trim()) { + Logger.debug('usersettings.yaml 未找到,自动创建默认配置'); + const defaultYaml = this.host.configModel.toYaml(); + await bridge.saveFile('usersettings.yaml', defaultYaml); + Logger.info(`已创建默认配置文件: ${this.host.configDir}\\usersettings.yaml`); + } else { + this.host.configModel.loadFromYaml(yamlStr); + Logger.debug('usersettings.yaml 已加载'); + } } catch { Logger.debug('usersettings.yaml 未找到,自动创建默认配置'); const defaultYaml = this.host.configModel.toYaml(); await bridge.saveFile('usersettings.yaml', defaultYaml); Logger.info(`已创建默认配置文件: ${this.host.configDir}\\usersettings.yaml`); } + + const stored = await bridge.getGuiAutomationSettings?.(); + const migrated = this.host.configModel.migratedGuiAutomation; + const migratedDecisive = + this.host.configModel.migratedLegacyDecisiveAutomation; + const decisiveAutomation: Partial = {}; + if (typeof migratedDecisive.autoDecisive === 'boolean') { + decisiveAutomation.autoDecisive = + migratedDecisive.autoDecisive; + } + if (migratedDecisive.templateId) { + decisiveAutomation.decisiveTemplateId = + normalizeDecisiveAutomationSource( + migratedDecisive.templateId, + ); + } + const storedSettings = stored?.exists ? stored.settings : {}; + const merged = { + ...migrated, + ...decisiveAutomation, + ...storedSettings, + }; + if ( + storedSettings.autoLoot === true + && !Object.prototype.hasOwnProperty.call( + storedSettings, + 'lootPlanId', + ) + && !Object.prototype.hasOwnProperty.call(migrated, 'lootPlanId') + ) { + merged.autoLoot = false; + } + this.host.configModel.replaceGuiAutomation(merged); + + const hasLegacyGuiAutomation = Object.keys(migrated).length > 0; + const hasLegacyDecisiveAutomation = + Object.keys(decisiveAutomation).length > 0; + const storedIsPartial = stored?.exists === true + && GUI_AUTOMATION_FIELDS.some(field => ( + !Object.prototype.hasOwnProperty.call(storedSettings, field) + )); + let guiAutomationMigrated = false; + if ( + hasLegacyGuiAutomation + || hasLegacyDecisiveAutomation + || storedIsPartial + ) { + if (typeof bridge.setGuiAutomationSettings !== 'function') { + Logger.warn( + 'GUI 自动化配置迁移接口不可用,旧 YAML 字段已保留', + ); + } else { + const expected = structuredClone( + this.host.configModel.currentGuiAutomation, + ); + const saved = await bridge.setGuiAutomationSettings(expected); + for (const field of GUI_AUTOMATION_FIELDS) { + if (!guiAutomationFieldMatches(saved, expected, field)) { + throw new Error(`GUI 自动化配置回读字段不一致: ${field}`); + } + } + this.host.configModel.replaceGuiAutomation(saved); + if (hasLegacyGuiAutomation) { + this.host.configModel.markLegacyGuiAutomationMigrated(); + guiAutomationMigrated = true; + Logger.info( + '已按字段优先级将旧版 GUI 调度字段迁移到 gui_settings.json', + ); + } + } + } + const decisiveMigrated = + await this.migrateLegacyDecisiveAutomation(); + if ( + guiAutomationMigrated + || decisiveMigrated + ) { + await bridge.saveFile('usersettings.yaml', this.host.configModel.toYaml()); + } + } + + /** + * 将旧版决战开关和模板升级为正式配置,并归档票数保留原值。 + * 只有主进程写入、回读和用户提示全部完成后才允许清理 YAML。 + */ + private async migrateLegacyDecisiveAutomation(): + Promise { + const settings = + this.host.configModel.migratedLegacyDecisiveAutomation; + const invalidFields = + this.host.configModel.unmigratedLegacyDecisiveFields; + const suppliedFields = [ + 'autoDecisive', + 'ticketReserve', + 'templateId', + ] as const; + const supplied = suppliedFields.filter(field => ( + Object.prototype.hasOwnProperty.call(settings, field) + )); + if (supplied.length === 0) { + if (invalidFields.length > 0) { + await showAlert( + '旧版决战配置暂未迁移', + `以下字段格式无法识别,已继续保留在 usersettings.yaml:\n${ + invalidFields.join('、') + }`, + ); + } + return false; + } + + const bridge = window.electronBridge; + if ( + typeof bridge?.migrateLegacyDecisiveAutomation !== 'function' + ) { + await showAlert( + '旧版决战配置暂未迁移', + '当前配置迁移接口不可用,原字段仍保留在 usersettings.yaml。' + + '请完整重启 GUI 后重试。', + ); + return false; + } + + try { + const verified = + await bridge.migrateLegacyDecisiveAutomation(settings); + for (const field of supplied) { + if (verified[field] !== settings[field]) { + throw new Error(`回读字段不一致: ${field}`); + } + } + const summary = this.legacyDecisiveMigrationSummary(settings); + if (invalidFields.length > 0) { + summary.push( + `格式无法识别并继续保留在 usersettings.yaml:${ + invalidFields.join('、') + }`, + ); + } + summary.push( + '', + '自动决战开关和模板已升级为正式 GUI 自动化设置;' + + '决战票保留仅无损保存,不参与执行轮数。' + + '迁移不会覆盖当前决战计划。', + ); + await showAlert('旧版决战配置已升级', summary.join('\n')); + this.host.configModel.markLegacyDecisiveAutomationMigrated( + settings, + ); + Logger.info('已将旧版决战自动化原值迁移到 gui_settings.json'); + return true; + } catch (error) { + Logger.warn( + `旧版决战配置迁移失败: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + await showAlert( + '旧版决战配置迁移失败', + '原字段仍保留在 usersettings.yaml,将在下次启动时重试。', + ); + return false; + } + } + + /** 生成人能直接核对的逐字段迁移结果。 */ + private legacyDecisiveMigrationSummary( + settings: LegacyDecisiveAutomationSettings, + ): string[] { + const summary: string[] = []; + if ( + Object.prototype.hasOwnProperty.call(settings, 'autoDecisive') + ) { + summary.push( + `自动决战:${settings.autoDecisive ? '开启' : '关闭'}`, + ); + } + if ( + Object.prototype.hasOwnProperty.call(settings, 'ticketReserve') + ) { + summary.push(`决战票保留:${settings.ticketReserve}`); + } + if ( + Object.prototype.hasOwnProperty.call(settings, 'templateId') + ) { + summary.push(`决战模板:${settings.templateId}`); + } + return summary; } /** 渲染配置视图 */ renderConfig(): void { const cfg = this.host.configModel.current; + const gui = this.host.configModel.currentGuiAutomation; + const windowPreferences = window.electronBridge?.getWindowPreferences?.() ?? { + defaultWidth: 1280, + defaultHeight: 720, + rememberBounds: false, + }; const vo: ConfigViewObject = { emulatorType: cfg.emulator.type, emulatorPath: cfg.emulator.path || '', @@ -56,19 +298,23 @@ export class ConfigController { updateMode: window.electronBridge?.getUpdateMode?.() ?? (localStorage.getItem('updateMode') === 'manual' ? 'manual' : 'auto'), autoExpedition: cfg.daily_automation.auto_expedition, - expeditionInterval: cfg.daily_automation.expedition_interval, + expeditionInterval: gui.expeditionInterval, autoBattle: cfg.daily_automation.auto_battle, battleType: cfg.daily_automation.battle_type, autoExercise: cfg.daily_automation.auto_exercise, - exerciseFleetId: cfg.daily_automation.exercise_fleet_id, - battleTimes: cfg.daily_automation.battle_times, + exerciseFleetId: cfg.daily_automation.exercise_fleet_id ?? 1, + battleTimes: gui.battleTimes, autoNormalFight: cfg.daily_automation.auto_normal_fight, - autoDecisive: cfg.daily_automation.auto_decisive, - decisiveTicketReserve: cfg.daily_automation.decisive_ticket_reserve, - decisiveTemplateId: cfg.daily_automation.decisive_template_id, - autoLoot: cfg.daily_automation.auto_loot, - lootPlanIndex: cfg.daily_automation.loot_plan_index, - lootStopCount: cfg.daily_automation.loot_stop_count, + normalFightTasks: cfg.daily_automation.normal_fight_tasks, + autoDecisive: gui.autoDecisive, + decisiveTemplateId: gui.decisiveTemplateId, + autoLoot: gui.autoLoot, + lootPlanSource: gui.lootPlanSource, + lootPlanId: gui.lootPlanId, + lootPlans: structuredClone(gui.lootPlans), + lootStopCount: gui.lootStopCount, + logLevel: cfg.log.level, + logRoot: cfg.log.root, themeMode: getThemeMode(), accentColor: getAccentColor(), debugMode: localStorage.getItem('debugMode') === 'true', @@ -76,24 +322,84 @@ export class ConfigController { backendStartupMode: window.electronBridge?.getBackendStartupMode?.() ?? 'managed', backendRepoPath: window.electronBridge?.getBackendRepoPath?.() ?? '', ocrGpuMode: window.electronBridge?.getOcrGpuMode?.() ?? 'auto', + ocrGpu: cfg.ocr.gpu, + ocrMirror: cfg.ocr.mirror, + ocrConfidence: cfg.ocr.ship_name_match_confidence, + shipNameAliasesText: this.formatStringMap( + cfg.ocr.ship_name_aliases, + ), + shipNameCorrectionsText: this.formatStringMap( + cfg.ocr.ship_name_corrections, + ), cudaPath: window.electronBridge?.getCudaPath?.() ?? '', saveBackendScreenshots: window.electronBridge?.getSaveBackendScreenshots?.() ?? false, pythonPath: window.electronBridge?.getPythonPath?.() ?? '', + defaultWindowWidth: windowPreferences.defaultWidth, + defaultWindowHeight: windowPreferences.defaultHeight, + rememberWindowBounds: windowPreferences.rememberBounds, + operationDelayMin: cfg.operation_delay_min, + operationDelayMax: cfg.operation_delay_max, + dockFullDestroy: cfg.dock_full_destroy, + repairManually: cfg.repair_manually, + bathroomCount: cfg.bathroom_count, + destroyShipWorkMode: cfg.destroy_ship_work_mode, + destroyShipTypes: cfg.destroy_ship_types, + removeEquipmentMode: cfg.remove_equipment_mode, + planRoot: cfg.plan_root ?? '', }; this.host.configView.render(vo); - this.host.templateCtrl.populateDecisiveSelect(cfg.daily_automation.decisive_template_id); } /** 保存配置并同步各组件 */ async saveConfig(): Promise { - const collected = this.host.configView.collect(); + let collected: ConfigViewObject; + let shipNameAliases: Record; + let shipNameCorrections: Record; + try { + collected = this.host.configView.collect(); + shipNameAliases = this.parseStringMap( + collected.shipNameAliasesText, + '自定义舰名映射', + ); + shipNameCorrections = this.parseStringMap( + collected.shipNameCorrectionsText, + '识别纠错规则', + ); + } catch (error) { + await showAlert( + '设置格式错误', + error instanceof Error ? error.message : String(error), + ); + return; + } const bridge = window.electronBridge; + const saveApis = bridge && [ + bridge.saveFile, + bridge.setUpdateMode, + bridge.setBackendPort, + bridge.setBackendStartupMode, + bridge.setBackendRepoPath, + bridge.setOcrGpuMode, + bridge.setCudaPath, + bridge.setSaveBackendScreenshots, + bridge.setPythonPath, + bridge.setWindowPreferences, + bridge.setGuiAutomationSettings, + ]; + if (!bridge || !saveApis || saveApis.some(api => typeof api !== 'function')) { + await showAlert( + '保存失败', + '设置保存接口不完整,请完整重启 GUI 后再操作。', + ); + return; + } if (collected.backendStartupMode === 'external' && !collected.backendRepoPath.trim()) { await showAlert('请配置本地后端路径', '启用“使用本地后端”时必须选择本地后端仓库路径。'); return; } + try { // 界面设置 → localStorage localStorage.setItem('themeMode', collected.themeMode); localStorage.setItem('accentColor', collected.accentColor); @@ -128,6 +434,13 @@ export class ConfigController { if (bridge?.setPythonPath) { await bridge.setPythonPath(collected.pythonPath || null); } + if (bridge?.setWindowPreferences) { + await bridge.setWindowPreferences({ + defaultWidth: collected.defaultWindowWidth, + defaultHeight: collected.defaultWindowHeight, + rememberBounds: collected.rememberWindowBounds, + }); + } this.host.configModel.update({ emulator: { @@ -136,56 +449,111 @@ export class ConfigController { serial: collected.emulatorSerial || undefined, }, account: { game_app: collected.gameApp }, + ocr: { + ...this.host.configModel.current.ocr, + gpu: collected.ocrGpu, + mirror: collected.ocrMirror, + ship_name_match_confidence: collected.ocrConfidence, + ship_name_aliases: shipNameAliases, + ship_name_corrections: shipNameCorrections, + }, + log: { + ...this.host.configModel.current.log, + level: collected.logLevel, + root: collected.logRoot, + }, daily_automation: { + ...this.host.configModel.current.daily_automation, auto_expedition: collected.autoExpedition, - expedition_interval: collected.expeditionInterval, auto_battle: collected.autoBattle, battle_type: collected.battleType, auto_exercise: collected.autoExercise, exercise_fleet_id: collected.exerciseFleetId, - battle_times: collected.battleTimes, auto_normal_fight: collected.autoNormalFight, - auto_decisive: collected.autoDecisive, - decisive_ticket_reserve: collected.decisiveTicketReserve, - decisive_template_id: collected.decisiveTemplateId, - auto_loot: collected.autoLoot, - loot_plan_index: collected.lootPlanIndex, - loot_stop_count: collected.lootStopCount, + normal_fight_tasks: collected.normalFightTasks, }, + operation_delay_min: collected.operationDelayMin, + operation_delay_max: collected.operationDelayMax, + dock_full_destroy: collected.dockFullDestroy, + repair_manually: collected.repairManually, + bathroom_count: collected.bathroomCount, + destroy_ship_work_mode: collected.destroyShipWorkMode, + destroy_ship_types: collected.destroyShipTypes, + remove_equipment_mode: collected.removeEquipmentMode, + plan_root: collected.planRoot || undefined, }); + this.host.configModel.updateGuiAutomation({ + expeditionInterval: collected.expeditionInterval, + battleTimes: collected.battleTimes, + autoDecisive: collected.autoDecisive, + decisiveTemplateId: collected.decisiveTemplateId, + autoLoot: collected.autoLoot, + lootPlanSource: collected.lootPlanSource, + lootPlanId: collected.lootPlanId, + lootPlans: collected.lootPlans, + lootStopCount: collected.lootStopCount, + }); + const savedGuiAutomation = await bridge.setGuiAutomationSettings( + this.host.configModel.currentGuiAutomation, + ); + for (const field of GUI_AUTOMATION_FIELDS) { + if (!guiAutomationFieldMatches( + savedGuiAutomation, + this.host.configModel.currentGuiAutomation, + field, + )) { + throw new Error(`GUI 自动化配置回读字段不一致: ${field}`); + } + } + this.host.configModel.replaceGuiAutomation(savedGuiAutomation); + this.host.configModel.markLegacyGuiAutomationMigrated(); + // 同步 CronScheduler const da = this.host.configModel.current.daily_automation; + const gui = this.host.configModel.currentGuiAutomation; this.host.cronScheduler.updateConfig({ autoExercise: da.auto_exercise, - exerciseFleetId: da.exercise_fleet_id, + exerciseFleetId: da.exercise_fleet_id ?? 1, autoBattle: da.auto_battle, battleType: da.battle_type, - battleTimes: da.battle_times, + battleTimes: gui.battleTimes, autoNormalFight: da.auto_normal_fight, - autoLoot: da.auto_loot, - lootPlanIndex: da.loot_plan_index, - lootStopCount: da.loot_stop_count, + autoDecisive: gui.autoDecisive, + decisiveTemplateId: gui.decisiveTemplateId, + autoLoot: gui.autoLoot, + lootPlanSource: gui.lootPlanSource, + lootPlanId: gui.lootPlanId, + lootStopCount: gui.lootStopCount, }); - // 远征检查间隔 - this.host.scheduler.setExpeditionInterval(da.expedition_interval); + // 自动远征开关与检查间隔 + this.host.scheduler.setAutoExpedition(da.auto_expedition); + this.host.scheduler.setExpeditionInterval(gui.expeditionInterval); const yamlStr = this.host.configModel.toYaml(); - console.log('保存配置:\n', yamlStr); - - if (bridge) { - await bridge.saveFile('usersettings.yaml', yamlStr); + if (collected.debugMode) { + Logger.debug(`保存配置:\n${yamlStr}`, 'config'); } - Logger.info('配置已保存'); + await bridge.saveFile('usersettings.yaml', yamlStr); + + Logger.info('设置已保存,后端启动项将在重启后生效'); + } catch (error) { + await showAlert( + '保存失败', + error instanceof Error ? error.message : String(error), + ); + return; + } + showSaveSuccess('设置保存成功'); // 未连接 → 尝试重连 if (this.host.scheduler.status === 'not_connected') { const alive = await this.host.scheduler.ping(); if (alive) { Logger.info('配置已更新,正在重新连接模拟器…'); - this.host.startupCtrl.startSystem(); + this.host.startupCtrl?.startSystem(); } else { Logger.warn('后端未运行,请重启应用'); } @@ -204,13 +572,15 @@ export class ConfigController { const result = await bridge.detectEmulator(); if (!result) return; - const patch: { type?: string; path?: string; serial?: string } = {}; + const patch: Partial = {}; if (!cfg.emulator.path && result.path) patch.path = result.path; if (!cfg.emulator.serial && result.serial) patch.serial = result.serial; if (result.type) patch.type = result.type; if (Object.keys(patch).length > 0) { - this.host.configModel.update({ emulator: patch as any }); + this.host.configModel.update({ + emulator: { ...cfg.emulator, ...patch }, + }); const yamlStr = this.host.configModel.toYaml(); await bridge.saveFile('usersettings.yaml', yamlStr); Logger.debug(`自动检测到模拟器: type=${result.type} path=${result.path} serial=${result.serial}`); @@ -282,4 +652,40 @@ export class ConfigController { }; }); } + + private formatStringMap(value: Record): string { + if (Object.keys(value).length === 0) return ''; + return yamlCodec.stringify( + value, + { lineWidth: -1, noRefs: true }, + ).trim(); + } + + private parseStringMap( + source: string, + label: string, + ): Record { + if (!source.trim()) return {}; + let parsed: unknown; + try { + parsed = yamlCodec.parse(source); + } catch (error) { + throw new Error( + `${label}不是合法 YAML: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error(`${label}必须使用“识别名称: 标准名称”的映射格式`); + } + const output: Record = {}; + for (const [key, value] of Object.entries(parsed)) { + if (!key.trim() || typeof value !== 'string' || !value.trim()) { + throw new Error(`${label}中的键和值都必须是非空文字`); + } + output[key.trim()] = value.trim(); + } + return output; + } } diff --git a/src/controller/app/NavigationController.ts b/src/controller/app/NavigationController.ts new file mode 100644 index 0000000..4790ce7 --- /dev/null +++ b/src/controller/app/NavigationController.ts @@ -0,0 +1,104 @@ +/** 处理主页面导航、标签切换和当前页面状态。 */ +import { FleetPlannerController } from '../plan/FleetPlannerController'; +import { PlanController } from '../plan/PlanController'; + +export interface NavigationControllerHost { + readonly fleetPlannerController: FleetPlannerController; + getPlanController(): PlanController; + refreshAdbStatus(): Promise; + refreshShipLibraryStatus(): Promise; +} + +export class NavigationController { + private navTabs: HTMLElement | null = null; + private navIndicator: HTMLElement | null = null; + private navResizeObserver: ResizeObserver | null = null; + + constructor(private readonly host: NavigationControllerHost) {} + + bindNavigation(): void { + this.navTabs = document.querySelector('.nav-tabs'); + this.navIndicator = this.navTabs?.querySelector( + '.nav-tab-indicator', + ) ?? null; + + document.querySelectorAll('.nav-tab').forEach((tab) => { + tab.addEventListener('click', () => { + const pageId = tab.dataset['page']; + if (pageId) this.switchPage(pageId); + }); + }); + + this.updateNavigationIndicator(); + requestAnimationFrame(() => { + this.navIndicator?.classList.add('is-ready'); + }); + + if (this.navTabs) { + this.navResizeObserver?.disconnect(); + this.navResizeObserver = new ResizeObserver( + () => this.updateNavigationIndicator(), + ); + this.navResizeObserver.observe(this.navTabs); + } + } + + bindPlanNavigation(): void { + document.querySelectorAll('[data-plan-tab]').forEach((tab) => { + tab.addEventListener('click', () => { + const tabId = tab.dataset['planTab']; + if (tabId) this.showPlanTab(tabId); + }); + }); + } + + showPlanTab(tabId: string): void { + document.querySelectorAll('[data-plan-tab]').forEach((tab) => { + const active = tab.dataset['planTab'] === tabId; + tab.classList.toggle('active', active); + tab.setAttribute('aria-selected', String(active)); + }); + document.querySelectorAll('[data-plan-panel]').forEach((panel) => { + const active = panel.dataset['planPanel'] === tabId; + panel.classList.toggle('active', active); + panel.hidden = !active; + }); + if (tabId === 'fleet') { + void this.host.fleetPlannerController.load(); + } else if (tabId === 'scheme') { + void this.host.getPlanController().ensureDefaultPlan(); + } else if (tabId === 'manage') { + void this.host.fleetPlannerController.loadManagement(); + } + } + + switchPage(pageId: string, planTab?: string): void { + document.querySelectorAll('.nav-tab').forEach((t) => t.classList.remove('active')); + document.querySelector(`.nav-tab[data-page="${pageId}"]`)?.classList.add('active'); + this.updateNavigationIndicator(); + document.querySelectorAll('.page').forEach((p) => p.classList.remove('active')); + document.getElementById(`page-${pageId}`)?.classList.add('active'); + if (pageId === 'plan') { + if (planTab) this.showPlanTab(planTab); + const activeTab = document.querySelector('[data-plan-tab].active') + ?.dataset['planTab'] ?? 'fleet'; + if (activeTab === 'fleet') { + void this.host.fleetPlannerController.load(); + } + } + if (pageId === 'config') { + void this.host.refreshAdbStatus(); + void this.host.refreshShipLibraryStatus(); + } + } + + private updateNavigationIndicator(): void { + const activeTab = this.navTabs?.querySelector('.nav-tab.active'); + if (!activeTab || !this.navIndicator) return; + + this.navIndicator.style.width = `${activeTab.offsetWidth}px`; + this.navIndicator.style.transform = ( + `translate3d(${activeTab.offsetLeft}px, 0, 0)` + ); + } +} diff --git a/src/controller/app/OperationsController.ts b/src/controller/app/OperationsController.ts new file mode 100644 index 0000000..1ccbf6d --- /dev/null +++ b/src/controller/app/OperationsController.ts @@ -0,0 +1,43 @@ +/** 编排远征收取、奖励领取等常用自动化操作。 */ +import type { ApiResponse } from '../../types/api.js'; +import { ApiClient } from '../../model/ApiClient'; +import { MainView } from '../../view/main/MainView'; +import { Logger } from '../../utils/Logger'; + +export class OperationsController { + constructor( + private readonly api: ApiClient, + private readonly mainView: MainView, + private readonly logger: typeof Logger, + ) {} + + bindOpsActions(): void { + const wrap = (btnId: string, label: string, action: () => Promise) => { + document.getElementById(btnId)?.addEventListener('click', async () => { + const btn = document.getElementById(btnId) as HTMLButtonElement; + btn.disabled = true; + this.mainView.setOpsStatus(`${label}中…`); + try { + const res = await action(); + if (res.success) { this.logger.info(`${label}完成`); this.mainView.setOpsStatus(`${label}完成`); } + else { Logger.warn(`${label}失败: ${res.message ?? '未知错误'}`); this.mainView.setOpsStatus(`${label}失败`); } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`${label}异常: ${msg}`); this.mainView.setOpsStatus(`${label}异常`); + } finally { + btn.disabled = false; + setTimeout(() => { this.mainView.setOpsStatus(''); }, 3000); + } + }); + }; + wrap('btn-ops-expedition', '收取远征', () => this.api.expeditionCheck()); + wrap('btn-ops-reward', '收取奖励', () => this.api.rewardCollect()); + wrap('btn-ops-build-collect', '收取建造', () => this.api.buildCollect()); + wrap('btn-ops-cook', '食堂烹饪', () => this.api.cook()); + wrap('btn-ops-repair', '浴室修理', () => this.api.repairBath()); + } + + updateOpsAvailability(connected: boolean): void { + this.mainView.setOpsAvailability(connected); + } +} diff --git a/src/controller/app/SchedulerBinder.ts b/src/controller/app/SchedulerBinder.ts index 50ffa1b..8eea5cd 100644 --- a/src/controller/app/SchedulerBinder.ts +++ b/src/controller/app/SchedulerBinder.ts @@ -1,20 +1,50 @@ +/** 绑定 Scheduler 回调并同步日志、进度、队列和连接状态。 */ /** * SchedulerBinder —— 调度器回调绑定子控制器。 * 封装 Scheduler / CronScheduler 的回调绑定逻辑 + 关联的可变状态。 */ -import { TaskPriority, type Scheduler, type SchedulerStatus, type CronScheduler } from '../../model/scheduler'; +import { + TaskPriority, + type CronScheduler, + type LogicalTaskCancelReason, + type Scheduler, + type SchedulerStatus, +} from '../../model/scheduler'; import type { ApiClient } from '../../model/ApiClient'; +import type { ConfigModel } from '../../model/ConfigModel'; import type { TemplateModel } from '../../model/TemplateModel'; import { PlanModel } from '../../model/PlanModel'; -import type { NormalFightReq } from '../../types/api'; +import { DailySortieStats } from '../../model/statistics/DailySortieStats'; +import type { + DecisiveReq, + EventFightReq, + NormalFightReq, +} from '../../types/api.js'; +import type { DailySortieStatsSnapshot } from '../../types/statistics.js'; +import { + normalizeLootAutomationPlan, + type LootPlanSource, +} from '../../shared/lootPlans.js'; +import { + SYSTEM_DECISIVE_TEMPLATE_ID, + USER_DECISIVE_PLAN_ID, + normalizeDecisiveAutomationSource, + type DecisiveAutomationSource, +} from '../../shared/decisiveAutomation.js'; import { Logger } from '../../utils/Logger'; import { normalizeSelectedNodesForBackend } from '../plan/selectedNodes'; +import { buildPlanQueueRequest } from '../taskGroup/queueLoader'; +import { + buildAutomaticDecisivePlanRequest, + buildAutomaticDecisivePresetRequest, +} from './AutomaticDecisiveTask'; export interface SchedulerBinderHost { readonly scheduler: Scheduler; readonly cronScheduler: CronScheduler; readonly api: ApiClient; readonly templateModel: TemplateModel; + readonly configModel: ConfigModel; renderMain(): void; updateOpsAvailability(connected: boolean): void; } @@ -26,20 +56,30 @@ export class SchedulerBinder { // ── 状态(从 AppController 迁移而来) ── private pendingExerciseTaskId: string | null = null; private pendingBattleTaskId: string | null = null; + private pendingDecisiveTaskId: string | null = null; private pendingLootTaskId: string | null = null; + private pendingNormalFightTaskIds = new Set(); private exerciseTotal = SchedulerBinder.DEFAULT_EXERCISE_TOTAL; private exerciseCurrent = 0; private exerciseRoundInProgress = false; private lastParsedLogMessage = ''; private lastParsedLogTaskId = ''; private lastParsedLogAt = 0; + private readonly dailySortieStats = new DailySortieStats(); + private dailyStatsRefreshTimer: ReturnType | null = null; currentProgress = ''; trackedLoot = ''; trackedShip = ''; wsConnected = false; expeditionTimerText = '--:--'; - constructor(private readonly host: SchedulerBinderHost) {} + constructor(private readonly host: SchedulerBinderHost) { + this.scheduleDailyStatsRefresh(); + } + + get dailyStatsSnapshot(): DailySortieStatsSnapshot { + return this.dailySortieStats.getSnapshot(); + } /** 绑定 Scheduler 回调 */ bindSchedulerCallbacks(): void { @@ -61,15 +101,14 @@ export class SchedulerBinder { this.host.renderMain(); }, - onTaskCompleted: (taskId, success, _result, _error) => { - this.currentProgress = ''; - this.resetExerciseProgress(); - this.lastParsedLogMessage = ''; - this.lastParsedLogTaskId = ''; - this.lastParsedLogAt = 0; - this.trackedLoot = ''; - this.trackedShip = ''; - if (taskId === this.pendingExerciseTaskId) { + onTaskCompleted: (_taskId, _success, _result, _error) => { + this.resetTaskProgress(); + this.host.renderMain(); + }, + + onLogicalTaskCompleted: (logicalId, success) => { + this.resetTaskProgress(); + if (logicalId === this.pendingExerciseTaskId) { if (success) { this.host.cronScheduler.markExerciseCompleted(); } else { @@ -77,14 +116,30 @@ export class SchedulerBinder { } this.pendingExerciseTaskId = null; } - if (taskId === this.pendingBattleTaskId) { + if (logicalId === this.pendingBattleTaskId) { this.host.cronScheduler.markBattleHandled(); this.pendingBattleTaskId = null; } - if (taskId === this.pendingLootTaskId) { + if (logicalId === this.pendingDecisiveTaskId) { + this.host.cronScheduler.markDecisiveHandled(); + this.pendingDecisiveTaskId = null; + } + if (logicalId === this.pendingLootTaskId) { this.host.cronScheduler.markLootHandled(); this.pendingLootTaskId = null; } + if (this.pendingNormalFightTaskIds.delete(logicalId) + && this.pendingNormalFightTaskIds.size === 0) { + this.host.cronScheduler.markNormalFightHandled(); + } + this.host.renderMain(); + }, + + onLogicalTaskCanceled: (logicalId, reason) => { + if (!this.host.scheduler.currentRunningTask) { + this.resetTaskProgress(); + } + this.handleLogicalTaskCanceled(logicalId, reason); this.host.renderMain(); }, @@ -128,6 +183,72 @@ export class SchedulerBinder { this.exerciseRoundInProgress = false; } + /** 清理当前物理轮次在界面上的进度和计数。 */ + private resetTaskProgress(): void { + this.currentProgress = ''; + this.resetExerciseProgress(); + this.lastParsedLogMessage = ''; + this.lastParsedLogTaskId = ''; + this.lastParsedLogAt = 0; + this.trackedLoot = ''; + this.trackedShip = ''; + } + + /** + * 同步被取消的父任务。 + * 用户删除或清空表示本时段主动放弃;系统停止则只释放 pending, + * 允许下次启动后重新触发。 + */ + private handleLogicalTaskCanceled( + logicalId: string, + reason: LogicalTaskCancelReason, + ): void { + const allowRetry = reason === 'system_stopped'; + + if (logicalId === this.pendingExerciseTaskId) { + if (allowRetry) { + this.host.cronScheduler.clearExercisePending(); + } else { + this.host.cronScheduler.markExerciseHandled(); + } + this.pendingExerciseTaskId = null; + } + if (logicalId === this.pendingBattleTaskId) { + if (allowRetry) { + this.host.cronScheduler.clearBattlePending(); + } else { + this.host.cronScheduler.markBattleHandled(); + } + this.pendingBattleTaskId = null; + } + if (logicalId === this.pendingDecisiveTaskId) { + if (allowRetry) { + this.host.cronScheduler.clearDecisivePending(); + } else { + this.host.cronScheduler.markDecisiveHandled(); + } + this.pendingDecisiveTaskId = null; + } + if (logicalId === this.pendingLootTaskId) { + if (allowRetry) { + this.host.cronScheduler.clearLootPending(); + } else { + this.host.cronScheduler.markLootHandled(); + } + this.pendingLootTaskId = null; + } + if ( + this.pendingNormalFightTaskIds.delete(logicalId) + && this.pendingNormalFightTaskIds.size === 0 + ) { + if (allowRetry) { + this.host.cronScheduler.clearNormalFightPending(); + } else { + this.host.cronScheduler.markNormalFightHandled(); + } + } + } + /** * 后端回传战役剩余次数时,同步更新队列中战役任务的 remainingTimes。 * 仅当后端报告的剩余次数小于当前任务记录的剩余次数时才更新(避免覆盖用户设置)。 @@ -150,7 +271,7 @@ export class SchedulerBinder { // 仅当后端报告的剩余次数更小时才同步(说明有其他战役消耗了次数) if (remains < campaignRemaining) { - const diff = campaignRemaining - remains; + let diff = campaignRemaining - remains; Logger.info(`战役次数同步: 后端报告剩余 ${remains},前端队列待执行 ${campaignRemaining},减少 ${diff} 次`); // 优先从队列末尾的战役任务扣减 @@ -159,6 +280,7 @@ export class SchedulerBinder { if (task.type !== 'campaign') continue; const deduct = Math.min(diff, task.remainingTimes); task.remainingTimes -= deduct; + diff -= deduct; if (task.remainingTimes <= 0) { scheduler.removeTask(task.id); } @@ -172,7 +294,9 @@ export class SchedulerBinder { * 返回 true 表示有可视状态变化,需要触发 renderMain。 */ private consumeRuntimeLogMessage(message: string): boolean { - let changed = false; + const normalized = message.trim(); + let changed = this.dailySortieStats.consume(normalized); + if (changed) this.scheduleDailyStatsRefresh(); const lootMatch = message.match(/\[UI\] 战利品数量: (\d+\/\d+)/); if (lootMatch && lootMatch[1] !== this.trackedLoot) { @@ -197,7 +321,6 @@ export class SchedulerBinder { const running = this.host.scheduler.currentRunningTask; if (running?.type !== 'exercise') return changed; - const normalized = message.trim(); const now = Date.now(); const duplicate = this.lastParsedLogTaskId === running.id @@ -213,6 +336,29 @@ export class SchedulerBinder { return changed || progressChanged; } + /** 在掉落提示到期或跨日时主动刷新,不依赖下一条后端日志。 */ + private scheduleDailyStatsRefresh(): void { + if (this.dailyStatsRefreshTimer) { + clearTimeout(this.dailyStatsRefreshTimer); + } + + const now = Date.now(); + const tomorrow = new Date(now); + tomorrow.setHours(24, 0, 0, 0); + const noticeUntil = this.dailySortieStats.getSnapshot(now) + .dropNotice?.visibleUntil; + const nextRefreshAt = noticeUntil && noticeUntil > now + ? Math.min(noticeUntil, tomorrow.getTime()) + : tomorrow.getTime(); + const delay = Math.max(25, nextRefreshAt - now + 25); + + this.dailyStatsRefreshTimer = setTimeout(() => { + this.dailySortieStats.getSnapshot(); + this.host.renderMain(); + this.scheduleDailyStatsRefresh(); + }, delay); + } + /** * 处理后端 stdout 日志(用于 WS 日志延迟/缺失时的进度兜底)。 */ @@ -340,8 +486,16 @@ export class SchedulerBinder { Logger.info(`定时任务「${taskKey}」已触发`); }, - onLootDue: (planIndex, stopCount) => { - this.autoLoadLootTask(planIndex, stopCount); + onLootDue: (source, planId, stopCount) => { + this.autoLoadLootTask(source, planId, stopCount); + }, + + onNormalFightDue: () => { + void this.autoLoadNormalFightTasks(); + }, + + onDecisiveDue: (source) => { + void this.autoLoadDecisiveTask(source); }, onLog: (level, message) => { @@ -350,36 +504,217 @@ export class SchedulerBinder { }); } - /** 自动战利品:加载内置捞胖次方案并加入队列 */ - private async autoLoadLootTask(planIndex: number, stopCount: number): Promise { - const tpl = this.host.templateModel.get('builtin_farm_loot'); - if (!tpl) { - Logger.error('自动战利品:未找到内置 builtin_farm_loot 模板'); - this.host.cronScheduler.clearLootPending(); + /** 自动决战:用户计划和系统预设保持独立,不互相回退。 */ + private async autoLoadDecisiveTask( + source: DecisiveAutomationSource, + ): Promise { + try { + const selectedSource = normalizeDecisiveAutomationSource(source); + let label: string; + let request: DecisiveReq; + if (selectedSource === USER_DECISIVE_PLAN_ID) { + const bridge = window.electronBridge; + if (typeof bridge?.getDecisivePlanSettings !== 'function') { + throw new Error('决战计划读取接口不可用'); + } + request = buildAutomaticDecisivePlanRequest( + await bridge.getDecisivePlanSettings(), + ); + label = '用户计划'; + } else { + const template = this.host.templateModel.get( + SYSTEM_DECISIVE_TEMPLATE_ID, + ); + if (!template || template.type !== 'decisive') { + throw new Error('找不到系统决战预设'); + } + request = buildAutomaticDecisivePresetRequest(template); + label = '系统预设'; + } + const id = this.host.scheduler.addTask( + `自动决战·${label}`, + 'decisive', + request, + TaskPriority.DAILY, + 1, + ); + this.pendingDecisiveTaskId = id; + Logger.info( + `自动决战已加入队列 (${label}, 第 ${request.chapter} 章 ×1)`, + ); + this.host.scheduler.startConsuming(); + } catch (error) { + Logger.error( + `自动决战加载失败: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + this.host.cronScheduler.clearDecisivePending(); + } + } + + /** 自动出征:按照 usersettings.yaml 中的顺序加载并加入队列。 */ + private async autoLoadNormalFightTasks(): Promise { + const queuedTasks = [ + this.host.scheduler.currentRunningTask, + ...this.host.scheduler.taskQueue, + ...this.host.scheduler.waitingTaskList.map(item => item.task), + ]; + if (queuedTasks.some(task => ( + task?.unlimited === true + && task.name.startsWith('自动出征·') + ))) { + this.host.cronScheduler.markNormalFightHandled(); + Logger.info('已有无限自动出征任务在运行,本次不重复加入'); + return; + } + + const tasks = this.host.configModel.current.daily_automation.normal_fight_tasks; + if (tasks.length === 0) { + Logger.warn('自动出征已启用,但任务列表为空'); + this.host.cronScheduler.markNormalFightHandled(); + return; + } + const bridge = window.electronBridge; + if (!bridge) { + this.host.cronScheduler.clearNormalFightPending(); + return; + } + + let loaded = 0; + for (const task of tasks) { + try { + const resolved = await this.resolveNormalFightPlan(task.name); + if (!resolved) throw new Error(`找不到出征计划: ${task.name}`); + const plan = PlanModel.fromYaml(resolved.content, resolved.path); + const { req: request, selectedFleetId } = buildPlanQueueRequest( + { + path: resolved.path, + kind: 'plan', + times: task.times ?? 1, + label: plan.mapName, + fleet_id: task.fleet_id, + fleetPresetIndex: task.fleet_preset_index, + }, + plan, + resolved.path, + ); + const id = this.host.scheduler.addTask( + `自动出征·${plan.mapName}`, + plan.isEvent ? 'event_fight' : 'normal_fight', + request, + TaskPriority.DAILY, + task.times ?? Number.POSITIVE_INFINITY, + plan.data.stop_condition, + undefined, + selectedFleetId, + undefined, + undefined, + undefined, + true, + ); + this.pendingNormalFightTaskIds.add(id); + loaded++; + } catch (error) { + Logger.error( + `自动出征加载「${task.name}」失败: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + + if (loaded === 0) { + this.host.cronScheduler.clearNormalFightPending(); return; } - const paths = tpl.planPaths ?? []; - const planPath = paths[planIndex] ?? paths[0]; - if (!planPath) { - Logger.error('自动战利品:模板缺少方案文件'); + Logger.info(`自动出征已按顺序加入 ${loaded} 个任务`); + this.host.scheduler.startConsuming(); + } + + /** 支持绝对路径、plan_root 目录和 GUI 自带后端数据目录。 */ + private async resolveNormalFightPlan( + name: string, + ): Promise<{ path: string; content: string } | null> { + const bridge = window.electronBridge; + if (!bridge) return null; + const root = this.host.configModel.current.plan_root?.replace(/[\\/]$/, ''); + const suffixes = /\.ya?ml$/i.test(name) ? [name] : [name, `${name}.yaml`]; + const candidates = new Set(suffixes); + for (const category of ['normal_fight', 'event']) { + for (const suffix of suffixes) { + if (root) candidates.add(`${root}/${category}/${suffix}`); + candidates.add(`autowsgr/data/plan/${category}/${suffix}`); + } + } + for (const candidate of candidates) { + if (bridge.readCombatPlanFile) { + const result = await bridge.readCombatPlanFile(candidate); + if ( + result.success + && result.content?.trim() + && result.path + ) { + return { + path: result.runtimePath ?? result.path, + content: result.content, + }; + } + continue; + } + const content = await bridge.readFile(candidate); + if (content.trim()) return { path: candidate, content }; + } + return null; + } + + /** 自动战利品:读取受管计划并只应用设置页停止数量。 */ + private async autoLoadLootTask( + source: LootPlanSource, + planId: string, + stopCount: number, + ): Promise { + const managedPlan = normalizeLootAutomationPlan({ + source, + file: planId, + name: planId, + }); + if (!managedPlan) { + Logger.error(`自动战利品加载失败: 未知计划 ${String(planId)}`); this.host.cronScheduler.clearLootPending(); return; } + const managedFile = managedPlan.file; const bridge = window.electronBridge; if (!bridge) { this.host.cronScheduler.clearLootPending(); return; } try { - const content = await bridge.readFile(planPath); + const loaded = await bridge.readManagedCombatPlan( + managedPlan.source, + managedFile, + ); + if (!loaded.success || !loaded.content || !loaded.path) { + throw new Error(loaded.error || `无法读取 ${managedFile}`); + } + const planPath = loaded.runtimePath ?? loaded.path; + const content = loaded.content; const plan = PlanModel.fromYaml(content, planPath); - const resolvedPlanId = await bridge.resolveAppPath(plan.fileName); - const req: NormalFightReq = { - type: 'normal_fight', - plan_id: resolvedPlanId, - times: 1, - gap: plan.data.gap ?? 0, - }; + const req: NormalFightReq | EventFightReq = plan.isEvent + ? { + type: 'event_fight', + plan_id: planPath, + times: 1, + gap: plan.data.gap ?? 0, + fleet_id: plan.data.fleet_id ?? 1, + } + : { + type: 'normal_fight', + plan_id: planPath, + times: 1, + gap: plan.data.gap ?? 0, + }; if (plan.data.selected_nodes.length > 0) { req.plan = req.plan ?? {}; req.plan.selected_nodes = normalizeSelectedNodesForBackend(plan.data.selected_nodes); @@ -391,7 +726,7 @@ export class SchedulerBinder { const stopCondition = { loot_count_ge: stopCount }; const id = this.host.scheduler.addTask( `自动刷胖次·${plan.mapName}`, - 'normal_fight', + plan.isEvent ? 'event_fight' : 'normal_fight', req, TaskPriority.DAILY, 99, diff --git a/src/controller/app/SettingsController.ts b/src/controller/app/SettingsController.ts new file mode 100644 index 0000000..e1245c3 --- /dev/null +++ b/src/controller/app/SettingsController.ts @@ -0,0 +1,599 @@ +/** 编排设置页的环境检测、设备连接、资料库更新和主题交互。 */ +import type { ConfigView } from '../../view/config/ConfigView'; +import type { ManagedBattlePlanSelection } from '../../types/ipc.js'; +import type { LootAutomationPlan } from '../../shared/lootPlans.js'; +import { ApiClient } from '../../model/ApiClient'; +import { Logger } from '../../utils/Logger'; +import { showAlert, showConfirm } from '../shared/DialogHelper'; +import { applyTheme } from './theme'; + +export interface SettingsControllerHost { + readonly configView: ConfigView; + getConfigDir(): string; + saveConfig(): Promise; + pickAutomationPlan(): Promise; + pickLootAutomationPlans( + currentPlans: readonly LootAutomationPlan[], + ): Promise; + reloadShipLibrary(): Promise; +} + +export class SettingsController { + private shipLibraryUpdating = false; + + constructor(private readonly host: SettingsControllerHost) {} + + bindActions(): void { + const { configView } = this.host; + window.electronBridge?.onShipLibraryUpdateProgress?.((progress) => { + if (this.shipLibraryUpdating) { + configView.setShipLibraryStatus(progress.message, 'unknown'); + } + }); + + document.getElementById('btn-save-config')?.addEventListener( + 'click', + () => void this.host.saveConfig(), + ); + document.getElementById('btn-open-config-dir')?.addEventListener( + 'click', + () => this.openFolder(this.host.getConfigDir()), + ); + document.getElementById('btn-browse-emu')?.addEventListener( + 'click', + () => void this.browseDirectory( + '选择模拟器安装目录', + path => configView.setEmulatorPath(path), + ), + ); + document.getElementById('btn-browse-python')?.addEventListener( + 'click', + () => void this.browsePython(), + ); + document.getElementById('btn-browse-backend-repo')?.addEventListener( + 'click', + () => void this.browseDirectory( + '选择本地后端仓库目录', + path => configView.setBackendRepoPath(path), + ), + ); + document.getElementById('btn-browse-cuda')?.addEventListener( + 'click', + () => void this.browseDirectory( + '选择 CUDA Toolkit 根目录/bin 或 PyTorch torch\\lib 目录', + path => configView.setCudaPath(path), + ), + ); + document.getElementById('btn-browse-log-root')?.addEventListener( + 'click', + () => void this.browseDirectory( + '选择后端日志目录', + path => configView.setLogRoot(path), + ), + ); + document.getElementById('btn-browse-plan-root')?.addEventListener( + 'click', + () => void this.browseDirectory( + '选择后端作战方案根目录', + path => configView.setPlanRoot(path), + ), + ); + document.getElementById('btn-add-normal-fight-task')?.addEventListener( + 'click', + () => void this.selectAutomationPlan(), + ); + document.getElementById('btn-load-loot-plans')?.addEventListener( + 'click', + () => void this.selectLootAutomationPlans(), + ); + document.getElementById('btn-check-backend')?.addEventListener( + 'click', + () => void this.checkBackend(), + ); + document.getElementById('btn-validate-cuda')?.addEventListener( + 'click', + () => void this.validateCuda(), + ); + document.getElementById('btn-validate-python')?.addEventListener( + 'click', + () => void this.validatePython(), + ); + document.getElementById('btn-check-updates')?.addEventListener( + 'click', + () => void this.checkUpdatesManually(), + ); + document.getElementById('btn-update-ship-library')?.addEventListener( + 'click', + () => void this.updateShipLibrary(), + ); + document.getElementById('btn-connect-adb')?.addEventListener( + 'click', + () => void this.changeAdbConnection('connect'), + ); + document.getElementById('btn-disconnect-adb')?.addEventListener( + 'click', + () => void this.changeAdbConnection('disconnect'), + ); + document.getElementById('btn-check-adb')?.addEventListener( + 'click', + () => void this.checkAdbDevices(), + ); + document.getElementById('btn-reset-accent')?.addEventListener( + 'click', + () => { + configView.resetAccentColor('#0f7dff'); + localStorage.setItem('accentColor', '#0f7dff'); + applyTheme(); + }, + ); + document.getElementById('cfg-theme-mode')?.addEventListener( + 'change', + (event) => { + localStorage.setItem( + 'themeMode', + (event.target as HTMLSelectElement).value, + ); + applyTheme(); + }, + ); + document.getElementById('cfg-accent-color')?.addEventListener( + 'input', + (event) => { + localStorage.setItem( + 'accentColor', + (event.target as HTMLInputElement).value, + ); + applyTheme(); + }, + ); + } + + async refreshAdbStatus(): Promise { + const { configView } = this.host; + configView.setAdbStatus('检测中…', 'unknown'); + const bridge = window.electronBridge; + if (!bridge?.checkAdbDevices) { + configView.setAdbStatus('ADB 功能不可用', 'offline'); + return; + } + try { + const devices = await bridge.checkAdbDevices(); + const online = devices.filter(device => device.status === 'device'); + const configuredSerial = configView.getEmulatorSerial(); + if ( + configuredSerial + && online.some(device => device.serial === configuredSerial) + ) { + configView.setAdbStatus( + `在线 (${configuredSerial})`, + 'online', + ); + } else if (online.length > 0) { + configView.setAdbStatus( + `当前地址未连接(发现 ${online.map(device => device.serial).join(', ')})`, + 'offline', + ); + } else { + configView.setAdbStatus('未发现在线设备', 'offline'); + } + } catch { + configView.setAdbStatus('ADB 检测失败', 'offline'); + } + } + + async refreshShipLibraryStatus(): Promise { + if (this.shipLibraryUpdating) return; + const bridge = window.electronBridge; + if (!bridge?.getShipLibraryStatus) return; + try { + const status = await bridge.getShipLibraryStatus(); + if (status.error) { + this.host.configView.setShipLibraryStatus(status.error, 'error'); + } else if (!status.exists) { + this.host.configView.setShipLibraryStatus( + '尚未建立本地资料库', + 'unknown', + ); + } else if (status.missingAssets > 0) { + this.host.configView.setShipLibraryStatus( + `已收录 ${status.shipCount} 艘,缺少 ${status.missingAssets} 个资源`, + 'error', + ); + } else { + const updatedAt = status.generatedAt + ? new Date(status.generatedAt).toLocaleString( + 'zh-CN', + { hour12: false }, + ) + : '时间未知'; + this.host.configView.setShipLibraryStatus( + `已收录 ${status.shipCount} 艘 · ${updatedAt}`, + 'ok', + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.host.configView.setShipLibraryStatus( + `状态读取失败: ${message}`, + 'error', + ); + } + } + + private async browseDirectory( + title: string, + applyPath: (path: string) => void, + ): Promise { + const path = await window.electronBridge?.openDirectoryDialog(title); + if (path) applyPath(path); + } + + private async browsePython(): Promise { + const bridge = window.electronBridge; + if (!bridge) return; + const result = await bridge.openFileDialog([ + { name: 'Python', extensions: ['exe'] }, + ]); + if (result) this.host.configView.setPythonPath(result.path); + } + + private async selectAutomationPlan(): Promise { + const selected = await this.host.pickAutomationPlan(); + if (!selected) return; + try { + const result = await window.electronBridge?.readManagedCombatPlan( + selected.plan.source, + selected.plan.file, + ); + if (!result?.success || !result.path) { + throw new Error(result?.error || '无法读取所选出征计划'); + } + const fleetPresetIndex = selected.fleetPresetIndex; + if (fleetPresetIndex === undefined) { + throw new Error('自动出征计划必须选择使用舰队'); + } + const fleetName = selected.plan.fleets[fleetPresetIndex]?.name; + if (!fleetName) throw new Error('所选使用舰队不存在'); + this.host.configView.setNormalFightPlan( + result.path, + fleetPresetIndex, + fleetName, + ); + } catch (error) { + await showAlert( + '无法加载出征计划', + error instanceof Error ? error.message : String(error), + ); + } + } + + private async selectLootAutomationPlans(): Promise { + const plans = await this.host.pickLootAutomationPlans( + this.host.configView.getLootPlans(), + ); + if (plans) this.host.configView.setLootPlans(plans); + } + + private async checkBackend(): Promise { + const button = document.getElementById( + 'btn-check-backend', + ) as HTMLButtonElement | null; + if (!button) return; + const port = this.host.configView.getBackendPort(); + button.disabled = true; + button.textContent = '检测中…'; + this.host.configView.setBackendStatus('正在连接', 'unknown'); + try { + const result = await new ApiClient( + `http://localhost:${port}`, + ).health(); + this.host.configView.setBackendStatus( + result.success + ? '接口正常' + : (result.error || result.message || '接口异常'), + result.success ? 'ok' : 'error', + ); + } catch { + this.host.configView.setBackendStatus('无法连接', 'error'); + } finally { + button.disabled = false; + button.textContent = '检测'; + } + } + + private async validateCuda(): Promise { + const bridge = window.electronBridge; + if (!bridge?.validateCudaPath) return; + const { configView } = this.host; + const cudaPath = configView.getCudaPath(); + configView.setCudaValidateLoading(true); + configView.setCudaStatus( + '检测中', + 'unknown', + '正在检测 PyTorch、CUDA 和显卡', + ); + try { + const result = await bridge.validateCudaPath(cudaPath); + if (result.valid) { + if (result.path) configView.setCudaPath(result.path); + const details = [ + result.device ?? 'CUDA 可用', + result.version ? `CUDA ${result.version}` : null, + result.torchVersion ? `PyTorch ${result.torchVersion}` : null, + ].filter(Boolean); + configView.setCudaStatus( + result.version ? `CUDA ${result.version}` : 'GPU 可用', + 'ok', + details.join(';'), + ); + } else { + const error = result.error ?? '未检测到可用 CUDA'; + const shortStatus = ( + result.torchVersion?.includes('+cpu') + || error.includes('未检测到可用 CUDA') + ) + ? '仅 CPU' + : error.includes('路径') + || error.includes('目录') + || error.includes('Runtime DLL') + ? '路径无效' + : '检测失败'; + configView.setCudaStatus(shortStatus, 'error', error); + } + } catch { + configView.setCudaStatus('检测失败', 'error', '硬件检测失败'); + } finally { + configView.setCudaValidateLoading(false); + } + } + + private async validatePython(): Promise { + const bridge = window.electronBridge; + if (!bridge?.validatePython) return; + const { configView } = this.host; + const pythonPath = configView.getPythonPath(); + if (!pythonPath) { + configView.setPythonStatus('"留空"将自动检测', 'unknown'); + return; + } + configView.setPythonValidateLoading(true); + try { + const result = await bridge.validatePython(pythonPath); + configView.setPythonStatus( + result.valid ? `✓ ${result.version}` : (result.error ?? '不兼容'), + result.valid ? 'ok' : 'error', + ); + } catch { + configView.setPythonStatus('检测失败', 'error'); + } finally { + configView.setPythonValidateLoading(false); + } + } + + private async checkAdbDevices(): Promise { + const bridge = window.electronBridge; + if (!bridge?.checkAdbDevices) return; + const button = document.getElementById( + 'btn-check-adb', + ) as HTMLButtonElement | null; + if (!button) return; + button.disabled = true; + button.textContent = '检测中…'; + try { + const devices = await bridge.checkAdbDevices(); + const online = devices.filter(device => device.status === 'device'); + if (online.length === 0) { + await showAlert( + 'ADB 检测', + '未发现在线设备。\n请确认模拟器已启动。', + ); + } else if (online.length === 1) { + this.host.configView.setEmulatorSerial(online[0].serial); + this.host.configView.setAdbStatus( + `在线 (${online[0].serial})`, + 'online', + ); + Logger.info(`ADB 检测到在线设备: ${online[0].serial},已自动填入`); + } else { + const list = online.map(device => device.serial).join('\n'); + const confirmed = await showConfirm( + 'ADB 检测', + `发现 ${online.length} 个在线设备:\n\n${list}\n\n是否将第一个设备填入 serial?`, + ); + if (confirmed) { + this.host.configView.setEmulatorSerial(online[0].serial); + this.host.configView.setAdbStatus( + `在线 (${online[0].serial})`, + 'online', + ); + } + } + } catch (error) { + await showAlert( + 'ADB 检测失败', + error instanceof Error ? error.message : String(error), + ); + } finally { + button.disabled = false; + button.textContent = '检测 ADB'; + } + } + + private async changeAdbConnection( + action: 'connect' | 'disconnect', + ): Promise { + const bridge = window.electronBridge; + const method = action === 'connect' + ? bridge?.connectAdbDevice + : bridge?.disconnectAdbDevice; + if (!method) return; + + const serial = this.host.configView.getEmulatorSerial(); + if (!serial) { + await showAlert( + 'ADB 地址为空', + '请先填写 ADB 地址,例如 127.0.0.1:16384。', + ); + return; + } + + const buttonId = action === 'connect' + ? 'btn-connect-adb' + : 'btn-disconnect-adb'; + const button = document.getElementById( + buttonId, + ) as HTMLButtonElement | null; + const originalText = button?.textContent ?? ''; + if (button) { + button.disabled = true; + button.textContent = action === 'connect' ? '连接中…' : '断开中…'; + } + this.host.configView.setAdbStatus( + action === 'connect' ? '正在连接' : '正在断开', + 'unknown', + ); + + try { + const result = await method(serial); + if (result.success) { + const connected = action === 'connect'; + this.host.configView.setAdbStatus( + connected ? `在线 (${serial})` : '已断开', + connected ? 'online' : 'offline', + ); + Logger.info(`ADB ${connected ? '连接' : '断开'}成功: ${serial}`); + } else { + this.host.configView.setAdbStatus( + `${action === 'connect' ? '连接' : '断开'}失败`, + 'offline', + ); + await showAlert( + `ADB ${action === 'connect' ? '连接' : '断开'}失败`, + result.message, + ); + } + } catch (error) { + this.host.configView.setAdbStatus('ADB 命令执行失败', 'offline'); + await showAlert( + 'ADB 操作失败', + error instanceof Error ? error.message : String(error), + ); + } finally { + if (button) { + button.disabled = false; + button.textContent = originalText; + } + } + } + + private async updateShipLibrary(): Promise { + const bridge = window.electronBridge; + if (!bridge?.updateShipLibrary || this.shipLibraryUpdating) return; + this.shipLibraryUpdating = true; + this.host.configView.setShipLibraryUpdateLoading(true); + this.host.configView.setShipLibraryStatus( + '正在准备更新…', + 'unknown', + ); + try { + const result = await bridge.updateShipLibrary(); + if (!result.success) { + const message = result.error || result.failures?.[0] || '未知错误'; + this.host.configView.setShipLibraryStatus( + `更新失败: ${message}`, + 'error', + ); + Logger.error(`舰船资料库更新失败: ${message}`); + return; + } + const summary = [ + `${result.ship_count ?? 0} 艘`, + `新增 ${result.added ?? 0}`, + `变化 ${result.updated ?? 0}`, + `下载 ${result.downloaded ?? 0}`, + ].join(','); + this.host.configView.setShipLibraryStatus( + `更新完成:${summary}`, + 'ok', + ); + Logger.info(`舰船资料库更新完成:${summary}`); + await this.host.reloadShipLibrary(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.host.configView.setShipLibraryStatus( + `更新失败: ${message}`, + 'error', + ); + Logger.error(`舰船资料库更新异常: ${message}`); + } finally { + this.shipLibraryUpdating = false; + this.host.configView.setShipLibraryUpdateLoading(false); + } + } + + private async checkUpdatesManually(): Promise { + const bridge = window.electronBridge; + if (!bridge) return; + const updateMode = bridge.getUpdateMode?.() ?? 'auto'; + const button = document.getElementById( + 'btn-check-updates', + ) as HTMLButtonElement | null; + if (button) { + button.disabled = true; + button.textContent = '检查中…'; + } + + try { + Logger.info('已跳过后端源码更新检查(测试接口已停用)'); + try { + const guiUpdate = await bridge.checkGuiUpdates?.(); + if (guiUpdate?.status === 'error') { + Logger.warn(`GUI 更新检查失败: ${guiUpdate.message}`); + return; + } + if (updateMode === 'auto') { + if (guiUpdate?.status === 'available') { + Logger.info( + `检测到 GUI 新版本 v${guiUpdate.version},自动模式下将自动下载`, + ); + } else { + Logger.info('GUI 已是最新版本'); + } + return; + } + if (guiUpdate?.status === 'available') { + const confirmed = await showConfirm( + 'GUI 更新', + `发现 GUI 新版本 v${guiUpdate.version},是否立即下载?`, + ); + if (confirmed) { + const result = await bridge.downloadGuiUpdate?.(); + if (result?.success) { + Logger.info(`GUI 更新下载开始: v${guiUpdate.version}`); + } else { + Logger.warn( + `GUI 更新下载失败: ${result?.message || '未知错误'}`, + ); + } + } else { + Logger.info('已取消 GUI 更新下载'); + } + } else { + Logger.info('GUI 已是最新版本'); + } + } catch { + Logger.warn('GUI 更新检查失败'); + } + } finally { + if (button) { + button.disabled = false; + button.textContent = '立即检查更新'; + } + } + } + + private openFolder(folderPath: string): void { + if (!folderPath) return; + window.electronBridge?.openFolder?.(folderPath); + } +} diff --git a/src/controller/app/constants.ts b/src/controller/app/constants.ts index 7fe1a93..82df283 100644 --- a/src/controller/app/constants.ts +++ b/src/controller/app/constants.ts @@ -1,5 +1,6 @@ +/** 维护任务优先级、状态文案和修理模式等界面常量。 */ import { TaskPriority } from '../../model/scheduler'; -import { REPAIR_MODE_NAMES } from '../../types/model'; +import { REPAIR_MODE_NAMES } from '../../types/model.js'; /** 优先级 → 中文标签 */ export const PRIORITY_LABELS: Record = { diff --git a/src/controller/app/index.ts b/src/controller/app/index.ts deleted file mode 100644 index 6ed9cb3..0000000 --- a/src/controller/app/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AppController } from './AppController'; diff --git a/src/controller/app/rendering.ts b/src/controller/app/rendering.ts index cabfd39..610af23 100644 --- a/src/controller/app/rendering.ts +++ b/src/controller/app/rendering.ts @@ -1,5 +1,12 @@ -import type { MainViewObject, TaskQueueItemVO } from '../../types/view'; +/** 把调度器和游戏状态转换为主页面 ViewObject。 */ +import type { + CurrentFleetShipVO, + MainViewObject, + TaskQueueItemVO, +} from '../../types/view.js'; import type { Scheduler } from '../../model/scheduler'; +import type { TaskRequest } from '../../types/api.js'; +import type { DailySortieStatsSnapshot } from '../../types/statistics.js'; import { PRIORITY_LABELS, STATUS_TEXT } from './constants'; export interface RenderingState { @@ -7,6 +14,7 @@ export interface RenderingState { currentProgress: string; trackedLoot: string; trackedShip: string; + dailySortieStats: DailySortieStatsSnapshot; wsConnected: boolean; expeditionTimerText: string; } @@ -19,9 +27,68 @@ export function buildAcquisitionText(trackedLoot: string, trackedShip: string): return parts.length > 0 ? parts.join(' | ') : undefined; } +function normalizedShipName(value: unknown): string { + return typeof value === 'string' ? value.trim() : ''; +} + +function fleetRuleShip(rule: unknown): CurrentFleetShipVO | null { + if (typeof rule === 'string') { + const name = rule.trim(); + return name ? { name } : null; + } + if (!rule || typeof rule !== 'object') return null; + const record = rule as Record; + const searchName = normalizedShipName(record['search_name']); + const name = normalizedShipName(record['name']) || searchName; + if (!name) return null; + return searchName && searchName !== name + ? { name, searchName } + : { name }; +} + +function firstCandidateShip(rule: unknown): CurrentFleetShipVO | null { + if (!rule || typeof rule !== 'object') return null; + const candidates = (rule as Record)['candidates']; + if (!Array.isArray(candidates)) return null; + return fleetRuleShip(candidates[0]); +} + +/** 仅返回当前请求中能够明确识别的编队,不根据舰队编号猜测舰船。 */ +export function resolveCurrentFleet( + request: TaskRequest, +): CurrentFleetShipVO[] { + if (request.type !== 'normal_fight' && request.type !== 'event_fight') { + return []; + } + const rules = Array.isArray(request.plan?.fleet_rules) + ? request.plan.fleet_rules + : []; + const fleet = Array.isArray(request.plan?.fleet) + ? request.plan.fleet + : []; + const slotCount = Math.min(6, Math.max(rules.length, fleet.length)); + const ships: CurrentFleetShipVO[] = []; + for (let index = 0; index < slotCount; index += 1) { + const fleetName = normalizedShipName(fleet[index]); + const ship = fleetRuleShip(rules[index]) + || firstCandidateShip(rules[index]) + || (fleetName ? { name: fleetName } : null); + if (ship) ships.push(ship); + } + return ships; +} + /** 从调度器状态 + 追踪数据拼装 MainViewObject */ export function buildMainViewObject(state: RenderingState): MainViewObject { - const { scheduler, currentProgress, trackedLoot, trackedShip, wsConnected, expeditionTimerText } = state; + const { + scheduler, + currentProgress, + trackedLoot, + trackedShip, + dailySortieStats, + wsConnected, + expeditionTimerText, + } = state; const running = scheduler.currentRunningTask; const queue = scheduler.taskQueue; @@ -43,6 +110,7 @@ export function buildMainViewObject(state: RenderingState): MainViewObject { priorityLabel: PRIORITY_LABELS[running.priority] ?? '用户', remaining: running.remainingTimes, totalTimes: running.totalTimes, + unlimited: running.unlimited, progress: currentProgress || undefined, progressPercent, acquisitionText: buildAcquisitionText(trackedLoot, trackedShip), @@ -56,6 +124,23 @@ export function buildMainViewObject(state: RenderingState): MainViewObject { priorityLabel: PRIORITY_LABELS[t.priority] ?? '用户', remaining: t.remainingTimes, totalTimes: t.totalTimes, + unlimited: t.unlimited, + }); + } + + for (const waiting of scheduler.waitingTaskList) { + const t = waiting.task; + taskQueueVo.push({ + id: t.id, + name: t.name, + priorityLabel: PRIORITY_LABELS[t.priority] ?? '用户', + remaining: t.remainingTimes, + totalTimes: t.totalTimes, + unlimited: t.unlimited, + waiting: true, + waitingText: waiting.reason === 'gap' + ? '等待下一轮' + : '等待重试', }); } @@ -70,6 +155,10 @@ export function buildMainViewObject(state: RenderingState): MainViewObject { startedAt: '', } : null, + currentFleet: running + ? resolveCurrentFleet(running.request) + : [], + dailySortieStats, expeditionTimer: expeditionTimerText, taskQueue: taskQueueVo, wsConnected, diff --git a/src/controller/app/theme.ts b/src/controller/app/theme.ts index d792f27..701803a 100644 --- a/src/controller/app/theme.ts +++ b/src/controller/app/theme.ts @@ -1,20 +1,44 @@ +/** 读取并应用主题、强调色和系统主题变化。 */ /** 获取当前主题模式 */ export function getThemeMode(): 'dark' | 'light' | 'system' { - return (localStorage.getItem('themeMode') as 'dark' | 'light' | 'system') || 'dark'; + const mode = localStorage.getItem('themeMode'); + return mode === 'dark' || mode === 'system' ? mode : 'light'; } /** 获取当前主色调 */ export function getAccentColor(): string { - return localStorage.getItem('accentColor') || '#0f7dff'; + const accent = localStorage.getItem('accentColor') || '#0f7dff'; + return /^#[0-9a-f]{6}$/i.test(accent) ? accent : '#0f7dff'; } -/** 将 hex 颜色加亮指定百分比 */ -export function lightenColor(hex: string, percent: number): string { - const num = parseInt(hex.slice(1), 16); - const r = Math.min(255, (num >> 16) + Math.round(2.55 * percent)); - const g = Math.min(255, ((num >> 8) & 0xff) + Math.round(2.55 * percent)); - const b = Math.min(255, (num & 0xff) + Math.round(2.55 * percent)); - return `#${((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1)}`; +interface RgbColor { + r: number; + g: number; + b: number; +} + +/** 将十六进制颜色转换为 RGB。 */ +function hexToRgb(hex: string): RgbColor { + const value = parseInt(hex.slice(1), 16); + return { + r: value >> 16, + g: (value >> 8) & 0xff, + b: value & 0xff, + }; +} + +/** 将颜色按比例混入黑色或白色。 */ +function tintColor(hex: string, target: 0 | 255, ratio: number): string { + const rgb = hexToRgb(hex); + const channel = (value: number) => Math.round(value + (target - value) * ratio); + const value = (channel(rgb.r) << 16) | (channel(rgb.g) << 8) | channel(rgb.b); + return `#${value.toString(16).padStart(6, '0')}`; +} + +/** 根据强调色亮度选择清晰的按钮文字颜色。 */ +function getAccentForeground({ r, g, b }: RgbColor): string { + const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; + return luminance > 0.62 ? '#172033' : '#ffffff'; } /** 根据主题模式 + 主色调更新 DOM */ @@ -29,6 +53,11 @@ export function applyTheme(): void { document.documentElement.setAttribute('data-theme', resolved); const accent = getAccentColor(); + const rgb = hexToRgb(accent); + const hover = tintColor(accent, resolved === 'light' ? 0 : 255, resolved === 'light' ? 0.14 : 0.16); document.documentElement.style.setProperty('--accent', accent); - document.documentElement.style.setProperty('--accent-hover', lightenColor(accent, 20)); + document.documentElement.style.setProperty('--accent-hover', hover); + document.documentElement.style.setProperty('--accent-foreground', getAccentForeground(rgb)); + document.documentElement.style.setProperty('--accent-glow', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${resolved === 'light' ? 0.2 : 0.18})`); + document.documentElement.style.setProperty('--accent-subtle', `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${resolved === 'light' ? 0.12 : 0.09})`); } diff --git a/src/controller/migration/MigrationConflictController.ts b/src/controller/migration/MigrationConflictController.ts new file mode 100644 index 0000000..908964b --- /dev/null +++ b/src/controller/migration/MigrationConflictController.ts @@ -0,0 +1,71 @@ +/** 协调迁移冲突弹窗与主进程安全文件操作。 */ +import { + MigrationConflictView, +} from '../../view/migration/MigrationConflictView.js'; +import { showAlert, showConfirm } from '../shared/DialogHelper.js'; + +/** 启动时强制用户处理尚未确认的迁移 YAML 冲突。 */ +export class MigrationConflictController { + private finishReview: (() => void) | null = null; + + private readonly view = new MigrationConflictView({ + onSubmit: keepIds => { + void this.submit(keepIds); + }, + }); + + async reviewPending(): Promise { + const bridge = window.electronBridge; + if (!bridge?.getMigrationConflicts) return; + const result = await bridge.getMigrationConflicts(); + if (result.pending && result.conflicts.length > 0) { + this.view.open(result.conflicts); + await new Promise(resolve => { + this.finishReview = resolve; + }); + } + } + + private async submit(keepIds: string[]): Promise { + const bridge = window.electronBridge; + if (!bridge?.resolveMigrationConflicts) { + this.view.setStatus('当前环境不支持处理迁移冲突,请重启 GUI。'); + return; + } + const deleteCount = this.view.deleteCount(); + if (deleteCount > 0) { + const confirmed = await showConfirm( + '确认删除未保留的 YAML', + [ + `将直接删除 ${deleteCount} 个用户配置文件,此操作无法撤销。`, + '与系统预设完全相同的任务列表和自动胖次引用会自动切换到系统预设。', + '其他被删除配置的现有引用可能失效,存在数据丢失风险。', + ].join('\n'), + ); + if (!confirmed) return; + } + + this.view.setBusy(true); + this.view.setStatus(''); + try { + const result = await bridge.resolveMigrationConflicts(keepIds); + if (result.remaining.length > 0) { + this.view.replace(result.remaining); + this.view.setStatus(result.errors.join('\n')); + return; + } + this.view.close(); + this.finishReview?.(); + this.finishReview = null; + await showAlert( + '迁移冲突处理完成', + `保留 ${result.kept} 项,删除 ${result.deleted} 项。`, + ); + } catch (error) { + this.view.setBusy(false); + this.view.setStatus( + error instanceof Error ? error.message : String(error), + ); + } + } +} diff --git a/src/controller/plan/BattlePlanLoaderController.ts b/src/controller/plan/BattlePlanLoaderController.ts new file mode 100644 index 0000000..0381f5a --- /dev/null +++ b/src/controller/plan/BattlePlanLoaderController.ts @@ -0,0 +1,492 @@ +/** 管理受管作战方案选择器的加载、筛选、选择和结果返回流程。 */ +import type { + ManagedBattlePlan, + ManagedBattlePlanSelection, + ManagedTeamPlan, + PlanPresetSource, + PlanTeamBinding, +} from '../../types/ipc.js'; +import { + lootAutomationPlanKey, + normalizeLootAutomationPlans, + type LootAutomationPlan, +} from '../../shared/lootPlans.js'; +import { + BattlePlanLoaderView, + type BattlePlanLoaderPurpose, + type BattlePlanSortField, +} from '../../view/plan/BattlePlanLoaderView'; +import { Logger } from '../../utils/Logger'; +import { + showAlert, + showConfirm, + showSaveSuccess, +} from '../shared/DialogHelper'; + +export interface BattlePlanLoaderHost { + getCurrentPlanIdentity(): { + file: string | null; + source: PlanPresetSource; + }; + openManagedPlan(file: string, source: PlanPresetSource): Promise; +} + +export class BattlePlanLoaderController { + private plans: ManagedBattlePlan[] = []; + private selectedPlan: ManagedBattlePlan | null = null; + private selectedFleetIndex: number | null = null; + private sortField: BattlePlanSortField = 'modifiedAt'; + private purpose: BattlePlanLoaderPurpose = 'editor'; + private resolveSelection: ( + (selection: ManagedBattlePlanSelection | null) => void + ) | null = null; + private lootPlanDraft: LootAutomationPlan[] = []; + private resolveLootPlans: ( + (plans: LootAutomationPlan[] | null) => void + ) | null = null; + + constructor( + private readonly view: BattlePlanLoaderView, + private readonly host: BattlePlanLoaderHost, + ) {} + + bindActions(): void { + this.view.bindActions({ + onCancel: () => this.close(), + onImportLocal: () => void this.importLocal(), + onRefresh: () => void this.refresh(), + onFiltersChange: () => this.render(), + onSortFieldChange: (field) => { + this.sortField = field; + this.view.setSortField(field); + this.render(); + }, + onSelectPlan: (file, source) => this.selectPlan(file, source), + onSelectFleet: (index) => this.selectFleet(index), + onAddLootPlan: (file, source) => this.addLootPlan(file, source), + onDeleteLootPlan: (source, file) => { + void this.deleteLootPlan(source, file); + }, + onConfirm: () => void this.confirm(), + }); + this.view.setSortField(this.sortField); + } + + openForEditor(): Promise { + this.finishSelection(null); + this.purpose = 'editor'; + this.selectedFleetIndex = null; + this.prepareAndOpen(); + return this.refresh().then(() => this.view.focusSearch()); + } + + pick( + purpose: Exclude< + BattlePlanLoaderPurpose, + 'editor' | 'loot-automation' + >, + ): Promise { + this.finishSelection(null); + this.purpose = purpose; + this.selectedFleetIndex = null; + this.prepareAndOpen(); + void this.refresh().then(() => this.view.focusSearch()); + return new Promise((resolve) => { + this.resolveSelection = resolve; + }); + } + + pickLootPlans( + currentPlans: readonly LootAutomationPlan[], + ): Promise { + this.finishSelection(null); + this.finishLootPlans(null); + this.purpose = 'loot-automation'; + this.selectedFleetIndex = null; + this.lootPlanDraft = normalizeLootAutomationPlans(currentPlans, []); + this.prepareAndOpen(); + void this.refresh().then(() => this.view.focusSearch()); + return new Promise((resolve) => { + this.resolveLootPlans = resolve; + }); + } + + private prepareAndOpen(): void { + this.view.setPurposeCopy(this.purpose); + this.view.resetSearch(); + this.view.open(); + } + + private close(): void { + this.view.close(); + this.finishSelection(null); + this.finishLootPlans(null); + this.selectedFleetIndex = null; + this.purpose = 'editor'; + this.view.setPurposeCopy(this.purpose); + } + + private finishSelection( + selection: ManagedBattlePlanSelection | null, + ): void { + const resolve = this.resolveSelection; + this.resolveSelection = null; + resolve?.(selection); + } + + private finishLootPlans(plans: LootAutomationPlan[] | null): void { + const resolve = this.resolveLootPlans; + this.resolveLootPlans = null; + resolve?.(plans); + } + + private async importLocal(): Promise { + const bridge = window.electronBridge; + if (!bridge?.importLocalCombatPlan) { + await showAlert('导入失败', '请完整重启 GUI 后再操作'); + return; + } + this.view.setImportLoading(true); + try { + const result = await bridge.importLocalCombatPlan(); + if (result.canceled) return; + if (!result.success || !result.file) { + throw new Error(result.error || '本地 YAML 导入失败'); + } + + await this.refresh(); + const imported = this.plans.find(plan => ( + plan.source === 'user' && plan.file === result.file + )); + if (imported) this.selectPlan(imported.file, imported.source); + Logger.info(`本地出征计划已升级并导入: ${result.file}`); + showSaveSuccess( + result.kind === 'preset' + ? '已添加本地任务预设' + : `已添加本地 YAML,并升级 ${ + result.teamFiles?.length ?? 0 + } 支关联编队`, + ); + } catch (error) { + await showAlert( + '导入失败', + error instanceof Error ? error.message : String(error), + ); + } finally { + this.view.setImportLoading(false); + } + } + + private async refresh(): Promise { + const bridge = window.electronBridge; + if (!bridge?.getPlanManagement) { + this.view.setStatus('请完整重启 GUI 后再操作'); + return; + } + this.view.setStatus('正在读取作战计划...'); + try { + const result = await bridge.getPlanManagement(); + const detailedPlans = ( + result as typeof result & { battlePlans?: ManagedBattlePlan[] } + ).battlePlans; + const compatibilityMode = !Array.isArray(detailedPlans) + || detailedPlans.some(plan => ( + !Array.isArray(plan.fleets) + || typeof plan.fleetId !== 'number' + )); + this.plans = compatibilityMode + ? this.plansFromBindings(result.bindings, result.teamPlans) + : detailedPlans; + const visiblePlans = this.visiblePlans(); + const current = this.host.getCurrentPlanIdentity(); + this.selectedPlan = visiblePlans.find(plan => ( + Boolean(current.file) + && plan.file === current.file + && plan.source === current.source + )) ?? visiblePlans[0] ?? null; + this.resetFleetSelection(this.selectedPlan); + this.view.setCount(this.plans.length); + const errorCount = result.errors.filter( + error => error.kind === 'battle', + ).length; + const message = compatibilityMode + ? '当前主进程未更新,已显示基础列表;完整重启 GUI 后显示计划摘要' + : errorCount > 0 + ? `${errorCount} 个 YAML 无法读取,已从列表中排除` + : ''; + this.view.setStatus(message); + this.render(); + } catch (error) { + this.plans = []; + this.selectedPlan = null; + this.selectedFleetIndex = null; + this.view.setStatus( + `读取失败:${error instanceof Error ? error.message : String(error)}`, + ); + this.render(); + } + } + + private plansFromBindings( + bindings: PlanTeamBinding[], + teamPlans: ManagedTeamPlan[], + ): ManagedBattlePlan[] { + const plans = new Map(); + bindings.forEach((binding) => { + const key = `${binding.source}/${binding.planFile}`; + const existing = plans.get(key); + if (existing) { + if (binding.teamName) { + existing.fleets.push(this.compatibilityFleetSummary( + binding.teamName, + binding.source, + teamPlans, + )); + existing.fleetCount = existing.fleets.length; + } + return; + } + const fleets = binding.teamName + ? [this.compatibilityFleetSummary( + binding.teamName, + binding.source, + teamPlans, + )] + : []; + plans.set(key, { + kind: 'battle', + file: binding.planFile, + name: binding.planName, + source: binding.source, + modifiedAt: 0, + chapter: '?', + map: '?', + times: 0, + gap: 0, + fleetId: 1, + repairMode: 1, + result: null, + lootCountGe: -1, + shipCountGe: -1, + fleetCount: fleets.length, + nodeCount: 0, + fleets, + }); + }); + return [...plans.values()]; + } + + private compatibilityFleetSummary( + name: string, + battleSource: PlanPresetSource, + teamPlans: ManagedTeamPlan[], + ): ManagedBattlePlan['fleets'][number] { + const matchingPlan = teamPlans.find(plan => ( + plan.name === name && plan.source === battleSource + )) ?? teamPlans.find(plan => plan.name === name); + return { + name, + source: matchingPlan?.source ?? 'deleted', + primaryCount: 0, + backupCount: 0, + }; + } + + private visiblePlans(): ManagedBattlePlan[] { + const filters = this.view.getFilters(); + const direction = filters.ascending ? 1 : -1; + return this.plans + .filter(plan => ( + ( + this.purpose !== 'automation' + && this.purpose !== 'loot-automation' + ) + || plan.kind === 'battle' + )) + .filter(plan => !filters.excludeSystem || plan.source !== 'system') + .filter((plan) => { + if (!filters.keyword) return true; + return [ + plan.name, + plan.file, + String(plan.chapter), + String(plan.map), + `${plan.chapter}-${plan.map}`, + plan.taskType ?? '', + plan.campaignName ?? '', + ].some(value => ( + value.toLocaleLowerCase('zh-CN').includes(filters.keyword) + )); + }) + .sort((left, right) => { + const result = this.sortField === 'name' + ? left.name.localeCompare(right.name, 'zh-CN') + : left.modifiedAt - right.modifiedAt; + return ( + result || left.name.localeCompare(right.name, 'zh-CN') + ) * direction; + }); + } + + private render(): void { + const visiblePlans = this.visiblePlans(); + const previousSelection = this.selectedPlan; + if ( + !this.selectedPlan + || !visiblePlans.some(plan => ( + this.samePlan(plan, this.selectedPlan) + )) + ) { + this.selectedPlan = visiblePlans[0] ?? null; + } + if (!this.samePlan(this.selectedPlan, previousSelection)) { + this.resetFleetSelection(this.selectedPlan); + } + this.view.render({ + plans: visiblePlans, + totalPlanCount: this.plans.length, + selectedPlan: this.selectedPlan, + selectedFleetIndex: this.selectedFleetIndex, + purpose: this.purpose, + lootPlans: this.lootPlanDraft, + }); + } + + private selectPlan(file: string, source: PlanPresetSource): void { + const selected = this.plans.find(plan => ( + plan.file === file && plan.source === source + )); + if (!selected) return; + if (!this.samePlan(selected, this.selectedPlan)) { + this.resetFleetSelection(selected); + } + this.selectedPlan = selected; + this.render(); + } + + private resetFleetSelection(plan: ManagedBattlePlan | null): void { + this.selectedFleetIndex = ( + this.isPickingWithFleet() + && plan?.kind === 'battle' + && plan.fleets.length === 1 + ) ? 0 : null; + } + + private selectFleet(index: number): void { + if ( + !this.isPickingWithFleet() + || !this.selectedPlan?.fleets[index] + ) { + return; + } + this.selectedFleetIndex = index; + this.render(); + } + + private addLootPlan( + file: string, + source: PlanPresetSource, + ): void { + if (this.purpose !== 'loot-automation') return; + const plan = this.plans.find(item => ( + item.kind === 'battle' + && item.file === file + && item.source === source + )); + if (!plan) return; + const next: LootAutomationPlan = { + source: plan.source, + file: plan.file, + name: plan.name, + }; + const key = lootAutomationPlanKey(next); + if ( + this.lootPlanDraft.some(item => ( + lootAutomationPlanKey(item) === key + )) + ) { + return; + } + this.lootPlanDraft.push(next); + this.render(); + } + + private async deleteLootPlan( + source: PlanPresetSource, + file: string, + ): Promise { + if (this.purpose !== 'loot-automation') return; + const target = this.lootPlanDraft.find(plan => ( + plan.source === source && plan.file === file + )); + if (!target) return; + const confirmed = await showConfirm( + '删除自动胖次计划', + `确定从自动胖次下拉列表中删除「${target.name}」吗?`, + ); + if (!confirmed) return; + this.lootPlanDraft = this.lootPlanDraft.filter(plan => ( + plan.source !== source || plan.file !== file + )); + this.render(); + } + + private async confirm(): Promise { + if (this.purpose === 'loot-automation') { + this.finishLootPlans(structuredClone(this.lootPlanDraft)); + this.close(); + return; + } + if (!this.selectedPlan) return; + if (this.isPickingWithFleet()) { + if ( + this.requiresFleetSelection(this.selectedPlan) + && this.selectedFleetIndex === null + ) { + return; + } + this.finishSelection({ + plan: this.selectedPlan, + ...(this.selectedFleetIndex === null + ? {} + : { fleetPresetIndex: this.selectedFleetIndex }), + }); + this.close(); + return; + } + const { file, source } = this.selectedPlan; + const loaded = await this.host.openManagedPlan(file, source); + if (loaded) this.close(); + } + + private isPickingWithFleet(): boolean { + return ( + this.purpose === 'queue' + || this.purpose === 'task-list' + || this.purpose === 'automation' + ); + } + + private requiresFleetSelection(plan: ManagedBattlePlan): boolean { + if (plan.kind === 'preset') return false; + return ( + this.purpose === 'automation' + || ( + this.isPickingWithFleet() + && plan.fleets.length > 0 + ) + ); + } + + private samePlan( + left: ManagedBattlePlan | null, + right: ManagedBattlePlan | null, + ): boolean { + return Boolean( + left + && right + && left.file === right.file + && left.source === right.source, + ); + } +} diff --git a/src/controller/plan/DecisivePlanController.ts b/src/controller/plan/DecisivePlanController.ts new file mode 100644 index 0000000..0637854 --- /dev/null +++ b/src/controller/plan/DecisivePlanController.ts @@ -0,0 +1,115 @@ +/** 独立持有决战舰队草稿并协调加载、编辑和保存。 */ +import { + decisivePlanRepository, +} from '../../adapter/IpcAdapter'; +import type { + DecisivePlanRepository, +} from '../../adapter/IpcAdapter'; +import { + DecisiveFleetDraft, + DEFAULT_DECISIVE_PLAN_SETTINGS, +} from '../../model/fleet/DecisiveFleetDraft'; +import { + DecisivePlanView, +} from '../../view/plan/DecisivePlanView'; +import type { + DecisivePlanSaveResult, + DecisivePlanViewState, +} from '../../view/plan/DecisivePlanView'; + +export class DecisivePlanController { + private readonly draft = new DecisiveFleetDraft(); + private readonly view: DecisivePlanView; + + constructor( + private readonly repository: DecisivePlanRepository = + decisivePlanRepository, + ) { + this.view = new DecisivePlanView({ + getState: () => this.getViewState(), + setChapter: chapter => this.draft.setChapter(chapter), + changeChapter: chapter => this.changeChapter(chapter), + setUseQuickRepair: useQuickRepair => ( + this.draft.setUseQuickRepair(useQuickRepair) + ), + findShip: name => this.draft.find(name), + placeShip: (name, level, requestedIndex, maxIndex) => ( + this.draft.place(name, level, requestedIndex, maxIndex) + ), + removeShip: (level, index) => this.draft.remove(level, index), + moveShip: ( + sourceLevel, + sourceIndex, + targetLevel, + targetIndex, + ) => this.draft.move( + sourceLevel, + sourceIndex, + targetLevel, + targetIndex, + ), + resetTeams: () => this.resetTeams(), + save: () => this.save(), + }); + } + + bindActions(): void { + this.view.bindActions(); + } + + async load(): Promise { + try { + this.draft.load(await this.repository.loadSettings()); + const manifest = await this.repository.loadShipLibrary(); + this.view.showLoaded(manifest); + } catch (error) { + this.draft.load(DEFAULT_DECISIVE_PLAN_SETTINGS); + this.view.showLoadFailure(); + console.error('[DecisivePlan] 读取配置失败', error); + } + } + + private getViewState(): DecisivePlanViewState { + return { + chapter: this.draft.chapter, + useQuickRepair: this.draft.useQuickRepair, + level1: [...this.draft.queue('level1')], + level2: [...this.draft.queue('level2')], + dirty: this.draft.dirty, + }; + } + + private async save(): Promise { + try { + this.draft.load( + await this.repository.saveSettings(this.draft.toSettings()), + ); + return { success: true }; + } catch (error) { + return { success: false, error }; + } + } + + private async changeChapter( + chapter: number, + ): Promise { + try { + this.draft.load(await this.repository.loadChapter(chapter)); + return { success: true }; + } catch (error) { + return { success: false, error }; + } + } + + private async resetTeams(): Promise { + try { + const defaults = await this.repository.loadSystemDefaults( + this.draft.chapter, + ); + this.draft.resetTeams(defaults); + return { success: true }; + } catch (error) { + return { success: false, error }; + } + } +} diff --git a/src/controller/plan/FleetPlannerController.ts b/src/controller/plan/FleetPlannerController.ts new file mode 100644 index 0000000..2fbb302 --- /dev/null +++ b/src/controller/plan/FleetPlannerController.ts @@ -0,0 +1,355 @@ +/** 持有普通舰队草稿并协调舰船库、规则编辑和计划管理。 */ +import { + fleetPlannerRepository, +} from '../../adapter/IpcAdapter'; +import type { + FleetPlannerRepository, +} from '../../adapter/IpcAdapter'; +import { + browserStorageStore, +} from '../../adapter/StorageAdapter'; +import type { + StorageStore, +} from '../../adapter/StorageAdapter'; +import { + cloneFleetRule, + copyFleetRule, + createFleetCandidateDraft, + createFleetDraft, + createFleetRuleDraft, + createFleetSlotDraft, + fleetDraftFromTeamPlan, + fleetDraftSnapshot, + fleetDraftToTeamPlan, + hasFleetDraftChanges, +} from '../../model/fleet/FleetDraft'; +import type { + BackupFollowMode, +} from '../../model/fleet/FleetDraft'; +import type { + PlanPresetSource, + ShipLibraryManifest, + UserTeamPlan, + UserTeamPlanSlot, + UserTeamShipRule, +} from '../../types/ipc.js'; +import type { + FleetShipLibraryViewObject, + TeamPlanListViewObject, + TeamPlanShipRuleViewObject, + TeamPlanSlotViewObject, + TeamPlanViewObject, +} from '../../types/view.js'; +import { + FleetPlannerView, +} from '../../view/plan/FleetPlannerView'; +import type { + PlanManagementTaskGroup, +} from '../../view/plan/PlanManagementView'; + +const REFIT_FILTER_STORAGE_KEY = 'fleetPlannerRefitFilter'; +const BACKUP_FOLLOW_MODE_STORAGE_KEY = 'fleetPlannerBackupFollowMode'; + +export class FleetPlannerController { + private draft = createFleetDraft(); + private savedDraftSnapshot = fleetDraftSnapshot(this.draft); + private readonly view: FleetPlannerView; + private readonly teamPlans = new Map(); + private readonly teamPlanIds = new Map(); + private shipLibrary: FleetShipLibraryViewObject | null = null; + private shipLibraryLoading: Promise | null = null; + private nextTeamPlanId = 1; + + constructor( + private readonly repository: FleetPlannerRepository + = fleetPlannerRepository, + private readonly storage: StorageStore = browserStorageStore, + ) { + this.view = new FleetPlannerView({ + loadShipLibrary: force => this.loadShipLibrary(force), + loadTeamPlans: () => this.loadTeamPlans(), + saveTeamPlan: name => this.saveTeamPlan(name), + applyTeamPlan: planId => this.applyTeamPlan(planId), + loadPlanManagement: () => repository.getPlanManagement(), + exportUserPlans: selections => ( + repository.exportUserPlans(selections) + ), + setPlanUnlinkedIgnored: (kind, source, file, ignored) => ( + repository.setPlanUnlinkedIgnored( + kind, + source, + file, + ignored, + ) + ), + renameUserCombatPlan: (file, newName) => ( + repository.renameUserCombatPlan(file, newName) + ), + deleteUserCombatPlan: file => ( + repository.deleteUserCombatPlan(file) + ), + deleteUserTeamPlan: file => repository.deleteUserTeamPlan(file), + openTeamPlan: (file, source) => this.openTeamPlan(file, source), + getRefitFilter: () => ( + storage.get(REFIT_FILTER_STORAGE_KEY) === 'true' + ), + setRefitFilter: enabled => ( + storage.set(REFIT_FILTER_STORAGE_KEY, String(enabled)) + ), + getBackupFollowMode: (): BackupFollowMode => ( + storage.get(BACKUP_FOLLOW_MODE_STORAGE_KEY) === 'position' + ? 'position' + : 'ship' + ), + setBackupFollowMode: mode => ( + storage.set(BACKUP_FOLLOW_MODE_STORAGE_KEY, mode) + ), + currentDraft: () => this.draft, + setDraftName: name => { + this.draft.name = name; + }, + resetDraft: () => this.resetDraft(), + createRuleDraft: () => createFleetRuleDraft(), + createCandidateDraft: ship => createFleetCandidateDraft(ship), + createSlotDraft: () => createFleetSlotDraft(), + cloneRule: source => cloneFleetRule(source), + copyRule: (target, source) => copyFleetRule(target, source), + hasUnsavedDraftChanges: name => hasFleetDraftChanges( + { + ...this.draft, + name, + }, + this.savedDraftSnapshot, + ), + }); + } + + set onOpenBattlePlan( + handler: ( + (file: string, source: PlanPresetSource) => Promise + ) | null, + ) { + this.view.onOpenBattlePlan = handler; + } + + setTaskGroupsProvider( + provider: () => ReadonlyArray, + ): void { + this.view.setTaskGroupsProvider(provider); + } + + load(force = false): Promise { + return this.loadShipLibrary(force); + } + + loadManagement(): Promise { + return this.view.loadManagement(); + } + + private loadShipLibrary(force: boolean): Promise { + if (this.shipLibraryLoading) return this.shipLibraryLoading; + if (this.shipLibrary && !force) { + this.view.showShipLibrary(this.shipLibrary); + return Promise.resolve(); + } + + this.view.showShipLibraryLoading(); + this.shipLibraryLoading = this.repository.getShipLibraryManifest() + .then(manifest => { + this.shipLibrary = this.toShipLibraryViewObject(manifest); + this.view.showShipLibrary(this.shipLibrary); + }) + .catch(error => { + this.view.showShipLibraryError( + error instanceof Error ? error.message : String(error), + ); + }) + .finally(() => { + this.shipLibraryLoading = null; + }); + return this.shipLibraryLoading; + } + + private toShipLibraryViewObject( + manifest: ShipLibraryManifest, + ): FleetShipLibraryViewObject { + const ships = manifest.ships.filter(ship => ( + Number.isFinite(ship.id) + && Boolean(ship.name) + && Boolean(ship.portraitUrl) + )); + return { + labels: manifest.labels, + ships, + colorfulBackgroundUrl: ships.find( + ship => ship.rarity === 6 && Boolean(ship.backgroundUrl), + )?.backgroundUrl ?? '', + }; + } + + private async loadTeamPlans(): Promise { + const result = await this.repository.listTeamPlans(); + this.teamPlans.clear(); + const plans = result.plans.map((plan, index) => { + const source = plan.source ?? 'user'; + const identity = this.teamPlanIdentity( + source, + plan.file ?? `missing-${index}-${plan.name}`, + ); + let id = this.teamPlanIds.get(identity); + if (!id) { + id = `team-plan-${this.nextTeamPlanId}`; + this.nextTeamPlanId += 1; + this.teamPlanIds.set(identity, id); + } + this.teamPlans.set(id, plan); + return this.toTeamPlanViewObject(id, plan, source); + }); + return { + plans, + errorCount: result.errors.length, + }; + } + + private toTeamPlanViewObject( + id: string, + plan: UserTeamPlan, + source: PlanPresetSource, + ): TeamPlanViewObject { + return { + id, + name: plan.name, + source, + modifiedAt: plan.modifiedAt, + selected: Boolean( + plan.file + && this.draft.file + && source === this.draft.source + && this.teamPlanIdentity(source, plan.file) + === this.teamPlanIdentity(this.draft.source, this.draft.file), + ), + ships: plan.ships.map(slot => this.toTeamPlanSlotViewObject(slot)), + }; + } + + private toTeamPlanSlotViewObject( + slot: UserTeamPlanSlot, + ): TeamPlanSlotViewObject { + const primary = slot.name + ? this.toTeamPlanRuleViewObject({ + name: slot.name, + search_name: slot.search_name, + ship_type: slot.ship_type, + min_level: slot.min_level, + max_level: slot.max_level, + }) + : undefined; + return { + primary, + candidates: (slot.candidates ?? []).map( + candidate => this.toTeamPlanRuleViewObject(candidate), + ), + }; + } + + private toTeamPlanRuleViewObject( + rule: UserTeamShipRule, + ): TeamPlanShipRuleViewObject { + return { + name: rule.name, + searchName: rule.search_name, + shipTypes: [...(rule.ship_type ?? [])], + minLevel: rule.min_level, + maxLevel: rule.max_level, + }; + } + + private async saveTeamPlan(rawName: string): Promise { + try { + const plan = fleetDraftToTeamPlan(this.draft, rawName); + const currentFile = this.draft.file ?? undefined; + const currentSource = this.draft.source; + let result = await this.repository.saveUserTeamPlan( + plan, + false, + currentFile, + currentSource, + ); + if (result.exists) { + if (!await this.view.confirmTeamPlanOverwrite()) return; + result = await this.repository.saveUserTeamPlan( + plan, + true, + currentFile, + currentSource, + ); + } + if (!result.success) { + throw new Error(result.error || '保存失败'); + } + + this.draft.name = plan.name; + this.draft.file = result.file ?? result.plan?.file ?? null; + this.draft.source = result.plan?.source ?? 'user'; + this.savedDraftSnapshot = fleetDraftSnapshot(this.draft); + this.view.showDraftName(plan.name); + this.view.showTeamPlanSaved(plan.name); + } catch (error) { + await this.view.showTeamPlanSaveError( + error instanceof Error ? error.message : String(error), + ); + } + } + + private async applyTeamPlan( + planId: string, + ): Promise<{ success: boolean; error?: string }> { + const plan = this.teamPlans.get(planId); + if (!plan) { + return { + success: false, + error: '未找到对应的舰队方案', + }; + } + try { + this.draft = fleetDraftFromTeamPlan( + plan, + this.shipLibrary?.ships ?? [], + ); + this.savedDraftSnapshot = fleetDraftSnapshot(this.draft); + this.view.showDraft(this.draft.name); + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + private async openTeamPlan( + file: string, + source: PlanPresetSource, + ): Promise { + await this.loadShipLibrary(false); + await this.loadTeamPlans(); + const id = this.teamPlanIds.get(this.teamPlanIdentity(source, file)); + if (!id || !this.teamPlans.has(id)) { + await this.view.showTeamPlanLoadError('未找到对应的舰队方案'); + return; + } + await this.view.openTeamPlan(id); + } + + private resetDraft(): void { + this.draft = createFleetDraft(); + this.savedDraftSnapshot = fleetDraftSnapshot(this.draft); + } + + private teamPlanIdentity( + source: PlanPresetSource, + file: string, + ): string { + return `${source}:${file.trim().toLocaleLowerCase('en-US')}`; + } +} diff --git a/src/controller/plan/PlanController.ts b/src/controller/plan/PlanController.ts index 1f82bb5..6fe3dd8 100644 --- a/src/controller/plan/PlanController.ts +++ b/src/controller/plan/PlanController.ts @@ -1,22 +1,52 @@ +/** 协调作战方案加载、预览、编辑、保存和任务执行。 */ /** - * PlanController —— 方案 & 任务预设子控制器(瘦身版)。 - * 核心逻辑委托给 importExport / presetFlow / nodeEditor / rendering 模块。 + * 编排受管方案、任务预设、节点编辑和计划预览。 */ import { PlanPreviewView } from '../../view/plan/PlanPreviewView'; -import type { PlanModel } from '../../model/PlanModel'; -import type { CombatPlanReq, EventFightReq, NodeDecisionReq, NormalFightReq } from '../../types/api'; +import { PlanModel } from '../../model/PlanModel'; +import type { CombatPlanReq, EventFightReq, NodeDecisionReq, NormalFightReq } from '../../types/api.js'; import type { Scheduler } from '../../model/scheduler'; import { TaskPriority } from '../../model/scheduler'; -import type { NodeArgs, TaskPreset } from '../../types/model'; -import { getNodeType, isNightNode, isDetourNode, isTerminalNode } from '../../model/MapDataLoader'; +import type { + EventMapCatalogEntry, + FleetPreset, + NodeArgs, + TaskPreset, +} from '../../types/model.js'; +import { + getNodeType, + isNightNode, + isDetourNode, + isTerminalNode, + loadEventMapCatalog, + loadMapData, + loadEventMapData, +} from '../../model/MapDataLoader'; import type { MapData } from '../../model/MapDataLoader'; -import { toBackendName, resolveFleetPreset, resolveFleetPresetRules, shipSlotLabel } from '../../data/shipData'; +import type { + ManagedBattlePlanSelection, + PlanPresetSource, +} from '../../types/ipc.js'; +import { BattlePlanLoaderView } from '../../view/plan/BattlePlanLoaderView'; +import { + resolveFleetPreset, + shipSlotLabel, +} from '../../model/fleet/ShipMatcher'; +import { resolveFleetPresetRules } from '../../model/fleet/FleetRuleMapper'; +import { toBackendName } from '../../model/fleet/ShipNameNormalizer'; import { Logger } from '../../utils/Logger'; -import { importPlanFlow, exportPlanFlow, savePlanFlow, confirmNewPlanFlow, type PlanSetters } from './importExport'; -import { importTaskPresetFlow, showPresetDetailFlow, closePresetDetailFlow, executePresetFlow, type PresetState } from './presetFlow'; +import { + showAlert, + showConfirm, + showSaveSuccess, +} from '../shared/DialogHelper'; +import { importTaskPresetFlow, closePresetDetailFlow, executePresetFlow, type PresetState } from './presetFlow'; +import { yamlCodec, jsonCodec } from '../../adapter'; import { saveNodeEditorValues } from './nodeEditor'; import { buildPlanPreviewVO } from './rendering'; import { normalizeSelectedNodesForBackend } from './selectedNodes'; +import { BattlePlanLoaderController } from './BattlePlanLoaderController'; +import type { LootAutomationPlan } from '../../shared/lootPlans.js'; export interface PlanHost { readonly scheduler: Scheduler; @@ -31,19 +61,69 @@ export class PlanController { private editingNodeId: string | null = null; private currentPreset: TaskPreset | null = null; private currentPresetFilePath = ''; + private mapLoadVersion = 0; + private planPresetName = ''; + private currentManagedPlanFile: string | null = null; + private currentPlanSource: PlanPresetSource = 'user'; + private savedPlanSnapshot = ''; + private eventMapCatalog: EventMapCatalogEntry[] = []; + private readonly battlePlanLoader: BattlePlanLoaderController; constructor( private readonly planView: PlanPreviewView, readonly host: PlanHost, - ) {} + ) { + this.battlePlanLoader = new BattlePlanLoaderController( + new BattlePlanLoaderView(), + { + getCurrentPlanIdentity: () => ({ + file: this.currentManagedPlanFile, + source: this.currentPlanSource, + }), + openManagedPlan: (file, source) => ( + this.openManagedPlan(file, source) + ), + }, + ); + } // ── 公共访问器 ── getCurrentPlan(): PlanModel | null { return this.currentPlan; } + pickManagedBattlePlan(): Promise { + return this.battlePlanLoader.pick('task-list'); + } + + pickManagedBattlePlanForQueue(): Promise { + return this.battlePlanLoader.pick('queue'); + } + + pickManagedBattlePlanForAutomation(): Promise { + return this.battlePlanLoader.pick('automation'); + } + + pickManagedLootPlans( + currentPlans: readonly LootAutomationPlan[], + ): Promise { + return this.battlePlanLoader.pickLootPlans(currentPlans); + } + setCurrentPlan(plan: PlanModel, mapData: MapData | null): void { + this.editingNodeId = null; + this.planView.resetNodeEditorDrafts(); + this.planView.hideNodeEditor(); + this.mapLoadVersion++; this.currentPlan = plan; this.currentMapData = mapData; + this.planPresetName = this.planNameFromPath(plan.fileName); + this.currentManagedPlanFile = null; + this.currentPlanSource = 'user'; + this.savedPlanSnapshot = this.planDraftSnapshot(); + } + + private async ensureEventMapCatalog(): Promise { + this.eventMapCatalog = await loadEventMapCatalog(); } getCurrentPresetInfo(): { preset: TaskPreset; filePath: string } | null { @@ -65,29 +145,24 @@ export class PlanController { }; } - private get planSetters(): PlanSetters { - return { - setCurrentPlan: (plan) => { this.currentPlan = plan; }, - setCurrentMapData: (mapData) => { this.currentMapData = mapData; }, - renderPlanPreview: () => this.renderPlanPreview(), - importTaskPreset: (preset, fp) => this.importTaskPreset(preset, fp), - }; - } - // ════════════════════════════════════════ // 事件绑定 // ════════════════════════════════════════ bindActions(): void { - document.getElementById('btn-import-plan')?.addEventListener('click', () => this.importPlan()); - document.getElementById('btn-import-plan-2')?.addEventListener('click', () => this.importPlan()); - document.getElementById('btn-close-plan')?.addEventListener('click', () => this.closePlan()); - document.getElementById('btn-execute-plan')?.addEventListener('click', () => { - this.executePlan().catch((e) => { - const msg = e instanceof Error ? e.message : String(e); - Logger.error(`执行方案失败: ${msg}`); - }); - }); + document.getElementById('btn-new-battle-plan')?.addEventListener( + 'click', + () => void this.newPlan(), + ); + document.getElementById('btn-load-battle-plan')?.addEventListener( + 'click', + () => void this.loadPlan(), + ); + document.getElementById('btn-save-plan')?.addEventListener( + 'click', + () => void this.savePlan(), + ); + this.battlePlanLoader.bindActions(); // 节点编辑 this.planView.onNodeClick = (nodeId) => { @@ -102,7 +177,7 @@ export class PlanController { const canDetour = this.currentMapData ? isDetourNode(this.currentMapData, nodeId) : false; const isEndpoint = (this.currentPlan.data.endpoint_nodes ?? []).includes(nodeId); const isTerminal = this.currentMapData ? isTerminalNode(this.currentMapData, nodeId) : false; - this.planView.showNodeEditor(nodeId, nodeType as any, { + this.planView.showNodeEditor(nodeId, nodeType, { enabled: isEnabled, formation: args.formation ?? 2, night: args.night ?? false, @@ -112,6 +187,7 @@ export class PlanController { canDetour, slWhenDetourFails: args.SL_when_detour_fails ?? false, isEndpoint, + result: this.currentPlan.data.result, isTerminal, enemyRules: rulesText, }, mapNight); @@ -129,46 +205,12 @@ export class PlanController { } }); - document.getElementById('btn-export-plan')?.addEventListener('click', () => - exportPlanFlow(this.currentPlan, this.host, () => this.renderPlanPreview())); - document.getElementById('btn-save-plan')?.addEventListener('click', () => - savePlanFlow(this.currentPlan, this.host, () => this.renderPlanPreview())); - - const updateNewPlanMapOptions = (chapterVal: string): void => { - const mapSelect = document.getElementById('new-plan-map') as HTMLSelectElement | null; - if (!mapSelect) return; - - const mapCountByChapter: Record = { - '1': 5, - '2': 6, - '3': 4, - '4': 4, - '5': 5, - '6': 4, - '7': 5, - '8': 5, - '9': 5, - '10': 1, - Ex: 12, - }; - - const count = mapCountByChapter[chapterVal] ?? 6; - mapSelect.innerHTML = Array.from({ length: count }, (_, i) => - ``).join(''); + this.planView.onMapChange = (chapter, map) => { + void this.changeMap(chapter, map); + }; + this.planView.onPresetNameChange = (name) => { + this.planPresetName = name; }; - - document.getElementById('btn-new-plan')?.addEventListener('click', () => { - const chapterSelect = document.getElementById('new-plan-chapter') as HTMLSelectElement | null; - if (chapterSelect) updateNewPlanMapOptions(chapterSelect.value); - this.planView.showNewPlanDialog(); - }); - document.getElementById('btn-new-plan-confirm')?.addEventListener('click', () => - confirmNewPlanFlow(this.planView, this.host, this.planSetters)); - document.getElementById('btn-new-plan-cancel')?.addEventListener('click', () => this.planView.hideNewPlanDialog()); - - document.getElementById('new-plan-chapter')?.addEventListener('change', (e) => { - updateNewPlanMapOptions((e.target as HTMLSelectElement).value); - }); this.planView.onPlanFieldChange = (field, value) => { if (!this.currentPlan) return; @@ -183,36 +225,229 @@ export class PlanController { const sc = this.currentPlan.data.stop_condition; if (sc.loot_count_ge == null && sc.ship_count_ge == null) this.currentPlan.data.stop_condition = undefined; } - if (this.currentPlan.fileName) { - window.electronBridge?.saveFile(this.currentPlan.fileName, this.currentPlan.toYaml()); - } }; - this.planView.onFleetPresetChange = (action, index, preset) => { + this.planView.onUserTeamChange = (teams) => { if (!this.currentPlan) return; - if (!this.currentPlan.data.fleet_presets) this.currentPlan.data.fleet_presets = []; - const presets = this.currentPlan.data.fleet_presets; - if (action === 'add' && preset) presets.push({ name: preset.name, ships: preset.ships }); - else if (action === 'edit' && preset && index >= 0 && index < presets.length) presets[index] = { name: preset.name, ships: preset.ships }; - else if (action === 'delete' && index >= 0 && index < presets.length) presets.splice(index, 1); - if (this.currentPlan.fileName) window.electronBridge?.saveFile(this.currentPlan.fileName, this.currentPlan.toYaml()); - this.planView.renderFleetPresets(presets.map(p => ({ name: p.name, ships: p.ships }))); + this.applyFleetPresets(teams); }; + } - this.planView.onCommentChange = (comment) => { - if (!this.currentPlan) return; - this.currentPlan.comment = comment; - if (this.currentPlan.fileName) window.electronBridge?.saveFile(this.currentPlan.fileName, this.currentPlan.toYaml()); - }; + private applyFleetPresets(presets: FleetPreset[]): void { + if (!this.currentPlan) return; + this.currentPlan.data.fleet_presets = presets.map(team => ({ + name: team.name, + ships: team.ships.map(slot => ( + slot === null || typeof slot === 'string' + ? slot + : { + name: slot.name, + candidates: slot.candidates + ? slot.candidates.map(candidate => ({ + ...candidate, + ship_type: candidate.ship_type + ? [...candidate.ship_type] + : undefined, + })) + : undefined, + search_name: slot.search_name, + ship_type: slot.ship_type + ? [...slot.ship_type] + : undefined, + min_level: slot.min_level, + max_level: slot.max_level, + } + )), + })); } // ── 委托方法 ── - async importPlan(): Promise { - return importPlanFlow(this.planView, this.host, this.planSetters); + async openManagedPlan( + file: string, + source: PlanPresetSource, + skipDiscardConfirm = false, + ): Promise { + if ( + !skipDiscardConfirm + && !(await this.confirmDiscardUnsaved()) + ) { + return false; + } + const bridge = window.electronBridge; + if (!bridge?.readManagedCombatPlan) { + await showAlert('加载失败', '请完整重启 GUI 后再操作'); + return false; + } + try { + const result = await bridge.readManagedCombatPlan(source, file); + if (!result.success || !result.path || result.content === undefined) { + await showAlert('加载失败', result.error || '无法读取出征计划'); + return false; + } + const parsed = yamlCodec.parse(result.content); + if ( + parsed + && typeof parsed === 'object' + && 'task_type' in parsed + && !('map' in parsed) + ) { + this.importTaskPreset(parsed as TaskPreset, result.path); + return true; + } + const plan = PlanModel.fromYaml(result.content, result.path); + await this.ensureEventMapCatalog(); + const { chapter, map } = plan.data; + const mapData = plan.isEvent + ? await loadEventMapData(plan.data.event ?? '', chapter, map) + : await loadMapData(Number(chapter), Number(map)); + this.setCurrentPlan(plan, mapData); + this.currentManagedPlanFile = file; + this.currentPlanSource = source; + this.savedPlanSnapshot = this.planDraftSnapshot(); + this.renderPlanPreview(); + this.host.switchPage('plan'); + return true; + } catch (error) { + await showAlert( + '加载失败', + error instanceof Error ? error.message : String(error), + ); + return false; + } + } + + private async newPlan(): Promise { + if (this.hasUnsavedPlanChanges()) { + const confirmed = await showConfirm( + '新建出征预设', + '当前出征规划存在未保存修改,继续新建将丢失这些修改,是否继续?', + ); + if (!confirmed) return; + } + this.planView.resetNodeEditorDrafts(); + this.currentPlan = null; + this.currentMapData = null; + this.planPresetName = ''; + this.currentManagedPlanFile = null; + this.currentPlanSource = 'user'; + this.planView.hideNodeEditor(); + await this.changeMap('1', 1); + this.savedPlanSnapshot = this.planDraftSnapshot(); + this.planView.focusPresetName(); + } + + private async loadPlan(): Promise { + await this.battlePlanLoader.openForEditor(); + } + + private async confirmDiscardUnsaved( + action: '加载' = '加载', + ): Promise { + if (!this.hasUnsavedPlanChanges()) return true; + return showConfirm( + '未保存修改', + `当前出征规划存在未保存修改,继续${action}将丢失这些修改,是否继续?`, + ); + } + + private planDraftSnapshot(): string { + if (!this.currentPlan) return ''; + return jsonCodec.stringify({ + name: this.planPresetName, + yaml: this.currentPlan.toYaml(), + }); + } + + private hasUnsavedPlanChanges(): boolean { + return this.planDraftSnapshot() !== this.savedPlanSnapshot; + } + + private planNameFromPath(filePath: string): string { + const file = filePath.split(/[\\/]/).pop() ?? ''; + return file + .replace(/\.ya?ml$/i, '') + .replace(/^bettle-/i, ''); + } + + private normalizePlanName(value: string): string { + return value + .trim() + .replace(/\.ya?ml$/i, '') + .replace(/^bettle-/i, '') + .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') + .replace(/[. ]+$/g, '') + .slice(0, 100); + } + + private async savePlan(): Promise { + if (!this.currentPlan) return; + const bridge = window.electronBridge; + if (!bridge?.saveManagedCombatPlan) { + await showAlert('保存失败', '当前环境不支持保存出征规划'); + return; + } + const name = this.normalizePlanName(this.planView.getPresetName()); + if (!name) { + await showAlert('保存失败', '请先填写预设名称'); + return; + } + + try { + this.applyFleetPresets(this.planView.getSelectedPresets()); + const content = this.currentPlan.toYaml(); + const copiedFromSystem = this.currentPlanSource === 'system'; + const currentFile = copiedFromSystem + ? undefined + : this.currentManagedPlanFile ?? undefined; + let result = await bridge.saveManagedCombatPlan( + name, + content, + false, + currentFile, + ); + if (result.exists) { + const conflictDetails = result.conflicts?.length + ? `\n\n${result.conflicts.join('\n')}` + : ''; + const overwrite = await showConfirm( + '覆盖配置', + `存在同名配置,是否覆盖?${conflictDetails}`, + ); + if (!overwrite) return; + result = await bridge.saveManagedCombatPlan( + name, + content, + true, + currentFile, + ); + } + if (!result.success) { + throw new Error(result.error || '保存失败'); + } + + this.currentPlan.fileName = result.path ?? this.currentPlan.fileName; + this.currentManagedPlanFile = result.file ?? `bettle-${name}.yaml`; + this.currentPlanSource = result.source ?? 'user'; + this.planPresetName = name; + this.savedPlanSnapshot = this.planDraftSnapshot(); + this.renderPlanPreview(); + Logger.info(`出征规划已保存: ${this.currentManagedPlanFile}`); + showSaveSuccess( + copiedFromSystem + ? `出征规划「${name}」已保存为用户配置` + : `出征规划「${name}」保存成功`, + ); + } catch (error) { + await showAlert( + '保存失败', + error instanceof Error ? error.message : String(error), + ); + } } importTaskPreset(preset: TaskPreset, filePath: string): void { + this.mapLoadVersion++; importTaskPresetFlow(preset, filePath, this.planView, this.host, this.presetState); } @@ -224,22 +459,84 @@ export class PlanController { executePresetFlow(this.planView, this.host, this.presetState); } - closePlan(): void { - this.currentPlan = null; - this.currentMapData = null; - this.editingNodeId = null; - this.planView.render(null); - this.planView.hideNodeEditor(); - this.planView.hidePresetDetail(); - } - renderPlanPreview(): void { if (!this.currentPlan) { this.planView.render(null); return; } - const vo = buildPlanPreviewVO(this.currentPlan, this.currentMapData); + const vo = buildPlanPreviewVO( + this.currentPlan, + this.currentMapData, + this.eventMapCatalog, + ); this.planView.render(vo); + this.planView.setPresetName(this.planPresetName); this.planView.showPlanView(); } + async ensureDefaultPlan(): Promise { + if (this.currentPreset) return; + if (this.currentPlan) { + this.renderPlanPreview(); + return; + } + await this.changeMap('1', 1); + this.savedPlanSnapshot = this.planDraftSnapshot(); + } + + private async changeMap( + chapterValue: string, + map: number | string, + ): Promise { + await this.ensureEventMapCatalog(); + const version = ++this.mapLoadVersion; + const eventMatch = chapterValue.match(/^event:([^:]+):(E|H)$/); + const eventName = eventMatch?.[1]; + const eventChapter = eventMatch?.[2]; + let chapter: number | string; + let mapValue: number | string; + let mapData: MapData | null; + + if (eventName && eventChapter) { + chapter = eventChapter; + mapValue = String(map).trim().toLowerCase(); + mapData = await loadEventMapData(eventName, chapter, mapValue); + } else { + chapter = Number(chapterValue); + mapValue = Number(map); + mapData = await loadMapData(Number(chapter), mapValue); + } + if (version !== this.mapLoadVersion) return; + if (!mapData) { + Logger.error(`地图 ${chapterValue}-${mapValue} 数据不存在`); + this.renderPlanPreview(); + return; + } + + this.planView.resetNodeEditorDrafts(); + const selectedNodes = Object.keys(mapData).sort(); + if (!this.currentPlan) { + this.currentPlan = PlanModel.create( + chapter, + mapValue, + selectedNodes, + eventName, + ); + } else { + this.currentPlan.data.chapter = chapter; + this.currentPlan.data.map = mapValue; + this.currentPlan.data.mode = undefined; + this.currentPlan.data.event = eventName; + this.currentPlan.data.selected_nodes = selectedNodes; + this.currentPlan.data.endpoint_nodes = undefined; + this.currentPlan.data.result = undefined; + this.currentPlan.data.node_args = {}; + } + + this.currentMapData = mapData; + this.editingNodeId = null; + this.planView.hideNodeEditor(); + this.renderPlanPreview(); + Logger.info(`已切换地图 ${this.currentPlan.mapName}`); + } + // ── 执行方案 ── private toNodeDecisionReq(args?: NodeArgs): NodeDecisionReq | undefined { @@ -253,7 +550,17 @@ export class PlanController { if (args.proceed_stop != null) mapped.proceed_stop = args.proceed_stop; if (args.SL_when_detour_fails != null) mapped.SL_when_detour_fails = args.SL_when_detour_fails; if (args.enemy_rules && args.enemy_rules.length > 0) { - mapped.enemy_rules = args.enemy_rules.map(([cond, action]) => [String(cond), String(action)]); + mapped.enemy_rules = args.enemy_rules.map(([cond, action]) => [String(cond), action]); + } + if (args.enemy_formation_rules && args.enemy_formation_rules.length > 0) { + mapped.enemy_formation_rules = args.enemy_formation_rules.map(([cond, action]) => [String(cond), action]); + } + if (args.SL_when_spot_enemy_fails != null) { + mapped.SL_when_spot_enemy_fails = args.SL_when_spot_enemy_fails; + } + if (args.SL_when_enter_fight != null) mapped.SL_when_enter_fight = args.SL_when_enter_fight; + if (args.formation_when_spot_enemy_fails != null) { + mapped.formation_when_spot_enemy_fails = args.formation_when_spot_enemy_fails; } return Object.keys(mapped).length > 0 ? mapped : undefined; } @@ -292,38 +599,45 @@ export class PlanController { return inlinePlan; } - private buildInlinePlanFilePath(plan: PlanModel): string { - const safeMap = plan.mapName.replace(/[^a-zA-Z0-9_-]+/g, '_'); - const ts = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14); - const fileName = `_ui_inline_${safeMap}_${ts}.yaml`; - return this.host.plansDir ? `${this.host.plansDir}\\${fileName}` : fileName; - } - private async ensurePlanFileForExecution(plan: PlanModel): Promise { const bridge = window.electronBridge; - if (!bridge) return null; + if (!bridge?.prepareCombatPlanExecution) return null; - let planPath = plan.fileName?.trim(); - if (!planPath) { - planPath = this.buildInlinePlanFilePath(plan); - plan.fileName = planPath; - Logger.warn(`当前方案尚未保存,已自动保存为临时文件: ${planPath}`); - this.renderPlanPreview(); + const result = await bridge.prepareCombatPlanExecution( + plan.toYaml(), + plan.mapName, + ); + if (!result.success || !result.path) { + Logger.warn( + result.error || '无法生成运行时出征计划', + ); + return null; } - - await bridge.saveFile(planPath, plan.toYaml()); - return bridge.resolveAppPath(planPath); + return result.path; } private async executePlan(): Promise { if (!this.currentPlan) return; const plan = this.currentPlan; const times = plan.data.times ?? 1; - const stopCondition = plan.data.stop_condition; + const lootCountGe = plan.data.stop_condition?.loot_count_ge; + const shipCountGe = plan.data.stop_condition?.ship_count_ge; + const stopCondition = ( + (lootCountGe !== undefined && lootCountGe > 0) + || (shipCountGe !== undefined && shipCountGe > 0) + ) + ? { + loot_count_ge: lootCountGe !== undefined && lootCountGe > 0 + ? lootCountGe + : undefined, + ship_count_ge: shipCountGe !== undefined && shipCountGe > 0 + ? shipCountGe + : undefined, + } + : undefined; const selectedPresets = this.planView.getSelectedPresets(); const firstPreset = selectedPresets.length > 0 ? selectedPresets[0] : undefined; - const configuredFleetId = plan.data.fleet_id ?? 1; - let effectiveFleetId = configuredFleetId; + const effectiveFleetId = plan.data.fleet_id ?? 1; const req: NormalFightReq | EventFightReq = plan.isEvent ? { type: 'event_fight', times: 1, gap: plan.data.gap ?? 0, fleet_id: effectiveFleetId } @@ -348,16 +662,12 @@ export class PlanController { if (firstPreset && firstPreset.ships.length > 0) { const resolved = resolveFleetPreset(firstPreset.ships); if (resolved.length > 0) { - if (configuredFleetId === 1) { - // 后端不支持对第一分队自动改编,运行时自动切换到第二分队。 - effectiveFleetId = 2; - Logger.warn('检测到自动编队请求且当前为第1分队,已自动切换为第2分队执行本次任务'); - } if (req.type === 'event_fight') req.fleet_id = effectiveFleetId; if (!req.plan) req.plan = {}; req.plan.fleet = resolved.map(toBackendName); req.plan.fleet_id = effectiveFleetId; - req.plan.fleet_rules = resolveFleetPresetRules(firstPreset.ships); + const fleetRules = resolveFleetPresetRules(firstPreset.ships); + if (fleetRules.length > 0) req.plan.fleet_rules = fleetRules; } } @@ -370,7 +680,7 @@ export class PlanController { this.host.scheduler.addTask( plan.mapName, taskType, req, TaskPriority.USER_TASK, times, stopCondition, bathRepairConfig, fleetId, fleetPresets, currentPresetIndex, - undefined, undefined, plan.data.endpoint_nodes, + undefined, undefined, plan.data.endpoint_nodes, plan.data.result, ); const planRef = req.plan_id ?? '(inline-unsaved)'; Logger.debug(`executePlan: map=${plan.mapName} plan_id=${planRef} times=${times} gap=${req.gap}${firstPreset ? ' fleet=' + firstPreset.ships.map(s => shipSlotLabel(s)).join(',') : ''}${fleetPresets ? ' rotation=' + fleetPresets.length + '套' : ''}`); diff --git a/src/controller/plan/importExport.ts b/src/controller/plan/importExport.ts deleted file mode 100644 index 35fd2f8..0000000 --- a/src/controller/plan/importExport.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * importExport —— 方案导入/导出/新建流程独立函数。 - */ -import type { PlanPreviewView } from '../../view/plan/PlanPreviewView'; -import { PlanModel } from '../../model/PlanModel'; -import { loadMapData, loadExMapData, loadEventMapData } from '../../model/MapDataLoader'; -import type { MapData } from '../../model/MapDataLoader'; -import type { TaskPreset } from '../../types/model'; -import { Logger } from '../../utils/Logger'; -import type { PlanHost } from './PlanController'; - -export interface PlanSetters { - setCurrentPlan(plan: PlanModel): void; - setCurrentMapData(mapData: MapData | null): void; - renderPlanPreview(): void; - importTaskPreset(preset: TaskPreset, filePath: string): void; -} - -function defaultPlanFileName(currentPlan: PlanModel): string { - return currentPlan.fileName - ? currentPlan.fileName.split(/[\\/]/).pop() || `${currentPlan.mapName}.yaml` - : `${currentPlan.mapName}.yaml`; -} - -function buildDefaultPlanPath(currentPlan: PlanModel, host: PlanHost): string { - const fileName = defaultPlanFileName(currentPlan); - return host.plansDir ? `${host.plansDir}\\${fileName}` : fileName; -} - -/** 导入 Plan 或 TaskPreset YAML 文件 */ -export async function importPlanFlow( - planView: PlanPreviewView, - host: PlanHost, - setters: PlanSetters, -): Promise { - const bridge = window.electronBridge; - if (!bridge) { - console.error('electronBridge 未注入,无法打开文件对话框'); - return; - } - - const result = await bridge.openFileDialog([ - { name: 'YAML 方案/任务预设', extensions: ['yaml', 'yml'] }, - ], host.plansDir || undefined); - if (!result) return; - - try { - const parsed = (await import('js-yaml')).load(result.content) as Record; - - // 含 chapter + map → 战斗方案 - if (parsed && typeof parsed === 'object' && 'chapter' in parsed && 'map' in parsed) { - const plan = PlanModel.fromYaml(result.content, result.path); - Logger.debug(`方案已导入: ${result.path}`); - const { chapter, map } = plan.data; - const mapData = plan.isEvent - ? await loadEventMapData(plan.data.event ?? '', chapter, map) - : chapter === 99 - ? await loadExMapData(Number(map)) - : await loadMapData(Number(chapter), Number(map)); - setters.setCurrentPlan(plan); - setters.setCurrentMapData(mapData); - setters.renderPlanPreview(); - host.switchPage('plan'); - return; - } - - // 仅 task_type → 任务预设 - if (parsed && typeof parsed === 'object' && 'task_type' in parsed) { - setters.importTaskPreset(parsed as unknown as TaskPreset, result.path); - return; - } - - throw new Error('文件缺少 chapter/map 或 task_type 字段'); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - console.error('YAML 解析失败:', msg); - Logger.error(`文件导入失败: ${msg}`); - } -} - -/** 导出当前方案为 YAML 文件 */ -export async function exportPlanFlow( - currentPlan: PlanModel | null, - host: PlanHost, - renderPlanPreview: () => void, -): Promise { - if (!currentPlan) return; - const bridge = window.electronBridge; - if (!bridge) return; - - const yamlStr = currentPlan.toYaml(); - const defaultPath = buildDefaultPlanPath(currentPlan, host); - - const saved = await bridge.saveFileDialog(defaultPath, yamlStr, [ - { name: 'YAML 方案', extensions: ['yaml', 'yml'] }, - ]); - if (saved) { - currentPlan.fileName = saved; - Logger.info(`方案已另存为: ${saved}`); - renderPlanPreview(); - } -} - -/** 保存当前方案(有路径则覆盖保存,无路径则走另存为) */ -export async function savePlanFlow( - currentPlan: PlanModel | null, - host: PlanHost, - renderPlanPreview: () => void, -): Promise { - if (!currentPlan) return; - const bridge = window.electronBridge; - if (!bridge) return; - - const yamlStr = currentPlan.toYaml(); - const existingPath = currentPlan.fileName?.trim(); - - if (existingPath) { - await bridge.saveFile(existingPath, yamlStr); - Logger.info(`方案已保存: ${existingPath}`); - renderPlanPreview(); - return; - } - - const defaultPath = buildDefaultPlanPath(currentPlan, host); - const saved = await bridge.saveFileDialog(defaultPath, yamlStr, [ - { name: 'YAML 方案', extensions: ['yaml', 'yml'] }, - ]); - if (saved) { - currentPlan.fileName = saved; - Logger.info(`方案已保存: ${saved}`); - renderPlanPreview(); - } -} - -/** 确认新建方案 */ -export async function confirmNewPlanFlow( - planView: PlanPreviewView, - host: PlanHost, - setters: PlanSetters, -): Promise { - const formVals = planView.getNewPlanFormValues(); - const chapterVal = formVals.chapter; - planView.hideNewPlanDialog(); - - try { - let mapData: MapData | null; - let chapter: number; - let map: number; - let mapLabel: string; - - map = formVals.map; - - if (chapterVal === 'Ex') { - mapData = await loadExMapData(map); - chapter = 99; - mapLabel = `Ex-${map}`; - } else { - chapter = parseInt(chapterVal, 10); - mapData = await loadMapData(chapter, map); - mapLabel = `${chapter}-${map}`; - } - - if (!mapData) { - Logger.error(`地图 ${mapLabel} 数据不存在`); - return; - } - - const allNodes = Object.keys(mapData).sort(); - const plan = PlanModel.create(chapter, map, allNodes); - setters.setCurrentPlan(plan); - setters.setCurrentMapData(mapData); - setters.renderPlanPreview(); - host.switchPage('plan'); - Logger.info(`已新建方案 ${mapLabel},共 ${allNodes.length} 个节点`); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - Logger.error(`新建方案失败: ${msg}`); - } -} diff --git a/src/controller/plan/index.ts b/src/controller/plan/index.ts deleted file mode 100644 index 23d4eee..0000000 --- a/src/controller/plan/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PlanController } from './PlanController'; -export type { PlanHost } from './PlanController'; diff --git a/src/controller/plan/nodeEditor.ts b/src/controller/plan/nodeEditor.ts index b4ad780..487e040 100644 --- a/src/controller/plan/nodeEditor.ts +++ b/src/controller/plan/nodeEditor.ts @@ -1,14 +1,15 @@ +/** 编排节点编辑表单与 PlanModel 节点规则更新。 */ /** * nodeEditor —— 节点编辑面板值保存逻辑。 */ import type { PlanPreviewView } from '../../view/plan/PlanPreviewView'; import type { PlanModel } from '../../model/PlanModel'; -import type { EnemyRule } from '../../types/model'; +import type { EnemyRule, RuleAction } from '../../types/model.js'; import { Logger } from '../../utils/Logger'; -function normalizeRuleAction(actionRaw: string): string | number { +function normalizeRuleAction(actionRaw: string): RuleAction | null { const trimmed = actionRaw.trim(); - if (!trimmed) return trimmed; + if (!trimmed) return null; const aliases: Record = { detour: 'detour', @@ -20,9 +21,12 @@ function normalizeRuleAction(actionRaw: string): string | number { const lower = trimmed.toLowerCase(); const normalized = aliases[lower] ?? aliases[trimmed] ?? trimmed; if (/^\d+$/.test(normalized)) { - return Number(normalized); + const value = Number(normalized); + return value >= 1 && value <= 5 ? (value as RuleAction) : null; } - return normalized; + return normalized === 'retreat' || normalized === 'detour' + ? normalized + : null; } function parseRuleLine(rawLine: string): EnemyRule | null { @@ -44,11 +48,12 @@ function parseRuleLine(rawLine: string): EnemyRule | null { } if (!expr || !action) return null; - return [expr, normalizeRuleAction(action)]; + const normalizedAction = normalizeRuleAction(action); + return normalizedAction === null ? null : [expr, normalizedAction]; } /** - * 从节点编辑面板收集值并写回 PlanModel,然后即时保存到文件。 + * 从节点编辑面板收集值并写回 PlanModel。 * 返回 true 表示成功执行。 */ export function saveNodeEditorValues( @@ -73,10 +78,18 @@ export function saveNodeEditorValues( currentPlan.data.node_args = undefined; } } - - if (currentPlan.fileName) { - const bridge = window.electronBridge; - bridge?.saveFile(currentPlan.fileName, currentPlan.toYaml()); + const endpointNodes = + currentPlan.data.endpoint_nodes; + if (endpointNodes) { + const endpointIndex = + endpointNodes.indexOf(editingNodeId); + if (endpointIndex >= 0) { + endpointNodes.splice(endpointIndex, 1); + } + if (endpointNodes.length === 0) { + currentPlan.data.endpoint_nodes = undefined; + currentPlan.data.result = undefined; + } } planView.hideNodeEditor(); @@ -99,6 +112,9 @@ export function saveNodeEditorValues( } if (currentPlan.data.endpoint_nodes.length === 0) { currentPlan.data.endpoint_nodes = undefined; + currentPlan.data.result = undefined; + } else if (vals.isEndpoint) { + currentPlan.data.result = vals.result; } // 解析索敌规则文本 @@ -134,12 +150,6 @@ export function saveNodeEditorValues( enemy_rules: rules.length > 0 ? rules : undefined, }; - // 即时保存到文件 - if (currentPlan.fileName) { - const bridge = window.electronBridge; - bridge?.saveFile(currentPlan.fileName, currentPlan.toYaml()); - } - planView.hideNodeEditor(); return true; } diff --git a/src/controller/plan/presetFlow.ts b/src/controller/plan/presetFlow.ts index 4c4be73..b7ea508 100644 --- a/src/controller/plan/presetFlow.ts +++ b/src/controller/plan/presetFlow.ts @@ -2,7 +2,7 @@ * presetFlow —— 任务预设相关流程独立函数。 */ import type { PlanPreviewView } from '../../view/plan/PlanPreviewView'; -import type { TaskRequest, NormalFightReq } from '../../types/api'; +import type { TaskRequest } from '../../types/api'; import { TaskPriority } from '../../model/scheduler'; import type { TaskPreset } from '../../types/model'; import { TemplateController } from '../template/TemplateController'; diff --git a/src/controller/plan/rendering.ts b/src/controller/plan/rendering.ts index e378f85..4e17ddd 100644 --- a/src/controller/plan/rendering.ts +++ b/src/controller/plan/rendering.ts @@ -1,3 +1,4 @@ +/** 把作战方案、地图和节点数据转换为预览 ViewObject。 */ /** * rendering —— 方案预览 ViewObject 构建逻辑。 */ @@ -5,9 +6,12 @@ import type { PlanPreviewViewObject, NodeViewObject, MapEdgeVO, -} from '../../types/view'; +} from '../../types/view.js'; import type { PlanModel } from '../../model/PlanModel'; -import { FORMATION_NAMES } from '../../types/model'; +import { + FORMATION_NAMES, + type EventMapCatalogEntry, +} from '../../types/model.js'; import { getNodeType, isDetourNode, isNightNode } from '../../model/MapDataLoader'; import type { MapData } from '../../model/MapDataLoader'; @@ -15,6 +19,7 @@ import type { MapData } from '../../model/MapDataLoader'; export function buildPlanPreviewVO( currentPlan: PlanModel, currentMapData: MapData | null, + eventMaps: EventMapCatalogEntry[] = [], ): PlanPreviewViewObject { const plan = currentPlan; const mapData = currentMapData; @@ -39,6 +44,7 @@ export function buildPlanPreviewVO( // 构建地图可视化数据 let allNodes: NodeViewObject[] | undefined; let edges: MapEdgeVO[] | undefined; + let mapAspectRatio: number | undefined; if (mapData) { const positions = new Map(); for (const [id, pt] of Object.entries(mapData)) { @@ -59,13 +65,16 @@ export function buildPlanPreviewVO( const PAD = 6; const innerW = 100 - PAD * 2; const innerH = 100 - PAD * 2; - const scale = Math.min(innerW / rangeX, innerH / rangeY); - const offsetX = PAD + (innerW - rangeX * scale) / 2; - const offsetY = PAD + (innerH - rangeY * scale) / 2; + const scaleX = innerW / rangeX; + const scaleY = innerH / rangeY; + mapAspectRatio = rangeX / rangeY; const scaledPos = new Map(); for (const [id, [x, y]] of positions) { - scaledPos.set(id, [(x - minX) * scale + offsetX, (y - minY) * scale + offsetY]); + scaledPos.set(id, [ + (x - minX) * scaleX + PAD, + (y - minY) * scaleY + PAD, + ]); } allNodes = Object.entries(mapData).map(([id, pt]) => { @@ -102,6 +111,8 @@ export function buildPlanPreviewVO( chapter: plan.data.chapter, map: plan.data.map, mapName: plan.mapName, + event: plan.data.event, + eventMaps, repairModeValue: Array.isArray(plan.repairMode) ? plan.repairMode[0] ?? 1 : plan.repairMode, fightConditionValue: plan.fightCondition, fleetId: plan.data.fleet_id ?? 1, @@ -109,6 +120,7 @@ export function buildPlanPreviewVO( comment: plan.comment, allNodes, edges, + mapAspectRatio, fleetPresets: plan.data.fleet_presets?.map(p => ({ name: p.name, ships: p.ships })), times: plan.data.times, gap: plan.data.gap, diff --git a/src/controller/plan/selectedNodes.ts b/src/controller/plan/selectedNodes.ts index 16bcade..198778d 100644 --- a/src/controller/plan/selectedNodes.ts +++ b/src/controller/plan/selectedNodes.ts @@ -1,3 +1,4 @@ +/** 维护地图节点选择顺序并同步方案的选中节点。 */ /** * 将前端方案的 selected_nodes 规范化为后端可用格式。 * diff --git a/src/controller/shared/ControllerHost.ts b/src/controller/shared/ControllerHost.ts deleted file mode 100644 index 1bbecb2..0000000 --- a/src/controller/shared/ControllerHost.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Scheduler } from '../../model/scheduler'; - -/** - * ControllerHost —— 子控制器通用 Host 基接口。 - * 各子控制器(PlanController / TaskGroupController 等) - * 通过此接口与 AppController 通信。 - */ -export interface ControllerHost { - readonly scheduler: Scheduler; - plansDir: string; - renderMain(): void; - switchPage(page: string): void; -} diff --git a/src/controller/shared/DialogHelper.ts b/src/controller/shared/DialogHelper.ts index 3d659dd..43b6ccc 100644 --- a/src/controller/shared/DialogHelper.ts +++ b/src/controller/shared/DialogHelper.ts @@ -1,8 +1,48 @@ +/** 封装 Renderer 通用确认、提示和输入对话框。 */ /** * DialogHelper —— 通用对话框工具类。 - * 封装 showPrompt / showConfirm / showAlert,使用 #generic-prompt 遮罩层。 + * 封装通用对话框和页面顶部的保存成功提示。 */ +let saveSuccessTimer: number | null = null; + +/** 仅在数据确认写入成功后调用。 */ +export function showSaveSuccess(message = '保存成功'): void { + let notice = document.getElementById('save-success-notice'); + if (!notice) { + notice = document.createElement('div'); + notice.id = 'save-success-notice'; + notice.className = 'save-success-notice'; + notice.setAttribute('role', 'status'); + notice.setAttribute('aria-live', 'polite'); + notice.setAttribute('aria-hidden', 'true'); + + const icon = document.createElement('span'); + icon.className = 'save-success-notice-icon'; + icon.textContent = '✓'; + icon.setAttribute('aria-hidden', 'true'); + + const text = document.createElement('span'); + text.className = 'save-success-notice-text'; + notice.append(icon, text); + document.body.append(notice); + } + + const text = notice.querySelector('.save-success-notice-text'); + if (text) text.textContent = message; + notice.classList.add('is-visible'); + notice.setAttribute('aria-hidden', 'false'); + + if (saveSuccessTimer !== null) { + window.clearTimeout(saveSuccessTimer); + } + saveSuccessTimer = window.setTimeout(() => { + notice?.classList.remove('is-visible'); + notice?.setAttribute('aria-hidden', 'true'); + saveSuccessTimer = null; + }, 2400); +} + /** 弹出输入框,返回用户输入的字符串,取消返回 null */ export function showPrompt(title: string, message = '', defaultValue = ''): Promise { const overlay = document.getElementById('generic-prompt')!; diff --git a/src/controller/shared/index.ts b/src/controller/shared/index.ts deleted file mode 100644 index 28bc9fd..0000000 --- a/src/controller/shared/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { ControllerHost } from './ControllerHost'; -export { showPrompt, showConfirm, showAlert } from './DialogHelper'; diff --git a/src/controller/startup/StartupController.ts b/src/controller/startup/StartupController.ts index 34b6688..7a3c67c 100644 --- a/src/controller/startup/StartupController.ts +++ b/src/controller/startup/StartupController.ts @@ -1,8 +1,9 @@ +/** 统一协调应用启动、后端连接、环境检查和资源释放。 */ /** * StartupController —— 应用启动流程控制器(精简版)。 * 环境检查 → envAndUpdates.ts,后端连接 → connection.ts。 */ -import type { ElectronBridge } from '../../types/electronBridge'; +import type { ElectronBridge } from '../../types/ipc.js'; import type { Scheduler, CronScheduler } from '../../model/scheduler'; import type { ConfigModel } from '../../model/ConfigModel'; import { Logger } from '../../utils/Logger'; @@ -28,6 +29,7 @@ export interface StartupHost { detectAndApplyEmulator(): Promise; showSetupWizard(): Promise; loadModelsAndRender(bridge: ElectronBridge): Promise; + reviewMigrationConflicts(): Promise; bindBackendLog(bridge: ElectronBridge): void; renderMain(): void; startHeartbeat(): void; @@ -38,6 +40,8 @@ export interface StartupHost { // ════════════════════════════════════════ export class StartupController { + private heartbeatTimer: ReturnType | null = null; + constructor(private readonly host: StartupHost) {} /** 完整的异步启动流程 */ @@ -56,6 +60,9 @@ export class StartupController { Logger.info(`配置文件目录: ${this.host.configDir}`); Logger.info(`方案文件目录: ${this.host.plansDir}`); + // 在配置和任务组读入内存前处理冲突,避免保存时恢复已删除文件的旧引用。 + await this.host.reviewMigrationConflicts(); + // 1. 加载配置 & 渲染 await this.host.loadConfigAndSync(); Logger.debug('配置加载完成'); @@ -87,7 +94,11 @@ export class StartupController { } else { Logger.info('正在启动后端服务…'); } - await bridge.startBackend(); + const backendResult = await bridge.startBackend(); + if (!backendResult.success) { + Logger.error(`后端启动失败: ${backendResult.message}`); + return; + } waitForBackendAndConnect(this.host); } @@ -110,4 +121,32 @@ export class StartupController { startSystem(): void { import('./connection.js').then(m => m.startSystem(this.host)); } + + startHeartbeat(): void { + this.stopHeartbeat(); + let consecutiveFails = 0; + this.heartbeatTimer = setInterval(async () => { + try { + const alive = await this.host.scheduler.ping(); + consecutiveFails = alive ? 0 : consecutiveFails + 1; + } catch { + consecutiveFails++; + } + if (consecutiveFails < 3) return; + + Logger.error('后端连续 3 次心跳失败,尝试自动重启…'); + this.stopHeartbeat(); + const bridge = window.electronBridge; + if (bridge?.startBackend) { + await bridge.startBackend(); + this.waitForBackendAndConnect(); + } + }, 30_000); + } + + stopHeartbeat(): void { + if (!this.heartbeatTimer) return; + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } } diff --git a/src/controller/startup/connection.ts b/src/controller/startup/connection.ts index 898da05..bbcfffb 100644 --- a/src/controller/startup/connection.ts +++ b/src/controller/startup/connection.ts @@ -1,3 +1,4 @@ +/** 检测后端可用性并管理启动阶段的连接等待。 */ /** * connection —— 后端连接与系统启动逻辑。 */ @@ -28,13 +29,14 @@ export function waitForBackendAndConnect(host: StartupHost, retries = 30): void /** 向后端发送 system/start (连接模拟器+启动游戏) */ export function startSystem(host: StartupHost): void { - const configPath = host.appRoot - ? `${host.appRoot.replace(/\\/g, '/')}/usersettings.yaml` + const configPath = host.configDir + ? `${host.configDir.replace(/\\/g, '/')}/usersettings.yaml` : undefined; - host.scheduler.setExpeditionInterval( - host.configModel.current.daily_automation.expedition_interval, - ); + const automation = host.configModel.current.daily_automation; + const guiAutomation = host.configModel.currentGuiAutomation; + host.scheduler.setAutoExpedition(automation.auto_expedition); + host.scheduler.setExpeditionInterval(guiAutomation.expeditionInterval); host.scheduler.start(configPath).then((ok) => { if (ok) { diff --git a/src/controller/startup/envAndUpdates.ts b/src/controller/startup/envAndUpdates.ts index 675265e..3f76aa6 100644 --- a/src/controller/startup/envAndUpdates.ts +++ b/src/controller/startup/envAndUpdates.ts @@ -1,7 +1,8 @@ +/** 编排运行环境准备、依赖安装和 GUI 更新检查。 */ /** * envAndUpdates —— 环境检查、依赖安装、更新检查逻辑。 */ -import type { ElectronBridge } from '../../types/electronBridge'; +import type { ElectronBridge } from '../../types/ipc.js'; import type { StartupHost } from './StartupController'; import { Logger } from '../../utils/Logger'; diff --git a/src/controller/startup/index.ts b/src/controller/startup/index.ts deleted file mode 100644 index 01b4584..0000000 --- a/src/controller/startup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { StartupController, type StartupHost } from './StartupController'; diff --git a/src/controller/taskGroup/DailyTaskLoaderController.ts b/src/controller/taskGroup/DailyTaskLoaderController.ts new file mode 100644 index 0000000..bc2ad63 --- /dev/null +++ b/src/controller/taskGroup/DailyTaskLoaderController.ts @@ -0,0 +1,181 @@ +/** 管理日常任务浮窗的加载、分类、卡片参数和提交动作。 */ +import type { + DailyPlanSelection, + DailyPlanType, + ManagedDailyPlan, +} from '../../types/ipc.js'; +import { DailyTaskLoaderView } from '../../view/taskGroup/DailyTaskLoaderView'; +import { showAlert } from '../shared/DialogHelper'; + +export interface DailyTaskLoaderActions { + addToList(selection: DailyPlanSelection): void; + addToQueue(selection: DailyPlanSelection): Promise; +} + +/** 为任务列表提供独立于普通作战计划的日常任务选择流程。 */ +export class DailyTaskLoaderController { + private plans: ManagedDailyPlan[] = []; + private activeType: DailyPlanType = 'exercise'; + private selectedPlan: ManagedDailyPlan | null = null; + private readonly drafts = new Map(); + + constructor( + private readonly view: DailyTaskLoaderView, + private readonly actions: DailyTaskLoaderActions, + ) {} + + bindActions(): void { + this.view.bindActions({ + onClose: () => this.view.close(), + onTabChange: type => this.changeTab(type), + onSelect: plan => this.select(plan), + onTimesChange: (plan, times) => { + const draft = this.draft(plan); + draft.times = Math.max(1, times); + this.selectedPlan = plan; + this.render(); + }, + onQuickRepairChange: (plan, useQuickRepair) => { + const draft = this.draft(plan); + draft.useQuickRepair = useQuickRepair; + this.selectedPlan = plan; + this.render(); + }, + onAddToList: () => this.addToList(), + onAddToQueue: () => void this.addToQueue(), + }); + } + + open(): void { + this.view.open(); + void this.refresh(); + } + + private async refresh(): Promise { + const bridge = window.electronBridge; + if (!bridge?.listDailyPlans) { + this.view.setStatus('请完整重启 GUI 后再操作'); + return; + } + this.view.setStatus('正在读取日常任务...'); + try { + const result = await bridge.listDailyPlans(); + this.plans = result.plans; + this.plans.forEach(plan => this.draft(plan)); + const visible = this.visiblePlans(); + const selectedPlan = this.selectedPlan; + if ( + !selectedPlan + || !this.plans.some(plan => this.samePlan(plan, selectedPlan)) + ) { + this.selectedPlan = visible[0] ?? null; + } + this.view.setStatus( + result.errors.length > 0 + ? `${result.errors.length} 个日常任务 YAML 无法读取` + : '', + ); + this.render(); + } catch (error) { + this.plans = []; + this.selectedPlan = null; + this.view.setStatus( + `读取失败:${error instanceof Error ? error.message : String(error)}`, + ); + this.render(); + } + } + + private changeTab(type: DailyPlanType): void { + this.activeType = type; + const visible = this.visiblePlans(); + if ( + !this.selectedPlan + || this.selectedPlan.taskType !== type + ) { + this.selectedPlan = visible[0] ?? null; + } + this.render(); + } + + private select(plan: ManagedDailyPlan): void { + this.selectedPlan = plan; + this.render(); + } + + private addToList(): void { + const selection = this.selection(); + if (!selection) return; + this.actions.addToList(selection); + this.view.close(); + } + + private async addToQueue(): Promise { + const selection = this.selection(); + if (!selection) return; + try { + await this.actions.addToQueue(selection); + this.view.close(); + } catch (error) { + await showAlert( + '加入执行队列失败', + error instanceof Error ? error.message : String(error), + ); + } + } + + private selection(): DailyPlanSelection | null { + if (!this.selectedPlan) return null; + const draft = this.draft(this.selectedPlan); + return { + plan: this.selectedPlan, + times: this.selectedPlan.taskType === 'exercise' + ? 1 + : draft.times, + useQuickRepair: this.selectedPlan.taskType === 'decisive' + ? draft.useQuickRepair + : undefined, + }; + } + + private draft(plan: ManagedDailyPlan): { + times: number; + useQuickRepair: boolean; + } { + const key = this.planKey(plan); + const existing = this.drafts.get(key); + if (existing) return existing; + const created = { + times: Math.max(1, plan.times || 1), + useQuickRepair: plan.useQuickRepair !== false, + }; + this.drafts.set(key, created); + return created; + } + + private render(): void { + this.view.render( + this.plans, + this.activeType, + this.selection(), + ); + } + + private visiblePlans(): ManagedDailyPlan[] { + return this.plans.filter(plan => plan.taskType === this.activeType); + } + + private samePlan( + left: ManagedDailyPlan, + right: ManagedDailyPlan, + ): boolean { + return left.source === right.source && left.file === right.file; + } + + private planKey(plan: ManagedDailyPlan): string { + return `${plan.source}/${plan.file}`; + } +} diff --git a/src/controller/taskGroup/TaskGroupController.ts b/src/controller/taskGroup/TaskGroupController.ts index e6395d5..ffc058a 100644 --- a/src/controller/taskGroup/TaskGroupController.ts +++ b/src/controller/taskGroup/TaskGroupController.ts @@ -1,20 +1,57 @@ +/** 管理任务组选择、增删改和 Model 到 ViewObject 的映射。 */ /** * TaskGroupController —— 任务组控制器(瘦身版)。 - * 核心逻辑委托给 importExport / addItems / queueLoader / contextMenu / metaLoader 模块。 + * 核心逻辑委托给 addItems / queueLoader / contextMenu / metaLoader 模块。 */ -import { TaskGroupModel, type TaskGroupItem } from '../../model/TaskGroupModel'; -import { TaskGroupView, type TaskGroupItemMeta } from '../../view/taskGroup/TaskGroupView'; +import { + TaskGroupModel, + type TaskGroupItem, +} from '../../model/TaskGroupModel'; +import { TaskGroupView } from '../../view/taskGroup/TaskGroupView'; import { TemplateModel } from '../../model/TemplateModel'; import type { PlanModel } from '../../model/PlanModel'; import type { Scheduler } from '../../model/scheduler'; -import type { TaskPreset } from '../../types/model'; +import type { TaskPreset } from '../../types/model.js'; +import type { TaskGroupItemViewObject } from '../../types/view.js'; +import type { + ManagedBattlePlanSelection, + PlanPresetSource, +} from '../../types/ipc.js'; import type { MapData } from '../../model/MapDataLoader'; -import { showPrompt, showConfirm, showAlert } from '../shared/DialogHelper'; -import { exportTaskGroupFlow, importTaskGroupFlow } from './importExport'; -import { addCurrentPlanToGroup, addFileToGroup, addPresetToGroup } from './addItems'; -import { loadGroupToQueue, loadSingleItemToQueue } from './queueLoader'; +import { showAlert, showSaveSuccess } from '../shared/DialogHelper'; +import { + addCurrentPlanToGroup, + addDailyPlanToGroup, + addManagedPlanToGroup, + addPresetToGroup, +} from './addItems'; +import { + loadDailyPlanToQueue, + loadGroupToQueue, + loadSingleItemToQueue, +} from './queueLoader'; import { showContextMenuForItem, hideContextMenu, handleContextMenuEdit, type ContextMenuTarget, type ContextMenuHost } from './contextMenu'; import { loadItemMetas } from './metaLoader'; +import { TaskListLoaderController } from './TaskListLoaderController'; +import { DailyTaskLoaderController } from './DailyTaskLoaderController'; +import { DailyTaskLoaderView } from '../../view/taskGroup/DailyTaskLoaderView'; + +function toTaskGroupItemViewObject( + item: TaskGroupItem, +): TaskGroupItemViewObject { + return { + path: item.path, + managedSource: item.managedSource, + managedFile: item.managedFile, + dailySource: item.dailySource, + dailyFile: item.dailyFile, + dailyTaskType: item.dailyTaskType, + templateId: item.templateId, + kind: item.kind, + times: item.times, + label: item.label, + }; +} export interface TaskGroupHost { readonly scheduler: Scheduler; @@ -28,10 +65,14 @@ export interface TaskGroupHost { closePresetDetail(): void; executePreset(): void; getCurrentPresetInfo(): { preset: TaskPreset; filePath: string } | null; + pickManagedBattlePlan(): Promise; + openManagedPlan(file: string, source: PlanPresetSource): Promise; } export class TaskGroupController { private contextMenuTarget: ContextMenuTarget | null = null; + private readonly taskListLoader: TaskListLoaderController; + private readonly dailyTaskLoader: DailyTaskLoaderController; constructor( private readonly taskGroupModel: TaskGroupModel, @@ -39,50 +80,69 @@ export class TaskGroupController { private readonly templateModel: TemplateModel, private readonly mainView: { onDropFromTaskGroup?: (index: number) => void; onEditQueueItem?: (taskId: string, x: number, y: number) => void }, readonly host: TaskGroupHost, - ) {} + ) { + this.taskListLoader = new TaskListLoaderController( + this.taskGroupModel, + () => this.render(), + ); + this.dailyTaskLoader = new DailyTaskLoaderController( + new DailyTaskLoaderView(), + { + addToList: selection => addDailyPlanToGroup( + this.taskGroupModel, + selection, + () => this.render(), + ), + addToQueue: selection => loadDailyPlanToQueue( + selection, + this.host, + ), + }, + ); + } bindActions(): void { - this.taskGroupView.onSelectGroup = (name) => { - this.taskGroupModel.setActiveGroup(name); - this.render(); - }; - + this.dailyTaskLoader.bindActions(); this.taskGroupView.onNewGroup = async () => { - const name = await showPrompt('新建任务列表', '请输入名称:'); - if (!name?.trim()) return; - const trimmed = name.trim(); - if (this.taskGroupModel.getGroup(trimmed)) { - await showAlert('提示', `任务列表「${trimmed}」已存在,请换一个名称或直接选择它。`); - return; + const baseName = '新任务列表'; + let name = baseName; + let suffix = 2; + while (this.taskGroupModel.getGroup(name)) { + name = `${baseName} ${suffix}`; + suffix += 1; } - this.taskGroupModel.upsertGroup(trimmed); - this.taskGroupModel.setActiveGroup(trimmed); - this.taskGroupModel.save(); - this.render(); - }; - - this.taskGroupView.onDeleteGroup = async () => { - const active = this.taskGroupModel.getActiveGroup(); - if (!active) return; - const yes = await showConfirm('删除确认', `确认删除任务列表「${active.name}」?`); - if (!yes) return; - this.taskGroupModel.deleteGroup(active.name); - this.taskGroupModel.save(); + this.taskGroupModel.upsertGroup(name); + this.taskGroupModel.setActiveGroup(name); + await this.taskGroupModel.save(); this.render(); }; - this.taskGroupView.onRenameGroup = async () => { + this.taskGroupView.onSaveGroup = async () => { const active = this.taskGroupModel.getActiveGroup(); - if (!active) return; - const newName = await showPrompt('重命名', '新名称:', active.name); - if (!newName?.trim() || newName.trim() === active.name) return; - const trimmed = newName.trim(); - if (!this.taskGroupModel.renameGroup(active.name, trimmed)) { - await showAlert('提示', `名称「${trimmed}」已被占用。`); + const name = this.taskGroupView.getGroupName(); + if (!name) { + await showAlert('提示', '请输入任务列表名称。'); + return; + } + if (!active) { + this.taskGroupModel.upsertGroup(name); + this.taskGroupModel.setActiveGroup(name); + } else if (active.name !== name) { + if (!this.taskGroupModel.renameGroup(active.name, name)) { + await showAlert('提示', `名称「${name}」已被占用。`); + return; + } + } + const saved = await this.taskGroupModel.save(); + if (!saved) { + await showAlert('保存失败', '任务列表未能写入本地文件。'); return; } - this.taskGroupModel.save(); this.render(); + showSaveSuccess(`任务列表「${name}」保存成功`); + }; + this.taskGroupView.onOpenGroupLoader = () => { + this.taskListLoader.open(); }; this.taskGroupView.onRemoveItem = (index) => { @@ -109,11 +169,28 @@ export class TaskGroupController { }; this.taskGroupView.onLoadAll = () => loadGroupToQueue(this.taskGroupModel, this.templateModel, this.host); - this.taskGroupView.onAddFile = () => addFileToGroup(this.taskGroupModel, this.host.plansDir, () => this.render()); - this.taskGroupView.onExportGroup = () => exportTaskGroupFlow(this.taskGroupModel); - this.taskGroupView.onImportGroup = () => importTaskGroupFlow(this.taskGroupModel, () => this.render()); - + this.taskGroupView.onAddManagedPlan = async () => { + const selection = await this.host.pickManagedBattlePlan(); + if (!selection) return; + addManagedPlanToGroup( + this.taskGroupModel, + selection.plan, + selection.fleetPresetIndex, + () => this.render(), + ); + }; + this.taskGroupView.onAddDailyPlan = () => { + this.dailyTaskLoader.open(); + }; this.taskGroupView.onDropToQueue = () => {}; + this.taskGroupView.onLoadItem = (index) => { + void loadSingleItemToQueue( + index, + this.taskGroupModel, + this.templateModel, + this.host, + ); + }; this.mainView.onDropFromTaskGroup = (index) => loadSingleItemToQueue(index, this.taskGroupModel, this.templateModel, this.host); this.taskGroupView.onEditItem = (index, x, y) => { @@ -156,11 +233,12 @@ export class TaskGroupController { const groups = this.taskGroupModel.groups; const active = this.taskGroupModel.getActiveGroup(); const items = active?.items ?? []; + const itemViews = items.map(toTaskGroupItemViewObject); this.taskGroupView.render({ groups: groups.map(g => ({ name: g.name, itemCount: g.items.length })), activeGroupName: this.taskGroupModel.activeGroupName, - items, + items: itemViews, }); if (items.length > 0) { @@ -169,7 +247,7 @@ export class TaskGroupController { this.taskGroupView.render({ groups: groups.map(g => ({ name: g.name, itemCount: g.items.length })), activeGroupName: this.taskGroupModel.activeGroupName, - items, + items: itemViews, itemMetas: metas, }); }); diff --git a/src/controller/taskGroup/TaskListLoaderController.ts b/src/controller/taskGroup/TaskListLoaderController.ts new file mode 100644 index 0000000..fd7c7cb --- /dev/null +++ b/src/controller/taskGroup/TaskListLoaderController.ts @@ -0,0 +1,340 @@ +/** 协调任务列表文件选择、解析和批量载入。 */ +/** + * Task-list loader shown on the home page. + * + * The left column selects a saved task group. The right column previews the + * group's plans and keeps drag sorting in memory until the user confirms. + */ +import type { + TaskGroupItem, + TaskGroupModel, +} from '../../model/TaskGroupModel'; +import type { ManagedBattlePlan } from '../../types/ipc.js'; +import { + captureScrollPosition, + restoreScrollPosition, +} from '../../view/shared/scrollPosition'; +import { showConfirm } from '../shared/DialogHelper'; + +export class TaskListLoaderController { + private readonly dialog: HTMLElement; + private readonly countEl: HTMLElement; + private readonly groupsEl: HTMLElement; + private readonly previewTitleEl: HTMLElement; + private readonly previewEl: HTMLElement; + private readonly confirmButton: HTMLButtonElement; + private selectedGroupName = ''; + private draftItems: TaskGroupItem[] = []; + private managedPlans: ManagedBattlePlan[] = []; + private draggedIndex: number | null = null; + + constructor( + private readonly model: TaskGroupModel, + private readonly onLoaded: () => void, + ) { + this.dialog = document.getElementById('task-list-loader')!; + this.countEl = document.getElementById('task-list-loader-count')!; + this.groupsEl = document.getElementById('task-list-loader-groups')!; + this.previewTitleEl = document.getElementById( + 'task-list-loader-preview-title', + )!; + this.previewEl = document.getElementById('task-list-loader-preview')!; + this.confirmButton = document.getElementById( + 'btn-confirm-task-list-loader', + ) as HTMLButtonElement; + this.bindActions(); + } + + open(): void { + const groups = this.model.groups; + this.countEl.textContent = `共 ${groups.length} 个计划组`; + const activeExists = groups.some( + group => group.name === this.model.activeGroupName, + ); + this.selectGroup( + activeExists + ? this.model.activeGroupName + : groups[0]?.name ?? '', + ); + this.dialog.style.display = 'flex'; + void this.refreshManagedPlans(); + } + + private async refreshManagedPlans(): Promise { + const bridge = window.electronBridge; + if (!bridge?.getPlanManagement) return; + try { + const result = await bridge.getPlanManagement(); + this.managedPlans = result.battlePlans; + if (this.dialog.style.display !== 'none') this.renderPreview(); + } catch { + this.managedPlans = []; + } + } + + private bindActions(): void { + document.getElementById('btn-cancel-task-list-loader') + ?.addEventListener('click', () => this.close()); + this.confirmButton.addEventListener('click', () => { + void this.confirm(); + }); + this.dialog.addEventListener('click', event => { + if (event.target === this.dialog) this.close(); + }); + document.addEventListener('keydown', event => { + if (event.key === 'Escape' && this.dialog.style.display !== 'none') { + this.close(); + } + }); + } + + private close(): void { + this.dialog.style.display = 'none'; + this.draggedIndex = null; + } + + private selectGroup(name: string): void { + this.selectedGroupName = name; + const group = name ? this.model.getGroup(name) : null; + this.draftItems = group + ? group.items.map(item => ({ ...item })) + : []; + this.confirmButton.disabled = !group; + this.renderGroups(); + this.renderPreview(); + } + + private renderGroups(): void { + const scrollPosition = captureScrollPosition(this.groupsEl); + this.groupsEl.innerHTML = ''; + if (this.model.groups.length === 0) { + this.groupsEl.innerHTML = [ + '
', + '暂无已保存的任务列表', + '
', + ].join(''); + restoreScrollPosition(this.groupsEl, scrollPosition); + return; + } + + for (const group of this.model.groups) { + const card = document.createElement('div'); + card.className = 'task-list-loader-group-card'; + card.classList.toggle('active', group.name === this.selectedGroupName); + + const selectButton = document.createElement('button'); + selectButton.type = 'button'; + selectButton.className = 'task-list-loader-group-select'; + const name = document.createElement('strong'); + name.textContent = group.name; + const count = document.createElement('span'); + count.textContent = `${group.items.length} 个出征计划`; + selectButton.append(name, count); + selectButton.addEventListener( + 'click', + () => this.selectGroup(group.name), + ); + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'task-list-loader-group-delete'; + deleteButton.textContent = '删除'; + deleteButton.title = `删除任务列表「${group.name}」`; + deleteButton.addEventListener('click', () => { + void this.deleteGroup(group.name); + }); + + card.append(selectButton, deleteButton); + this.groupsEl.appendChild(card); + } + restoreScrollPosition(this.groupsEl, scrollPosition); + } + + private renderPreview(): void { + this.previewTitleEl.textContent = this.selectedGroupName + ? `计划列表预览:${this.selectedGroupName}` + : '计划列表预览:未选择'; + this.previewEl.innerHTML = ''; + + if (!this.selectedGroupName) { + this.previewEl.innerHTML = [ + '
', + '从左侧选择一个计划组', + '
', + ].join(''); + return; + } + if (this.draftItems.length === 0) { + this.previewEl.innerHTML = [ + '
', + '该计划组尚未关联出征计划', + '
', + ].join(''); + return; + } + + this.draftItems.forEach((item, index) => { + this.previewEl.appendChild(this.createPreviewCard(item, index)); + }); + } + + private createPreviewCard( + item: TaskGroupItem, + index: number, + ): HTMLElement { + const card = document.createElement('div'); + card.className = 'task-list-loader-plan-card'; + card.draggable = true; + card.dataset['index'] = String(index); + + const handle = document.createElement('span'); + handle.className = 'tg-drag-handle'; + handle.textContent = '⠿'; + + const order = document.createElement('span'); + order.className = 'tg-order'; + order.textContent = String(index + 1).padStart(2, '0'); + + const content = document.createElement('div'); + content.className = 'tg-content'; + const heading = document.createElement('div'); + heading.className = 'tg-item-heading'; + const label = document.createElement('strong'); + label.className = 'tg-label'; + label.textContent = item.label; + const fleetName = this.fleetPresetName(item); + const fleetTag = document.createElement('span'); + fleetTag.className = 'tg-fleet-tag'; + fleetTag.textContent = fleetName; + fleetTag.title = `使用队伍:${fleetName}`; + fleetTag.hidden = !fleetName; + const source = document.createElement('span'); + source.className = `tg-source ${this.sourceClass(item)}`; + source.textContent = this.sourceLabel(item); + heading.append(label, fleetTag, source); + + const fileName = document.createElement('span'); + fileName.className = 'tg-detail'; + fileName.textContent = item.managedFile + ?? item.path?.split(/[\\/]/).pop() + ?? item.templateId + ?? '-'; + content.append(heading, fileName); + + const times = document.createElement('span'); + times.className = 'task-list-loader-plan-times'; + times.textContent = `执行 ${item.times} 次`; + card.append(handle, order, content, times); + + card.addEventListener('dragstart', event => { + this.draggedIndex = index; + card.classList.add('dragging'); + event.dataTransfer!.effectAllowed = 'move'; + }); + card.addEventListener('dragend', () => { + this.draggedIndex = null; + card.classList.remove('dragging'); + this.previewEl.querySelectorAll('.drag-over').forEach(element => { + element.classList.remove('drag-over'); + }); + }); + card.addEventListener('dragover', event => { + event.preventDefault(); + card.classList.add('drag-over'); + event.dataTransfer!.dropEffect = 'move'; + }); + card.addEventListener('dragleave', () => { + card.classList.remove('drag-over'); + }); + card.addEventListener('drop', event => { + event.preventDefault(); + card.classList.remove('drag-over'); + this.moveDraftItem(index); + }); + return card; + } + + private fleetPresetName(item: TaskGroupItem): string { + if ( + item.kind !== 'plan' + || !item.managedSource + || !item.managedFile + ) { + return ''; + } + const plan = this.managedPlans.find(candidate => ( + candidate.source === item.managedSource + && candidate.file === item.managedFile + )); + const presetIndex = item.fleetPresetIndex ?? 0; + return plan?.fleets[presetIndex]?.name ?? ''; + } + + private moveDraftItem(toIndex: number): void { + const fromIndex = this.draggedIndex; + if ( + fromIndex === null + || fromIndex === toIndex + || fromIndex < 0 + || fromIndex >= this.draftItems.length + ) { + return; + } + const [item] = this.draftItems.splice(fromIndex, 1); + this.draftItems.splice(toIndex, 0, item); + this.draggedIndex = null; + this.renderPreview(); + } + + private async deleteGroup(name: string): Promise { + const groupIndex = this.model.groups.findIndex( + group => group.name === name, + ); + if (!name || groupIndex < 0) return; + + const confirmed = await showConfirm( + '确认删除任务列表', + `确定删除任务列表「${name}」?\n\n删除后无法恢复。`, + ); + if (!confirmed || !this.model.deleteGroup(name)) return; + + await this.model.save(); + this.countEl.textContent = `共 ${this.model.groups.length} 个计划组`; + let nextGroupName = this.selectedGroupName; + if (name === this.selectedGroupName) { + const nextIndex = Math.min(groupIndex, this.model.groups.length - 1); + nextGroupName = this.model.groups[nextIndex]?.name ?? ''; + } + this.selectGroup(nextGroupName); + this.onLoaded(); + } + + private async confirm(): Promise { + if (!this.selectedGroupName) return; + this.model.upsertGroup( + this.selectedGroupName, + this.draftItems.map(item => ({ ...item })), + ); + this.model.setActiveGroup(this.selectedGroupName); + await this.model.save(); + this.onLoaded(); + this.close(); + } + + private sourceClass(item: TaskGroupItem): string { + if (item.kind === 'daily') return 'daily'; + if (item.managedSource === 'system') return 'system'; + if (item.managedSource === 'user') return 'user'; + if (item.kind === 'template') return 'template'; + return 'local'; + } + + private sourceLabel(item: TaskGroupItem): string { + if (item.kind === 'daily') return '日常任务'; + if (item.managedSource === 'system') return '系统预设'; + if (item.managedSource === 'user') return '用户预设'; + if (item.kind === 'template') return '任务模板'; + if (item.kind === 'preset') return '任务预设'; + return '本地文件'; + } +} diff --git a/src/controller/taskGroup/addItems.ts b/src/controller/taskGroup/addItems.ts index 322b47d..425a326 100644 --- a/src/controller/taskGroup/addItems.ts +++ b/src/controller/taskGroup/addItems.ts @@ -1,10 +1,16 @@ +/** 将方案、模板和预设添加为任务组条目。 */ /** * addItems —— 向任务组添加条目的独立函数。 */ import type { TaskGroupModel } from '../../model/TaskGroupModel'; import type { PlanModel } from '../../model/PlanModel'; -import type { TaskPreset } from '../../types/model'; +import type { TaskPreset } from '../../types/model.js'; +import type { + DailyPlanSelection, + ManagedBattlePlan, +} from '../../types/ipc.js'; import { Logger } from '../../utils/Logger'; +import { yamlCodec } from '../../adapter'; function buildInlinePlanPath(plan: PlanModel, plansDir: string): string { const safeMap = plan.mapName.replace(/[^a-zA-Z0-9_-]+/g, '_'); @@ -23,6 +29,54 @@ function ensureActiveGroup(taskGroupModel: TaskGroupModel) { return group; } +/** 将计划管理中的系统或用户方案添加到任务列表 */ +export function addManagedPlanToGroup( + taskGroupModel: TaskGroupModel, + plan: ManagedBattlePlan, + fleetPresetIndex: number | undefined, + render: () => void, +): void { + const group = ensureActiveGroup(taskGroupModel); + taskGroupModel.addItem(group.name, { + managedSource: plan.source, + managedFile: plan.file, + kind: plan.kind === 'preset' ? 'preset' : 'plan', + times: Math.max(1, plan.times || 1), + label: plan.name, + fleetPresetIndex, + }); + void taskGroupModel.save(); + render(); + Logger.info(`已将「${plan.name}」加入任务列表「${group.name}」`); +} + +/** 将日常任务加入任务列表,并保存卡片上的次数和快修选择。 */ +export function addDailyPlanToGroup( + taskGroupModel: TaskGroupModel, + selection: DailyPlanSelection, + render: () => void, +): void { + const group = ensureActiveGroup(taskGroupModel); + const { plan } = selection; + taskGroupModel.addItem(group.name, { + dailySource: plan.source, + dailyFile: plan.file, + dailyTaskType: plan.taskType, + kind: 'daily', + times: plan.taskType === 'exercise' + ? 1 + : Math.max(1, selection.times), + label: plan.name, + chapter: plan.chapter, + useQuickRepair: plan.taskType === 'decisive' + ? selection.useQuickRepair !== false + : undefined, + }); + void taskGroupModel.save(); + render(); + Logger.info(`已将日常任务「${plan.name}」加入任务列表「${group.name}」`); +} + /** 将当前已加载的 Plan 添加到任务组 */ export async function addCurrentPlanToGroup( taskGroupModel: TaskGroupModel, @@ -69,17 +123,23 @@ export async function addFileToGroup( ], plansDir || undefined); if (!result) return; - const parsed = (await import('js-yaml')).load(result.content) as Record; + const parsed = yamlCodec.parse>(result.content); let itemKind: 'plan' | 'preset' = 'plan'; - if (parsed && typeof parsed === 'object' && 'task_type' in parsed && !('chapter' in parsed)) { + if ( + parsed + && typeof parsed === 'object' + && 'task_type' in parsed + && !('map' in parsed) + ) { itemKind = 'preset'; } const label = result.path.split(/[\\/]/).pop()?.replace(/\.ya?ml$/i, '') ?? result.path; + const parsedTimes = Number(parsed?.times); taskGroupModel.addItem(group.name, { path: result.path, kind: itemKind, - times: (parsed as any)?.times ?? 1, + times: Number.isFinite(parsedTimes) && parsedTimes > 0 ? parsedTimes : 1, label, }); taskGroupModel.save(); diff --git a/src/controller/taskGroup/contextMenu.ts b/src/controller/taskGroup/contextMenu.ts index 1b240cc..b3f78c0 100644 --- a/src/controller/taskGroup/contextMenu.ts +++ b/src/controller/taskGroup/contextMenu.ts @@ -1,13 +1,16 @@ +/** 处理任务组条目的右键菜单、编辑、复制和删除。 */ /** * contextMenu —— 右键上下文菜单逻辑。 */ import type { TaskGroupModel } from '../../model/TaskGroupModel'; import type { Scheduler } from '../../model/scheduler'; import { PlanModel } from '../../model/PlanModel'; -import { loadMapData, loadExMapData, loadEventMapData } from '../../model/MapDataLoader'; +import { loadMapData, loadEventMapData } from '../../model/MapDataLoader'; import type { MapData } from '../../model/MapDataLoader'; -import type { TaskPreset } from '../../types/model'; +import type { TaskPreset } from '../../types/model.js'; +import type { PlanPresetSource } from '../../types/ipc.js'; import { Logger } from '../../utils/Logger'; +import { yamlCodec } from '../../adapter'; export interface ContextMenuTarget { source: 'taskgroup' | 'queue'; @@ -20,6 +23,7 @@ export interface ContextMenuHost { setCurrentPlan(plan: PlanModel, mapData: MapData | null): void; renderPlanPreview(): void; switchPage(page: string): void; + openManagedPlan(file: string, source: PlanPresetSource): Promise; } export function showContextMenuForItem( @@ -59,11 +63,26 @@ export async function handleContextMenuEdit( Logger.info(`模板「${item.label}」请在模板库中查看和编辑`); return; } + if (item.kind === 'daily') { + Logger.info( + item.dailyTaskType === 'decisive' + ? `决战日常任务「${item.label}」请在决战计划页面编辑` + : `日常任务「${item.label}」由日常任务浮窗管理`, + ); + return; + } + if (item.managedSource && item.managedFile) { + await host.openManagedPlan(item.managedFile, item.managedSource); + return; + } + if (!item.path) { + Logger.error(`「${item.label}」没有关联的配置文件`); + return; + } await openItemForEdit(item.path!, item.kind, host); } else { const taskId = target.id as string; - const running = host.scheduler.currentRunningTask; - const task = (running?.id === taskId) ? running : host.scheduler.taskQueue.find(t => t.id === taskId); + const task = host.scheduler.findTask(taskId); if (!task) return; const req = task.request; @@ -89,19 +108,17 @@ export async function openItemForEdit( try { const content = await bridge.readFile(filePath); - const parsed = (await import('js-yaml')).load(content) as Record; + const parsed = yamlCodec.parse>(content); if (!parsed || typeof parsed !== 'object') return; - if (kind === 'preset' || ('task_type' in parsed && !('chapter' in parsed))) { + if (kind === 'preset' || ('task_type' in parsed && !('map' in parsed))) { host.importTaskPreset(parsed as unknown as TaskPreset, filePath); } else { const plan = PlanModel.fromYaml(content, filePath); const { chapter, map } = plan.data; const mapData = plan.isEvent ? await loadEventMapData(plan.data.event ?? '', chapter, map) - : chapter === 99 - ? await loadExMapData(Number(map)) - : await loadMapData(Number(chapter), Number(map)); + : await loadMapData(Number(chapter), Number(map)); host.setCurrentPlan(plan, mapData); host.renderPlanPreview(); } diff --git a/src/controller/taskGroup/importExport.ts b/src/controller/taskGroup/importExport.ts deleted file mode 100644 index 5933949..0000000 --- a/src/controller/taskGroup/importExport.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * importExport —— 任务组导入/导出独立函数。 - */ -import type { TaskGroupModel } from '../../model/TaskGroupModel'; -import { Logger } from '../../utils/Logger'; -import { showAlert } from '../shared/DialogHelper'; - -/** 导出当前激活的任务组为 JSON */ -export async function exportTaskGroupFlow(taskGroupModel: TaskGroupModel): Promise { - const group = taskGroupModel.getActiveGroup(); - if (!group || group.items.length === 0) { - Logger.warn('当前任务列表为空,无法导出'); - return; - } - const bridge = window.electronBridge; - if (!bridge) return; - - const data = { name: group.name, items: group.items }; - const json = JSON.stringify(data, null, 2); - const saved = await bridge.saveFileDialog( - `${group.name}.taskgroup.json`, - json, - [{ name: '任务列表模板', extensions: ['taskgroup.json', 'json'] }], - ); - if (saved) { - Logger.info(`已导出任务列表「${group.name}」`); - } -} - -/** 从 JSON 文件导入任务组 */ -export async function importTaskGroupFlow( - taskGroupModel: TaskGroupModel, - render: () => void, -): Promise { - const bridge = window.electronBridge; - if (!bridge) return; - - const result = await bridge.openFileDialog([ - { name: '任务列表模板', extensions: ['taskgroup.json', 'json'] }, - ]); - if (!result) return; - - let data: { name?: string; items?: unknown[] }; - try { - data = JSON.parse(result.content); - } catch { - await showAlert('导入失败', '文件格式不正确,无法解析 JSON。'); - return; - } - - if (!Array.isArray(data.items) || data.items.length === 0) { - await showAlert('导入失败', '模板中没有有效的任务条目。'); - return; - } - - let groupName = typeof data.name === 'string' && data.name.trim() ? data.name.trim() : '导入的列表'; - if (taskGroupModel.getGroup(groupName)) { - let suffix = 2; - while (taskGroupModel.getGroup(`${groupName} (${suffix})`)) suffix++; - groupName = `${groupName} (${suffix})`; - } - - taskGroupModel.upsertGroup(groupName); - taskGroupModel.setActiveGroup(groupName); - - for (const raw of data.items) { - if (!raw || typeof raw !== 'object') continue; - const item = raw as Record; - if (typeof item.kind !== 'string') continue; - - const times = typeof item.times === 'number' && item.times > 0 ? item.times : 1; - const fleetId = typeof item.fleet_id === 'number' && item.fleet_id >= 1 && item.fleet_id <= 4 - ? item.fleet_id - : undefined; - const fleetPresetIndex = typeof item.fleetPresetIndex === 'number' && item.fleetPresetIndex >= 0 - ? Math.floor(item.fleetPresetIndex) - : undefined; - const forceRetry = typeof item.forceRetry === 'boolean' ? item.forceRetry : undefined; - const allowPolling = typeof item.allowPolling === 'boolean' ? item.allowPolling : undefined; - const autoFleetFallback = typeof item.autoFleetFallback === 'boolean' ? item.autoFleetFallback : undefined; - - if (item.kind === 'template') { - if (typeof item.templateId !== 'string' || !item.templateId.trim()) continue; - const label = typeof item.label === 'string' && item.label.trim() - ? item.label - : item.templateId; - taskGroupModel.addItem(groupName, { - templateId: item.templateId, - kind: 'template', - times, - label, - campaignName: typeof item.campaignName === 'string' ? item.campaignName : undefined, - chapter: typeof item.chapter === 'number' ? item.chapter : undefined, - fleet_id: fleetId, - forceRetry, - allowPolling, - }); - continue; - } - - if (typeof item.path !== 'string') continue; - const kind = item.kind === 'preset' ? 'preset' : 'plan'; - const label = typeof item.label === 'string' && item.label.trim() - ? item.label - : item.path.split(/[\\/]/).pop()?.replace(/\.ya?ml$/i, '') ?? String(item.path); - - taskGroupModel.addItem(groupName, { - path: item.path, - kind, - times, - label, - fleet_id: fleetId, - fleetPresetIndex, - forceRetry, - allowPolling, - autoFleetFallback, - }); - } - - taskGroupModel.save(); - render(); - Logger.info(`已导入任务列表「${groupName}」(${data.items.length} 项)`); -} diff --git a/src/controller/taskGroup/index.ts b/src/controller/taskGroup/index.ts deleted file mode 100644 index 6377fc1..0000000 --- a/src/controller/taskGroup/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TaskGroupController } from './TaskGroupController'; -export type { TaskGroupHost } from './TaskGroupController'; diff --git a/src/controller/taskGroup/managedPlanReader.ts b/src/controller/taskGroup/managedPlanReader.ts new file mode 100644 index 0000000..71430a1 --- /dev/null +++ b/src/controller/taskGroup/managedPlanReader.ts @@ -0,0 +1,68 @@ +/** 读取受管作战方案并统一返回内容和来源信息。 */ +/** + * Reads a task-list item from either the managed plan directories or a + * legacy/local file path. + */ +import type { TaskGroupItem } from '../../model/TaskGroupModel'; + +export interface TaskGroupItemFile { + content: string; + path: string; +} + +export async function readTaskGroupItemFile( + item: TaskGroupItem, +): Promise { + const bridge = window.electronBridge; + if (!bridge) throw new Error('Electron bridge is unavailable'); + + if (item.dailySource && item.dailyFile) { + if (!bridge.readDailyPlan) { + throw new Error('当前 GUI 不支持读取日常任务目录'); + } + const result = await bridge.readDailyPlan( + item.dailySource, + item.dailyFile, + ); + if (!result.success || result.content === undefined || !result.path) { + throw new Error(result.error || `无法读取 ${item.dailyFile}`); + } + return { + content: result.content, + path: result.path, + }; + } + + if (item.managedSource && item.managedFile) { + if (!bridge.readManagedCombatPlan) { + throw new Error('当前 GUI 不支持读取计划管理目录'); + } + const result = await bridge.readManagedCombatPlan( + item.managedSource, + item.managedFile, + ); + if (!result.success || result.content === undefined || !result.path) { + throw new Error(result.error || `无法读取 ${item.managedFile}`); + } + return { + content: result.content, + path: result.runtimePath ?? result.path, + }; + } + + if (!item.path) throw new Error('任务没有关联配置文件'); + if (bridge.readCombatPlanFile) { + const result = await bridge.readCombatPlanFile(item.path); + if (!result.success || result.content === undefined || !result.path) { + throw new Error(result.error || `无法读取 ${item.path}`); + } + return { + content: result.content, + path: result.runtimePath ?? result.path, + }; + } + return { + content: await bridge.readFile(item.path), + path: item.path, + }; +} diff --git a/src/controller/taskGroup/metaLoader.ts b/src/controller/taskGroup/metaLoader.ts index 5ba9381..a071ed2 100644 --- a/src/controller/taskGroup/metaLoader.ts +++ b/src/controller/taskGroup/metaLoader.ts @@ -1,9 +1,12 @@ +/** 批量读取任务条目元数据并生成界面展示摘要。 */ /** * metaLoader —— 异步加载任务组条目的元信息。 */ import type { TaskGroupItem } from '../../model/TaskGroupModel'; import type { TemplateModel } from '../../model/TemplateModel'; -import type { TaskGroupItemMeta } from '../../view/taskGroup/TaskGroupView'; +import type { TaskGroupItemMeta } from '../../types/view.js'; +import { readTaskGroupItemFile } from './managedPlanReader'; +import { yamlCodec } from '../../adapter'; const REPAIR: Record = { 1: '中破就修', 2: '大破才修' }; const TYPE_LABELS: Record = { @@ -34,20 +37,20 @@ export async function loadItemMetas( return meta; } - const content = await bridge.readFile(item.path!); - const parsed = (await import('js-yaml')).load(content) as Record; + const { content } = await readTaskGroupItemFile(item); + const parsed = yamlCodec.parse>(content); if (!parsed || typeof parsed !== 'object') return null; const meta: TaskGroupItemMeta = {}; - if (item.autoFleetFallback) meta.autoFleetFallback = true; if ('chapter' in parsed && 'map' in parsed) { const ch = String(parsed.chapter); const mp = String(parsed.map); if ('event' in parsed || /^[EH]$/i.test(ch)) { - const match = mp.match(/^(\d+)([ab])?$/i); - const entrance = match?.[2]?.toLowerCase() === 'b' ? 'β' : 'α'; - meta.mapName = `${ch.toUpperCase()}-Ex-${match?.[1] ?? mp}-${entrance}`; + const chapter = ch.toUpperCase(); + meta.mapName = chapter === 'EX' + ? `EX-${mp.toUpperCase()}` + : `${chapter}${mp.toUpperCase()}`; meta.typeLabel = '活动出击'; } else { const chapterNumber = Number(ch); @@ -63,7 +66,7 @@ export async function loadItemMetas( else if (Array.isArray(rm)) meta.repairMode = REPAIR[rm[0]] ?? `修理${rm[0]}`; } - if ('task_type' in parsed && !('chapter' in parsed)) { + if ('task_type' in parsed && !('map' in parsed)) { meta.typeLabel = TYPE_LABELS[String(parsed.task_type)] ?? String(parsed.task_type); } @@ -71,6 +74,14 @@ export async function loadItemMetas( meta.fleet = (parsed.fleet as unknown[]).map(s => String(s || '')).filter(Boolean); } + if ('fleet_presets' in parsed && Array.isArray(parsed.fleet_presets)) { + const presetIndex = item.fleetPresetIndex ?? 0; + const preset = parsed.fleet_presets[presetIndex]; + if (preset && typeof preset === 'object' && 'name' in preset) { + meta.fleetPresetName = String(preset.name || '').trim() || undefined; + } + } + return meta; } catch { return null; diff --git a/src/controller/taskGroup/queueLoader.ts b/src/controller/taskGroup/queueLoader.ts index 2279677..e67f91f 100644 --- a/src/controller/taskGroup/queueLoader.ts +++ b/src/controller/taskGroup/queueLoader.ts @@ -1,16 +1,222 @@ +/** 把任务组条目解析为 Scheduler 可执行任务并加入队列。 */ /** * queueLoader —— 将任务组条目加载到调度队列的独立函数。 */ import type { TaskGroupModel, TaskGroupItem } from '../../model/TaskGroupModel'; import type { TemplateModel } from '../../model/TemplateModel'; import { PlanModel } from '../../model/PlanModel'; -import { TaskPriority } from '../../model/scheduler'; -import type { EventFightReq, NormalFightReq, TaskRequest } from '../../types/api'; -import type { TaskPreset } from '../../types/model'; -import { resolveFleetPreset, resolveFleetPresetRules, toBackendName } from '../../data/shipData'; +import { TaskPriority, type Scheduler } from '../../model/scheduler'; +import type { EventFightReq, NormalFightReq, TaskRequest } from '../../types/api.js'; +import type { + DailyPlanSelection, + ManagedBattlePlanSelection, +} from '../../types/ipc.js'; +import type { TaskPreset } from '../../types/model.js'; +import { resolveFleetPreset } from '../../model/fleet/ShipMatcher'; +import { resolveFleetPresetRules } from '../../model/fleet/FleetRuleMapper'; +import { toBackendName } from '../../model/fleet/ShipNameNormalizer'; import { Logger } from '../../utils/Logger'; import { normalizeSelectedNodesForBackend } from '../plan/selectedNodes'; import type { TaskGroupHost } from './TaskGroupController'; +import { readTaskGroupItemFile } from './managedPlanReader'; +import { yamlCodec } from '../../adapter'; + +export function buildPlanQueueRequest( + item: TaskGroupItem, + plan: PlanModel, + planId: string, +): { + req: NormalFightReq | EventFightReq; + selectedFleetId: number | undefined; +} { + const req: NormalFightReq | EventFightReq = { + type: plan.isEvent ? 'event_fight' : 'normal_fight', + plan_id: planId, + times: 1, + gap: plan.data.gap ?? 0, + }; + if (plan.data.selected_nodes.length > 0) { + req.plan = req.plan ?? {}; + req.plan.selected_nodes = normalizeSelectedNodesForBackend( + plan.data.selected_nodes, + ); + } + + const selectedFleetId = item.fleet_id ?? plan.data.fleet_id; + if (selectedFleetId != null) { + if (req.type === 'event_fight') req.fleet_id = selectedFleetId; + req.plan = req.plan ?? {}; + req.plan.fleet_id = selectedFleetId; + } + + const presets = plan.data.fleet_presets; + if (presets?.length) { + // 旧任务列表未保存索引时沿用原行为,默认使用第一支编队。 + const presetIndex = item.fleetPresetIndex ?? 0; + const preset = presets[presetIndex]; + if (!preset) { + throw new Error(`选择的使用舰队不存在(索引 ${presetIndex})`); + } + const resolved = resolveFleetPreset(preset.ships); + const rules = resolveFleetPresetRules(preset.ships); + if (resolved.length === 0 || rules.length === 0) { + throw new Error(`使用舰队「${preset.name}」没有可用舰船`); + } + + // 后端覆盖请求只携带这一支编队,其他 fleet_presets 不进入请求。 + req.plan = req.plan ?? {}; + req.plan.fleet = resolved.map(toBackendName); + req.plan.fleet_rules = rules; + } else if (item.fleetPresetIndex != null) { + throw new Error('作战计划中已没有所选使用舰队'); + } + + return { req, selectedFleetId }; +} + +interface PlanQueueHost { + readonly scheduler: Scheduler; + renderMain(): void; +} + +function addPlanTaskToQueue( + item: TaskGroupItem, + plan: PlanModel, + planId: string, + scheduler: Scheduler, +): void { + const { req, selectedFleetId } = buildPlanQueueRequest(item, plan, planId); + scheduler.addTask( + plan.mapName, + plan.isEvent ? 'event_fight' : 'normal_fight', + req, + TaskPriority.USER_TASK, + item.times, + plan.data.stop_condition, + undefined, + selectedFleetId, + undefined, + undefined, + !!item.forceRetry, + !!item.allowPolling, + plan.data.endpoint_nodes, + plan.data.result, + typeof plan.data.chapter === 'number' + ? plan.data.chapter || undefined + : undefined, + ); +} + +/** 按任务预设类型构造请求并直接加入调度队列。 */ +function addPresetTaskToQueue( + item: TaskGroupItem, + preset: TaskPreset, + scheduler: Scheduler, +): void { + let req: TaskRequest; + if (preset.task_type === 'campaign') { + req = { + type: 'campaign', + campaign_name: preset.campaign_name ?? '', + times: 1, + }; + } else if (preset.task_type === 'exercise') { + req = { + type: 'exercise', + fleet_id: preset.fleet_id ?? 1, + }; + } else if (preset.task_type === 'decisive') { + req = { + type: 'decisive', + chapter: preset.chapter, + level1: preset.level1 ?? [], + level2: preset.level2 ?? [], + flagship_priority: preset.flagship_priority ?? [], + use_quick_repair: item.useQuickRepair + ?? preset.use_quick_repair + ?? true, + }; + } else { + req = { + type: preset.task_type, + plan_id: preset.plan_id, + times: 1, + gap: preset.gap ?? 0, + fleet_id: preset.fleet_id, + }; + } + const effectiveTimes = preset.task_type === 'exercise' + ? 1 + : Math.max(1, item.times || preset.times || 1); + scheduler.addTask( + item.label, + preset.task_type, + req, + TaskPriority.USER_TASK, + effectiveTimes, + preset.stop_condition, + undefined, + preset.fleet_id, + ); +} + +/** 将计划浮窗选中的受管计划直接加入任务队列。 */ +export async function loadManagedPlanToQueue( + selection: ManagedBattlePlanSelection, + host: PlanQueueHost, +): Promise { + const item: TaskGroupItem = { + managedSource: selection.plan.source, + managedFile: selection.plan.file, + kind: selection.plan.kind === 'preset' ? 'preset' : 'plan', + times: Math.max(1, selection.plan.times || 1), + label: selection.plan.name, + fleetPresetIndex: selection.fleetPresetIndex, + }; + const { content, path } = await readTaskGroupItemFile(item); + const parsed = yamlCodec.parse>(content); + if ( + item.kind === 'preset' + || ('task_type' in parsed && !('map' in parsed)) + ) { + addPresetTaskToQueue( + item, + parsed as unknown as TaskPreset, + host.scheduler, + ); + } else { + const plan = PlanModel.fromYaml(content, path); + addPlanTaskToQueue(item, plan, path, host.scheduler); + } + Logger.info(`已将「${selection.plan.name}」加入任务队列`); + host.renderMain(); +} + +/** 将日常任务浮窗选中的卡片直接加入调度队列。 */ +export async function loadDailyPlanToQueue( + selection: DailyPlanSelection, + host: PlanQueueHost, +): Promise { + const item: TaskGroupItem = { + dailySource: selection.plan.source, + dailyFile: selection.plan.file, + dailyTaskType: selection.plan.taskType, + kind: 'daily', + times: selection.plan.taskType === 'exercise' + ? 1 + : Math.max(1, selection.times), + label: selection.plan.name, + chapter: selection.plan.chapter, + useQuickRepair: selection.plan.taskType === 'decisive' + ? selection.useQuickRepair !== false + : undefined, + }; + const { content } = await readTaskGroupItemFile(item); + const preset = yamlCodec.parse(content); + addPresetTaskToQueue(item, preset, host.scheduler); + Logger.info(`已将日常任务「${selection.plan.name}」加入任务队列`); + host.renderMain(); +} /** 加载整个任务组到调度队列 */ export async function loadGroupToQueue( @@ -31,62 +237,19 @@ export async function loadGroupToQueue( continue; } - const content = await bridge.readFile(item.path!); - const parsed = (await import('js-yaml')).load(content) as Record; + const { content, path } = await readTaskGroupItemFile(item); + const parsed = yamlCodec.parse>(content); if (!parsed || typeof parsed !== 'object') continue; - if (item.kind === 'preset' || ('task_type' in parsed && !('chapter' in parsed))) { - host.importTaskPreset(parsed as unknown as TaskPreset, item.path!); - } else { - const plan = PlanModel.fromYaml(content, item.path!); - const resolvedPlanId = await bridge.resolveAppPath(plan.fileName); - const times = item.times; - const req: NormalFightReq | EventFightReq = { - type: plan.isEvent ? 'event_fight' : 'normal_fight', - plan_id: resolvedPlanId, - times: 1, - gap: plan.data.gap ?? 0, - }; - if (plan.data.selected_nodes.length > 0) { - req.plan = req.plan ?? {}; - req.plan.selected_nodes = normalizeSelectedNodesForBackend(plan.data.selected_nodes); - } - let selectedFleetId = item.fleet_id ?? plan.data.fleet_id; - if (item.autoFleetFallback && selectedFleetId === 1) { - selectedFleetId = 2; - } - if (selectedFleetId != null) { - if (req.type === 'event_fight') req.fleet_id = selectedFleetId; - req.plan = req.plan ?? {}; - req.plan.fleet_id = selectedFleetId; - } - if (item.fleetPresetIndex != null && plan.data.fleet_presets) { - const preset = plan.data.fleet_presets[item.fleetPresetIndex]; - if (preset) { - const resolved = resolveFleetPreset(preset.ships); - if (resolved.length > 0) { - req.plan = req.plan ?? {}; - req.plan.fleet = resolved.map(toBackendName); - req.plan.fleet_rules = resolveFleetPresetRules(preset.ships); - } - } - } - host.scheduler.addTask( - plan.mapName, - plan.isEvent ? 'event_fight' : 'normal_fight', - req, - TaskPriority.USER_TASK, - times, - plan.data.stop_condition, - undefined, - selectedFleetId, - undefined, - undefined, - !!item.forceRetry, - !!item.allowPolling, - plan.data.endpoint_nodes, - typeof plan.data.chapter === 'number' ? plan.data.chapter || undefined : undefined, + if (item.kind === 'preset' || ('task_type' in parsed && !('map' in parsed))) { + addPresetTaskToQueue( + item, + parsed as unknown as TaskPreset, + host.scheduler, ); + } else { + const plan = PlanModel.fromYaml(content, path); + addPlanTaskToQueue(item, plan, plan.fileName, host.scheduler); } loadedCount++; } catch (e) { @@ -166,61 +329,19 @@ export async function loadSingleItemToQueue( if (!bridge) return; try { - const content = await bridge.readFile(item.path!); - const parsed = (await import('js-yaml')).load(content) as Record; + const { content, path } = await readTaskGroupItemFile(item); + const parsed = yamlCodec.parse>(content); if (!parsed || typeof parsed !== 'object') return; - if (item.kind === 'preset' || ('task_type' in parsed && !('chapter' in parsed))) { - host.importTaskPreset(parsed as unknown as TaskPreset, item.path!); - } else { - const plan = PlanModel.fromYaml(content, item.path!); - const resolvedPlanId = await bridge.resolveAppPath(plan.fileName); - const req: NormalFightReq | EventFightReq = { - type: plan.isEvent ? 'event_fight' : 'normal_fight', - plan_id: resolvedPlanId, - times: 1, - gap: plan.data.gap ?? 0, - }; - if (plan.data.selected_nodes.length > 0) { - req.plan = req.plan ?? {}; - req.plan.selected_nodes = normalizeSelectedNodesForBackend(plan.data.selected_nodes); - } - let selectedFleetId = item.fleet_id ?? plan.data.fleet_id; - if (item.autoFleetFallback && selectedFleetId === 1) { - selectedFleetId = 2; - } - if (selectedFleetId != null) { - if (req.type === 'event_fight') req.fleet_id = selectedFleetId; - req.plan = req.plan ?? {}; - req.plan.fleet_id = selectedFleetId; - } - if (item.fleetPresetIndex != null && plan.data.fleet_presets) { - const preset = plan.data.fleet_presets[item.fleetPresetIndex]; - if (preset) { - const resolved = resolveFleetPreset(preset.ships); - if (resolved.length > 0) { - req.plan = req.plan ?? {}; - req.plan.fleet = resolved.map(toBackendName); - req.plan.fleet_rules = resolveFleetPresetRules(preset.ships); - } - } - } - host.scheduler.addTask( - plan.mapName, - plan.isEvent ? 'event_fight' : 'normal_fight', - req, - TaskPriority.USER_TASK, - item.times, - plan.data.stop_condition, - undefined, - selectedFleetId, - undefined, - undefined, - !!item.forceRetry, - !!item.allowPolling, - plan.data.endpoint_nodes, - typeof plan.data.chapter === 'number' ? plan.data.chapter || undefined : undefined, + if (item.kind === 'preset' || ('task_type' in parsed && !('map' in parsed))) { + addPresetTaskToQueue( + item, + parsed as unknown as TaskPreset, + host.scheduler, ); + } else { + const plan = PlanModel.fromYaml(content, path); + addPlanTaskToQueue(item, plan, plan.fileName, host.scheduler); } Logger.info(`已将「${item.label}」加入队列`); diff --git a/src/controller/template/TemplateController.ts b/src/controller/template/TemplateController.ts index 13d17ac..12342f4 100644 --- a/src/controller/template/TemplateController.ts +++ b/src/controller/template/TemplateController.ts @@ -1,3 +1,4 @@ +/** 协调模板库、模板向导和任务组添加流程。 */ /** * TemplateController —— 模板系统控制器(精简版)。 * 向导逻辑 → wizard.ts,使用模板 → useTemplate.ts, @@ -5,14 +6,16 @@ */ import { TemplateModel } from '../../model/TemplateModel'; import { TaskGroupModel } from '../../model/TaskGroupModel'; -import type { TemplateLibraryItemVO } from '../../types/view'; +import type { PlanData } from '../../types/model.js'; +import type { TemplateLibraryItemVO } from '../../types/view.js'; import { TemplateLibraryView } from '../../view/template/TemplateLibraryView'; import { TemplateWizardView } from '../../view/template/TemplateWizardView'; import { Logger } from '../../utils/Logger'; -import { showWizard, showWizardWithTemplate, wizardNav, finishWizard } from './wizard'; -import { useTemplateFlow, addPlanToTaskList, type UseTemplateCallbacks } from './useTemplate'; -import { showPlanSelector, showFleetPresetPicker, showCampaignSelector, showExerciseFleetSelector, showDecisiveChapterSelector } from './selectors'; +import { showWizard, wizardNav, finishWizard } from './wizard'; +import { useTemplateFlow, type UseTemplateCallbacks } from './useTemplate'; +import { showPlanSelector, showCampaignSelector, showExerciseFleetSelector, showDecisiveChapterSelector } from './selectors'; import { editTemplate, deleteTemplate, renameTemplate, importTemplatesFlow } from './crud'; +import { yamlCodec } from '../../adapter'; export class TemplateController { static readonly TEMPLATE_TYPE_LABELS: Record = { @@ -23,15 +26,11 @@ export class TemplateController { decisive: '决战', }; - private _wizardPlanPaths: string[] = []; - private _editingTemplateId: string | null = null; + private readonly wizardPlanPathsRef: { value: string[] } = { value: [] }; + private readonly editingIdRef: { value: string | null } = { value: null }; private libraryView: TemplateLibraryView; private wizardView: TemplateWizardView; - /** Ref-wrapper for extracted modules to mutate state */ - private get wizardPlanPathsRef() { return { value: this._wizardPlanPaths, set: (v: string[]) => { this._wizardPlanPaths = v; } }; } - private get editingIdRef() { return { value: this._editingTemplateId, set: (v: string | null) => { this._editingTemplateId = v; } }; } - constructor( private readonly templateModel: TemplateModel, private readonly taskGroupModel: TaskGroupModel, @@ -50,12 +49,12 @@ export class TemplateController { bindActions(): void { // 创建模板按钮 document.getElementById('btn-create-template')?.addEventListener('click', () => { - showWizard(this.wizardView, this.wizardPlanPathsRef as any, this.editingIdRef as any); + showWizard(this.wizardView, this.wizardPlanPathsRef, this.editingIdRef); }); // 导入模板按钮 document.getElementById('btn-import-template')?.addEventListener('click', () => { - importTemplatesFlow(this.templateModel, this.wizardView, this.wizardPlanPathsRef as any, this.appRoot, () => this.renderLibrary()); + importTemplatesFlow(this.templateModel, this.wizardView, this.wizardPlanPathsRef, this.appRoot, () => this.renderLibrary()); }); // 向导:上一步 / 下一步 / 取消 @@ -82,23 +81,28 @@ export class TemplateController { ); if (!result) return; const filePath = result.path; - if (!this._wizardPlanPaths.includes(filePath)) { - this._wizardPlanPaths.push(filePath); - this.wizardView.renderPlanList(this._wizardPlanPaths); + if (!this.wizardPlanPathsRef.value.includes(filePath)) { + this.wizardPlanPathsRef.value.push(filePath); + this.wizardView.renderPlanList(this.wizardPlanPathsRef.value); } this.wizardView.setPlanPathInput(filePath); - if (this._wizardPlanPaths.length === 1) { + if (this.wizardPlanPathsRef.value.length === 1) { try { - const parsed = (await import('js-yaml')).load(result.content) as Record; + const parsed = yamlCodec.parse>(result.content); if (!parsed || typeof parsed !== 'object') return; - if (parsed.fleet_id) { + if (typeof parsed.fleet_id === 'number') { this.wizardView.setFleetId(parsed.fleet_id); } - const presets = parsed.fleet_presets as any[] | undefined; - if (presets?.length && presets[0].ships?.length) { - this.wizardView.fillFleetGrid('nf', presets[0].ships); + const presets = Array.isArray(parsed.fleet_presets) + ? parsed.fleet_presets + : []; + const fixedShips = presets[0]?.ships.filter( + (ship): ship is string => typeof ship === 'string', + ) ?? []; + if (fixedShips.length > 0) { + this.wizardView.fillFleetGrid('nf', fixedShips); } - const sc = parsed.stop_condition as any; + const sc = parsed.stop_condition; if (sc) { if (sc.loot_count_ge != null && sc.loot_count_ge >= 0) { this.wizardView.setStopConditions(sc.loot_count_ge, undefined); @@ -111,7 +115,7 @@ export class TemplateController { if (fileName) { this.wizardView.setName(fileName); } - if (parsed.times) { + if (typeof parsed.times === 'number' && parsed.times > 0) { this.wizardView.setDefaultTimes(parsed.times); } } catch { /* YAML 解析失败不影响流程 */ } @@ -126,12 +130,14 @@ export class TemplateController { let added = 0; for (const f of files) { const fullPath = `${this.plansDir}\\${f.file}`; - if (!this._wizardPlanPaths.includes(fullPath)) { - this._wizardPlanPaths.push(fullPath); + if (!this.wizardPlanPathsRef.value.includes(fullPath)) { + this.wizardPlanPathsRef.value.push(fullPath); added++; } } - if (added > 0) this.wizardView.renderPlanList(this._wizardPlanPaths); + if (added > 0) { + this.wizardView.renderPlanList(this.wizardPlanPathsRef.value); + } Logger.info(`扫描到 ${files.length} 个方案文件,新增 ${added} 个`); }); @@ -140,9 +146,9 @@ export class TemplateController { const btn = (e.target as HTMLElement).closest('.btn-remove-plan') as HTMLElement | null; if (!btn) return; const idx = parseInt(btn.dataset.idx ?? '-1'); - if (idx >= 0 && idx < this._wizardPlanPaths.length) { - this._wizardPlanPaths.splice(idx, 1); - this.wizardView.renderPlanList(this._wizardPlanPaths); + if (idx >= 0 && idx < this.wizardPlanPathsRef.value.length) { + this.wizardPlanPathsRef.value.splice(idx, 1); + this.wizardView.renderPlanList(this.wizardPlanPathsRef.value); } }); @@ -157,7 +163,7 @@ export class TemplateController { // Library view 回调 this.libraryView.onUse = (id) => this.doUseTemplate(id); - this.libraryView.onEdit = (id) => editTemplate(id, this.templateModel, this.wizardView, this.wizardPlanPathsRef as any, this.editingIdRef as any); + this.libraryView.onEdit = (id) => editTemplate(id, this.templateModel, this.wizardView, this.wizardPlanPathsRef, this.editingIdRef); this.libraryView.onDelete = (id) => deleteTemplate(id, this.templateModel, () => this.renderLibrary()); this.libraryView.onRename = (id) => renameTemplate(id, this.templateModel, () => this.renderLibrary()); @@ -166,7 +172,8 @@ export class TemplateController { } renderLibrary(): void { - const templates = this.templateModel.getAll(); + const templates = this.templateModel.getAll() + .filter(tpl => tpl.type !== 'decisive'); const items: TemplateLibraryItemVO[] = templates.map(tpl => ({ id: tpl.id, name: tpl.name, @@ -178,14 +185,6 @@ export class TemplateController { isBuiltin: !!tpl.builtin, })); this.libraryView.render(items); - this.populateDecisiveSelect(); - } - - populateDecisiveSelect(selectedId?: string): void { - const decisiveTemplates = this.templateModel.getAll() - .filter(t => t.type === 'decisive') - .map(t => ({ id: t.id, name: t.name })); - this.libraryView.populateDecisiveSelect(decisiveTemplates, selectedId); } // ════════════════════════════════════════ @@ -195,7 +194,7 @@ export class TemplateController { private async doFinishWizard(): Promise { await finishWizard( this.wizardView, this.templateModel, - this._wizardPlanPaths, this.editingIdRef as any, + this.wizardPlanPathsRef.value, this.editingIdRef, () => this.renderLibrary(), ); } diff --git a/src/controller/template/crud.ts b/src/controller/template/crud.ts index 407a560..65fb15f 100644 --- a/src/controller/template/crud.ts +++ b/src/controller/template/crud.ts @@ -1,11 +1,69 @@ +/** 实现模板创建、编辑、删除、导入和导出用例。 */ /** * crud —— 模板 CRUD 操作。 */ import type { TemplateModel } from '../../model/TemplateModel'; import type { TemplateWizardView } from '../../view/template/TemplateWizardView'; +import type { WizardPrefillData } from '../../types/view.js'; import { Logger } from '../../utils/Logger'; import { showPrompt, showConfirm, showAlert } from '../shared/DialogHelper'; import { showWizardWithTemplate } from './wizard'; +import { jsonCodec } from '../../adapter'; + +function isTemplateRecord( + value: unknown, +): value is Record & { name: string; type: string } { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const record = value as Record; + return typeof record.name === 'string' && typeof record.type === 'string'; +} + +function stringArray(value: unknown): string[] | undefined { + if (!Array.isArray(value)) return undefined; + return value.filter((item): item is string => typeof item === 'string'); +} + +function toWizardPrefillData( + record: Record & { name: string; type: string }, +): WizardPrefillData { + const stop = record.defaultStopCondition; + const stopRecord = stop && typeof stop === 'object' && !Array.isArray(stop) + ? stop as Record + : null; + return { + type: record.type, + name: record.name, + defaultTimes: typeof record.defaultTimes === 'number' + ? record.defaultTimes + : undefined, + planPaths: stringArray(record.planPaths), + planPath: typeof record.planPath === 'string' + ? record.planPath + : undefined, + fleet_id: typeof record.fleet_id === 'number' + ? record.fleet_id + : undefined, + fleet: stringArray(record.fleet), + campaign_name: typeof record.campaign_name === 'string' + ? record.campaign_name + : undefined, + chapter: typeof record.chapter === 'number' ? record.chapter : undefined, + level1: stringArray(record.level1), + level2: stringArray(record.level2), + flagship_priority: stringArray(record.flagship_priority), + use_quick_repair: typeof record.use_quick_repair === 'boolean' + ? record.use_quick_repair + : undefined, + defaultStopCondition: stopRecord ? { + loot_count_ge: typeof stopRecord.loot_count_ge === 'number' + ? stopRecord.loot_count_ge + : undefined, + ship_count_ge: typeof stopRecord.ship_count_ge === 'number' + ? stopRecord.ship_count_ge + : undefined, + } : undefined, + }; +} /** 编辑模板 */ export function editTemplate( @@ -19,7 +77,7 @@ export function editTemplate( const tpl = templateModel.get(id); if (!tpl) return; editingTemplateId.value = id; - showWizardWithTemplate(tpl as any, wizardView, wizardPlanPaths); + showWizardWithTemplate(tpl, wizardView, wizardPlanPaths); wizardView.setTitle('编辑模板'); } @@ -73,20 +131,24 @@ export async function importTemplatesFlow( let arr: unknown[]; try { - const parsed = JSON.parse(result.content); + const parsed = jsonCodec.parse(result.content); arr = Array.isArray(parsed) ? parsed : [parsed]; } catch { showAlert('导入失败', '文件格式错误,请选择有效的 JSON 模板文件。'); return; } - const valid = arr.filter(item => item && typeof item === 'object' && (item as any).name && (item as any).type); + const valid = arr.filter(isTemplateRecord); if (valid.length === 0) { showAlert('导入失败', '未找到有效的模板数据(需包含 name 和 type 字段)。'); return; } - showWizardWithTemplate(valid[0] as Record, wizardView, wizardPlanPaths); + showWizardWithTemplate( + toWizardPrefillData(valid[0]), + wizardView, + wizardPlanPaths, + ); if (valid.length > 1) { const rest = valid.slice(1); const count = await templateModel.importFromJson(rest); diff --git a/src/controller/template/index.ts b/src/controller/template/index.ts deleted file mode 100644 index e934017..0000000 --- a/src/controller/template/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TemplateController } from './TemplateController'; diff --git a/src/controller/template/selectors.ts b/src/controller/template/selectors.ts index cfd7eb2..d2c8fc1 100644 --- a/src/controller/template/selectors.ts +++ b/src/controller/template/selectors.ts @@ -1,14 +1,19 @@ +/** 提供方案、舰队和任务参数的模板选择流程。 */ /** * selectors —— 模板使用时的选择弹窗逻辑。 */ import type { TemplateWizardView } from '../../view/template/TemplateWizardView'; import type { TaskGroupModel } from '../../model/TaskGroupModel'; -import type { TaskTemplate } from '../../types/model'; -import type { SelectorOption } from '../../types/view'; -import { shipSlotLabel, toBackendName } from '../../data/shipData'; +import type { + FleetPreset, + PlanData, + TaskTemplate, +} from '../../types/model.js'; +import type { SelectorOption } from '../../types/view.js'; +import { shipSlotLabel } from '../../model/fleet/ShipMatcher'; import { Logger } from '../../utils/Logger'; import { addPlanToTaskList } from './useTemplate'; -import { showAlert } from '../shared/DialogHelper'; +import { yamlCodec } from '../../adapter'; const CAMPAIGN_OPTIONS: string[] = [ '困难潜艇', '困难航母', '困难驱逐', '困难巡洋', '困难战列', @@ -23,65 +28,26 @@ const DECISIVE_CHAPTERS = [ const NORMAL_FLEET_IDS = [1, 2, 3, 4] as const; -function normalizeShipName(raw: unknown): string | null { - if (typeof raw !== 'string') return null; - const trimmed = raw.trim(); - if (!trimmed) return null; - return toBackendName(trimmed); -} - -/** - * 判断编队预设是否“需要改编队内舰船”。 - * 规则: - * 1) 含筛选对象槽位时,视为需要自动改编; - * 2) 与方案默认 fleet 配置逐槽位不一致时,视为需要自动改编。 - */ -function presetRequiresFleetRewrite(rawPreset: unknown, planFleet: unknown): boolean { - const ships = Array.isArray((rawPreset as { ships?: unknown[] })?.ships) - ? ((rawPreset as { ships: unknown[] }).ships) - : []; - if (ships.length === 0) return false; - - if (ships.some((slot) => typeof slot !== 'string')) { - return true; - } - - const baseFleet = Array.isArray(planFleet) ? planFleet : []; - if (baseFleet.length === 0) { - return true; - } - - if (ships.length !== baseFleet.length) { - return true; - } - - for (let i = 0; i < ships.length; i++) { - const target = normalizeShipName(ships[i]); - const current = normalizeShipName(baseFleet[i]); - if (target !== current) { - return true; - } - } - - return false; -} - async function showNormalFightFleetSelector( templateName: string, planName: string, defaultFleetId: number, wizardView: TemplateWizardView, -): Promise { - const options: SelectorOption[] = NORMAL_FLEET_IDS.map((fleetId) => ({ - icon: '⚓', - label: fleetId === defaultFleetId - ? `第 ${fleetId} 分队(默认${fleetId === 1 ? ',不支持自动改编' : ''})` - : `第 ${fleetId} 分队${fleetId === 1 ? '(不支持自动改编)' : ''}`, - })); - - const result = await wizardView.showSelector(`「${templateName} / ${planName}」— 选择使用分队(自动改编不支持第1分队)`, options); +): Promise { + const options: SelectorOption[] = [ + { icon: '📄', label: '仅使用方案,不指定编队预设' }, + ...NORMAL_FLEET_IDS.map((fleetId) => ({ + icon: '⚓', + label: fleetId === defaultFleetId + ? `第 ${fleetId} 分队(默认)` + : `第 ${fleetId} 分队`, + })), + ]; + + const result = await wizardView.showSelector(`「${templateName} / ${planName}」— 选择使用分队`, options); if (!result) return null; - return NORMAL_FLEET_IDS[result.index] ?? null; + if (result.index === 0) return undefined; + return NORMAL_FLEET_IDS[result.index - 1] ?? null; } /** 方案选择器(多方案模板) */ @@ -105,24 +71,32 @@ export async function showPlanSelector( const planPath = paths[idx]; const planName = planPath.split(/[\\/]/).pop()?.replace(/\.ya?ml$/i, '') ?? planPath; - let parsedPlan: Record | undefined; + let parsedPlan: Partial | undefined; const bridge = window.electronBridge; if (bridge) { try { const content = await bridge.readFile(planPath); - parsedPlan = (await import('js-yaml')).load(content) as Record; - const rawPresets = Array.isArray(parsedPlan?.fleet_presets) ? parsedPlan.fleet_presets as any[] : []; + parsedPlan = yamlCodec.parse>(content); + const rawPresets = Array.isArray(parsedPlan?.fleet_presets) + ? parsedPlan.fleet_presets + : []; if (rawPresets.length > 0) { let defaultFleetId = tpl.fleet_id ?? 1; const rawFleetId = Number(parsedPlan?.fleet_id); if (Number.isFinite(rawFleetId) && rawFleetId >= 1 && rawFleetId <= 4) { defaultFleetId = rawFleetId; } - const planFleet = Array.isArray(parsedPlan?.fleet) ? parsedPlan.fleet : []; const selectedFleetId = await showNormalFightFleetSelector(tpl.name, planName, defaultFleetId, wizardView); - if (selectedFleetId == null) return; + if (selectedFleetId === null) return; + if (selectedFleetId === undefined) { + addPlanToTaskList(tpl, planPath, groupName, taskGroupModel); + taskGroupModel.save(); + renderTaskGroup(); + Logger.info(`模板「${tpl.name}」→ 已加入任务列表「${groupName}」(方案: ${planName})`); + return; + } await showFleetPresetPicker( tpl, @@ -133,7 +107,6 @@ export async function showPlanSelector( taskGroupModel, renderTaskGroup, selectedFleetId, - planFleet, ); return; } @@ -150,19 +123,18 @@ export async function showPlanSelector( export async function showFleetPresetPicker( tpl: TaskTemplate, planPath: string, - rawPresets: any[], + rawPresets: FleetPreset[], groupName: string, wizardView: TemplateWizardView, taskGroupModel: TaskGroupModel, renderTaskGroup: () => void, selectedFleetId: number, - planFleet: unknown[] = [], ): Promise { const planName = planPath.split(/[\\/]/).pop()?.replace(/\.ya?ml$/i, '') ?? ''; - const options: SelectorOption[] = rawPresets.map((p: any, i: number) => { + const options: SelectorOption[] = rawPresets.map((p, i) => { const pName = p.name ?? `预设${i + 1}`; const ships = Array.isArray(p.ships) ? p.ships : []; - const shipsHtml = ships.map((s: any) => { + const shipsHtml = ships.map((s) => { if (typeof s === 'string') return `${s}`; const label = shipSlotLabel(s); return `${label}`; @@ -173,18 +145,9 @@ export async function showFleetPresetPicker( const indices = await wizardView.showMultiSelector(`「${planName}」— 选择编队(可多选)`, options); if (indices.length === 0) return; - const fallbackPresetNames: string[] = []; for (const idx of indices) { const preset = rawPresets[idx]; const presetName = preset?.name ?? ''; - const needRewrite = presetRequiresFleetRewrite(preset, planFleet); - const autoFleetFallback = selectedFleetId === 1 && needRewrite; - const effectiveFleetId = autoFleetFallback ? 2 : selectedFleetId; - - if (autoFleetFallback) { - fallbackPresetNames.push(presetName || `预设${idx + 1}`); - } - addPlanToTaskList( tpl, planPath, @@ -192,25 +155,13 @@ export async function showFleetPresetPicker( taskGroupModel, idx, presetName, - effectiveFleetId, - autoFleetFallback, + selectedFleetId, ); } taskGroupModel.save(); renderTaskGroup(); const names = indices.map(i => rawPresets[i]?.name ?? '').join(', '); - if (fallbackPresetNames.length > 0) { - const fallbackText = fallbackPresetNames.join('、'); - Logger.warn(`模板「${tpl.name}」中的编队预设「${fallbackText}」需要自动改编,第1分队不支持,已自动改用第2分队并写入任务配置`); - await showAlert( - '分队已自动调整', - `以下预设需要自动改编,后端不支持对第1分队自动编队,已自动改为第2分队并保存到任务组配置:\n${fallbackText}`, - ); - } - const fallbackSuffix = fallbackPresetNames.length > 0 - ? `, 自动调整${fallbackPresetNames.length}项到第2分队` - : ''; - Logger.info(`模板「${tpl.name}」→ 已加入任务列表「${groupName}」(方案: ${planName}, 分队: ${selectedFleetId}${fallbackSuffix}, 编队预设: ${names})`); + Logger.info(`模板「${tpl.name}」→ 已加入任务列表「${groupName}」(方案: ${planName}, 分队: ${selectedFleetId}, 编队预设: ${names})`); } /** 战役类型选择器 */ diff --git a/src/controller/template/useTemplate.ts b/src/controller/template/useTemplate.ts index 92abd5e..15fee6d 100644 --- a/src/controller/template/useTemplate.ts +++ b/src/controller/template/useTemplate.ts @@ -1,9 +1,10 @@ +/** 把模板参数实例化为可加入任务组的任务条目。 */ /** * useTemplate —— 模板应用到任务列表的逻辑。 */ import type { TemplateModel } from '../../model/TemplateModel'; import type { TaskGroupModel } from '../../model/TaskGroupModel'; -import type { TaskTemplate } from '../../types/model'; +import type { TaskTemplate } from '../../types/model.js'; import { Logger } from '../../utils/Logger'; export interface UseTemplateCallbacks { @@ -72,7 +73,6 @@ export function addPlanToTaskList( fleetPresetIndex?: number, presetName?: string, fleetId?: number, - autoFleetFallback?: boolean, ): void { const planName = planPath.split(/[\\/]/).pop()?.replace(/\.ya?ml$/i, '') ?? tpl.name; const label = presetName @@ -85,7 +85,6 @@ export function addPlanToTaskList( label, fleetPresetIndex, fleet_id: fleetId, - autoFleetFallback, forceRetry: tpl.forceRetry, allowPolling: tpl.allowPolling, }); diff --git a/src/controller/template/wizard.ts b/src/controller/template/wizard.ts index 52692e5..b4b6788 100644 --- a/src/controller/template/wizard.ts +++ b/src/controller/template/wizard.ts @@ -1,10 +1,13 @@ +/** 维护模板向导步骤、预填数据和表单提交。 */ /** * wizard —— 模板向导显示/导航/完成逻辑。 */ import type { TemplateWizardView } from '../../view/template/TemplateWizardView'; import type { TemplateModel } from '../../model/TemplateModel'; -import type { TaskTemplate } from '../../types/model'; +import type { TaskTemplate } from '../../types/model.js'; +import type { WizardPrefillData } from '../../types/view.js'; import { Logger } from '../../utils/Logger'; +import { showAlert, showSaveSuccess } from '../shared/DialogHelper'; /** 打开创建模板向导 */ export function showWizard( @@ -19,7 +22,7 @@ export function showWizard( /** 用已有模板数据预填向导(导入/编辑) */ export function showWizardWithTemplate( - tpl: Record, + tpl: WizardPrefillData, wizardView: TemplateWizardView, wizardPlanPaths: { value: string[] }, ): void { @@ -30,7 +33,7 @@ export function showWizardWithTemplate( if (planPaths.length > 0) { wizardView.renderPlanList(planPaths); } - wizardView.prefill(tpl as any); + wizardView.prefill(tpl); wizardView.setStep(2); } @@ -110,14 +113,27 @@ export async function finishWizard( break; } - if (editingTemplateId.value) { - await templateModel.update(editingTemplateId.value, partial); - Logger.info(`模板「${name}」已更新`); - editingTemplateId.value = null; - } else { - await templateModel.add(partial); - Logger.info(`模板「${name}」已创建`); + try { + if (editingTemplateId.value) { + const updated = await templateModel.update( + editingTemplateId.value, + partial, + ); + if (!updated) throw new Error('模板不存在或不可编辑'); + Logger.info(`模板「${name}」已更新`); + editingTemplateId.value = null; + } else { + await templateModel.add(partial); + Logger.info(`模板「${name}」已创建`); + } + } catch (error) { + await showAlert( + '保存失败', + error instanceof Error ? error.message : String(error), + ); + return; } wizardView.hide(); renderLibrary(); + showSaveSuccess(`模板「${name}」保存成功`); } diff --git a/src/data/shipData.ts b/src/data/shipData.ts deleted file mode 100644 index 5a7b879..0000000 --- a/src/data/shipData.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * 舰船数据 — 从 yltx/asgrgui 项目 resource/ship_details.json 导入 - * 仅保留 name / nation / ship_type 用于自动补全 - */ - -export interface ShipInfo { - name: string; - nation: string; - ship_type: string; -} - -// ship_type 代号 → 中文 -export const TYPE_LABELS: Record = { - bb: '战列', bbv: '航战', bbg: '导战', - bc: '战巡', cbg: '大巡', - cv: '航母', cvl: '轻母', av: '装母', - ca: '重巡', cav: '航巡', - cl: '轻巡', clt: '雷巡', cf: '旗舰', - dd: '驱逐', ddg: '导驱', ddgaa: '防驱', - ss: '潜艇', sc: '炮潜', ssg: '导潜', - ss_or_ssg: '潜艇/导潜', - bm: '重炮', ap: '补给', cg: '导巡', cgaa: '防巡', -}; - -export function shipTypeLabel(code: string): string { - return TYPE_LABELS[code] || code; -} - -import rawShips from './ship_details.json'; - -/** 全部舰船(包含改造版本) */ -export const ALL_SHIPS: ShipInfo[] = (rawShips as any[]).map(s => ({ - name: s.name as string, - nation: s.nation as string, - ship_type: s.ship_type as string, -})); - -/** 所有出现过的国籍(去重,常用国家在前) */ -export const ALL_NATIONS: string[] = (() => { - const priority = ['中国', '日本', '德国', '美国', '英国', '苏联', '法国', '意大利']; - const all = [...new Set(ALL_SHIPS.map(s => s.nation))]; - return [...priority.filter(n => all.includes(n)), ...all.filter(n => !priority.includes(n))]; -})(); - -/** - * 将前端显示名转换为后端 API 使用的名称。 - * 规则: - * 1) 去掉 "·改" 后缀(改造型在游戏内与原型同名) - * 2) 去掉尾部括号限定说明(如“(岛风型驱逐舰)”) - * 例: "飞龙·改" → "飞龙", "岛风(岛风型驱逐舰)·改" → "岛风" - */ -export function toBackendName(displayName: string): string { - const noRefit = displayName.endsWith('·改') ? displayName.slice(0, -2) : displayName; - return noRefit.replace(/\s*[((][^()()]*[))]\s*$/, '').trim(); -} - -// ════════════════════════════════════════ -// 模糊匹配 (ShipFilter) -// ════════════════════════════════════════ - -import type { FleetRuleReq } from '../types/api'; -import type { ShipFilter, ShipSlot } from '../types/model'; - -/** 将 ShipFilter 转为显示标签, 如 "德国 驱逐" */ -export function shipFilterLabel(filter: ShipFilter): string { - const parts: string[] = []; - if (filter.name) { - parts.push(filter.name); - } - if (filter.nation) parts.push(filter.nation); - if (filter.ship_type) parts.push(shipTypeLabel(filter.ship_type)); - if (filter.priority && filter.priority.length > 0) { - parts.push(`优先:${filter.priority.join(' > ')}`); - } - if (filter.min_level != null || filter.max_level != null) { - if (filter.min_level != null && filter.max_level != null) { - parts.push(`Lv${filter.min_level}-${filter.max_level}`); - } else if (filter.min_level != null) { - parts.push(`Lv>=${filter.min_level}`); - } else if (filter.max_level != null) { - parts.push(`Lv<=${filter.max_level}`); - } - } - return parts.join(' ') || '任意舰船'; -} - -/** 判断 ShipSlot 是否为 ShipFilter 对象 */ -export function isShipFilter(slot: ShipSlot): slot is ShipFilter { - return typeof slot === 'object' && slot !== null; -} - -/** 将 ShipSlot 转为显示文本 */ -export function shipSlotLabel(slot: ShipSlot): string { - return isShipFilter(slot) ? shipFilterLabel(slot) : slot; -} - -function dedupeNames(names: string[]): string[] { - const seen = new Set(); - const out: string[] = []; - for (const name of names) { - if (!name || seen.has(name)) continue; - seen.add(name); - out.push(name); - } - return out; -} - -function matchShipType(filter: ShipFilter, shipType: string): boolean { - if (!filter.ship_type) return true; - if (filter.ship_type === 'ss_or_ssg') return shipType === 'ss' || shipType === 'ssg'; - return shipType === filter.ship_type; -} - -function matchesFilter(filter: ShipFilter, ship: ShipInfo): boolean { - if (filter.nation && ship.nation !== filter.nation) return false; - if (!matchShipType(filter, ship.ship_type)) return false; - if (filter.name) { - if (toBackendName(ship.name) !== toBackendName(filter.name)) { - return false; - } - } - return true; -} - -function sortByPriority(candidates: string[], filter: ShipFilter): string[] { - if (!filter.priority || filter.priority.length === 0) return candidates; - const priorityNames = dedupeNames(filter.priority.map(toBackendName)); - const candidateSet = new Set(candidates); - const front = priorityNames.filter(name => candidateSet.has(name)); - const frontSet = new Set(front); - return [...front, ...candidates.filter(name => !frontSet.has(name))]; -} - -function buildShipCandidates(filter: ShipFilter, exclude: string[]): string[] { - const excludeSet = new Set(exclude.map(toBackendName)); - const matched = ALL_SHIPS.filter(ship => matchesFilter(filter, ship)); - const refit = matched - .filter(ship => ship.name.endsWith('·改')) - .map(ship => toBackendName(ship.name)) - .filter(name => !excludeSet.has(name)); - const normal = matched - .filter(ship => !ship.name.endsWith('·改')) - .map(ship => toBackendName(ship.name)) - .filter(name => !excludeSet.has(name)); - - const candidates = dedupeNames([...refit, ...normal]); - - // 如果是固定舰名规则且数据集中未命中,仍保留该名称用于后端精确尝试。 - if (candidates.length === 0 && filter.name) { - const normalized = toBackendName(filter.name); - if (!excludeSet.has(normalized)) { - candidates.push(normalized); - } - } - - return sortByPriority(candidates, filter); -} - -/** - * 解析单个模糊筛选条件,返回匹配的舰船名称。 - * 默认优先选择改造版本 (·改),可通过 priority 字段调整优先顺序。 - */ -export function resolveShipFilter(filter: ShipFilter, exclude: string[]): string | null { - const candidates = buildShipCandidates(filter, exclude); - return candidates.length > 0 ? candidates[0] : null; -} - -/** - * 解析编队预设中的所有槽位,将 ShipFilter 解析为具体舰船名。 - * 返回纯字符串数组,可直接传给后端。 - */ -export function resolveFleetPreset(ships: ShipSlot[]): string[] { - const resolved: string[] = []; - for (const slot of ships) { - if (typeof slot === 'string') { - resolved.push(toBackendName(slot)); - } else { - const name = resolveShipFilter(slot, resolved); - if (name) resolved.push(name); - } - } - return resolved; -} - -/** - * 解析编队预设槽位为后端规则。 - * - 字符串槽位: 直接使用具体舰名 - * - 模糊槽位: 生成候选列表(等级范围暂不下发) - */ -export function resolveFleetPresetRules(ships: ShipSlot[]): Array { - const rules: Array = []; - const reserved: string[] = []; - - for (const slot of ships) { - if (typeof slot === 'string') { - const name = toBackendName(slot); - rules.push(name); - reserved.push(name); - continue; - } - - const candidates = buildShipCandidates(slot, reserved); - if (candidates.length === 0) continue; - - const rule: FleetRuleReq = { candidates }; - if (slot.name) { - // 保留原始舰名作为搜索关键词,避免同名异型(如大淀)被归一化后无法区分。 - const searchName = String(slot.name).trim(); - if (searchName) rule.search_name = searchName; - } - if (slot.ship_type) { - rule.ship_type = String(slot.ship_type).trim(); - } - if (slot.min_level != null && Number.isFinite(slot.min_level)) { - rule.min_level = Math.max(1, Math.floor(slot.min_level)); - } - if (slot.max_level != null && Number.isFinite(slot.max_level)) { - rule.max_level = Math.max(1, Math.floor(slot.max_level)); - } - rules.push(rule); - - // 预留首选项,尽量避免后续槽位重复选中同名舰船。 - reserved.push(candidates[0]); - } - - return rules; -} diff --git a/src/data/ship_details.json b/src/data/ship_details.json index 3076a73..646c30e 100644 --- a/src/data/ship_details.json +++ b/src/data/ship_details.json @@ -2397,7 +2397,7 @@ "id": 343, "name": "蔚山", "nation": "韩国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -2537,7 +2537,7 @@ "id": 363, "name": "炽热", "nation": "苏联", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -2803,7 +2803,7 @@ "id": 401, "name": "胆大", "nation": "苏联", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -3251,7 +3251,7 @@ "id": 467, "name": "鞍山", "nation": "中国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -3496,7 +3496,7 @@ "id": 502, "name": "济南", "nation": "中国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -3713,7 +3713,7 @@ "id": 533, "name": "格罗兹尼", "nation": "苏联", - "ship_type": "cg", + "ship_type": "kp", "is_retrofit": false }, { @@ -4049,7 +4049,7 @@ "id": 581, "name": "塞瓦斯托波尔(导弹巡洋舰)", "nation": "苏联", - "ship_type": "cg", + "ship_type": "kp", "is_retrofit": false }, { @@ -4154,7 +4154,7 @@ "id": 596, "name": "九江", "nation": "中国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -4301,7 +4301,7 @@ "id": 617, "name": "南京", "nation": "中国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -4420,14 +4420,14 @@ "id": 634, "name": "自豪", "nation": "苏联", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { "id": 636, "name": "活泼", "nation": "苏联", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": false }, { @@ -4567,14 +4567,14 @@ "id": 1020, "name": "阿拉斯加·改", "nation": "美国", - "ship_type": "cbg", + "ship_type": "bg", "is_retrofit": true }, { "id": 1021, "name": "关岛·改", "nation": "美国", - "ship_type": "cbg", + "ship_type": "bg", "is_retrofit": true }, { @@ -5050,7 +5050,7 @@ "id": 1097, "name": "长春", "nation": "中国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": true }, { @@ -5855,7 +5855,7 @@ "id": 1343, "name": "蔚山·改", "nation": "韩国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": true }, { @@ -5911,7 +5911,7 @@ "id": 1400, "name": "七省联盟·改", "nation": "荷兰", - "ship_type": "cgaa", + "ship_type": "cg", "is_retrofit": true }, { @@ -5925,7 +5925,7 @@ "id": 1413, "name": "加里波第·改", "nation": "意大利", - "ship_type": "cgaa", + "ship_type": "cg", "is_retrofit": true }, { @@ -5960,7 +5960,7 @@ "id": 1430, "name": "捷尔任斯基·改", "nation": "苏联", - "ship_type": "cgaa", + "ship_type": "cg", "is_retrofit": true }, { @@ -5981,7 +5981,7 @@ "id": 1467, "name": "鞍山·改", "nation": "中国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": true }, { @@ -6051,7 +6051,7 @@ "id": 1528, "name": "芝加哥(CA-136)·改", "nation": "美国", - "ship_type": "cgaa", + "ship_type": "cg", "is_retrofit": true }, { @@ -6107,7 +6107,7 @@ "id": 1606, "name": "汉堡·改", "nation": "德国", - "ship_type": "ddg", + "ship_type": "asdg", "is_retrofit": true }, { @@ -6124,4 +6124,4 @@ "ship_type": "bbv", "is_retrofit": false } -] \ No newline at end of file +] diff --git a/src/model/ApiClient.ts b/src/model/ApiClient.ts index a9fb81b..3c092c1 100644 --- a/src/model/ApiClient.ts +++ b/src/model/ApiClient.ts @@ -1,3 +1,4 @@ +/** 提供后端业务 API、任务控制和 WebSocket 事件客户端。 */ /** * ApiClient —— 与 AutoWSGR 后端 HTTP Server 通信的服务层。 * 封装所有 REST 调用和 WebSocket 连接管理。 @@ -7,6 +8,13 @@ const DEFAULT_BASE_URL = 'http://localhost:8438'; const WS_RECONNECT_DELAY = 3000; import { Logger } from '../utils/Logger'; +import { + createHttpTransport, + type HttpTransport, + webSocketTransport, + type WebSocketTransport, + jsonCodec, +} from '../adapter/index.js'; import type { ApiResponse, ApiClientCallbacks, @@ -20,7 +28,7 @@ import type { WsLogMessage, WsTaskUpdate, WsTaskCompleted, -} from '../types/api'; +} from '../types/api.js'; // ════════════════════════════════════════ // ApiClient 实现 @@ -30,11 +38,20 @@ export class ApiClient { private baseUrl: string; private wsLog: WebSocket | null = null; private wsTask: WebSocket | null = null; + private shouldReconnectWebSockets = false; private callbacks: ApiClientCallbacks = {}; private reconnectTimers: { log?: ReturnType; task?: ReturnType } = {}; - - constructor(baseUrl: string = DEFAULT_BASE_URL) { + private readonly http: HttpTransport; + private readonly ws: WebSocketTransport; + + constructor( + baseUrl: string = DEFAULT_BASE_URL, + http: HttpTransport = createHttpTransport(baseUrl), + ws: WebSocketTransport = webSocketTransport, + ) { this.baseUrl = baseUrl; + this.http = http; + this.ws = ws; } setCallbacks(cb: ApiClientCallbacks): void { @@ -44,20 +61,8 @@ export class ApiClient { // ── HTTP 方法 ── private async request(method: string, path: string, body?: unknown, timeoutMs?: number): Promise> { - const url = `${this.baseUrl}${path}`; - Logger.debug(`HTTP ${method} ${path}${body ? ' body=' + JSON.stringify(body) : ''}`, 'api'); - const init: RequestInit = { - method, - headers: body ? { 'Content-Type': 'application/json' } : undefined, - body: body ? JSON.stringify(body) : undefined, - }; - if (timeoutMs) { - const ac = new AbortController(); - init.signal = ac.signal; - setTimeout(() => ac.abort(), timeoutMs); - } - const resp = await fetch(url, init); - return resp.json() as Promise>; + Logger.debug(`HTTP ${method} ${path}${body ? ' body=' + jsonCodec.stringify(body) : ''}`, 'api'); + return this.http.request>(method, path, body, timeoutMs); } // ── 系统管理 ── @@ -107,13 +112,13 @@ export class ApiClient { } private static makeLegacyTaskRequest(req: TaskRequest): TaskRequest { - const cloned = JSON.parse(JSON.stringify(req)) as TaskRequest & { + const cloned = jsonCodec.parse; }; - }; + }>(jsonCodec.stringify(req)); const plan = cloned.plan; if (!plan) return cloned; @@ -203,7 +208,9 @@ export class ApiClient { } /** 单船泡澡修理(后端接受舰船名称,自动导航到浴室并修理) */ - async repairShip(shipName: string): Promise { + async repairShip( + shipName: string, + ): Promise> { return this.request('POST', '/api/repair/ship', { ship_name: shipName }); } @@ -220,13 +227,16 @@ export class ApiClient { // ── WebSocket ── connectWebSockets(): void { + this.shouldReconnectWebSockets = true; this.connectLogWs(); this.connectTaskWs(); } disconnectWebSockets(): void { + this.shouldReconnectWebSockets = false; clearTimeout(this.reconnectTimers.log); clearTimeout(this.reconnectTimers.task); + this.reconnectTimers = {}; this.wsLog?.close(); this.wsTask?.close(); this.wsLog = null; @@ -238,68 +248,76 @@ export class ApiClient { } private connectLogWs(): void { - if (this.wsLog?.readyState === WebSocket.OPEN) return; + if (!this.shouldReconnectWebSockets) return; + if (this.wsLog && ( + this.wsLog.readyState === WebSocket.OPEN + || this.wsLog.readyState === WebSocket.CONNECTING + )) return; try { - this.wsLog = new WebSocket(`${this.wsBaseUrl()}/ws/logs`); - - this.wsLog.onmessage = (ev) => { - try { - const msg = JSON.parse(ev.data) as WsMessage; - if (msg.type === 'log' && this.callbacks.onLog) { - this.callbacks.onLog(msg as WsLogMessage); + this.wsLog = this.ws.connect(`${this.wsBaseUrl()}/ws/logs`, { + onMessage: data => { + try { + const msg = jsonCodec.parse(data); + if (msg.type === 'log' && this.callbacks.onLog) this.callbacks.onLog(msg as WsLogMessage); + } catch { + Logger.debug('WS /logs: malformed message', 'api'); } - } catch { - Logger.debug('WS /logs: malformed message', 'api'); - } - }; - - this.wsLog.onopen = () => { - Logger.debug('WS /logs connected', 'api'); - this.callbacks.onWsStatusChange?.(true); - }; + }, + onOpen: () => { + Logger.debug('WS /logs connected', 'api'); + this.callbacks.onWsStatusChange?.(true); + }, + onClose: () => { + this.callbacks.onWsStatusChange?.(false); + if (!this.shouldReconnectWebSockets) return; + Logger.debug('WS /logs disconnected, reconnect in 3s', 'api'); + this.reconnectTimers.log = setTimeout(() => this.connectLogWs(), WS_RECONNECT_DELAY); + }, + onError: () => this.wsLog?.close(), + }); - this.wsLog.onclose = () => { - Logger.debug('WS /logs disconnected, reconnect in 3s', 'api'); - this.callbacks.onWsStatusChange?.(false); - this.reconnectTimers.log = setTimeout(() => this.connectLogWs(), WS_RECONNECT_DELAY); - }; - - this.wsLog.onerror = () => { - this.wsLog?.close(); - }; } catch { - this.reconnectTimers.log = setTimeout(() => this.connectLogWs(), WS_RECONNECT_DELAY); + if (this.shouldReconnectWebSockets) { + this.reconnectTimers.log = setTimeout( + () => this.connectLogWs(), + WS_RECONNECT_DELAY, + ); + } } } private connectTaskWs(): void { - if (this.wsTask?.readyState === WebSocket.OPEN) return; + if (!this.shouldReconnectWebSockets) return; + if (this.wsTask && ( + this.wsTask.readyState === WebSocket.OPEN + || this.wsTask.readyState === WebSocket.CONNECTING + )) return; try { - this.wsTask = new WebSocket(`${this.wsBaseUrl()}/ws/task`); - - this.wsTask.onmessage = (ev) => { - try { - const msg = JSON.parse(ev.data) as WsMessage; - if (msg.type === 'task_update' && this.callbacks.onTaskUpdate) { - this.callbacks.onTaskUpdate(msg as WsTaskUpdate); - } else if (msg.type === 'task_completed' && this.callbacks.onTaskCompleted) { - this.callbacks.onTaskCompleted(msg as WsTaskCompleted); + this.wsTask = this.ws.connect(`${this.wsBaseUrl()}/ws/task`, { + onMessage: data => { + try { + const msg = jsonCodec.parse(data); + if (msg.type === 'task_update' && this.callbacks.onTaskUpdate) this.callbacks.onTaskUpdate(msg as WsTaskUpdate); + else if (msg.type === 'task_completed' && this.callbacks.onTaskCompleted) this.callbacks.onTaskCompleted(msg as WsTaskCompleted); + } catch { + Logger.debug('WS /task: malformed message', 'api'); } - } catch { - Logger.debug('WS /task: malformed message', 'api'); - } - }; + }, + onClose: () => { + if (!this.shouldReconnectWebSockets) return; + Logger.debug('WS /task disconnected, reconnect in 3s', 'api'); + this.reconnectTimers.task = setTimeout(() => this.connectTaskWs(), WS_RECONNECT_DELAY); + }, + onError: () => this.wsTask?.close(), + }); - this.wsTask.onclose = () => { - Logger.debug('WS /task disconnected, reconnect in 3s', 'api'); - this.reconnectTimers.task = setTimeout(() => this.connectTaskWs(), WS_RECONNECT_DELAY); - }; - - this.wsTask.onerror = () => { - this.wsTask?.close(); - }; } catch { - this.reconnectTimers.task = setTimeout(() => this.connectTaskWs(), WS_RECONNECT_DELAY); + if (this.shouldReconnectWebSockets) { + this.reconnectTimers.task = setTimeout( + () => this.connectTaskWs(), + WS_RECONNECT_DELAY, + ); + } } } } diff --git a/src/model/ConfigModel.ts b/src/model/ConfigModel.ts index cbc3a27..865a443 100644 --- a/src/model/ConfigModel.ts +++ b/src/model/ConfigModel.ts @@ -1,9 +1,31 @@ +/** 持有用户配置状态并负责默认值、迁移和 YAML 转换。 */ /** * ConfigModel —— 用户配置(UserSettings)的 Model 层。 * 负责从 YAML 加载、导出配置,以及局部更新。 */ -import * as yaml from 'js-yaml'; -import type { UserSettings } from '../types/model'; +import { yamlCodec } from '../adapter/index.js'; +import type { + GuiAutomationSettings, + NormalFightTaskConfig, + UserSettings, +} from '../types/model.js'; +import { + DEFAULT_LOOT_PLAN_ID, + DEFAULT_LOOT_PLANS, + LEGACY_LOOT_PLAN_IDS, + findLootAutomationPlan, + lootPlanIdFromIndex, + migrateLootPlanId, + normalizeLootAutomationPlans, +} from '../shared/lootPlans.js'; +import type { + LegacyDecisiveAutomationSettings, + LegacyDecisiveYamlKey, +} from '../shared/legacyDecisiveAutomation.js'; +import { + SYSTEM_DECISIVE_PRESET_ID, + normalizeDecisiveAutomationSource, +} from '../shared/decisiveAutomation.js'; import { Logger } from '../utils/Logger'; const DEFAULT_SETTINGS: UserSettings = { @@ -13,29 +35,78 @@ const DEFAULT_SETTINGS: UserSettings = { account: { game_app: '官服', }, + ocr: { + gpu: false, + mirror: 'modelscope', + ship_name_match_confidence: 0.65, + ship_name_corrections: {}, + ship_name_aliases: {}, + }, + log: { + level: 'INFO', + root: 'log', + }, daily_automation: { - auto_expedition: true, - expedition_interval: 15, + auto_expedition: false, + auto_gain_bonus: false, + auto_bath_repair: false, + auto_set_support: false, + bath_repair_blacklist: [], auto_battle: false, battle_type: '困难潜艇', - battle_times: 3, auto_exercise: false, - exercise_fleet_id: 1, + exercise_fleet_id: null, auto_normal_fight: false, - auto_decisive: false, - decisive_ticket_reserve: 0, - decisive_template_id: '', - auto_loot: false, - loot_plan_index: 0, - loot_stop_count: 50, + normal_fight_tasks: [], + quick_repair_limit: null, + stop_max_ship: false, + stop_max_loot: false, }, + operation_delay_min: 0, + operation_delay_max: 0, + dock_full_destroy: false, + repair_manually: false, + bathroom_count: 2, + destroy_ship_work_mode: 0, + destroy_ship_types: [], + remove_equipment_mode: true, +}; + +const DEFAULT_GUI_AUTOMATION: GuiAutomationSettings = { + expeditionInterval: 15, + battleTimes: 3, + autoDecisive: false, + decisiveTemplateId: SYSTEM_DECISIVE_PRESET_ID, + autoLoot: false, + lootPlanSource: 'system', + lootPlanId: DEFAULT_LOOT_PLAN_ID, + lootPlans: DEFAULT_LOOT_PLANS.map(plan => ({ ...plan })), + lootStopCount: 50, }; +const LEGACY_DAILY_KEYS = [ + 'expedition_interval', + 'battle_times', + 'auto_loot', + 'loot_plan_id', + 'loot_plan_index', + 'loot_stop_count', +] as const; + export class ConfigModel { private settings: UserSettings; + private guiAutomation: GuiAutomationSettings; + private legacyGuiAutomation: Partial = {}; + private legacyDecisiveAutomation: + LegacyDecisiveAutomationSettings = {}; + private invalidLegacyDecisiveFields: + LegacyDecisiveYamlKey[] = []; + /** 原始 YAML 根对象,用于保留 GUI 尚未建模的后端配置 */ + private rawRoot: Record = {}; constructor() { this.settings = structuredClone(DEFAULT_SETTINGS); + this.guiAutomation = structuredClone(DEFAULT_GUI_AUTOMATION); } /** 当前配置 (只读引用) */ @@ -43,37 +114,209 @@ export class ConfigModel { return this.settings; } + /** GUI 自身定时调度配置,不参与 usersettings.yaml 序列化。 */ + get currentGuiAutomation(): GuiAutomationSettings { + return this.guiAutomation; + } + + /** 旧版 usersettings.yaml 中可迁移的 GUI 私有字段。 */ + get migratedGuiAutomation(): Partial { + return structuredClone(this.legacyGuiAutomation); + } + + /** 等待迁入 gui_settings.json 的旧版决战自动化原值。 */ + get migratedLegacyDecisiveAutomation(): + LegacyDecisiveAutomationSettings { + return structuredClone(this.legacyDecisiveAutomation); + } + + /** 格式无法识别且必须继续留在 YAML 中的旧版决战字段。 */ + get unmigratedLegacyDecisiveFields(): + readonly LegacyDecisiveYamlKey[] { + return [...this.invalidLegacyDecisiveFields]; + } + /** 从 YAML 字符串加载配置,缺失字段保留默认值 */ loadFromYaml(yamlStr: string): void { - const parsed = yaml.load(yamlStr) as Record | null; - if (!parsed || typeof parsed !== 'object') { + const parsed = yamlCodec.parse | null>(yamlStr); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { Logger.debug('配置 YAML 解析结果为空,使用默认值'); return; } + this.rawRoot = structuredClone(parsed); const base = structuredClone(DEFAULT_SETTINGS); - if (parsed.emulator && typeof parsed.emulator === 'object') { - Object.assign(base.emulator, parsed.emulator); + const emulator = this.asRecord(parsed.emulator); + if (emulator) { + if (typeof emulator.type === 'string') base.emulator.type = emulator.type; + if (typeof emulator.path === 'string') base.emulator.path = emulator.path; + if (typeof emulator.serial === 'string') base.emulator.serial = emulator.serial; + if (typeof emulator.process_name === 'string') { + base.emulator.process_name = emulator.process_name; + } + } + + const account = this.asRecord(parsed.account); + if (account && typeof account.game_app === 'string') { + base.account.game_app = account.game_app; } - if (parsed.account && typeof parsed.account === 'object') { - Object.assign(base.account, parsed.account); + + const ocr = this.asRecord(parsed.ocr); + if (ocr) { + if (typeof ocr.gpu === 'boolean') base.ocr.gpu = ocr.gpu; + if (['origin', 'github', 'tencent', 'modelscope'].includes(String(ocr.mirror))) { + base.ocr.mirror = String(ocr.mirror) as UserSettings['ocr']['mirror']; + } + base.ocr.ship_name_match_confidence = this.clampNumber( + ocr.ship_name_match_confidence, + 0, + 1, + base.ocr.ship_name_match_confidence, + ); + base.ocr.ship_name_corrections = this.stringMap( + ocr.ship_name_corrections, + ); + base.ocr.ship_name_aliases = this.stringMap(ocr.ship_name_aliases); } - if (parsed.daily_automation && typeof parsed.daily_automation === 'object') { - Object.assign(base.daily_automation, parsed.daily_automation); - // 确保 expedition_interval 在合理范围 - const ei = base.daily_automation.expedition_interval; - if (typeof ei !== 'number' || ei < 1 || ei > 120) { - base.daily_automation.expedition_interval = 15; + + const log = this.asRecord(parsed.log); + if (log) { + const levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']; + if (levels.includes(String(log.level))) { + base.log.level = String(log.level) as UserSettings['log']['level']; + } + if (typeof log.root === 'string') base.log.root = log.root; + if (typeof log.dir === 'string' || log.dir === null) base.log.dir = log.dir; + for (const key of [ + 'show_decisive_battle_info', + 'show_emulator_debug', + 'show_ui_debug', + 'show_vision_debug', + 'show_ops_debug', + 'show_combat_state_debug', + 'show_combat_recognition_debug', + ] as const) { + if (typeof log[key] === 'boolean') base.log[key] = log[key]; + } + if (this.asRecord(log.channels)) { + base.log.channels = this.stringMap(log.channels); } } + const daily = this.asRecord(parsed.daily_automation); + if (daily) { + for (const key of [ + 'auto_expedition', + 'auto_gain_bonus', + 'auto_bath_repair', + 'auto_set_support', + 'auto_battle', + 'auto_exercise', + 'auto_normal_fight', + 'stop_max_ship', + 'stop_max_loot', + ] as const) { + if (typeof daily[key] === 'boolean') base.daily_automation[key] = daily[key]; + } + if (typeof daily.battle_type === 'string') { + base.daily_automation.battle_type = daily.battle_type; + } + if (daily.exercise_fleet_id === null || Number.isFinite(Number(daily.exercise_fleet_id))) { + base.daily_automation.exercise_fleet_id = daily.exercise_fleet_id === null + ? null + : Math.max(1, Math.trunc(Number(daily.exercise_fleet_id))); + } + base.daily_automation.bath_repair_blacklist = this.stringList( + daily.bath_repair_blacklist, + ); + base.daily_automation.normal_fight_tasks = this.normalFightTasks( + daily.normal_fight_tasks, + ); + if (daily.quick_repair_limit === null || Number.isFinite(Number(daily.quick_repair_limit))) { + base.daily_automation.quick_repair_limit = daily.quick_repair_limit === null + ? null + : Math.max(0, Math.trunc(Number(daily.quick_repair_limit))); + } + this.legacyGuiAutomation = this.readLegacyGuiAutomation(daily); + this.legacyDecisiveAutomation = + this.readLegacyDecisiveAutomation(daily); + } else { + this.legacyGuiAutomation = {}; + this.legacyDecisiveAutomation = {}; + this.invalidLegacyDecisiveFields = []; + } + + base.operation_delay_min = this.clampNumber( + parsed.operation_delay_min, + 0, + 10, + 0, + ); + base.operation_delay_max = this.clampNumber( + parsed.operation_delay_max, + 0, + 10, + 0, + ); + if (typeof parsed.dock_full_destroy === 'boolean') { + base.dock_full_destroy = parsed.dock_full_destroy; + } + if (typeof parsed.repair_manually === 'boolean') { + base.repair_manually = parsed.repair_manually; + } + base.bathroom_count = Math.max( + 1, + Math.min(12, Math.trunc(Number(parsed.bathroom_count) || 2)), + ); + base.destroy_ship_work_mode = this.destroyMode(parsed.destroy_ship_work_mode); + base.destroy_ship_types = this.stringList(parsed.destroy_ship_types); + if (typeof parsed.remove_equipment_mode === 'boolean') { + base.remove_equipment_mode = parsed.remove_equipment_mode; + } + if (typeof parsed.plan_root === 'string' && parsed.plan_root.trim()) { + base.plan_root = parsed.plan_root; + } + this.settings = base; } /** 导出当前配置为 YAML 字符串 */ toYaml(): string { - return yaml.dump(this.settings, { lineWidth: -1, noRefs: true }); + const output = structuredClone(this.rawRoot); + output.emulator = this.mergeSection(this.rawRoot.emulator, this.settings.emulator); + output.account = this.mergeSection(this.rawRoot.account, this.settings.account); + output.ocr = this.mergeSection( + this.rawRoot.ocr, + this.settings.ocr, + ['ship_name_corrections', 'ship_name_aliases'], + ); + output.log = this.mergeSection( + this.rawRoot.log, + this.settings.log, + ['channels'], + ); + + const daily = this.mergeSection( + this.rawRoot.daily_automation, + this.settings.daily_automation, + ); + if (daily.exercise_fleet_id === null) delete daily.exercise_fleet_id; + if (daily.quick_repair_limit === null) delete daily.quick_repair_limit; + output.daily_automation = daily; + + output.operation_delay_min = this.settings.operation_delay_min; + output.operation_delay_max = this.settings.operation_delay_max; + output.dock_full_destroy = this.settings.dock_full_destroy; + output.repair_manually = this.settings.repair_manually; + output.bathroom_count = this.settings.bathroom_count; + output.destroy_ship_work_mode = this.settings.destroy_ship_work_mode; + output.destroy_ship_types = [...this.settings.destroy_ship_types]; + output.remove_equipment_mode = this.settings.remove_equipment_mode; + if (this.settings.plan_root) output.plan_root = this.settings.plan_root; + else delete output.plan_root; + + return yamlCodec.stringify(output, { lineWidth: -1, noRefs: true }); } /** 局部更新配置 (深合并) */ @@ -84,8 +327,348 @@ export class ConfigModel { if (partial.account) { Object.assign(this.settings.account, partial.account); } + if (partial.ocr) { + Object.assign(this.settings.ocr, partial.ocr); + } + if (partial.log) { + Object.assign(this.settings.log, partial.log); + } if (partial.daily_automation) { Object.assign(this.settings.daily_automation, partial.daily_automation); } + for (const key of [ + 'operation_delay_min', + 'operation_delay_max', + 'dock_full_destroy', + 'repair_manually', + 'bathroom_count', + 'destroy_ship_work_mode', + 'destroy_ship_types', + 'remove_equipment_mode', + 'plan_root', + ] as const) { + if (key in partial) { + (this.settings as unknown as Record)[key] = + structuredClone(partial[key]); + } + } + } + + updateGuiAutomation(partial: Partial): void { + Object.assign(this.guiAutomation, partial); + const lootPlans = normalizeLootAutomationPlans( + this.guiAutomation.lootPlans, + ); + const selectedLootPlan = findLootAutomationPlan( + lootPlans, + this.guiAutomation.lootPlanSource, + this.guiAutomation.lootPlanId, + ); + const fallbackLootPlan = lootPlans[0] ?? DEFAULT_LOOT_PLANS[0]; + this.guiAutomation.expeditionInterval = Math.max( + 1, + Math.min(120, Math.trunc(this.guiAutomation.expeditionInterval || 15)), + ); + this.guiAutomation.battleTimes = Math.max( + 1, + Math.trunc(this.guiAutomation.battleTimes || 3), + ); + this.guiAutomation.autoDecisive = + this.guiAutomation.autoDecisive === true; + this.guiAutomation.decisiveTemplateId = + normalizeDecisiveAutomationSource( + this.guiAutomation.decisiveTemplateId, + ); + this.guiAutomation.lootPlans = lootPlans; + this.guiAutomation.lootPlanSource = + selectedLootPlan?.source ?? fallbackLootPlan?.source ?? 'system'; + this.guiAutomation.lootPlanId = + selectedLootPlan?.file ?? fallbackLootPlan?.file ?? DEFAULT_LOOT_PLAN_ID; + if (!selectedLootPlan) this.guiAutomation.autoLoot = false; + this.guiAutomation.lootStopCount = Math.max( + 1, + Math.min(50, Math.trunc(this.guiAutomation.lootStopCount || 50)), + ); + } + + /** 用默认值重置后再应用迁移结果,避免上一次加载状态参与字段优先级。 */ + replaceGuiAutomation(partial: Partial): void { + this.guiAutomation = structuredClone(DEFAULT_GUI_AUTOMATION); + this.updateGuiAutomation(partial); + } + + /** 仅在完整 GUI 自动化配置持久化成功后清理旧 YAML 字段。 */ + markLegacyGuiAutomationMigrated(): void { + const daily = this.asRecord(this.rawRoot.daily_automation); + if (!daily) return; + for (const key of LEGACY_DAILY_KEYS) delete daily[key]; + this.legacyGuiAutomation = this.readLegacyGuiAutomation(daily); + } + + /** + * 仅清理已经由主进程写入并回读确认的旧版决战字段。 + * 未迁移或格式异常的字段继续由 rawRoot 原样写回 YAML。 + */ + markLegacyDecisiveAutomationMigrated( + settings: LegacyDecisiveAutomationSettings, + ): void { + const daily = this.asRecord(this.rawRoot.daily_automation); + if (!daily) return; + if ( + Object.prototype.hasOwnProperty.call(settings, 'autoDecisive') + ) { + delete daily.auto_decisive; + } + if ( + Object.prototype.hasOwnProperty.call(settings, 'ticketReserve') + ) { + delete daily.decisive_ticket_reserve; + } + if ( + Object.prototype.hasOwnProperty.call(settings, 'templateId') + ) { + delete daily.decisive_template_id; + } + this.legacyDecisiveAutomation = + this.readLegacyDecisiveAutomation(daily); + } + + private asRecord(value: unknown): Record | null { + return value && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : null; + } + + private mergeSection( + raw: unknown, + values: object, + stringMapKeys: readonly string[] = [], + ): Record { + const output = structuredClone(this.asRecord(raw) ?? {}); + for (const [key, value] of Object.entries(values)) { + if (value === undefined) { + delete output[key]; + } else if (stringMapKeys.includes(key)) { + output[key] = this.mergeStringMap(output[key], value); + } else { + const nested = this.asRecord(value); + output[key] = nested + ? this.mergeSection(output[key], nested) + : structuredClone(value); + } + } + return output; + } + + /** 合并动态字符串映射,同时保留 GUI 无法识别的扩展值。 */ + private mergeStringMap( + raw: unknown, + value: unknown, + ): Record { + const rawEntries = this.asRecord(raw) ?? {}; + const current = this.asRecord(value) ?? {}; + const output = structuredClone(rawEntries); + for (const [key, rawValue] of Object.entries(rawEntries)) { + if ( + typeof rawValue === 'string' + && !Object.prototype.hasOwnProperty.call(current, key) + ) { + delete output[key]; + } + } + for (const [key, currentValue] of Object.entries(current)) { + if (currentValue === undefined) { + delete output[key]; + } else { + output[key] = structuredClone(currentValue); + } + } + return output; + } + + private clampNumber( + value: unknown, + min: number, + max: number, + fallback: number, + ): number { + const number = Number(value); + if (!Number.isFinite(number)) return fallback; + return Math.max(min, Math.min(max, number)); + } + + private stringList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value + .filter(item => typeof item === 'string') + .map(item => item.trim()) + .filter(Boolean); + } + + private stringMap(value: unknown): Record { + const record = this.asRecord(value); + if (!record) return {}; + const output: Record = {}; + for (const [key, item] of Object.entries(record)) { + if (typeof item === 'string' && key.trim() && item.trim()) { + output[key.trim()] = item.trim(); + } + } + return output; + } + + private normalFightTasks(value: unknown): NormalFightTaskConfig[] { + if (!Array.isArray(value)) return []; + const output: NormalFightTaskConfig[] = []; + for (const item of value) { + if (typeof item === 'string' && item.trim()) { + output.push({ name: item.trim() }); + continue; + } + if (Array.isArray(item) && item.length > 0) { + const name = String(item[0] ?? '').trim(); + if (!name) continue; + const task: NormalFightTaskConfig = { name }; + if (item[1] != null && Number.isFinite(Number(item[1]))) { + task.fleet_id = Math.max(1, Math.trunc(Number(item[1]))); + } + if (item[2] != null && Number.isFinite(Number(item[2]))) { + task.times = Math.max(1, Math.trunc(Number(item[2]))); + } + output.push(task); + continue; + } + const record = this.asRecord(item); + if (!record || typeof record.name !== 'string' || !record.name.trim()) continue; + const task: NormalFightTaskConfig = { name: record.name.trim() }; + if (record.fleet_id != null && Number.isFinite(Number(record.fleet_id))) { + task.fleet_id = Math.max(1, Math.trunc(Number(record.fleet_id))); + } + if ( + record.fleet_preset_index != null + && Number.isFinite(Number(record.fleet_preset_index)) + ) { + task.fleet_preset_index = Math.max( + 0, + Math.trunc(Number(record.fleet_preset_index)), + ); + } + if (record.times != null && Number.isFinite(Number(record.times))) { + task.times = Math.max(1, Math.trunc(Number(record.times))); + } + output.push(task); + } + return output; + } + + private readLegacyGuiAutomation( + daily: Record, + ): Partial { + const output: Partial = {}; + if (Number.isFinite(Number(daily.expedition_interval))) { + output.expeditionInterval = Number(daily.expedition_interval); + } + if (Number.isFinite(Number(daily.battle_times))) { + output.battleTimes = Number(daily.battle_times); + } + if (typeof daily.auto_loot === 'boolean') output.autoLoot = daily.auto_loot; + if (typeof daily.loot_plan_id === 'string') { + const migrated = migrateLootPlanId(daily.loot_plan_id); + if (migrated) { + output.lootPlanSource = 'system'; + output.lootPlanId = migrated; + } else { + output.autoLoot = false; + } + } else if ( + Object.prototype.hasOwnProperty.call(daily, 'loot_plan_index') + ) { + const resolved = lootPlanIdFromIndex( + daily.loot_plan_index, + LEGACY_LOOT_PLAN_IDS, + ); + if (resolved) { + output.lootPlanSource = 'system'; + output.lootPlanId = resolved; + } + else output.autoLoot = false; + } + if (output.autoLoot === true && !output.lootPlanId) { + output.autoLoot = false; + } + if (Number.isFinite(Number(daily.loot_stop_count))) { + output.lootStopCount = Number(daily.loot_stop_count); + } + return output; + } + + /** 读取可安全迁移的旧决战值,并记录必须原样保留的异常字段。 */ + private readLegacyDecisiveAutomation( + daily: Record, + ): LegacyDecisiveAutomationSettings { + const output: LegacyDecisiveAutomationSettings = {}; + const invalid: LegacyDecisiveYamlKey[] = []; + if ( + Object.prototype.hasOwnProperty.call(daily, 'auto_decisive') + ) { + if (typeof daily.auto_decisive === 'boolean') { + output.autoDecisive = daily.auto_decisive; + } else { + invalid.push('auto_decisive'); + } + } + if ( + Object.prototype.hasOwnProperty.call( + daily, + 'decisive_ticket_reserve', + ) + ) { + const raw = daily.decisive_ticket_reserve; + const numericString = typeof raw === 'string' && raw.trim(); + if ( + (typeof raw === 'number' && Number.isFinite(raw)) + || ( + numericString + && Number.isFinite(Number(raw)) + ) + ) { + output.ticketReserve = Number(raw); + } else { + invalid.push('decisive_ticket_reserve'); + } + } + if ( + Object.prototype.hasOwnProperty.call( + daily, + 'decisive_template_id', + ) + ) { + if ( + typeof daily.decisive_template_id === 'string' + && daily.decisive_template_id.length > 0 + ) { + output.templateId = daily.decisive_template_id; + } else { + invalid.push('decisive_template_id'); + } + } + this.invalidLegacyDecisiveFields = invalid; + return output; + } + + private destroyMode(value: unknown): number { + const aliases: Record = { + '不启用': 0, + disable: 0, + '黑名单': 1, + include: 1, + '白名单': 2, + exclude: 2, + }; + if (typeof value === 'string' && value.trim() in aliases) { + return aliases[value.trim()]; + } + const number = Math.trunc(Number(value)); + return [0, 1, 2].includes(number) ? number : 0; } } diff --git a/src/model/MapDataLoader.ts b/src/model/MapDataLoader.ts index c4494fe..554a278 100644 --- a/src/model/MapDataLoader.ts +++ b/src/model/MapDataLoader.ts @@ -1,7 +1,14 @@ +/** 加载、缓存并查询地图节点、连线和位置信息。 */ /** * MapDataLoader —— 加载地图 JSON 数据。 * 通过 IPC 从 resource/maps/ 目录读取地图数据文件。 */ +import { jsonCodec, mapDataRepository } from '../adapter/index.js'; +import type { + EventChapter, + EventMapCatalog, + EventMapCatalogEntry, +} from '../types/model.js'; /** 地图节点类型 */ export type MapNodeType = 'Start' | 'Normal' | 'Boss' | 'Resource' | 'Penalty' | 'Suppress' | 'Aerial' | 'Hard'; @@ -20,39 +27,41 @@ export type MapData = Record; /** 地图数据缓存 */ const mapCache = new Map(); +let eventCatalogPromise: Promise | null = null; +const eventMapFiles = new Map(); -/** 加载指定章节-关卡的地图数据 */ -export async function loadMapData(chapter: number, map: number): Promise { - const key = `${chapter}-${map}`; - if (mapCache.has(key)) return mapCache.get(key)!; +function eventMapKey( + eventName: string, + chapter: EventChapter, + map: string, +): string { + return `${eventName}:${chapter}:${map}`; +} - const filePath = `resource/maps/${key}.json`; +function isSafeMapFileName(value: unknown): value is string { + return typeof value === 'string' + && /^[^/\\]+\.json$/i.test(value) + && value !== '.json' + && !value.includes('..'); +} + +async function readMap(filePath: string): Promise { try { - const bridge = (window as any).electronBridge; - const content: string = await bridge.readFile(filePath); - const data: MapData = JSON.parse(content); - mapCache.set(key, data); - return data; + const content = await mapDataRepository.read(filePath); + return jsonCodec.parse(content); } catch { return null; } } -/** 加载 Ex 关卡地图数据 */ -export async function loadExMapData(exNumber: number): Promise { - const key = `Ex-${exNumber}`; +/** 加载指定章节-关卡的地图数据 */ +export async function loadMapData(chapter: number, map: number): Promise { + const key = `${chapter}-${map}`; if (mapCache.has(key)) return mapCache.get(key)!; - const filePath = `resource/maps/${key}.json`; - try { - const bridge = (window as any).electronBridge; - const content: string = await bridge.readFile(filePath); - const data: MapData = JSON.parse(content); - mapCache.set(key, data); - return data; - } catch { - return null; - } + const data = await readMap(`resource/maps/normal/${key}.json`); + if (data) mapCache.set(key, data); + return data; } function normalizeEventNodeType(raw: unknown): MapNodeType { @@ -67,40 +76,127 @@ function normalizeEventNodeType(raw: unknown): MapNodeType { } } -/** 加载活动地图。map 可为 1/1a/3b,入口默认 α。 */ +/** 加载 GUI 内置的活动、难度和真实入口清单。 */ +export function loadEventMapCatalog(): Promise { + if (eventCatalogPromise) return eventCatalogPromise; + eventCatalogPromise = mapDataRepository + .read('resource/maps/event/index.json') + .then((content) => { + const catalog = jsonCodec.parse(content); + if ( + catalog.schema_version !== 2 + || !Array.isArray(catalog.events) + ) { + return []; + } + eventMapFiles.clear(); + return catalog.events.flatMap((entry) => { + if ( + !entry + || typeof entry.event !== 'string' + || !/^\d{8}$/.test(entry.event) + ) { + return []; + } + const chapters: Record = { E: [], H: [] }; + const files: Record> = { + E: {}, + H: {}, + }; + const eventChapters: EventChapter[] = ['E', 'H']; + for (const chapter of eventChapters) { + const maps = entry.chapters?.[chapter]; + if (!Array.isArray(maps)) continue; + chapters[chapter] = maps + .map(String) + .filter(map => /^\d+(?:[ab])?$/.test(map)); + const chapterFiles = entry.files?.[chapter]; + if (!chapterFiles || typeof chapterFiles !== 'object') continue; + for (const map of chapters[chapter]) { + const file = chapterFiles[map]; + if (!isSafeMapFileName(file)) continue; + files[chapter][map] = file; + eventMapFiles.set( + eventMapKey(entry.event, chapter, map), + file, + ); + } + } + const normalized: EventMapCatalogEntry = { + event: entry.event, + chapters, + }; + if ( + Object.keys(files.E).length > 0 + || Object.keys(files.H).length > 0 + ) { + normalized.files = files; + } + return [normalized]; + }); + }) + .catch(() => []); + return eventCatalogPromise; +} + +function normalizeEventNodeId(nodeId: string): string { + return nodeId === 'α' || nodeId === 'β' ? '0' : nodeId; +} + +/** 按 AutoWSGR 活动目录和原始文件名加载地图。 */ export async function loadEventMapData( eventName: string, chapter: number | string, map: number | string, ): Promise { const match = String(map).trim().match(/^(\d+)([ab])?$/i); - const difficulty = String(chapter).trim().toUpperCase(); - if (!eventName || !match || !['E', 'H'].includes(difficulty)) return null; + const rawDifficulty = String(chapter).trim().toUpperCase(); + const normalizedEvent = String(eventName).trim(); + if ( + !/^\d{8}$/.test(normalizedEvent) + || !match + || !['E', 'H'].includes(rawDifficulty) + ) { + return null; + } + const difficulty: EventChapter = rawDifficulty === 'E' ? 'E' : 'H'; const stage = match[1]; - const entrance = (match[2] ?? 'a').toLowerCase(); - const key = `event-${eventName}-${difficulty}-${stage}${entrance}`; + const entrance = match[2]?.toLowerCase() ?? ''; + const mapCode = `${stage}${entrance}`; + const key = `event-${normalizedEvent}-${difficulty}-${mapCode}`; if (mapCache.has(key)) return mapCache.get(key)!; - const filePath = `resource/maps/event/${eventName}/${difficulty}-${stage}${entrance}.json`; - try { - const bridge = (window as any).electronBridge; - const content: string = await bridge.readFile(filePath); - const raw = JSON.parse(content) as Record>; - const data: MapData = {}; - for (const [id, point] of Object.entries(raw)) { - data[id] = { - type: normalizeEventNodeType(point.type), - detour: !!point.detour, - night: !!point.night, - position: Array.isArray(point.position) ? point.position as [number, number] : [0, 0], - next: Array.isArray(point.next) ? point.next.map(String) : [], - }; - } - mapCache.set(key, data); - return data; - } catch { - return null; + await loadEventMapCatalog(); + const canonicalFile = eventMapFiles.get( + eventMapKey(normalizedEvent, difficulty, mapCode), + ); + if (!canonicalFile) return null; + const raw = await readMap( + `resource/maps/event/${normalizedEvent}/${canonicalFile}`, + ); + if (!raw) return null; + + const data: MapData = {}; + for (const [id, point] of Object.entries(raw)) { + const normalizedId = normalizeEventNodeId(id); + const position: [number, number] = ( + Array.isArray(point.position) + && point.position.length === 2 + ) + ? [Number(point.position[0]), Number(point.position[1])] + : [0, 0]; + data[normalizedId] = { + type: normalizeEventNodeType(point.type), + detour: !!point.detour, + night: !!point.night, + position, + next: Array.isArray(point.next) + ? point.next.map(node => normalizeEventNodeId(String(node))) + : [], + }; } + mapCache.set(key, data); + return data; } /** 获取地图中某节点的类型,找不到则返回 'Normal' */ diff --git a/src/model/PlanModel.ts b/src/model/PlanModel.ts index 1f8557f..c829e45 100644 --- a/src/model/PlanModel.ts +++ b/src/model/PlanModel.ts @@ -1,25 +1,161 @@ +/** 持有作战方案状态并处理节点规则、迁移和 YAML 往返。 */ /** * PlanModel —— 战斗方案(Plan)的 Model 层。 * 负责从 YAML 文件解析战斗方案,并提供节点参数的查询与合并。 */ -import * as yaml from 'js-yaml'; -import type { PlanData, NodeArgs, FleetPreset, ShipSlot, ShipFilter, EnemyRule } from '../types/model'; +import { yamlCodec } from '../adapter/index.js'; +import type { + PlanData, + NodeArgs, + FleetPreset, + ShipSlot, + ShipFilter, + ShipRule, + EnemyRule, + BattleResultGrade, +} from '../types/model.js'; + +type YamlPath = string[]; +const BATTLE_RESULT_GRADES = new Set(['D', 'C', 'B', 'A', 'S', 'SS']); + +function isYamlMapping(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function isYamlScalar(value: unknown): boolean { + return value === null || ['string', 'number', 'boolean'].includes(typeof value); +} + +/** 使用 YAML 流式语法输出单个值,不改变值本身。 */ +function dumpFlowYaml(value: unknown): string { + return yamlCodec.stringify(value, { + flowLevel: 0, + lineWidth: -1, + noCompatMode: true, + noRefs: true, + sortKeys: false, + }).trimEnd(); +} + +function shouldInlineMapping(path: YamlPath): boolean { + return ( + (path.length === 1 && path[0] === 'node_defaults') + || (path.length === 2 && path[0] === 'node_args') + ); +} + +function canInlineValue(value: unknown, path: YamlPath): boolean { + if (isYamlScalar(value)) return true; + if (Array.isArray(value)) { + return value.length === 0 || value.every(isYamlScalar); + } + if (isYamlMapping(value)) { + return Object.keys(value).length === 0 || shouldInlineMapping(path); + } + return false; +} + +function serializeYamlSequence( + values: unknown[], + indent: number, + path: YamlPath, +): string[] { + const padding = ' '.repeat(indent); + const lines: string[] = []; + const inlineItems = path[path.length - 1] === 'candidates'; + + for (const value of values) { + if ( + isYamlScalar(value) + || (Array.isArray(value) && value.every(isYamlScalar)) + || (inlineItems && isYamlMapping(value)) + ) { + lines.push(`${padding}- ${dumpFlowYaml(value)}`); + continue; + } + + if (Array.isArray(value)) { + lines.push(`${padding}-`); + lines.push(...serializeYamlSequence(value, indent + 2, [...path, '[]'])); + continue; + } + + if (isYamlMapping(value)) { + const itemLines = serializeYamlMapping(value, indent + 2, [...path, '[]']); + if (itemLines.length === 0) { + lines.push(`${padding}- {}`); + continue; + } + const itemPadding = ' '.repeat(indent + 2); + lines.push(`${padding}- ${itemLines[0].slice(itemPadding.length)}`); + lines.push(...itemLines.slice(1)); + continue; + } + + lines.push(`${padding}- ${dumpFlowYaml(value)}`); + } + + return lines; +} + +function serializeYamlMapping( + value: Record, + indent: number, + path: YamlPath, +): string[] { + const padding = ' '.repeat(indent); + const lines: string[] = []; + + for (const [key, item] of Object.entries(value)) { + if (item === undefined) continue; + const itemPath = [...path, key]; + const yamlKey = dumpFlowYaml(key); + + if (canInlineValue(item, itemPath)) { + lines.push(`${padding}${yamlKey}: ${dumpFlowYaml(item)}`); + continue; + } + + lines.push(`${padding}${yamlKey}:`); + if (Array.isArray(item)) { + lines.push(...serializeYamlSequence(item, indent + 2, itemPath)); + } else if (isYamlMapping(item)) { + lines.push(...serializeYamlMapping(item, indent + 2, itemPath)); + } else { + lines.push(`${' '.repeat(indent + 2)}${dumpFlowYaml(item)}`); + } + } + + return lines; +} + +function serializePlanYaml(value: Record): string { + return `${serializeYamlMapping(value, 0, []).join('\n')}\n`; +} export class PlanModel { data: PlanData; fileName: string; comment: string; - - private constructor(data: PlanData, fileName: string, comment: string) { + /** 原始 YAML 根对象,用于保留 GUI 尚未建模的方案字段 */ + private rawRoot: Record; + + private constructor( + data: PlanData, + fileName: string, + comment: string, + rawRoot: Record = {}, + ) { this.data = data; this.fileName = fileName; this.comment = comment; + this.rawRoot = rawRoot; } /** 从 YAML 字符串 + 文件路径创建 PlanModel */ static fromYaml(content: string, path: string): PlanModel { const comment = PlanModel.extractComment(content); - const parsed = yaml.load(content) as Record; + const parsed = yamlCodec.parse>(content); if (!parsed || typeof parsed !== 'object') { throw new Error('无效的方案文件'); } @@ -48,6 +184,10 @@ export class PlanModel { endpoint_nodes: Array.isArray(parsed.endpoint_nodes) ? parsed.endpoint_nodes.map(String) : undefined, + result: typeof parsed.result === 'string' + && BATTLE_RESULT_GRADES.has(parsed.result.toUpperCase() as BattleResultGrade) + ? parsed.result.toUpperCase() as BattleResultGrade + : undefined, fight_condition: parsed.fight_condition != null ? Number(parsed.fight_condition) : undefined, repair_mode: parsed.repair_mode != null ? (Array.isArray(parsed.repair_mode) @@ -65,7 +205,7 @@ export class PlanModel { scheduled_time: typeof parsed.scheduled_time === 'string' ? parsed.scheduled_time : undefined, }; - return new PlanModel(data, path, comment); + return new PlanModel(data, path, comment, structuredClone(parsed)); } /** 地图名,如 "7-4" 或 "Ex-3" */ @@ -74,8 +214,13 @@ export class PlanModel { const rawMap = String(this.data.map); const match = rawMap.match(/^(\d+)([ab])?$/i); const stage = match?.[1] ?? rawMap; - const entrance = match?.[2]?.toLowerCase() === 'b' ? 'β' : 'α'; - return `${String(this.data.chapter).toUpperCase()}-Ex-${stage}-${entrance}`; + const entranceCode = match?.[2]?.toLowerCase(); + const entrance = entranceCode === 'a' + ? '-α' + : entranceCode === 'b' + ? '-β' + : ''; + return `${String(this.data.chapter).toUpperCase()}-Ex-${stage}${entrance}`; } if (this.data.chapter === 99) return `Ex-${this.data.map}`; return `${this.data.chapter}-${this.data.map}`; @@ -126,10 +271,16 @@ export class PlanModel { } /** 创建空方案 (新建方案用) */ - static create(chapter: number, map: number, selectedNodes: string[]): PlanModel { + static create( + chapter: number | string, + map: number | string, + selectedNodes: string[], + event?: string, + ): PlanModel { const data: PlanData = { chapter, map, + event, selected_nodes: selectedNodes, fight_condition: 1, repair_mode: 1, @@ -142,52 +293,150 @@ export class PlanModel { /** 序列化为 YAML 字符串 */ toYaml(): string { - const obj: Record = { - chapter: this.data.chapter, - map: this.data.map, - selected_nodes: this.data.selected_nodes, - }; - - if (this.data.mode) obj.mode = this.data.mode; - if (this.data.event) obj.event = this.data.event; - - if (this.data.fleet_id != null) obj.fleet_id = this.data.fleet_id; - if (this.data.endpoint_nodes && this.data.endpoint_nodes.length > 0) obj.endpoint_nodes = this.data.endpoint_nodes; - if (this.data.fight_condition != null) obj.fight_condition = this.data.fight_condition; - if (this.data.repair_mode != null) obj.repair_mode = this.data.repair_mode; - - if (this.data.node_defaults && Object.keys(this.data.node_defaults).length > 0) { - obj.node_defaults = this.cleanNodeArgs(this.data.node_defaults); + const obj = structuredClone(this.rawRoot); + const rawFleetPresets = obj.fleet_presets; + delete obj.fleet_presets; + obj.chapter = this.data.chapter; + obj.map = this.data.map; + obj.selected_nodes = [...this.data.selected_nodes]; + + this.setOptionalField(obj, 'mode', this.data.mode); + this.setOptionalField(obj, 'event', this.data.event); + this.setOptionalField(obj, 'fleet_id', this.data.fleet_id); + this.setOptionalField( + obj, + 'endpoint_nodes', + this.data.endpoint_nodes?.length ? [...this.data.endpoint_nodes] : undefined, + ); + this.setOptionalField(obj, 'result', this.data.result); + this.setOptionalField(obj, 'fight_condition', this.data.fight_condition); + this.setOptionalField(obj, 'repair_mode', this.data.repair_mode); + + if (this.data.node_defaults) { + obj.node_defaults = this.mergeNodeArgs( + this.rawRoot.node_defaults, + this.data.node_defaults, + ); + } else { + delete obj.node_defaults; } if (this.data.node_args) { + const rawNodeArgs = this.rawRoot.node_args; + const rawMap = rawNodeArgs && typeof rawNodeArgs === 'object' && !Array.isArray(rawNodeArgs) + ? rawNodeArgs as Record + : {}; const cleaned: Record = {}; for (const [nodeId, args] of Object.entries(this.data.node_args)) { - const c = this.cleanNodeArgs(args); - if (Object.keys(c).length > 0) cleaned[nodeId] = c; + const merged = this.mergeNodeArgs(rawMap[nodeId], args); + if (Object.keys(merged).length > 0) cleaned[nodeId] = merged; } if (Object.keys(cleaned).length > 0) obj.node_args = cleaned; + else delete obj.node_args; + } else { + delete obj.node_args; } - // 编队预设 - if (this.data.fleet_presets && this.data.fleet_presets.length > 0) { - obj.fleet_presets = this.data.fleet_presets; - } + this.setOptionalField(obj, 'times', this.data.times); + this.setOptionalField(obj, 'gap', this.data.gap); + this.setOptionalField(obj, 'stop_condition', this.data.stop_condition); + this.setOptionalField(obj, 'scheduled_time', this.data.scheduled_time); - // 任务级字段 (仅导出已设置的) - if (this.data.times != null) obj.times = this.data.times; - if (this.data.gap != null) obj.gap = this.data.gap; - if (this.data.stop_condition != null) obj.stop_condition = this.data.stop_condition; - if (this.data.scheduled_time) obj.scheduled_time = this.data.scheduled_time; + if (this.data.fleet_presets !== undefined) { + if (this.data.fleet_presets.length > 0) { + obj.fleet_presets = this.mergeFleetPresets(this.data.fleet_presets); + } + } else if (rawFleetPresets !== undefined) { + obj.fleet_presets = rawFleetPresets; + } let result = ''; if (this.comment) { result = this.comment.split('\n').map(l => `# ${l}`).join('\n') + '\n'; } - result += yaml.dump(obj, { lineWidth: -1, noRefs: true, sortKeys: false }); + result += serializePlanYaml(obj); return result; } + /** 写入可选字段;值被清空时同步删除旧 YAML 中的字段 */ + private setOptionalField( + obj: Record, + key: string, + value: unknown, + ): void { + if (value == null || value === '') delete obj[key]; + else obj[key] = structuredClone(value); + } + + /** 更新 GUI 管理的节点参数,同时保留后端新增的未知参数 */ + private mergeNodeArgs(raw: unknown, args: NodeArgs): Record { + const output = raw && typeof raw === 'object' && !Array.isArray(raw) + ? structuredClone(raw as Record) + : {}; + const managedKeys = [ + 'formation', + 'night', + 'long_missile_support', + 'proceed', + 'detour', + 'SL_when_detour_fails', + 'enemy_rules', + 'proceed_stop', + ]; + for (const key of managedKeys) delete output[key]; + Object.assign(output, this.cleanNodeArgs(args)); + return output; + } + + /** 更新编队预设,同时保留预设和槽位中 GUI 尚未建模的字段 */ + private mergeFleetPresets(presets: FleetPreset[]): Record[] { + const rawPresets = Array.isArray(this.rawRoot.fleet_presets) + ? this.rawRoot.fleet_presets + : []; + + return presets.map((preset, index) => { + const rawPreset = rawPresets.find((item) => ( + item + && typeof item === 'object' + && !Array.isArray(item) + && (item as Record).name === preset.name + )) ?? rawPresets[index]; + const output = rawPreset && typeof rawPreset === 'object' && !Array.isArray(rawPreset) + ? structuredClone(rawPreset as Record) + : {}; + const rawShips = Array.isArray(output.ships) ? output.ships : []; + + output.name = preset.name; + output.ships = preset.ships.map((slot, slotIndex) => ( + this.mergeShipSlot(rawShips[slotIndex], slot) + )); + return output; + }); + } + + /** 覆盖单个槽位的正式字段,并清理旧版 priority 字段 */ + private mergeShipSlot(raw: unknown, slot: ShipSlot): ShipSlot | Record { + if (slot === null) return null; + if (typeof slot === 'string') return slot; + + const output = raw && typeof raw === 'object' && !Array.isArray(raw) + ? structuredClone(raw as Record) + : {}; + const managedKeys = [ + 'name', + 'nation', + 'search_name', + 'ship_type', + 'priority', + 'candidates', + 'min_level', + 'max_level', + ]; + for (const key of managedKeys) delete output[key]; + Object.assign(output, structuredClone(slot)); + return output; + } + /** 清理节点参数:移除 undefined 值 */ private cleanNodeArgs(args: NodeArgs): Record { const out: Record = {}; @@ -211,6 +460,7 @@ export class PlanModel { const obj = item as Record; if (typeof obj.name !== 'string' || !Array.isArray(obj.ships)) continue; presets.push({ + ...obj, name: obj.name, ships: (obj.ships as unknown[]).map(PlanModel.parseShipSlot), }); @@ -220,6 +470,7 @@ export class PlanModel { /** 解析单个舰船槽位: 字符串 → 具体舰船, 对象 → ShipFilter */ private static parseShipSlot(raw: unknown): ShipSlot { + if (raw === null) return null; if (typeof raw === 'string') return raw; if (raw && typeof raw === 'object') { const obj = raw as Record; @@ -228,33 +479,105 @@ export class PlanModel { const normalizedName = String(obj.name).trim(); if (normalizedName) filter.name = normalizedName; } - if (typeof obj.nation === 'string') filter.nation = obj.nation; - if (typeof obj.ship_type === 'string') filter.ship_type = obj.ship_type; - if (Array.isArray(obj.priority)) { - const names = (obj.priority as unknown[]) - .filter(v => typeof v === 'string') - .map(v => String(v).trim()) - .filter(Boolean); - if (names.length > 0) filter.priority = names; - } - if (obj.min_level != null && Number.isFinite(Number(obj.min_level))) { - filter.min_level = Math.max(1, Math.floor(Number(obj.min_level))); + if (typeof obj.search_name === 'string' && obj.search_name.trim()) { + filter.search_name = obj.search_name.trim(); } - if (obj.max_level != null && Number.isFinite(Number(obj.max_level))) { - filter.max_level = Math.max(1, Math.floor(Number(obj.max_level))); + if (typeof obj.nation === 'string') filter.nation = obj.nation; + const shipTypes = PlanModel.parseShipTypes(obj.ship_type); + if (shipTypes) filter.ship_type = shipTypes; + PlanModel.assignLevelRange(filter, obj); + + const rawCandidates = Array.isArray(obj.candidates) + ? obj.candidates + : obj.priority; + if (Array.isArray(rawCandidates)) { + const candidateOnlyWithLegacyNames = ( + !filter.name + && rawCandidates.some(value => typeof value === 'string') + ); + const candidates = rawCandidates.flatMap((candidate) => { + if (typeof candidate === 'string') { + const name = candidate.trim(); + if (!name) return []; + const legacyRule: ShipRule = { name }; + if (filter.search_name) { + legacyRule.search_name = filter.search_name; + } + if (filter.ship_type) legacyRule.ship_type = [...filter.ship_type]; + if (filter.min_level !== undefined) { + legacyRule.min_level = filter.min_level; + } + if (filter.max_level !== undefined) { + legacyRule.max_level = filter.max_level; + } + return [legacyRule]; + } + const parsed = PlanModel.parseShipRule(candidate); + return parsed ? [parsed] : []; + }); + if (candidates.length > 0) filter.candidates = candidates; + if (candidateOnlyWithLegacyNames) delete filter.search_name; } - if (filter.name || filter.nation || filter.ship_type || filter.priority || filter.min_level != null || filter.max_level != null) return filter; + return filter; } return String(raw); } - private static normalizeRuleAction(actionRaw: unknown): string | number { + private static parseShipRule(raw: unknown): ShipRule | null { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null; + const obj = raw as Record; + if (typeof obj.name !== 'string' || !obj.name.trim()) return null; + + const rule: ShipRule = { + name: obj.name.trim(), + }; + if (typeof obj.search_name === 'string' && obj.search_name.trim()) { + rule.search_name = obj.search_name.trim(); + } + const shipTypes = PlanModel.parseShipTypes(obj.ship_type); + if (shipTypes) rule.ship_type = shipTypes; + PlanModel.assignLevelRange(rule, obj); + return rule; + } + + private static parseShipTypes(raw: unknown): string[] | undefined { + const values = typeof raw === 'string' + ? [raw] + : Array.isArray(raw) + ? raw + : []; + const result = Array.from(new Set( + values + .filter(value => typeof value === 'string') + .map(value => String(value).trim().toLowerCase()) + .filter(Boolean), + )); + return result.length > 0 ? result : undefined; + } + + private static assignLevelRange( + rule: { + min_level?: number; + max_level?: number; + }, + raw: Record, + ): void { + if (raw.min_level != null && Number.isFinite(Number(raw.min_level))) { + rule.min_level = Math.max(1, Math.floor(Number(raw.min_level))); + } + if (raw.max_level != null && Number.isFinite(Number(raw.max_level))) { + rule.max_level = Math.max(1, Math.floor(Number(raw.max_level))); + } + } + + private static normalizeRuleAction(actionRaw: unknown): EnemyRule[1] | null { if (typeof actionRaw === 'number' && Number.isFinite(actionRaw)) { - return Math.trunc(actionRaw); + const value = Math.trunc(actionRaw); + return value >= 1 && value <= 5 ? (value as EnemyRule[1]) : null; } const raw = String(actionRaw ?? '').trim(); - if (!raw) return raw; + if (!raw) return null; const aliases: Record = { detour: 'detour', @@ -266,9 +589,12 @@ export class PlanModel { const lower = raw.toLowerCase(); const normalized = aliases[lower] ?? aliases[raw] ?? raw; if (/^\d+$/.test(normalized)) { - return Number(normalized); + const value = Number(normalized); + return value >= 1 && value <= 5 ? (value as EnemyRule[1]) : null; } - return normalized; + return normalized === 'retreat' || normalized === 'detour' + ? normalized + : null; } private static normalizeEnemyRules(rules: unknown): EnemyRule[] | undefined { @@ -280,6 +606,7 @@ export class PlanModel { const expr = String(item[0] ?? '').trim(); if (!expr) continue; const action = PlanModel.normalizeRuleAction(item[1]); + if (action === null) continue; normalized.push([expr, action]); } @@ -290,6 +617,9 @@ export class PlanModel { if (!args) return undefined; const normalized: NodeArgs = { ...args }; normalized.enemy_rules = PlanModel.normalizeEnemyRules(args.enemy_rules); + normalized.enemy_formation_rules = PlanModel.normalizeEnemyRules( + args.enemy_formation_rules, + ); return normalized; } diff --git a/src/model/TaskGroupModel.ts b/src/model/TaskGroupModel.ts index 95ed6d6..32561cc 100644 --- a/src/model/TaskGroupModel.ts +++ b/src/model/TaskGroupModel.ts @@ -1,8 +1,14 @@ +/** 持有任务组及条目状态,并负责 JSON 迁移和持久化。 */ /** * TaskGroupModel —— 任务组数据模型。 * 管理多个任务组的增删改查和持久化(通过 IPC 读写 task_groups.json)。 */ import { Logger } from '../utils/Logger'; +import { jsonCodec, rendererFileRepository } from '../adapter/index.js'; +import type { + DailyPlanType, + PlanPresetSource, +} from '../types/ipc.js'; // ════════════════════════════════════════ // 数据结构 @@ -10,12 +16,23 @@ import { Logger } from '../utils/Logger'; /** 任务组中的单个条目 */ export interface TaskGroupItem { + [key: string]: unknown; /** 文件路径 (战斗方案/预设 YAML) — plan/preset 类型必填 */ path?: string; + /** 计划管理目录来源;与 managedFile 一起使用,避免持久化绝对路径 */ + managedSource?: PlanPresetSource; + /** 计划管理目录中的实际文件名 */ + managedFile?: string; + /** 日常任务目录来源;与 dailyFile 一起使用 */ + dailySource?: PlanPresetSource; + /** 日常任务目录中的实际文件名 */ + dailyFile?: string; + /** 日常任务类型,用于限制可编辑字段 */ + dailyTaskType?: DailyPlanType; /** 模板 ID — template 类型必填 */ templateId?: string; - /** 条目类型: plan=战斗方案YAML, preset=任务预设YAML, template=模板库引用 */ - kind: 'plan' | 'preset' | 'template'; + /** 条目类型:daily 使用独立日常任务目录 */ + kind: 'plan' | 'preset' | 'template' | 'daily'; /** 执行次数 */ times: number; /** 显示名称 */ @@ -30,14 +47,15 @@ export interface TaskGroupItem { allowPolling?: boolean; /** 章节覆盖(仅 decisive 模板使用时选择) */ chapter?: number; + /** 决战快速修理开关覆盖 */ + useQuickRepair?: boolean; /** 编队预设索引(plan 类型条目指定默认使用的编队预设) */ fleetPresetIndex?: number; - /** 是否因“第一分队不支持自动编队”而自动切换到第二分队 */ - autoFleetFallback?: boolean; } /** 一个任务组 */ export interface TaskGroup { + [key: string]: unknown; /** 唯一名称 */ name: string; /** 有序的任务条目 */ @@ -46,6 +64,8 @@ export interface TaskGroup { /** 持久化格式 */ interface TaskGroupsData { + [key: string]: unknown; + version: number; /** 当前选中的组名 */ activeGroup: string; /** 所有组 */ @@ -57,9 +77,21 @@ interface TaskGroupsData { // ════════════════════════════════════════ const STORAGE_FILE = 'task_groups.json'; +const TASK_GROUPS_VERSION = 4; +const LEGACY_SYSTEM_PLAN_FILES: Readonly> = { + '周常2章-2-1.yaml': 'bettle-周常-2-1.yaml', + '周常3章-3-1.yaml': 'bettle-周常-3-1.yaml', + '周常4章-4-1.yaml': 'bettle-周常-4-1.yaml', + '周常5章-5-5.yaml': 'bettle-周常-5-5.yaml', + '周常6章-6-4.yaml': 'bettle-周常-6-4.yaml', + '周常7章-7-4.yaml': 'bettle-周常-7-4.yaml', + '周常8章-8-2.yaml': 'bettle-周常-8-2.yaml', + '周常9章-9-2.yaml': 'bettle-周常-9-2.yaml', + '周常10章-10-1.yaml': 'bettle-周常-10-1.yaml', +}; export class TaskGroupModel { - private data: TaskGroupsData = { activeGroup: '', groups: [] }; + private data: TaskGroupsData = { version: TASK_GROUPS_VERSION, activeGroup: '', groups: [] }; get groups(): ReadonlyArray { return this.data.groups; @@ -159,28 +191,112 @@ export class TaskGroupModel { /** 从文件加载 */ async load(): Promise { try { - const bridge = (window as any).electronBridge; - if (!bridge?.readFile) return; - const content = await bridge.readFile(STORAGE_FILE); - const parsed = JSON.parse(content) as TaskGroupsData; + const content = await rendererFileRepository.readFile(STORAGE_FILE); + const parsed = jsonCodec.parse & { groups?: unknown }>(content); if (parsed && Array.isArray(parsed.groups)) { - this.data = parsed; - Logger.debug(`任务组已加载: ${parsed.groups.length} 个组`); + const needsMigration = parsed.version !== TASK_GROUPS_VERSION; + this.data = this.migrate({ ...parsed, groups: parsed.groups }); + if (needsMigration) await this.save(); + Logger.debug(`任务组已加载: ${this.data.groups.length} 个组 (v${this.data.version})`); } } catch { // 文件不存在是正常的 } } + private migrate(raw: Partial & { groups: unknown[] }): TaskGroupsData { + const groups = raw.groups.flatMap((group): TaskGroup[] => { + if (!group || typeof group !== 'object' || Array.isArray(group)) return []; + const source = group as unknown as Record; + const name = typeof source.name === 'string' && source.name.trim() + ? source.name + : '默认'; + const items = Array.isArray(source.items) + ? source.items.flatMap(item => this.migrateItem(item)) + : []; + return [{ ...source, name, items } as TaskGroup]; + }); + const activeGroup = typeof raw.activeGroup === 'string' + && groups.some(group => group.name === raw.activeGroup) + ? raw.activeGroup + : groups[0]?.name ?? ''; + return { + ...raw, + version: TASK_GROUPS_VERSION, + activeGroup, + groups, + }; + } + + private migrateItem(raw: unknown): TaskGroupItem[] { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []; + const source = { ...(raw as Record) }; + const kind = ( + source.kind === 'preset' + || source.kind === 'template' + || source.kind === 'daily' + ) + ? source.kind + : 'plan'; + const pathValue = typeof source.path === 'string' && source.path.trim() + ? source.path + : undefined; + const managedFile = typeof source.managedFile === 'string' && source.managedFile.trim() + ? source.managedFile + : undefined; + const managedSource = source.managedSource === 'system' || source.managedSource === 'user' + ? source.managedSource + : this.inferManagedSource(pathValue); + const dailySource = source.dailySource === 'system' || source.dailySource === 'user' + ? source.dailySource + : undefined; + const dailyFile = typeof source.dailyFile === 'string' && source.dailyFile.trim() + ? source.dailyFile + : undefined; + const inferredFile = managedFile ?? this.inferManagedFile(pathValue); + const migratedFile = managedSource === 'system' && inferredFile + ? LEGACY_SYSTEM_PLAN_FILES[inferredFile] ?? inferredFile + : inferredFile; + const label = typeof source.label === 'string' && source.label.trim() + ? source.label + : (migratedFile ?? pathValue ?? '任务').split(/[\\/]/).pop()?.replace(/\.ya?ml$/i, '') ?? '任务'; + return [{ + ...source, + kind, + path: pathValue, + managedSource, + managedFile: migratedFile, + dailySource, + dailyFile, + times: typeof source.times === 'number' && source.times > 0 ? source.times : 1, + label, + } as TaskGroupItem]; + } + + private inferManagedSource(value: string | undefined): PlanPresetSource | undefined { + if (!value) return undefined; + if (/system_battle_plans|builtin_plans|system[\\/]/i.test(value)) return 'system'; + if (/user_battle_plans|plans[\\/]|user[\\/]/i.test(value)) return 'user'; + return undefined; + } + + private inferManagedFile(value: string | undefined): string | undefined { + if (!value || !/\.ya?ml$/i.test(value)) return undefined; + const source = this.inferManagedSource(value); + if (!source) return undefined; + const file = value.split(/[\\/]/).pop(); + return file && file.trim() ? file : undefined; + } + /** 保存到文件 */ - async save(): Promise { + async save(): Promise { try { - const bridge = (window as any).electronBridge; - if (!bridge?.saveFile) return; - await bridge.saveFile(STORAGE_FILE, JSON.stringify(this.data, null, 2)); + await rendererFileRepository.saveFile(STORAGE_FILE, jsonCodec.stringify(this.data)); Logger.debug(`任务组已保存: ${this.data.groups.length} 个组`); + return true; } catch (e) { Logger.error(`保存任务组失败: ${e instanceof Error ? e.message : String(e)}`); + return false; } } diff --git a/src/model/TemplateModel.ts b/src/model/TemplateModel.ts index 7aa0400..3fc6e77 100644 --- a/src/model/TemplateModel.ts +++ b/src/model/TemplateModel.ts @@ -1,4 +1,6 @@ -import type { TaskTemplate } from '../types/model'; +/** 管理内置与用户模板,负责校验、CRUD 和持久化。 */ +import { jsonCodec, type FileRepository } from '../adapter/index.js'; +import type { TaskTemplate, TemplateType } from '../types/model.js'; const FILE_PATH = 'templates/templates.json'; const BUILTIN_PATH = 'resource/builtin_templates.json'; @@ -8,12 +10,17 @@ function generateId(): string { return `tpl_${Date.now()}_${++idCounter}`; } -/** 通过 IPC 桥读写文件的接口 */ -interface FileIO { - readFile: (path: string) => Promise; - saveFile: (path: string, content: string) => Promise; +function isTemplateType(value: unknown): value is TemplateType { + return value === 'normal_fight' + || value === 'event_fight' + || value === 'exercise' + || value === 'campaign' + || value === 'decisive'; } +/** 通过 IPC 桥读写文件的接口 */ +type FileIO = Pick; + export class TemplateModel { private builtinTemplates: TaskTemplate[] = []; private userTemplates: TaskTemplate[] = []; @@ -75,13 +82,13 @@ export class TemplateModel { } /** 更新模板字段(内置模板不可更新) */ - async update(id: string, fields: Partial>): Promise { - if (this.isBuiltin(id)) return; + async update(id: string, fields: Partial>): Promise { + if (this.isBuiltin(id)) return false; const tpl = this.userTemplates.find(t => t.id === id); - if (tpl) { - Object.assign(tpl, fields); - await this.save(); - } + if (!tpl) return false; + Object.assign(tpl, fields); + await this.save(); + return true; } /** 从 JSON 数组导入模板,自动分配新 id */ @@ -90,14 +97,16 @@ export class TemplateModel { for (const item of raw) { if (!item || typeof item !== 'object') continue; const rec = item as Record; - if (!rec.name || !rec.type) continue; + if (typeof rec.name !== 'string' || !isTemplateType(rec.type)) continue; const tpl: TaskTemplate = { - ...(rec as any), + ...(rec as Partial), + name: rec.name, + type: rec.type, id: generateId(), createdAt: new Date().toISOString(), }; // 导入的模板始终为用户模板 - delete (tpl as any).builtin; + delete tpl.builtin; this.userTemplates.push(tpl); count++; } @@ -107,8 +116,8 @@ export class TemplateModel { /** 持久化用户模板到本地文件 */ private async save(): Promise { - if (!this.io) return; - await this.io.saveFile(FILE_PATH, JSON.stringify(this.userTemplates, null, 2)); + if (!this.io) throw new Error('模板存储尚未初始化'); + await this.io.saveFile(FILE_PATH, jsonCodec.stringify(this.userTemplates)); } /** 从本地文件加载用户模板 */ @@ -117,7 +126,7 @@ export class TemplateModel { try { const raw = await this.io.readFile(FILE_PATH); if (raw) { - this.userTemplates = JSON.parse(raw); + this.userTemplates = jsonCodec.parse(raw); return; } } catch { /* 文件不存在 */ } @@ -125,7 +134,7 @@ export class TemplateModel { try { const raw = await this.io.readFile('templates.json'); if (raw) { - this.userTemplates = JSON.parse(raw); + this.userTemplates = jsonCodec.parse(raw); await this.save(); // 保存到新路径 } } catch { /* 旧文件也不存在,使用空列表 */ } @@ -137,7 +146,7 @@ export class TemplateModel { try { const raw = await this.io.readFile(BUILTIN_PATH); if (raw) { - const arr = JSON.parse(raw) as TaskTemplate[]; + const arr = jsonCodec.parse(raw); // 确保内置标记 this.builtinTemplates = arr.map(t => ({ ...t, builtin: true })); } diff --git a/src/model/fleet/DecisiveFleetDraft.ts b/src/model/fleet/DecisiveFleetDraft.ts new file mode 100644 index 0000000..6d0b60f --- /dev/null +++ b/src/model/fleet/DecisiveFleetDraft.ts @@ -0,0 +1,210 @@ +/** 维护决战舰队独立草稿及决战配置转换规则。 */ +import type { DecisivePlanSettings } from '../../types/ipc.js'; + +export type DecisiveFleetLevel = 'level1' | 'level2'; + +export interface DecisiveFleetPosition { + level: DecisiveFleetLevel; + index: number; +} + +export const DEFAULT_DECISIVE_PLAN_SETTINGS: DecisivePlanSettings = { + chapter: 6, + useQuickRepair: true, + level1: [ + 'U-47', + 'U-1405', + 'U-1206', + 'U-2540', + 'U-81', + 'U-96', + ], + level2: [ + 'U-505', + '射水鱼', + '大青花鱼', + 'M-296', + '鹦鹉螺', + 'S-49', + 'IIIA', + 'K-21', + 'U-441', + '潜甲', + '潜乙', + '伊-201', + '伊-25', + '鲃鱼', + '伊-400', + '激流', + 'U-4501', + 'U-459', + 'U-14', + 'U-35', + 'K1', + ], +}; + +export function cloneDecisiveFleetDraft( + settings: DecisivePlanSettings, +): DecisivePlanSettings { + return { + chapter: settings.chapter, + useQuickRepair: settings.useQuickRepair, + level1: [...settings.level1], + level2: [...settings.level2], + }; +} + +export function decisiveFleetDraftSnapshot( + settings: DecisivePlanSettings, +): string { + return JSON.stringify({ + chapter: settings.chapter, + useQuickRepair: settings.useQuickRepair, + level1: [...settings.level1], + level2: [...settings.level2], + }); +} + +export function hasDecisiveFleetDraftChanges( + settings: DecisivePlanSettings, + savedSnapshot: string, +): boolean { + return decisiveFleetDraftSnapshot(settings) !== savedSnapshot; +} + +export class DecisiveFleetDraft { + private settings: DecisivePlanSettings; + private savedSnapshot: string; + + constructor( + settings: DecisivePlanSettings = DEFAULT_DECISIVE_PLAN_SETTINGS, + ) { + this.settings = cloneDecisiveFleetDraft(settings); + this.savedSnapshot = decisiveFleetDraftSnapshot(this.settings); + } + + get chapter(): number { + return this.settings.chapter; + } + + get useQuickRepair(): boolean { + return this.settings.useQuickRepair; + } + + get dirty(): boolean { + return hasDecisiveFleetDraftChanges( + this.settings, + this.savedSnapshot, + ); + } + + setChapter(chapter: number): void { + this.settings.chapter = chapter; + } + + setUseQuickRepair(useQuickRepair: boolean): void { + this.settings.useQuickRepair = useQuickRepair; + } + + queue(level: DecisiveFleetLevel): readonly string[] { + return this.settings[level]; + } + + find(name: string): DecisiveFleetPosition | null { + for (const level of ['level1', 'level2'] as const) { + const index = this.settings[level].indexOf(name); + if (index >= 0) return { level, index }; + } + return null; + } + + place( + name: string, + level: DecisiveFleetLevel, + requestedIndex: number, + maxIndex: number, + ): number { + const queue = this.settings[level]; + let target = Math.min(Math.max(0, requestedIndex), maxIndex); + if (target < queue.length) { + queue[target] = name; + } else { + target = Math.min(target, queue.length); + queue.splice(target, 0, name); + } + return target; + } + + remove(level: DecisiveFleetLevel, index: number): boolean { + const queue = this.settings[level]; + if (!Number.isInteger(index) || index < 0 || index >= queue.length) { + return false; + } + queue.splice(index, 1); + return true; + } + + move( + sourceLevel: DecisiveFleetLevel, + sourceIndex: number, + targetLevel: DecisiveFleetLevel, + targetIndex: number, + ): number | null { + const sourceQueue = this.settings[sourceLevel]; + const targetQueue = this.settings[targetLevel]; + const sourceName = sourceQueue[sourceIndex]; + if (!sourceName) return null; + + if (sourceLevel === targetLevel) { + if (sourceIndex === targetIndex) return null; + if (targetIndex < targetQueue.length) { + [targetQueue[sourceIndex], targetQueue[targetIndex]] = [ + targetQueue[targetIndex], + sourceName, + ]; + } else { + sourceQueue.splice(sourceIndex, 1); + targetQueue.push(sourceName); + targetIndex = targetQueue.length - 1; + } + } else { + const targetName = targetQueue[targetIndex]; + if (targetName) { + sourceQueue[sourceIndex] = targetName; + targetQueue[targetIndex] = sourceName; + } else { + sourceQueue.splice(sourceIndex, 1); + targetQueue.splice( + Math.min(targetIndex, targetQueue.length), + 0, + sourceName, + ); + targetIndex = Math.min(targetIndex, targetQueue.length - 1); + } + } + + return targetIndex; + } + + resetTeams( + defaults: Pick = + DEFAULT_DECISIVE_PLAN_SETTINGS, + ): void { + this.settings.level1 = [ + ...defaults.level1, + ]; + this.settings.level2 = [ + ...defaults.level2, + ]; + } + + load(settings: DecisivePlanSettings): void { + this.settings = cloneDecisiveFleetDraft(settings); + this.savedSnapshot = decisiveFleetDraftSnapshot(this.settings); + } + + toSettings(): DecisivePlanSettings { + return cloneDecisiveFleetDraft(this.settings); + } +} diff --git a/src/model/fleet/FleetDraft.ts b/src/model/fleet/FleetDraft.ts new file mode 100644 index 0000000..0f2302b --- /dev/null +++ b/src/model/fleet/FleetDraft.ts @@ -0,0 +1,593 @@ +/** 维护普通舰队槽位、候选舰和拖拽编辑的唯一草稿。 */ +import type { + PlanPresetSource, + ShipLibraryShip, + UserTeamPlan, + UserTeamPlanSlot, + UserTeamShipRule, +} from '../../types/ipc.js'; +import { + FLEET_SHIP_TYPE_CODES, +} from '../../shared/fleetShipTypes.js'; + +const DEFAULT_BACKUP_SLOT_COUNT = 6; +const FLEET_SLOT_COUNT = 6; +const ALLOWED_FLEET_SHIP_TYPES = new Set(FLEET_SHIP_TYPE_CODES); + +export interface FleetRuleDraft { + shipTypes: string[]; + levelEnabled: boolean; + minLevel: number | null; + maxLevel: number | null; +} + +export interface FleetCandidateDraft extends FleetRuleDraft { + ship: ShipLibraryShip | null; +} + +export interface FleetSlotDraft extends FleetRuleDraft { + primary: ShipLibraryShip | null; + candidates: FleetCandidateDraft[]; +} + +export interface FleetDraft { + name: string; + file: string | null; + source: PlanPresetSource; + slots: FleetSlotDraft[]; +} + +export type BackupFollowMode = 'ship' | 'position'; + +/** + * 执行槽位重排。位置跟随模式只移动主选及其规则,备选队列保留在原位置。 + */ +export function reorderFleetSlots( + slots: FleetSlotDraft[], + mode: BackupFollowMode, + reorder: () => T, +): T { + if (mode === 'ship') return reorder(); + + const candidatesByPosition = slots.map(slot => slot.candidates); + try { + return reorder(); + } finally { + slots.forEach((slot, index) => { + slot.candidates = candidatesByPosition[index]; + }); + } +} + +export function isFleetSlotEmpty(slot: FleetSlotDraft): boolean { + return slot.primary === null + && slot.candidates.every(candidate => candidate.ship === null); +} + +export function hasOtherPrimaryShip( + slots: FleetSlotDraft[], + searchName: string, + targetPosition: number, +): boolean { + return slots.some((slot, position) => ( + position !== targetPosition + && slot.primary?.search_name === searchName + )); +} + +/** + * 槽位真正为空时整体左移。纯备选槽位也属于已占用槽位。 + */ +export function compactFleetDraftSlots(slots: FleetSlotDraft[]): void { + const occupied = slots.filter(slot => !isFleetSlotEmpty(slot)); + slots.splice( + 0, + slots.length, + ...occupied, + ...Array.from( + { length: Math.max(0, FLEET_SLOT_COUNT - occupied.length) }, + createFleetSlotDraft, + ), + ); +} + +export function resolveFleetSlotPosition( + slots: FleetSlotDraft[], + focusedSlot: FleetSlotDraft, + fallbackPosition: number, +): number { + const position = slots.indexOf(focusedSlot); + return position >= 0 ? position : fallbackPosition; +} + +/** + * 删除主选并压缩空槽。删除其他位置时,焦点继续跟随原来选中的槽位。 + */ +export function removeFleetPrimary( + slots: FleetSlotDraft[], + removedPosition: number, + activePosition: number, +): number { + const removed = slots[removedPosition]; + if (!removed?.primary) return activePosition; + + const focused = slots[activePosition]; + const deletingFocused = removedPosition === activePosition; + removed.primary = null; + copyFleetRule(removed, createFleetRuleDraft()); + compactFleetDraftSlots(slots); + + const preferred = deletingFocused ? removed : focused; + return resolveFleetSlotPosition( + slots, + preferred, + Math.min(Math.max(0, removedPosition), FLEET_SLOT_COUNT - 1), + ); +} + +export interface GalleryFormationAssignment { + targetPosition: number; + activePosition: number; +} + +/** + * 图鉴点击只在最右侧连续新增时右移焦点,替换已有舰船时保留焦点。 + */ +export function resolveGalleryFormationAssignment( + slots: FleetSlotDraft[], + activePosition: number, + searchName: string, +): GalleryFormationAssignment { + const currentPosition = Math.min( + Math.max(0, activePosition), + FLEET_SLOT_COUNT - 1, + ); + const existingPosition = slots.findIndex( + slot => slot.primary?.search_name === searchName, + ); + if (existingPosition >= 0) { + return { + targetPosition: existingPosition, + activePosition: currentPosition, + }; + } + + const current = slots[currentPosition]; + if (current && isFleetSlotEmpty(current)) { + const firstEmpty = slots.findIndex(isFleetSlotEmpty); + const targetPosition = firstEmpty >= 0 + ? firstEmpty + : currentPosition; + const nextPosition = targetPosition + 1; + const canAdvance = nextPosition < FLEET_SLOT_COUNT + && slots.slice(nextPosition).every(isFleetSlotEmpty); + return { + targetPosition, + activePosition: canAdvance ? nextPosition : targetPosition, + }; + } + const nextPosition = currentPosition + 1; + const canAdvance = nextPosition < FLEET_SLOT_COUNT + && slots.slice(nextPosition).every(isFleetSlotEmpty); + if (canAdvance) { + return { + targetPosition: nextPosition, + activePosition: nextPosition, + }; + } + return { + targetPosition: currentPosition, + activePosition: currentPosition, + }; +} + +/** 图鉴拖入时优先填补目标左侧的第一个空位。 */ +export function resolveGalleryFormationDropTarget( + slots: FleetSlotDraft[], + targetPosition: number, +): number { + const boundedTarget = Math.min( + Math.max(0, targetPosition), + FLEET_SLOT_COUNT - 1, + ); + const firstEmpty = slots.findIndex(isFleetSlotEmpty); + return firstEmpty >= 0 && firstEmpty < boundedTarget + ? firstEmpty + : boundedTarget; +} + +/** + * 将图鉴舰船插入编队目标位,目标位及右侧整体后移。 + * 编队没有可用空位时不覆盖已有内容。 + */ +export function insertFleetPrimary( + slots: FleetSlotDraft[], + targetPosition: number, + inserted: FleetSlotDraft, + mode: BackupFollowMode, +): FleetSlotDraft | null { + if ( + targetPosition < 0 + || targetPosition >= slots.length + || isFleetSlotEmpty(inserted) + ) { + return null; + } + const emptyPosition = slots.findIndex(( + slot, + position, + ) => position >= targetPosition && isFleetSlotEmpty(slot)); + if (emptyPosition < 0) return null; + + return reorderFleetSlots(slots, mode, () => { + slots.splice(emptyPosition, 1); + slots.splice(targetPosition, 0, inserted); + return inserted; + }); +} + +/** + * 将图鉴舰船插入备选目标位,保留左侧顺序并压缩空槽。 + */ +export function insertFleetCandidate( + candidates: FleetCandidateDraft[], + targetIndex: number, + inserted: FleetCandidateDraft, +): number { + if ( + targetIndex < 0 + || targetIndex >= candidates.length + || !inserted.ship + ) { + return -1; + } + const emptyPosition = candidates.findIndex(( + candidate, + index, + ) => index >= targetIndex && candidate.ship === null); + if (emptyPosition >= 0) candidates.splice(emptyPosition, 1); + candidates.splice(targetIndex, 0, inserted); + + const occupied = candidates.filter(candidate => candidate.ship !== null); + const slotCount = Math.max(DEFAULT_BACKUP_SLOT_COUNT, occupied.length); + candidates.splice( + 0, + candidates.length, + ...occupied, + ...Array.from( + { length: slotCount - occupied.length }, + createFleetCandidateDraft, + ), + ); + return candidates.indexOf(inserted); +} + +/** + * 移动编队卡片。位置模式拖动纯备选卡时交换完整位置。 + */ +export function moveFleetPrimary( + slots: FleetSlotDraft[], + sourcePosition: number, + targetPosition: number, + mode: BackupFollowMode, +): FleetSlotDraft | null { + const source = slots[sourcePosition]; + const target = slots[targetPosition]; + if (!source || !target || isFleetSlotEmpty(source)) return null; + if (sourcePosition === targetPosition) return source; + + if (!source.primary) { + if (mode !== 'position') return null; + if (!isFleetSlotEmpty(target)) { + [slots[sourcePosition], slots[targetPosition]] = [ + target, + source, + ]; + return source; + } + + slots.splice(sourcePosition, 1); + const firstEmpty = slots.findIndex(isFleetSlotEmpty); + slots.splice(firstEmpty < 0 ? slots.length : firstEmpty, 0, source); + return source; + } + + if ( + mode === 'position' + && !target.primary + && !isFleetSlotEmpty(target) + ) { + target.primary = source.primary; + copyFleetRule(target, source); + source.primary = null; + copyFleetRule(source, createFleetRuleDraft()); + if (isFleetSlotEmpty(source)) compactFleetDraftSlots(slots); + return target; + } + + return reorderFleetSlots(slots, mode, () => { + if (!isFleetSlotEmpty(target)) { + [slots[sourcePosition], slots[targetPosition]] = [ + target, + source, + ]; + return source; + } + + slots.splice(sourcePosition, 1); + const firstEmpty = slots.findIndex(isFleetSlotEmpty); + slots.splice(firstEmpty < 0 ? slots.length : firstEmpty, 0, source); + return source; + }); +} + +export function createFleetRuleDraft(): FleetRuleDraft { + return { + shipTypes: [], + levelEnabled: false, + minLevel: null, + maxLevel: null, + }; +} + +export function createFleetCandidateDraft( + ship: ShipLibraryShip | null = null, +): FleetCandidateDraft { + return { + ship, + ...createFleetRuleDraft(), + }; +} + +export function createFleetSlotDraft(): FleetSlotDraft { + return { + primary: null, + candidates: Array.from( + { length: DEFAULT_BACKUP_SLOT_COUNT }, + () => createFleetCandidateDraft(), + ), + ...createFleetRuleDraft(), + }; +} + +export function createFleetDraft(): FleetDraft { + return { + name: '', + file: null, + source: 'user', + slots: Array.from( + { length: FLEET_SLOT_COUNT }, + createFleetSlotDraft, + ), + }; +} + +export function cloneFleetRule(source: FleetRuleDraft): FleetRuleDraft { + return { + shipTypes: [...source.shipTypes], + levelEnabled: source.levelEnabled, + minLevel: source.minLevel, + maxLevel: source.maxLevel, + }; +} + +export function copyFleetRule( + target: FleetRuleDraft, + source: FleetRuleDraft, +): void { + target.shipTypes = [...source.shipTypes]; + target.levelEnabled = source.levelEnabled; + target.minLevel = source.minLevel; + target.maxLevel = source.maxLevel; +} + +export function fleetDraftSnapshot(draft: FleetDraft): string { + const ruleSnapshot = (rule: FleetRuleDraft) => ({ + shipTypes: [...rule.shipTypes], + levelEnabled: rule.levelEnabled, + minLevel: rule.minLevel, + maxLevel: rule.maxLevel, + }); + return JSON.stringify({ + name: draft.name, + slots: draft.slots.map(slot => ({ + primary: slot.primary?.id ?? null, + rule: ruleSnapshot(slot), + candidates: slot.candidates.map(candidate => ({ + ship: candidate.ship?.id ?? null, + rule: ruleSnapshot(candidate), + })), + })), + }); +} + +export function hasFleetDraftChanges( + draft: FleetDraft, + savedSnapshot: string, +): boolean { + return fleetDraftSnapshot(draft) !== savedSnapshot; +} + +function teamRuleConstraintsFromDraft( + rule: FleetRuleDraft, + field: string, +): Omit { + const invalidShipType = rule.shipTypes.find( + shipType => !ALLOWED_FLEET_SHIP_TYPES.has(shipType), + ); + if (invalidShipType) { + throw new Error(`${field} 的舰种不符合后端接口:${invalidShipType}`); + } + if (rule.levelEnabled) { + if ( + rule.minLevel !== null + && (!Number.isInteger(rule.minLevel) || rule.minLevel < 1) + ) { + throw new Error(`${field} 的最小等级不合法`); + } + if ( + rule.maxLevel !== null + && (!Number.isInteger(rule.maxLevel) || rule.maxLevel < 1) + ) { + throw new Error(`${field} 的最大等级不合法`); + } + if ( + rule.minLevel !== null + && rule.maxLevel !== null + && rule.maxLevel < rule.minLevel + ) { + throw new Error(`${field} 的最大等级不能小于最小等级`); + } + } + + const result: Omit = {}; + if (rule.shipTypes.length > 0) { + result.ship_type = [...rule.shipTypes]; + } + if (rule.levelEnabled && rule.minLevel !== null) { + result.min_level = rule.minLevel; + } + if (rule.levelEnabled && rule.maxLevel !== null) { + result.max_level = rule.maxLevel; + } + return result; +} + +function teamShipRuleFromDraft( + ship: ShipLibraryShip, + rule: FleetRuleDraft, + field: string, +): UserTeamShipRule { + const result: UserTeamShipRule = { + name: ship.name, + ...teamRuleConstraintsFromDraft(rule, field), + }; + if (ship.search_name && ship.search_name !== ship.name) { + result.search_name = ship.search_name; + } + return result; +} + +function teamPlanSlotFromDraft( + slot: FleetSlotDraft, + index: number, +): UserTeamPlanSlot { + const backups = slot.candidates.filter( + (candidate): candidate is FleetCandidateDraft & { + ship: ShipLibraryShip; + } => candidate.ship !== null, + ); + const result: UserTeamPlanSlot = slot.primary + ? teamShipRuleFromDraft( + slot.primary, + slot, + `位置 ${index + 1} 主选`, + ) + : teamRuleConstraintsFromDraft(slot, `位置 ${index + 1}`); + if (backups.length > 0) { + result.candidates = backups.map((candidate, candidateIndex) => ( + teamShipRuleFromDraft( + candidate.ship, + candidate, + `位置 ${index + 1} 备选 ${candidateIndex + 1}`, + ) + )); + } + return result; +} + +/** 将编辑草稿转换为持久化计划,并在模型边界校验后端规则。 */ +export function fleetDraftToTeamPlan( + draft: FleetDraft, + rawName: string, +): UserTeamPlan { + const name = rawName.trim(); + if (!name) throw new Error('请输入舰队预设名称'); + + const occupiedSlots = draft.slots.filter( + slot => !isFleetSlotEmpty(slot), + ); + if (occupiedSlots.length === 0) { + throw new Error('当前编队至少需要一艘主选或备选舰船'); + } + return { + name, + ships: occupiedSlots.map(teamPlanSlotFromDraft), + }; +} + +function findTeamPlanShip( + rule: UserTeamShipRule, + ships: readonly ShipLibraryShip[], +): ShipLibraryShip { + const ship = ships.find(item => item.name === rule.name) + ?? ships.find(item => item.search_name === rule.search_name); + if (!ship) throw new Error(`舰船不存在: ${rule.name}`); + return ship; +} + +function fleetCandidateFromTeamRule( + rule: UserTeamShipRule, + ships: readonly ShipLibraryShip[], +): FleetCandidateDraft { + return { + ship: findTeamPlanShip(rule, ships), + shipTypes: [...(rule.ship_type ?? [])], + levelEnabled: ( + rule.min_level !== undefined || rule.max_level !== undefined + ), + minLevel: rule.min_level ?? null, + maxLevel: rule.max_level ?? null, + }; +} + +function fleetSlotFromTeamPlan( + slot: UserTeamPlanSlot, + ships: readonly ShipLibraryShip[], +): FleetSlotDraft { + const primary = slot.name + ? findTeamPlanShip({ + name: slot.name, + search_name: slot.search_name, + ship_type: slot.ship_type, + min_level: slot.min_level, + max_level: slot.max_level, + }, ships) + : null; + const backups = (slot.candidates ?? []).map(candidate => ( + fleetCandidateFromTeamRule(candidate, ships) + )); + return { + primary, + candidates: [ + ...backups, + ...Array.from( + { length: Math.max(0, DEFAULT_BACKUP_SLOT_COUNT - backups.length) }, + () => createFleetCandidateDraft(), + ), + ], + shipTypes: [...(slot.ship_type ?? [])], + levelEnabled: ( + slot.min_level !== undefined || slot.max_level !== undefined + ), + minLevel: slot.min_level ?? null, + maxLevel: slot.max_level ?? null, + }; +} + +/** 将持久化计划恢复为编辑草稿,文件身份仅保留在模型中。 */ +export function fleetDraftFromTeamPlan( + plan: UserTeamPlan, + ships: readonly ShipLibraryShip[], +): FleetDraft { + const slots = plan.ships.map(slot => fleetSlotFromTeamPlan(slot, ships)); + while (slots.length < FLEET_SLOT_COUNT) { + slots.push(createFleetSlotDraft()); + } + return { + name: plan.name, + file: plan.file ?? null, + source: plan.source ?? 'user', + slots: slots.slice(0, FLEET_SLOT_COUNT), + }; +} diff --git a/src/model/fleet/FleetPresetIdentity.ts b/src/model/fleet/FleetPresetIdentity.ts new file mode 100644 index 0000000..df7ec79 --- /dev/null +++ b/src/model/fleet/FleetPresetIdentity.ts @@ -0,0 +1,67 @@ +/** 统一编队规则格式,并生成规则标识和“名称 + 规则”的预设身份标识。 */ +import type { + FleetPreset, + ShipFilter, + ShipRule, + ShipSlot, +} from '../../types/model.js'; + +function normalizeText(value: string | undefined): string | null { + const normalized = value?.trim(); + return normalized || null; +} + +function normalizeShipTypes(values: string[] | undefined): string[] | null { + if (!values || values.length === 0) return null; + const normalized = [...new Set( + values.map(value => value.trim()).filter(Boolean), + )].sort(); + return normalized.length > 0 ? normalized : null; +} + +function normalizeRule(rule: ShipRule): Record { + return { + name: rule.name.trim(), + search_name: normalizeText(rule.search_name), + ship_type: normalizeShipTypes(rule.ship_type), + min_level: rule.min_level ?? null, + max_level: rule.max_level ?? null, + }; +} + +function normalizeSlot(slot: ShipSlot): unknown { + if (slot === null) return null; + if (typeof slot === 'string') { + return normalizeFilter({ name: slot }); + } + return normalizeFilter(slot); +} + +function normalizeFilter(filter: ShipFilter): Record { + return { + name: normalizeText(filter.name), + candidates: filter.candidates?.length + ? filter.candidates.map(normalizeRule) + : null, + search_name: normalizeText(filter.search_name), + nation: normalizeText(filter.nation), + ship_type: normalizeShipTypes(filter.ship_type), + min_level: filter.min_level ?? null, + max_level: filter.max_level ?? null, + }; +} + +export function fleetPresetRuleKey( + preset: Pick, +): string { + return JSON.stringify(preset.ships.map(normalizeSlot)); +} + +export function fleetPresetIdentityKey( + preset: Pick, +): string { + return JSON.stringify([ + preset.name.trim(), + fleetPresetRuleKey(preset), + ]); +} diff --git a/src/model/fleet/FleetRuleMapper.ts b/src/model/fleet/FleetRuleMapper.ts new file mode 100644 index 0000000..aa5a56f --- /dev/null +++ b/src/model/fleet/FleetRuleMapper.ts @@ -0,0 +1,43 @@ +/** 在舰队槽位规则和后端 fleet_rules 请求之间转换。 */ +import type { FleetRuleReq, FleetShipRuleReq } from '../../types/api.js'; +import type { ShipRule, ShipSlot } from '../../types/model.js'; +import { buildShipCandidates } from './ShipMatcher.js'; +import { toBackendName } from './ShipNameNormalizer.js'; + +function toFleetShipRule(rule: ShipRule): FleetShipRuleReq { + const result: FleetShipRuleReq = { name: rule.name }; + if (rule.search_name) result.search_name = rule.search_name; + if (rule.ship_type) result.ship_type = [...rule.ship_type]; + if (rule.min_level != null && Number.isFinite(rule.min_level)) result.min_level = Math.max(1, Math.floor(rule.min_level)); + if (rule.max_level != null && Number.isFinite(rule.max_level)) result.max_level = Math.max(1, Math.floor(rule.max_level)); + return result; +} + +export function resolveFleetPresetRules(ships: ShipSlot[]): Array { + const rules: Array = []; + const reserved: string[] = []; + for (const slot of ships) { + if (slot === null) continue; + if (typeof slot === 'string') { + const name = toBackendName(slot); + rules.push(name); + reserved.push(name); + continue; + } + const candidates = buildShipCandidates(slot, reserved); + if (candidates.length === 0) continue; + const candidateOnly = !slot.name && (slot.candidates?.length ?? 0) > 0; + const resolvedPrimary = candidateOnly ? undefined : slot.name ?? candidates[0]; + const rule: FleetRuleReq = {}; + if (resolvedPrimary) rule.name = resolvedPrimary; + const searchName = slot.search_name ?? resolvedPrimary; + if (searchName?.trim()) rule.search_name = searchName.trim(); + if (slot.ship_type) rule.ship_type = [...slot.ship_type]; + if (slot.min_level != null && Number.isFinite(slot.min_level)) rule.min_level = Math.max(1, Math.floor(slot.min_level)); + if (slot.max_level != null && Number.isFinite(slot.max_level)) rule.max_level = Math.max(1, Math.floor(slot.max_level)); + if (slot.candidates?.length) rule.candidates = slot.candidates.map(toFleetShipRule); + rules.push(rule); + reserved.push(candidates[0]); + } + return rules; +} diff --git a/src/model/fleet/ShipCatalog.ts b/src/model/fleet/ShipCatalog.ts new file mode 100644 index 0000000..aa7924b --- /dev/null +++ b/src/model/fleet/ShipCatalog.ts @@ -0,0 +1,20 @@ +/** 加载静态舰船资料并提供舰名和国籍只读目录。 */ +import rawShips from '../../data/ship_details.json'; + +export interface ShipInfo { + name: string; + nation: string; + ship_type: string; +} + +export const ALL_SHIPS: ShipInfo[] = rawShips.map(ship => ({ + name: ship.name, + nation: ship.nation, + ship_type: ship.ship_type, +})); + +export const ALL_NATIONS: string[] = (() => { + const priority = ['中国', '日本', '德国', '美国', '英国', '苏联', '法国', '意大利']; + const all = [...new Set(ALL_SHIPS.map(ship => ship.nation))]; + return [...priority.filter(nation => all.includes(nation)), ...all.filter(nation => !priority.includes(nation))]; +})(); diff --git a/src/model/fleet/ShipMatcher.ts b/src/model/fleet/ShipMatcher.ts new file mode 100644 index 0000000..06a706b --- /dev/null +++ b/src/model/fleet/ShipMatcher.ts @@ -0,0 +1,87 @@ +/** 解析舰队预设、匹配舰船规则并生成槽位显示文本。 */ +import type { ShipFilter, ShipSlot } from '../../types/model.js'; +import { shipTypeLabel } from '../../shared/fleetShipTypes.js'; +import { ALL_SHIPS, type ShipInfo } from './ShipCatalog.js'; +import { toBackendName } from './ShipNameNormalizer.js'; + +function dedupeNames(names: string[]): string[] { + const seen = new Set(); + return names.filter(name => name && !seen.has(name) && seen.add(name)); +} + +function matchShipType(filter: ShipFilter, shipType: string): boolean { + if (!filter.ship_type || filter.ship_type.length === 0) return true; + return filter.ship_type.some(rule => rule === 'ss_or_ssg' + ? shipType === 'ss' || shipType === 'ssg' + : shipType === rule); +} + +function matchesFilter(filter: ShipFilter, ship: ShipInfo): boolean { + if (filter.nation && ship.nation !== filter.nation) return false; + if (!matchShipType(filter, ship.ship_type)) return false; + return !filter.name || toBackendName(ship.name) === toBackendName(filter.name); +} + +export function buildShipCandidates(filter: ShipFilter, exclude: string[] = []): string[] { + const excluded = new Set(exclude.map(toBackendName)); + const matched = ALL_SHIPS.filter(ship => matchesFilter(filter, ship)); + const refit = matched.filter(ship => ship.name.endsWith('·改')).map(ship => toBackendName(ship.name)); + const normal = matched.filter(ship => !ship.name.endsWith('·改')).map(ship => toBackendName(ship.name)); + const candidates = dedupeNames([...refit, ...normal]).filter(name => !excluded.has(name)); + if (candidates.length === 0 && filter.name) { + const normalized = toBackendName(filter.name); + if (!excluded.has(normalized)) candidates.push(normalized); + } + return candidates; +} + +export function resolveShipFilter(filter: ShipFilter, exclude: string[] = []): string | null { + return buildShipCandidates(filter, exclude)[0] ?? null; +} + +export function resolveFleetPreset(ships: ShipSlot[]): string[] { + const resolved: string[] = []; + for (const slot of ships) { + if (slot === null) continue; + if (typeof slot === 'string') resolved.push(toBackendName(slot)); + else { + const name = resolveShipFilter(slot, resolved); + if (name) resolved.push(name); + } + } + return resolved; +} + +export function shipFilterLabel(filter: ShipFilter): string { + const parts: string[] = []; + if (filter.name) parts.push(filter.name); + else if (filter.search_name) parts.push(filter.search_name); + if (filter.nation) parts.push(filter.nation); + if (filter.ship_type) { + parts.push(filter.ship_type.map(shipTypeLabel).join('/')); + } + if (filter.candidates?.length) { + parts.push( + `备选:${filter.candidates.map(rule => rule.name).join(' > ')}`, + ); + } + if (filter.min_level != null || filter.max_level != null) { + if (filter.min_level != null && filter.max_level != null) { + parts.push(`Lv${filter.min_level}-${filter.max_level}`); + } else if (filter.min_level != null) { + parts.push(`Lv>=${filter.min_level}`); + } else if (filter.max_level != null) { + parts.push(`Lv<=${filter.max_level}`); + } + } + return parts.join(' ') || '任意舰船'; +} + +export function isShipFilter(slot: ShipSlot): slot is ShipFilter { + return typeof slot === 'object' && slot !== null; +} + +export function shipSlotLabel(slot: ShipSlot): string { + if (slot === null) return '空'; + return isShipFilter(slot) ? shipFilterLabel(slot) : slot; +} diff --git a/src/model/fleet/ShipNameNormalizer.ts b/src/model/fleet/ShipNameNormalizer.ts new file mode 100644 index 0000000..8d646d8 --- /dev/null +++ b/src/model/fleet/ShipNameNormalizer.ts @@ -0,0 +1,5 @@ +/** 统一舰名别名、后端标准名和搜索名转换。 */ +export function toBackendName(displayName: string): string { + const noRefit = displayName.endsWith('·改') ? displayName.slice(0, -2) : displayName; + return noRefit.replace(/\s*[((][^()()]*[))]\s*$/, '').trim(); +} diff --git a/src/model/fleet/index.ts b/src/model/fleet/index.ts new file mode 100644 index 0000000..2bbfb07 --- /dev/null +++ b/src/model/fleet/index.ts @@ -0,0 +1,8 @@ +/** 导出舰队草稿、匹配、名称和规则映射等领域能力。 */ +export * from './ShipCatalog.js'; +export * from './ShipNameNormalizer.js'; +export * from './ShipMatcher.js'; +export * from './FleetPresetIdentity.js'; +export * from './FleetRuleMapper.js'; +export * from './FleetDraft.js'; +export * from './DecisiveFleetDraft.js'; diff --git a/src/model/scheduler/CronScheduler.ts b/src/model/scheduler/CronScheduler.ts index 4453e24..d194472 100644 --- a/src/model/scheduler/CronScheduler.ts +++ b/src/model/scheduler/CronScheduler.ts @@ -1,3 +1,10 @@ +/** 维护定时任务触发器、最后执行时间和 pending 状态。 */ +import { browserStorageStore, type StorageStore } from '../../adapter/index.js'; +import type { LootPlanSource } from '../../shared/lootPlans.js'; +import type { + DecisiveAutomationSource, +} from '../../shared/decisiveAutomation.js'; + /** * CronScheduler —— 基于系统时钟的定时任务调度器。 * @@ -8,7 +15,7 @@ * - 支持 YAML 中 scheduled_time 定时触发 * * 核心机制: - * 通过 localStorage 记录演习/战役任务的【实际完成】时间戳, + * 通过注入的持久化存储记录演习/战役任务的【实际完成】时间戳, * 而非记录"是否已触发"。这样即使 App 因 ADB 断开等原因重启, * 只要任务未真正完成、时间戳就不更新,下次启动后仍会补发任务。 */ @@ -30,10 +37,16 @@ export interface CronConfig { battleTimes: number; /** 启用自动常规出击(每日执行任务列表) */ autoNormalFight: boolean; + /** 启用每日自动决战 */ + autoDecisive: boolean; + /** 自动决战使用计划页方案或系统预设 */ + decisiveTemplateId: DecisiveAutomationSource; /** 启用每日自动刷战利品 */ autoLoot: boolean; - /** 战利品方案索引 (builtin_farm_loot.planPaths) */ - lootPlanIndex: number; + /** 战利品受管计划来源。 */ + lootPlanSource: LootPlanSource; + /** 战利品受管计划文件名,不依赖列表顺序。 */ + lootPlanId: string; /** 战利品停止数量 */ lootStopCount: number; } @@ -46,8 +59,14 @@ export interface CronCallbacks { onCampaignDue?: (campaignName: string, times: number) => void; /** 请求执行任务列表中所有任务 */ onNormalFightDue?: () => void; + /** 请求按指定来源添加一轮决战任务 */ + onDecisiveDue?: (source: DecisiveAutomationSource) => void; /** 请求添加战利品任务 */ - onLootDue?: (planIndex: number, stopCount: number) => void; + onLootDue?: ( + source: LootPlanSource, + planId: string, + stopCount: number, + ) => void; /** 请求添加定时方案任务 */ onScheduledTaskDue?: (taskKey: string) => void; /** 日志 */ @@ -67,10 +86,11 @@ export interface ScheduledTask { // 演习刷新时间点 (小时) const EXERCISE_REFRESH_HOURS = [0, 12, 18]; -/** localStorage key — 记录任务实际完成时间 */ +/** 持久化 key — 记录任务实际完成时间 */ const LS_KEY_LAST_EXERCISE_RUN = 'cron_lastExerciseRun'; // ISO 时间戳 const LS_KEY_LAST_BATTLE_RUN = 'cron_lastBattleRun'; // YYYY-MM-DD const LS_KEY_LAST_NORMAL_FIGHT_RUN = 'cron_lastNormalFightRun'; // YYYY-MM-DD +const LS_KEY_LAST_DECISIVE_RUN = 'cron_lastDecisiveRun'; // YYYY-MM-DD const LS_KEY_LAST_LOOT_RUN = 'cron_lastLootRun'; // YYYY-MM-DD // ════════════════════════════════════════ @@ -81,6 +101,7 @@ export class CronScheduler { private config: CronConfig; private callbacks: CronCallbacks = {}; private timer: ReturnType | null = null; + private storage: StorageStore; /** 上一次演习任务实际完成的时间 */ private lastExerciseRun: Date | null = null; @@ -97,15 +118,22 @@ export class CronScheduler { private lastNormalFightRun = ''; private normalFightPending = false; private normalFightPendingDate = ''; + /** 上一次自动决战实际处理的日期 (YYYY-MM-DD) */ + private lastDecisiveRun = ''; + private decisivePending = false; + private decisivePendingDate = ''; /** 上一次战利品任务实际完成的日期 (YYYY-MM-DD) */ private lastLootRun = ''; private lootPending = false; private lootPendingDate = ''; /** 注册的定时方案任务 */ private scheduledTasks: ScheduledTask[] = []; + /** 上次检查定时方案标记时的日期 */ + private scheduledTaskDate = ''; - constructor(config: CronConfig) { + constructor(config: CronConfig, storage: StorageStore = browserStorageStore) { this.config = { ...config }; + this.storage = storage; } setCallbacks(cb: CronCallbacks): void { @@ -121,7 +149,11 @@ export class CronScheduler { start(): void { this.stop(); this.loadTimestamps(); - this.log('info', `定时调度配置: 演习=${this.config.autoExercise}, 战役=${this.config.autoBattle}, 常规出击=${this.config.autoNormalFight}`); + this.log( + 'info', + `定时调度配置: 演习=${this.config.autoExercise}, 战役=${this.config.autoBattle}, ` + + `常规出击=${this.config.autoNormalFight}, 决战=${this.config.autoDecisive}`, + ); if (this.lastExerciseRun) { this.log('info', `上次演习完成: ${this.lastExerciseRun.toLocaleString()}`); } @@ -149,17 +181,30 @@ export class CronScheduler { this.lastExerciseRun = new Date(); this.exercisePending = false; try { - localStorage.setItem(LS_KEY_LAST_EXERCISE_RUN, this.lastExerciseRun.toISOString()); + this.storage.set(LS_KEY_LAST_EXERCISE_RUN, this.lastExerciseRun.toISOString()); } catch { /* ignore */ } this.log('info', '演习任务完成,已记录运行时间'); } + /** 演习任务被用户取消,本刷新时段不再重复触发。 */ + markExerciseHandled(): void { + this.lastExerciseRun = new Date(); + this.exercisePending = false; + try { + this.storage.set( + LS_KEY_LAST_EXERCISE_RUN, + this.lastExerciseRun.toISOString(), + ); + } catch { /* ignore */ } + this.log('info', '演习任务已取消,本刷新时段不再重复触发'); + } + /** Controller 在战役任务成功完成后调用 */ markBattleCompleted(): void { this.lastBattleRun = this.dateKey(new Date()); this.battlePending = false; try { - localStorage.setItem(LS_KEY_LAST_BATTLE_RUN, this.lastBattleRun); + this.storage.set(LS_KEY_LAST_BATTLE_RUN, this.lastBattleRun); } catch { /* ignore */ } this.log('info', '战役任务完成,已记录运行时间'); } @@ -174,7 +219,7 @@ export class CronScheduler { this.lastBattleRun = this.dateKey(new Date()); this.battlePending = false; try { - localStorage.setItem(LS_KEY_LAST_BATTLE_RUN, this.lastBattleRun); + this.storage.set(LS_KEY_LAST_BATTLE_RUN, this.lastBattleRun); } catch { /* ignore */ } this.log('info', '战役任务已处理,今日不再重复触发'); } @@ -194,7 +239,7 @@ export class CronScheduler { this.lastNormalFightRun = this.dateKey(new Date()); this.normalFightPending = false; try { - localStorage.setItem(LS_KEY_LAST_NORMAL_FIGHT_RUN, this.lastNormalFightRun); + this.storage.set(LS_KEY_LAST_NORMAL_FIGHT_RUN, this.lastNormalFightRun); } catch { /* ignore */ } this.log('info', '自动常规出击完成,已记录运行时间'); } @@ -204,7 +249,7 @@ export class CronScheduler { this.lastNormalFightRun = this.dateKey(new Date()); this.normalFightPending = false; try { - localStorage.setItem(LS_KEY_LAST_NORMAL_FIGHT_RUN, this.lastNormalFightRun); + this.storage.set(LS_KEY_LAST_NORMAL_FIGHT_RUN, this.lastNormalFightRun); } catch { /* ignore */ } } @@ -213,12 +258,40 @@ export class CronScheduler { this.normalFightPending = false; } + /** 自动决战成功完成,记录今日执行状态。 */ + markDecisiveCompleted(): void { + this.lastDecisiveRun = this.dateKey(new Date()); + this.decisivePending = false; + try { + this.storage.set(LS_KEY_LAST_DECISIVE_RUN, this.lastDecisiveRun); + } catch { /* ignore */ } + this.log('info', '自动决战完成,已记录运行时间'); + } + + /** + * 自动决战已结束,无论成功与否今日均不重复。 + * 决战可能在失败前消耗票数,禁止同日盲目重试。 + */ + markDecisiveHandled(): void { + this.lastDecisiveRun = this.dateKey(new Date()); + this.decisivePending = false; + try { + this.storage.set(LS_KEY_LAST_DECISIVE_RUN, this.lastDecisiveRun); + } catch { /* ignore */ } + this.log('info', '自动决战已处理,今日不再重复触发'); + } + + /** 决战任务尚未入队时清除 pending,允许下次 tick 重试。 */ + clearDecisivePending(): void { + this.decisivePending = false; + } + /** Controller 在战利品任务完成后调用 */ markLootCompleted(): void { this.lastLootRun = this.dateKey(new Date()); this.lootPending = false; try { - localStorage.setItem(LS_KEY_LAST_LOOT_RUN, this.lastLootRun); + this.storage.set(LS_KEY_LAST_LOOT_RUN, this.lastLootRun); } catch { /* ignore */ } this.log('info', '自动战利品任务完成,已记录运行时间'); } @@ -228,7 +301,7 @@ export class CronScheduler { this.lastLootRun = this.dateKey(new Date()); this.lootPending = false; try { - localStorage.setItem(LS_KEY_LAST_LOOT_RUN, this.lastLootRun); + this.storage.set(LS_KEY_LAST_LOOT_RUN, this.lastLootRun); } catch { /* ignore */ } } @@ -239,17 +312,18 @@ export class CronScheduler { // ── 持久化 ── - /** 从 localStorage 加载上次运行时间戳 */ + /** 从持久化存储加载上次运行时间戳 */ private loadTimestamps(): void { try { - const exRaw = localStorage.getItem(LS_KEY_LAST_EXERCISE_RUN); + const exRaw = this.storage.get(LS_KEY_LAST_EXERCISE_RUN); if (exRaw) { const d = new Date(exRaw); if (!isNaN(d.getTime())) this.lastExerciseRun = d; } - this.lastBattleRun = localStorage.getItem(LS_KEY_LAST_BATTLE_RUN) || ''; - this.lastNormalFightRun = localStorage.getItem(LS_KEY_LAST_NORMAL_FIGHT_RUN) || ''; - this.lastLootRun = localStorage.getItem(LS_KEY_LAST_LOOT_RUN) || ''; + this.lastBattleRun = this.storage.get(LS_KEY_LAST_BATTLE_RUN) || ''; + this.lastNormalFightRun = this.storage.get(LS_KEY_LAST_NORMAL_FIGHT_RUN) || ''; + this.lastDecisiveRun = this.storage.get(LS_KEY_LAST_DECISIVE_RUN) || ''; + this.lastLootRun = this.storage.get(LS_KEY_LAST_LOOT_RUN) || ''; } catch { /* ignore */ } } @@ -283,12 +357,13 @@ export class CronScheduler { private tick(): void { const now = new Date(); + this.resetDailyFlags(now); this.checkExercise(now); this.checkCampaign(now); this.checkNormalFight(now); + this.checkDecisive(now); this.checkLoot(now); this.checkScheduledTasks(now); - this.resetDailyFlags(now); } /** @@ -369,6 +444,31 @@ export class CronScheduler { this.callbacks.onNormalFightDue?.(); } + /** + * 检查决战: + * 每日最多加入一轮。无法查询剩余票数,因此不推导票数保留或执行轮数。 + */ + private checkDecisive(now: Date): void { + if (!this.config.autoDecisive) return; + + const todayStr = this.dateKey(now); + if ( + this.decisivePending + && this.decisivePendingDate === todayStr + ) { + return; + } + if (this.lastDecisiveRun >= todayStr) return; + + this.decisivePending = true; + this.decisivePendingDate = todayStr; + this.log( + 'info', + `自动决战触发 (方案=${this.config.decisiveTemplateId}, 轮数=1)`, + ); + this.callbacks.onDecisiveDue?.(this.config.decisiveTemplateId); + } + /** * 检查战利品: * 每日 0 点刷新。若 lastLootRun 不是今天则触发。 @@ -386,8 +486,15 @@ export class CronScheduler { this.lootPending = true; this.lootPendingDate = todayStr; - this.log('info', `自动战利品触发 (方案#${this.config.lootPlanIndex}, 停止数量=${this.config.lootStopCount})`); - this.callbacks.onLootDue?.(this.config.lootPlanIndex, this.config.lootStopCount); + this.log( + 'info', + `自动战利品触发 (来源=${this.config.lootPlanSource}, 方案=${this.config.lootPlanId}, 停止数量=${this.config.lootStopCount})`, + ); + this.callbacks.onLootDue?.( + this.config.lootPlanSource, + this.config.lootPlanId, + this.config.lootStopCount, + ); } /** 检查定时方案任务 */ @@ -406,11 +513,16 @@ export class CronScheduler { /** 跨日重置: 日期变化时清除 firedToday 标记 */ private resetDailyFlags(now: Date): void { - // 重置定时方案的 firedToday (0:00 附近) - if (now.getHours() === 0 && now.getMinutes() === 0) { - for (const task of this.scheduledTasks) { - task.firedToday = false; - } + const today = this.dateKey(now); + if (!this.scheduledTaskDate) { + this.scheduledTaskDate = today; + return; + } + if (this.scheduledTaskDate === today) return; + + this.scheduledTaskDate = today; + for (const task of this.scheduledTasks) { + task.firedToday = false; } } diff --git a/src/model/scheduler/ExpeditionTimer.ts b/src/model/scheduler/ExpeditionTimer.ts index ced3ef1..23c5293 100644 --- a/src/model/scheduler/ExpeditionTimer.ts +++ b/src/model/scheduler/ExpeditionTimer.ts @@ -1,3 +1,4 @@ +/** 根据远征状态计算并发布下一次收取倒计时。 */ /** * ExpeditionTimer — 远征定时器。 * 从 Scheduler 中拆出,负责远征检查的定时触发和倒计时。 diff --git a/src/model/scheduler/RepairManager.ts b/src/model/scheduler/RepairManager.ts index cd0bf0d..6339dce 100644 --- a/src/model/scheduler/RepairManager.ts +++ b/src/model/scheduler/RepairManager.ts @@ -1,3 +1,4 @@ +/** 跟踪泡澡舰船并编排修理、替换和状态恢复。 */ /** * RepairManager —— 泡澡修理管理器 * @@ -13,10 +14,15 @@ */ import type { ApiClient } from '../ApiClient'; -import type { ShipData } from '../../types/api'; -import type { BathRepairConfig, RepairThreshold, ShipSlot } from '../../types/model'; +import type { ShipData } from '../../types/api.js'; +import type { BathRepairConfig, RepairThreshold, ShipSlot } from '../../types/model.js'; import { Logger } from '../../utils/Logger'; -import { toBackendName } from '../../data/shipData'; +import { + browserStorageStore, + jsonCodec, + type StorageStore, +} from '../../adapter/index.js'; +import { toBackendName } from '../fleet/index.js'; /** 正在泡澡的舰船记录 */ export interface BathingShip { @@ -44,10 +50,12 @@ export class RepairManager { private api: ApiClient; /** 正在泡澡的舰船列表 (舰船名 → 记录) */ private bathingShips: Map = new Map(); + private storage: StorageStore; private static readonly STORAGE_KEY = 'autowsgr_bathing_ships'; - constructor(api: ApiClient) { + constructor(api: ApiClient, storage: StorageStore = browserStorageStore) { this.api = api; + this.storage = storage; this.restoreFromStorage(); } @@ -167,7 +175,7 @@ export class RepairManager { const entry = this.bathingShips.get(key); if (!entry) continue; - const rawSeconds: unknown = (resp.data as any)?.repair_seconds; + const rawSeconds: unknown = resp.data?.repair_seconds; const validatedSeconds = RepairManager.validateRepairSeconds(rawSeconds); if (validatedSeconds > 0) { @@ -276,7 +284,7 @@ export class RepairManager { } /** - * 将 bathingShips 持久化到 localStorage。 + * 将 bathingShips 写入持久化存储。 * 仅保存尚未完成的维修记录。 */ private saveToStorage(): void { @@ -296,9 +304,9 @@ export class RepairManager { })); if (data.length === 0) { - localStorage.removeItem(RepairManager.STORAGE_KEY); + this.storage.remove(RepairManager.STORAGE_KEY); } else { - localStorage.setItem(RepairManager.STORAGE_KEY, JSON.stringify(data)); + this.storage.set(RepairManager.STORAGE_KEY, jsonCodec.stringify(data)); } } catch (e) { Logger.warn(`保存泡澡状态失败: ${e}`, 'repair'); @@ -306,12 +314,12 @@ export class RepairManager { } /** - * 从 localStorage 恢复泡澡状态。 + * 从持久化存储恢复泡澡状态。 * 在构造函数中调用,用于 GUI 重启后恢复维修记录。 */ private restoreFromStorage(): void { try { - const raw = localStorage.getItem(RepairManager.STORAGE_KEY); + const raw = this.storage.get(RepairManager.STORAGE_KEY); if (!raw) return; const data: Array<{ @@ -320,11 +328,11 @@ export class RepairManager { startTime: number; repairEndTime: number; requestSent: boolean; - }> = JSON.parse(raw); + }> = jsonCodec.parse(raw); if (!Array.isArray(data)) { Logger.warn('泡澡状态数据格式异常,已清空', 'repair'); - localStorage.removeItem(RepairManager.STORAGE_KEY); + this.storage.remove(RepairManager.STORAGE_KEY); return; } @@ -353,7 +361,7 @@ export class RepairManager { } catch (e) { Logger.warn(`恢复泡澡状态失败,已清空: ${e}`, 'repair'); this.bathingShips.clear(); - localStorage.removeItem(RepairManager.STORAGE_KEY); + this.storage.remove(RepairManager.STORAGE_KEY); } } } diff --git a/src/model/scheduler/Scheduler.ts b/src/model/scheduler/Scheduler.ts index 7c21db0..a439010 100644 --- a/src/model/scheduler/Scheduler.ts +++ b/src/model/scheduler/Scheduler.ts @@ -1,3 +1,4 @@ +/** 持有当前任务和运行状态,驱动任务消费、重试与后续任务。 */ /** * Scheduler —— 前端任务调度器 (Model 层)。 * @@ -15,21 +16,47 @@ import { ApiClient } from '../ApiClient'; import type { TaskRequest, TaskResult, -} from '../../types/api'; -import type { StopCondition, BathRepairConfig, FleetPreset } from '../../types/model'; + RoundResult, +} from '../../types/api.js'; +import type { + StopCondition, + BathRepairConfig, + FleetPreset, + BattleResultGrade, +} from '../../types/model.js'; import { Logger } from '../../utils/Logger'; +import { jsonCodec } from '../../adapter/index.js'; import { RepairManager } from './RepairManager'; import { StopConditionChecker } from './StopConditionChecker'; import { ExpeditionTimer } from './ExpeditionTimer'; import { TaskQueue, generateTaskId, parseUiCount } from './TaskQueue'; -import { TaskPriority, type SchedulerTaskType, type SchedulerTask, type SchedulerStatus, type SchedulerCallbacks } from '../../types/scheduler'; -import { toBackendName } from '../../data/shipData'; +import { + TaskPriority, + type LogicalTaskCancelReason, + type SchedulerTaskType, + type SchedulerTask, + type SchedulerStatus, + type SchedulerCallbacks, + type SchedulerWaitingTask, +} from '../../types/scheduler'; +import { toBackendName } from '../fleet/index.js'; +import { buildFollowUpTask, createSchedulerTask } from './SchedulerTaskPolicy.js'; + +const RESULT_GRADE_ORDER: BattleResultGrade[] = ['D', 'C', 'B', 'A', 'S', 'SS']; // ════════════════════════════════════════ // Scheduler 实现 // ════════════════════════════════════════ const DEFAULT_EXPEDITION_INTERVAL_MS = 15 * 60 * 1000; // 15 分钟 +const STOP_CONFIRM_POLL_INTERVAL_MS = 250; +const STOP_CONFIRM_TIMEOUT_MS = 30_000; + +type StopRequestReason = 'manual' | 'condition'; + +interface WaitingTaskEntry extends SchedulerWaitingTask { + timer: ReturnType; +} export class Scheduler { private api: ApiClient; @@ -41,9 +68,15 @@ export class Scheduler { // ── 状态 ── private _status: SchedulerStatus = 'not_connected'; - private connected = false; - /** 用户主动停止标志,阻止 handleTaskFinished 创建后续任务 */ - private _stopped = false; + private systemActive = false; + private autoExpedition = true; + /** 当前停止请求的来源,避免把停止条件误当作用户暂停。 */ + private stopRequest: { + taskId: string; + reason: StopRequestReason; + } | null = null; + /** 失败重试和轮次间隔中的任务,保留任务身份并允许取消。 */ + private waitingTasks = new Map(); // ── 远征定时器 & 停止条件 ── private expeditionTimer: ExpeditionTimer; @@ -76,6 +109,16 @@ export class Scheduler { this.expeditionTimer.setInterval(clamped * 60 * 1000); } + /** 开关自动远征检查;运行中修改时立即启停定时器 */ + setAutoExpedition(enabled: boolean): void { + this.autoExpedition = enabled; + if (!enabled) { + this.expeditionTimer.stop(); + } else if (this.systemActive && !this.expeditionTimer.isRunning) { + this.expeditionTimer.start(); + } + } + get status(): SchedulerStatus { return this._status; } @@ -88,23 +131,46 @@ export class Scheduler { return this._taskQueue.items; } + /** 获取轮次间隔或失败重试中的任务。 */ + get waitingTaskList(): ReadonlyArray { + return [...this.waitingTasks.values()] + .sort((left, right) => left.readyAt - right.readyAt) + .map(({ task, reason, readyAt }) => ({ + task, + reason, + readyAt, + })); + } + + /** 按物理任务 ID 查找运行中、排队中或等待中的任务。 */ + findTask(taskId: string): SchedulerTask | null { + if (this.currentTask?.id === taskId) return this.currentTask; + return this._taskQueue.findTask(taskId) + ?? this.waitingTasks.get(taskId)?.task + ?? null; + } + /** 启动系统 (连接模拟器 + 启动游戏) */ async start(configPath?: string): Promise { const resp = await this.api.systemStart(configPath, 300_000); if (!resp.success) return false; + this.systemActive = true; + this.stopRequest = null; this.api.connectWebSockets(); this.setStatus('idle'); - // 系统启动后立即检查远征,确保远征页面不会阻碍后续任务 - this.emitLog('info', '正在检查远征...'); - try { - await this.api.expeditionCheck(); - this.emitLog('info', '远征检查完成'); - } catch { - this.emitLog('debug', '远征检查跳过'); + if (this.autoExpedition) { + // 系统启动后立即检查远征,确保远征页面不会阻碍后续任务 + this.emitLog('info', '正在检查远征...'); + try { + await this.api.expeditionCheck(); + this.emitLog('info', '远征检查完成'); + } catch { + this.emitLog('debug', '远征检查跳过'); + } + this.expeditionTimer.start(); } - this.expeditionTimer.start(); return true; } @@ -123,23 +189,52 @@ export class Scheduler { * 建立 WebSocket、设置状态、启动远征检查。 */ recoverAfterTimeout(): void { + this.systemActive = true; + this.stopRequest = null; this.api.connectWebSockets(); this.setStatus('idle'); - this.expeditionTimer.start(); + if (this.autoExpedition) this.expeditionTimer.start(); } /** 停止系统 */ async stop(): Promise { + const hadRunningTask = this.currentTask !== null; + const canceledLogicalIds = this.collectLogicalIds([ + ...(this.currentTask ? [this.currentTask] : []), + ...this._taskQueue.items, + ...this._taskQueue.deferredItems, + ...this.waitingTaskList.map(item => item.task), + ]); + this.systemActive = false; + this.stopRequest = null; this.expeditionTimer.stop(); - if (this.currentTask) { - await this.api.taskStop(); - } - this._taskQueue.clearQueue(); + this.clearWaitingTasks(); + this._taskQueue.clear(); this.currentTask = null; - await this.api.systemStop(); - this.api.disconnectWebSockets(); - this.setStatus('not_connected'); - this.notifyQueueChange(); + this.emitLogicalCancellations( + canceledLogicalIds, + 'system_stopped', + ); + + try { + if (hadRunningTask) { + try { + await this.api.taskStop(); + } catch (e) { + this.emitLog('warn', `停止当前任务失败: ${String(e)}`); + } + } + try { + await this.api.systemStop(); + } catch (e) { + this.emitLog('warn', `停止后端系统失败: ${String(e)}`); + } + } finally { + this.api.disconnectWebSockets(); + this.stopRequest = null; + this.setStatus('not_connected'); + this.notifyQueueChange(); + } } /** 添加任务到队列 */ @@ -157,6 +252,7 @@ export class Scheduler { forceRetry?: boolean, allowPolling?: boolean, endpointNodes?: string[], + endpointResult?: BattleResultGrade, sortKey?: number, ): string { const id = this._taskQueue.addTask( @@ -173,6 +269,7 @@ export class Scheduler { forceRetry, allowPolling, endpointNodes, + endpointResult, sortKey, ); this.notifyQueueChange(); @@ -181,17 +278,26 @@ export class Scheduler { /** 手动开始消费队列 */ startConsuming(): void { - this._stopped = false; + if (!this.currentTask) this.stopRequest = null; if (this._status === 'idle' && !this.currentTask && this._taskQueue.length > 0) { this.consumeNext(); } } - /** 移除排队中的任务 (不能移除正在运行的) */ + /** 移除排队或等待中的任务(不能移除正在运行的任务)。 */ removeTask(taskId: string): boolean { - const removed = this._taskQueue.removeTask(taskId); - if (removed) this.notifyQueueChange(); - return removed; + const removed = this._taskQueue.removeTask(taskId) + ?? this.removeWaitingTask(taskId); + if (!removed) return false; + + if (!this.hasLogicalTask(removed.logicalId)) { + this.callbacks.onLogicalTaskCanceled?.( + removed.logicalId, + 'removed', + ); + } + this.notifyQueueChange(); + return true; } /** 请求停止当前正在运行的任务 */ @@ -203,8 +309,8 @@ export class Scheduler { } /** - * 立即停止当前任务,并将其恢复为“未开始执行”状态重新放回队列。 - * 用于用户手动点击“停止”后的可恢复场景。 + * 请求停止当前任务,等待后端 worker 退出后再放回队列。 + * WebSocket 完成事件和状态轮询任一确认停止即可结束等待。 */ async stopRunning(): Promise { Logger.debug(`stopRunning: currentTask=${this.currentTask?.name ?? 'null'} queueLen=${this._taskQueue.length}`, 'scheduler'); @@ -217,26 +323,90 @@ export class Scheduler { return; } - this._stopped = true; + this.stopRequest = { + taskId: runningTask.id, + reason: 'manual', + }; this.setStatus('stopping'); try { - await this.api.taskStop(); - } catch { - /* ignore */ + const response = await this.api.taskStop(); + if (this.currentTask?.id !== runningTask.id) return; + if (!response.success) { + this.stopRequest = null; + this.setStatus('running'); + throw new Error(response.error || '后端拒绝停止任务'); + } + } catch (error) { + if (this.currentTask?.id === runningTask.id) { + this.stopRequest = null; + this.setStatus('running'); + } + throw error; + } + + const deadline = Date.now() + STOP_CONFIRM_TIMEOUT_MS; + while (this.currentTask?.id === runningTask.id) { + try { + const response = await this.api.taskStatus(); + if ( + response.success + && response.data + && response.data.status !== 'running' + ) { + this.finishManualStop(runningTask); + return; + } + } catch { + // WebSocket 仍可能确认结束;轮询错误在超时前继续重试。 + } + + if (Date.now() >= deadline) { + this.stopRequest = null; + this.setStatus('running'); + throw new Error('停止请求已发送,但后端在 30 秒内未确认任务结束'); + } + await new Promise(resolve => { + setTimeout(resolve, STOP_CONFIRM_POLL_INTERVAL_MS); + }); } + } + + /** 后端确认退出后,将手动停止的任务恢复为未开始状态。 */ + private finishManualStop(runningTask: SchedulerTask): void { + if (this.currentTask?.id !== runningTask.id) return; - // 用户手动停止后,恢复为“未开始执行”状态放回队列。 runningTask.retryCount = 0; runningTask.backendTaskId = undefined; this.currentTask = null; this._taskQueue.insertByPriority(runningTask, !runningTask.allowPolling); - this._stopped = false; + this.stopRequest = null; this._taskQueue.clearDeferredTimer(); this.setStatus('idle'); this.notifyQueueChange(); } + /** 停止条件命中后结束父任务,不把当前轮重新放回队列。 */ + private finishStopCondition(runningTask: SchedulerTask): void { + if (this.currentTask?.id !== runningTask.id) return; + + runningTask.backendTaskId = undefined; + this.currentTask = null; + this.stopRequest = null; + this.emitLog( + 'info', + `任务「${runningTask.name}」满足停止条件,逻辑任务已结束`, + ); + this.callbacks.onLogicalTaskCompleted?.( + runningTask.logicalId, + true, + null, + ); + this.setStatus('idle'); + this.notifyQueueChange(); + this.consumeNext(); + } + /** 处理后端进程 stdout 日志行(用于解析 OCR 数据和触发停止条件) */ processBackendLog(message: string): void { const loot = parseUiCount(message, '战利品数量'); @@ -253,8 +423,18 @@ export class Scheduler { /** 清空队列 (不影响当前正在运行的) */ clearQueue(): void { + const canceledLogicalIds = this.collectLogicalIds([ + ...this._taskQueue.items, + ...this._taskQueue.deferredItems, + ...this.waitingTaskList.map(item => item.task), + ]); + this.clearWaitingTasks(); this._taskQueue.clear(); this.repairManager.clearAll(); + this.emitLogicalCancellations( + canceledLogicalIds, + 'queue_cleared', + ); this.notifyQueueChange(); } @@ -272,6 +452,7 @@ export class Scheduler { // ── 内部: 消费循环 ── private async consumeNext(): Promise { + if (!this.systemActive) return; if (this.currentTask) return; // 还有任务在跑 if (this._taskQueue.length === 0) { if (this._taskQueue.hasDeferredTasks) { @@ -292,7 +473,7 @@ export class Scheduler { this.setStatus('running'); this.notifyQueueChange(); - Logger.debug(`consumeNext: 「${task.name}」 type=${task.type} remaining=${task.remainingTimes}/${task.totalTimes} req=${JSON.stringify(task.request)}`, 'scheduler'); + Logger.debug(`consumeNext: 「${task.name}」 type=${task.type} remaining=${task.remainingTimes}/${task.totalTimes} req=${jsonCodec.stringify(task.request)}`, 'scheduler'); // 远征任务: 直接调用远征 API,不走 taskStart 流程 if (task.type === 'expedition') { @@ -304,8 +485,10 @@ export class Scheduler { this.emitLog('debug', '远征检查跳过'); } + if (!this.systemActive || this.currentTask?.id !== task.id) return; await this.handlePostExpedition(); + if (!this.systemActive || this.currentTask?.id !== task.id) return; this.currentTask = null; this.consumeNext(); return; @@ -314,9 +497,11 @@ export class Scheduler { // 发起前预检停止条件 if (task.stopCondition) { const preflightMet = await this.stopChecker.preflightCheck(task.stopCondition, task.name); + if (!this.systemActive || this.currentTask?.id !== task.id) return; if (preflightMet) { this.emitLog('info', `任务「${task.name}」启动前已满足停止条件,跳过`); this.callbacks.onTaskCompleted?.(task.id, true, null, null); + this.callbacks.onLogicalTaskCompleted?.(task.logicalId, true, null); this.currentTask = null; this.consumeNext(); return; @@ -326,6 +511,7 @@ export class Scheduler { // 泡澡修理编排: 检查 → 送泡澡 → 轮换预设 → 是否 defer if (task.bathRepairConfig?.enabled && task.fleetId) { const repairResult = await this.prepareRepair(task); + if (!this.systemActive || this.currentTask?.id !== task.id) return; if (repairResult === 'deferred') return; } @@ -355,6 +541,7 @@ export class Scheduler { if (healthyIdx >= 0) { this.emitLog('info', `任务「${task.name}」: 轮换至编队预设「${presets[healthyIdx].name}」`); this._taskQueue.switchTaskPreset(task, healthyIdx); + this.notifyQueueChange(); return 'proceed'; } this.emitLog('info', `任务「${task.name}」: 所有编队预设的舰船都在修理中,任务延迟`); @@ -374,18 +561,22 @@ export class Scheduler { private async executeTaskStart(task: SchedulerTask): Promise { try { const resp = await this.api.taskStart(task.request); + if (!this.systemActive || this.currentTask?.id !== task.id) return; if (resp.success && resp.data) { task.backendTaskId = resp.data.task_id; } else { this.currentTask = null; if (this.scheduleRetry(task, resp.error ?? '任务启动失败')) return; this.callbacks.onTaskCompleted?.(task.id, false, null, resp.error ?? '任务启动失败'); + this.callbacks.onLogicalTaskCompleted?.(task.logicalId, false, resp.error ?? '任务启动失败'); this.consumeNext(); } } catch (e) { + if (!this.systemActive || this.currentTask?.id !== task.id) return; this.currentTask = null; if (this.scheduleRetry(task, String(e))) return; this.callbacks.onTaskCompleted?.(task.id, false, null, String(e)); + this.callbacks.onLogicalTaskCompleted?.(task.logicalId, false, String(e)); this.consumeNext(); } } @@ -395,16 +586,17 @@ export class Scheduler { * @returns true 表示已安排重试,调用方应 return;false 表示重试耗尽。 */ private scheduleRetry(task: SchedulerTask, reason: string): boolean { + if (!this.systemActive) return false; if (task.retryCount >= task.maxRetries) return false; task.retryCount++; const retryHint = task.forceRetry ? ',强制重试' : ''; this.emitLog('warn', `任务「${task.name}」${reason},${task.retryCount}/${task.maxRetries} 次重试${retryHint} (5s 后)`); - setTimeout(() => { - const prioritizeCurrent = !!task.forceRetry || !task.allowPolling; - this._taskQueue.insertByPriority(task, prioritizeCurrent); - this.notifyQueueChange(); - this.consumeNext(); - }, 5000); + this.scheduleWaitingTask( + task, + 5000, + 'retry', + !!task.forceRetry || !task.allowPolling, + ); return true; } @@ -412,17 +604,22 @@ export class Scheduler { /** 任务完成后的后触发处理 */ private async handleTaskFinished(success: boolean, result?: TaskResult | null, error?: string | null): Promise { + if (!this.systemActive) return; const finished = this.currentTask; if (!finished) return; - // 用户主动停止后,不再创建后续任务 - if (this._stopped) { - Logger.debug(`handleTaskFinished: _stopped flag set, skipping follow-up for 「${finished.name}」`, 'scheduler'); - this._stopped = false; - this.callbacks.onTaskCompleted?.(finished.id, success, result, error); - this.currentTask = null; - this.setStatus('idle'); - this.notifyQueueChange(); + const stopReason = this.stopRequest?.taskId === finished.id + ? this.stopRequest.reason + : null; + + // 用户主动停止后保留原任务;停止条件命中则结束整个逻辑任务。 + if (stopReason === 'manual') { + Logger.debug(`handleTaskFinished: confirmed manual stop for 「${finished.name}」`, 'scheduler'); + this.finishManualStop(finished); + return; + } + if (stopReason === 'condition') { + this.finishStopCondition(finished); return; } @@ -432,10 +629,14 @@ export class Scheduler { this.currentTask = null; if (this.scheduleRetry(finished, '执行失败')) return; // 重试耗尽时,将失败轮计入次数并继续剩余轮次 - const nextRemaining = finished.remainingTimes - 1; + const nextRemaining = finished.unlimited + ? 1 + : finished.remainingTimes - 1; if (nextRemaining > 0) { const followUp = this.buildFollowUpTask(finished, nextRemaining); this._taskQueue.insertByPriority(followUp, !finished.allowPolling); + } else { + this.callbacks.onLogicalTaskCompleted?.(finished.logicalId, false, error); } this.consumeNext(); return; @@ -445,24 +646,34 @@ export class Scheduler { if (!shouldCountRound) { const endpoints = this.getEndpointNodes(finished); if (endpoints.length > 0) { - this.emitLog('info', `任务「${finished.name}」未到达终点节点 ${endpoints.join('/')},本轮不计入次数`); + const resultHint = finished.endpointResult + ? `或终点战果未达到 ${finished.endpointResult}` + : ''; + this.emitLog( + 'info', + `任务「${finished.name}」未到达终点节点 ${endpoints.join('/')}${resultHint},本轮不计入次数`, + ); } } this.callbacks.onTaskCompleted?.(finished.id, true, result, error); - const nextRemainingTimes = shouldCountRound - ? finished.remainingTimes - 1 - : finished.remainingTimes; + const nextRemainingTimes = finished.unlimited + ? 1 + : shouldCountRound + ? finished.remainingTimes - 1 + : finished.remainingTimes; // 后触发: 若还有剩余次数,追加一个新任务回队列。 // 注意: 未达到终点节点时,本轮不计数,remainingTimes 不减少。 if (nextRemainingTimes > 0) { if (finished.stopCondition) { const shouldStop = await this.stopChecker.checkCondition(finished.stopCondition, finished.name); + if (!this.systemActive || this.currentTask?.id !== finished.id) return; if (shouldStop) { this.emitLog('info', `任务「${finished.name}」满足停止条件,不再继续`); this.currentTask = null; + this.callbacks.onLogicalTaskCompleted?.(finished.logicalId, true, null); this.consumeNext(); return; } @@ -470,7 +681,16 @@ export class Scheduler { const followUp: SchedulerTask = this.buildFollowUpTask(finished, nextRemainingTimes); Logger.debug(`followUp: 「${finished.name}」 remaining=${followUp.remainingTimes}/${followUp.totalTimes}`, 'scheduler'); - this._taskQueue.insertByPriority(followUp, !finished.allowPolling); + const gapSeconds = 'gap' in finished.request + ? Math.max(0, Number(finished.request.gap) || 0) + : 0; + if (shouldCountRound && gapSeconds > 0) { + this.scheduleFollowUpAfterGap(followUp, gapSeconds); + } else { + this._taskQueue.insertByPriority(followUp, !finished.allowPolling); + } + } else { + this.callbacks.onLogicalTaskCompleted?.(finished.logicalId, true, null); } this.currentTask = null; @@ -480,12 +700,14 @@ export class Scheduler { private buildFollowUpTask(finished: SchedulerTask, remainingTimes: number): SchedulerTask { return { id: generateTaskId(), + logicalId: finished.logicalId, name: finished.name, type: finished.type, priority: finished.priority, request: finished.request, remainingTimes, totalTimes: finished.totalTimes, + unlimited: finished.unlimited, stopCondition: finished.stopCondition, maxRetries: finished.maxRetries, retryCount: 0, @@ -496,10 +718,73 @@ export class Scheduler { fleetPresets: finished.fleetPresets, currentPresetIndex: finished.currentPresetIndex, endpointNodes: finished.endpointNodes, + endpointResult: finished.endpointResult, sortKey: finished.sortKey, }; } + /** 按方案 gap 等待后再把下一轮放回队列。 */ + private scheduleFollowUpAfterGap( + task: SchedulerTask, + gapSeconds: number, + ): void { + this.emitLog( + 'info', + `任务「${task.name}」将在 ${gapSeconds} 秒后开始下一轮`, + ); + this.scheduleWaitingTask( + task, + gapSeconds * 1000, + 'gap', + !task.allowPolling, + ); + } + + /** 将任务保存为可见、可取消的等待项,时间到后再回到就绪队列。 */ + private scheduleWaitingTask( + task: SchedulerTask, + delayMs: number, + reason: SchedulerWaitingTask['reason'], + beforeSamePriority: boolean, + ): void { + this.removeWaitingTask(task.id); + const readyAt = Date.now() + delayMs; + const timer = setTimeout(() => { + const entry = this.waitingTasks.get(task.id); + if (!entry || entry.timer !== timer) return; + this.waitingTasks.delete(task.id); + if (!this.systemActive) return; + this._taskQueue.insertByPriority(task, beforeSamePriority); + this.notifyQueueChange(); + this.consumeNext(); + }, delayMs); + this.waitingTasks.set(task.id, { + task, + reason, + readyAt, + timer, + }); + if (!this.currentTask) this.setStatus('idle'); + this.notifyQueueChange(); + } + + /** 移除一个等待项并取消其定时器。 */ + private removeWaitingTask(taskId: string): SchedulerTask | null { + const entry = this.waitingTasks.get(taskId); + if (!entry) return null; + clearTimeout(entry.timer); + this.waitingTasks.delete(taskId); + return entry.task; + } + + /** 取消全部轮次间隔和失败重试定时器。 */ + private clearWaitingTasks(): void { + for (const entry of this.waitingTasks.values()) { + clearTimeout(entry.timer); + } + this.waitingTasks.clear(); + } + /** * 获取本轮认定完成的终点节点列表。 * 优先使用 task.endpointNodes(用户在 plan 中显式配置), @@ -524,7 +809,15 @@ export class Scheduler { if (endpoints.length === 0) return true; const details = result?.details; - if (!details || details.length === 0) return true; + if (!details || details.length === 0) return !task.endpointResult; + + if (task.endpointResult) { + return details.some((round) => this.roundMeetsEndpointResult( + round, + endpoints, + task.endpointResult!, + )); + } // 失败轮次保持原有行为(计入次数),避免在异常场景下无限重跑。 if (details.some((round) => !round.success)) return true; @@ -538,11 +831,114 @@ export class Scheduler { }); } + /** 检查指定终点的战果是否达到用户要求。 */ + private roundMeetsEndpointResult( + round: RoundResult, + endpoints: string[], + requiredResult: BattleResultGrade, + ): boolean { + const endpointFightEvents = (round.events ?? []).filter((event) => { + const node = String(event.node ?? '').trim().toUpperCase(); + return event.type === 'FIGHT_RESULT' && endpoints.includes(node); + }); + + if (endpointFightEvents.length > 0) { + return endpointFightEvents.some((event) => this.resultMeetsRequirement( + event.result, + requiredResult, + )); + } + + // 兼容不返回 events 的旧后端:仅当最后经过的节点是终点时使用 round.grade。 + const lastNode = round.nodes?.[round.nodes.length - 1]; + if (!lastNode || !endpoints.includes(String(lastNode).trim().toUpperCase())) { + return false; + } + return this.resultMeetsRequirement(round.grade, requiredResult); + } + + private resultMeetsRequirement( + actual: unknown, + required: BattleResultGrade, + ): boolean { + if (typeof actual !== 'string') return false; + const actualIndex = RESULT_GRADE_ORDER.indexOf( + actual.trim().toUpperCase() as BattleResultGrade, + ); + const requiredIndex = RESULT_GRADE_ORDER.indexOf(required); + return actualIndex >= requiredIndex; + } + /** 任务执行中实时检查停止条件,满足则立即发送 taskStop */ private checkAndStopRunningTask(cond: StopCondition): void { - if (this.stopChecker.checkRunning(cond)) { - this._stopped = true; - this.api.taskStop().catch(() => {}); + const runningTask = this.currentTask; + if (!runningTask || this.stopRequest) return; + if (!this.stopChecker.checkRunning(cond)) return; + + this.stopRequest = { + taskId: runningTask.id, + reason: 'condition', + }; + this.setStatus('stopping'); + void this.stopForCondition(runningTask); + } + + /** 请求停止满足条件的当前轮,并等待后端确认退出。 */ + private async stopForCondition(runningTask: SchedulerTask): Promise { + try { + const response = await this.api.taskStop(); + if ( + this.currentTask?.id !== runningTask.id + || this.stopRequest?.reason !== 'condition' + ) { + return; + } + if (!response.success) { + throw new Error(response.error || '后端拒绝停止任务'); + } + } catch (error) { + if (this.currentTask?.id === runningTask.id) { + this.stopRequest = null; + this.setStatus('running'); + this.emitLog( + 'warn', + `任务「${runningTask.name}」停止条件已满足,但停止失败: ${String(error)}`, + ); + } + return; + } + + const deadline = Date.now() + STOP_CONFIRM_TIMEOUT_MS; + while ( + this.currentTask?.id === runningTask.id + && this.stopRequest?.reason === 'condition' + ) { + try { + const response = await this.api.taskStatus(); + if ( + response.success + && response.data + && response.data.status !== 'running' + ) { + this.finishStopCondition(runningTask); + return; + } + } catch { + // WebSocket 仍可能确认结束;轮询错误在超时前继续重试。 + } + + if (Date.now() >= deadline) { + this.stopRequest = null; + this.setStatus('running'); + this.emitLog( + 'warn', + `任务「${runningTask.name}」停止条件已满足,但后端未确认停止`, + ); + return; + } + await new Promise(resolve => { + setTimeout(resolve, STOP_CONFIRM_POLL_INTERVAL_MS); + }); } } @@ -622,12 +1018,14 @@ export class Scheduler { /** 远征定时器回调 — 向队列插入远征任务,由调度器按优先级消费。 */ private handleExpeditionTrigger(): void { + if (!this.systemActive || !this.autoExpedition) return; if (this.currentTask?.type === 'expedition') return; if (this._taskQueue.hasType('expedition')) return; const id = generateTaskId(); const task: SchedulerTask = { id, + logicalId: id, name: '远征检查', type: 'expedition', priority: TaskPriority.EXPEDITION, @@ -675,7 +1073,6 @@ export class Scheduler { }, onWsStatusChange: (connected) => { - this.connected = connected; this.callbacks.onConnectionChange?.(connected); if (!connected && this._status !== 'not_connected') { // WebSocket 断开但系统可能还在运行,不改状态 @@ -703,6 +1100,39 @@ export class Scheduler { }); } + /** 判断同一父任务是否仍有运行、排队、延迟或等待中的物理任务。 */ + private hasLogicalTask(logicalId: string): boolean { + if (this.currentTask?.logicalId === logicalId) return true; + if (this._taskQueue.items.some(task => task.logicalId === logicalId)) { + return true; + } + if ( + this._taskQueue.deferredItems.some( + task => task.logicalId === logicalId, + ) + ) { + return true; + } + return [...this.waitingTasks.values()].some( + entry => entry.task.logicalId === logicalId, + ); + } + + private collectLogicalIds( + tasks: ReadonlyArray, + ): Set { + return new Set(tasks.map(task => task.logicalId)); + } + + private emitLogicalCancellations( + logicalIds: ReadonlySet, + reason: LogicalTaskCancelReason, + ): void { + for (const logicalId of logicalIds) { + this.callbacks.onLogicalTaskCanceled?.(logicalId, reason); + } + } + notifyQueueChange(): void { this.callbacks.onQueueChange?.(this._taskQueue.items); } diff --git a/src/model/scheduler/SchedulerRepairPolicy.ts b/src/model/scheduler/SchedulerRepairPolicy.ts new file mode 100644 index 0000000..34b640c --- /dev/null +++ b/src/model/scheduler/SchedulerRepairPolicy.ts @@ -0,0 +1,18 @@ +/** 提供舰船损伤阈值、替换候选和修理时长的纯规则。 */ +/** Pure scheduler repair waiting policy. */ + +export function calculateRepairWaitMs( + bathingShips?: ReadonlyMap, + now = Date.now(), +): number { + if (!bathingShips || bathingShips.size === 0) return -1; + + let minEndTime = Infinity; + for (const ship of bathingShips.values()) { + if (ship.repairEndTime > 0) minEndTime = Math.min(minEndTime, ship.repairEndTime); + } + + if (!Number.isFinite(minEndTime)) return -1; + if (minEndTime <= now) return 5_000; + return Math.min(minEndTime - now + 5_000, 30_000); +} diff --git a/src/model/scheduler/SchedulerTaskPolicy.ts b/src/model/scheduler/SchedulerTaskPolicy.ts new file mode 100644 index 0000000..f5e0522 --- /dev/null +++ b/src/model/scheduler/SchedulerTaskPolicy.ts @@ -0,0 +1,98 @@ +/** 提供任务完成、重试、轮询和后续任务构造的纯规则。 */ +/** Pure scheduler task construction and queue ordering policies. */ +import type { TaskRequest } from '../../types/api.js'; +import type { + StopCondition, + BathRepairConfig, + FleetPreset, + BattleResultGrade, +} from '../../types/model.js'; +import { TaskPriority, type SchedulerTaskType, type SchedulerTask } from '../../types/scheduler'; + +export function findPriorityInsertionIndex( + queue: ReadonlyArray, + task: SchedulerTask, + beforeSamePriority = false, +): number { + return queue.findIndex((current) => { + if (current.priority < task.priority) return false; + if (current.priority > task.priority) return true; + const currentKey = current.sortKey ?? Infinity; + const taskKey = task.sortKey ?? Infinity; + return beforeSamePriority ? currentKey >= taskKey : currentKey > taskKey; + }); +} + +export interface SchedulerTaskOptions { + name: string; + type: SchedulerTaskType; + request: TaskRequest; + priority?: TaskPriority; + times?: number; + stopCondition?: StopCondition; + bathRepairConfig?: BathRepairConfig; + fleetId?: number; + fleetPresets?: FleetPreset[]; + currentPresetIndex?: number; + forceRetry?: boolean; + allowPolling?: boolean; + endpointNodes?: string[]; + endpointResult?: BattleResultGrade; + sortKey?: number; + id: string; +} + +export function createSchedulerTask(options: SchedulerTaskOptions): SchedulerTask { + const times = options.times ?? 1; + const unlimited = !Number.isFinite(times); + const normalizedTimes = unlimited ? 1 : Math.max(1, Math.trunc(times)); + return { + id: options.id, + logicalId: options.id, + name: options.name, + type: options.type, + priority: options.priority ?? TaskPriority.USER_TASK, + request: options.request, + remainingTimes: normalizedTimes, + totalTimes: normalizedTimes, + unlimited, + stopCondition: options.stopCondition, + maxRetries: 2, + retryCount: 0, + forceRetry: options.forceRetry, + allowPolling: !!options.allowPolling, + bathRepairConfig: options.bathRepairConfig, + fleetId: options.fleetId, + fleetPresets: options.fleetPresets, + currentPresetIndex: options.currentPresetIndex ?? -1, + endpointNodes: options.endpointNodes, + endpointResult: options.endpointResult, + sortKey: options.sortKey, + }; +} + +export function buildFollowUpTask(task: SchedulerTask, remainingTimes: number, id: string): SchedulerTask { + return { + id, + logicalId: task.logicalId, + name: task.name, + type: task.type, + priority: task.priority, + request: task.request, + remainingTimes, + totalTimes: task.totalTimes, + unlimited: task.unlimited, + stopCondition: task.stopCondition, + maxRetries: task.maxRetries, + retryCount: 0, + forceRetry: task.forceRetry, + allowPolling: task.allowPolling, + bathRepairConfig: task.bathRepairConfig, + fleetId: task.fleetId, + fleetPresets: task.fleetPresets, + currentPresetIndex: task.currentPresetIndex, + endpointNodes: task.endpointNodes, + endpointResult: task.endpointResult, + sortKey: task.sortKey, + }; +} diff --git a/src/model/scheduler/StopConditionChecker.ts b/src/model/scheduler/StopConditionChecker.ts index cbf00b3..0e8ddf6 100644 --- a/src/model/scheduler/StopConditionChecker.ts +++ b/src/model/scheduler/StopConditionChecker.ts @@ -1,10 +1,12 @@ +/** 读取游戏统计并判断掉落、舰船上限等停止条件。 */ /** * StopConditionChecker — 停止条件检查器。 * 从 Scheduler 中拆出,负责判断任务是否满足停止条件。 */ import type { ApiClient } from '../ApiClient'; -import type { StopCondition } from '../../types/model'; +import type { StopCondition } from '../../types/model.js'; import { Logger } from '../../utils/Logger'; +import { jsonCodec } from '../../adapter/index.js'; export class StopConditionChecker { /** 从后端 [UI] 日志 OCR 中解析的战利品/舰船当前值 */ @@ -44,7 +46,7 @@ export class StopConditionChecker { * 仅依赖 OCR:调用 /api/game/acquisition 读取出征面板数量。 */ async preflightCheck(cond: StopCondition, taskName: string): Promise { - Logger.debug(`[StopCond] 预飞OCR检查: 「${taskName}」 条件=${JSON.stringify(cond)}`, 'scheduler'); + Logger.debug(`[StopCond] 预飞OCR检查: 「${taskName}」 条件=${jsonCodec.stringify(cond)}`, 'scheduler'); try { const resp = await this.api.gameAcquisition(); diff --git a/src/model/scheduler/TaskQueue.ts b/src/model/scheduler/TaskQueue.ts index 725e3b3..2a629fa 100644 --- a/src/model/scheduler/TaskQueue.ts +++ b/src/model/scheduler/TaskQueue.ts @@ -1,12 +1,20 @@ +/** 唯一持有就绪与延迟队列,并实现优先级和轮询排序。 */ /** * TaskQueue —— 优先级任务队列 + 延迟任务管理。 * 从 Scheduler.ts 拆出,封装队列数据结构及相关操作。 */ -import type { TaskRequest } from '../../types/api'; -import type { StopCondition, BathRepairConfig, FleetPreset } from '../../types/model'; +import type { TaskRequest } from '../../types/api.js'; +import type { + StopCondition, + BathRepairConfig, + FleetPreset, + BattleResultGrade, +} from '../../types/model.js'; import type { BathingShip } from './RepairManager'; import { TaskPriority, type SchedulerTaskType, type SchedulerTask } from '../../types/scheduler'; -import { resolveFleetPreset, resolveFleetPresetRules, toBackendName } from '../../data/shipData'; +import { resolveFleetPreset, resolveFleetPresetRules, toBackendName } from '../fleet/index.js'; +import { calculateRepairWaitMs } from './SchedulerRepairPolicy.js'; +import { createSchedulerTask, findPriorityInsertionIndex } from './SchedulerTaskPolicy.js'; // ════════════════════════════════════════ // ID 生成 & 辅助函数 @@ -72,17 +80,7 @@ export class TaskQueue { * beforeSamePriority=true 时,会插入到同优先级任务之前(用于重试/跟随)。 */ insertByPriority(task: SchedulerTask, beforeSamePriority = false): void { - const idx = this.queue.findIndex((t) => { - if (t.priority < task.priority) return false; - if (t.priority > task.priority) return true; - // 同优先级: 按 sortKey 排序 - const tKey = t.sortKey ?? Infinity; - const taskKey = task.sortKey ?? Infinity; - if (beforeSamePriority) { - return tKey >= taskKey; - } - return tKey > taskKey; - }); + const idx = findPriorityInsertionIndex(this.queue, task, beforeSamePriority); if (idx === -1) { this.queue.push(task); } else { @@ -105,39 +103,51 @@ export class TaskQueue { forceRetry?: boolean, allowPolling?: boolean, endpointNodes?: string[], + endpointResult?: BattleResultGrade, sortKey?: number, ): string { const id = generateTaskId(); - const task: SchedulerTask = { + const task = createSchedulerTask({ id, name, type, - priority, request, - remainingTimes: times, - totalTimes: times, + priority, + times, stopCondition, - maxRetries: 2, - retryCount: 0, - forceRetry, - allowPolling: !!allowPolling, bathRepairConfig, fleetId, fleetPresets, - currentPresetIndex: currentPresetIndex ?? -1, + currentPresetIndex, + forceRetry, + allowPolling, endpointNodes, + endpointResult, sortKey, - }; + }); this.insertByPriority(task); return id; } - /** 移除排队中的任务 */ - removeTask(taskId: string): boolean { + /** 查找就绪或修理延迟中的任务。 */ + findTask(taskId: string): SchedulerTask | null { + return this.queue.find(task => task.id === taskId) + ?? this.deferredTasks.find(task => task.id === taskId) + ?? null; + } + + /** 移除就绪或修理延迟中的任务,并返回被移除的任务。 */ + removeTask(taskId: string): SchedulerTask | null { const idx = this.queue.findIndex((t) => t.id === taskId); - if (idx === -1) return false; - this.queue.splice(idx, 1); - return true; + if (idx !== -1) { + return this.queue.splice(idx, 1)[0]; + } + + const deferredIdx = this.deferredTasks.findIndex( + task => task.id === taskId, + ); + if (deferredIdx === -1) return null; + return this.deferredTasks.splice(deferredIdx, 1)[0]; } /** 移动队列中的任务顺序 */ @@ -266,11 +276,15 @@ export class TaskQueue { const fleet = resolved.map(toBackendName); const fleetRules = resolveFleetPresetRules(preset.ships); if (req.plan) { - req.plan.fleet = fleet; - req.plan.fleet_rules = fleetRules; + if (fleet.length > 0) req.plan.fleet = fleet; + if (fleetRules.length > 0) req.plan.fleet_rules = fleetRules; req.plan.fleet_id = task.fleetId; } else { - (req as any).plan = { fleet, fleet_rules: fleetRules, fleet_id: task.fleetId }; + req.plan = { + ...(fleet.length > 0 ? { fleet } : {}), + ...(fleetRules.length > 0 ? { fleet_rules: fleetRules } : {}), + fleet_id: task.fleetId, + }; } if (req.type === 'event_fight') req.fleet_id = task.fleetId; } diff --git a/src/model/scheduler/index.ts b/src/model/scheduler/index.ts index f3055c8..429ef87 100644 --- a/src/model/scheduler/index.ts +++ b/src/model/scheduler/index.ts @@ -1,8 +1,11 @@ +/** 导出调度器、任务队列、定时器和修理管理器公共接口。 */ /** * scheduler/ 模块入口 —— barrel re-export。 * 外部统一从 '../model/scheduler' 导入,不需要关心内部拆分细节。 */ export { Scheduler } from './Scheduler'; +export * from './SchedulerTaskPolicy'; +export * from './SchedulerRepairPolicy'; export { CronScheduler } from './CronScheduler'; export type { CronConfig, CronCallbacks, ScheduledTask } from './CronScheduler'; @@ -13,4 +16,6 @@ export { type SchedulerTask, type SchedulerStatus, type SchedulerCallbacks, + type LogicalTaskCancelReason, + type SchedulerWaitingTask, } from '../../types/scheduler'; diff --git a/src/model/statistics/DailySortieStats.ts b/src/model/statistics/DailySortieStats.ts new file mode 100644 index 0000000..ef29024 --- /dev/null +++ b/src/model/statistics/DailySortieStats.ts @@ -0,0 +1,331 @@ +/** 从后端成功日志维护可持久化的“今日出征统计”。 */ +import { + BATTLE_GRADES, + type BattleGrade, + type BattleGradeCounts, + type DailySortieStatsSnapshot, + type ShipDropNotice, +} from '../../types/statistics.js'; +import { + browserStorageStore, + type StorageStore, +} from '../../adapter/StorageAdapter.js'; + +const STORAGE_KEY = 'daily_sortie_stats_v1'; +const STORAGE_VERSION = 1; +const LOG_DEDUP_WINDOW_MS = 1_200; +const DROP_NOTICE_DURATION_MS = 60_000; +const DEFAULT_LOOT_LIMIT = 50; +const DEFAULT_SHIP_LIMIT = 500; + +interface PersistedDailySortieStats { + version: number; + dateKey: string; + battleCount: number; + grades: BattleGradeCounts; + quickRepairCount: number; + bathRepairCount: number; + lootCount: number; + lootLimit: number; + shipCount: number; + shipLimit: number; + expeditionCount: number; + shipDrops: Record; +} + +type ParsedStatsEvent = + | { type: 'battle'; grade: BattleGrade } + | { type: 'quick-repair'; count: number } + | { type: 'bath-repair' } + | { type: 'loot-count'; count: number; limit: number } + | { type: 'ship-count'; count: number; limit: number } + | { type: 'ship-drop'; shipName: string } + | { type: 'expedition'; count: number }; + +function emptyGrades(): BattleGradeCounts { + return { SS: 0, S: 0, A: 0, B: 0, C: 0, D: 0 }; +} + +function localDateKey(timestamp: number): string { + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +function safeCount(value: unknown, fallback = 0): number { + return typeof value === 'number' && Number.isFinite(value) + ? Math.max(0, Math.floor(value)) + : fallback; +} + +function createEmptyState(timestamp: number): PersistedDailySortieStats { + return { + version: STORAGE_VERSION, + dateKey: localDateKey(timestamp), + battleCount: 0, + grades: emptyGrades(), + quickRepairCount: 0, + bathRepairCount: 0, + lootCount: 0, + lootLimit: DEFAULT_LOOT_LIMIT, + shipCount: 0, + shipLimit: DEFAULT_SHIP_LIMIT, + expeditionCount: 0, + shipDrops: {}, + }; +} + +function sanitizeState( + value: unknown, + timestamp: number, +): PersistedDailySortieStats { + if (!value || typeof value !== 'object') return createEmptyState(timestamp); + const source = value as Partial; + if ( + source.version !== STORAGE_VERSION + || source.dateKey !== localDateKey(timestamp) + ) { + return createEmptyState(timestamp); + } + + const grades = emptyGrades(); + for (const grade of BATTLE_GRADES) { + grades[grade] = safeCount(source.grades?.[grade]); + } + + const shipDrops: Record = {}; + if (source.shipDrops && typeof source.shipDrops === 'object') { + for (const [rawName, rawCount] of Object.entries(source.shipDrops)) { + const name = rawName.trim(); + const count = safeCount(rawCount); + if (name && count > 0) shipDrops[name] = count; + } + } + + return { + version: STORAGE_VERSION, + dateKey: source.dateKey, + battleCount: safeCount(source.battleCount), + grades, + quickRepairCount: safeCount(source.quickRepairCount), + bathRepairCount: safeCount(source.bathRepairCount), + lootCount: safeCount(source.lootCount), + lootLimit: safeCount(source.lootLimit, DEFAULT_LOOT_LIMIT) + || DEFAULT_LOOT_LIMIT, + shipCount: safeCount(source.shipCount), + shipLimit: safeCount(source.shipLimit, DEFAULT_SHIP_LIMIT) + || DEFAULT_SHIP_LIMIT, + expeditionCount: safeCount(source.expeditionCount), + shipDrops, + }; +} + +function parseStatsEvent(message: string): ParsedStatsEvent | null { + const battleMatch = message.match( + /\[Combat\]\s*战果:\s*.*?评价\s*[=::]\s*(SS|S|A|B|C|D)\b/i, + ); + if (battleMatch) { + return { + type: 'battle', + grade: battleMatch[1].toUpperCase() as BattleGrade, + }; + } + + const quickRepairMatch = message.match( + /\[UI\]\s*修理位置:\s*\[([0-9,\s]*)\]/, + ); + if (quickRepairMatch) { + const count = quickRepairMatch[1] + .split(',') + .map(value => value.trim()) + .filter(value => /^\d+$/.test(value)) + .length; + return { type: 'quick-repair', count }; + } + + if ( + /\[OPS\]\s*浴室修理(?:派单成功|操作完成(?:\s*:.*)?)(?:\s|$)/.test( + message, + ) + ) { + return { type: 'bath-repair' }; + } + + const lootMatch = message.match( + /\[UI\]\s*战利品数量:\s*(\d+)\s*\/\s*(\d+)/, + ); + if (lootMatch) { + return { + type: 'loot-count', + count: Number(lootMatch[1]), + limit: Number(lootMatch[2]), + }; + } + + const shipCountMatch = message.match( + /\[UI\]\s*舰船数量:\s*(\d+)\s*\/\s*(\d+)/, + ); + if (shipCountMatch) { + return { + type: 'ship-count', + count: Number(shipCountMatch[1]), + limit: Number(shipCountMatch[2]), + }; + } + + const shipDropMatch = message.match( + /\[Combat\]\s*获得舰船:\s*(.+?)\s*$/, + ); + if (shipDropMatch?.[1].trim()) { + return { type: 'ship-drop', shipName: shipDropMatch[1].trim() }; + } + + const expeditionMatch = message.match( + /\[UI\]\s*远征收取:\s*(\d+)\s*支/, + ); + if (expeditionMatch) { + return { type: 'expedition', count: Number(expeditionMatch[1]) }; + } + + return null; +} + +export class DailySortieStats { + private state: PersistedDailySortieStats; + private dropNotice: ShipDropNotice | null = null; + private readonly recentLogs = new Map(); + + constructor( + private readonly storage: StorageStore = browserStorageStore, + private readonly now: () => number = () => Date.now(), + ) { + this.state = this.load(this.now()); + } + + /** 解析一条运行日志。返回 true 表示统计显示内容发生变化。 */ + consume(message: string, timestamp = this.now()): boolean { + const dayChanged = this.ensureCurrentDay(timestamp); + const normalized = message.trim(); + const event = parseStatsEvent(normalized); + if (!event) return dayChanged; + + if (this.isDuplicate(normalized, timestamp)) return dayChanged; + + switch (event.type) { + case 'battle': + this.state.battleCount += 1; + this.state.grades[event.grade] += 1; + break; + case 'quick-repair': + if (event.count === 0) return dayChanged; + this.state.quickRepairCount += event.count; + break; + case 'bath-repair': + this.state.bathRepairCount += 1; + break; + case 'loot-count': + this.state.lootCount = safeCount(event.count); + this.state.lootLimit = safeCount(event.limit, DEFAULT_LOOT_LIMIT) + || DEFAULT_LOOT_LIMIT; + break; + case 'ship-count': + this.state.shipCount = safeCount(event.count); + this.state.shipLimit = safeCount(event.limit, DEFAULT_SHIP_LIMIT) + || DEFAULT_SHIP_LIMIT; + break; + case 'ship-drop': + this.state.shipCount += 1; + this.state.shipDrops[event.shipName] = + (this.state.shipDrops[event.shipName] ?? 0) + 1; + this.dropNotice = { + shipName: event.shipName, + dailyIndex: this.state.shipCount, + visibleUntil: timestamp + DROP_NOTICE_DURATION_MS, + }; + break; + case 'expedition': + this.state.expeditionCount += safeCount(event.count); + break; + } + + this.save(); + return true; + } + + getSnapshot(timestamp = this.now()): DailySortieStatsSnapshot { + this.ensureCurrentDay(timestamp); + if (this.dropNotice && this.dropNotice.visibleUntil <= timestamp) { + this.dropNotice = null; + } + + return { + battleCount: this.state.battleCount, + grades: { ...this.state.grades }, + quickRepairCount: this.state.quickRepairCount, + bathRepairCount: this.state.bathRepairCount, + lootCount: this.state.lootCount, + lootLimit: this.state.lootLimit, + shipCount: this.state.shipCount, + shipLimit: this.state.shipLimit, + expeditionCount: this.state.expeditionCount, + shipDrops: Object.entries(this.state.shipDrops) + .map(([name, count]) => ({ name, count })) + .sort((left, right) => ( + right.count - left.count + || left.name.localeCompare(right.name, 'zh-CN') + )), + dropNotice: this.dropNotice ? { ...this.dropNotice } : null, + }; + } + + reset(timestamp = this.now()): void { + this.state = createEmptyState(timestamp); + this.dropNotice = null; + this.recentLogs.clear(); + this.save(); + } + + private ensureCurrentDay(timestamp: number): boolean { + if (this.state.dateKey === localDateKey(timestamp)) return false; + this.reset(timestamp); + return true; + } + + private isDuplicate(message: string, timestamp: number): boolean { + const previous = this.recentLogs.get(message); + this.recentLogs.set(message, timestamp); + + for (const [loggedMessage, loggedAt] of this.recentLogs) { + if ( + timestamp < loggedAt + || timestamp - loggedAt >= LOG_DEDUP_WINDOW_MS + ) { + this.recentLogs.delete(loggedMessage); + } + } + + return previous !== undefined + && timestamp >= previous + && timestamp - previous < LOG_DEDUP_WINDOW_MS; + } + + private load(timestamp: number): PersistedDailySortieStats { + try { + const raw = this.storage.get(STORAGE_KEY); + if (!raw) return createEmptyState(timestamp); + return sanitizeState(JSON.parse(raw), timestamp); + } catch { + return createEmptyState(timestamp); + } + } + + private save(): void { + try { + this.storage.set(STORAGE_KEY, JSON.stringify(this.state)); + } catch { + // localStorage 不可用时只保留本次运行内的统计。 + } + } +} diff --git a/src/shared/decisiveAutomation.ts b/src/shared/decisiveAutomation.ts new file mode 100644 index 0000000..82eb3a0 --- /dev/null +++ b/src/shared/decisiveAutomation.ts @@ -0,0 +1,18 @@ +/** 自动决战只允许使用计划页方案或内置系统预设。 */ + +export const USER_DECISIVE_PLAN_ID = 'user_plan'; +export const SYSTEM_DECISIVE_PRESET_ID = 'system_preset'; +export const SYSTEM_DECISIVE_TEMPLATE_ID = 'builtin_decisive_6'; + +export type DecisiveAutomationSource = + | typeof USER_DECISIVE_PLAN_ID + | typeof SYSTEM_DECISIVE_PRESET_ID; + +/** 将旧模板 ID 和异常值收口到当前两种稳定来源。 */ +export function normalizeDecisiveAutomationSource( + value: unknown, +): DecisiveAutomationSource { + return value === USER_DECISIVE_PLAN_ID + ? USER_DECISIVE_PLAN_ID + : SYSTEM_DECISIVE_PRESET_ID; +} diff --git a/src/shared/fleetShipTypes.ts b/src/shared/fleetShipTypes.ts new file mode 100644 index 0000000..ddf75dd --- /dev/null +++ b/src/shared/fleetShipTypes.ts @@ -0,0 +1,71 @@ +/** 提供 22 种规范舰种代码、标签、映射和契约校验。 */ +import { + NATIVE_FLEET_SHIP_TYPE_CODES, + NATIVE_FLEET_SHIP_TYPE_LABELS, +} from './nativeFleetShipTypes.generated'; + +export { + NATIVE_FLEET_SHIP_TYPE_CODES, + NATIVE_FLEET_SHIP_TYPE_LABELS, +} from './nativeFleetShipTypes.generated'; + +/** GUI 显示和 YAML 校验共用的后端舰种代码。 */ +export const TYPE_LABELS: Readonly> = Object.freeze({ + ...NATIVE_FLEET_SHIP_TYPE_LABELS, + ss_or_ssg: '潜艇/导潜', +}); + +export const FLEET_SHIP_TYPE_CODES = Object.freeze([ + ...NATIVE_FLEET_SHIP_TYPE_CODES, + 'ss_or_ssg', +]); + +const CANONICAL_FLEET_SHIP_TYPES = new Set(FLEET_SHIP_TYPE_CODES); + +/** 舰船资料库的显示优先级;native 新增类型会自动追加。 */ +const SHIP_TYPE_FILTER_PRIORITY: readonly string[] = Object.freeze([ + 'ap', + 'av', + 'cv', + 'bb', + 'bbg', + 'bbv', + 'bc', + 'bm', + 'ca', + 'cav', + 'cl', + 'clt', + 'cvl', + 'dd', + 'asdg', + 'kp', + 'ssg', + 'sc', + 'ss', + 'aadg', + 'cg', + 'bg', +]); + +export const SHIP_TYPE_FILTER_ORDER: readonly string[] = Object.freeze([ + ...SHIP_TYPE_FILTER_PRIORITY.filter( + code => NATIVE_FLEET_SHIP_TYPE_CODES.includes(code), + ), + ...NATIVE_FLEET_SHIP_TYPE_CODES.filter( + code => !SHIP_TYPE_FILTER_PRIORITY.includes(code), + ), +]); + +/** 规范大小写并校验后端 canonical code。 */ +export function normalizeFleetShipTypeCode( + value: string, +): string | null { + const code = value.trim().toLowerCase(); + return CANONICAL_FLEET_SHIP_TYPES.has(code) ? code : null; +} + +export function shipTypeLabel(code: string): string { + const canonical = normalizeFleetShipTypeCode(code); + return canonical ? TYPE_LABELS[canonical] : code; +} diff --git a/src/shared/legacyDecisiveAutomation.ts b/src/shared/legacyDecisiveAutomation.ts new file mode 100644 index 0000000..05ef179 --- /dev/null +++ b/src/shared/legacyDecisiveAutomation.ts @@ -0,0 +1,20 @@ +/** + * 旧版设置页保存过的决战自动化配置。 + * + * 自动决战开关和模板可升级到正式 GUI 自动化配置;票数保留仅做 + * 无损归档,不参与执行轮数,也不能覆盖独立维护的决战计划。 + */ +export interface LegacyDecisiveAutomationSettings { + autoDecisive?: boolean; + ticketReserve?: number; + templateId?: string; +} + +export const LEGACY_DECISIVE_YAML_KEYS = [ + 'auto_decisive', + 'decisive_ticket_reserve', + 'decisive_template_id', +] as const; + +export type LegacyDecisiveYamlKey = + typeof LEGACY_DECISIVE_YAML_KEYS[number]; diff --git a/src/shared/lootPlans.ts b/src/shared/lootPlans.ts new file mode 100644 index 0000000..0ac8159 --- /dev/null +++ b/src/shared/lootPlans.ts @@ -0,0 +1,239 @@ +/** + * 自动战利品计划稳定标识及旧索引迁移规则。 + * + * 旧版本只保存 planPaths 数组下标,数组调整后同一个数字会指向其他地图。 + * 当前版本改为保存系统计划文件名,文件名同时作为稳定计划标识。 + * 界面顺序可以继续调整,但不能再改变已保存配置的执行目标。 + * + * 迁移分为三种来源: + * 1. 当前 GUI 早期版本的 gui_settings.json 使用四项新数组; + * 2. 旧 usersettings.yaml 默认使用 PR 前的四项数组; + * 3. 完整旧安装优先读取其 builtin_templates.json 还原真实顺序。 + * + * 所有迁移结果都必须落入白名单,未知值回退到默认 9-2。 + * v6 删除旧独立资源后,旧 9-4、8-5 标识按地图迁移到 old 计划。 + * 该模块只处理标识转换,不读取文件,也不负责执行计划。 + */ + +export const LOOT_PLAN_IDS = [ + 'bettle-old-9-2ADGHM速刷胖次.yaml', + 'bettle-old-8-5AI六潜胖次.yaml', + 'bettle-old-8-2BJ低耗胖次.yaml', + 'bettle-old-9-4六潜练级.yaml', + 'bettle-周常-9-2.yaml', + 'bettle-周常-7-4.yaml', + 'bettle-周常-8-2.yaml', + 'bettle-周常-2-1.yaml', +] as const; + +export type LootPlanId = typeof LOOT_PLAN_IDS[number]; + +export const DEFAULT_LOOT_PLAN_ID: LootPlanId = 'bettle-周常-9-2.yaml'; + +export type LootPlanSource = 'system' | 'user'; + +/** 自动胖次下拉列表只保存受管计划身份,不保存任意绝对路径。 */ +export interface LootAutomationPlan { + source: LootPlanSource; + file: string; + name: string; +} + +const DEFAULT_LOOT_PLAN_NAMES: Readonly> = { + 'bettle-old-9-2ADGHM速刷胖次.yaml': 'old 9-2 ADGHM 速刷', + 'bettle-old-8-5AI六潜胖次.yaml': 'old 8-5 AI 六潜', + 'bettle-old-8-2BJ低耗胖次.yaml': 'old 8-2 BJ 低耗', + 'bettle-old-9-4六潜练级.yaml': 'old 9-4 六潜', + 'bettle-周常-9-2.yaml': '周常 9-2', + 'bettle-周常-7-4.yaml': '周常 7-4', + 'bettle-周常-8-2.yaml': '周常 8-2', + 'bettle-周常-2-1.yaml': '周常 2-1', +}; + +/** 首次使用沿用原有 8 项,之后由用户在加载浮窗中维护。 */ +export const DEFAULT_LOOT_PLANS: readonly LootAutomationPlan[] = + LOOT_PLAN_IDS.map(file => ({ + source: 'system', + file, + name: DEFAULT_LOOT_PLAN_NAMES[file], + })); + +/** PR 调整后的旧 GUI JSON 数组,用于一次性迁移 lootPlanIndex。 */ +export const INTERIM_LOOT_PLAN_IDS: readonly LootPlanId[] = [ + 'bettle-周常-9-2.yaml', + 'bettle-周常-7-4.yaml', + 'bettle-周常-8-2.yaml', + 'bettle-周常-2-1.yaml', +]; + +/** PR 前四项数组,用于没有安装资源可供识别的 usersettings.yaml。 */ +export const LEGACY_LOOT_PLAN_IDS: readonly LootPlanId[] = [ + 'bettle-周常-9-2.yaml', + 'bettle-周常-7-4.yaml', + 'bettle-old-8-5AI六潜胖次.yaml', + 'bettle-周常-2-1.yaml', +]; + +const LOOT_PLAN_ID_SET = new Set(LOOT_PLAN_IDS); +const LEGACY_LOOT_PLAN_ID_MAP: Readonly> = { + 'bettle-捞胖次-9-4-6SS.yaml': 'bettle-old-9-4六潜练级.yaml', + 'bettle-捞胖次-8-5.yaml': 'bettle-old-8-5AI六潜胖次.yaml', + 'bettle-主库-9-2ADGHM速刷胖次.yaml': + 'bettle-old-9-2ADGHM速刷胖次.yaml', + 'bettle-主库-8-5AI六潜胖次.yaml': + 'bettle-old-8-5AI六潜胖次.yaml', + 'bettle-主库-8-2BJ低耗胖次.yaml': + 'bettle-old-8-2BJ低耗胖次.yaml', + 'bettle-主库-9-4六潜练级.yaml': + 'bettle-old-9-4六潜练级.yaml', +}; + +/** 判断外部值是否为当前支持的稳定计划标识。 */ +export function isLootPlanId(value: unknown): value is LootPlanId { + return typeof value === 'string' && LOOT_PLAN_ID_SET.has(value); +} + +/** 接受当前标识和已发布的旧标识,未知值返回 null。 */ +export function migrateLootPlanId(value: unknown): LootPlanId | null { + if (isLootPlanId(value)) return value; + return typeof value === 'string' + ? LEGACY_LOOT_PLAN_ID_MAP[value] ?? null + : null; +} + +/** 只接受当前内置资源中明确支持的稳定计划标识。 */ +export function normalizeLootPlanId(value: unknown): LootPlanId { + return migrateLootPlanId(value) ?? DEFAULT_LOOT_PLAN_ID; +} + +/** 生成来源与文件名共同组成的稳定选择值。 */ +export function lootAutomationPlanKey( + plan: Pick, +): string { + return `${plan.source}:${encodeURIComponent(plan.file)}`; +} + +/** 仅接受受管目录中的 YAML 文件名。 */ +function normalizeManagedPlanFile(value: unknown): string | null { + if (typeof value !== 'string') return null; + const file = value.trim(); + if ( + !file + || file.length > 255 + || /[\\/\x00-\x1f]/.test(file) + || !/\.ya?ml$/i.test(file) + ) { + return null; + } + return file; +} + +/** 清理一项自动胖次计划,并升级已发布的旧系统文件名。 */ +export function normalizeLootAutomationPlan( + value: unknown, +): LootAutomationPlan | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null; + } + const record = value as Record; + if (record.source !== 'system' && record.source !== 'user') return null; + const rawFile = normalizeManagedPlanFile(record.file); + if (!rawFile) return null; + const file = record.source === 'system' + ? migrateLootPlanId(rawFile) ?? rawFile + : rawFile; + const fallbackName = file + .replace(/\.ya?ml$/i, '') + .replace(/^bettle-/i, ''); + const name = typeof record.name === 'string' + ? record.name.trim().slice(0, 100) + : ''; + return { + source: record.source, + file, + name: name || fallbackName, + }; +} + +/** + * 归一化自动胖次计划列表。 + * 非数组表示旧配置并使用默认列表;显式空数组表示用户已删除全部项目。 + */ +export function normalizeLootAutomationPlans( + value: unknown, + fallback: readonly LootAutomationPlan[] = DEFAULT_LOOT_PLANS, +): LootAutomationPlan[] { + const source = Array.isArray(value) ? value : fallback; + const output: LootAutomationPlan[] = []; + const seen = new Set(); + for (const item of source) { + const normalized = normalizeLootAutomationPlan(item); + if (!normalized) continue; + const key = lootAutomationPlanKey(normalized); + if (seen.has(key)) continue; + seen.add(key); + output.push(normalized); + } + return output; +} + +/** 在下拉列表中查找指定来源和文件名。 */ +export function findLootAutomationPlan( + plans: readonly LootAutomationPlan[], + source: unknown, + file: unknown, +): LootAutomationPlan | null { + if ( + (source !== 'system' && source !== 'user') + || typeof file !== 'string' + ) { + return null; + } + const migratedFile = source === 'system' + ? migrateLootPlanId(file) ?? file + : file; + return plans.find(plan => ( + plan.source === source && plan.file === migratedFile + )) ?? null; +} + +/** 只接受整数或非空整数字符串,避免 null/false 被当成索引 0。 */ +export function parseLootPlanIndex(value: unknown): number | null { + if ( + typeof value !== 'number' + && (typeof value !== 'string' || !value.trim()) + ) { + return null; + } + const number = Number(value); + return Number.isInteger(number) ? number : null; +} + +/** 按指定历史数组解释数字索引,非法或越界时交给调用方安全回退。 */ +export function lootPlanIdFromIndex( + value: unknown, + planIds: readonly LootPlanId[], +): LootPlanId | null { + const index = parseLootPlanIndex(value); + if (index === null) return null; + return planIds[index] ?? null; +} + +/** 从旧模板中的路径恢复地图语义,不依赖路径所在目录。 */ +export function lootPlanIdFromLegacyPath( + value: unknown, +): LootPlanId | null { + if (typeof value !== 'string') return null; + const file = value.replace(/\\/g, '/').split('/').pop() ?? ''; + if (/9-2ADGHM/i.test(file)) { + return 'bettle-old-9-2ADGHM速刷胖次.yaml'; + } + if (/8-2BJ/i.test(file)) return 'bettle-old-8-2BJ低耗胖次.yaml'; + if (/9-4/i.test(file)) return 'bettle-old-9-4六潜练级.yaml'; + if (/9-2/i.test(file)) return 'bettle-周常-9-2.yaml'; + if (/7-4/i.test(file)) return 'bettle-周常-7-4.yaml'; + if (/8-5/i.test(file)) return 'bettle-old-8-5AI六潜胖次.yaml'; + if (/8-2/i.test(file)) return 'bettle-周常-8-2.yaml'; + if (/2-1/i.test(file)) return 'bettle-周常-2-1.yaml'; + return null; +} diff --git a/src/shared/migrationConflicts.ts b/src/shared/migrationConflicts.ts new file mode 100644 index 0000000..592b729 --- /dev/null +++ b/src/shared/migrationConflicts.ts @@ -0,0 +1,33 @@ +/** 迁移后需要用户确认的 YAML 冲突类型。 */ +export type MigrationConflictKind = 'battle' | 'daily'; + +/** 一条可展示、可扩展的冲突原因。 */ +export interface MigrationConflictReason { + reasonCode: string; + reason: string; + relatedFile?: string; +} + +/** Renderer 只接收受管文件身份,不接收本地绝对路径。 */ +export interface MigrationConflictItem { + id: string; + kind: MigrationConflictKind; + file: string; + name: string; + reasons: MigrationConflictReason[]; +} + +/** 启动时读取的待确认冲突。 */ +export interface MigrationConflictListResult { + pending: boolean; + conflicts: MigrationConflictItem[]; +} + +/** 用户提交保留清单后的处理结果。 */ +export interface MigrationConflictResolutionResult { + success: boolean; + kept: number; + deleted: number; + errors: string[]; + remaining: MigrationConflictItem[]; +} diff --git a/src/shared/nativeFleetShipTypes.generated.ts b/src/shared/nativeFleetShipTypes.generated.ts new file mode 100644 index 0000000..e06abcb --- /dev/null +++ b/src/shared/nativeFleetShipTypes.generated.ts @@ -0,0 +1,31 @@ +/** 保存由 autowsgr_native 生成的舰种代码,供前端漂移检查。 */ +/* 此文件由 scripts/sync-fleet-ship-types.js 生成,禁止手工修改。 */ +export const NATIVE_FLEET_SHIP_TYPE_LABELS: Readonly< + Record +> = Object.freeze({ + "aadg": "防驱", + "ap": "补给", + "asdg": "导驱", + "av": "装母", + "bb": "战列", + "bbg": "导战", + "bbv": "航战", + "bc": "战巡", + "bg": "大巡", + "bm": "重炮", + "ca": "重巡", + "cav": "航巡", + "cg": "防巡", + "cl": "轻巡", + "clt": "雷巡", + "cv": "航母", + "cvl": "轻母", + "dd": "驱逐", + "kp": "导巡", + "sc": "炮潜", + "ss": "潜艇", + "ssg": "导潜", +}); + +export const NATIVE_FLEET_SHIP_TYPE_CODES: readonly string[] = + Object.freeze(Object.keys(NATIVE_FLEET_SHIP_TYPE_LABELS)); diff --git a/src/types/api.ts b/src/types/api.ts index ae22f12..a022fc0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,11 +1,4 @@ -/** - * API / WebSocket 通信类型定义。 - * 从 ApiClient.ts 提取,供各层直接引用。 - */ - -// ════════════════════════════════════════ -// 后端 API 响应类型 -// ════════════════════════════════════════ +/** 定义 Renderer 与后端 HTTP/WebSocket 通信使用的请求、响应和事件类型。 */ export interface ApiResponse { success: boolean; @@ -25,34 +18,6 @@ export interface TaskProgress { node: string | null; } -export interface CombatEvent { - type: string; - node: string | null; - action: string | null; - result?: unknown; - enemies?: Record; - ship_stats?: number[]; -} - -export interface RoundResult { - round: number; - success: boolean; - nodes?: string[]; - mvp?: string | null; - ship_damage?: number[]; - grade?: string | null; - node_count?: number; - enemies?: Record>; - events?: CombatEvent[]; - error?: string; -} - -export interface TaskResult { - total_runs: number; - success_runs: number; - details: RoundResult[]; -} - export interface TaskStatus { task_id: string | null; status: 'idle' | 'running' | 'completed' | 'failed' | 'stopped'; @@ -142,9 +107,33 @@ export interface GameAcquisitionData { loot_max: number | null; } -// ════════════════════════════════════════ -// 请求体类型 -// ════════════════════════════════════════ +export interface CombatEvent { + type: string; + node: string | null; + action: string | null; + result?: unknown; + enemies?: Record; + ship_stats?: number[]; +} + +export interface RoundResult { + round: number; + success: boolean; + nodes?: string[]; + mvp?: string | null; + ship_damage?: number[]; + grade?: string | null; + node_count?: number; + enemies?: Record>; + events?: CombatEvent[]; + error?: string; +} + +export interface TaskResult { + total_runs: number; + success_runs: number; + details: RoundResult[]; +} export interface NodeDecisionReq { formation?: number; @@ -154,22 +143,28 @@ export interface NodeDecisionReq { proceed_stop?: number[]; detour?: boolean; SL_when_detour_fails?: boolean; - enemy_rules?: string[][] | null; + enemy_rules?: Array<[string, RuleAction]> | null; + enemy_formation_rules?: Array<[string, RuleAction]> | null; + SL_when_spot_enemy_fails?: boolean; + SL_when_enter_fight?: boolean; + formation_when_spot_enemy_fails?: number | null; } -export interface FleetRuleReq { - /** 候选舰船名(按优先级顺序) */ - candidates: string[]; - /** 搜索关键词(用于同名舰船精确筛选) */ +export type RuleAction = 'retreat' | 'detour' | 1 | 2 | 3 | 4 | 5; + +export interface FleetShipRuleReq { + name: string; search_name?: string; - /** 舰种约束(如 cl/cav/ss),用于同名舰船二次筛选 */ - ship_type?: string; - /** 等级下限(仅选择 >= 该等级) */ + ship_type?: string[]; min_level?: number; - /** 等级上限(仅选择 <= 该等级) */ max_level?: number; } +export interface FleetRuleReq extends Omit { + name?: string; + candidates?: FleetShipRuleReq[]; +} + export interface CombatPlanReq { name?: string; mode?: string; @@ -217,6 +212,8 @@ export interface ExerciseReq { export interface DecisiveReq { type: 'decisive'; chapter?: number; + decisive_rounds?: number; + use_new_fleet_change_algorithm?: boolean; level1?: string[]; level2?: string[]; flagship_priority?: string[]; @@ -230,10 +227,6 @@ export type TaskRequest = | ExerciseReq | DecisiveReq; -// ════════════════════════════════════════ -// WebSocket 消息类型 -// ════════════════════════════════════════ - export interface WsLogMessage { type: 'log'; timestamp: string; @@ -259,10 +252,6 @@ export interface WsTaskCompleted { export type WsMessage = WsLogMessage | WsTaskUpdate | WsTaskCompleted; -// ════════════════════════════════════════ -// 事件回调类型 -// ════════════════════════════════════════ - export interface ApiClientCallbacks { onLog?: (msg: WsLogMessage) => void; onTaskUpdate?: (msg: WsTaskUpdate) => void; diff --git a/src/types/electronBridge.ts b/src/types/electronBridge.ts deleted file mode 100644 index 2c751aa..0000000 --- a/src/types/electronBridge.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** 通过 preload 注入的 IPC 桥 */ -export interface ElectronBridge { - openDirectoryDialog: (title?: string) => Promise; - openFileDialog: (filters: { name: string; extensions: string[] }[], defaultDir?: string) => Promise<{ path: string; content: string } | null>; - saveFile: (path: string, content: string) => Promise; - saveFileDialog: (defaultName: string, content: string, filters: { name: string; extensions: string[] }[]) => Promise; - readFile: (path: string) => Promise; - appendFile: (path: string, content: string) => Promise; - detectEmulator: () => Promise<{ type: string; path: string; serial: string; adbPath: string } | null>; - checkAdbDevices: () => Promise<{ serial: string; status: string }[]>; - getAppRoot: () => Promise; - resolveAppPath: (filePath: string) => Promise; - getPlansDir: () => Promise; - getConfigDir: () => Promise; - listPlanFiles: () => Promise<{ name: string; file: string }[]>; - openFolder: (folderPath: string) => Promise; - checkEnvironment: () => Promise<{ - pythonCmd: string | null; - pythonVersion: string | null; - missingPackages: string[]; - allReady: boolean; - }>; - /* - * 测试期接口(后端源码更新)已停用,类型保留便于回滚恢复。 - checkUpdates: () => Promise<{ - gitAvailable: boolean; - hasUpdates: boolean; - currentBranch: string; - behindCount: number; - remoteUrl: string; - }>; - */ - installDeps: () => Promise<{ success: boolean; output: string }>; - /* - * 测试期接口(后端源码更新)已停用,类型保留便于回滚恢复。 - pullUpdates: () => Promise<{ success: boolean; output: string }>; - */ - startBackend: () => Promise<{ success: boolean; message: string }>; - runSetup: () => Promise<{ success: boolean; output: string }>; - installPortablePython: () => Promise<{ success: boolean }>; - checkGuiUpdates: () => Promise<{ version: string } | null>; - downloadGuiUpdate: () => Promise<{ success: boolean; message?: string }>; - installGuiUpdate: () => void; - onUpdateStatus: (callback: (status: any) => void) => void; - onBackendLog: (callback: (line: string) => void) => void; - onSetupLog: (callback: (text: string) => void) => void; - getAppVersion: () => string; - getBackendPort: () => number; - setBackendPort: (port: number) => Promise; - getBackendStartupMode: () => 'managed' | 'external'; - setBackendStartupMode: (mode: 'managed' | 'external') => Promise; - getBackendRepoPath: () => string; - setBackendRepoPath: (repoPath: string | null) => Promise; - getOcrGpuMode: () => 'auto' | 'cpu' | 'cuda'; - setOcrGpuMode: (mode: 'auto' | 'cpu' | 'cuda') => Promise; - getCudaPath: () => string; - setCudaPath: (cudaPath: string | null) => Promise; - validateCudaPath: (cudaPath: string) => Promise<{ valid: boolean; path: string; version: string | null; kind?: 'toolkit' | 'runtime'; error?: string }>; - getSaveBackendScreenshots: () => boolean; - setSaveBackendScreenshots: (enabled: boolean) => Promise; - getUpdateMode: () => 'auto' | 'manual'; - setUpdateMode: (mode: 'auto' | 'manual') => Promise; - getPythonPath: () => string | null; - setPythonPath: (pythonPath: string | null) => Promise; - validatePython: (pythonPath: string) => Promise<{ valid: boolean; version: string | null; error?: string }>; -} - -declare global { - interface Window { - electronBridge?: ElectronBridge; - } -} diff --git a/src/types/ipc.ts b/src/types/ipc.ts new file mode 100644 index 0000000..b353d76 --- /dev/null +++ b/src/types/ipc.ts @@ -0,0 +1,479 @@ +/** 定义 Renderer 与 Electron 主进程之间的桥接方法、文件结果和资源契约。 */ + +import type { + BattleResultGrade, + GuiAutomationSettings, + TaskPreset, +} from './model.js'; +import type { + LegacyDecisiveAutomationSettings, +} from '../shared/legacyDecisiveAutomation.js'; +import type { + MigrationConflictListResult, + MigrationConflictResolutionResult, +} from '../shared/migrationConflicts.js'; + +export type { GuiAutomationSettings } from './model.js'; +export type { + LegacyDecisiveAutomationSettings, +} from '../shared/legacyDecisiveAutomation.js'; + +export interface WindowPreferences { + defaultWidth: number; + defaultHeight: number; + rememberBounds: boolean; +} + +export interface DecisivePlanSettings { + chapter: number; + useQuickRepair: boolean; + level1: string[]; + level2: string[]; +} + +export interface AdbOperationResult { + success: boolean; + serial: string; + status: string; + message: string; +} + +export interface CudaValidationResult { + valid: boolean; + path: string; + version: string | null; + kind?: 'toolkit' | 'runtime'; + torchVersion?: string | null; + device?: string | null; + error?: string; +} + +export type GuiUpdateStatus = + | { + status: 'available'; + version: string; + releaseNotes?: string; + } + | { + status: 'up-to-date'; + } + | { + status: 'downloading'; + percent: number; + transferred: number; + total: number; + } + | { + status: 'downloaded'; + version: string; + } + | { + status: 'installing' | 'error'; + message: string; + }; + +export type PlanPresetSource = 'system' | 'user'; +export type DailyPlanType = 'exercise' | 'campaign' | 'decisive'; + +export interface UserTeamShipRule { + name: string; + search_name?: string; + ship_type?: string[]; + min_level?: number; + max_level?: number; +} + +export interface UserTeamPlanSlot { + name?: string; + search_name?: string; + ship_type?: string[]; + min_level?: number; + max_level?: number; + candidates?: UserTeamShipRule[]; +} + +export interface UserTeamPlan { + file?: string; + modifiedAt?: number; + source?: PlanPresetSource; + name: string; + ships: UserTeamPlanSlot[]; +} + +export interface UserTeamPlanResult { + success: boolean; + canceled?: boolean; + exists?: boolean; + file?: string; + plan?: UserTeamPlan; + error?: string; +} + +export interface UserTeamPlanListResult { + plans: UserTeamPlan[]; + errors: PlanFileReadError[]; +} + +export interface PlanTeamBinding { + planFile: string; + planName: string; + source: PlanPresetSource; + teamName: string | null; +} + +export interface ManagedTeamPlan { + file: string; + name: string; + source: PlanPresetSource; +} + +export interface ManagedBattlePlanFleet { + name: string; + source: PlanPresetSource | 'deleted'; + primaryCount: number; + backupCount: number; +} + +export interface ManagedBattlePlan { + kind: 'battle' | 'preset'; + file: string; + name: string; + source: PlanPresetSource; + modifiedAt: number; + chapter: number | string; + map: number | string; + times: number; + gap: number; + fleetId: number; + repairMode: number | number[]; + result: BattleResultGrade | null; + lootCountGe: number; + shipCountGe: number; + fleetCount: number; + nodeCount: number; + fleets: ManagedBattlePlanFleet[]; + taskType?: TaskPreset['task_type']; + campaignName?: string; +} + +export interface ManagedBattlePlanSelection { + plan: ManagedBattlePlan; + fleetPresetIndex?: number; +} + +export interface ManagedDailyPlan { + source: PlanPresetSource; + file: string; + name: string; + taskType: DailyPlanType; + times: number; + fleetId?: number; + campaignName?: string; + chapter?: number; + useQuickRepair?: boolean; +} + +export interface DailyPlanListResult { + plans: ManagedDailyPlan[]; + errors: Array<{ + source: PlanPresetSource; + file: string; + message: string; + }>; +} + +export interface DailyPlanSelection { + plan: ManagedDailyPlan; + times: number; + useQuickRepair?: boolean; +} + +export interface PlanFileReadError { + file: string; + source: PlanPresetSource; + kind: 'battle' | 'team'; + message: string; +} + +export interface PlanManagementResult { + bindings: PlanTeamBinding[]; + battlePlans: ManagedBattlePlan[]; + teamPlans: ManagedTeamPlan[]; + errors: PlanFileReadError[]; + ignoredUnlinkedPlans: string[]; +} + +export interface UserPlanExportSelection { + kind: 'battle' | 'team'; + file: string; +} + +export interface UserPlanExportResult { + success: boolean; + canceled?: boolean; + path?: string; + count?: number; + error?: string; +} + +export interface PlanFileOperationResult { + success: boolean; + kind?: 'battle' | 'preset' | 'daily'; + canceled?: boolean; + exists?: boolean; + file?: string; + path?: string; + sourcePath?: string; + runtimePath?: string; + content?: string; + source?: PlanPresetSource; + teamFiles?: string[]; + conflicts?: string[]; + error?: string; +} + +export interface ShipLibraryStatus { + exists: boolean; + path: string; + generatedAt?: string; + shipCount: number; + assetCount: number; + missingAssets: number; + error?: string; +} + +export interface ShipLibraryUpdateResult { + success: boolean; + output?: string; + generated_at?: string; + ship_count?: number; + asset_count?: number; + added?: number; + updated?: number; + removed?: number; + downloaded?: number; + failed?: number; + failures?: string[]; + error?: string; +} + +export interface ShipLibraryLabels { + locale?: string; + ship_types: Record; + size_classes: Record; + role_classes: Record; + countries: Record; + variants: Record; +} + +export interface ShipLibraryShip { + id: number; + name: string; + search_name: string; + variant: 'normal' | 'refit' | 'special'; + rarity: number; + ship_type: string; + size_class: string; + role_class: string; + country: string; + portraitUrl: string; + backgroundUrl: string; + frameUrl: string; + typeIconUrl: string; + wiki_url?: string; +} + +export interface ShipLibraryManifest { + schemaVersion: number; + generatedAt: string; + labels: ShipLibraryLabels; + typeGroups: { + size_classes: Record; + role_classes: Record; + }; + ships: ShipLibraryShip[]; +} + +export interface ElectronBridge { + openDirectoryDialog: (title?: string) => Promise; + openFileDialog: ( + filters: { name: string; extensions: string[] }[], + defaultDir?: string, + ) => Promise<{ path: string; content: string } | null>; + saveFile: (path: string, content: string) => Promise; + saveFileDialog: ( + defaultName: string, + content: string, + filters: { name: string; extensions: string[] }[], + ) => Promise; + readFile: (path: string) => Promise; + appendFile: (path: string, content: string) => Promise; + detectEmulator: () => Promise<{ + type: string; + path: string; + serial: string; + adbPath: string; + } | null>; + checkAdbDevices: () => Promise<{ serial: string; status: string }[]>; + connectAdbDevice: (serial: string) => Promise; + disconnectAdbDevice: (serial: string) => Promise; + getAppRoot: () => Promise; + resolveAppPath: (filePath: string) => Promise; + getPlansDir: () => Promise; + getConfigDir: () => Promise; + listPlanFiles: () => Promise<{ name: string; file: string }[]>; + openFolder: (folderPath: string) => Promise; + checkEnvironment: () => Promise<{ + pythonCmd: string | null; + pythonVersion: string | null; + missingPackages: string[]; + allReady: boolean; + }>; + installDeps: () => Promise<{ success: boolean; output: string }>; + startBackend: () => Promise<{ success: boolean; message: string }>; + runSetup: () => Promise<{ success: boolean; output: string }>; + installPortablePython: () => Promise<{ success: boolean }>; + checkGuiUpdates: () => Promise< + | { status: 'available'; version: string } + | { status: 'up-to-date' } + | { status: 'error'; message: string } + >; + downloadGuiUpdate: () => Promise<{ + success: boolean; + message?: string; + }>; + installGuiUpdate: () => Promise<{ + success: boolean; + message?: string; + }>; + onUpdateStatus: ( + callback: (status: GuiUpdateStatus) => void, + ) => void; + onBackendLog: (callback: (line: string) => void) => void; + onSetupLog: (callback: (text: string) => void) => void; + getAppVersion: () => string; + getBackendPort: () => number; + setBackendPort: (port: number) => Promise; + getGuiAutomationSettings: () => Promise<{ + exists: boolean; + settings: Partial; + }>; + setGuiAutomationSettings: ( + settings: GuiAutomationSettings, + ) => Promise; + migrateLegacyDecisiveAutomation: ( + settings: LegacyDecisiveAutomationSettings, + ) => Promise; + getDecisivePlanSettings: () => Promise; + setDecisivePlanSettings: ( + settings: DecisivePlanSettings, + ) => Promise; + getBackendStartupMode: () => 'managed' | 'external'; + setBackendStartupMode: ( + mode: 'managed' | 'external', + ) => Promise; + getBackendRepoPath: () => string; + setBackendRepoPath: ( + repoPath: string | null, + ) => Promise; + getOcrGpuMode: () => 'auto' | 'cpu' | 'cuda'; + setOcrGpuMode: ( + mode: 'auto' | 'cpu' | 'cuda', + ) => Promise; + getCudaPath: () => string; + setCudaPath: (cudaPath: string | null) => Promise; + validateCudaPath: ( + cudaPath: string, + ) => Promise; + getSaveBackendScreenshots: () => boolean; + setSaveBackendScreenshots: (enabled: boolean) => Promise; + getWindowPreferences: () => WindowPreferences; + setWindowPreferences: ( + preferences: WindowPreferences, + ) => Promise; + getUpdateMode: () => 'auto' | 'manual'; + setUpdateMode: (mode: 'auto' | 'manual') => Promise; + getShipLibraryStatus: () => Promise; + getShipLibraryManifest: () => Promise; + updateShipLibrary: () => Promise; + onShipLibraryUpdateProgress: ( + callback: (progress: { message: string }) => void, + ) => void; + saveUserTeamPlan: ( + plan: UserTeamPlan, + overwrite?: boolean, + currentFile?: string, + source?: PlanPresetSource, + ) => Promise; + pickUserTeamPlan: () => Promise; + listTeamPlans: () => Promise; + getPlanManagement: () => Promise; + listDailyPlans: () => Promise; + readDailyPlan: ( + source: PlanPresetSource, + file: string, + ) => Promise; + getDailyDecisivePlan: ( + chapter: number, + ) => Promise; + getSystemDailyDecisivePlan: ( + chapter: number, + ) => Promise; + saveDailyDecisivePlan: ( + settings: DecisivePlanSettings, + ) => Promise; + getMigrationConflicts: () => Promise; + resolveMigrationConflicts: ( + keepIds: string[], + ) => Promise; + exportUserPlans: ( + selections: UserPlanExportSelection[], + ) => Promise; + importLocalCombatPlan: () => Promise; + setPlanUnlinkedIgnored: ( + kind: 'battle' | 'team', + source: PlanPresetSource, + file: string, + ignored: boolean, + ) => Promise; + readManagedCombatPlan: ( + source: PlanPresetSource, + file: string, + ) => Promise; + readCombatPlanFile: ( + filePath: string, + ) => Promise; + prepareCombatPlanExecution: ( + content: string, + hint: string, + ) => Promise; + saveManagedCombatPlan: ( + name: string, + content: string, + overwrite?: boolean, + currentFile?: string, + ) => Promise; + renameUserCombatPlan: ( + file: string, + newName: string, + ) => Promise; + deleteUserCombatPlan: ( + file: string, + ) => Promise; + deleteUserTeamPlan: ( + file: string, + ) => Promise; + getPythonPath: () => string | null; + setPythonPath: (pythonPath: string | null) => Promise; + validatePython: (pythonPath: string) => Promise<{ + valid: boolean; + version: string | null; + error?: string; + }>; +} + +declare global { + interface Window { + electronBridge?: ElectronBridge; + } +} diff --git a/src/types/model.ts b/src/types/model.ts index 5311de6..389828a 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -1,126 +1,186 @@ -/** - * Model 层类型定义 —— 与后端 AutoWSGR 数据结构对应。 - * 这些是"高内聚"的业务实体,包含内部状态操作。 - */ +/** 定义配置、作战方案、舰队规则、模板和修理等领域数据结构。 */ -// ════════════════════════════════════════ -// 战斗方案 (Plan) -// ════════════════════════════════════════ +import type { + LootAutomationPlan, + LootPlanSource, +} from '../shared/lootPlans.js'; +import type { + DecisiveAutomationSource, +} from '../shared/decisiveAutomation.js'; -/** 索敌规则条目: [条件表达式, 动作] */ -export type EnemyRule = [string, string | number]; +export interface EmulatorConfig { + type: string; + path?: string; + serial?: string; + process_name?: string; +} + +export interface AccountConfig { + game_app: string; +} + +export interface NormalFightTaskConfig { + name: string; + fleet_id?: number; + fleet_preset_index?: number; + times?: number; +} + +export interface DailyAutomationConfig { + auto_expedition: boolean; + auto_gain_bonus: boolean; + auto_bath_repair: boolean; + auto_set_support: boolean; + bath_repair_blacklist: string[]; + auto_battle: boolean; + battle_type: string; + auto_exercise: boolean; + exercise_fleet_id: number | null; + auto_normal_fight: boolean; + normal_fight_tasks: NormalFightTaskConfig[]; + quick_repair_limit: number | null; + stop_max_ship: boolean; + stop_max_loot: boolean; +} + +export interface OCRConfig { + gpu: boolean; + mirror: 'origin' | 'github' | 'tencent' | 'modelscope'; + ship_name_match_confidence: number; + ship_name_corrections: Record; + ship_name_aliases: Record; +} + +export interface LogConfig { + level: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL'; + root: string; + dir?: string | null; + show_decisive_battle_info?: boolean; + show_emulator_debug?: boolean; + show_ui_debug?: boolean; + show_vision_debug?: boolean; + show_ops_debug?: boolean; + show_combat_state_debug?: boolean; + show_combat_recognition_debug?: boolean; + channels?: Record; +} + +export interface GuiAutomationSettings { + expeditionInterval: number; + battleTimes: number; + autoDecisive: boolean; + /** 自动决战使用计划页方案或系统预设。 */ + decisiveTemplateId: DecisiveAutomationSource; + autoLoot: boolean; + /** 当前自动胖次计划来源。 */ + lootPlanSource: LootPlanSource; + /** 当前自动胖次计划文件名,不受界面选项顺序影响。 */ + lootPlanId: string; + /** 设置页自动胖次下拉列表。 */ + lootPlans: LootAutomationPlan[]; + lootStopCount: number; +} + +export interface UserSettings { + emulator: EmulatorConfig; + account: AccountConfig; + ocr: OCRConfig; + log: LogConfig; + daily_automation: DailyAutomationConfig; + operation_delay_min: number; + operation_delay_max: number; + dock_full_destroy: boolean; + repair_manually: boolean; + bathroom_count: number; + destroy_ship_work_mode: number; + destroy_ship_types: string[]; + remove_equipment_mode: boolean; + plan_root?: string; +} + +export type RuleAction = 'retreat' | 'detour' | 1 | 2 | 3 | 4 | 5; +export type EnemyRule = [string, RuleAction]; + +export type BattleResultGrade = 'D' | 'C' | 'B' | 'A' | 'S' | 'SS'; -/** 单个节点的战斗参数 */ export interface NodeArgs { enemy_rules?: EnemyRule[]; - formation?: number; // 1-5 + formation?: number; night?: boolean; - /** 是否开启远程打击(导潜等可远程打击舰船) */ long_missile_support?: boolean; proceed?: boolean; detour?: boolean; - proceed_stop?: number[]; // 6 个元素 + proceed_stop?: number[]; SL_when_detour_fails?: boolean; + enemy_formation_rules?: EnemyRule[]; + SL_when_spot_enemy_fails?: boolean; + SL_when_enter_fight?: boolean; + formation_when_spot_enemy_fails?: number | null; +} + +export interface ShipRule { + name: string; + search_name?: string; + ship_type?: string[]; + min_level?: number; + max_level?: number; } -/** 舰船筛选条件 (按舰名/国籍/舰种选船) */ export interface ShipFilter { - /** 固定舰名(可选) */ name?: string; - /** 国籍, 如 "德国", "日本" */ + search_name?: string; nation?: string; - /** 舰种代号, 如 "dd", "ss" */ - ship_type?: string; - /** 优先舰船名列表(按顺序尝试) */ - priority?: string[]; - /** 等级下限(仅选择 >= 该等级) */ + ship_type?: string[]; + candidates?: ShipRule[]; min_level?: number; - /** 等级上限(仅选择 <= 该等级) */ max_level?: number; } -/** 编队槽位: 具体舰船名 或 模糊筛选条件 */ -export type ShipSlot = string | ShipFilter; +export type ShipSlot = string | ShipFilter | null; -/** 编队预设: 一组预定义的舰船配置 */ export interface FleetPreset { - /** 显示名称 */ name: string; - /** 舰船槽位列表 (按位置顺序) */ ships: ShipSlot[]; } -/** Plan 文件解析后的完整数据 */ +export type EventChapter = 'E' | 'H'; + +export interface EventMapCatalogEntry { + event: string; + chapters: Record; + files?: Record>; +} + +export interface EventMapCatalog { + schema_version: number; + events: EventMapCatalogEntry[]; +} + export interface PlanData { - /** 常规图为数字章节;活动图为 E/H。 */ chapter: number | string; - /** 常规图为数字地图;活动图可带入口后缀,如 3a/3b。 */ map: number | string; - /** 作战模式;活动方案使用 event。 */ mode?: string; - /** 活动资源标识,如 20260730。后端 YAML 字段名为 event。 */ event?: string; selected_nodes: string[]; - /** 终点节点列表:经过其中任一节点即认定本轮完成。未设置时回退到最后一个 selected_node。 */ endpoint_nodes?: string[]; - fight_condition?: number; // 1-5, 默认 1 - repair_mode?: number | number[]; // 1 或 2(或每舰位数组), 默认 1 - fleet_id?: number; // 编队号 + result?: BattleResultGrade; + fight_condition?: number; + repair_mode?: number | number[]; + fleet_id?: number; node_defaults?: NodeArgs; node_args?: Record; - /** 预定义编队预设列表 */ fleet_presets?: FleetPreset[]; - // 任务级字段(可内联在 plan 中,无需单独的 preset 文件) times?: number; gap?: number; stop_condition?: StopCondition; - /** 定时触发时间 "HH:MM" 格式,到时自动加入队列 */ scheduled_time?: string; } -// ════════════════════════════════════════ -// 用户配置 -// ════════════════════════════════════════ - -export interface EmulatorConfig { - type: string; - path?: string; - serial?: string; -} - -export interface AccountConfig { - game_app: string; - account?: string; - password?: string; -} - -export interface DailyAutomation { - auto_expedition: boolean; - expedition_interval: number; // 远征检查间隔(分钟) - auto_battle: boolean; - battle_type: string; - battle_times: number; - auto_exercise: boolean; - exercise_fleet_id: number; - auto_normal_fight: boolean; - auto_decisive: boolean; - decisive_ticket_reserve: number; - decisive_template_id: string; - auto_loot: boolean; - loot_plan_index: number; // builtin_farm_loot 中选择的方案索引 - loot_stop_count: number; // 战利品达到此数量时停止 -} - -export interface UserSettings { - emulator: EmulatorConfig; - account: AccountConfig; - daily_automation: DailyAutomation; +export interface StopCondition { + loot_count_ge?: number; + ship_count_ge?: number; } -// ════════════════════════════════════════ -// 常量映射 -// ════════════════════════════════════════ - export const FORMATION_NAMES: Record = { 1: '单纵阵', 2: '复纵阵', @@ -142,112 +202,65 @@ export const REPAIR_MODE_NAMES: Record = { 2: '大破才修', }; -// ════════════════════════════════════════ -// 停止条件 (Stop Condition) -// ════════════════════════════════════════ - -/** 停止条件: 满足时自动终止任务循环 */ -export interface StopCondition { - /** 战利品数量达到上限时停止 */ - loot_count_ge?: number; - /** 舰船获取数量达到上限时停止 */ - ship_count_ge?: number; -} - -// ════════════════════════════════════════ -// 任务预设 (Task Preset) -// ════════════════════════════════════════ - -/** 任务预设 YAML 解析后的结构 (task_type 字段用于区分) */ export interface TaskPreset { - task_type: 'normal_fight' | 'event_fight' | 'campaign' | 'exercise' | 'decisive'; - // normal_fight / event_fight + task_type: + | 'normal_fight' + | 'event_fight' + | 'campaign' + | 'exercise' + | 'decisive'; plan_id?: string; times?: number; gap?: number; fleet_id?: number; - // campaign campaign_name?: string; - // decisive chapter?: number; level1?: string[]; level2?: string[]; flagship_priority?: string[]; - /** 决战是否启用快修(桶修) */ use_quick_repair?: boolean; - // 停止条件 stop_condition?: StopCondition; - /** 定时触发时间 "HH:MM" 格式 */ scheduled_time?: string; } -// ════════════════════════════════════════ -// 任务模板 (Task Template) -// ════════════════════════════════════════ - -/** 模板类型 */ -export type TemplateType = 'normal_fight' | 'event_fight' | 'exercise' | 'campaign' | 'decisive'; +export type TemplateType = + | 'normal_fight' + | 'event_fight' + | 'exercise' + | 'campaign' + | 'decisive'; -/** 任务模板:可复用的任务蓝图 */ export interface TaskTemplate { id: string; name: string; type: TemplateType; createdAt: string; - - /** 是否为内置模板(只读,不可删除/编辑) */ builtin?: boolean; - /** 内置模板的描述说明 */ description?: string; - /** 是否强制重试失败任务(重试时插回同优先级队首,避免跳到下一条) */ forceRetry?: boolean; - /** 是否允许同优先级轮询(true=轮询,false/未设置=连续执行当前任务直至次数结束) */ allowPolling?: boolean; - - // normal_fight / event_fight - planPath?: string; // 引用的方案文件路径(单方案,向后兼容) - planPaths?: string[]; // 可选方案列表(多方案模板) + planPath?: string; + planPaths?: string[]; fleet_id?: number; - fleet?: string[]; // 编队舰船名称 (6 个位置) - - // exercise - // fleet_id 已定义 - - // campaign + fleet?: string[]; campaign_name?: string; - - // decisive chapter?: number; level1?: string[]; level2?: string[]; flagship_priority?: string[]; - /** 决战是否启用快修(桶修) */ use_quick_repair?: boolean; - - // 默认运行时参数 defaultTimes?: number; defaultGap?: number; defaultStopCondition?: StopCondition; } -// ════════════════════════════════════════ -// 泡澡修理配置 (Bath Repair) -// ════════════════════════════════════════ - -/** 单船修理阈值: 血量低于阈值时送入泡澡 */ export interface RepairThreshold { - /** 阈值类型: percent=百分比 (如0.25=25%), absolute=绝对值 (如13点HP) */ type: 'percent' | 'absolute'; - /** 阈值数值 */ value: number; } -/** 任务级泡澡修理配置 */ export interface BathRepairConfig { - /** 是否启用泡澡修理(启用后不使用快修,等待泡澡完成) */ enabled: boolean; - /** 默认修理阈值(适用于所有舰船) */ defaultThreshold: RepairThreshold; - /** 按舰船名覆盖修理阈值 (显示名 → 阈值) */ shipThresholds?: Record; } diff --git a/src/types/scheduler.ts b/src/types/scheduler.ts index 2195c5f..98425cf 100644 --- a/src/types/scheduler.ts +++ b/src/types/scheduler.ts @@ -1,9 +1,11 @@ -/** - * Scheduler 公共类型定义。 - * 从 Scheduler.ts 提取,供 Controller / View 层直接引用。 - */ -import type { TaskRequest, TaskResult, WsLogMessage } from './api'; -import type { StopCondition, BathRepairConfig, FleetPreset } from './model'; +/** 定义任务队列、调度状态及 Scheduler 对外回调契约。 */ +import type { TaskRequest, TaskResult, WsLogMessage } from './api.js'; +import type { + StopCondition, + BathRepairConfig, + FleetPreset, + BattleResultGrade, +} from './model.js'; // ════════════════════════════════════════ // 任务队列项 @@ -26,6 +28,8 @@ export type SchedulerTaskType = export interface SchedulerTask { id: string; + /** Stable identity for all single-round follow-up tasks. */ + logicalId: string; name: string; type: SchedulerTaskType; priority: TaskPriority; @@ -34,6 +38,8 @@ export interface SchedulerTask { remainingTimes: number; /** 总次数(用于显示进度) */ totalTimes: number; + /** 后端 times=None:任务不受次数限制,完成一轮后继续排队。 */ + unlimited?: boolean; /** 后端返回的 task_id (仅当前正在运行的任务有值) */ backendTaskId?: string; /** 可选的停止条件: 每轮完成后检查,满足则不再后触发 */ @@ -56,6 +62,8 @@ export interface SchedulerTask { currentPresetIndex?: number; /** 终点节点列表:经过其中任一节点即认定本轮完成。未设置时回退到最后一个 selected_node。 */ endpointNodes?: string[]; + /** 终点节点的最低战果要求;未设置时仅判断是否经过终点。 */ + endpointResult?: BattleResultGrade; /** 同优先级内排序键(数值越小越靠前),用于周常等需要严格按章节顺序执行的场景 */ sortKey?: number; } @@ -66,6 +74,19 @@ export interface SchedulerTask { export type SchedulerStatus = 'idle' | 'running' | 'stopping' | 'not_connected'; +/** 逻辑任务被取消的来源,用于区分用户放弃和系统关闭。 */ +export type LogicalTaskCancelReason = + | 'removed' + | 'queue_cleared' + | 'system_stopped'; + +/** 尚未回到就绪队列的任务,例如轮次间隔或失败重试。 */ +export interface SchedulerWaitingTask { + task: SchedulerTask; + reason: 'gap' | 'retry'; + readyAt: number; +} + // ════════════════════════════════════════ // 事件回调 // ════════════════════════════════════════ @@ -77,6 +98,13 @@ export interface SchedulerCallbacks { onProgressUpdate?: (taskId: string, progress: { current: number; total: number; node: string | null }) => void; /** 任务完成 (单轮) */ onTaskCompleted?: (taskId: string, success: boolean, result?: TaskResult | null, error?: string | null) => void; + /** Emitted only when the logical task has no follow-up round. */ + onLogicalTaskCompleted?: (logicalId: string, success: boolean, error?: string | null) => void; + /** 逻辑任务被删除、清空或随系统停止,不等同于完成或失败。 */ + onLogicalTaskCanceled?: ( + logicalId: string, + reason: LogicalTaskCancelReason, + ) => void; /** 新日志消息 */ onLog?: (msg: WsLogMessage) => void; /** 队列变化 */ diff --git a/src/types/statistics.ts b/src/types/statistics.ts new file mode 100644 index 0000000..1ae269a --- /dev/null +++ b/src/types/statistics.ts @@ -0,0 +1,32 @@ +/** 今日出征统计在 Model、Controller 和 View 之间共享的数据结构。 */ + +export const BATTLE_GRADES = ['SS', 'S', 'A', 'B', 'C', 'D'] as const; + +export type BattleGrade = typeof BATTLE_GRADES[number]; + +export type BattleGradeCounts = Record; + +export interface ShipDropCount { + name: string; + count: number; +} + +export interface ShipDropNotice { + shipName: string; + dailyIndex: number; + visibleUntil: number; +} + +export interface DailySortieStatsSnapshot { + battleCount: number; + grades: BattleGradeCounts; + quickRepairCount: number; + bathRepairCount: number; + lootCount: number; + lootLimit: number; + shipCount: number; + shipLimit: number; + expeditionCount: number; + shipDrops: ShipDropCount[]; + dropNotice: ShipDropNotice | null; +} diff --git a/src/types/view.ts b/src/types/view.ts index 20886b2..340eaf4 100644 --- a/src/types/view.ts +++ b/src/types/view.ts @@ -1,80 +1,215 @@ -/** - * ViewObject 接口定义 —— Controller 传递给 View 的唯一数据结构。 - * View 层只认识这些接口,不依赖任何 Model 或后端类型。 - */ +/** 定义 Controller 交给各页面渲染的 ViewObject、表单值和展示状态。 */ -// ════════════════════════════════════════ -// 主页面 ViewObject -// ════════════════════════════════════════ +import type { + PlanPresetSource, + ShipLibraryLabels, + ShipLibraryShip, +} from './ipc.js'; +import type { + EventMapCatalogEntry, + NormalFightTaskConfig, + ShipSlot, +} from './model.js'; +import type { DailySortieStatsSnapshot } from './statistics.js'; +import type { + LootAutomationPlan, + LootPlanSource, +} from '../shared/lootPlans.js'; +import type { + DecisiveAutomationSource, +} from '../shared/decisiveAutomation.js'; -/** 当前运行状态 */ -export type AppStatus = 'idle' | 'running' | 'stopping' | 'error' | 'not_connected'; +export interface ConfigViewObject { + emulatorType: string; + emulatorPath: string; + emulatorSerial: string; + gameApp: string; + updateMode: 'auto' | 'manual'; + autoExpedition: boolean; + expeditionInterval: number; + autoBattle: boolean; + battleType: string; + autoExercise: boolean; + exerciseFleetId: number; + battleTimes: number; + autoNormalFight: boolean; + normalFightTasks: NormalFightTaskConfig[]; + autoDecisive: boolean; + decisiveTemplateId: DecisiveAutomationSource; + autoLoot: boolean; + lootPlanSource: LootPlanSource; + lootPlanId: string; + lootPlans: LootAutomationPlan[]; + lootStopCount: number; + logLevel: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL'; + logRoot: string; + themeMode: 'dark' | 'light' | 'system'; + accentColor: string; + debugMode: boolean; + backendPort: number; + backendStartupMode: 'managed' | 'external'; + backendRepoPath: string; + ocrGpuMode: 'auto' | 'cpu' | 'cuda'; + ocrGpu: boolean; + ocrMirror: 'origin' | 'github' | 'tencent' | 'modelscope'; + ocrConfidence: number; + shipNameAliasesText: string; + shipNameCorrectionsText: string; + cudaPath: string; + saveBackendScreenshots: boolean; + pythonPath: string; + defaultWindowWidth: number; + defaultWindowHeight: number; + rememberWindowBounds: boolean; + operationDelayMin: number; + operationDelayMax: number; + dockFullDestroy: boolean; + repairManually: boolean; + bathroomCount: number; + destroyShipWorkMode: number; + destroyShipTypes: string[]; + removeEquipmentMode: boolean; + planRoot: string; +} + +export type AppStatus = + | 'idle' + | 'running' + | 'stopping' + | 'error' + | 'not_connected'; + +export interface CurrentFleetShipVO { + name: string; + searchName?: string; +} -/** 主页面 VO */ export interface MainViewObject { status: AppStatus; statusText: string; currentTask: TaskViewObject | null; - expeditionTimer: string; // 如 "12:34" 下次远征检查倒计时 + currentFleet: CurrentFleetShipVO[]; + dailySortieStats: DailySortieStatsSnapshot; + expeditionTimer: string; taskQueue: TaskQueueItemVO[]; wsConnected: boolean; - /** 当前正在运行的任务 ID(与 taskQueue 中的 id 对应) */ runningTaskId: string | null; } -/** 正在执行的任务 */ export interface TaskViewObject { name: string; - type: 'normal_fight' | 'event_fight' | 'campaign' | 'exercise' | 'expedition' | 'decisive'; - progress: string; // 如 "2/5" + type: + | 'normal_fight' + | 'event_fight' + | 'campaign' + | 'exercise' + | 'expedition' + | 'decisive'; + progress: string; startedAt: string; } -/** 队列中排队的任务 */ export interface TaskQueueItemVO { id: string; name: string; - priorityLabel: string; // "远征" | "用户" | "日常" + priorityLabel: string; remaining: number; totalTimes: number; - /** 进度文本,如 "2/5",仅当前运行的任务有值 */ + unlimited?: boolean; + /** 任务正在轮次间隔或失败重试倒计时中。 */ + waiting?: boolean; + waitingText?: string; progress?: string; - /** 进度百分比 0~1,用于进度条 */ progressPercent?: number; - /** 实时资源文本(后端出征面板 OCR 结果),如 "装备 3/200 | 舰船 253/500" */ acquisitionText?: string; } -/** 日志条目 */ export interface LogEntryVO { - time: string; // HH:MM:SS - level: string; // debug/info/warning/error + time: string; + level: string; channel: string; message: string; } -// ════════════════════════════════════════ -// Plan 预览页 ViewObject -// ════════════════════════════════════════ +export interface FleetRuleDraftViewObject { + shipTypes: string[]; + levelEnabled: boolean; + minLevel: number | null; + maxLevel: number | null; +} + +export interface FleetCandidateDraftViewObject + extends FleetRuleDraftViewObject { + ship: ShipLibraryShip | null; +} + +export interface FleetSlotDraftViewObject + extends FleetRuleDraftViewObject { + primary: ShipLibraryShip | null; + candidates: FleetCandidateDraftViewObject[]; +} -/** 地图节点类型 */ -export type MapNodeType = 'Start' | 'Normal' | 'Boss' | 'Resource' | 'Penalty' | 'Suppress' | 'Aerial' | 'Hard'; +export interface FleetDraftViewObject { + name: string; + slots: FleetSlotDraftViewObject[]; +} + +export interface FleetShipLibraryViewObject { + labels: ShipLibraryLabels; + ships: ShipLibraryShip[]; + colorfulBackgroundUrl: string; +} + +export interface TeamPlanShipRuleViewObject { + name: string; + searchName?: string; + shipTypes: string[]; + minLevel?: number; + maxLevel?: number; +} + +export interface TeamPlanSlotViewObject { + primary?: TeamPlanShipRuleViewObject; + candidates: TeamPlanShipRuleViewObject[]; +} + +export interface TeamPlanViewObject { + id: string; + name: string; + source: PlanPresetSource; + modifiedAt?: number; + selected: boolean; + ships: TeamPlanSlotViewObject[]; +} + +export interface TeamPlanListViewObject { + plans: TeamPlanViewObject[]; + errorCount: number; +} + +export type MapNodeType = + | 'Start' + | 'Normal' + | 'Boss' + | 'Resource' + | 'Penalty' + | 'Suppress' + | 'Aerial' + | 'Hard'; -/** 节点信息 */ export interface NodeViewObject { - id: string; // 节点名,如 "A", "B", "M" - formation: string; // 阵型中文名 + id: string; + formation: string; night: boolean; proceed: boolean; - hasCustomRules: boolean; // 是否有自定义 enemy_rules - note: string; // 简要备注 - nodeType: MapNodeType; // 地图数据中的节点类型 - detour: boolean; // 是否为迂回点 - mapNight: boolean; // 地图数据中标记为夜战点 - position?: [number, number]; // 地图上的坐标 (已缩放) + hasCustomRules: boolean; + note: string; + nodeType: MapNodeType; + detour: boolean; + mapNight: boolean; + position?: [number, number]; } -/** 地图边 (连线) */ export interface MapEdgeVO { from: [number, number]; to: [number, number]; @@ -82,75 +217,96 @@ export interface MapEdgeVO { toId: string; } -/** Plan 预览 VO */ export interface PlanPreviewViewObject { fileName: string; chapter: number | string; map: number | string; - mapName: string; // "7-4" 格式 - repairModeValue: number; // 1 或 2 - fightConditionValue: number; // 1-5 - fleetId: number; // 1-4 + mapName: string; + event?: string; + eventMaps: EventMapCatalogEntry[]; + repairModeValue: number; + fightConditionValue: number; + fleetId: number; selectedNodes: NodeViewObject[]; - comment: string; // yaml 文件顶部注释 - /** 所有地图节点(含未选中的),用于地图可视化 */ + comment: string; allNodes?: NodeViewObject[]; - /** 地图连线 */ edges?: MapEdgeVO[]; - /** 编队预设列表 */ + mapAspectRatio?: number; fleetPresets?: FleetPresetVO[]; - /** 任务配置 */ times?: number; gap?: number; lootCountGe?: number; shipCountGe?: number; } -/** 编队预设 VO */ export interface FleetPresetVO { name: string; - ships: import('./model.js').ShipSlot[]; + ships: ShipSlot[]; } -// ════════════════════════════════════════ -// 配置页 ViewObject -// ════════════════════════════════════════ - -export interface ConfigViewObject { - emulatorType: string; - emulatorPath: string; - emulatorSerial: string; - gameApp: string; - updateMode: 'auto' | 'manual'; - autoExpedition: boolean; - expeditionInterval: number; - autoBattle: boolean; - battleType: string; - autoExercise: boolean; - exerciseFleetId: number; - battleTimes: number; - autoNormalFight: boolean; - autoDecisive: boolean; - decisiveTicketReserve: number; - decisiveTemplateId: string; - autoLoot: boolean; - lootPlanIndex: number; - lootStopCount: number; - themeMode: 'dark' | 'light' | 'system'; - accentColor: string; - debugMode: boolean; - backendPort: number; - backendStartupMode: 'managed' | 'external'; - backendRepoPath: string; - ocrGpuMode: 'auto' | 'cpu' | 'cuda'; - cudaPath: string; - saveBackendScreenshots: boolean; +export interface SetupWizardVO { + emuType: string; + serial: string; pythonPath: string; } -// ════════════════════════════════════════ -// 模板库 ViewObject -// ════════════════════════════════════════ +export interface PresetDetailVO { + name: string; + typeLabel: string; + taskType: string; + fleetId?: number; + exerciseFleetId?: number; + campaignName?: string; + chapter?: number; + level1?: string[]; + level2?: string[]; + flagshipPriority?: string[]; + useQuickRepair?: boolean; + planId?: string; + times?: number; +} + +export interface PresetFormValues { + times: number; + exerciseFleetId?: number; + campaignName?: string; + chapter?: number; + level1?: string[]; + level2?: string[]; + flagshipPriority?: string[]; + useQuickRepair?: boolean; + planId?: string; + fightFleetId?: number; +} + +export interface TaskGroupItemViewObject { + path?: string; + managedSource?: PlanPresetSource; + managedFile?: string; + dailySource?: PlanPresetSource; + dailyFile?: string; + dailyTaskType?: 'exercise' | 'campaign' | 'decisive'; + templateId?: string; + kind: 'plan' | 'preset' | 'template' | 'daily'; + times: number; + label: string; +} + +export interface TaskGroupItemMeta { + mapName?: string; + fleetId?: number; + repairMode?: string; + typeLabel?: string; + fleet?: string[]; + fleetPresetName?: string; +} + +export interface TaskGroupViewObject { + groups: ReadonlyArray<{ name: string; itemCount: number }>; + activeGroupName: string; + items: ReadonlyArray; + itemMetas?: ReadonlyArray; +} export interface TemplateLibraryItemVO { id: string; @@ -163,28 +319,19 @@ export interface TemplateLibraryItemVO { isBuiltin: boolean; } -// ════════════════════════════════════════ -// 模板向导 ViewObject -// ════════════════════════════════════════ - -/** 向导 collectForm() 返回的表单数据 */ export interface WizardFormData { type: string; name: string; defaultTimes: number; stopLoot: number; stopShip: number; - /** 普通出击 */ planPath?: string; fleetId?: number; fleetNf?: string[]; - /** 演习 */ exerciseFleetId?: number; fleetEx?: string[]; - /** 战役 */ campaignName?: string; fleetCp?: string[]; - /** 决战 */ chapter?: number; level1?: string[]; level2?: string[]; @@ -192,7 +339,6 @@ export interface WizardFormData { useQuickRepair?: boolean; } -/** 向导预填数据 (editTemplate / importTemplate 时传入) */ export interface WizardPrefillData { type?: string; name?: string; @@ -207,63 +353,14 @@ export interface WizardPrefillData { level2?: string[]; flagship_priority?: string[]; use_quick_repair?: boolean; - defaultStopCondition?: { loot_count_ge?: number; ship_count_ge?: number }; + defaultStopCondition?: { + loot_count_ge?: number; + ship_count_ge?: number; + }; } -/** 选择器弹窗选项 */ export interface SelectorOption { icon: string; label: string; sublabel?: string; } - -// ════════════════════════════════════════ -// 首次运行引导 ViewObject -// ════════════════════════════════════════ - -export interface SetupWizardVO { - emuType: string; - serial: string; - pythonPath: string; -} - -// ════════════════════════════════════════ -// 任务预设 ViewObject -// ════════════════════════════════════════ - -/** 预设详情面板数据 */ -export interface PresetDetailVO { - name: string; - typeLabel: string; - taskType: string; - fleetId?: number; - exerciseFleetId?: number; - campaignName?: string; - chapter?: number; - level1?: string[]; - level2?: string[]; - flagshipPriority?: string[]; - useQuickRepair?: boolean; - planId?: string; - times?: number; -} - -/** 预设表单收集值 */ -export interface PresetFormValues { - times: number; - exerciseFleetId?: number; - campaignName?: string; - chapter?: number; - level1?: string[]; - level2?: string[]; - flagshipPriority?: string[]; - useQuickRepair?: boolean; - planId?: string; - fightFleetId?: number; -} - -/** 新建方案表单值 */ -export interface NewPlanFormValues { - chapter: string; - map: number; -} diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 12075e9..6c6b88f 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,3 +1,4 @@ +/** 提供统一日志级别、频道和控制台输出格式。 */ /** * Logger —— 集中式前端日志工具。 * @@ -41,13 +42,12 @@ function dateTag(): string { class LoggerImpl { private opts: LoggerOptions | null = null; private buffer: string[] = []; - private flushTimer: ReturnType | null = null; /** 初始化 Logger(应在 AppController.initAsync 中调用一次) */ init(opts: LoggerOptions): void { this.opts = opts; // 定时 flush,避免频繁 IPC - this.flushTimer = setInterval(() => this.flush(), 2000); + setInterval(() => this.flush(), 2000); } info(message: string, channel = 'GUI'): void { diff --git a/src/view/config/ConfigView.ts b/src/view/config/ConfigView.ts index de2d2c1..11bf602 100644 --- a/src/view/config/ConfigView.ts +++ b/src/view/config/ConfigView.ts @@ -1,123 +1,147 @@ +/** 渲染设置页面、收集表单输入并发出保存和检测意图。 */ /** - * ConfigView —— 配置页纯渲染组件。 + * ConfigView —— 设置页纯渲染组件。 * 接收 ConfigViewObject 填充表单,用户修改后由 Controller 收集。 */ -import type { ConfigViewObject } from '../../types/view'; +import type { NormalFightTaskConfig } from '../../types/model.js'; +import type { ConfigViewObject } from '../../types/view.js'; +import { + DEFAULT_LOOT_PLAN_ID, + findLootAutomationPlan, + lootAutomationPlanKey, + normalizeLootAutomationPlans, + type LootAutomationPlan, + type LootPlanSource, +} from '../../shared/lootPlans.js'; +import { + normalizeDecisiveAutomationSource, +} from '../../shared/decisiveAutomation.js'; + +type StatusKind = 'ok' | 'error' | 'unknown'; + +function element(id: string): T { + const target = document.getElementById(id); + if (!target) throw new Error(`设置控件不存在: ${id}`); + return target as T; +} export class ConfigView { - private emuType: HTMLSelectElement; - private emuPath: HTMLInputElement; - private emuSerial: HTMLInputElement; - private gameApp: HTMLSelectElement; - private updateMode: HTMLSelectElement; - private autoExpedition: HTMLInputElement; - private expeditionInterval: HTMLInputElement; - private autoBattle: HTMLInputElement; - private battleType: HTMLSelectElement; - private autoExercise: HTMLInputElement; - private exerciseFleetId: HTMLSelectElement; - private battleTimes: HTMLInputElement; - private autoNormalFight: HTMLInputElement; - private autoDecisive: HTMLInputElement; - private decisiveTicketReserve: HTMLInputElement; - private decisiveTemplate: HTMLSelectElement; - private autoLoot: HTMLInputElement; - private lootPlan: HTMLSelectElement; - private lootStopCount: HTMLInputElement; - private autoExpeditionBody: HTMLElement | null; - private autoBattleBody: HTMLElement | null; - private autoExerciseBody: HTMLElement | null; - private autoDecisiveBody: HTMLElement | null; - private autoLootBody: HTMLElement | null; - private themeMode: HTMLSelectElement; - private accentColor: HTMLInputElement; - private accentLabel: HTMLElement; - private debugMode: HTMLInputElement; - private backendPort: HTMLInputElement; - private backendStartupMode: HTMLInputElement; - private backendRepoPath: HTMLInputElement; - private ocrGpuMode: HTMLSelectElement; - private cudaPath: HTMLInputElement; - private cudaStatus: HTMLElement | null; - private validateCudaBtn: HTMLButtonElement | null; - private saveBackendScreenshots: HTMLInputElement; - private debugAdvancedWrap: HTMLElement | null; - private backendRepoWrap: HTMLElement | null; - private pythonPath: HTMLInputElement; - private pythonStatus: HTMLElement | null; - private adbStatus: HTMLElement | null; - private validatePythonBtn: HTMLButtonElement | null; + private configTabs = Array.from(document.querySelectorAll('[data-config-tab]')); + private configPanels = Array.from(document.querySelectorAll('[data-config-panel]')); + private configTabDescription = document.getElementById('config-tab-description'); + private normalFightTasks: NormalFightTaskConfig[] = []; + private normalFightFleetNames = new Map(); + private lootPlans: LootAutomationPlan[] = []; + + private emuType = element('cfg-emu-type'); + private emuPath = element('cfg-emu-path'); + private emuSerial = element('cfg-emu-serial'); + private gameApp = element('cfg-game-app'); + private updateMode = element('cfg-update-mode'); + private autoExpedition = element('cfg-auto-expedition'); + private expeditionInterval = element('cfg-expedition-interval'); + private autoBattle = element('cfg-auto-battle'); + private battleType = element('cfg-battle-type'); + private autoExercise = element('cfg-auto-exercise'); + private exerciseFleetId = element('cfg-exercise-fleet'); + private battleTimes = element('cfg-battle-times'); + private autoNormalFight = element('cfg-auto-normal-fight'); + private autoDecisive = element('cfg-auto-decisive'); + private decisiveTemplate = element('cfg-decisive-template'); + private autoLoot = element('cfg-auto-loot'); + private lootPlan = element('cfg-loot-plan'); + private lootStopCount = element('cfg-loot-stop-count'); + private normalFightTaskList = element('cfg-normal-fight-tasks'); + + private logLevel = element('cfg-log-level'); + private logRoot = element('cfg-log-root'); + private themeMode = element('cfg-theme-mode'); + private accentColor = element('cfg-accent-color'); + private accentLabel = element('cfg-accent-label'); + private debugMode = element('cfg-debug-mode'); + private backendPort = element('cfg-backend-port'); + private backendStatus = document.getElementById('cfg-backend-status'); + private backendStartupMode = element('cfg-use-external-backend'); + private backendRepoPath = element('cfg-backend-repo-path'); + private ocrGpuMode = element('cfg-ocr-gpu-mode'); + private ocrGpu = element('cfg-ocr-gpu'); + private ocrMirror = element('cfg-ocr-mirror'); + private ocrConfidence = element('cfg-ocr-confidence'); + private ocrConfidenceRange = element('cfg-ocr-confidence-range'); + private shipNameAliases = element('cfg-ship-name-aliases'); + private shipNameCorrections = element('cfg-ship-name-corrections'); + private cudaPath = element('cfg-cuda-path'); + private cudaStatus = document.getElementById('cfg-cuda-status'); + private validateCudaBtn = document.getElementById('btn-validate-cuda') as HTMLButtonElement | null; + private saveBackendScreenshots = element('cfg-save-backend-screenshots'); + private debugAdvancedWrap = document.getElementById('cfg-debug-advanced'); + private backendRepoWrap = document.getElementById('cfg-backend-repo-wrap'); + private pythonPath = element('cfg-python-path'); + private pythonStatus = document.getElementById('cfg-python-status'); + private adbStatus = document.getElementById('cfg-adb-status'); + private validatePythonBtn = document.getElementById('btn-validate-python') as HTMLButtonElement | null; + private shipLibraryStatus = document.getElementById('ship-library-status'); + private updateShipLibraryBtn = document.getElementById('btn-update-ship-library') as HTMLButtonElement | null; + private defaultWindowWidth = element('cfg-window-width'); + private defaultWindowHeight = element('cfg-window-height'); + private rememberWindowBounds = element('cfg-remember-window-bounds'); + + private delayMin = element('cfg-delay-min'); + private delayMinRange = element('cfg-delay-min-range'); + private delayMax = element('cfg-delay-max'); + private delayMaxRange = element('cfg-delay-max-range'); + private dockFullDestroy = element('cfg-dock-full-destroy'); + private repairManually = element('cfg-repair-manually'); + private bathroomCount = element('cfg-bathroom-count'); + private destroyShipWorkMode = element('cfg-destroy-ship-mode'); + private destroyShipTypes = element('cfg-destroy-ship-types'); + private removeEquipmentMode = element('cfg-remove-equipment-mode'); + private planRoot = element('cfg-plan-root'); constructor() { - this.emuType = document.getElementById('cfg-emu-type') as HTMLSelectElement; - this.emuPath = document.getElementById('cfg-emu-path') as HTMLInputElement; - this.emuSerial = document.getElementById('cfg-emu-serial') as HTMLInputElement; - this.gameApp = document.getElementById('cfg-game-app') as HTMLSelectElement; - this.updateMode = document.getElementById('cfg-update-mode') as HTMLSelectElement; - this.autoExpedition = document.getElementById('cfg-auto-expedition') as HTMLInputElement; - this.expeditionInterval = document.getElementById('cfg-expedition-interval') as HTMLInputElement; - this.autoBattle = document.getElementById('cfg-auto-battle') as HTMLInputElement; - this.battleType = document.getElementById('cfg-battle-type') as HTMLSelectElement; - this.autoExercise = document.getElementById('cfg-auto-exercise') as HTMLInputElement; - this.exerciseFleetId = document.getElementById('cfg-exercise-fleet') as HTMLSelectElement; - this.battleTimes = document.getElementById('cfg-battle-times') as HTMLInputElement; - this.autoNormalFight = document.getElementById('cfg-auto-normal-fight') as HTMLInputElement; - this.autoDecisive = document.getElementById('cfg-auto-decisive') as HTMLInputElement; - this.decisiveTicketReserve = document.getElementById('cfg-decisive-ticket-reserve') as HTMLInputElement; - this.decisiveTemplate = document.getElementById('cfg-decisive-template') as HTMLSelectElement; - this.autoLoot = document.getElementById('cfg-auto-loot') as HTMLInputElement; - this.lootPlan = document.getElementById('cfg-loot-plan') as HTMLSelectElement; - this.lootStopCount = document.getElementById('cfg-loot-stop-count') as HTMLInputElement; - this.autoExpeditionBody = document.getElementById('cfg-auto-expedition-body'); - this.autoBattleBody = document.getElementById('cfg-auto-battle-body'); - this.autoExerciseBody = document.getElementById('cfg-auto-exercise-body'); - this.autoDecisiveBody = document.getElementById('cfg-auto-decisive-body'); - this.autoLootBody = document.getElementById('cfg-auto-loot-body'); - this.themeMode = document.getElementById('cfg-theme-mode') as HTMLSelectElement; - this.accentColor = document.getElementById('cfg-accent-color') as HTMLInputElement; - this.accentLabel = document.getElementById('cfg-accent-label')!; - this.debugMode = document.getElementById('cfg-debug-mode') as HTMLInputElement; - this.backendPort = document.getElementById('cfg-backend-port') as HTMLInputElement; - this.backendStartupMode = document.getElementById('cfg-use-external-backend') as HTMLInputElement; - this.backendRepoPath = document.getElementById('cfg-backend-repo-path') as HTMLInputElement; - this.ocrGpuMode = document.getElementById('cfg-ocr-gpu-mode') as HTMLSelectElement; - this.cudaPath = document.getElementById('cfg-cuda-path') as HTMLInputElement; - this.cudaStatus = document.getElementById('cfg-cuda-status'); - this.validateCudaBtn = document.getElementById('btn-validate-cuda') as HTMLButtonElement | null; - this.saveBackendScreenshots = document.getElementById('cfg-save-backend-screenshots') as HTMLInputElement; - this.debugAdvancedWrap = document.getElementById('cfg-debug-advanced'); - this.backendRepoWrap = document.getElementById('cfg-backend-repo-wrap'); - this.pythonPath = document.getElementById('cfg-python-path') as HTMLInputElement; - this.pythonStatus = document.getElementById('cfg-python-status'); - this.adbStatus = document.getElementById('cfg-adb-status'); - this.validatePythonBtn = document.getElementById('btn-validate-python') as HTMLButtonElement | null; - - // 调色盘实时预览 + for (const tab of this.configTabs) { + tab.addEventListener('click', () => this.showConfigTab(tab.dataset['configTab'] ?? 'system')); + } + this.showConfigTab('system'); + this.accentColor.addEventListener('input', () => { this.accentLabel.textContent = this.accentColor.value; }); - this.debugMode.addEventListener('change', () => { this.updateDebugAdvancedVisibility(); this.updateBackendRepoVisibility(); }); - - this.backendStartupMode.addEventListener('change', () => { - this.updateBackendRepoVisibility(); + this.backendStartupMode.addEventListener('change', () => this.updateBackendRepoVisibility()); + this.cudaPath.addEventListener('input', () => { + const hasPath = this.cudaPath.value.trim().length > 0; + this.setCudaStatus( + hasPath ? '待检测' : '系统环境', + 'unknown', + hasPath ? 'CUDA 路径已修改,请点击检测' : 'CUDA 路径留空,将检测当前系统环境', + ); }); + this.lootPlan.addEventListener( + 'change', + () => this.updateLootPlanSelectWidth(), + ); + this.bindNumberRange(this.delayMinRange, this.delayMin); + this.bindNumberRange(this.delayMaxRange, this.delayMax); + this.bindNumberRange(this.ocrConfidenceRange, this.ocrConfidence); - this.autoExpedition.addEventListener('change', () => this.updateAutoOptionVisibility()); - this.autoBattle.addEventListener('change', () => this.updateAutoOptionVisibility()); - this.autoExercise.addEventListener('change', () => this.updateAutoOptionVisibility()); - this.autoDecisive.addEventListener('change', () => this.updateAutoOptionVisibility()); - this.autoLoot.addEventListener('change', () => this.updateAutoOptionVisibility()); + this.ocrGpuMode.addEventListener('change', () => { + if (this.ocrGpuMode.value === 'cpu') this.ocrGpu.checked = false; + if (this.ocrGpuMode.value === 'cuda') this.ocrGpu.checked = true; + }); + this.ocrGpu.addEventListener('change', () => { + this.ocrGpuMode.value = this.ocrGpu.checked ? 'cuda' : 'cpu'; + }); - this.updateDebugAdvancedVisibility(); - this.updateBackendRepoVisibility(); - this.updateAutoOptionVisibility(); + document.querySelectorAll( + '#page-config select.input', + ).forEach(select => this.updateSettingSelectWidth(select)); } - /** 用 ViewObject 填充表单 */ + /** 用 ViewObject 填充表单。 */ render(vo: ConfigViewObject): void { this.emuType.value = vo.emulatorType; this.emuPath.value = vo.emulatorPath; @@ -132,13 +156,17 @@ export class ConfigView { this.exerciseFleetId.value = String(vo.exerciseFleetId); this.battleTimes.value = String(vo.battleTimes); this.autoNormalFight.checked = vo.autoNormalFight; + this.normalFightTasks = structuredClone(vo.normalFightTasks); + this.normalFightFleetNames.clear(); + this.renderNormalFightTasks(); this.autoDecisive.checked = vo.autoDecisive; - this.decisiveTicketReserve.value = String(vo.decisiveTicketReserve); - // 决战模板下拉列表由 Controller 填充 options this.decisiveTemplate.value = vo.decisiveTemplateId; - this.autoLoot.checked = vo.autoLoot; - this.lootPlan.value = String(vo.lootPlanIndex); + this.lootPlans = normalizeLootAutomationPlans(vo.lootPlans, []); + this.renderLootPlanOptions(vo.lootPlanSource, vo.lootPlanId); + this.autoLoot.checked = vo.autoLoot && this.lootPlans.length > 0; this.lootStopCount.value = String(vo.lootStopCount); + this.logLevel.value = vo.logLevel; + this.logRoot.value = vo.logRoot; this.themeMode.value = vo.themeMode; this.accentColor.value = vo.accentColor; this.accentLabel.textContent = vo.accentColor; @@ -147,128 +175,336 @@ export class ConfigView { this.backendStartupMode.checked = vo.backendStartupMode === 'external'; this.backendRepoPath.value = vo.backendRepoPath; this.ocrGpuMode.value = vo.ocrGpuMode; + this.ocrGpu.checked = vo.ocrGpu; + this.ocrMirror.value = vo.ocrMirror; + this.setRangeValue(this.ocrConfidenceRange, this.ocrConfidence, vo.ocrConfidence); + this.shipNameAliases.value = vo.shipNameAliasesText; + this.shipNameCorrections.value = vo.shipNameCorrectionsText; this.cudaPath.value = vo.cudaPath; + this.setCudaStatus( + vo.cudaPath ? '待检测' : '系统环境', + 'unknown', + vo.cudaPath ? '已配置 CUDA 路径,请点击检测' : 'CUDA 路径留空,将检测当前系统环境', + ); this.saveBackendScreenshots.checked = vo.saveBackendScreenshots; this.pythonPath.value = vo.pythonPath; + this.defaultWindowWidth.value = String(vo.defaultWindowWidth); + this.defaultWindowHeight.value = String(vo.defaultWindowHeight); + this.rememberWindowBounds.checked = vo.rememberWindowBounds; + this.setRangeValue(this.delayMinRange, this.delayMin, vo.operationDelayMin); + this.setRangeValue(this.delayMaxRange, this.delayMax, vo.operationDelayMax); + this.dockFullDestroy.checked = vo.dockFullDestroy; + this.repairManually.value = String(vo.repairManually); + this.bathroomCount.value = String(vo.bathroomCount); + this.destroyShipWorkMode.value = String(vo.destroyShipWorkMode); + this.removeEquipmentMode.checked = vo.removeEquipmentMode; + this.planRoot.value = vo.planRoot; + for (const checkbox of Array.from( + this.destroyShipTypes.querySelectorAll('input[type="checkbox"]'), + )) { + checkbox.checked = vo.destroyShipTypes.includes(checkbox.value); + } this.updateDebugAdvancedVisibility(); this.updateBackendRepoVisibility(); - this.updateAutoOptionVisibility(); - } - - private updateDebugAdvancedVisibility(): void { - if (!this.debugAdvancedWrap) return; - const show = this.debugMode.checked; - this.debugAdvancedWrap.style.display = show ? '' : 'none'; - if (!show && this.backendRepoWrap) { - this.backendRepoWrap.style.display = 'none'; - } - } - - private updateBackendRepoVisibility(): void { - if (!this.backendRepoWrap) return; - const show = this.debugMode.checked && this.backendStartupMode.checked; - this.backendRepoWrap.style.display = show ? '' : 'none'; - this.backendRepoPath.required = show; } - private updateAutoOptionVisibility(): void { - if (this.autoExpeditionBody) { - this.autoExpeditionBody.style.display = this.autoExpedition.checked ? '' : 'none'; - } - if (this.autoBattleBody) { - this.autoBattleBody.style.display = this.autoBattle.checked ? '' : 'none'; - } - if (this.autoExerciseBody) { - this.autoExerciseBody.style.display = this.autoExercise.checked ? '' : 'none'; - } - if (this.autoDecisiveBody) { - this.autoDecisiveBody.style.display = this.autoDecisive.checked ? '' : 'none'; + /** 收集并校验当前表单。 */ + collect(): ConfigViewObject { + const operationDelayMin = this.clamp(this.delayMin.value, 0, 10, 0); + const operationDelayMax = this.clamp(this.delayMax.value, 0, 10, 0); + if (operationDelayMin > operationDelayMax) { + throw new Error('全局延迟的最小值不能大于最大值'); } - if (this.autoLootBody) { - this.autoLootBody.style.display = this.autoLoot.checked ? '' : 'none'; + const selectedLootPlan = this.selectedLootPlan(); + if (this.autoLoot.checked && !selectedLootPlan) { + throw new Error('启用自动胖次前必须先加载并选择出征计划'); } - } - - /** 从表单收集当前值 (Controller 调用) */ - collect(): ConfigViewObject { return { emulatorType: this.emuType.value, - emulatorPath: this.emuPath.value, - emulatorSerial: this.emuSerial.value, + emulatorPath: this.emuPath.value.trim(), + emulatorSerial: this.emuSerial.value.trim(), gameApp: this.gameApp.value, updateMode: this.updateMode.value === 'manual' ? 'manual' : 'auto', autoExpedition: this.autoExpedition.checked, - expeditionInterval: Math.max(1, Math.min(120, Number(this.expeditionInterval.value) || 15)), + expeditionInterval: this.clamp(this.expeditionInterval.value, 1, 120, 15), autoBattle: this.autoBattle.checked, battleType: this.battleType.value, autoExercise: this.autoExercise.checked, - exerciseFleetId: Number(this.exerciseFleetId.value) || 1, - battleTimes: Number(this.battleTimes.value) || 3, + exerciseFleetId: Math.trunc(this.clamp(this.exerciseFleetId.value, 1, 4, 1)), + battleTimes: Math.trunc(this.clamp(this.battleTimes.value, 1, 99, 3)), autoNormalFight: this.autoNormalFight.checked, + normalFightTasks: this.collectNormalFightTasks(), autoDecisive: this.autoDecisive.checked, - decisiveTicketReserve: Math.max(0, Number(this.decisiveTicketReserve.value) || 0), - decisiveTemplateId: this.decisiveTemplate.value, + decisiveTemplateId: normalizeDecisiveAutomationSource( + this.decisiveTemplate.value, + ), autoLoot: this.autoLoot.checked, - lootPlanIndex: Number(this.lootPlan.value) || 0, - lootStopCount: Math.max(1, Math.min(50, Number(this.lootStopCount.value) || 50)), - themeMode: this.themeMode.value as 'dark' | 'light' | 'system', + lootPlanSource: selectedLootPlan?.source ?? 'system', + lootPlanId: selectedLootPlan?.file ?? DEFAULT_LOOT_PLAN_ID, + lootPlans: structuredClone(this.lootPlans), + lootStopCount: Math.trunc(this.clamp(this.lootStopCount.value, 1, 50, 50)), + logLevel: this.logLevel.value as ConfigViewObject['logLevel'], + logRoot: this.logRoot.value.trim() || 'log', + themeMode: this.themeMode.value as ConfigViewObject['themeMode'], accentColor: this.accentColor.value, debugMode: this.debugMode.checked, - backendPort: Math.max(1, Math.min(65535, Number(this.backendPort.value) || 8438)), + backendPort: Math.trunc(this.clamp(this.backendPort.value, 1, 65535, 8438)), backendStartupMode: this.backendStartupMode.checked ? 'external' : 'managed', backendRepoPath: this.backendRepoPath.value.trim(), - ocrGpuMode: (['auto', 'cpu', 'cuda'].includes(this.ocrGpuMode.value) ? this.ocrGpuMode.value : 'auto') as 'auto' | 'cpu' | 'cuda', + ocrGpuMode: this.ocrGpuMode.value as ConfigViewObject['ocrGpuMode'], + ocrGpu: this.ocrGpu.checked, + ocrMirror: this.ocrMirror.value as ConfigViewObject['ocrMirror'], + ocrConfidence: this.clamp(this.ocrConfidence.value, 0, 1, 0.65), + shipNameAliasesText: this.shipNameAliases.value, + shipNameCorrectionsText: this.shipNameCorrections.value, cudaPath: this.cudaPath.value.trim(), saveBackendScreenshots: this.saveBackendScreenshots.checked, pythonPath: this.pythonPath.value.trim(), + defaultWindowWidth: Math.trunc(this.clamp(this.defaultWindowWidth.value, 854, 10000, 1280)), + defaultWindowHeight: Math.trunc(this.clamp(this.defaultWindowHeight.value, 480, 10000, 720)), + rememberWindowBounds: this.rememberWindowBounds.checked, + operationDelayMin, + operationDelayMax, + dockFullDestroy: this.dockFullDestroy.checked, + repairManually: this.repairManually.value === 'true', + bathroomCount: Math.trunc(this.clamp(this.bathroomCount.value, 1, 12, 2)), + destroyShipWorkMode: Math.trunc(this.clamp(this.destroyShipWorkMode.value, 0, 2, 0)), + destroyShipTypes: Array.from( + this.destroyShipTypes.querySelectorAll('input[type="checkbox"]:checked'), + checkbox => checkbox.value, + ), + removeEquipmentMode: this.removeEquipmentMode.checked, + planRoot: this.planRoot.value.trim(), + }; + } + + setNormalFightPlan( + path: string, + fleetPresetIndex: number, + fleetName: string, + ): void { + this.normalFightTasks = [{ + name: path, + fleet_preset_index: fleetPresetIndex, + }]; + this.normalFightFleetNames.clear(); + this.normalFightFleetNames.set( + this.normalFightFleetKey(path, fleetPresetIndex), + fleetName, + ); + this.renderNormalFightTasks(); + } + + private collectNormalFightTasks(): NormalFightTaskConfig[] { + return structuredClone(this.normalFightTasks); + } + + private renderNormalFightTasks(): void { + this.normalFightTaskList.replaceChildren(); + if (this.normalFightTasks.length === 0) { + const empty = document.createElement('span'); + empty.className = 'config-empty-note'; + empty.textContent = '尚未加载计划'; + this.normalFightTaskList.appendChild(empty); + return; + } + const primaryTask = this.normalFightTasks[0]; + if (!primaryTask) return; + const name = document.createElement('span'); + const fileName = primaryTask.name.split(/[\\/]/).pop() ?? primaryTask.name; + const displayName = fileName + .replace(/\.ya?ml$/i, '') + .replace(/^bettle-/i, ''); + name.className = 'config-task-name'; + name.title = this.normalFightTasks.map(task => task.name).join('\n'); + name.textContent = this.normalFightTasks.length > 1 + ? `${displayName} 等 ${this.normalFightTasks.length} 个任务` + : displayName; + this.normalFightTaskList.appendChild(name); + + if (primaryTask.fleet_preset_index != null) { + const fleetIndex = primaryTask.fleet_preset_index; + const fleetName = this.normalFightFleetNames.get( + this.normalFightFleetKey(primaryTask.name, fleetIndex), + ) ?? `队伍 ${fleetIndex + 1}`; + const fleetTag = document.createElement('span'); + fleetTag.className = 'tg-fleet-tag'; + fleetTag.textContent = fleetName; + fleetTag.title = `使用队伍:${fleetName}`; + this.normalFightTaskList.appendChild(fleetTag); + } + } + + private normalFightFleetKey(path: string, fleetPresetIndex: number): string { + return `${path.toLowerCase()}\u0000${fleetPresetIndex}`; + } + + private showConfigTab(tag: string): void { + const descriptions: Record = { + system: '管理运行环境、自动任务、日志和界面设置。', + behavior: '管理操作延迟、OCR 识别和舰队相关行为。', }; + for (const tab of this.configTabs) { + const active = tab.dataset['configTab'] === tag; + tab.classList.toggle('active', active); + tab.setAttribute('aria-selected', String(active)); + } + for (const panel of this.configPanels) panel.hidden = panel.dataset['configPanel'] !== tag; + if (this.configTabDescription) this.configTabDescription.textContent = descriptions[tag] ?? ''; } - /* ── 单字段 setter / getter(Controller 用) ── */ + private updateDebugAdvancedVisibility(): void { + if (!this.debugAdvancedWrap) return; + this.debugAdvancedWrap.style.display = this.debugMode.checked ? '' : 'none'; + if (!this.debugMode.checked && this.backendRepoWrap) this.backendRepoWrap.style.display = 'none'; + } - setEmulatorPath(path: string): void { - this.emuPath.value = path; + private updateBackendRepoVisibility(): void { + if (!this.backendRepoWrap) return; + const show = this.debugMode.checked && this.backendStartupMode.checked; + this.backendRepoWrap.style.display = show ? '' : 'none'; + this.backendRepoPath.required = show; } - setPythonPath(path: string): void { - this.pythonPath.value = path; + getLootPlans(): LootAutomationPlan[] { + return structuredClone(this.lootPlans); } - setBackendRepoPath(path: string): void { - this.backendRepoPath.value = path; + setLootPlans(plans: readonly LootAutomationPlan[]): void { + const selected = this.selectedLootPlan(); + this.lootPlans = normalizeLootAutomationPlans(plans, []); + this.renderLootPlanOptions(selected?.source, selected?.file); + if (this.lootPlans.length === 0) this.autoLoot.checked = false; } - setCudaPath(path: string): void { - this.cudaPath.value = path; + private renderLootPlanOptions( + source?: LootPlanSource, + file?: string, + ): void { + const selected = findLootAutomationPlan(this.lootPlans, source, file) + ?? this.lootPlans[0] + ?? null; + this.lootPlan.replaceChildren(); + this.lootPlans.forEach((plan) => { + const option = document.createElement('option'); + option.value = lootAutomationPlanKey(plan); + option.textContent = plan.name; + option.title = `${plan.name} (${plan.file})`; + this.lootPlan.append(option); + }); + this.lootPlan.disabled = this.lootPlans.length === 0; + this.lootPlan.value = selected + ? lootAutomationPlanKey(selected) + : ''; + this.updateLootPlanSelectWidth(); } - getCudaPath(): string { - return this.cudaPath.value.trim(); + private selectedLootPlan(): LootAutomationPlan | null { + return this.lootPlans.find(plan => ( + lootAutomationPlanKey(plan) === this.lootPlan.value + )) ?? null; } - setCudaStatus(text: string, status: 'ok' | 'error' | 'unknown'): void { - if (!this.cudaStatus) return; - this.cudaStatus.textContent = text; - const cls = status === 'ok' ? 'adb-status-online' : status === 'error' ? 'adb-status-offline' : 'adb-status-unknown'; - this.cudaStatus.className = `adb-status ${cls}`; + private updateLootPlanSelectWidth(): void { + const option = this.lootPlan.selectedOptions[0]; + const text = option?.textContent?.trim() || '任务预设'; + this.updateSettingSelectWidth(this.lootPlan); + this.lootPlan.title = option?.title || text; } - setCudaValidateLoading(loading: boolean): void { - if (!this.validateCudaBtn) return; - this.validateCudaBtn.disabled = loading; - this.validateCudaBtn.textContent = loading ? '检测中…' : '检测'; + private updateSettingSelectWidth(select: HTMLSelectElement): void { + const fixedWidth = Number(select.dataset['configSelectWidth']); + if (Number.isFinite(fixedWidth) && fixedWidth > 0) { + select.style.setProperty( + '--config-select-width', + `${fixedWidth}px`, + ); + return; + } + + const style = window.getComputedStyle(select); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) return; + + context.font = style.font; + const contentWidth = Math.max( + 0, + ...Array.from( + select.options, + item => context.measureText(item.text.trim()).width, + ), + ); + const requiredWidth = Math.ceil(contentWidth + 38); + const widthSteps = [96, 124, 168, 204, 240, 280, 320]; + const width = widthSteps.find(step => step >= requiredWidth) + ?? widthSteps[widthSteps.length - 1]!; + select.style.setProperty('--config-select-width', `${width}px`); } - getPythonPath(): string { - return this.pythonPath.value.trim(); + private bindNumberRange(range: HTMLInputElement, number: HTMLInputElement): void { + range.addEventListener('input', () => { number.value = range.value; }); + number.addEventListener('input', () => { range.value = number.value; }); } - setPythonStatus(text: string, status: 'ok' | 'error' | 'unknown'): void { - if (!this.pythonStatus) return; - this.pythonStatus.textContent = text; - const cls = status === 'ok' ? 'adb-status-online' : status === 'error' ? 'adb-status-offline' : 'adb-status-unknown'; - this.pythonStatus.className = `adb-status ${cls}`; + private setRangeValue(range: HTMLInputElement, number: HTMLInputElement, value: number): void { + range.value = String(value); + number.value = String(value); + } + + private clamp(value: string, min: number, max: number, fallback: number): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? Math.max(min, Math.min(max, parsed)) : fallback; + } + + private setStatus(target: HTMLElement | null, text: string, status: StatusKind): void { + if (!target) return; + target.textContent = text; + const cls = status === 'ok' + ? 'adb-status-online' + : status === 'error' + ? 'adb-status-offline' + : 'adb-status-unknown'; + target.className = `adb-status ${cls}`; + } + + setEmulatorPath(path: string): void { this.emuPath.value = path; } + setEmulatorSerial(serial: string): void { this.emuSerial.value = serial; } + setPythonPath(path: string): void { this.pythonPath.value = path; } + setBackendRepoPath(path: string): void { this.backendRepoPath.value = path; } + setLogRoot(path: string): void { this.logRoot.value = path; } + setPlanRoot(path: string): void { this.planRoot.value = path; } + setCudaPath(path: string): void { this.cudaPath.value = path; } + getEmulatorSerial(): string { return this.emuSerial.value.trim(); } + getCudaPath(): string { return this.cudaPath.value.trim(); } + getPythonPath(): string { return this.pythonPath.value.trim(); } + getBackendPort(): number { return Math.trunc(this.clamp(this.backendPort.value, 1, 65535, 8438)); } + + setCudaStatus(text: string, status: StatusKind, details = text): void { + this.setStatus(this.cudaStatus, text, status); + if (this.cudaStatus) this.cudaStatus.title = details; + } + setPythonStatus(text: string, status: StatusKind): void { this.setStatus(this.pythonStatus, text, status); } + setBackendStatus(text: string, status: StatusKind): void { this.setStatus(this.backendStatus, text, status); } + setShipLibraryStatus(text: string, status: StatusKind): void { this.setStatus(this.shipLibraryStatus, text, status); } + setAdbStatus(text: string, status: 'online' | 'offline' | 'unknown'): void { + if (!this.adbStatus) return; + this.adbStatus.title = text; + this.adbStatus.textContent = status === 'online' + ? '在线' + : status === 'offline' + ? '离线' + : text.includes('中') + ? text + : '未检测'; + this.adbStatus.className = `adb-status adb-status-${status}`; + } + + setCudaValidateLoading(loading: boolean): void { + if (!this.validateCudaBtn) return; + this.validateCudaBtn.disabled = loading; + this.validateCudaBtn.textContent = loading ? '检测中…' : '检测'; } setPythonValidateLoading(loading: boolean): void { @@ -277,18 +513,14 @@ export class ConfigView { this.validatePythonBtn.textContent = loading ? '检测中…' : '检测'; } - setEmulatorSerial(serial: string): void { - this.emuSerial.value = serial; + setShipLibraryUpdateLoading(loading: boolean): void { + if (!this.updateShipLibraryBtn) return; + this.updateShipLibraryBtn.disabled = loading; + this.updateShipLibraryBtn.textContent = loading ? '正在更新…' : '更新舰船数据库'; } resetAccentColor(defaultColor: string): void { this.accentColor.value = defaultColor; this.accentLabel.textContent = defaultColor; } - - setAdbStatus(text: string, status: 'online' | 'offline' | 'unknown'): void { - if (!this.adbStatus) return; - this.adbStatus.textContent = text; - this.adbStatus.className = `adb-status adb-status-${status}`; - } } diff --git a/src/view/index.html b/src/view/index.html index 95abc69..3f7d51f 100644 --- a/src/view/index.html +++ b/src/view/index.html @@ -9,11 +9,33 @@ @@ -25,71 +47,151 @@
-
-
-
- 未连接 -
-
- 远征检查倒计时: - --:-- -
-
- - - - - - -
-
- - -
-
-

当前无运行任务

-
- +
+ +
+
+

当前无运行任务

+
+ +
-
-