diff --git a/.claude-plugin/uv-pyright-lsp/.claude-plugin/plugin.json b/.claude-plugin/uv-pyright-lsp/.claude-plugin/plugin.json new file mode 100644 index 0000000000..f4c602be01 --- /dev/null +++ b/.claude-plugin/uv-pyright-lsp/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "uv-pyright-lsp", + "version": "0.1.0", + "description": "Pyright LSP server launched via uv run for project-specific Python type checking", + "author": { + "name": "OneDragon", + "email": "one-dragon@example.com" + }, + "lspServers": { + "uv-pyright": { + "command": "uv", + "args": [ + "run", + "pyright-langserver", + "--stdio" + ], + "extensionToLanguage": { + ".py": "python", + ".pyi": "python" + } + } + } +} diff --git a/.claude-plugin/uv-pyright-lsp/README.md b/.claude-plugin/uv-pyright-lsp/README.md new file mode 100644 index 0000000000..f9c82efa39 --- /dev/null +++ b/.claude-plugin/uv-pyright-lsp/README.md @@ -0,0 +1,122 @@ +# uv-pyright-lsp + +Pyright Language Server Protocol (LSP) integration for Claude Code, launched via `uv run` for project-specific Python type checking. + +## Features + +- **Project-isolated Pyright**: Uses the project's own Python environment and dependencies via `uv` +- **Seamless Integration**: Provides type checking, code completion, go-to-definition, and more +- **No Global Installation**: Works with Pyright installed in the project's virtual environment + +## Supported Extensions + +- `.py` - Python source files +- `.pyi` - Python type stub files + +## How It Works + +This plugin launches Pyright LSP server using `uv run pyright-langserver --stdio`, which: + +1. Runs `pyright-langserver` from the project's virtual environment +2. Communicates with Claude Code via standard input/output (stdio) +3. Provides language features like type checking, autocomplete, and navigation + +## Installation + +The plugin should already be enabled in this project. To verify: + +```bash +# Check if plugin is loaded +claude plugin list +``` + +## Requirements + +This plugin requires: + +1. **uv** - The Python package installer (https://github.com/astral-sh/uv) +2. **Pyright** - Must be installed in the project via uv + +To install Pyright in the current project: + +```bash +uv add pyright +# or +uv pip install pyright +``` + +## Usage + +Once enabled, Claude Code automatically uses this plugin for `.py` and `.pyi` files: + +- **Type Checking**: Automatic type errors detection as you type +- **Code Completion**: Intelligent autocomplete based on type information +- **Go to Definition**: Navigate to function/class definitions +- **Find References**: Find all usages of a symbol +- **Refactoring**: Safe code refactoring with type checking + +## Configuration + +Pyright configuration is loaded from standard locations: + +- `pyrightconfig.json` in project root +- `[tool.pyright]` section in `pyproject.toml` + +Example `pyrightconfig.json`: + +```json +{ + "include": ["src"], + "exclude": ["**/node_modules", + "**/__pycache__", + "src/zzz_od/geometry" + ], + "ignore": [], + "defineConstant": { + "DEBUG": true + }, + "stubPath": "src/typings", + "typeCheckingMode": "standard" +} +``` + +## Troubleshooting + +### Pyright not found + +If you see errors about `pyright-langserver` not being found: + +```bash +# Install pyright in the project +uv pip install pyright + +# Verify installation +uv run pyright --version +``` + +### Type checking not working + +1. Check that `pyrightconfig.json` exists and is valid +2. Verify the Python environment is correctly configured +3. Check Claude Code's LSP output for errors + +### uv command not found + +Install uv following official documentation: https://github.com/astral-sh/uv#installation + +## Comparison with pyright-lsp + +This plugin differs from the official `pyright-lsp` plugin: + +| Feature | pyright-lsp | uv-pyright-lsp | +|---------|-------------|-----------------| +| Pyright installation | Global (npm/pip/pipx) | Project-local (uv) | +| Command | `pyright-langserver` | `uv run pyright-langserver` | +| Isolation | Uses global Pyright | Uses project's virtual environment | +| Best for | General Python development | Project-specific environments | + +## More Information + +- [Pyright Documentation](https://github.com/microsoft/pyright) +- [uv Documentation](https://github.com/astral-sh/uv) +- [Claude Code LSP Integration](https://docs.anthropic.com/claude-code/lsp) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1199f18373..d62aa7ba3b 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -4,6 +4,8 @@ on: push: tags: - 'v*' + pull_request: + types: [opened, synchronize, reopened, labeled] workflow_dispatch: inputs: create_release: @@ -12,15 +14,19 @@ on: default: false type: boolean +concurrency: + group: build-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: CREATE_RELEASE: ${{ (github.event_name == 'workflow_dispatch' && inputs.create_release == true) || startsWith(github.ref, 'refs/tags/') }} jobs: version: + if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'build') }} runs-on: windows-latest outputs: version: ${{ steps.get_version.outputs.version }} - tag: ${{ steps.get_version.outputs.tag }} steps: - name: Checkout code @@ -28,65 +34,18 @@ jobs: with: fetch-depth: 1 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Get version id: get_version shell: pwsh - run: | - if ($env:GITHUB_REF -like 'refs/tags/*') { - # 已由 tag 推送触发,直接使用该 tag 作为版本 - $version = $env:GITHUB_REF.Substring(10) - $tag = $version - } elseif ($env:CREATE_RELEASE -eq 'false') { - # 非 release 构建,使用日期作为版本号 - $version = (Get-Date).ToUniversalTime().AddHours(8).ToString('vyyyy.MMdd.HHmm') - $tag = $version - } else { - # 手动 workflow_dispatch 触发:使用 git 的语义化版本排序,选出最新 tag,然后生成 / 递增 beta 版本 - # --refs 可避免出现带 ^{} 的 peeled 行,--sort=-version:refname 以版本名倒序排序,'v*' 仅匹配语义化版本前缀 - $latestRef = git ls-remote --refs --tags --sort=-version:refname origin 'v*' | - Where-Object { $_ -match 'refs/tags/(v\d+\.\d+\.\d+(?:-beta\.\d+)?)$' } | - Select-Object -First 1 - - if (-not $latestRef) { - # 仓库还没有任何符合语义版本的 tag,初始化为 v0.1.0-beta.1 - $baseVersion = 'v0.1.0' - $betaNumber = 1 - $tag = "$baseVersion-beta.$betaNumber" - } else { - # 行格式: \trefs/tags/ - if ($latestRef -match 'refs/tags/(.+)$') { - $latestTag = $matches[1] - } else { - throw "无法从 ls-remote 输出中解析最新 tag:$latestRef" - } - - if ($latestTag -match '^(v\d+\.\d+\.\d+)-beta\.(\d+)$') { - # 最新即为 beta,直接在 beta 编号上 +1 - $baseVersion = $matches[1] - $betaNumber = [int]$matches[2] + 1 - $tag = "$baseVersion-beta.$betaNumber" - } else { - # 最新为稳定版本,从该稳定版本开始新的 beta 序列 - $latestTag -match '^(v\d+\.\d+\.)(\d+)$' - $baseVersion = $matches[1] + ([int]$matches[2] + 1) - $betaNumber = 1 - $tag = "$baseVersion-beta.$betaNumber" - } - } - - $version = $tag - - git config --global user.email "actions@github.com" - git config --global user.name "GitHub Actions" - # 创建并推送新 beta tag - git tag $tag - git push origin $tag - } - - echo "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - echo "tag=$tag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + run: python tools/ci/get_version.py build: + if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'build') }} runs-on: windows-latest needs: version outputs: @@ -101,7 +60,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11.9' + python-version: '3.11' - name: Install uv shell: pwsh @@ -142,8 +101,10 @@ jobs: echo "__version__ = '$env:RELEASE_VERSION'" | Out-File -FilePath src/one_dragon/version.py -Encoding utf8 .\.venv\Scripts\Activate.ps1 cd deploy - pyinstaller "OneDragon-Installer.spec" - pyinstaller "OneDragon-Launcher.spec" + uv run pyinstaller --noconfirm --clean "OneDragon-Installer.spec" + uv run pyinstaller --noconfirm --clean "OneDragon-Launcher.spec" + uv run pyinstaller --noconfirm --clean "OneDragon-RuntimeLauncher.spec" + Copy-Item -Path "..\src" -Destination "dist\OneDragon-RuntimeLauncher\src" -Recurse -Force - name: Bundle dependencies into wheels shell: pwsh @@ -173,6 +134,14 @@ jobs: name: Launcher path: deploy/dist/OneDragon-Launcher.exe + - name: Upload RuntimeLauncher + if: ${{ env.CREATE_RELEASE == 'false' }} + uses: actions/upload-artifact@v4 + with: + name: RuntimeLauncher + include-hidden-files: true + path: deploy/dist/OneDragon-RuntimeLauncher/ + - name: Upload Wheels if: ${{ env.CREATE_RELEASE == 'false' }} uses: actions/upload-artifact@v4 @@ -185,6 +154,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: dist + include-hidden-files: true if-no-files-found: error path: deploy/dist @@ -198,6 +168,7 @@ jobs: if-no-files-found: error path: | .\deploy\dist\OneDragon-Launcher.exe + .\deploy\dist\OneDragon-RuntimeLauncher\OneDragon-RuntimeLauncher.exe .\deploy\dist\OneDragon-Installer.exe sign: @@ -205,7 +176,9 @@ jobs: needs: - version - build - if: ${{ (github.event_name == 'workflow_dispatch' && inputs.create_release == true) || startsWith(github.ref, 'refs/tags/') }} + if: ${{ github.repository_owner == 'OneDragon-Anything' && + ((github.event_name == 'workflow_dispatch' && inputs.create_release == true) || startsWith(github.ref, 'refs/tags/')) + }} env: SIGNED_DIR: 'signed' SIGNPATH_SIGNING_POLICY_SLUG: 'release-signing' @@ -236,13 +209,22 @@ jobs: - version - build - sign - if: ${{ ((github.event_name == 'workflow_dispatch' && inputs.create_release == true) || startsWith(github.ref, 'refs/tags/')) && needs.sign.result == 'success' }} + if: ${{ always() && + needs.build.result == 'success' && + ((github.event_name == 'workflow_dispatch' && inputs.create_release == true) || startsWith(github.ref, 'refs/tags/')) && + (needs.sign.result == 'success') + }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 1 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Download dist uses: actions/download-artifact@v4 with: @@ -256,209 +238,17 @@ jobs: name: signed path: deploy/dist/signed - - name: Replace unsigned executables with signed ones - if: ${{ needs.sign.result == 'success' }} - shell: pwsh - run: | - Copy-Item "deploy/dist/signed/OneDragon-Installer.exe" -Destination "deploy/dist/OneDragon-Installer.exe" -Force - Copy-Item "deploy/dist/signed/OneDragon-Launcher.exe" -Destination "deploy/dist/OneDragon-Launcher.exe" -Force - - name: Prepare release directory and models shell: pwsh env: RELEASE_VERSION: ${{ needs.version.outputs.version }} - run: | - # 将 dist 移动到当前目录的父目录下 - $distDir = "deploy/dist" - $parentDir = Split-Path -Path (Get-Location) -Parent - $targetDist = Join-Path $parentDir "dist" - - Move-Item -Path $distDir -Destination $targetDist -Force - $distDir = $targetDist - $envDir = ".install" - New-Item -ItemType Directory -Path $envDir -Force | Out-Null - - # 准备离线运行所需的 .install 资源 - Invoke-WebRequest -Uri "https://github.com/OneDragon-Anything/OneDragon-Env/releases/download/ZenlessZoneZero-OneDragon/uv-x86_64-pc-windows-msvc.zip" -OutFile "$envDir/uv-x86_64-pc-windows-msvc.zip" - Invoke-WebRequest -Uri "https://github.com/OneDragon-Anything/OneDragon-Env/releases/download/ZenlessZoneZero-OneDragon/cpython-3.11.zip" -OutFile "$envDir/cpython-3.11.zip" - - # 将安装器复制到仓库根目录 - Copy-Item "$distDir/OneDragon-Installer.exe" -Destination "OneDragon-Installer.exe" -Force - - # 打包启动器 - Compress-Archive -Path "$distDir/OneDragon-Launcher.exe" -DestinationPath "$distDir/ZenlessZoneZero-OneDragon-Launcher.zip" -Force - Copy-Item "$distDir/OneDragon-Launcher.exe" -Destination "OneDragon-Launcher.exe" -Force - - # 模型目录(放在仓库根 assets/models 下) - $modelBase = "assets/models" - New-Item -ItemType Directory -Path "$modelBase/onnx_ocr" -Force | Out-Null - New-Item -ItemType Directory -Path "$modelBase/flash_classifier" -Force | Out-Null - New-Item -ItemType Directory -Path "$modelBase/hollow_zero_event" -Force | Out-Null - New-Item -ItemType Directory -Path "$modelBase/lost_void_det" -Force | Out-Null - - # 临时模型目录 - $tempDir = "temp_models" - New-Item -ItemType Directory -Path $tempDir -Force - - # 通过文件末尾最高8位数字获取最新模型 - function Get-LatestModelByNumber { - param ( - [string]$repo, - [string]$pattern - ) - $apiUrl = "https://api.github.com/repos/$repo/releases" - $releases = Invoke-RestMethod -Uri $apiUrl -Headers @{ "Accept" = "application/vnd.github.v3+json" } - - $bestAsset = $null - $maxNumber = -1 - - foreach ($release in $releases) { - foreach ($asset in $release.assets) { - if ($asset.name -match $pattern) { - # 从文件名末尾提取8位数字(在.zip之前) - if ($asset.name -match '(\d{8})\.zip$') { - $number = [int]$matches[1] - if ($number -gt $maxNumber) { - $maxNumber = $number - $bestAsset = @{ - url = $asset.browser_download_url - name = $asset.name - version_number = $number - } - } - } - # 对于ppocrv5和其他没有8位数字后缀的模型,作为备用选项 - elseif ($bestAsset -eq $null) { - $bestAsset = @{ - url = $asset.browser_download_url - name = $asset.name - version_number = 0 - } - } - } - } - } - return $bestAsset - } - - function Expand-ZipIntoNamedFolder { - param ( - [string]$url, - [string]$downloadPath, - [string]$destRoot, - [string]$folderName - ) - Invoke-WebRequest -Uri $url -OutFile $downloadPath - $targetPath = Join-Path $destRoot $folderName - New-Item -ItemType Directory -Path $targetPath -Force - Expand-Archive -Path $downloadPath -DestinationPath $targetPath -Force - } - - # 获取 ppocrv5 模型 (onnx_ocr) - $ppocrModel = Get-LatestModelByNumber -repo "OneDragon-Anything/OneDragon-Env" -pattern "ppocrv5\.zip$" - if ($ppocrModel) { - $ppocrName = $ppocrModel.name -replace "\.zip$", "" - Write-Host "找到 ppocrv5 模型: $($ppocrModel.name) (版本: $($ppocrModel.version_number))" - Expand-ZipIntoNamedFolder ` - -url $ppocrModel.url ` - -downloadPath "$tempDir/ppocrv5.zip" ` - -destRoot "$modelBase/onnx_ocr" ` - -folderName $ppocrName - } else { - Write-Warning "无法找到 ppocrv5 模型,使用备用版本" - Expand-ZipIntoNamedFolder ` - -url "https://github.com/OneDragon-Anything/OneDragon-Env/releases/download/ppocrv5/ppocrv5.zip" ` - -downloadPath "$tempDir/ppocrv5.zip" ` - -destRoot "$modelBase/onnx_ocr" ` - -folderName "ppocrv5" - } - - # 获取 flash classifier 模型 - $flashModel = Get-LatestModelByNumber -repo "OneDragon-Anything/OneDragon-YOLO" -pattern "flash.*\.zip$" - if ($flashModel) { - $flashName = $flashModel.name -replace "\.zip$", "" - Write-Host "找到 flash 模型: $($flashModel.name) (版本: $($flashModel.version_number))" - Expand-ZipIntoNamedFolder ` - -url $flashModel.url ` - -downloadPath "$tempDir/flash.zip" ` - -destRoot "$modelBase/flash_classifier" ` - -folderName $flashName - } else { - Write-Warning "无法找到 flash 模型,使用备用版本" - Expand-ZipIntoNamedFolder ` - -url "https://github.com/OneDragon-Anything/OneDragon-YOLO/releases/download/zzz_model/yolov8n-640-flash-0127.zip" ` - -downloadPath "$tempDir/flash.zip" ` - -destRoot "$modelBase/flash_classifier" ` - -folderName "yolov8n-640-flash-0127" - } - - # 获取 hollow zero event 模型 - $hollowModel = Get-LatestModelByNumber -repo "OneDragon-Anything/OneDragon-YOLO" -pattern "hollow.*\.zip$" - if ($hollowModel) { - $hollowName = $hollowModel.name -replace "\.zip$", "" - Write-Host "找到 hollow 模型: $($hollowModel.name) (版本: $($hollowModel.version_number))" - Expand-ZipIntoNamedFolder ` - -url $hollowModel.url ` - -downloadPath "$tempDir/hollow.zip" ` - -destRoot "$modelBase/hollow_zero_event" ` - -folderName $hollowName - } else { - Write-Warning "无法找到 hollow 模型,使用备用版本" - Expand-ZipIntoNamedFolder ` - -url "https://github.com/OneDragon-Anything/OneDragon-YOLO/releases/download/zzz_model/yolov8s-736-hollow-zero-event-0126.zip" ` - -downloadPath "$tempDir/hollow.zip" ` - -destRoot "$modelBase/hollow_zero_event" ` - -folderName "yolov8s-736-hollow-zero-event-0126" - } - - # 获取 lost void detection 模型 - $lostModel = Get-LatestModelByNumber -repo "OneDragon-Anything/OneDragon-YOLO" -pattern "lost.*\.zip$" - if ($lostModel) { - $lostName = $lostModel.name -replace "\.zip$", "" - Write-Host "找到 lost void 模型: $($lostModel.name) (版本: $($lostModel.version_number))" - Expand-ZipIntoNamedFolder ` - -url $lostModel.url ` - -downloadPath "$tempDir/lost.zip" ` - -destRoot "$modelBase/lost_void_det" ` - -folderName $lostName - } else { - Write-Warning "无法找到 lost void 模型,使用备用版本" - Expand-ZipIntoNamedFolder ` - -url "https://github.com/OneDragon-Anything/OneDragon-YOLO/releases/download/zzz_model/yolov8n-736-lost-void-det-20250612.zip" ` - -downloadPath "$tempDir/lost.zip" ` - -destRoot "$modelBase/lost_void_det" ` - -folderName "yolov8n-736-lost-void-det-20250612" - } - - # 获取版本号 - $version = $env:RELEASE_VERSION - - # 打包前删除不需要的目录 - Remove-Item -Path "deploy/build" -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path ".venv" -Recurse -Force -ErrorAction SilentlyContinue - Remove-Item -Path ".install/uv_cache" -Recurse -Force -ErrorAction SilentlyContinue - git reset --hard HEAD - git clean -fd | Out-Null - - # 第一次打包(Full)枚举根目录全部项目 - $items = Get-ChildItem -Force | ForEach-Object { $_.FullName } - Compress-Archive -Path $items -DestinationPath "$distDir/ZenlessZoneZero-OneDragon-$version-Full.zip" -Force - - # 将环境依赖包放入 .install 后进行第二次打包(Full-Environment) - Copy-Item "$distDir/ZenlessZoneZero-OneDragon-Environment.zip" -Destination "$envDir/ZenlessZoneZero-OneDragon-Environment.zip" -Force - $items = Get-ChildItem -Force | ForEach-Object { $_.FullName } - Compress-Archive -Path $items -DestinationPath "$distDir/ZenlessZoneZero-OneDragon-$version-Full-Environment.zip" -Force - - # 复制安装器 - Copy-Item "OneDragon-Installer.exe" -Destination "ZenlessZoneZero-OneDragon-$version-Installer.exe" -Force - - # 将所有待发布文件移到当前目录 - Move-Item -Path "$distDir/*.zip" -Destination (Get-Location) -Force + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python tools/ci/prepare_release_assets.py --release-version "$env:RELEASE_VERSION" - name: Create Release uses: softprops/action-gh-release@v1 with: - tag_name: ${{ needs.version.outputs.tag }} + tag_name: ${{ needs.version.outputs.version }} name: "Release ${{ needs.version.outputs.version }}" body: | ## 安装方式 @@ -466,7 +256,9 @@ jobs: - `ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-Full-Environment.zip` 为带环境的完整包,不需要额外下载资源。 - `ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-Full.zip` 为完整包,解压后选择解压目录为安装目录,只需要下载环境依赖。 - `ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-Installer.exe` 为精简安装程序,运行后会自动下载所需的资源。 + - `ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-WithRuntime.zip` 为内嵌运行时 + 源码的独立包(无需安装 Python),解压即用。 - 如果你想更新启动器,前往主程序【设置】-【资源下载】页面更新,或者下载 `ZenlessZoneZero-OneDragon-Launcher.zip`,解压后替换。 + - `ZenlessZoneZero-OneDragon-RuntimeLauncher.zip` 为集成启动器更新包(exe + .runtime),用于已有环境的就地升级。 - __不要下载Source Code__ 安装前请查看 [安装指南](https://one-dragon.com/zzz/zh/quickstart.html) @@ -494,8 +286,10 @@ jobs: ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-Full-Environment.zip ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-Full.zip ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-Installer.exe + ZenlessZoneZero-OneDragon-${{ needs.version.outputs.version }}-WithRuntime.zip ZenlessZoneZero-OneDragon-Environment.zip ZenlessZoneZero-OneDragon-Launcher.zip + ZenlessZoneZero-OneDragon-RuntimeLauncher.zip generate_release_notes: false prerelease: ${{ contains(needs.version.outputs.version, '-beta.') }} env: diff --git a/.github/workflows/build-running-resources.yml b/.github/workflows/build-running-resources.yml index 38bde46401..0254ab66a8 100644 --- a/.github/workflows/build-running-resources.yml +++ b/.github/workflows/build-running-resources.yml @@ -6,6 +6,9 @@ on: push: branches: [main] +env: + TZ: Asia/Shanghai + jobs: build-running-resources: runs-on: windows-latest @@ -16,6 +19,8 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 1 + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.head_ref || github.ref }} - name: Install uv shell: pwsh @@ -33,7 +38,7 @@ jobs: .\.venv\Scripts\Activate.ps1 uv sync --group dev - - name: Build auto-battle merged files + - name: Build files shell: pwsh run: | chcp 65001 @@ -43,30 +48,38 @@ jobs: .\.venv\Scripts\Activate.ps1 uv run python src/one_dragon/devtools/compile_po.py uv run python src/zzz_od/auto_battle/build_utils.py + uv run python deploy/generate_module_manifest.py env: PYTHONPATH: src - - name: Commit changes - if: github.event_name != 'pull_request' + - name: Set timestamp + shell: pwsh + run: echo "BUILD_TIMESTAMP=$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" >> $env:GITHUB_ENV + + - name: Push changes + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + uses: actions-js/push@v1.5 + with: + rebase: true + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.head_ref || github.ref_name }} + message: | + ci: 自动构建运行资源 ${{ env.BUILD_TIMESTAMP }} + + [skip ci] + + - name: Check build artifacts (fork PR) + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository shell: pwsh run: | - git config user.name github-actions - git config user.email github-actions@github.com - - # 检查是否有修改的文件(包括新文件) - $modifiedFiles = $(git status --porcelain config/auto_battle/*.merged.yml) - $hasChangesAutoBattle = if ($modifiedFiles) { 1 } else { 0 } - - # 检查.mo文件的修改 - $modifiedMoFiles = $(git status --porcelain assets/text/output/**/*.mo) - $hasChangesMo = if ($modifiedMoFiles) { 1 } else { 0 } - - if ($hasChangesAutoBattle -ne 0 -or $hasChangesMo -ne 0) { - git add config/auto_battle/*.merged.yml - git add assets/text/output/**/*.mo - $date = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - git commit -m "自动构建运行资源 $date" - git push - } else { - Write-Host "No file modifications" - } \ No newline at end of file + $changes = git status --porcelain config/auto_battle/ assets/text/output/ deploy/module_manifest.py + if ($changes) { + Write-Host "::error::构建产物未提交,请在本地运行以下命令后提交:" + Write-Host "::error:: uv run python src/one_dragon/devtools/compile_po.py" + Write-Host "::error:: uv run python src/zzz_od/auto_battle/build_utils.py" + Write-Host "::error:: uv run python deploy/generate_module_manifest.py" + Write-Host "变更文件:" + Write-Host $changes + exit 1 + } + Write-Host "构建产物已是最新" diff --git a/.github/workflows/mirrorchyan_release_note.yml b/.github/workflows/mirrorchyan_release_note.yml index 860b23e4ec..f0120f06c2 100644 --- a/.github/workflows/mirrorchyan_release_note.yml +++ b/.github/workflows/mirrorchyan_release_note.yml @@ -12,7 +12,7 @@ on: jobs: mirrorchyan_release_note: runs-on: macos-latest - if: ${{ github.repository_owner == 'OneDragon-Anything' && (github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'release') }} + if: ${{ github.repository_owner == 'OneDragon-Anything' && github.event.workflow_run.event != 'pull_request' && (github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'release') }} steps: - name: Upload Release Note if: ${{ github.repository_owner == 'OneDragon-Anything' }} diff --git a/.github/workflows/mirrorchyan_uploading.yml b/.github/workflows/mirrorchyan_uploading.yml index af7c1ce938..c27638c806 100644 --- a/.github/workflows/mirrorchyan_uploading.yml +++ b/.github/workflows/mirrorchyan_uploading.yml @@ -10,15 +10,26 @@ on: jobs: mirrorchyan: runs-on: macos-latest - if: ${{ github.repository_owner == 'OneDragon-Anything' && (github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch') }} + if: ${{ github.repository_owner == 'OneDragon-Anything' && github.event.workflow_run.event != 'pull_request' && (github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch') }} + strategy: + fail-fast: false + matrix: + arch: [x86_64, aarch64] + include: + - arch: x86_64 + type: WithRuntime + - arch: aarch64 + type: Full-Environment steps: - uses: MirrorChyan/uploading-action@v1 with: filetype: latest-release - filename: "ZenlessZoneZero-OneDragon-*-Full-Environment.zip" + filename: ${{ format('ZenlessZoneZero-OneDragon-*-{0}*', matrix.type) }} mirrorchyan_rid: ZZZ-OneDragon - github_token: ${{ secrets.GITHUB_TOKEN }} - owner: DoctorReid + arch: ${{ matrix.arch }} + + owner: OneDragon-Anything repo: ZenlessZoneZero-OneDragon + github_token: ${{ secrets.GITHUB_TOKEN }} upload_token: ${{ secrets.MirrorChyanUploadToken }} diff --git "a/.github/\345\274\200\345\217\221\346\226\207\346\241\243/\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/.github/\345\274\200\345\217\221\346\226\207\346\241\243/\345\274\200\345\217\221\346\214\207\345\215\227.md" deleted file mode 100644 index 6828026697..0000000000 --- "a/.github/\345\274\200\345\217\221\346\226\207\346\241\243/\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ /dev/null @@ -1,50 +0,0 @@ -# 1.开发环境 - -## 1.1.uv - -项目适用 uv 进行环境管理,[下载地址](https://github.com/astral-sh/uv/releases/latest) - -## 1.2.环境安装 - -普通使用 - -```shell -uv sync -``` - -开发使用 - -```shell -uv sync --group dev -``` - -# 2.打包 - -进入 deploy 文件夹,运行 `build_full.bat` - -## 2.1.安装器 - -生成spec文件并打包 - -```shell -uv run pyinstaller --onefile --windowed --uac-admin --icon="../assets/ui/installer_logo.ico" --add-data "../config/project.yml;config" ../src/zzz_od/gui/zzz_installer.py -n "OneDragon Installer" -``` - -使用spec打包 - -```shell -uv run pyinstaller "OneDragon Installer.spec" -``` - -## 2.2.启动器 - -生成spec文件并打包 - -```shell -uv run pyinstaller --onefile --uac-admin --icon="../assets/ui/zzz_logo.ico" ../src/zzz_od/win_exe/launcher.py -n "OneDragon Launcher" -``` - -使用spec打包 -```shell -uv run pyinstaller "OneDragon Launcher.spec" -``` diff --git a/.gitignore b/.gitignore index 55939b7409..4c35788cca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # 开发过程产生的临时文件 build/ dist/ +zzz-od-test/ .idea/ .vscode/ __pycache__/ @@ -10,10 +11,11 @@ deploy/dist/ recording.jsonl QWEN.md GEMINI.md -CLAUDE.md .lingma/ .qwen/ .claude/ +!.claude/settings.json +nul # 用户独有的配置文件 env.bat @@ -59,6 +61,7 @@ config/world_patrol_route_list/* .debug/ .conda/ .venv/ +.runtime/ notice_cache/ *.bak diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..4aeb010f04 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "zzz-od-test"] + path = zzz-od-test + url = git@github.com:OneDragon-Anything/zzz-od-test.git diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..d16ed663fa --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,26 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +详细的项目介绍和快速开始请参考:[@docs/develop/project-overview.md](docs/develop/project-overview.md) + +## 规范说明 + +- **开发流程**: [@docs/develop/standards/workflow-standard.md](docs/develop/standards/workflow-standard.md) +- **编码规范**: [@docs/develop/standards/coding-standards.md](docs/develop/standards/coding-standards.md) +- **测试规范**: [@docs/develop/standards/testing-standards.md](docs/develop/standards/testing-standards.md) +- **文档规范**: [@docs/develop/standards/documentation-standards.md](docs/develop/standards/documentation-standards.md) + +## 工具使用规范 + +### Bash工具规范 + +- 优先使用Bash工具的后台运行能力,避免在命令最后使用 `&` 来实现后台运行。 +- 禁止使用 `python`, `python3`, `pip`, `pip3` 等命令,使用 `uv` 运行python相关命令。 +- `uv run` 时,必须使用 `--env-file .env` 加载环境变量。 + +### 其他工具规范 + +- 积极使用 `context7` 工具查询依赖库的用法,包括类、属性、方法、参数等。 diff --git a/assets/game_data/compendium_data.yml b/assets/game_data/compendium_data.yml index 59cf8cdc8c..fa583aad8b 100644 --- a/assets/game_data/compendium_data.yml +++ b/assets/game_data/compendium_data.yml @@ -65,8 +65,10 @@ - mission_name: "自定义卡组4" - mission_name: "自定义卡组5" - mission_type_name: "代理人方案培养" + mission_type_name_display: "特训目标" mission_list: - mission_name: "代理人方案培养" + mission_name_display: "特训目标" - category_name: "区域巡防" mission_type_list: @@ -95,6 +97,7 @@ - mission_type_name: "诡步与重壁" mission_type_name_display: "沧浪行歌 流光咏叹" - mission_type_name: "代理人方案培养" + mission_type_name_display: "特训目标" - category_name: "专业挑战室" mission_type_list: @@ -111,7 +114,9 @@ - mission_type_name: "牲鬼·凶魁愚者" - mission_type_name: "秽蚀·狛野真斗" - mission_type_name: "秽息行者·蝎骸" + - mission_type_name: "牲鬼·卫律使者" - mission_type_name: "代理人方案培养" + mission_type_name_display: "特训目标" - category_name: "恶名狩猎" mission_type_list: @@ -126,6 +131,7 @@ alias_list: - "魇缚者·???" - mission_type_name: "代理人方案培养" + mission_type_name_display: "特训目标" - tab_name: "作战" category_list: @@ -135,6 +141,7 @@ mission_list: - mission_name: "战线肃清" - mission_name: "特遣调查" + - mission_name: "矩阵行动" - mission_type_name: "断层之谜" - mission_type_name: "枯萎之都" - mission_type_name: "旧都列车" diff --git a/assets/game_data/screen_info/3d_map.yml b/assets/game_data/screen_info/3d_map.yml index 010f775a10..806a0edcf9 100644 --- a/assets/game_data/screen_info/3d_map.yml +++ b/assets/game_data/screen_info/3d_map.yml @@ -199,3 +199,17 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 按钮-区域信息-关闭 + id_mark: false + pc_rect: + - 1760 + - 100 + - 1870 + - 200 + text: '' + lcs_percent: 0.5 + template_sub_dir: normal_world_investigation + template_id: btn_area_info_close + template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/_od_merged.yml b/assets/game_data/screen_info/_od_merged.yml index 6c44c2ab62..30accf1892 100644 --- a/assets/game_data/screen_info/_od_merged.yml +++ b/assets/game_data/screen_info/_od_merged.yml @@ -199,6 +199,20 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 按钮-区域信息-关闭 + id_mark: false + pc_rect: + - 1760 + - 100 + - 1870 + - 200 + text: '' + lcs_percent: 0.5 + template_sub_dir: normal_world_investigation + template_id: btn_area_info_close + template_match_threshold: 0.7 + color_range: null + goto_list: [] - screen_id: arcade screen_name: 电玩店 pc_alt: false @@ -505,6 +519,7 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: menu - area_name: 战斗结果-撤退 id_mark: false pc_rect: @@ -883,13 +898,13 @@ goto_list: [] - screen_id: coffee_shop screen_name: 咖啡店 - pc_alt: true + pc_alt: false area_list: - area_name: 点单 id_mark: false pc_rect: - 1702 - - 966 + - 880 - 1914 - 1020 text: 点单 @@ -1497,7 +1512,7 @@ id_mark: false pc_rect: - 716 - - 120 + - 100 - 1630 - 178 text: '' @@ -1558,8 +1573,8 @@ - 308 text: '' lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' + template_sub_dir: compendium + template_id: completed template_match_threshold: 0.7 color_range: null goto_list: [] @@ -1577,14 +1592,28 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 活跃度奖励-奖励预览 + id_mark: false + pc_rect: + - 429 + - 195 + - 655 + - 303 + text: 奖励预览 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 今日最大活跃度 id_mark: false pc_rect: - - 502 - - 270 - - 580 - - 310 - text: '' + - 417 + - 221 + - 646 + - 274 + text: 今日最大活跃度 lcs_percent: 0.5 template_sub_dir: '' template_id: '' @@ -1655,7 +1684,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -1670,7 +1699,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -1685,7 +1714,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -1700,7 +1729,7 @@ id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -1721,7 +1750,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 @@ -1756,7 +1785,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -1771,7 +1800,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -1786,7 +1815,7 @@ id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -1807,7 +1836,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -1822,7 +1851,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 @@ -1857,7 +1886,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -1872,7 +1901,7 @@ id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -1893,7 +1922,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -1908,7 +1937,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -1923,7 +1952,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 @@ -1958,7 +1987,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -1973,7 +2002,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -1988,7 +2017,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -2003,7 +2032,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -2018,7 +2047,7 @@ id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 @@ -2059,7 +2088,7 @@ id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -2080,7 +2109,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -2095,7 +2124,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -2110,7 +2139,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -2125,7 +2154,7 @@ id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 @@ -3961,6 +3990,108 @@ template_match_threshold: 0.7 color_range: null goto_list: [] +- screen_id: intel_board + screen_name: 情报板 + pc_alt: false + area_list: + - area_name: 点数兑换 + id_mark: false + pc_rect: + - 954 + - 991 + - 1242 + - 1064 + text: 点数兑换 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 刷新按钮 + id_mark: false + pc_rect: + - 801 + - 999 + - 860 + - 1058 + text: '' + lcs_percent: 0.5 + template_sub_dir: intel_board + template_id: refresh + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 筛选按钮 + id_mark: false + pc_rect: + - 878 + - 999 + - 936 + - 1058 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 重置按钮 + id_mark: false + pc_rect: + - 1422 + - 998 + - 1704 + - 1058 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 搜索区域 + id_mark: false + pc_rect: + - 1200 + - 100 + - 1920 + - 960 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 关闭筛选 + id_mark: false + pc_rect: + - 1800 + - 40 + - 1800 + - 40 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 进度文本 + id_mark: false + pc_rect: + - 322 + - 1019 + - 513 + - 1061 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - screen_id: inter_knot screen_name: 绳网 pc_alt: false @@ -4007,6 +4138,20 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 按钮-追踪 + id_mark: false + pc_rect: + - 410 + - 900 + - 546 + - 988 + text: 追踪 + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - screen_id: life_on_line screen_name: 真拿命验收 pc_alt: false @@ -4182,7 +4327,7 @@ pc_rect: - 192 - 22 - - 252 + - 290 - 78 text: 详情 lcs_percent: 0.5 @@ -4506,10 +4651,10 @@ - area_name: 区域-藏品名称 id_mark: false pc_rect: - - 90 - - 508 - - 1906 - - 570 + - 80 + - 411 + - 1911 + - 593 text: '' lcs_percent: 0.5 template_sub_dir: '' @@ -4559,6 +4704,7 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: minimap - area_name: 按钮-刷新 id_mark: false pc_rect: @@ -4611,21 +4757,6 @@ screen_name: 迷失之地-入口 pc_alt: false area_list: - - area_name: 按钮-街区 - id_mark: true - pc_rect: - - 234 - - 26 - - 394 - - 78 - text: 街区 - lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' - template_match_threshold: 0.7 - color_range: null - goto_list: - - 大世界-普通 - area_name: 按钮-更新弹窗-关闭 id_mark: false pc_rect: @@ -4641,12 +4772,12 @@ color_range: null goto_list: [] - area_name: 按钮-悬赏委托 - id_mark: true + id_mark: false pc_rect: - - 1550 - - 24 - - 1762 - - 74 + - 1181 + - 19 + - 1346 + - 70 text: 悬赏委托 lcs_percent: 0.5 template_sub_dir: '' @@ -4654,21 +4785,6 @@ template_match_threshold: 0.7 color_range: null goto_list: [] - - area_name: 按钮-战线肃清 - id_mark: true - pc_rect: - - 850 - - 206 - - 1738 - - 290 - text: 战线肃清 - lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' - template_match_threshold: 0.7 - color_range: null - goto_list: - - 迷失之地-战线肃清 - area_name: 按钮-悬赏委托-全部领取 id_mark: false pc_rect: @@ -4697,30 +4813,99 @@ template_match_threshold: 0.7 color_range: null goto_list: [] - - area_name: 按键-特遣调查 + - area_name: 区域-悬赏委托-进度 + id_mark: false + pc_rect: + - 736 + - 12 + - 1138 + - 92 + text: 8000/8000 + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 区域-矩阵行动 id_mark: true pc_rect: - - 196 - - 212 - - 794 - - 310 - text: 特遣调查 + - 745 + - 233 + - 907 + - 288 + text: 矩阵行动 lcs_percent: 0.5 template_sub_dir: '' template_id: '' template_match_threshold: 0.7 color_range: null - goto_list: - - 迷失之地-特遣调查 - - area_name: 区域-悬赏委托-进度 + goto_list: [] + - area_name: 按钮-前往挑战 id_mark: false pc_rect: - - 1170 - - 50 - - 1520 - - 100 - text: 8000/8000 - lcs_percent: 1.0 + - 1142 + - 746 + - 1384 + - 809 + text: 前往挑战 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 按钮-下一步 + id_mark: false + pc_rect: + - 1633 + - 1001 + - 1802 + - 1052 + text: 下一步 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 按钮-常规 + id_mark: false + pc_rect: + - 1616 + - 19 + - 1740 + - 89 + text: 常规 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 按钮-战线肃清 + id_mark: false + pc_rect: + - 878 + - 318 + - 1069 + - 365 + text: 战线肃清 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 按钮-特遣调查 + id_mark: false + pc_rect: + - 1425 + - 311 + - 1613 + - 362 + text: 特遣调查 + lcs_percent: 0.5 template_sub_dir: '' template_id: '' template_match_threshold: 0.7 @@ -4890,6 +5075,20 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 武备名称 + id_mark: false + pc_rect: + - 452 + - 459 + - 1504 + - 567 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - screen_id: lost_void_lottery screen_name: 迷失之地-抽奖机 pc_alt: false @@ -4971,10 +5170,10 @@ - area_name: 区域-文本提示 id_mark: false pc_rect: - - 1564 - - 50 - - 1778 - - 268 + - 1454 + - 30 + - 1814 + - 368 text: '' lcs_percent: 0.5 template_sub_dir: '' @@ -5038,6 +5237,7 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: minimap - area_name: 战斗-菜单 id_mark: true pc_rect: @@ -5052,6 +5252,7 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: menu - area_name: 区域-对话内容 id_mark: false pc_rect: @@ -5317,6 +5518,122 @@ template_match_threshold: 0.7 color_range: null goto_list: [] +- screen_id: matrix_action + screen_name: 迷失之地-矩阵行动 + pc_alt: false + area_list: + - area_name: 预备编队 + id_mark: false + pc_rect: + - 679 + - 24 + - 825 + - 71 + text: 预备编队 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 主战编队 + id_mark: false + pc_rect: + - 1024 + - 116 + - 1129 + - 150 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 协战代理人 + id_mark: false + pc_rect: + - 1026 + - 312 + - 1151 + - 350 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 编队列表 + id_mark: false + pc_rect: + - 185 + - 156 + - 966 + - 1059 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 按钮-开始挑战 + id_mark: true + pc_rect: + - 1633 + - 998 + - 1811 + - 1052 + text: 开始挑战 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 代理人列表 + id_mark: false + pc_rect: + - 188 + - 96 + - 966 + - 1059 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 主战编队槽 + id_mark: false + pc_rect: + - 1028 + - 162 + - 1633 + - 299 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 协战编队槽 + id_mark: false + pc_rect: + - 1018 + - 350 + - 1181 + - 509 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - screen_id: menu screen_name: 菜单 pc_alt: false @@ -5875,10 +6192,11 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: menu - area_name: 地图 id_mark: false pc_rect: - - 1706 + - 1696 - 319 - 1858 - 400 @@ -5889,20 +6207,7 @@ template_match_threshold: 0.5 color_range: null goto_list: [] - - area_name: 功能导览 - id_mark: false - pc_rect: - - 1776 - - 28 - - 1869 - - 117 - text: '' - lcs_percent: 0.5 - template_sub_dir: normal_world - template_id: function_menu - template_match_threshold: 0.7 - color_range: null - goto_list: [] + gamepad_key: map - area_name: 快捷手册 id_mark: false pc_rect: @@ -5918,6 +6223,7 @@ color_range: null goto_list: - 快捷手册-训练 + gamepad_key: compendium - area_name: 好感度标题 id_mark: false pc_rect: @@ -5974,6 +6280,35 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: minimap + - area_name: 对话框确认 + id_mark: false + pc_rect: + - 1036 + - 593 + - 1212 + - 657 + text: 确认 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] + - area_name: 任务追踪 + id_mark: false + pc_rect: + - 10 + - 115 + - 350 + - 300 + text: 按自己的步调度过这一天 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - screen_id: normal_world_basic screen_name: 大世界-普通 pc_alt: true @@ -6007,6 +6342,7 @@ color_range: null goto_list: - 菜单 + gamepad_key: menu - area_name: 快捷手册 id_mark: false pc_rect: @@ -6022,6 +6358,7 @@ color_range: null goto_list: - 快捷手册-训练 + gamepad_key: compendium - area_name: 星期 id_mark: false pc_rect: @@ -6050,6 +6387,7 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: function_menu - screen_id: normal_world_investigation screen_name: 大世界-勘域 pc_alt: true @@ -6083,6 +6421,7 @@ color_range: null goto_list: - 菜单 + gamepad_key: menu - area_name: 快捷手册 id_mark: false pc_rect: @@ -6098,6 +6437,7 @@ color_range: null goto_list: - 快捷手册-训练 + gamepad_key: compendium - screen_id: notorious_hunt screen_name: 恶名狩猎 pc_alt: false @@ -6462,6 +6802,20 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 按钮-关闭 + id_mark: false + pc_rect: + - 1250 + - 100 + - 1500 + - 350 + text: '' + lcs_percent: 0.5 + template_sub_dir: menu + template_id: btn_close + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 返回 id_mark: false pc_rect: @@ -6471,8 +6825,8 @@ - 84 text: '' lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' + template_sub_dir: menu + template_id: back template_match_threshold: 0.7 color_range: null goto_list: [] @@ -6672,6 +7026,20 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 换下 + id_mark: false + pc_rect: + - 1628 + - 1004 + - 1816 + - 1052 + text: 换下 + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 开始营业 id_mark: false pc_rect: @@ -6700,6 +7068,20 @@ template_match_threshold: 0.7 color_range: null goto_list: [] + - area_name: 营业后确认 + id_mark: false + pc_rect: + - 800 + - 600 + - 1050 + - 656 + text: 确认 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 正在营业 id_mark: false pc_rect: diff --git a/assets/game_data/screen_info/battle.yml b/assets/game_data/screen_info/battle.yml index bc614729eb..97b6ee6e1e 100644 --- a/assets/game_data/screen_info/battle.yml +++ b/assets/game_data/screen_info/battle.yml @@ -156,6 +156,7 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: menu - area_name: 战斗结果-撤退 id_mark: false pc_rect: diff --git a/assets/game_data/screen_info/coffee_shop.yml b/assets/game_data/screen_info/coffee_shop.yml index ac37d62907..e660c071a1 100644 --- a/assets/game_data/screen_info/coffee_shop.yml +++ b/assets/game_data/screen_info/coffee_shop.yml @@ -1,11 +1,12 @@ screen_id: coffee_shop screen_name: 咖啡店 -pc_alt: true +pc_alt: false area_list: - area_name: 点单 + id_mark: false pc_rect: - 1702 - - 966 + - 880 - 1914 - 1020 text: 点单 @@ -13,7 +14,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 咖啡列表 + id_mark: false pc_rect: - 132 - 682 @@ -24,7 +28,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 对话框前往 + id_mark: false pc_rect: - 728 - 600 @@ -35,7 +42,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 对话框确认 + id_mark: false pc_rect: - 1040 - 600 @@ -46,7 +56,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 点单后跳过 + id_mark: false pc_rect: - 1684 - 64 @@ -57,7 +70,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 电量确认 + id_mark: false pc_rect: - 876 - 706 @@ -68,7 +84,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 不可贪杯确认 + id_mark: false pc_rect: - 882 - 606 @@ -79,3 +98,5 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/compendium.yml b/assets/game_data/screen_info/compendium.yml index 51fc777396..a87eddcae8 100644 --- a/assets/game_data/screen_info/compendium.yml +++ b/assets/game_data/screen_info/compendium.yml @@ -6,7 +6,7 @@ area_list: id_mark: false pc_rect: - 716 - - 120 + - 100 - 1630 - 178 text: '' @@ -67,8 +67,8 @@ area_list: - 308 text: '' lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' + template_sub_dir: compendium + template_id: completed template_match_threshold: 0.7 color_range: null goto_list: [] @@ -86,14 +86,28 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 活跃度奖励-奖励预览 + id_mark: false + pc_rect: + - 429 + - 195 + - 655 + - 303 + text: 奖励预览 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 今日最大活跃度 id_mark: false pc_rect: - - 502 - - 270 - - 580 - - 310 - text: '' + - 417 + - 221 + - 646 + - 274 + text: 今日最大活跃度 lcs_percent: 0.5 template_sub_dir: '' template_id: '' diff --git a/assets/game_data/screen_info/compendium_combat.yml b/assets/game_data/screen_info/compendium_combat.yml index c4742288e0..1018200663 100644 --- a/assets/game_data/screen_info/compendium_combat.yml +++ b/assets/game_data/screen_info/compendium_combat.yml @@ -6,7 +6,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -21,7 +21,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -36,7 +36,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -51,7 +51,7 @@ area_list: id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -72,7 +72,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 diff --git a/assets/game_data/screen_info/compendium_errands.yml b/assets/game_data/screen_info/compendium_errands.yml index 7f8d09f19a..4d8d9438ac 100644 --- a/assets/game_data/screen_info/compendium_errands.yml +++ b/assets/game_data/screen_info/compendium_errands.yml @@ -6,7 +6,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -21,7 +21,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -36,7 +36,7 @@ area_list: id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -57,7 +57,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -72,7 +72,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 diff --git a/assets/game_data/screen_info/compendium_primer.yml b/assets/game_data/screen_info/compendium_primer.yml index bf91c77d9f..b2e9c0ffa1 100644 --- a/assets/game_data/screen_info/compendium_primer.yml +++ b/assets/game_data/screen_info/compendium_primer.yml @@ -6,7 +6,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -21,7 +21,7 @@ area_list: id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -42,7 +42,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -57,7 +57,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -72,7 +72,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 diff --git a/assets/game_data/screen_info/compendium_tactics.yml b/assets/game_data/screen_info/compendium_tactics.yml index d57d0b7802..a46c21345f 100644 --- a/assets/game_data/screen_info/compendium_tactics.yml +++ b/assets/game_data/screen_info/compendium_tactics.yml @@ -6,7 +6,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -21,7 +21,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -36,7 +36,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -51,7 +51,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -66,7 +66,7 @@ area_list: id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 diff --git a/assets/game_data/screen_info/compendium_training.yml b/assets/game_data/screen_info/compendium_training.yml index cbfddc9fc5..9ce99e38b2 100644 --- a/assets/game_data/screen_info/compendium_training.yml +++ b/assets/game_data/screen_info/compendium_training.yml @@ -6,7 +6,7 @@ area_list: id_mark: true pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 训练 @@ -27,7 +27,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 目标 @@ -42,7 +42,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 日常 @@ -57,7 +57,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 作战 @@ -72,7 +72,7 @@ area_list: id_mark: false pc_rect: - 708 - - 118 + - 100 - 1636 - 178 text: 战术 diff --git a/assets/game_data/screen_info/intel_board.yml b/assets/game_data/screen_info/intel_board.yml new file mode 100644 index 0000000000..d27b65fd6d --- /dev/null +++ b/assets/game_data/screen_info/intel_board.yml @@ -0,0 +1,102 @@ +screen_id: intel_board +screen_name: 情报板 +pc_alt: false +area_list: +- area_name: 点数兑换 + id_mark: false + pc_rect: + - 954 + - 991 + - 1242 + - 1064 + text: 点数兑换 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 刷新按钮 + id_mark: false + pc_rect: + - 801 + - 999 + - 860 + - 1058 + text: '' + lcs_percent: 0.5 + template_sub_dir: intel_board + template_id: refresh + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 筛选按钮 + id_mark: false + pc_rect: + - 878 + - 999 + - 936 + - 1058 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 重置按钮 + id_mark: false + pc_rect: + - 1422 + - 998 + - 1704 + - 1058 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 搜索区域 + id_mark: false + pc_rect: + - 1200 + - 100 + - 1920 + - 960 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 关闭筛选 + id_mark: false + pc_rect: + - 1800 + - 40 + - 1800 + - 40 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 进度文本 + id_mark: false + pc_rect: + - 322 + - 1019 + - 513 + - 1061 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/inter_knot.yml b/assets/game_data/screen_info/inter_knot.yml index e3bb809b36..1fa749fc23 100644 --- a/assets/game_data/screen_info/inter_knot.yml +++ b/assets/game_data/screen_info/inter_knot.yml @@ -44,3 +44,17 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 按钮-追踪 + id_mark: false + pc_rect: + - 410 + - 900 + - 546 + - 988 + text: 追踪 + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/lost_void_bangboo_store.yml b/assets/game_data/screen_info/lost_void_bangboo_store.yml index 7910b83540..1e548939e4 100644 --- a/assets/game_data/screen_info/lost_void_bangboo_store.yml +++ b/assets/game_data/screen_info/lost_void_bangboo_store.yml @@ -7,7 +7,7 @@ area_list: pc_rect: - 192 - 22 - - 252 + - 290 - 78 text: 详情 lcs_percent: 0.5 diff --git a/assets/game_data/screen_info/lost_void_choose_common.yml b/assets/game_data/screen_info/lost_void_choose_common.yml index 7e61038ea8..7fb5bb80d7 100644 --- a/assets/game_data/screen_info/lost_void_choose_common.yml +++ b/assets/game_data/screen_info/lost_void_choose_common.yml @@ -33,10 +33,10 @@ area_list: - area_name: 区域-藏品名称 id_mark: false pc_rect: - - 90 - - 508 - - 1906 - - 570 + - 80 + - 411 + - 1911 + - 593 text: '' lcs_percent: 0.5 template_sub_dir: '' @@ -86,6 +86,7 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: minimap - area_name: 按钮-刷新 id_mark: false pc_rect: diff --git a/assets/game_data/screen_info/lost_void_entry.yml b/assets/game_data/screen_info/lost_void_entry.yml index 08e65cbbcb..e2fc783bdb 100644 --- a/assets/game_data/screen_info/lost_void_entry.yml +++ b/assets/game_data/screen_info/lost_void_entry.yml @@ -2,21 +2,6 @@ screen_id: lost_void_entry screen_name: 迷失之地-入口 pc_alt: false area_list: -- area_name: 按钮-街区 - id_mark: true - pc_rect: - - 234 - - 26 - - 394 - - 78 - text: 街区 - lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' - template_match_threshold: 0.7 - color_range: null - goto_list: - - 大世界-普通 - area_name: 按钮-更新弹窗-关闭 id_mark: false pc_rect: @@ -32,12 +17,12 @@ area_list: color_range: null goto_list: [] - area_name: 按钮-悬赏委托 - id_mark: true + id_mark: false pc_rect: - - 1550 - - 24 - - 1762 - - 74 + - 1181 + - 19 + - 1346 + - 70 text: 悬赏委托 lcs_percent: 0.5 template_sub_dir: '' @@ -45,21 +30,6 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] -- area_name: 按钮-战线肃清 - id_mark: true - pc_rect: - - 850 - - 206 - - 1738 - - 290 - text: 战线肃清 - lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' - template_match_threshold: 0.7 - color_range: null - goto_list: - - 迷失之地-战线肃清 - area_name: 按钮-悬赏委托-全部领取 id_mark: false pc_rect: @@ -88,30 +58,99 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] -- area_name: 按键-特遣调查 +- area_name: 区域-悬赏委托-进度 + id_mark: false + pc_rect: + - 736 + - 12 + - 1138 + - 92 + text: 8000/8000 + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 区域-矩阵行动 id_mark: true pc_rect: - - 196 - - 212 - - 794 - - 310 - text: 特遣调查 + - 745 + - 233 + - 907 + - 288 + text: 矩阵行动 lcs_percent: 0.5 template_sub_dir: '' template_id: '' template_match_threshold: 0.7 color_range: null - goto_list: - - 迷失之地-特遣调查 -- area_name: 区域-悬赏委托-进度 + goto_list: [] +- area_name: 按钮-前往挑战 id_mark: false pc_rect: - - 1170 - - 50 - - 1520 - - 100 - text: 8000/8000 - lcs_percent: 1.0 + - 1142 + - 746 + - 1384 + - 809 + text: 前往挑战 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 按钮-下一步 + id_mark: false + pc_rect: + - 1633 + - 1001 + - 1802 + - 1052 + text: 下一步 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 按钮-常规 + id_mark: false + pc_rect: + - 1616 + - 19 + - 1740 + - 89 + text: 常规 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 按钮-战线肃清 + id_mark: false + pc_rect: + - 878 + - 318 + - 1069 + - 365 + text: 战线肃清 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 按钮-特遣调查 + id_mark: false + pc_rect: + - 1425 + - 311 + - 1613 + - 362 + text: 特遣调查 + lcs_percent: 0.5 template_sub_dir: '' template_id: '' template_match_threshold: 0.7 diff --git a/assets/game_data/screen_info/lost_void_gear.yml b/assets/game_data/screen_info/lost_void_gear.yml index 28422e2bc8..f903c81e5a 100644 --- a/assets/game_data/screen_info/lost_void_gear.yml +++ b/assets/game_data/screen_info/lost_void_gear.yml @@ -72,3 +72,17 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] +- area_name: 武备名称 + id_mark: false + pc_rect: + - 452 + - 459 + - 1504 + - 567 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/lost_void_normal_world.yml b/assets/game_data/screen_info/lost_void_normal_world.yml index 59c5d6c4fa..a70a6a16ce 100644 --- a/assets/game_data/screen_info/lost_void_normal_world.yml +++ b/assets/game_data/screen_info/lost_void_normal_world.yml @@ -5,10 +5,10 @@ area_list: - area_name: 区域-文本提示 id_mark: false pc_rect: - - 1564 - - 50 - - 1778 - - 268 + - 1454 + - 30 + - 1814 + - 368 text: '' lcs_percent: 0.5 template_sub_dir: '' @@ -72,6 +72,7 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: minimap - area_name: 战斗-菜单 id_mark: true pc_rect: @@ -86,6 +87,7 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: menu - area_name: 区域-对话内容 id_mark: false pc_rect: diff --git a/assets/game_data/screen_info/matrix_action.yml b/assets/game_data/screen_info/matrix_action.yml new file mode 100644 index 0000000000..86013e6713 --- /dev/null +++ b/assets/game_data/screen_info/matrix_action.yml @@ -0,0 +1,116 @@ +screen_id: matrix_action +screen_name: 迷失之地-矩阵行动 +pc_alt: false +area_list: +- area_name: 预备编队 + id_mark: false + pc_rect: + - 679 + - 24 + - 825 + - 71 + text: 预备编队 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 主战编队 + id_mark: false + pc_rect: + - 1024 + - 116 + - 1129 + - 150 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 协战代理人 + id_mark: false + pc_rect: + - 1026 + - 312 + - 1151 + - 350 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 编队列表 + id_mark: false + pc_rect: + - 185 + - 156 + - 966 + - 1059 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 按钮-开始挑战 + id_mark: true + pc_rect: + - 1633 + - 998 + - 1811 + - 1052 + text: 开始挑战 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 代理人列表 + id_mark: false + pc_rect: + - 188 + - 96 + - 966 + - 1059 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 主战编队槽 + id_mark: false + pc_rect: + - 1028 + - 162 + - 1633 + - 299 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 协战编队槽 + id_mark: false + pc_rect: + - 1018 + - 350 + - 1181 + - 509 + text: '' + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/normal_world.yml b/assets/game_data/screen_info/normal_world.yml index ad557d3ae0..29f14bf752 100644 --- a/assets/game_data/screen_info/normal_world.yml +++ b/assets/game_data/screen_info/normal_world.yml @@ -44,10 +44,11 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: menu - area_name: 地图 id_mark: false pc_rect: - - 1706 + - 1696 - 319 - 1858 - 400 @@ -58,20 +59,7 @@ area_list: template_match_threshold: 0.5 color_range: null goto_list: [] -- area_name: 功能导览 - id_mark: false - pc_rect: - - 1776 - - 28 - - 1869 - - 117 - text: '' - lcs_percent: 0.5 - template_sub_dir: normal_world - template_id: function_menu - template_match_threshold: 0.7 - color_range: null - goto_list: [] + gamepad_key: map - area_name: 快捷手册 id_mark: false pc_rect: @@ -87,6 +75,7 @@ area_list: color_range: null goto_list: - 快捷手册-训练 + gamepad_key: compendium - area_name: 好感度标题 id_mark: false pc_rect: @@ -143,3 +132,32 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: minimap +- area_name: 对话框确认 + id_mark: false + pc_rect: + - 1036 + - 593 + - 1212 + - 657 + text: 确认 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 任务追踪 + id_mark: false + pc_rect: + - 10 + - 115 + - 350 + - 300 + text: 按自己的步调度过这一天 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/game_data/screen_info/normal_world_basic.yml b/assets/game_data/screen_info/normal_world_basic.yml index 34a77907bf..6052421e9f 100644 --- a/assets/game_data/screen_info/normal_world_basic.yml +++ b/assets/game_data/screen_info/normal_world_basic.yml @@ -31,6 +31,7 @@ area_list: color_range: null goto_list: - 菜单 + gamepad_key: menu - area_name: 快捷手册 id_mark: false pc_rect: @@ -46,6 +47,7 @@ area_list: color_range: null goto_list: - 快捷手册-训练 + gamepad_key: compendium - area_name: 星期 id_mark: false pc_rect: @@ -74,3 +76,4 @@ area_list: template_match_threshold: 0.7 color_range: null goto_list: [] + gamepad_key: function_menu diff --git a/assets/game_data/screen_info/normal_world_investigation.yml b/assets/game_data/screen_info/normal_world_investigation.yml index 8e80afe9b6..175e0c34f4 100644 --- a/assets/game_data/screen_info/normal_world_investigation.yml +++ b/assets/game_data/screen_info/normal_world_investigation.yml @@ -31,6 +31,7 @@ area_list: color_range: null goto_list: - 菜单 + gamepad_key: menu - area_name: 快捷手册 id_mark: false pc_rect: @@ -46,3 +47,4 @@ area_list: color_range: null goto_list: - 快捷手册-训练 + gamepad_key: compendium diff --git a/assets/game_data/screen_info/random_play.yml b/assets/game_data/screen_info/random_play.yml index 60fd660dad..391f69f3c9 100644 --- a/assets/game_data/screen_info/random_play.yml +++ b/assets/game_data/screen_info/random_play.yml @@ -3,6 +3,7 @@ screen_name: 影像店营业 pc_alt: false area_list: - area_name: 昨日账本 + id_mark: false pc_rect: - 528 - 224 @@ -13,7 +14,24 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 按钮-关闭 + id_mark: false + pc_rect: + - 1250 + - 100 + - 1500 + - 350 + text: '' + lcs_percent: 0.5 + template_sub_dir: menu + template_id: btn_close + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 返回 + id_mark: false pc_rect: - 54 - 12 @@ -21,10 +39,13 @@ area_list: - 84 text: '' lcs_percent: 0.5 - template_sub_dir: '' - template_id: '' + template_sub_dir: menu + template_id: back template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 宣传员入口 + id_mark: false pc_rect: - 800 - 620 @@ -35,7 +56,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 录像带入口 + id_mark: false pc_rect: - 1106 - 618 @@ -46,7 +70,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 经营状况 + id_mark: false pc_rect: - 1272 - 14 @@ -57,7 +84,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 录像带主题-1 + id_mark: false pc_rect: - 784 - 136 @@ -68,7 +98,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 录像带主题-2 + id_mark: false pc_rect: - 914 - 136 @@ -79,7 +112,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 录像带主题-3 + id_mark: false pc_rect: - 1030 - 136 @@ -90,7 +126,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 宣传员-1 + id_mark: false pc_rect: - 532 - 144 @@ -101,7 +140,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 宣传员-2 + id_mark: false pc_rect: - 744 - 144 @@ -112,7 +154,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 确认 + id_mark: false pc_rect: - 1590 - 1004 @@ -123,7 +168,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 上架筛选 + id_mark: false pc_rect: - 1588 - 28 @@ -134,7 +182,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 主题筛选 + id_mark: false pc_rect: - 1124 - 104 @@ -145,7 +196,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 上架 + id_mark: false pc_rect: - 1628 - 1004 @@ -156,7 +210,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 下架 + id_mark: false pc_rect: - 1628 - 1004 @@ -167,7 +224,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 选择宣传员 + id_mark: false pc_rect: - 104 - 1006 @@ -178,7 +238,24 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 换下 + id_mark: false + pc_rect: + - 1628 + - 1004 + - 1816 + - 1052 + text: 换下 + lcs_percent: 1.0 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 开始营业 + id_mark: false pc_rect: - 1536 - 950 @@ -189,7 +266,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 开始营业-确认 + id_mark: false pc_rect: - 1032 - 600 @@ -200,7 +280,24 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] +- area_name: 营业后确认 + id_mark: false + pc_rect: + - 800 + - 600 + - 1050 + - 656 + text: 确认 + lcs_percent: 0.5 + template_sub_dir: '' + template_id: '' + template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 正在营业 + id_mark: false pc_rect: - 1536 - 950 @@ -211,7 +308,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 推荐上架 + id_mark: false pc_rect: - 1320 - 1002 @@ -222,7 +322,10 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] - area_name: 宣传员列表 + id_mark: false pc_rect: - 530 - 140 @@ -233,3 +336,5 @@ area_list: template_sub_dir: '' template_id: '' template_match_threshold: 0.7 + color_range: null + goto_list: [] diff --git a/assets/template/agent_state/aria_cheer_energy_3_1/config.yml b/assets/template/agent_state/aria_cheer_energy_3_1/config.yml new file mode 100644 index 0000000000..a5d7949e31 --- /dev/null +++ b/assets/template/agent_state/aria_cheer_energy_3_1/config.yml @@ -0,0 +1,22 @@ +sub_dir: agent_state +template_id: aria_cheer_energy_3_1 +template_name: 角色状态-爱芮-31 +template_shape: multi_rect +auto_mask: true +point_list: +- 111, 114 +- 114, 117 +- 131, 114 +- 135, 117 +- 150, 114 +- 154, 117 +- 170, 114 +- 173, 117 +- 188, 114 +- 192, 118 +- 207, 114 +- 211, 117 +- 226, 114 +- 231, 118 +- 245, 114 +- 250, 118 diff --git a/assets/template/agent_state/aria_cheer_energy_3_1/mask.png b/assets/template/agent_state/aria_cheer_energy_3_1/mask.png new file mode 100644 index 0000000000..7b6bf5a076 Binary files /dev/null and b/assets/template/agent_state/aria_cheer_energy_3_1/mask.png differ diff --git a/assets/template/agent_state/aria_cheer_energy_3_1/raw.png b/assets/template/agent_state/aria_cheer_energy_3_1/raw.png new file mode 100644 index 0000000000..4cba619e25 Binary files /dev/null and b/assets/template/agent_state/aria_cheer_energy_3_1/raw.png differ diff --git a/assets/template/battle/avatar_1_aria/mask.png b/assets/template/battle/avatar_1_aria/mask.png new file mode 100644 index 0000000000..3b9b81f4c8 Binary files /dev/null and b/assets/template/battle/avatar_1_aria/mask.png differ diff --git a/assets/template/battle/avatar_1_aria/raw.png b/assets/template/battle/avatar_1_aria/raw.png new file mode 100644 index 0000000000..bf2c34315f Binary files /dev/null and b/assets/template/battle/avatar_1_aria/raw.png differ diff --git a/assets/template/battle/avatar_1_aria_discordant_note/mask.png b/assets/template/battle/avatar_1_aria_discordant_note/mask.png new file mode 100644 index 0000000000..3b9b81f4c8 Binary files /dev/null and b/assets/template/battle/avatar_1_aria_discordant_note/mask.png differ diff --git a/assets/template/battle/avatar_1_aria_discordant_note/raw.png b/assets/template/battle/avatar_1_aria_discordant_note/raw.png new file mode 100644 index 0000000000..e696303b91 Binary files /dev/null and b/assets/template/battle/avatar_1_aria_discordant_note/raw.png differ diff --git a/assets/template/battle/avatar_1_panyinhu_culinary_jewel/mask.png b/assets/template/battle/avatar_1_panyinhu_culinary_jewel/mask.png new file mode 100644 index 0000000000..3b9b81f4c8 Binary files /dev/null and b/assets/template/battle/avatar_1_panyinhu_culinary_jewel/mask.png differ diff --git a/assets/template/battle/avatar_1_panyinhu_culinary_jewel/raw.png b/assets/template/battle/avatar_1_panyinhu_culinary_jewel/raw.png new file mode 100644 index 0000000000..2e10c6844c Binary files /dev/null and b/assets/template/battle/avatar_1_panyinhu_culinary_jewel/raw.png differ diff --git a/assets/template/battle/avatar_1_sunna_afternoon_tea_break/mask.png b/assets/template/battle/avatar_1_sunna_afternoon_tea_break/mask.png new file mode 100644 index 0000000000..3b9b81f4c8 Binary files /dev/null and b/assets/template/battle/avatar_1_sunna_afternoon_tea_break/mask.png differ diff --git a/assets/template/battle/avatar_1_sunna_afternoon_tea_break/raw.png b/assets/template/battle/avatar_1_sunna_afternoon_tea_break/raw.png new file mode 100644 index 0000000000..6cb72b062a Binary files /dev/null and b/assets/template/battle/avatar_1_sunna_afternoon_tea_break/raw.png differ diff --git a/assets/template/battle/avatar_2_aria/mask.png b/assets/template/battle/avatar_2_aria/mask.png new file mode 100644 index 0000000000..10d1f564c5 Binary files /dev/null and b/assets/template/battle/avatar_2_aria/mask.png differ diff --git a/assets/template/battle/avatar_2_aria/raw.png b/assets/template/battle/avatar_2_aria/raw.png new file mode 100644 index 0000000000..1d7b4dee06 Binary files /dev/null and b/assets/template/battle/avatar_2_aria/raw.png differ diff --git a/assets/template/battle/avatar_2_aria_discordant_note/mask.png b/assets/template/battle/avatar_2_aria_discordant_note/mask.png new file mode 100644 index 0000000000..10d1f564c5 Binary files /dev/null and b/assets/template/battle/avatar_2_aria_discordant_note/mask.png differ diff --git a/assets/template/battle/avatar_2_aria_discordant_note/raw.png b/assets/template/battle/avatar_2_aria_discordant_note/raw.png new file mode 100644 index 0000000000..570b16fe14 Binary files /dev/null and b/assets/template/battle/avatar_2_aria_discordant_note/raw.png differ diff --git a/assets/template/battle/avatar_2_panyinhu_culinary_jewel/mask.png b/assets/template/battle/avatar_2_panyinhu_culinary_jewel/mask.png new file mode 100644 index 0000000000..10d1f564c5 Binary files /dev/null and b/assets/template/battle/avatar_2_panyinhu_culinary_jewel/mask.png differ diff --git a/assets/template/battle/avatar_2_panyinhu_culinary_jewel/raw.png b/assets/template/battle/avatar_2_panyinhu_culinary_jewel/raw.png new file mode 100644 index 0000000000..569d26f265 Binary files /dev/null and b/assets/template/battle/avatar_2_panyinhu_culinary_jewel/raw.png differ diff --git a/assets/template/battle/avatar_2_sunna_afternoon_tea_break/mask.png b/assets/template/battle/avatar_2_sunna_afternoon_tea_break/mask.png new file mode 100644 index 0000000000..10d1f564c5 Binary files /dev/null and b/assets/template/battle/avatar_2_sunna_afternoon_tea_break/mask.png differ diff --git a/assets/template/battle/avatar_2_sunna_afternoon_tea_break/raw.png b/assets/template/battle/avatar_2_sunna_afternoon_tea_break/raw.png new file mode 100644 index 0000000000..8428e9e75e Binary files /dev/null and b/assets/template/battle/avatar_2_sunna_afternoon_tea_break/raw.png differ diff --git a/assets/template/battle/avatar_chain_aria/mask.png b/assets/template/battle/avatar_chain_aria/mask.png new file mode 100644 index 0000000000..2c7f7f8279 Binary files /dev/null and b/assets/template/battle/avatar_chain_aria/mask.png differ diff --git a/assets/template/battle/avatar_chain_aria/raw.png b/assets/template/battle/avatar_chain_aria/raw.png new file mode 100644 index 0000000000..2486b11505 Binary files /dev/null and b/assets/template/battle/avatar_chain_aria/raw.png differ diff --git a/assets/template/battle/avatar_chain_aria_discordant_note/mask.png b/assets/template/battle/avatar_chain_aria_discordant_note/mask.png new file mode 100644 index 0000000000..2c7f7f8279 Binary files /dev/null and b/assets/template/battle/avatar_chain_aria_discordant_note/mask.png differ diff --git a/assets/template/battle/avatar_chain_aria_discordant_note/raw.png b/assets/template/battle/avatar_chain_aria_discordant_note/raw.png new file mode 100644 index 0000000000..b629967507 Binary files /dev/null and b/assets/template/battle/avatar_chain_aria_discordant_note/raw.png differ diff --git a/assets/template/battle/avatar_chain_panyinhu_culinary_jewel/mask.png b/assets/template/battle/avatar_chain_panyinhu_culinary_jewel/mask.png new file mode 100644 index 0000000000..2c7f7f8279 Binary files /dev/null and b/assets/template/battle/avatar_chain_panyinhu_culinary_jewel/mask.png differ diff --git a/assets/template/battle/avatar_chain_panyinhu_culinary_jewel/raw.png b/assets/template/battle/avatar_chain_panyinhu_culinary_jewel/raw.png new file mode 100644 index 0000000000..00166b2907 Binary files /dev/null and b/assets/template/battle/avatar_chain_panyinhu_culinary_jewel/raw.png differ diff --git a/assets/template/battle/avatar_chain_sunna_afternoon_tea_break/mask.png b/assets/template/battle/avatar_chain_sunna_afternoon_tea_break/mask.png new file mode 100644 index 0000000000..2c7f7f8279 Binary files /dev/null and b/assets/template/battle/avatar_chain_sunna_afternoon_tea_break/mask.png differ diff --git a/assets/template/battle/avatar_chain_sunna_afternoon_tea_break/raw.png b/assets/template/battle/avatar_chain_sunna_afternoon_tea_break/raw.png new file mode 100644 index 0000000000..403a0314d9 Binary files /dev/null and b/assets/template/battle/avatar_chain_sunna_afternoon_tea_break/raw.png differ diff --git a/assets/template/battle/avatar_quick_aria/mask.png b/assets/template/battle/avatar_quick_aria/mask.png new file mode 100644 index 0000000000..d7740e56d7 Binary files /dev/null and b/assets/template/battle/avatar_quick_aria/mask.png differ diff --git a/assets/template/battle/avatar_quick_aria/raw.png b/assets/template/battle/avatar_quick_aria/raw.png new file mode 100644 index 0000000000..a3cfe91c74 Binary files /dev/null and b/assets/template/battle/avatar_quick_aria/raw.png differ diff --git a/assets/template/battle/avatar_quick_aria_discordant_note/mask.png b/assets/template/battle/avatar_quick_aria_discordant_note/mask.png new file mode 100644 index 0000000000..d7740e56d7 Binary files /dev/null and b/assets/template/battle/avatar_quick_aria_discordant_note/mask.png differ diff --git a/assets/template/battle/avatar_quick_aria_discordant_note/raw.png b/assets/template/battle/avatar_quick_aria_discordant_note/raw.png new file mode 100644 index 0000000000..a7b0dd165a Binary files /dev/null and b/assets/template/battle/avatar_quick_aria_discordant_note/raw.png differ diff --git a/assets/template/battle/avatar_quick_panyinhu_culinary_jewel/mask.png b/assets/template/battle/avatar_quick_panyinhu_culinary_jewel/mask.png new file mode 100644 index 0000000000..d7740e56d7 Binary files /dev/null and b/assets/template/battle/avatar_quick_panyinhu_culinary_jewel/mask.png differ diff --git a/assets/template/battle/avatar_quick_panyinhu_culinary_jewel/raw.png b/assets/template/battle/avatar_quick_panyinhu_culinary_jewel/raw.png new file mode 100644 index 0000000000..a1e183cab1 Binary files /dev/null and b/assets/template/battle/avatar_quick_panyinhu_culinary_jewel/raw.png differ diff --git a/assets/template/battle/avatar_quick_sunna_afternoon_tea_break/mask.png b/assets/template/battle/avatar_quick_sunna_afternoon_tea_break/mask.png new file mode 100644 index 0000000000..d7740e56d7 Binary files /dev/null and b/assets/template/battle/avatar_quick_sunna_afternoon_tea_break/mask.png differ diff --git a/assets/template/battle/avatar_quick_sunna_afternoon_tea_break/raw.png b/assets/template/battle/avatar_quick_sunna_afternoon_tea_break/raw.png new file mode 100644 index 0000000000..e30aaa1a39 Binary files /dev/null and b/assets/template/battle/avatar_quick_sunna_afternoon_tea_break/raw.png differ diff --git a/assets/template/compendium/completed/config.yml b/assets/template/compendium/completed/config.yml new file mode 100644 index 0000000000..77f703c269 --- /dev/null +++ b/assets/template/compendium/completed/config.yml @@ -0,0 +1,8 @@ +sub_dir: compendium +template_id: completed +template_name: 快捷手册-日常奖励 +template_shape: circle +auto_mask: true +point_list: +- 1567, 268 +- 1587, 268 diff --git a/assets/template/compendium/completed/mask.png b/assets/template/compendium/completed/mask.png new file mode 100644 index 0000000000..e512911eb9 Binary files /dev/null and b/assets/template/compendium/completed/mask.png differ diff --git a/assets/template/compendium/completed/raw.png b/assets/template/compendium/completed/raw.png new file mode 100644 index 0000000000..48729d38cb Binary files /dev/null and b/assets/template/compendium/completed/raw.png differ diff --git a/assets/template/hollow/avatar_aria/mask.png b/assets/template/hollow/avatar_aria/mask.png new file mode 100644 index 0000000000..4ba77ebfd2 Binary files /dev/null and b/assets/template/hollow/avatar_aria/mask.png differ diff --git a/assets/template/hollow/avatar_aria/raw.png b/assets/template/hollow/avatar_aria/raw.png new file mode 100644 index 0000000000..59aea34a63 Binary files /dev/null and b/assets/template/hollow/avatar_aria/raw.png differ diff --git a/assets/template/hollow/avatar_aria_discordant_note/mask.png b/assets/template/hollow/avatar_aria_discordant_note/mask.png new file mode 100644 index 0000000000..4ba77ebfd2 Binary files /dev/null and b/assets/template/hollow/avatar_aria_discordant_note/mask.png differ diff --git a/assets/template/hollow/avatar_aria_discordant_note/raw.png b/assets/template/hollow/avatar_aria_discordant_note/raw.png new file mode 100644 index 0000000000..8876604b07 Binary files /dev/null and b/assets/template/hollow/avatar_aria_discordant_note/raw.png differ diff --git a/assets/template/hollow/avatar_panyinhu_culinary_jewel/mask.png b/assets/template/hollow/avatar_panyinhu_culinary_jewel/mask.png new file mode 100644 index 0000000000..4ba77ebfd2 Binary files /dev/null and b/assets/template/hollow/avatar_panyinhu_culinary_jewel/mask.png differ diff --git a/assets/template/hollow/avatar_panyinhu_culinary_jewel/raw.png b/assets/template/hollow/avatar_panyinhu_culinary_jewel/raw.png new file mode 100644 index 0000000000..25a9944cba Binary files /dev/null and b/assets/template/hollow/avatar_panyinhu_culinary_jewel/raw.png differ diff --git a/assets/template/hollow/avatar_sunna_afternoon_tea_break/mask.png b/assets/template/hollow/avatar_sunna_afternoon_tea_break/mask.png new file mode 100644 index 0000000000..4ba77ebfd2 Binary files /dev/null and b/assets/template/hollow/avatar_sunna_afternoon_tea_break/mask.png differ diff --git a/assets/template/hollow/avatar_sunna_afternoon_tea_break/raw.png b/assets/template/hollow/avatar_sunna_afternoon_tea_break/raw.png new file mode 100644 index 0000000000..db153a3f51 Binary files /dev/null and b/assets/template/hollow/avatar_sunna_afternoon_tea_break/raw.png differ diff --git a/assets/template/intel_board/refresh/config.yml b/assets/template/intel_board/refresh/config.yml new file mode 100644 index 0000000000..c4d9a787f3 --- /dev/null +++ b/assets/template/intel_board/refresh/config.yml @@ -0,0 +1,8 @@ +sub_dir: intel_board +template_id: refresh +template_name: 情报板-刷新 +template_shape: circle +auto_mask: true +point_list: +- 831, 1028 +- 856, 1028 diff --git a/assets/template/intel_board/refresh/mask.png b/assets/template/intel_board/refresh/mask.png new file mode 100644 index 0000000000..7736069262 Binary files /dev/null and b/assets/template/intel_board/refresh/mask.png differ diff --git a/assets/template/intel_board/refresh/raw.png b/assets/template/intel_board/refresh/raw.png new file mode 100644 index 0000000000..8e58dd4544 Binary files /dev/null and b/assets/template/intel_board/refresh/raw.png differ diff --git a/assets/template/normal_world_investigation/btn_area_info_close/config.yml b/assets/template/normal_world_investigation/btn_area_info_close/config.yml new file mode 100644 index 0000000000..dcc524dc21 --- /dev/null +++ b/assets/template/normal_world_investigation/btn_area_info_close/config.yml @@ -0,0 +1,8 @@ +sub_dir: normal_world_investigation +template_id: btn_area_info_close +template_name: 区域信息-关闭 +template_shape: rectangle +auto_mask: true +point_list: +- 1795, 130 +- 1835, 170 diff --git a/assets/template/normal_world_investigation/btn_area_info_close/mask.png b/assets/template/normal_world_investigation/btn_area_info_close/mask.png new file mode 100644 index 0000000000..5152ecd14d Binary files /dev/null and b/assets/template/normal_world_investigation/btn_area_info_close/mask.png differ diff --git a/assets/template/normal_world_investigation/btn_area_info_close/raw.png b/assets/template/normal_world_investigation/btn_area_info_close/raw.png new file mode 100644 index 0000000000..11f975c807 Binary files /dev/null and b/assets/template/normal_world_investigation/btn_area_info_close/raw.png differ diff --git a/assets/template/predefined_team/avatar_aria/mask.png b/assets/template/predefined_team/avatar_aria/mask.png new file mode 100644 index 0000000000..ffff480e7d Binary files /dev/null and b/assets/template/predefined_team/avatar_aria/mask.png differ diff --git a/assets/template/predefined_team/avatar_aria/raw.png b/assets/template/predefined_team/avatar_aria/raw.png new file mode 100644 index 0000000000..b63280e81e Binary files /dev/null and b/assets/template/predefined_team/avatar_aria/raw.png differ diff --git a/assets/template/predefined_team/avatar_aria_discordant_note/mask.png b/assets/template/predefined_team/avatar_aria_discordant_note/mask.png new file mode 100644 index 0000000000..ffff480e7d Binary files /dev/null and b/assets/template/predefined_team/avatar_aria_discordant_note/mask.png differ diff --git a/assets/template/predefined_team/avatar_aria_discordant_note/raw.png b/assets/template/predefined_team/avatar_aria_discordant_note/raw.png new file mode 100644 index 0000000000..670d47c87a Binary files /dev/null and b/assets/template/predefined_team/avatar_aria_discordant_note/raw.png differ diff --git a/assets/template/predefined_team/avatar_panyinhu_culinary_jewel/mask.png b/assets/template/predefined_team/avatar_panyinhu_culinary_jewel/mask.png new file mode 100644 index 0000000000..ffff480e7d Binary files /dev/null and b/assets/template/predefined_team/avatar_panyinhu_culinary_jewel/mask.png differ diff --git a/assets/template/predefined_team/avatar_panyinhu_culinary_jewel/raw.png b/assets/template/predefined_team/avatar_panyinhu_culinary_jewel/raw.png new file mode 100644 index 0000000000..b9f04b0439 Binary files /dev/null and b/assets/template/predefined_team/avatar_panyinhu_culinary_jewel/raw.png differ diff --git a/assets/template/predefined_team/avatar_sunna_afternoon_tea_break/mask.png b/assets/template/predefined_team/avatar_sunna_afternoon_tea_break/mask.png new file mode 100644 index 0000000000..ffff480e7d Binary files /dev/null and b/assets/template/predefined_team/avatar_sunna_afternoon_tea_break/mask.png differ diff --git a/assets/template/predefined_team/avatar_sunna_afternoon_tea_break/raw.png b/assets/template/predefined_team/avatar_sunna_afternoon_tea_break/raw.png new file mode 100644 index 0000000000..bc361b972f Binary files /dev/null and b/assets/template/predefined_team/avatar_sunna_afternoon_tea_break/raw.png differ diff --git "a/config/auto_battle/\345\205\250\351\205\215\351\230\237\351\200\232\347\224\250.merged.yml" "b/config/auto_battle/\345\205\250\351\205\215\351\230\237\351\200\232\347\224\250.merged.yml" index 06c068403e..9a8dcf45ba 100644 --- "a/config/auto_battle/\345\205\250\351\205\215\351\230\237\351\200\232\347\224\250.merged.yml" +++ "b/config/auto_battle/\345\205\250\351\205\215\351\230\237\351\200\232\347\224\250.merged.yml" @@ -343,73 +343,33 @@ - "op_name": "设置状态" "state": "自定义-失衡时间" "states": "[连携技-2-叶瞬光]" - - "debug_name": "叶瞬光连携决策" + - "debug_name": "跳过连携" + "operations": + - "op_name": "设置状态" + "state": "自定义-连携跳过" + - "op_name": "设置状态" + "state": "自定义-失衡时间" + - "op_name": "等待秒数" + "seconds": 0.5 + - "op_name": "设置状态" + "state": "自定义-失衡时间" + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "设置状态" + "state": "自定义-连携取消" + - "op_name": "清除状态" + "state": "按键可用-连携技" "states": "[前台-叶瞬光]" - "sub_handlers": - - "debug_name": "叶瞬光点照-左" - "operations": - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "设置状态" - "seconds_add": -0.5 - "state": "自定义-失衡时间" - - "op_name": "按键-连携技-左" - - "op_name": "设置状态" - "seconds": 1 - "state": "自定义-无视闪光" - - "op_name": "设置状态" - "state": "自定义-连携换人" - - "op_name": "设置状态" - "state": "自定义-失衡时间" - "states": "[连携技-1-照]" - - "debug_name": "叶瞬光点照-右" - "operations": - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "设置状态" - "seconds_add": -0.5 - "state": "自定义-失衡时间" - - "op_name": "按键-连携技-右" - - "op_name": "设置状态" - "seconds": 1 - "state": "自定义-无视闪光" - - "op_name": "设置状态" - "state": "自定义-连携换人" - - "op_name": "设置状态" - "state": "自定义-失衡时间" - "states": "[连携技-2-照]" - - "debug_name": "叶瞬光点支援-左" - "operations": - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "设置状态" - "seconds_add": -0.5 - "state": "自定义-失衡时间" - - "op_name": "按键-连携技-左" - - "op_name": "设置状态" - "seconds": 1 - "state": "自定义-无视闪光" - - "op_name": "设置状态" - "state": "自定义-连携换人" - - "op_name": "设置状态" - "state": "自定义-失衡时间" - "states": "[连携技-1-支援]" - - "debug_name": "叶瞬光点支援-右" - "operations": - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "设置状态" - "seconds_add": -0.5 - "state": "自定义-失衡时间" - - "op_name": "按键-连携技-右" - - "op_name": "设置状态" - "seconds": 1 - "state": "自定义-无视闪光" - - "op_name": "设置状态" - "state": "自定义-连携换人" - - "op_name": "设置状态" - "state": "自定义-失衡时间" - "states": "[连携技-2-支援]" - "states": "[前台-仪玄]" "sub_handlers": - "debug_name": "跳过连携" @@ -424,7 +384,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" @@ -536,7 +505,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" @@ -646,7 +624,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" @@ -816,7 +803,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" @@ -1669,11 +1665,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 100}" "operations": @@ -1984,9 +1985,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2055,9 +2061,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[莱卡恩-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -2105,13 +2116,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -2168,13 +2184,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -2216,13 +2237,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -2337,9 +2363,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[自定义-终结技被强制释放, 0, 1]" @@ -2388,9 +2414,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -2429,9 +2455,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -2572,9 +2598,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2630,9 +2656,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[猫又-终结技可用]" - "operations": - "op_name": "设置状态" @@ -2789,11 +2815,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -2869,11 +2900,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -2940,16 +2976,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3012,16 +3053,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -3101,11 +3147,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3224,11 +3275,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[雅-终结技可用] & [雅-落霜]{0, 3}" - "debug_name": "强化特殊技二连" "operations": @@ -3277,13 +3333,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -3349,13 +3408,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -3413,9 +3475,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "states": "[自定义-黄光切人, 0, 1]" "sub_handlers": @@ -3477,9 +3542,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[赛斯-终结技可用] & [自定义-血量扣减, 0, 2] " - "operations": - "op_name": "设置状态" @@ -3517,9 +3585,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3579,9 +3650,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state": "自定义-合轴时间" - "op_name": "按键-普通攻击" @@ -3613,9 +3687,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3667,9 +3746,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -3710,9 +3794,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3770,9 +3857,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -3825,9 +3915,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3894,9 +3989,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -3972,9 +4072,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -4017,9 +4122,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -4052,301 +4162,158 @@ - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "![悠真-特殊技可用]" - - "debug_name": "非击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "debug_name": "异常队" - "states": "[后台-1-异常] | [后台-2-异常]" - "sub_handlers": - - "states": "[悠真-终结技可用]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "" - - "debug_name": "非异常队" - "states": "" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "[悠真-终结技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "debug_name": "击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-能量]{110, 120}" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" - "states": "[前台-11号]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "设置状态" - "seconds": 4.5 - "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 3.5 - "states": "[自定义-终结技被强制释放, 0, 1]" - - "operations": - - "op_name": "设置状态" - "seconds": 2 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "等待秒数" - "seconds": 0.5 - "states": "[自定义-黄光切人, 0, 1]" - - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 1 - "states": "[自定义-红光闪避, 0, 1]" - - "operations": - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "设置状态" - "seconds": 2 - "state": "自定义-动作不打断" - - "op_name": "等待秒数" - "seconds": 2 - "states": "[自定义-连携换人, 0, 0.5]" - - "debug_name": "切人后等待" - "operations": - - "op_name": "等待秒数" - "seconds": 0.3 - "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" - - "interrupt_states": "![11号-终结技可用]" - "operations": - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "设置状态" - "seconds": 4.5 - "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 3.5 - "states": "[11号-终结技可用]" - - "operations": - - "op_name": "按键-特殊攻击" - "post_delay": 1.2 - "states": "[11号-特殊技可用]" - - "interrupt_states": "[11号-特殊技可用] | [11号-终结技可用]" - "operations": - - "op_name": "按键-普通攻击" - "post_delay": 0.4 - "repeat": 100 - "states": "" - - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-安比]" - "states": "[前台-安比]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "设置状态" - "seconds": 4.5 - "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 3.5 - "states": "[自定义-终结技被强制释放, 0, 1]" - - "operations": - - "op_name": "设置状态" - "seconds": 2 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 - "states": "[自定义-黄光切人, 0, 1]" - - "operations": - - "op_name": "设置状态" - "seconds": 1 - "state": "自定义-动作不打断" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - "states": "[自定义-红光闪避, 0, 1]" - - "operations": - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "设置状态" - "seconds": 2 - "state": "自定义-动作不打断" - - "op_name": "等待秒数" - "seconds": 2 - "states": "[自定义-连携换人, 0, 0.5]" - - "debug_name": "切人后等待" - "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 4 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 19 + "states": "[悠真-特殊技可用]" + - "debug_name": "无脑EA" + "operations": + - "op_name": "按键-特殊攻击-按下" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "按键-特殊攻击-松开" + "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" + "states": "[前台-11号]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4.5 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 15 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "等待秒数" + "seconds": 2 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + - "interrupt_states": "![11号-终结技可用]" + "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4.5 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[11号-终结技可用]" + - "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 1.2 + "states": "[11号-特殊技可用]" + - "interrupt_states": "[11号-特殊技可用] | [11号-终结技可用]" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.4 + "repeat": 100 + "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-安比]" + "states": "[前台-安比]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4.5 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 1 + "state": "自定义-动作不打断" + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "等待秒数" + "seconds": 2 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" "sub_handlers": - "operations": - "op_name": "等待秒数" @@ -4365,9 +4332,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安比-终结技可用] & ![自定义-失衡时间, -10, 10]" - "states": "[自定义-黄光切人, 0, 5]" "sub_handlers": @@ -4447,9 +4417,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4505,9 +4478,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[珂蕾妲-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -4560,9 +4536,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -4591,25 +4565,34 @@ "state": "自定义-柏妮思-灼烧" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-黄光切人, 0, 1]" - "debug_name": "红光闪避" "operations": @@ -4639,50 +4622,68 @@ - "add": 150 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-连携换人, 0, 0.5]" - "debug_name": "切人后等待" "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" "sub_handlers": - "debug_name": "快速支援等待" "operations": - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[按键可用-快速支援, 0, 0.5]" - "debug_name": "短暂等待" "operations": @@ -4699,9 +4700,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -4969,9 +4968,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5035,9 +5039,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] & ![朱鸢-子弹数]{7, 9}" - "operations": - "op_name": "设置状态" @@ -5061,9 +5070,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] &![朱鸢-子弹数]{7, 9} & (![后台-1-击破] & ![后台-2-击破])" - "operations": - "op_name": "按键-移动-左-按下" @@ -5105,8 +5119,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5172,8 +5191,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[艾莲-终结技可用]" - "operations": - "op_name": "设置状态" @@ -5259,9 +5283,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5344,9 +5373,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[青衣-终结技可用] & [青衣-电压]{0,25}" - "interrupt_states": "[青衣-电压]{75,100}" "operations": @@ -5378,9 +5412,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5436,9 +5473,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -5474,9 +5514,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5532,9 +5575,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[比利-终结技可用]" - "operations": - "op_name": "设置状态" @@ -5577,9 +5623,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5650,9 +5699,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安东-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -5685,13 +5737,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -5760,13 +5815,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -5810,6 +5868,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5819,6 +5879,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "add": 2 "op_name": "设置状态" "state": "自定义-非失衡连携" @@ -5858,8 +5920,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5916,8 +5983,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[伊芙琳-终结技可用]" - "operations": - "op_name": "清除状态" @@ -5964,9 +6036,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -6113,9 +6190,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -6191,9 +6273,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -6340,8 +6427,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -6396,8 +6488,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -6436,8 +6533,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -6556,19 +6658,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6670,19 +6767,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -6764,19 +6856,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "清除状态" @@ -6849,9 +6936,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -6915,9 +7007,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -7116,7 +7213,7 @@ "seconds": 0.5 - "op_name": "按键-普通攻击-松开" - "op_name": "等待秒数" - "seconds": 0.1 + "seconds": 0.2 "states": "[仪玄-特殊技可用]" - "operations": - "op_name": "设置状态" @@ -7271,11 +7368,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7317,11 +7419,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "operations": - "op_name": "等待秒数" @@ -7336,11 +7443,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "debug_name": "熊猫只有合轴" "operations": @@ -7378,9 +7490,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7435,9 +7552,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[橘福福-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -7472,9 +7594,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7527,9 +7654,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -7567,9 +7699,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7670,9 +7807,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[爱丽丝-终结技可用] & [爱丽丝-剑仪]{0, 100}" - "operations": - "op_name": "设置状态" @@ -7703,8 +7845,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7786,8 +7933,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[席德-终结技可用] & ![自定义-席德-铁拳冲击, 0, 5]" - "interrupt_states": "[席德-钢能]{110, 999}" "states": "[席德-钢能]{0, 100}" @@ -7826,11 +7978,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7882,11 +8039,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 60}" "operations": @@ -7925,9 +8087,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7981,9 +8148,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[卢西娅-终结技可用] & ![自定义-失衡时间, -10, 12]" - "operations": - "op_name": "设置状态" @@ -8050,9 +8222,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -8105,9 +8282,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[真斗-终结技可用]" - "debug_name": "使用强化特殊技" "operations": @@ -8165,8 +8347,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "清除状态" @@ -8261,8 +8450,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[伊德海莉-终结技可用]" - "debug_name": "失衡期间直接追碾" "operations": @@ -8329,8 +8525,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -8410,8 +8611,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[琉音-终结技可用] & [琉音-好评]{90, 120} & ![自定义-失衡时间, -5, 15]" - "states": "[自定义-失衡时间, 0, 10]" "sub_handlers": @@ -8765,9 +8971,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -8915,9 +9126,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -9056,14 +9272,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1] & [叶瞬光-明心境]{0,0}" @@ -9131,14 +9352,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -9213,14 +9439,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -9250,22 +9481,52 @@ "post_delay": 0.1 "repeat": 50 "states": "" - - "debug_name": "非明心境-强化特殊攻击" - "interrupt_states": "[叶瞬光-明心境]{0,0}" + - "debug_name": "明心境-期间输出" "states": "[叶瞬光-明心境]{1,120}" "sub_handlers": - "debug_name": "等待变身就绪" - "interrupt_states": "[叶瞬光-明心境]{100,120}" + "interrupt_states": "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" "operations": - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" - "op_name": "按键-特殊攻击-松开" - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.7 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 "states": "[叶瞬光-能量]{117,120}" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{4,4}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{3,3}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{4,4}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,1}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{3,3}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{1,1}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + - "interrupt_states": "[叶瞬光-明心境]{0,0}" "operations": - "op_name": "设置状态" "state": "自定义-叶瞬光-收刀" @@ -9300,53 +9561,7 @@ - "op_name": "等待秒数" "seconds": 0.5 - "op_name": "按键-特殊攻击-松开" - "states": "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - - "interrupt_states": "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在天" - - "op_name": "设置状态" - "seconds": 15 - "state": "自定义-无视闪光" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 27 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在地" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 50 - "states": "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" + "states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-照]" "states": "[前台-照]" "sub_handlers": @@ -9359,7 +9574,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9425,7 +9647,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用]" - "debug_name": "照满霜寒值-登场技" "operations": @@ -9460,9 +9689,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9494,39 +9726,266 @@ "seconds": 2 "state": "自定义-动作不打断" - "op_name": "等待秒数" - "seconds": 2 + "seconds": 2 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "[按键可用-快速支援, 0, 0.3]" + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "" + - "debug_name": "千夏-执行特殊攻击" + "interrupt_states": "![千夏-特殊技可用]" + "operations": + - "op_name": "设置状态" + "seconds": 3.0 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "设置状态" + "state": "自定义-合轴时间" + "states": "[千夏-特殊技可用]" + - "debug_name": "千夏-打不了" + "operations": + - "op_name": "设置状态" + "state": "自定义-合轴时间" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-爱芮]" + "states": "[前台-爱芮]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-连携换人, 0, 0.5]" - "debug_name": "切人后等待" "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" "sub_handlers": - "operations": - "op_name": "等待秒数" - "seconds": 0.3 - "states": "[按键可用-快速支援, 0, 0.3]" + "seconds": 1.0 + "states": "[按键可用-快速支援, 0, 0.5]" - "operations": - "op_name": "等待秒数" "seconds": 0.3 "states": "" - - "debug_name": "千夏-执行特殊攻击" - "interrupt_states": "![千夏-特殊技可用]" - "operations": - - "op_name": "设置状态" - "seconds": 3.0 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "[千夏-特殊技可用]" - - "debug_name": "千夏-打不了" - "operations": - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 25 - "states": "" + - "states": "[自定义-失衡时间, -5, 15]" + "sub_handlers": + - "debug_name": "失衡期终结技" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用]" + - "debug_name": "失衡期0~2点开特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "失衡期7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "失衡期普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "states": "" + "sub_handlers": + - "debug_name": "无击破直接开大" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + - "debug_name": "7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "能量满120放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + - "debug_name": "0~2点放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" - "debug_name": "未知角色" "interrupt_states": "[前台-击破] | [前台-强攻] | [前台-支援] | [前台-防护] | [前台-异常] | [前台-命破]" "states": "![前台-击破] & ![前台-强攻] & ![前台-支援] & ![前台-防护] & ![前台-异常] & ![前台-命破]\ @@ -9617,6 +10076,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "op_name": "等待秒数" "seconds": 0.2 - "add": 2 @@ -9700,8 +10161,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "([琉音-终结技可用] & ![自定义-失衡时间, -10, 12])" - "debug_name": "青衣失衡离场" "states": "[前台-青衣] & [自定义-失衡时间, 0, 10]" @@ -9804,11 +10270,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 100}" "operations": @@ -10159,9 +10630,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用]" - "debug_name": "丽娜召唤人偶" "operations": @@ -10236,7 +10712,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用] & [照-霜寒值]{0, 80} & ![自定义-失衡时间, 0, 10]" - "debug_name": "千夏紧急切入" "states": "![前台-千夏]" @@ -10266,15 +10749,19 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[千夏-终结技可用] & ![自定义-失衡时间, 0, 10]" - - "debug_name": "雅满豆切入" + - "debug_name": "雅能量豆数切入" "operations": - "agent_name": "雅" "op_name": "按键-切换角色" - "states": "[雅-落霜]{6, 6} & ![前台-雅] & ![前台-支援]" + "states": "([雅-能量]{80, 999} | ([雅-能量]{40, 999} & [雅-落霜]{2, 6})) & ![前台-雅]\ + \ & ![前台-支援]" - "debug_name": "青衣击破准备" "operations": - "agent_name": "青衣" @@ -10345,9 +10832,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -10416,9 +10908,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[莱卡恩-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -10466,13 +10963,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -10529,13 +11031,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -10577,13 +11084,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -10698,9 +11210,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[自定义-终结技被强制释放, 0, 1]" @@ -10749,9 +11261,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -10790,9 +11302,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -10933,9 +11445,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -10991,9 +11503,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[猫又-终结技可用]" - "operations": - "op_name": "设置状态" @@ -11150,11 +11662,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -11230,11 +11747,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -11301,16 +11823,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11373,16 +11900,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -11462,11 +11994,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11585,11 +12122,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[雅-终结技可用] & [雅-落霜]{0, 3}" - "debug_name": "强化特殊技二连" "operations": @@ -11638,13 +12180,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -11710,13 +12255,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -11774,9 +12322,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "states": "[自定义-黄光切人, 0, 1]" "sub_handlers": @@ -11838,9 +12389,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[赛斯-终结技可用] & [自定义-血量扣减, 0, 2] " - "operations": - "op_name": "设置状态" @@ -11878,9 +12432,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11940,9 +12497,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state": "自定义-合轴时间" - "op_name": "按键-普通攻击" @@ -11974,9 +12534,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12028,9 +12593,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -12071,9 +12641,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12131,9 +12704,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -12186,9 +12762,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12255,9 +12836,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -12333,9 +12919,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -12367,91 +12958,9 @@ "post_delay": 0.1 "repeat": 4 - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 19 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "![悠真-特殊技可用]" - - "debug_name": "非击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "debug_name": "异常队" - "states": "[后台-1-异常] | [后台-2-异常]" - "sub_handlers": - - "states": "[悠真-终结技可用]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" - "op_name": "设置状态" "seconds": 3.6 "state": "自定义-动作不打断" @@ -12460,62 +12969,27 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 - "states": "" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "" - - "debug_name": "非异常队" - "states": "" - "sub_handlers": + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.8 + - "op_name": "按键-普通攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 18 + "states": "[悠真-特殊技可用]" - "operations": - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" "seconds": 2 - "states": "[悠真-终结技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 "state": "自定义-动作不打断" - "op_name": "按键-特殊攻击" "post_delay": 0.1 @@ -12524,70 +12998,40 @@ - "op_name": "等待秒数" "seconds": 1.8 - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - "op_name": "按键-普通攻击" "post_delay": 0.1 - "repeat": 10 + "repeat": 19 "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 + - "operations": - "op_name": "按键-普通攻击" "post_delay": 0.1 - "repeat": 15 + "repeat": 8 - "op_name": "设置状态" "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "debug_name": "击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-能量]{110, 120}" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" + "states": "![悠真-特殊技可用]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 4 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 19 + "states": "[悠真-特殊技可用]" + - "debug_name": "无脑EA" + "operations": + - "op_name": "按键-特殊攻击-按下" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "按键-特殊攻击-松开" + "states": "" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" "states": "[前台-11号]" "sub_handlers": @@ -12600,9 +13044,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12644,9 +13091,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[11号-终结技可用]" - "operations": - "op_name": "按键-特殊攻击" @@ -12670,9 +13120,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12726,9 +13179,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安比-终结技可用] & ![自定义-失衡时间, -10, 10]" - "states": "[自定义-黄光切人, 0, 5]" "sub_handlers": @@ -12808,9 +13264,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12866,9 +13325,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[珂蕾妲-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -12921,9 +13383,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -12952,25 +13412,34 @@ "state": "自定义-柏妮思-灼烧" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-黄光切人, 0, 1]" - "debug_name": "红光闪避" "operations": @@ -13000,50 +13469,68 @@ - "add": 150 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-连携换人, 0, 0.5]" - "debug_name": "切人后等待" "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" "sub_handlers": - "debug_name": "快速支援等待" "operations": - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[按键可用-快速支援, 0, 0.5]" - "debug_name": "短暂等待" "operations": @@ -13060,9 +13547,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -13330,9 +13815,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13396,9 +13886,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] & ![朱鸢-子弹数]{7, 9}" - "operations": - "op_name": "设置状态" @@ -13422,9 +13917,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] &![朱鸢-子弹数]{7, 9} & (![后台-1-击破] & ![后台-2-击破])" - "operations": - "op_name": "按键-移动-左-按下" @@ -13466,8 +13966,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13533,8 +14038,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[艾莲-终结技可用]" - "operations": - "op_name": "设置状态" @@ -13620,9 +14130,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13705,9 +14220,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[青衣-终结技可用] & [青衣-电压]{0,25}" - "interrupt_states": "[青衣-电压]{75,100}" "operations": @@ -13739,9 +14259,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13797,9 +14320,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -13835,9 +14361,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13893,9 +14422,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[比利-终结技可用]" - "operations": - "op_name": "设置状态" @@ -13938,9 +14470,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -14011,9 +14546,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安东-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -14046,13 +14584,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -14121,13 +14662,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -14171,6 +14715,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -14180,6 +14726,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "add": 2 "op_name": "设置状态" "state": "自定义-非失衡连携" @@ -14219,8 +14767,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -14277,8 +14830,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[伊芙琳-终结技可用]" - "operations": - "op_name": "清除状态" @@ -14325,9 +14883,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -14474,9 +15037,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -14552,9 +15120,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -14701,8 +15274,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -14757,8 +15335,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -14797,8 +15380,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -14917,19 +15505,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -15031,19 +15614,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -15125,19 +15703,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "清除状态" @@ -15210,9 +15783,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -15276,9 +15854,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -15477,7 +16060,7 @@ "seconds": 0.5 - "op_name": "按键-普通攻击-松开" - "op_name": "等待秒数" - "seconds": 0.1 + "seconds": 0.2 "states": "[仪玄-特殊技可用]" - "operations": - "op_name": "设置状态" @@ -15632,11 +16215,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -15678,11 +16266,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "operations": - "op_name": "等待秒数" @@ -15697,11 +16290,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "debug_name": "熊猫只有合轴" "operations": @@ -15739,9 +16337,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -15796,9 +16399,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[橘福福-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -15833,9 +16441,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -15888,9 +16501,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -15928,9 +16546,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -16031,9 +16654,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[爱丽丝-终结技可用] & [爱丽丝-剑仪]{0, 100}" - "operations": - "op_name": "设置状态" @@ -16064,8 +16692,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -16147,8 +16780,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[席德-终结技可用] & ![自定义-席德-铁拳冲击, 0, 5]" - "interrupt_states": "[席德-钢能]{110, 999}" "states": "[席德-钢能]{0, 100}" @@ -16187,11 +16825,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -16243,11 +16886,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 60}" "operations": @@ -16286,9 +16934,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -16342,9 +16995,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[卢西娅-终结技可用] & ![自定义-失衡时间, -10, 12]" - "operations": - "op_name": "设置状态" @@ -16411,9 +17069,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -16466,9 +17129,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[真斗-终结技可用]" - "debug_name": "使用强化特殊技" "operations": @@ -16526,8 +17194,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "清除状态" @@ -16622,8 +17297,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[伊德海莉-终结技可用]" - "debug_name": "失衡期间直接追碾" "operations": @@ -16690,8 +17372,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -16771,8 +17458,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[琉音-终结技可用] & [琉音-好评]{90, 120} & ![自定义-失衡时间, -5, 15]" - "states": "[自定义-失衡时间, 0, 10]" "sub_handlers": @@ -17126,9 +17818,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -17276,9 +17973,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -17417,14 +18119,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1] & [叶瞬光-明心境]{0,0}" @@ -17492,14 +18199,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -17574,14 +18286,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -17611,43 +18328,55 @@ "post_delay": 0.1 "repeat": 50 "states": "" - - "debug_name": "非明心境-强化特殊攻击" - "interrupt_states": "[叶瞬光-明心境]{0,0}" + - "debug_name": "明心境-期间输出" "states": "[叶瞬光-明心境]{1,120}" "sub_handlers": - "debug_name": "等待变身就绪" - "interrupt_states": "[叶瞬光-明心境]{100,120}" + "interrupt_states": "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" "operations": - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" - "op_name": "按键-特殊攻击-松开" - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.7 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 "states": "[叶瞬光-能量]{117,120}" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{4,4}" "operations": - - "op_name": "设置状态" - "state": "自定义-叶瞬光-收刀" - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{3,3}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{4,4}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,1}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{3,3}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{1,1}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + - "interrupt_states": "[叶瞬光-明心境]{0,0}" + "operations": + - "op_name": "设置状态" + "state": "自定义-叶瞬光-收刀" - "op_name": "按键-特殊攻击-按下" - "op_name": "等待秒数" "seconds": 0.5 @@ -17660,27 +18389,6 @@ - "op_name": "按键-特殊攻击-按下" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-特殊攻击-松开" - "states": "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - - "interrupt_states": "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在天" - - "op_name": "设置状态" - "seconds": 15 - "state": "自定义-无视闪光" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 27 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - "op_name": "按键-特殊攻击-按下" - "op_name": "等待秒数" "seconds": 0.5 @@ -17699,15 +18407,8 @@ - "op_name": "按键-特殊攻击-按下" - "op_name": "等待秒数" "seconds": 0.5 - "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在地" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 50 - "states": "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" + - "op_name": "按键-特殊攻击-松开" + "states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-照]" "states": "[前台-照]" "sub_handlers": @@ -17720,7 +18421,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -17786,7 +18494,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用]" - "debug_name": "照满霜寒值-登场技" "operations": @@ -17821,9 +18536,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -17888,6 +18606,233 @@ "post_delay": 0.1 "repeat": 25 "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-爱芮]" + "states": "[前台-爱芮]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "operations": + - "op_name": "等待秒数" + "seconds": 1.0 + "states": "[按键可用-快速支援, 0, 0.5]" + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "" + - "states": "[自定义-失衡时间, -5, 15]" + "sub_handlers": + - "debug_name": "失衡期终结技" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用]" + - "debug_name": "失衡期0~2点开特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "失衡期7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "失衡期普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "states": "" + "sub_handlers": + - "debug_name": "无击破直接开大" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + - "debug_name": "7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "能量满120放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + - "debug_name": "0~2点放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" - "debug_name": "未知角色" "interrupt_states": "[前台-击破] | [前台-强攻] | [前台-支援] | [前台-防护] | [前台-异常] | [前台-命破]" "states": "![前台-击破] & ![前台-强攻] & ![前台-支援] & ![前台-防护] & ![前台-异常] & ![前台-命破] &\ diff --git "a/config/auto_battle/\345\207\273\347\240\264\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" "b/config/auto_battle/\345\207\273\347\240\264\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" index 929914aa61..85a8c335b9 100644 --- "a/config/auto_battle/\345\207\273\347\240\264\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" +++ "b/config/auto_battle/\345\207\273\347\240\264\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" @@ -229,7 +229,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" @@ -1159,9 +1168,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1230,9 +1244,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[莱卡恩-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -1280,13 +1299,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -1343,13 +1367,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -1391,13 +1420,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -1512,9 +1546,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[自定义-终结技被强制释放, 0, 1]" @@ -1563,9 +1597,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -1604,9 +1638,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -1747,9 +1781,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1805,9 +1839,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[猫又-终结技可用]" - "operations": - "op_name": "设置状态" @@ -1964,11 +1998,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -2044,11 +2083,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -2115,16 +2159,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2187,16 +2236,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -2276,11 +2330,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2399,11 +2458,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[雅-终结技可用] & [雅-落霜]{0, 3}" - "debug_name": "强化特殊技二连" "operations": @@ -2452,13 +2516,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -2524,13 +2591,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -2588,9 +2658,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "states": "[自定义-黄光切人, 0, 1]" "sub_handlers": @@ -2652,9 +2725,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[赛斯-终结技可用] & [自定义-血量扣减, 0, 2] " - "operations": - "op_name": "设置状态" @@ -2692,9 +2768,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2754,9 +2833,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state": "自定义-合轴时间" - "op_name": "按键-普通攻击" @@ -2788,9 +2870,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2842,9 +2929,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -2885,9 +2977,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2945,9 +3040,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -3000,9 +3098,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3069,9 +3172,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -3147,9 +3255,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -3192,9 +3305,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -3227,181 +3345,29 @@ - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "![悠真-特殊技可用]" - - "debug_name": "非击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "debug_name": "异常队" - "states": "[后台-1-异常] | [后台-2-异常]" - "sub_handlers": - - "states": "[悠真-终结技可用]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "" - - "debug_name": "非异常队" - "states": "" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "[悠真-终结技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "debug_name": "击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-能量]{110, 120}" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 4 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 19 + "states": "[悠真-特殊技可用]" + - "debug_name": "无脑EA" + "operations": + - "op_name": "按键-特殊攻击-按下" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "按键-特殊攻击-松开" + "states": "" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" "states": "[前台-11号]" "sub_handlers": @@ -3414,9 +3380,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3458,9 +3427,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[11号-终结技可用]" - "operations": - "op_name": "按键-特殊攻击" @@ -3484,9 +3456,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3540,9 +3515,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安比-终结技可用] & ![自定义-失衡时间, -10, 10]" - "states": "[自定义-黄光切人, 0, 5]" "sub_handlers": @@ -3622,9 +3600,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3680,9 +3661,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[珂蕾妲-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -3735,9 +3719,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -3766,25 +3748,34 @@ "state": "自定义-柏妮思-灼烧" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-黄光切人, 0, 1]" - "debug_name": "红光闪避" "operations": @@ -3814,50 +3805,68 @@ - "add": 150 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" - - "op_name": "设置状态" - "state": "自定义-柏妮思-灼烧" - "states": "[自定义-连携换人, 0, 0.5]" - - "debug_name": "切人后等待" - "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" - "sub_handlers": - - "debug_name": "快速支援等待" - "operations": - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 - "op_name": "设置状态" - "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "设置状态" + "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "debug_name": "快速支援等待" + "operations": + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 + "op_name": "设置状态" + "state": "自定义-异常-火" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[按键可用-快速支援, 0, 0.5]" - "debug_name": "短暂等待" "operations": @@ -3874,9 +3883,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -4144,9 +4151,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4210,9 +4222,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] & ![朱鸢-子弹数]{7, 9}" - "operations": - "op_name": "设置状态" @@ -4236,9 +4253,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] &![朱鸢-子弹数]{7, 9} & (![后台-1-击破] & ![后台-2-击破])" - "operations": - "op_name": "按键-移动-左-按下" @@ -4280,8 +4302,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4347,8 +4374,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[艾莲-终结技可用]" - "operations": - "op_name": "设置状态" @@ -4434,9 +4466,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4519,9 +4556,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[青衣-终结技可用] & [青衣-电压]{0,25}" - "interrupt_states": "[青衣-电压]{75,100}" "operations": @@ -4553,9 +4595,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4611,9 +4656,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -4649,9 +4697,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4707,9 +4758,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[比利-终结技可用]" - "operations": - "op_name": "设置状态" @@ -4752,9 +4806,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4825,9 +4882,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安东-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -4860,13 +4920,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -4935,13 +4998,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -4985,6 +5051,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4994,6 +5062,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "add": 2 "op_name": "设置状态" "state": "自定义-非失衡连携" @@ -5033,8 +5103,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5091,8 +5166,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[伊芙琳-终结技可用]" - "operations": - "op_name": "清除状态" @@ -5139,9 +5219,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -5288,9 +5373,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -5366,9 +5456,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -5515,8 +5610,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -5571,8 +5671,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -5611,8 +5716,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -5731,19 +5841,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5845,19 +5950,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -5939,19 +6039,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "清除状态" @@ -6024,9 +6119,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -6090,9 +6190,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -6291,7 +6396,7 @@ "seconds": 0.5 - "op_name": "按键-普通攻击-松开" - "op_name": "等待秒数" - "seconds": 0.1 + "seconds": 0.2 "states": "[仪玄-特殊技可用]" - "operations": - "op_name": "设置状态" @@ -6446,11 +6551,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6492,11 +6602,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "operations": - "op_name": "等待秒数" @@ -6511,11 +6626,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "debug_name": "熊猫只有合轴" "operations": @@ -6553,9 +6673,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6610,9 +6735,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[橘福福-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -6647,9 +6777,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6702,9 +6837,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -6742,9 +6882,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6845,9 +6990,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[爱丽丝-终结技可用] & [爱丽丝-剑仪]{0, 100}" - "operations": - "op_name": "设置状态" @@ -6878,8 +7028,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6961,8 +7116,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[席德-终结技可用] & ![自定义-席德-铁拳冲击, 0, 5]" - "interrupt_states": "[席德-钢能]{110, 999}" "states": "[席德-钢能]{0, 100}" @@ -7001,11 +7161,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7057,11 +7222,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 60}" "operations": @@ -7100,9 +7270,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7156,9 +7331,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[卢西娅-终结技可用] & ![自定义-失衡时间, -10, 12]" - "operations": - "op_name": "设置状态" @@ -7225,9 +7405,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7280,9 +7465,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[真斗-终结技可用]" - "debug_name": "使用强化特殊技" "operations": @@ -7340,8 +7530,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "清除状态" @@ -7436,8 +7633,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[伊德海莉-终结技可用]" - "debug_name": "失衡期间直接追碾" "operations": @@ -7504,8 +7708,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7585,8 +7794,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[琉音-终结技可用] & [琉音-好评]{90, 120} & ![自定义-失衡时间, -5, 15]" - "states": "[自定义-失衡时间, 0, 10]" "sub_handlers": @@ -7940,9 +8154,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -8090,9 +8309,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -8231,14 +8455,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1] & [叶瞬光-明心境]{0,0}" @@ -8306,14 +8535,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -8388,14 +8622,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -8425,22 +8664,52 @@ "post_delay": 0.1 "repeat": 50 "states": "" - - "debug_name": "非明心境-强化特殊攻击" - "interrupt_states": "[叶瞬光-明心境]{0,0}" + - "debug_name": "明心境-期间输出" "states": "[叶瞬光-明心境]{1,120}" "sub_handlers": - "debug_name": "等待变身就绪" - "interrupt_states": "[叶瞬光-明心境]{100,120}" + "interrupt_states": "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" "operations": - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" - "op_name": "按键-特殊攻击-松开" - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.7 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 "states": "[叶瞬光-能量]{117,120}" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{4,4}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{3,3}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{4,4}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,1}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{3,3}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{1,1}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + - "interrupt_states": "[叶瞬光-明心境]{0,0}" "operations": - "op_name": "设置状态" "state": "自定义-叶瞬光-收刀" @@ -8475,53 +8744,7 @@ - "op_name": "等待秒数" "seconds": 0.5 - "op_name": "按键-特殊攻击-松开" - "states": "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - - "interrupt_states": "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在天" - - "op_name": "设置状态" - "seconds": 15 - "state": "自定义-无视闪光" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 27 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在地" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 50 - "states": "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" + "states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-照]" "states": "[前台-照]" "sub_handlers": @@ -8534,7 +8757,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -8600,7 +8830,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用]" - "debug_name": "照满霜寒值-登场技" "operations": @@ -8635,9 +8872,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -8702,6 +8942,233 @@ "post_delay": 0.1 "repeat": 25 "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-爱芮]" + "states": "[前台-爱芮]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "operations": + - "op_name": "等待秒数" + "seconds": 1.0 + "states": "[按键可用-快速支援, 0, 0.5]" + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "" + - "states": "[自定义-失衡时间, -5, 15]" + "sub_handlers": + - "debug_name": "失衡期终结技" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用]" + - "debug_name": "失衡期0~2点开特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "失衡期7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "失衡期普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "states": "" + "sub_handlers": + - "debug_name": "无击破直接开大" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + - "debug_name": "7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "能量满120放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + - "debug_name": "0~2点放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" - "debug_name": "未知角色" "interrupt_states": "[前台-击破] | [前台-强攻] | [前台-支援] | [前台-防护] | [前台-异常] | [前台-命破]" "states": "![前台-击破] & ![前台-强攻] & ![前台-支援] & ![前台-防护] & ![前台-异常] & ![前台-命破]\ diff --git "a/config/auto_battle/\345\274\202\345\270\270\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" "b/config/auto_battle/\345\274\202\345\270\270\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" index 05fb8921fa..5bab9ab9db 100644 --- "a/config/auto_battle/\345\274\202\345\270\270\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" +++ "b/config/auto_battle/\345\274\202\345\270\270\347\253\231\345\234\272-\345\274\272\346\224\273\351\200\237\345\210\207.merged.yml" @@ -201,7 +201,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" diff --git "a/config/auto_battle/\345\274\272\346\224\273\347\253\231\345\234\272-\345\207\273\347\240\264\346\224\257\346\217\264\351\200\237\345\210\207.merged.yml" "b/config/auto_battle/\345\274\272\346\224\273\347\253\231\345\234\272-\345\207\273\347\240\264\346\224\257\346\217\264\351\200\237\345\210\207.merged.yml" index 258a9c9abd..2c81d37bff 100644 --- "a/config/auto_battle/\345\274\272\346\224\273\347\253\231\345\234\272-\345\207\273\347\240\264\346\224\257\346\217\264\351\200\237\345\210\207.merged.yml" +++ "b/config/auto_battle/\345\274\272\346\224\273\347\253\231\345\234\272-\345\207\273\347\240\264\346\224\257\346\217\264\351\200\237\345\210\207.merged.yml" @@ -203,7 +203,16 @@ "state": "自定义-失衡时间" - "op_name": "按键-连携技-取消-按下" - "op_name": "等待秒数" - "seconds": 0.4 + "seconds": 0.1 + - "op_name": "按键-连携技-取消-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "按键-连携技-取消-按下" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-连携取消" - "op_name": "清除状态" @@ -225,9 +234,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -296,9 +310,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[莱卡恩-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -346,13 +365,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -409,13 +433,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -457,13 +486,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -578,9 +612,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[自定义-终结技被强制释放, 0, 1]" @@ -629,9 +663,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -670,9 +704,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -813,9 +847,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -871,9 +905,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[猫又-终结技可用]" - "operations": - "op_name": "设置状态" @@ -1030,11 +1064,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1110,11 +1149,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1181,16 +1225,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1253,16 +1302,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -1342,11 +1396,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1465,11 +1524,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[雅-终结技可用] & [雅-落霜]{0, 3}" - "debug_name": "强化特殊技二连" "operations": @@ -1518,13 +1582,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1590,13 +1657,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1654,9 +1724,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "states": "[自定义-黄光切人, 0, 1]" "sub_handlers": @@ -1718,9 +1791,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[赛斯-终结技可用] & [自定义-血量扣减, 0, 2] " - "operations": - "op_name": "设置状态" @@ -1758,9 +1834,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1820,9 +1899,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state": "自定义-合轴时间" - "op_name": "按键-普通攻击" @@ -1854,9 +1936,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1908,9 +1995,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -1951,9 +2043,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2011,9 +2106,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -2066,9 +2164,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2135,9 +2238,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -2212,9 +2320,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -2257,9 +2370,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -2292,181 +2410,29 @@ - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "![悠真-特殊技可用]" - - "debug_name": "非击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "debug_name": "异常队" - "states": "[后台-1-异常] | [后台-2-异常]" - "sub_handlers": - - "states": "[悠真-终结技可用]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "" - - "debug_name": "非异常队" - "states": "" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "[悠真-终结技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "debug_name": "击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-能量]{110, 120}" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 4 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 19 + "states": "[悠真-特殊技可用]" + - "debug_name": "无脑EA" + "operations": + - "op_name": "按键-特殊攻击-按下" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "按键-特殊攻击-松开" + "states": "" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" "states": "[前台-11号]" "sub_handlers": @@ -2479,9 +2445,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2523,9 +2492,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[11号-终结技可用]" - "operations": - "op_name": "按键-特殊攻击" @@ -2549,9 +2521,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2605,9 +2580,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安比-终结技可用] & ![自定义-失衡时间, -10, 10]" - "states": "[自定义-黄光切人, 0, 5]" "sub_handlers": @@ -2687,9 +2665,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2745,9 +2726,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[珂蕾妲-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -2800,9 +2784,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -2831,25 +2813,34 @@ "state": "自定义-柏妮思-灼烧" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-黄光切人, 0, 1]" - "debug_name": "红光闪避" "operations": @@ -2879,50 +2870,68 @@ - "add": 150 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" - - "op_name": "设置状态" - "state": "自定义-柏妮思-灼烧" - "states": "[自定义-连携换人, 0, 0.5]" - - "debug_name": "切人后等待" - "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" - "sub_handlers": - - "debug_name": "快速支援等待" - "operations": - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "设置状态" + "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "debug_name": "快速支援等待" + "operations": + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[按键可用-快速支援, 0, 0.5]" - "debug_name": "短暂等待" "operations": @@ -2939,9 +2948,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -3209,9 +3216,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3275,9 +3287,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] & ![朱鸢-子弹数]{7, 9}" - "operations": - "op_name": "设置状态" @@ -3301,9 +3318,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] &![朱鸢-子弹数]{7, 9} & (![后台-1-击破] & ![后台-2-击破])" - "operations": - "op_name": "按键-移动-左-按下" @@ -3345,8 +3367,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3412,8 +3439,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[艾莲-终结技可用]" - "operations": - "op_name": "设置状态" @@ -3499,9 +3531,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3584,9 +3621,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[青衣-终结技可用] & [青衣-电压]{0,25}" - "interrupt_states": "[青衣-电压]{75,100}" "operations": @@ -3618,9 +3660,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3676,9 +3721,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -3714,9 +3762,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3772,9 +3823,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[比利-终结技可用]" - "operations": - "op_name": "设置状态" @@ -3817,9 +3871,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3890,9 +3947,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安东-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -3925,13 +3985,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -4000,13 +4063,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -4050,6 +4116,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4059,6 +4127,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "add": 2 "op_name": "设置状态" "state": "自定义-非失衡连携" @@ -4098,8 +4168,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4156,8 +4231,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[伊芙琳-终结技可用]" - "operations": - "op_name": "清除状态" @@ -4204,9 +4284,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -4353,9 +4438,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -4431,9 +4521,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -4580,8 +4675,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -4636,8 +4736,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -4676,8 +4781,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -4796,19 +4906,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4910,19 +5015,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -5004,19 +5104,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "清除状态" @@ -5089,9 +5184,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -5155,9 +5255,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -5356,7 +5461,7 @@ "seconds": 0.5 - "op_name": "按键-普通攻击-松开" - "op_name": "等待秒数" - "seconds": 0.1 + "seconds": 0.2 "states": "[仪玄-特殊技可用]" - "operations": - "op_name": "设置状态" @@ -5511,11 +5616,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5557,11 +5667,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "operations": - "op_name": "等待秒数" @@ -5576,11 +5691,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "debug_name": "熊猫只有合轴" "operations": @@ -5618,9 +5738,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5675,9 +5800,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[橘福福-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -5712,9 +5842,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5767,9 +5902,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -5807,9 +5947,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5910,9 +6055,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[爱丽丝-终结技可用] & [爱丽丝-剑仪]{0, 100}" - "operations": - "op_name": "设置状态" @@ -5943,8 +6093,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6026,8 +6181,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[席德-终结技可用] & ![自定义-席德-铁拳冲击, 0, 5]" - "interrupt_states": "[席德-钢能]{110, 999}" "states": "[席德-钢能]{0, 100}" @@ -6066,11 +6226,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6122,11 +6287,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 60}" "operations": @@ -6165,9 +6335,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6221,9 +6396,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[卢西娅-终结技可用] & ![自定义-失衡时间, -10, 12]" - "operations": - "op_name": "设置状态" @@ -6290,9 +6470,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6345,9 +6530,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[真斗-终结技可用]" - "debug_name": "使用强化特殊技" "operations": @@ -6405,8 +6595,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "清除状态" @@ -6501,8 +6698,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[伊德海莉-终结技可用]" - "debug_name": "失衡期间直接追碾" "operations": @@ -6569,8 +6773,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6650,8 +6859,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[琉音-终结技可用] & [琉音-好评]{90, 120} & ![自定义-失衡时间, -5, 15]" - "states": "[自定义-失衡时间, 0, 10]" "sub_handlers": @@ -7005,9 +7219,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -7155,9 +7374,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -7296,14 +7520,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1] & [叶瞬光-明心境]{0,0}" @@ -7371,14 +7600,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -7453,14 +7687,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -7490,22 +7729,52 @@ "post_delay": 0.1 "repeat": 50 "states": "" - - "debug_name": "非明心境-强化特殊攻击" - "interrupt_states": "[叶瞬光-明心境]{0,0}" + - "debug_name": "明心境-期间输出" "states": "[叶瞬光-明心境]{1,120}" "sub_handlers": - "debug_name": "等待变身就绪" - "interrupt_states": "[叶瞬光-明心境]{100,120}" + "interrupt_states": "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" "operations": - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" - "op_name": "按键-特殊攻击-松开" - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.7 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 "states": "[叶瞬光-能量]{117,120}" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{4,4}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{3,3}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{4,4}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,1}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{3,3}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{1,1}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + - "interrupt_states": "[叶瞬光-明心境]{0,0}" "operations": - "op_name": "设置状态" "state": "自定义-叶瞬光-收刀" @@ -7540,53 +7809,7 @@ - "op_name": "等待秒数" "seconds": 0.5 - "op_name": "按键-特殊攻击-松开" - "states": "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - - "interrupt_states": "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在天" - - "op_name": "设置状态" - "seconds": 15 - "state": "自定义-无视闪光" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 27 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在地" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 50 - "states": "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" + "states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-照]" "states": "[前台-照]" "sub_handlers": @@ -7599,7 +7822,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7665,7 +7895,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用]" - "debug_name": "照满霜寒值-登场技" "operations": @@ -7700,9 +7937,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7767,6 +8007,233 @@ "post_delay": 0.1 "repeat": 25 "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-爱芮]" + "states": "[前台-爱芮]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "operations": + - "op_name": "等待秒数" + "seconds": 1.0 + "states": "[按键可用-快速支援, 0, 0.5]" + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "" + - "states": "[自定义-失衡时间, -5, 15]" + "sub_handlers": + - "debug_name": "失衡期终结技" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用]" + - "debug_name": "失衡期0~2点开特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "失衡期7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "失衡期普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "states": "" + "sub_handlers": + - "debug_name": "无击破直接开大" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + - "debug_name": "7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "能量满120放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + - "debug_name": "0~2点放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" - "debug_name": "未知角色" "interrupt_states": "[前台-击破] | [前台-强攻] | [前台-支援] | [前台-防护] | [前台-异常] | [前台-命破]" "states": "![前台-击破] & ![前台-强攻] & ![前台-支援] & ![前台-防护] & ![前台-异常] & ![前台-命破] & (![按键-切换角色-下一个,\ @@ -7837,9 +8304,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7908,9 +8380,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[莱卡恩-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -7958,13 +8435,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -8021,13 +8503,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -8069,13 +8556,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -8190,9 +8682,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[自定义-终结技被强制释放, 0, 1]" @@ -8241,9 +8733,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -8282,9 +8774,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -8425,9 +8917,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -8483,9 +8975,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[猫又-终结技可用]" - "operations": - "op_name": "设置状态" @@ -8642,11 +9134,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -8722,11 +9219,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -8793,16 +9295,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -8865,16 +9372,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -8954,11 +9466,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9077,11 +9594,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[雅-终结技可用] & [雅-落霜]{0, 3}" - "debug_name": "强化特殊技二连" "operations": @@ -9130,13 +9652,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -9202,13 +9727,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -9266,9 +9794,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "states": "[自定义-黄光切人, 0, 1]" "sub_handlers": @@ -9330,9 +9861,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[赛斯-终结技可用] & [自定义-血量扣减, 0, 2] " - "operations": - "op_name": "设置状态" @@ -9370,9 +9904,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9432,9 +9969,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state": "自定义-合轴时间" - "op_name": "按键-普通攻击" @@ -9466,9 +10006,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9520,9 +10065,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -9563,9 +10113,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9623,9 +10176,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -9678,9 +10234,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -9747,9 +10308,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -9824,9 +10390,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -9869,9 +10440,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -9904,181 +10480,29 @@ - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "![悠真-特殊技可用]" - - "debug_name": "非击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "debug_name": "异常队" - "states": "[后台-1-异常] | [后台-2-异常]" - "sub_handlers": - - "states": "[悠真-终结技可用]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "" - - "debug_name": "非异常队" - "states": "" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "[悠真-终结技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "debug_name": "击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-能量]{110, 120}" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 4 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 19 + "states": "[悠真-特殊技可用]" + - "debug_name": "无脑EA" + "operations": + - "op_name": "按键-特殊攻击-按下" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "按键-特殊攻击-松开" + "states": "" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" "states": "[前台-11号]" "sub_handlers": @@ -10091,9 +10515,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -10135,9 +10562,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[11号-终结技可用]" - "operations": - "op_name": "按键-特殊攻击" @@ -10161,9 +10591,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -10217,9 +10650,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安比-终结技可用] & ![自定义-失衡时间, -10, 10]" - "states": "[自定义-黄光切人, 0, 5]" "sub_handlers": @@ -10299,9 +10735,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -10357,9 +10796,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[珂蕾妲-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -10412,9 +10854,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -10443,25 +10883,34 @@ "state": "自定义-柏妮思-灼烧" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-黄光切人, 0, 1]" - "debug_name": "红光闪避" "operations": @@ -10491,50 +10940,68 @@ - "add": 150 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-连携换人, 0, 0.5]" - "debug_name": "切人后等待" "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" "sub_handlers": - "debug_name": "快速支援等待" "operations": - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 - "op_name": "设置状态" - "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 + "op_name": "设置状态" + "state": "自定义-异常-火" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[按键可用-快速支援, 0, 0.5]" - "debug_name": "短暂等待" "operations": @@ -10551,9 +11018,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -10821,9 +11286,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -10887,9 +11357,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] & ![朱鸢-子弹数]{7, 9}" - "operations": - "op_name": "设置状态" @@ -10913,9 +11388,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] &![朱鸢-子弹数]{7, 9} & (![后台-1-击破] & ![后台-2-击破])" - "operations": - "op_name": "按键-移动-左-按下" @@ -10957,8 +11437,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11024,8 +11509,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[艾莲-终结技可用]" - "operations": - "op_name": "设置状态" @@ -11111,9 +11601,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11196,9 +11691,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[青衣-终结技可用] & [青衣-电压]{0,25}" - "interrupt_states": "[青衣-电压]{75,100}" "operations": @@ -11230,9 +11730,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11288,9 +11791,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -11326,9 +11832,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11384,9 +11893,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[比利-终结技可用]" - "operations": - "op_name": "设置状态" @@ -11429,9 +11941,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11502,9 +12017,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安东-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -11537,13 +12055,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -11612,13 +12133,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -11662,6 +12186,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11671,6 +12197,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "add": 2 "op_name": "设置状态" "state": "自定义-非失衡连携" @@ -11710,8 +12238,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -11768,8 +12301,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[伊芙琳-终结技可用]" - "operations": - "op_name": "清除状态" @@ -11816,9 +12354,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -11965,9 +12508,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -12043,9 +12591,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -12192,8 +12745,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -12248,8 +12806,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -12288,8 +12851,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -12408,19 +12976,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -12522,19 +13085,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -12616,19 +13174,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "清除状态" @@ -12701,9 +13254,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -12767,9 +13325,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -12968,7 +13531,7 @@ "seconds": 0.5 - "op_name": "按键-普通攻击-松开" - "op_name": "等待秒数" - "seconds": 0.1 + "seconds": 0.2 "states": "[仪玄-特殊技可用]" - "operations": - "op_name": "设置状态" @@ -13123,11 +13686,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13169,11 +13737,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "operations": - "op_name": "等待秒数" @@ -13188,11 +13761,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "debug_name": "熊猫只有合轴" "operations": @@ -13230,9 +13808,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13287,9 +13870,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[橘福福-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -13324,9 +13912,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13379,9 +13972,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -13419,9 +14017,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13522,9 +14125,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[爱丽丝-终结技可用] & [爱丽丝-剑仪]{0, 100}" - "operations": - "op_name": "设置状态" @@ -13555,8 +14163,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13638,8 +14251,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[席德-终结技可用] & ![自定义-席德-铁拳冲击, 0, 5]" - "interrupt_states": "[席德-钢能]{110, 999}" "states": "[席德-钢能]{0, 100}" @@ -13678,11 +14296,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13734,11 +14357,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 60}" "operations": @@ -13777,9 +14405,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13833,9 +14466,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[卢西娅-终结技可用] & ![自定义-失衡时间, -10, 12]" - "operations": - "op_name": "设置状态" @@ -13902,9 +14540,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -13957,9 +14600,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[真斗-终结技可用]" - "debug_name": "使用强化特殊技" "operations": @@ -14017,8 +14665,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "清除状态" @@ -14113,8 +14768,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[伊德海莉-终结技可用]" - "debug_name": "失衡期间直接追碾" "operations": @@ -14181,8 +14843,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -14262,8 +14929,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[琉音-终结技可用] & [琉音-好评]{90, 120} & ![自定义-失衡时间, -5, 15]" - "states": "[自定义-失衡时间, 0, 10]" "sub_handlers": @@ -14617,9 +15289,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -14767,9 +15444,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -14908,14 +15590,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1] & [叶瞬光-明心境]{0,0}" @@ -14983,14 +15670,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -15065,14 +15757,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -15102,22 +15799,52 @@ "post_delay": 0.1 "repeat": 50 "states": "" - - "debug_name": "非明心境-强化特殊攻击" - "interrupt_states": "[叶瞬光-明心境]{0,0}" + - "debug_name": "明心境-期间输出" "states": "[叶瞬光-明心境]{1,120}" "sub_handlers": - "debug_name": "等待变身就绪" - "interrupt_states": "[叶瞬光-明心境]{100,120}" + "interrupt_states": "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" "operations": - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" - "op_name": "按键-特殊攻击-松开" - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.7 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 "states": "[叶瞬光-能量]{117,120}" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{4,4}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{3,3}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{4,4}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,1}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{3,3}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{1,1}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + - "interrupt_states": "[叶瞬光-明心境]{0,0}" "operations": - "op_name": "设置状态" "state": "自定义-叶瞬光-收刀" @@ -15152,53 +15879,7 @@ - "op_name": "等待秒数" "seconds": 0.5 - "op_name": "按键-特殊攻击-松开" - "states": "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - - "interrupt_states": "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在天" - - "op_name": "设置状态" - "seconds": 15 - "state": "自定义-无视闪光" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 27 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在地" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 50 - "states": "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" + "states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-照]" "states": "[前台-照]" "sub_handlers": @@ -15211,7 +15892,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -15277,7 +15965,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用]" - "debug_name": "照满霜寒值-登场技" "operations": @@ -15312,9 +16007,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -15379,6 +16077,233 @@ "post_delay": 0.1 "repeat": 25 "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-爱芮]" + "states": "[前台-爱芮]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "operations": + - "op_name": "等待秒数" + "seconds": 1.0 + "states": "[按键可用-快速支援, 0, 0.5]" + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "" + - "states": "[自定义-失衡时间, -5, 15]" + "sub_handlers": + - "debug_name": "失衡期终结技" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用]" + - "debug_name": "失衡期0~2点开特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "失衡期7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "失衡期普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "states": "" + "sub_handlers": + - "debug_name": "无击破直接开大" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + - "debug_name": "7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "能量满120放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + - "debug_name": "0~2点放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" - "debug_name": "未知角色" "interrupt_states": "[前台-击破] | [前台-强攻] | [前台-支援] | [前台-防护] | [前台-异常] | [前台-命破]" "states": "![前台-击破] & ![前台-强攻] & ![前台-支援] & ![前台-防护] & ![前台-异常] & ![前台-命破] & (![按键-切换角色-下一个,\ diff --git "a/config/auto_battle/\350\207\252\345\212\250\345\256\210\346\212\244.merged.yml" "b/config/auto_battle/\350\207\252\345\212\250\345\256\210\346\212\244.merged.yml" index 6cecef05bc..c93e58f23d 100644 --- "a/config/auto_battle/\350\207\252\345\212\250\345\256\210\346\212\244.merged.yml" +++ "b/config/auto_battle/\350\207\252\345\212\250\345\256\210\346\212\244.merged.yml" @@ -195,9 +195,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -266,9 +271,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "莱卡恩" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[莱卡恩-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -316,13 +326,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -379,13 +394,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -427,13 +447,18 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 10 - "op_name": "设置状态" "state": "自定义-苍角-展旗" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "苍角" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state": "自定义-动作不打断" - "op_name": "等待秒数" @@ -548,9 +573,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[自定义-终结技被强制释放, 0, 1]" @@ -599,9 +624,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -640,9 +665,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "妮可" + "op_name": "按键-切换角色" - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "[妮可-终结技可用]" @@ -783,9 +808,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -841,9 +866,9 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 1 + "repeat": 20 + - "agent_name": "猫又" + "op_name": "按键-切换角色" "states": "[猫又-终结技可用]" - "operations": - "op_name": "设置状态" @@ -1000,11 +1025,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1080,11 +1110,16 @@ - "op_name": "设置状态" "seconds": 6 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "派派" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 30 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 - "add": 288 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1151,16 +1186,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1223,16 +1263,21 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.5 - "add": 226 "op_name": "设置状态" "state": "自定义-异常-电" - "op_name": "设置状态" "state": "自定义-柳-流转" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "柳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 + - "op_name": "等待秒数" + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -1312,11 +1357,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1435,11 +1485,16 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "雅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "add": 309 "op_name": "设置状态" "state": "自定义-异常-烈霜" - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[雅-终结技可用] & [雅-落霜]{0, 3}" - "debug_name": "强化特殊技二连" "operations": @@ -1488,13 +1543,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1560,13 +1618,16 @@ - "op_name": "设置状态" "seconds": 4.4 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "简" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 14 - "op_name": "等待秒数" - "seconds": 1.4 + "seconds": 1 - "add": 193 "op_name": "设置状态" "state": "自定义-异常-物理" @@ -1624,9 +1685,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "states": "[自定义-黄光切人, 0, 1]" "sub_handlers": @@ -1688,9 +1752,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[赛斯-终结技可用] & [自定义-血量扣减, 0, 2] " - "operations": - "op_name": "设置状态" @@ -1728,9 +1795,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1790,9 +1860,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state": "自定义-合轴时间" - "op_name": "按键-普通攻击" @@ -1824,9 +1897,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1878,9 +1956,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "凯撒" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 25 - "op_name": "等待秒数" - "seconds": 4.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -1921,9 +2004,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -1981,9 +2067,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -2036,9 +2125,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2105,9 +2199,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -2183,9 +2282,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -2228,9 +2332,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 16 + "repeat": 20 + - "agent_name": "悠真" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 6 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 0.8 @@ -2263,181 +2372,29 @@ - "op_name": "设置状态" "state": "自定义-合轴时间" "states": "![悠真-特殊技可用]" - - "debug_name": "非击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "debug_name": "异常队" - "states": "[后台-1-异常] | [后台-2-异常]" - "sub_handlers": - - "states": "[悠真-终结技可用]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - "states": "" - - "debug_name": "非异常队" - "states": "" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 3.6 - "state": "自定义-动作不打断" - - "op_name": "设置状态" - "seconds_add": -1 - "state": "自定义-失衡时间" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 16 - - "op_name": "等待秒数" - "seconds": 2 - "states": "[悠真-终结技可用]" - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - "states": "[悠真-特殊技可用]" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" - - "debug_name": "击破队" - "states": "![后台-1-击破] & ![后台-2-击破]" - "sub_handlers": - - "operations": - - "op_name": "设置状态" - "seconds": 2.2 - "state": "自定义-动作不打断" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 1.8 - - "op_name": "按键-特殊攻击-松开" - - "op_name": "按键-移动-左-按下" - - "op_name": "按键-闪避" - "post_delay": 0.2 - - "op_name": "按键-移动-左-松开" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 18 - "states": "[悠真-能量]{110, 120}" - - "debug_name": "清空电壶离场" - "operations": - - "op_name": "按键-普通攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.8 - - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.1 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 15 - - "op_name": "设置状态" - "state": "自定义-合轴时间" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 30 - "states": "" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 4 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 1.8 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 19 + "states": "[悠真-特殊技可用]" + - "debug_name": "无脑EA" + "operations": + - "op_name": "按键-特殊攻击-按下" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "按键-特殊攻击-松开" + "states": "" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-11号]" "states": "[前台-11号]" "sub_handlers": @@ -2450,9 +2407,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2494,9 +2454,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[11号-终结技可用]" - "operations": - "op_name": "按键-特殊攻击" @@ -2520,9 +2483,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2576,9 +2542,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安比-终结技可用] & ![自定义-失衡时间, -10, 10]" - "states": "[自定义-黄光切人, 0, 5]" "sub_handlers": @@ -2658,9 +2627,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -2716,9 +2688,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[珂蕾妲-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -2771,9 +2746,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -2802,25 +2775,34 @@ "state": "自定义-柏妮思-灼烧" - "op_name": "等待秒数" "seconds": 0.5 - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[自定义-黄光切人, 0, 1]" - "debug_name": "红光闪避" "operations": @@ -2850,50 +2832,68 @@ - "add": 150 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" - - "op_name": "设置状态" - "state": "自定义-柏妮思-灼烧" - "states": "[自定义-连携换人, 0, 0.5]" - - "debug_name": "切人后等待" - "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" - "sub_handlers": - - "debug_name": "快速支援等待" - "operations": - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "add": 200 - "op_name": "设置状态" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "op_name": "设置状态" + "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "debug_name": "快速支援等待" + "operations": + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 + - "add": 300 + "op_name": "设置状态" "state": "自定义-异常-火" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 4 - - "op_name": "按键-移动-前-按下" - - "op_name": "按键-闪避" - "post_delay": 0.02 - "repeat": 4 - - "op_name": "按键-移动-前-松开" + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-按下" + - "op_name": "等待秒数" + "seconds": 0.3 + - "op_name": "按键-特殊攻击-松开" + - "op_name": "等待秒数" + "seconds": 0.1 - "op_name": "设置状态" "state": "自定义-柏妮思-灼烧" + - "op_name": "等待秒数" + "seconds": 0.6 "states": "[按键可用-快速支援, 0, 0.5]" - "debug_name": "短暂等待" "operations": @@ -2910,9 +2910,7 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 - - "op_name": "等待秒数" - "seconds": 2 + "repeat": 30 - "op_name": "清除状态" "state": "自定义-异常-火" - "op_name": "等待秒数" @@ -3180,9 +3178,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3246,9 +3249,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] & ![朱鸢-子弹数]{7, 9}" - "operations": - "op_name": "设置状态" @@ -3272,9 +3280,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "朱鸢" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 7 - "op_name": "等待秒数" - "seconds": 0.7 + "seconds": 1 "states": "[朱鸢-终结技可用] &![朱鸢-子弹数]{7, 9} & (![后台-1-击破] & ![后台-2-击破])" - "operations": - "op_name": "按键-移动-左-按下" @@ -3316,8 +3329,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3383,8 +3401,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "艾莲" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[艾莲-终结技可用]" - "operations": - "op_name": "设置状态" @@ -3470,9 +3493,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3555,9 +3583,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "青衣" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 "states": "[青衣-终结技可用] & [青衣-电压]{0,25}" - "interrupt_states": "[青衣-电压]{75,100}" "operations": @@ -3589,9 +3622,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3647,9 +3683,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-血量扣减" @@ -3685,9 +3724,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3743,9 +3785,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[比利-终结技可用]" - "operations": - "op_name": "设置状态" @@ -3788,9 +3833,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -3861,9 +3909,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[安东-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -3896,13 +3947,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -3971,13 +4025,16 @@ - "op_name": "设置状态" "seconds": 10 "state": "自定义-动作不打断" - - "op_name": "设置状态" - "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "格莉丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 70 - "op_name": "等待秒数" - "seconds": 1.5 + "seconds": 1 - "add": 176 "op_name": "设置状态" "state": "自定义-异常-电" @@ -4021,6 +4078,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4030,6 +4089,8 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 3 + - "agent_name": "耀嘉音" + "op_name": "按键-切换角色" - "add": 2 "op_name": "设置状态" "state": "自定义-非失衡连携" @@ -4069,8 +4130,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4127,8 +4193,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "伊芙琳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[伊芙琳-终结技可用]" - "operations": - "op_name": "清除状态" @@ -4175,9 +4246,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -4324,9 +4400,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -4402,9 +4483,14 @@ "state": "自定义-零号安比-白雷" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 25 + "repeat": 20 + - "agent_name": "零号安比" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2.4 + "seconds": 1 - "op_name": "设置状态" "seconds": 2 "state": "自定义-动作不打断" @@ -4551,8 +4637,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -4607,8 +4698,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -4647,8 +4743,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "扳机" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "设置状态" "state_list": - "自定义-合轴时间" @@ -4767,19 +4868,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -4881,19 +4977,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "按键-普通攻击" @@ -4975,19 +5066,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 - - "op_name": "等待秒数" - "seconds": 2.0 - - "op_name": "设置状态" - "seconds": 2.5 - "state": "自定义-动作不打断" - - "op_name": "按键-普通攻击" + "repeat": 20 + - "agent_name": "薇薇安" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 5 - - "op_name": "设置状态" - "state": "自定义-合轴时间" + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[薇薇安-终结技可用]" - "operations": - "op_name": "清除状态" @@ -5060,9 +5146,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -5126,9 +5217,14 @@ "state": "自定义-失衡时间" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "雨果" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 - "op_name": "按键-普通攻击-按下" - "op_name": "等待秒数" "seconds": 3.0 @@ -5327,7 +5423,7 @@ "seconds": 0.5 - "op_name": "按键-普通攻击-松开" - "op_name": "等待秒数" - "seconds": 0.1 + "seconds": 0.2 "states": "[仪玄-特殊技可用]" - "operations": - "op_name": "设置状态" @@ -5482,11 +5578,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5528,11 +5629,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "operations": - "op_name": "等待秒数" @@ -5547,11 +5653,16 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "潘引壶" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 10 - "op_name": "等待秒数" - "seconds": 3 + "seconds": 1 "states": "[潘引壶-终结技可用]" - "debug_name": "熊猫只有合轴" "operations": @@ -5589,9 +5700,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5646,9 +5762,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "橘福福" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.8 + "seconds": 1 "states": "[橘福福-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -5683,9 +5804,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5738,9 +5864,14 @@ "state": "自定义-浮波柚叶-狸之愿" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "浮波柚叶" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[浮波柚叶-终结技可用] & ![自定义-失衡时间, -10, 10]" - "operations": - "op_name": "设置状态" @@ -5778,9 +5909,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5881,9 +6017,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 28 + "repeat": 20 + - "agent_name": "爱丽丝" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 18 - "op_name": "等待秒数" - "seconds": 2.0 + "seconds": 1 "states": "[爱丽丝-终结技可用] & [爱丽丝-剑仪]{0, 100}" - "operations": - "op_name": "设置状态" @@ -5914,8 +6055,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -5997,8 +6143,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "席德" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 2.5 + "seconds": 1 "states": "[席德-终结技可用] & ![自定义-席德-铁拳冲击, 0, 5]" - "interrupt_states": "[席德-钢能]{110, 999}" "states": "[席德-钢能]{0, 100}" @@ -6037,11 +6188,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6093,11 +6249,16 @@ - "op_name": "设置状态" "seconds": 5.5 "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "奥菲丝" + "op_name": "按键-切换角色" - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 25 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[奥菲丝-终结技可用] & ![自定义-奥菲丝-喷火中, 0 ,5.5]" - "interrupt_states": "[奥菲丝-蓄炎]{0, 60}" "operations": @@ -6136,9 +6297,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6192,9 +6358,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "卢西娅" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[卢西娅-终结技可用] & ![自定义-失衡时间, -10, 12]" - "operations": - "op_name": "设置状态" @@ -6261,9 +6432,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6316,9 +6492,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 30 + "repeat": 20 + - "agent_name": "真斗" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 2 + "seconds": 1 "states": "[真斗-终结技可用]" - "debug_name": "使用强化特殊技" "operations": @@ -6376,8 +6557,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "清除状态" @@ -6472,8 +6660,15 @@ "seconds": 4.1 "state": "自定义-动作不打断" - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "伊德海莉" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 11 - "op_name": "等待秒数" - "seconds": 4.1 + "seconds": 1 "states": "[伊德海莉-终结技可用]" - "debug_name": "失衡期间直接追碾" "operations": @@ -6540,8 +6735,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -6621,8 +6821,13 @@ - "op_name": "按键-终结技" "post_delay": 0.1 "repeat": 20 + - "agent_name": "琉音" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 - "op_name": "等待秒数" - "seconds": 3.0 + "seconds": 1 "states": "[琉音-终结技可用] & [琉音-好评]{90, 120} & ![自定义-失衡时间, -5, 15]" - "states": "[自定义-失衡时间, 0, 10]" "sub_handlers": @@ -6976,9 +7181,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -7126,9 +7336,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "agent_name": "般岳" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 - "op_name": "等待秒数" - "seconds": 5 + "seconds": 1 - "op_name": "清除状态" "state_list": - "自定义-动作不打断" @@ -7267,14 +7482,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1] & [叶瞬光-明心境]{0,0}" @@ -7342,14 +7562,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -7424,14 +7649,19 @@ - "op_name": "设置状态" "seconds": 4 "state": "自定义-动作不打断" - - "op_name": "按键-终结技" - "post_delay": 0.1 - "repeat": 30 - "op_name": "设置状态" "state": "自定义-叶瞬光-在天" - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "叶瞬光" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 - "op_name": "等待秒数" "seconds": 1 "states": "[叶瞬光-终结技可用] & [叶瞬光-青溟剑势-红]{0,3}" @@ -7461,22 +7691,52 @@ "post_delay": 0.1 "repeat": 50 "states": "" - - "debug_name": "非明心境-强化特殊攻击" - "interrupt_states": "[叶瞬光-明心境]{0,0}" + - "debug_name": "明心境-期间输出" "states": "[叶瞬光-明心境]{1,120}" "sub_handlers": - "debug_name": "等待变身就绪" - "interrupt_states": "[叶瞬光-明心境]{100,120}" + "interrupt_states": "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" "operations": - "op_name": "设置状态" "seconds": 15 "state": "自定义-无视闪光" - "op_name": "按键-特殊攻击-松开" - "op_name": "按键-普通攻击-松开" - - "op_name": "等待秒数" - "seconds": 0.7 + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 "states": "[叶瞬光-能量]{117,120}" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{4,4}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{3,3}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{4,4}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{1,1}" + "operations": + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{3,3}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{1,1}" + - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0}" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 20 + "states": "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + - "interrupt_states": "[叶瞬光-明心境]{0,0}" "operations": - "op_name": "设置状态" "state": "自定义-叶瞬光-收刀" @@ -7511,53 +7771,7 @@ - "op_name": "等待秒数" "seconds": 0.5 - "op_name": "按键-特殊攻击-松开" - "states": "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - - "interrupt_states": "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在天" - - "op_name": "设置状态" - "seconds": 15 - "state": "自定义-无视闪光" - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 10 - - "op_name": "按键-特殊攻击" - "post_delay": 0.1 - "repeat": 27 - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 8 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - - "op_name": "按键-特殊攻击-按下" - - "op_name": "等待秒数" - "seconds": 0.5 - "states": "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - - "interrupt_states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" - "operations": - - "op_name": "清除状态" - "state": "自定义-叶瞬光-在地" - - "op_name": "按键-普通攻击" - "post_delay": 0.1 - "repeat": 50 - "states": "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" + "states": "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-照]" "states": "[前台-照]" "sub_handlers": @@ -7570,7 +7784,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7636,7 +7857,14 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 60 + "repeat": 20 + - "agent_name": "照" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 30 + - "op_name": "等待秒数" + "seconds": 1 "states": "[照-终结技可用]" - "debug_name": "照满霜寒值-登场技" "operations": @@ -7671,9 +7899,12 @@ "state": "自定义-动作不打断" - "op_name": "按键-终结技" "post_delay": 0.1 - "repeat": 10 + "repeat": 20 + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 15 - "op_name": "等待秒数" - "seconds": 3.5 + "seconds": 1 "states": "[自定义-终结技被强制释放, 0, 1]" - "operations": - "op_name": "设置状态" @@ -7738,6 +7969,233 @@ "post_delay": 0.1 "repeat": 25 "states": "" + - "interrupt_states": "[前台-能量, 0, 0.1] & ![前台-爱芮]" + "states": "[前台-爱芮]" + "sub_handlers": + - "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-终结技被强制释放, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-黄光切人, 0, 1]" + - "operations": + - "op_name": "按键-移动-左-按下" + - "op_name": "按键-闪避" + "post_delay": 0.2 + - "op_name": "按键-移动-左-松开" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + "states": "[自定义-红光闪避, 0, 1]" + - "operations": + - "op_name": "设置状态" + "seconds_add": -1 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[自定义-连携换人, 0, 0.5]" + - "debug_name": "切人后等待" + "states": "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + "sub_handlers": + - "operations": + - "op_name": "等待秒数" + "seconds": 1.0 + "states": "[按键可用-快速支援, 0, 0.5]" + - "operations": + - "op_name": "等待秒数" + "seconds": 0.3 + "states": "" + - "states": "[自定义-失衡时间, -5, 15]" + "sub_handlers": + - "debug_name": "失衡期终结技" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用]" + - "debug_name": "失衡期0~2点开特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "失衡期7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "失衡期普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" + - "states": "" + "sub_handlers": + - "debug_name": "无击破直接开大" + "operations": + - "op_name": "设置状态" + "seconds_add": -2 + "state": "自定义-失衡时间" + - "op_name": "设置状态" + "seconds": 4 + "state": "自定义-动作不打断" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 20 + - "agent_name": "爱芮" + "op_name": "按键-切换角色" + - "op_name": "按键-终结技" + "post_delay": 0.1 + "repeat": 10 + - "op_name": "等待秒数" + "seconds": 1 + "states": "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + - "debug_name": "7点以上长按" + "interrupt_states": "[爱芮-应援能量]{0, 1}" + "operations": + - "op_name": "设置状态" + "seconds": 10 + "state": "自定义-动作不打断" + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-按下" + - "op_name": "等待秒数" + "seconds": 1 + - "op_name": "按键-普通攻击-松开" + "states": "[爱芮-应援能量]{6, 8}" + - "debug_name": "能量满120放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + - "debug_name": "0~2点放特殊技" + "operations": + - "op_name": "设置状态" + "seconds": 2 + "state": "自定义-动作不打断" + - "op_name": "按键-特殊攻击" + "post_delay": 0.1 + "repeat": 20 + - "op_name": "等待秒数" + "seconds": 0.5 + "states": "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + - "debug_name": "普攻" + "operations": + - "op_name": "按键-普通攻击" + "post_delay": 0.1 + "repeat": 25 + "states": "" - "debug_name": "未知角色" "interrupt_states": "[前台-击破] | [前台-强攻] | [前台-支援] | [前台-防护] | [前台-异常] | [前台-命破]" "states": "![前台-击破] & ![前台-强攻] & ![前台-支援] & ![前台-防护] & ![前台-异常] & ![前台-命破] &\ diff --git "a/config/auto_battle_operation/\344\273\252\347\216\204-\345\207\270.sample.yml" "b/config/auto_battle_operation/\344\273\252\347\216\204-\345\207\270.sample.yml" index 64cb002ca9..1846b3355b 100644 --- "a/config/auto_battle_operation/\344\273\252\347\216\204-\345\207\270.sample.yml" +++ "b/config/auto_battle_operation/\344\273\252\347\216\204-\345\207\270.sample.yml" @@ -24,4 +24,4 @@ operations: seconds: 0.5 - op_name: "按键-普通攻击-松开" - op_name: "等待秒数" - seconds: 0.1 + seconds: 0.2 diff --git "a/config/auto_battle_operation/\344\274\212\345\276\267\346\265\267\350\216\211-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\344\274\212\345\276\267\346\265\267\350\216\211-\347\273\210\347\273\223\346\212\200.sample.yml" index 81e52c30a1..0726c09c82 100644 --- "a/config/auto_battle_operation/\344\274\212\345\276\267\346\265\267\350\216\211-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\344\274\212\345\276\267\346\265\267\350\216\211-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -6,5 +6,12 @@ operations: state: "自定义-动作不打断" seconds: 4.1 - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "伊德海莉" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 11 - op_name: "等待秒数" - seconds: 4.1 + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\344\274\212\350\212\231\347\220\263-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\344\274\212\350\212\231\347\220\263-\347\273\210\347\273\223\346\212\200.sample.yml" index 1da71ce880..8a7e96b09e 100644 --- "a/config/auto_battle_operation/\344\274\212\350\212\231\347\220\263-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\344\274\212\350\212\231\347\220\263-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -10,5 +10,10 @@ operations: - op_name: "按键-终结技" post_delay: 0.1 repeat: 20 + - op_name: "按键-切换角色" + agent_name: "伊芙琳" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 - op_name: "等待秒数" - seconds: 2.5 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\345\207\257\346\222\222-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\345\207\257\346\222\222-\347\273\210\347\273\223\346\212\200.sample.yml" index e2aca51572..fae2dca274 100644 --- "a/config/auto_battle_operation/\345\207\257\346\222\222-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\345\207\257\346\222\222-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 5.5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "凯撒" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 25 - op_name: "等待秒数" - seconds: 4.5 \ No newline at end of file + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\345\215\242\350\245\277\345\250\205-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\345\215\242\350\245\277\345\250\205-\347\273\210\347\273\223\346\212\200.sample.yml" index 09d2e9e044..ab60b2e891 100644 --- "a/config/auto_battle_operation/\345\215\242\350\245\277\345\250\205-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\345\215\242\350\245\277\345\250\205-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "卢西娅" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 2 \ No newline at end of file + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\346\224\266.sample.yml" "b/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\346\224\266.sample.yml" index 7dcdddfbfc..7bb9e69342 100644 --- "a/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\346\224\266.sample.yml" +++ "b/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\346\224\266.sample.yml" @@ -1,4 +1,4 @@ -# 根据动画实测,待补充 +# 根据动画实测,动画持续6.5秒 operations: - op_name: "设置状态" state: "自定义-失衡时间" @@ -6,9 +6,14 @@ operations: # 终结技动画期间, 失衡不会动 - op_name: "设置状态" state: "自定义-动作不打断" - seconds: 6.5 # 待补充具体时间 + seconds: 6.5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 40 # 待补充具体次数 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "叶瞬光" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 35 - op_name: "等待秒数" - seconds: 2.5 # 待补充具体时间 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\350\265\267.sample.yml" "b/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\350\265\267.sample.yml" index 20568afa92..6b9065705f 100644 --- "a/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\350\265\267.sample.yml" +++ "b/config/auto_battle_operation/\345\217\266\347\236\254\345\205\211-\347\273\210\347\273\223\346\212\200-\350\265\267.sample.yml" @@ -1,4 +1,4 @@ -# 根据动画实测,待补充 +# 根据动画实测,动画持续4秒 operations: - op_name: "设置状态" state: "自定义-失衡时间" @@ -6,14 +6,19 @@ operations: # 终结技动画期间, 失衡不会动 - op_name: "设置状态" state: "自定义-动作不打断" - seconds: 4 # 待补充具体时间 - - op_name: "按键-终结技" - post_delay: 0.1 - repeat: 30 # 待补充具体次数 + seconds: 4 - op_name: "设置状态" state: "自定义-叶瞬光-在天" - op_name: "设置状态" state: "自定义-无视闪光" seconds: 15 + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "叶瞬光" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 10 - op_name: "等待秒数" - seconds: 1 # 待补充具体时间 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\345\245\245\350\217\262\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\345\245\245\350\217\262\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" index 4af536dd9b..82cdc7afa6 100644 --- "a/config/auto_battle_operation/\345\245\245\350\217\262\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\345\245\245\350\217\262\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -6,8 +6,13 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 5.5 + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "奥菲丝" - op_name: "按键-终结技" post_delay: 0.1 repeat: 25 - op_name: "等待秒数" - seconds: 3.0 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\345\246\256\345\217\257-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\345\246\256\345\217\257-\347\273\210\347\273\223\346\212\200.sample.yml" index 2541b10b88..9266bdc3d6 100644 --- "a/config/auto_battle_operation/\345\246\256\345\217\257-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\345\246\256\345\217\257-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -11,8 +11,8 @@ operations: seconds: 2 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 - - op_name: "等待秒数" - seconds: 1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "妮可" - op_name: "设置状态" - state: "自定义-合轴时间" \ No newline at end of file + state: "自定义-合轴时间" diff --git "a/config/auto_battle_operation/\345\270\255\345\276\267-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\345\270\255\345\276\267-\347\273\210\347\273\223\346\212\200.sample.yml" index 71ebb4065f..b0b614e40e 100644 --- "a/config/auto_battle_operation/\345\270\255\345\276\267-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\345\270\255\345\276\267-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -1,4 +1,4 @@ -# 根据动画实测,动画持续2秒 +# 根据动画实测,动画持续4.5秒 operations: - op_name: "设置状态" state: "自定义-失衡时间" @@ -9,5 +9,10 @@ operations: - op_name: "按键-终结技" post_delay: 0.1 repeat: 20 + - op_name: "按键-切换角色" + agent_name: "席德" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 - op_name: "等待秒数" - seconds: 2.5 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\346\202\240\347\234\237-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\202\240\347\234\237-\347\273\210\347\273\223\346\212\200.sample.yml" index 8b40ae4662..0152a97c08 100644 --- "a/config/auto_battle_operation/\346\202\240\347\234\237-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\202\240\347\234\237-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -9,6 +9,11 @@ operations: seconds_add: -1 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 16 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "悠真" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 6 - op_name: "等待秒数" - seconds: 2 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\346\202\240\347\234\237-\350\207\252\345\212\250EA.sample.yml" "b/config/auto_battle_operation/\346\202\240\347\234\237-\350\207\252\345\212\250EA.sample.yml" new file mode 100644 index 0000000000..377d593a62 --- /dev/null +++ "b/config/auto_battle_operation/\346\202\240\347\234\237-\350\207\252\345\212\250EA.sample.yml" @@ -0,0 +1,7 @@ +# 悠真自动EA:按住E不停普攻 +operations: + - op_name: "按键-特殊攻击-按下" + - op_name: "按键-普通攻击" + repeat: 10 + post_delay: 0.1 + - op_name: "按键-特殊攻击-松开" diff --git "a/config/auto_battle_operation/\346\211\263\346\234\272-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\211\263\346\234\272-\347\273\210\347\273\223\346\212\200.sample.yml" index acdba148b8..c1951778d4 100644 --- "a/config/auto_battle_operation/\346\211\263\346\234\272-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\211\263\346\234\272-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -9,7 +9,12 @@ operations: - op_name: "按键-终结技" post_delay: 0.1 repeat: 20 + - op_name: "按键-切换角色" + agent_name: "扳机" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 10 - op_name: "等待秒数" - seconds: 2 + seconds: 1 - op_name: "设置状态" state_list: ["自定义-合轴时间", "自定义-扳机-强化追击"] diff --git "a/config/auto_battle_operation/\346\234\261\351\270\242-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\234\261\351\270\242-\347\273\210\347\273\223\346\212\200.sample.yml" index 1c57850ceb..3e914662c3 100644 --- "a/config/auto_battle_operation/\346\234\261\351\270\242-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\234\261\351\270\242-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -12,6 +12,11 @@ operations: seconds: 3.7 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "朱鸢" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 7 - op_name: "等待秒数" - seconds: 0.7 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\346\237\217\345\246\256\346\200\235-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\237\217\345\246\256\346\200\235-\347\273\210\347\273\223\346\212\200.sample.yml" index 26ad2dc198..2cf76a81b4 100644 --- "a/config/auto_battle_operation/\346\237\217\345\246\256\346\200\235-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\237\217\345\246\256\346\200\235-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,9 +8,7 @@ operations: seconds: 3 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 - - op_name: "等待秒数" - seconds: 2 + repeat: 30 - op_name: "清除状态" state: "自定义-异常-火" - op_name: "等待秒数" diff --git "a/config/auto_battle_operation/\346\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" index a07c3197d2..0b851d27f5 100644 --- "a/config/auto_battle_operation/\346\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -6,13 +6,18 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 5.5 - - op_name: "按键-终结技" - post_delay: 0.1 - repeat: 30 - - op_name: "等待秒数" - seconds: 2.5 - op_name: "设置状态" state: "自定义-异常-电" add: 226 - op_name: "设置状态" - state: "自定义-柳-流转" \ No newline at end of file + state: "自定义-柳-流转" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "柳" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 25 + - op_name: "等待秒数" + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\346\240\274\350\216\211\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\240\274\350\216\211\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" index 30e4c22d70..17113f28bb 100644 --- "a/config/auto_battle_operation/\346\240\274\350\216\211\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\240\274\350\216\211\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -6,13 +6,16 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 10 - - op_name: "设置状态" - state: "自定义-动作不打断" - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "格莉丝" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 70 - op_name: "等待秒数" - seconds: 1.5 + seconds: 1 - op_name: "设置状态" state: "自定义-异常-电" add: 176 diff --git "a/config/auto_battle_operation/\346\251\230\347\246\217\347\246\217-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\251\230\347\246\217\347\246\217-\347\273\210\347\273\223\346\212\200.sample.yml" index 3a957b5574..f1d2046372 100644 --- "a/config/auto_battle_operation/\346\251\230\347\246\217\347\246\217-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\251\230\347\246\217\347\246\217-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "橘福福" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 3.8 \ No newline at end of file + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\346\264\276\346\264\276-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\264\276\346\264\276-\347\273\210\347\273\223\346\212\200.sample.yml" index 766faf7e46..b9bcd5443c 100644 --- "a/config/auto_battle_operation/\346\264\276\346\264\276-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\264\276\346\264\276-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -6,11 +6,16 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 6 + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "派派" - op_name: "按键-终结技" post_delay: 0.1 repeat: 30 - op_name: "等待秒数" - seconds: 3 + seconds: 1 - op_name: "设置状态" state: "自定义-异常-物理" - add: 288 \ No newline at end of file + add: 288 diff --git "a/config/auto_battle_operation/\346\265\256\346\263\242\346\237\232\345\217\266-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\265\256\346\263\242\346\237\232\345\217\266-\347\273\210\347\273\223\346\212\200.sample.yml" index f05cce5de6..538f3d69bc 100644 --- "a/config/auto_battle_operation/\346\265\256\346\263\242\346\237\232\345\217\266-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\265\256\346\263\242\346\237\232\345\217\266-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -11,6 +11,11 @@ operations: state: "自定义-浮波柚叶-狸之愿" - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "浮波柚叶" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 2 \ No newline at end of file + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\346\275\230\345\274\225\345\243\266-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\346\275\230\345\274\225\345\243\266-\347\273\210\347\273\223\346\212\200.sample.yml" index 90b7591e71..875b3eeb00 100644 --- "a/config/auto_battle_operation/\346\275\230\345\274\225\345\243\266-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\346\275\230\345\274\225\345\243\266-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -1,4 +1,4 @@ -# 根据动画实测,动画持续5秒 +# 根据动画实测,动画持续4秒 operations: - op_name: "设置状态" state: "自定义-失衡时间" @@ -6,8 +6,13 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 4 + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "潘引壶" - op_name: "按键-终结技" post_delay: 0.1 repeat: 10 - op_name: "等待秒数" - seconds: 3 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\347\205\247-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\205\247-\347\273\210\347\273\223\346\212\200.sample.yml" index 76f9e49c22..e3786f27bb 100644 --- "a/config/auto_battle_operation/\347\205\247-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\347\205\247-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,4 +8,11 @@ operations: seconds: 6 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 60 \ No newline at end of file + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "照" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 30 + - op_name: "等待秒数" + seconds: 1 diff --git "a/config/auto_battle_operation/\347\210\261\344\270\275\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\210\261\344\270\275\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" index bc674d187e..789bd055fc 100644 --- "a/config/auto_battle_operation/\347\210\261\344\270\275\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\347\210\261\344\270\275\344\270\235-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 4.8 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 28 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "爱丽丝" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 18 - op_name: "等待秒数" - seconds: 2.0 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\345\274\272\345\214\226\347\211\271\346\256\212\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\345\274\272\345\214\226\347\211\271\346\256\212\346\212\200.sample.yml" new file mode 100644 index 0000000000..67b070e1d5 --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\345\274\272\345\214\226\347\211\271\346\256\212\346\212\200.sample.yml" @@ -0,0 +1,10 @@ +# 强化特殊技:狂点2秒 +operations: + - op_name: "设置状态" + state: "自定义-动作不打断" + seconds: 2 + - op_name: "按键-特殊攻击" + post_delay: 0.1 + repeat: 20 + - op_name: "等待秒数" + seconds: 0.5 diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\346\224\257\346\217\264\346\224\273\345\207\273.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\346\224\257\346\217\264\346\224\273\345\207\273.sample.yml" new file mode 100644 index 0000000000..9fc877b37a --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\346\224\257\346\217\264\346\224\273\345\207\273.sample.yml" @@ -0,0 +1,10 @@ +# 快速支援/支援攻击 +operations: + - op_name: "设置状态" + state: "自定义-动作不打断" + seconds: 2 + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 10 + - op_name: "等待秒数" + seconds: 1 diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\346\231\256\351\200\232\346\224\273\345\207\273.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\346\231\256\351\200\232\346\224\273\345\207\273.sample.yml" new file mode 100644 index 0000000000..44fcf7a2ff --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\346\231\256\351\200\232\346\224\273\345\207\273.sample.yml" @@ -0,0 +1,5 @@ +# 普攻产应援能量,第四段产1个 +operations: + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 25 diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\347\273\210\347\273\223\346\212\200.sample.yml" new file mode 100644 index 0000000000..9a1c67e9bc --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -0,0 +1,18 @@ +# 终结技:狂点4秒,动画2秒,等待1秒 +operations: + - op_name: "设置状态" + state: "自定义-失衡时间" + seconds_add: -2 + - op_name: "设置状态" + state: "自定义-动作不打断" + seconds: 4 + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "爱芮" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 10 + - op_name: "等待秒数" + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\350\277\236\346\220\272\346\224\273\345\207\273.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\350\277\236\346\220\272\346\224\273\345\207\273.sample.yml" new file mode 100644 index 0000000000..5aa1224f1f --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\350\277\236\346\220\272\346\224\273\345\207\273.sample.yml" @@ -0,0 +1,13 @@ +# 连携技 +operations: + - op_name: "设置状态" + state: "自定义-失衡时间" + seconds_add: -1 + - op_name: "设置状态" + state: "自定义-动作不打断" + seconds: 2 + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 10 + - op_name: "等待秒数" + seconds: 1 diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\351\225\277\346\214\211\346\224\273\345\207\273.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\351\225\277\346\214\211\346\224\273\345\207\273.sample.yml" new file mode 100644 index 0000000000..b74c09d173 --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\351\225\277\346\214\211\346\224\273\345\207\273.sample.yml" @@ -0,0 +1,37 @@ +# 长按普攻:绝对音准,吸收应援能量,消耗2个可跳第三段 +# 蓄力期间抗打断+40%减伤+无敌,最长蓄力15秒 +operations: + - op_name: "设置状态" + state: "自定义-动作不打断" + seconds: 10 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-按下" + - op_name: "等待秒数" + seconds: 1 + - op_name: "按键-普通攻击-松开" diff --git "a/config/auto_battle_operation/\347\210\261\350\212\256-\351\227\252A.sample.yml" "b/config/auto_battle_operation/\347\210\261\350\212\256-\351\227\252A.sample.yml" new file mode 100644 index 0000000000..9ea9aca64d --- /dev/null +++ "b/config/auto_battle_operation/\347\210\261\350\212\256-\351\227\252A.sample.yml" @@ -0,0 +1,6 @@ +# 闪避后普攻 +operations: + - operation_template: "通用-闪避-左" + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 10 diff --git "a/config/auto_battle_operation/\347\214\253\345\217\210-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\214\253\345\217\210-\347\273\210\347\273\223\346\212\200.sample.yml" index 36b7318a94..cb25794cc7 100644 --- "a/config/auto_battle_operation/\347\214\253\345\217\210-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\347\214\253\345\217\210-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,6 @@ operations: seconds: 2 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 - - op_name: "等待秒数" - seconds: 1 \ No newline at end of file + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "猫又" diff --git "a/config/auto_battle_operation/\347\220\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\220\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" index d499be817e..dfeab37124 100644 --- "a/config/auto_battle_operation/\347\220\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\347\220\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -12,5 +12,10 @@ operations: - op_name: "按键-终结技" post_delay: 0.1 repeat: 20 + - op_name: "按键-切换角色" + agent_name: "琉音" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 3.0 \ No newline at end of file + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\347\234\237\346\226\227-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\234\237\346\226\227-\347\273\210\347\273\223\346\212\200.sample.yml" index 6896d7a1c5..4455b1ed9c 100644 --- "a/config/auto_battle_operation/\347\234\237\346\226\227-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\347\234\237\346\226\227-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "真斗" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 2 + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\347\256\200-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\347\256\200-\347\273\210\347\273\223\346\212\200.sample.yml" index ad01aea75d..9daf3e8f6c 100644 --- "a/config/auto_battle_operation/\347\256\200-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\347\256\200-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -6,13 +6,16 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 4.4 - - op_name: "设置状态" - state: "自定义-动作不打断" - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "简" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 14 - op_name: "等待秒数" - seconds: 1.4 + seconds: 1 - op_name: "设置状态" state: "自定义-异常-物理" add: 193 diff --git "a/config/auto_battle_operation/\350\200\200\345\230\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\350\200\200\345\230\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" index 1f2ad7f016..c47404ccca 100644 --- "a/config/auto_battle_operation/\350\200\200\345\230\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\350\200\200\345\230\211\351\237\263-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -1,7 +1,9 @@ -# 根据动画实测,动画持续2秒 +# 耀嘉音切人有问题要频繁切 operations: - op_name: "设置状态" state: "自定义-耀嘉音-唱歌" - op_name: "按键-终结技" post_delay: 0.1 - repeat: 3 \ No newline at end of file + repeat: 3 + - op_name: "按键-切换角色" + agent_name: "耀嘉音" diff --git "a/config/auto_battle_operation/\350\210\254\345\262\263-\347\273\210\347\273\223\346\212\200-\346\222\274\345\244\251\345\212\250\345\234\260.sample.yml" "b/config/auto_battle_operation/\350\210\254\345\262\263-\347\273\210\347\273\223\346\212\200-\346\222\274\345\244\251\345\212\250\345\234\260.sample.yml" index f37dcc6019..a03cc1fd40 100644 --- "a/config/auto_battle_operation/\350\210\254\345\262\263-\347\273\210\347\273\223\346\212\200-\346\222\274\345\244\251\345\212\250\345\234\260.sample.yml" +++ "b/config/auto_battle_operation/\350\210\254\345\262\263-\347\273\210\347\273\223\346\212\200-\346\222\274\345\244\251\345\212\250\345\234\260.sample.yml" @@ -10,8 +10,13 @@ operations: seconds: 6 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "般岳" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 30 - op_name: "等待秒数" - seconds: 5 + seconds: 1 - op_name: "清除状态" state_list: ["自定义-动作不打断"] diff --git "a/config/auto_battle_operation/\350\211\276\350\216\262-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\350\211\276\350\216\262-\347\273\210\347\273\223\346\212\200.sample.yml" index 71ebb4065f..4892db2ded 100644 --- "a/config/auto_battle_operation/\350\211\276\350\216\262-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\350\211\276\350\216\262-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -9,5 +9,10 @@ operations: - op_name: "按键-终结技" post_delay: 0.1 repeat: 20 + - op_name: "按键-切换角色" + agent_name: "艾莲" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 - op_name: "等待秒数" - seconds: 2.5 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\350\213\215\350\247\222-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\350\213\215\350\247\222-\347\273\210\347\273\223\346\212\200.sample.yml" index bea280f926..42550fd1dc 100644 --- "a/config/auto_battle_operation/\350\213\215\350\247\222-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\350\213\215\350\247\222-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -5,13 +5,18 @@ operations: - op_name: "设置状态" state: "自定义-动作不打断" seconds: 6 - - op_name: "按键-终结技" - post_delay: 0.1 - repeat: 10 - op_name: "设置状态" state: "自定义-苍角-展旗" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "苍角" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 30 - op_name: "等待秒数" - seconds: 5 + seconds: 1 - op_name: "清除状态" state: "自定义-动作不打断" - op_name: "等待秒数" diff --git "a/config/auto_battle_operation/\350\216\261\345\215\241\346\201\251-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\350\216\261\345\215\241\346\201\251-\347\273\210\347\273\223\346\212\200.sample.yml" index 5a44c39d36..50dac1854a 100644 --- "a/config/auto_battle_operation/\350\216\261\345\215\241\346\201\251-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\350\216\261\345\215\241\346\201\251-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 4.5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "莱卡恩" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 - op_name: "等待秒数" - seconds: 3.5 \ No newline at end of file + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\350\226\207\350\226\207\345\256\211-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\350\226\207\350\226\207\345\256\211-\347\273\210\347\273\223\346\212\200.sample.yml" index 526dd8591d..7a252e7d4c 100644 --- "a/config/auto_battle_operation/\350\226\207\350\226\207\345\256\211-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\350\226\207\350\226\207\345\256\211-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,7 +8,11 @@ operations: seconds: 5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "薇薇安" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 2.0 - - operation_template: "薇薇安-5A悬落" + seconds: 1 \ No newline at end of file diff --git "a/config/auto_battle_operation/\350\277\236\346\220\272-\345\217\226\346\266\210.sample.yml" "b/config/auto_battle_operation/\350\277\236\346\220\272-\345\217\226\346\266\210.sample.yml" index 0b13db4637..64ed888215 100644 --- "a/config/auto_battle_operation/\350\277\236\346\220\272-\345\217\226\346\266\210.sample.yml" +++ "b/config/auto_battle_operation/\350\277\236\346\220\272-\345\217\226\346\266\210.sample.yml" @@ -5,6 +5,15 @@ operations: state: "自定义-失衡时间" - op_name: "按键-连携技-取消-按下" - op_name: "等待秒数" - seconds: 0.4 + seconds: 0.1 + - op_name: "按键-连携技-取消-松开" + - op_name: "等待秒数" + seconds: 0.1 + - op_name: "按键-连携技-取消-按下" + - op_name: "等待秒数" + seconds: 0.1 + - op_name: "按键-连携技-取消-按下" + - op_name: "等待秒数" + seconds: 0.1 - op_name: "设置状态" state: "自定义-连携取消" diff --git "a/config/auto_battle_operation/\351\200\232\347\224\250-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\351\200\232\347\224\250-\347\273\210\347\273\223\346\212\200.sample.yml" index 5a44c39d36..342ad5ef90 100644 --- "a/config/auto_battle_operation/\351\200\232\347\224\250-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\351\200\232\347\224\250-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,9 @@ operations: seconds: 4.5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 10 + repeat: 20 + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 - op_name: "等待秒数" - seconds: 3.5 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\351\233\205-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\351\233\205-\347\273\210\347\273\223\346\212\200.sample.yml" index 2a5e0a19ee..223d7ccb69 100644 --- "a/config/auto_battle_operation/\351\233\205-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\351\233\205-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -1,6 +1,5 @@ -# 根据动画实测 +# 根据动画实测,动画持续4.5秒 operations: - # 终结技实测动作无敌时间为4.5秒 - op_name: "设置状态" state: "自定义-失衡时间" seconds_add: -1 @@ -10,9 +9,14 @@ operations: - op_name: "按键-终结技" post_delay: 0.1 repeat: 20 + - op_name: "按键-切换角色" + agent_name: "雅" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 # 一次终结技可以打出3090点烈霜 - op_name: "设置状态" state: "自定义-异常-烈霜" add: 309 - op_name: "等待秒数" - seconds: 2.5 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_operation/\351\233\250\346\236\234-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\351\233\250\346\236\234-\347\273\210\347\273\223\346\212\200.sample.yml" index 2b45da1451..fb89441d38 100644 --- "a/config/auto_battle_operation/\351\233\250\346\236\234-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\351\233\250\346\236\234-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -9,7 +9,12 @@ operations: seconds_add: -1 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "雨果" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 2 - - operation_template: "雨果-普通攻击A4" \ No newline at end of file + seconds: 1 + - operation_template: "雨果-普通攻击A4" diff --git "a/config/auto_battle_operation/\351\233\266\345\217\267\345\256\211\346\257\224-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\351\233\266\345\217\267\345\256\211\346\257\224-\347\273\210\347\273\223\346\212\200.sample.yml" index 84354fe7e9..7bebd471f9 100644 --- "a/config/auto_battle_operation/\351\233\266\345\217\267\345\256\211\346\257\224-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\351\233\266\345\217\267\345\256\211\346\257\224-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -11,7 +11,12 @@ operations: add: 990 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 25 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "零号安比" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 20 - op_name: "等待秒数" - seconds: 2.4 + seconds: 1 - operation_template: "零号安比-特殊技突进" \ No newline at end of file diff --git "a/config/auto_battle_operation/\351\235\222\350\241\243-\347\273\210\347\273\223\346\212\200.sample.yml" "b/config/auto_battle_operation/\351\235\222\350\241\243-\347\273\210\347\273\223\346\212\200.sample.yml" index e9a6250f77..3de96f68d2 100644 --- "a/config/auto_battle_operation/\351\235\222\350\241\243-\347\273\210\347\273\223\346\212\200.sample.yml" +++ "b/config/auto_battle_operation/\351\235\222\350\241\243-\347\273\210\347\273\223\346\212\200.sample.yml" @@ -8,6 +8,11 @@ operations: seconds: 4.5 - op_name: "按键-终结技" post_delay: 0.1 - repeat: 30 + repeat: 20 + - op_name: "按键-切换角色" + agent_name: "青衣" + - op_name: "按键-终结技" + post_delay: 0.1 + repeat: 15 - op_name: "等待秒数" - seconds: 1.5 \ No newline at end of file + seconds: 1 diff --git "a/config/auto_battle_state_handler/\345\217\266\347\236\254\345\205\211-\345\244\261\350\241\241\345\206\263\347\255\226.sample.yml" "b/config/auto_battle_state_handler/\345\217\266\347\236\254\345\205\211-\345\244\261\350\241\241\345\206\263\347\255\226.sample.yml" index dbe354ca81..ba0aeb2bf8 100644 --- "a/config/auto_battle_state_handler/\345\217\266\347\236\254\345\205\211-\345\244\261\350\241\241\345\206\263\347\255\226.sample.yml" +++ "b/config/auto_battle_state_handler/\345\217\266\347\236\254\345\205\211-\345\244\261\350\241\241\345\206\263\347\255\226.sample.yml" @@ -19,32 +19,12 @@ handlers: # 前台是叶瞬光 - states: "[前台-叶瞬光]" - debug_name: "叶瞬光连携决策" - sub_handlers: - # 优先点照 - - states: "[连携技-1-照]" - debug_name: "叶瞬光点照-左" - operations: - - operation_template: "连携-左" - - op_name: "设置状态" - state: "自定义-失衡时间" - - states: "[连携技-2-照]" - debug_name: "叶瞬光点照-右" - operations: - - operation_template: "连携-右" - - op_name: "设置状态" - state: "自定义-失衡时间" - - # 其次点支援 - - states: "[连携技-1-支援]" - debug_name: "叶瞬光点支援-左" - operations: - - operation_template: "连携-左" - - op_name: "设置状态" - state: "自定义-失衡时间" - - states: "[连携技-2-支援]" - debug_name: "叶瞬光点支援-右" - operations: - - operation_template: "连携-右" - - op_name: "设置状态" - state: "自定义-失衡时间" \ No newline at end of file + debug_name: "跳过连携" + operations: + - op_name: "设置状态" + state: "自定义-连携跳过" + - op_name: "设置状态" + state: "自定义-失衡时间" + - operation_template: "连携-取消" + - op_name: "清除状态" + state: "按键可用-连携技" \ No newline at end of file diff --git "a/config/auto_battle_state_handler/\350\275\256\346\215\242-\347\264\247\346\200\245-\345\205\250\350\247\222\350\211\262.sample.yml" "b/config/auto_battle_state_handler/\350\275\256\346\215\242-\347\264\247\346\200\245-\345\205\250\350\247\222\350\211\262.sample.yml" index 8ebe519531..c5005df661 100644 --- "a/config/auto_battle_state_handler/\350\275\256\346\215\242-\347\264\247\346\200\245-\345\205\250\350\247\222\350\211\262.sample.yml" +++ "b/config/auto_battle_state_handler/\350\275\256\346\215\242-\347\264\247\346\200\245-\345\205\250\350\247\222\350\211\262.sample.yml" @@ -233,9 +233,9 @@ handlers: agent_name: "千夏" - operation_template: "通用-终结技" - # 星见雅 有6豆就切过去 - - states: "[雅-落霜]{6, 6} & ![前台-雅] & ![前台-支援]" - debug_name: "雅满豆切入" + # 星见雅 能量和落霜判断 + - states: "([雅-能量]{80, 999} | ([雅-能量]{40, 999} & [雅-落霜]{2, 6})) & ![前台-雅] & ![前台-支援]" + debug_name: "雅能量豆数切入" operations: - op_name: "按键-切换角色" agent_name: "雅" diff --git "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\205\250\350\247\222\350\211\262.sample.yml" "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\205\250\350\247\222\350\211\262.sample.yml" index d290590f45..b25dbaddf5 100644 --- "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\205\250\350\247\222\350\211\262.sample.yml" +++ "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\205\250\350\247\222\350\211\262.sample.yml" @@ -49,6 +49,7 @@ handlers: - state_template: "速切模板-叶瞬光" - state_template: "速切模板-照" - state_template: "速切模板-千夏" + - state_template: "速切模板-爱芮" # 下面是缺省值,如果有上面没有的角色会走下面,一般情况下最好不要有,否则可能会因为识别不到角色而进行错误的动作 diff --git "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\217\266\347\236\254\345\205\211.sample.yml" "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\217\266\347\236\254\345\205\211.sample.yml" index 0581032463..2a4f367e49 100644 --- "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\217\266\347\236\254\345\205\211.sample.yml" +++ "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\345\217\266\347\236\254\345\205\211.sample.yml" @@ -136,11 +136,10 @@ handlers: - operation_template: "叶瞬光-普通攻击" - states: "[叶瞬光-明心境]{1,120}" - interrupt_states: "[叶瞬光-明心境]{0,0}" - debug_name: "非明心境-强化特殊攻击" + debug_name: "明心境-期间输出" sub_handlers: - states: "[叶瞬光-能量]{117,120}" - interrupt_states: "[叶瞬光-明心境]{100,120}" + interrupt_states: "[叶瞬光-青溟剑势-白]{0,4} & [叶瞬光-明心境]{90,110}" debug_name: "等待变身就绪" operations: - op_name: "设置状态" @@ -148,29 +147,48 @@ handlers: seconds: 15 - op_name: "按键-特殊攻击-松开" - op_name: "按键-普通攻击-松开" - - op_name: "等待秒数" - seconds: 0.7 + - op_name: "按键-特殊攻击" + post_delay: 0.1 + repeat: 20 - - states: "[叶瞬光-青溟剑势-白]{0,0} & [叶瞬光-明心境]{0,100} | [叶瞬光-明心境]{0,20}" - interrupt_states: "[叶瞬光-青溟剑势-白]{1,6} & ![叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{0,5}" + - states: "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{90,110}" + interrupt_states: "[叶瞬光-青溟剑势-白]{4,4}" operations: - - op_name: "设置状态" - state: "自定义-叶瞬光-收刀" - - operation_template: "叶瞬光-乱砍" + - op_name: "按键-特殊攻击" + post_delay: 0.1 + repeat: 20 - - states: "[叶瞬光-青溟剑势-白]{6,6} & [叶瞬光-明心境]{100,120} | [自定义-叶瞬光-在天]" - interrupt_states: "[叶瞬光-明心境]{0,20} | [叶瞬光-明心境]{115,120}" + - states: "[叶瞬光-青溟剑势-白]{4,4}" + interrupt_states: "[叶瞬光-青溟剑势-白]{3,3}" operations: - - op_name: "清除状态" - state: "自定义-叶瞬光-在天" - - op_name: "设置状态" - state: "自定义-无视闪光" - seconds: 15 - - operation_template: "叶瞬光-eaeeeaE" + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 20 - - states: "[叶瞬光-青溟剑势-白]{1,6} | [自定义-叶瞬光-在地]" - interrupt_states: "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-青溟剑势-白]{6,6} | [叶瞬光-明心境]{0,20}" + - states: "[叶瞬光-青溟剑势-白]{3,3}" + interrupt_states: "[叶瞬光-青溟剑势-白]{1,1}" operations: - - op_name: "清除状态" - state: "自定义-叶瞬光-在地" - - operation_template: "叶瞬光-普通攻击" + - op_name: "按键-特殊攻击" + post_delay: 0.1 + repeat: 20 + + - states: "[叶瞬光-青溟剑势-白]{1,1}" + interrupt_states: "[叶瞬光-青溟剑势-白]{0,0}" + operations: + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 20 + + - states: "[叶瞬光-青溟剑势-白]{2,2} | [叶瞬光-青溟剑势-白]{5,5}" + interrupt_states: "[叶瞬光-青溟剑势-白]{0,0}" + operations: + - op_name: "按键-普通攻击" + post_delay: 0.1 + repeat: 20 + + - states: "[叶瞬光-青溟剑势-白]{0,0} | [叶瞬光-明心境]{10,20}" + interrupt_states: "[叶瞬光-明心境]{0,0}" + operations: + - op_name: "设置状态" + state: "自定义-叶瞬光-收刀" + - operation_template: "叶瞬光-乱砍" diff --git "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\202\240\347\234\237.sample.yml" "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\202\240\347\234\237.sample.yml" index a68d7d253e..386efc53f9 100644 --- "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\202\240\347\234\237.sample.yml" +++ "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\202\240\347\234\237.sample.yml" @@ -98,70 +98,11 @@ handlers: - op_name: "设置状态" state: "自定义-合轴时间" - # 非失衡时刻 - - # 后台如果没有击破 - - states: "![后台-1-击破] & ![后台-2-击破]" - debug_name: "非击破队" - sub_handlers: - # 异常队,无脑输出 - - states: "[后台-1-异常] | [后台-2-异常]" - debug_name: "异常队" - sub_handlers: - - states: "[悠真-终结技可用]" - sub_handlers: - - states: "[悠真-特殊技可用]" - operations: - - operation_template: "悠真-强化特殊技" - - operation_template: "悠真-终结技" - - operation_template: "悠真-闪A四刀" - - states: "" - operations: - - operation_template: "悠真-终结技" - - states: "[悠真-特殊技可用]" - operations: - - operation_template: "悠真-强化特殊技" - - operation_template: "悠真-闪A四刀" - # 没能量了再打打 - - states: "" - debug_name: "清空电壶离场" - operations: - - operation_template: "悠真-射箭四刀" # 清空电壶再下场 - - op_name: "设置状态" - state: "自定义-合轴时间" - - # 非异常队 - - states: "" - debug_name: "非异常队" - sub_handlers: - - states: "[悠真-终结技可用]" - operations: - - operation_template: "悠真-终结技" - - states: "[悠真-特殊技可用]" - operations: - - operation_template: "悠真-强化特殊技" - - operation_template: "悠真-闪A三刀" - - states: "" - debug_name: "清空电壶离场" - operations: - - operation_template: "悠真-射箭三刀" # 清空电壶再下场 - - op_name: "设置状态" - state: "自定义-合轴时间" - - operation_template: "悠真-普通攻击" - - - states: "![后台-1-击破] & ![后台-2-击破]" - debug_name: "击破队" - sub_handlers: - # 后台如果有击破,不管什么队友都留能量 - - states: "[悠真-能量]{110, 120}" - operations: - - operation_template: "悠真-强化特殊技" - - operation_template: "悠真-闪A四刀" + - states: "[悠真-特殊技可用]" + operations: + - operation_template: "悠真-强化特殊技速接普攻" - - states: "" - debug_name: "清空电壶离场" - operations: - - operation_template: "悠真-射箭三刀" # 清空电壶再下场 - - op_name: "设置状态" - state: "自定义-合轴时间" - - operation_template: "悠真-普通攻击" + - states: "" + debug_name: "无脑EA" + operations: + - operation_template: "悠真-自动EA" diff --git "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\237\217\345\246\256\346\200\235.sample.yml" "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\237\217\345\246\256\346\200\235.sample.yml" index 5a4b03c27e..eb77820893 100644 --- "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\237\217\345\246\256\346\200\235.sample.yml" +++ "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\346\237\217\345\246\256\346\200\235.sample.yml" @@ -11,7 +11,7 @@ handlers: debug_name: "黄光切人" operations: - operation_template: "柏妮思-格挡攻击" - - operation_template: "柏妮思-短按单双喷" + - operation_template: "柏妮思-直接长按特殊攻击" - states: "[自定义-红光闪避, 0, 1]" debug_name: "红光闪避" @@ -22,7 +22,7 @@ handlers: debug_name: "连携攻击" operations: - operation_template: "柏妮思-连携攻击" - - operation_template: "柏妮思-短按单双喷" + - operation_template: "柏妮思-直接长按特殊攻击" - states: "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" debug_name: "切人后等待" @@ -30,7 +30,7 @@ handlers: - states: "[按键可用-快速支援, 0, 0.5]" debug_name: "快速支援等待" operations: - - operation_template: "柏妮思-短按单双喷" + - operation_template: "柏妮思-直接长按特殊攻击" - states: "" debug_name: "短暂等待" operations: diff --git "a/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\347\210\261\350\212\256.sample.yml" "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\347\210\261\350\212\256.sample.yml" new file mode 100644 index 0000000000..2ce9431833 --- /dev/null +++ "b/config/auto_battle_state_handler/\351\200\237\345\210\207\346\250\241\346\235\277-\347\210\261\350\212\256.sample.yml" @@ -0,0 +1,94 @@ +template_name: "速切模板-爱芮" +handlers: + - states: "[前台-爱芮]" + interrupt_states: "[前台-能量, 0, 0.1] & ![前台-爱芮]" + sub_handlers: + - states: "[自定义-终结技被强制释放, 0, 1]" + operations: + - operation_template: "爱芮-终结技" + + # 黄光入场 + - states: "[自定义-黄光切人, 0, 1]" + operations: + - operation_template: "爱芮-支援攻击" + + # 红光 + - states: "[自定义-红光闪避, 0, 1]" + operations: + - operation_template: "爱芮-闪A" + + # 连携 + - states: "[自定义-连携换人, 0, 0.5]" + operations: + - operation_template: "爱芮-连携攻击" + + # 切人后等待 + - states: "([按键-切换角色-下一个, 0, 0.3]|[按键-切换角色-上一个, 0, 0.3])" + debug_name: "切人后等待" + sub_handlers: + - states: "[按键可用-快速支援, 0, 0.5]" + operations: + - op_name: "等待秒数" + seconds: 1.0 + - states: "" + operations: + - op_name: "等待秒数" + seconds: 0.3 + + # 失衡期间:开大 → 0~2点特殊技,7点以上长按,其他普攻 + - states: "[自定义-失衡时间, -5, 15]" + sub_handlers: + - states: "[爱芮-终结技可用]" + debug_name: "失衡期终结技" + operations: + - operation_template: "爱芮-终结技" + + - states: "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + debug_name: "失衡期0~2点开特殊技" + operations: + - operation_template: "爱芮-强化特殊技" + + - states: "[爱芮-应援能量]{6, 8}" + interrupt_states: "[爱芮-应援能量]{0, 1}" + debug_name: "失衡期7点以上长按" + operations: + - operation_template: "爱芮-长按攻击" + + - states: "" + debug_name: "失衡期普攻" + operations: + - operation_template: "爱芮-普通攻击" + + # 非失衡期间 + - states: "" + sub_handlers: + # 无击破时直接开大,有击破时等失衡 + - states: "[爱芮-终结技可用] & ![后台-1-击破] & ![后台-2-击破]" + debug_name: "无击破直接开大" + operations: + - operation_template: "爱芮-终结技" + + # 7点以上长按 + - states: "[爱芮-应援能量]{6, 8}" + interrupt_states: "[爱芮-应援能量]{0, 1}" + debug_name: "7点以上长按" + operations: + - operation_template: "爱芮-长按攻击" + + # 能量满120,防止溢出,必须放特殊技 + - states: "[爱芮-能量]{120, 120} & [爱芮-特殊技可用]" + debug_name: "能量满120放特殊技" + operations: + - operation_template: "爱芮-强化特殊技" + + # 0~2点特殊技可用就放(不管有没有击破) + - states: "[爱芮-应援能量]{0, 2} & [爱芮-特殊技可用]" + debug_name: "0~2点放特殊技" + operations: + - operation_template: "爱芮-强化特殊技" + + # 默认普攻 + - states: "" + debug_name: "普攻" + operations: + - operation_template: "爱芮-普通攻击" \ No newline at end of file diff --git a/config/project.yml b/config/project.yml index 79c2c71100..2ecab4c54b 100644 --- a/config/project.yml +++ b/config/project.yml @@ -6,6 +6,7 @@ github_ssh_repository: "git@github.com:OneDragon-Anything/ZenlessZoneZero-OneDra gitee_https_repository: "https://gitee.com/OneDragon-Anything/ZenlessZoneZero-OneDragon.git" gitee_ssh_repository: "git@gitee.com:OneDragon-Anything/ZenlessZoneZero-OneDragon.git" project_git_branch: "main" +manifest_path: "deploy/module_manifest.py" screen_standard_width: 1920 screen_standard_height: 1080 pip_source: "https://pypi.tuna.tsinghua.edu.cn/simple" @@ -13,4 +14,4 @@ notice_url: "https://one-dragon.com/notice/zzz/notice.json" qq_link: "https://pd.qq.com/g/onedrag00n" quick_start_link: "http://one-dragon.com/zzz/zh/quickstart.html" home_page_link: "https://one-dragon.com/zzz/zh/home.html" -doc_link: "https://docs.qq.com/doc/p/7add96a4600d363b75d2df83bb2635a7c6a969b5" \ No newline at end of file +doc_link: "https://docs.qq.com/doc/p/7add96a4600d363b75d2df83bb2635a7c6a969b5" diff --git a/config/redemption_codes.sample.yml b/config/redemption_codes.sample.yml index 414a60f221..0db1a5935d 100644 --- a/config/redemption_codes.sample.yml +++ b/config/redemption_codes.sample.yml @@ -1,12 +1,3 @@ -# 示例配置兑换码 -# 此文件会被Git追踪,可由开发者维护 -# 用户自定义的兑换码请保存到 redemption_codes.yml(不会被Git追踪) -# -# 格式: -# codes: -# 兑换码: 过期时间 -# 过期时间格式: YYYYMMDD 长期有效就填 20990101 -# -# 示例兑换码(开发者可在此添加官方发布的兑换码) codes: ZZZ888: 20990101 + NANGONG0324: 20260315 diff --git a/deploy/OneDragon-RuntimeLauncher.spec b/deploy/OneDragon-RuntimeLauncher.spec new file mode 100644 index 0000000000..a77e1b274b --- /dev/null +++ b/deploy/OneDragon-RuntimeLauncher.spec @@ -0,0 +1,108 @@ +# -*- mode: python ; coding: utf-8 -*- + +import importlib.util +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +from PyInstaller.utils.hooks import collect_submodules + +if TYPE_CHECKING: + from PyInstaller.building.api import COLLECT, EXE, PYZ + from PyInstaller.building.build_main import Analysis + +# 将源码目录添加到 sys.path,以便 collect_submodules 能找到模块 +REPO_ROOT = Path.cwd().parent +SRC_DIR = REPO_ROOT / "src" +if str(SRC_DIR) not in sys.path: + sys.path.insert(0, str(SRC_DIR)) + +# 保留的模块树 +# NOTE: 修改此列表新增不同顶层包前缀时,需同步更新 hook_path_inject.py 中的 __path__ 扩展 +KEEP_TREES = [ + "one_dragon.launcher", + "one_dragon.version", +] + +# 导入 generate_module_manifest 模块以生成 module_manifest.py 并获取源码包列表 +GEN_PATH = Path.cwd() / "generate_module_manifest.py" +spec = importlib.util.spec_from_file_location("generate_module_manifest", str(GEN_PATH)) +if spec is None or spec.loader is None: + raise FileNotFoundError(f"无法加载模块: {GEN_PATH}") +generate_module_manifest = importlib.util.module_from_spec(spec) +spec.loader.exec_module(generate_module_manifest) + +# 这里顺便生成了 module_manifest.py +src_packages = generate_module_manifest.main() + +# 收集所有源码包的所有子模块 +all_src_modules = set() +for package in src_packages: + all_src_modules.update(collect_submodules(package)) + +# 收集需要保留的模块:KEEP_TREES 及其所有父包和子模块 +keep_modules = set() + +for tree_path in KEEP_TREES: + # 添加路径本身 + keep_modules.add(tree_path) + + # 添加所有父包(one_dragon.launcher → one_dragon) + parts = tree_path.split(".") + for i in range(1, len(parts)): + keep_modules.add(".".join(parts[:i])) + + # 添加所有子模块 + keep_modules.update(m for m in all_src_modules if m.startswith(tree_path + ".")) + +# 排除所有不在保留列表中的模块 +excludes = sorted(all_src_modules - keep_modules) + + +a = Analysis( + ['..\\src\\zzz_od\\win_exe\\runtime_launcher.py', 'module_manifest.py'], + pathex=[], + binaries=[], + datas=[ + ('module_manifest.py', '.'), + ('../config/project.yml', 'config'), + ], + hiddenimports=['_cffi_backend'], + hookspath=[], + hooksconfig={}, + runtime_hooks=['hook_path_inject.py'], + excludes=excludes, + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='OneDragon-RuntimeLauncher', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + uac_admin=False, + icon=['..\\assets\\ui\\logo.ico'], + contents_directory='.runtime', +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='OneDragon-RuntimeLauncher', +) diff --git a/deploy/build_full.bat b/deploy/build_full.bat index 6e15d534e5..e06255f9b0 100644 --- a/deploy/build_full.bat +++ b/deploy/build_full.bat @@ -4,8 +4,9 @@ chcp 65001 2>&1 cd %~dp0 rem Build exe -uv run pyinstaller "OneDragon Installer.spec" -uv run pyinstaller "OneDragon Launcher.spec" +uv run pyinstaller --noconfirm --clean "OneDragon-Installer.spec" +uv run pyinstaller --noconfirm --clean "OneDragon-Launcher.spec" +uv run pyinstaller --noconfirm --clean "OneDragon-RuntimeLauncher.spec" set "DIST_DIR=%~dp0dist" set "TARGET_DIR=%DIST_DIR%\ZenlessZoneZero-OneDragon" @@ -13,8 +14,15 @@ if not exist "%TARGET_DIR%" ( mkdir "%TARGET_DIR%" ) -copy "%DIST_DIR%\OneDragon Installer.exe" "%TARGET_DIR%" -copy "%DIST_DIR%\OneDragon Launcher.exe" "%TARGET_DIR%" +copy "%DIST_DIR%\OneDragon-Installer.exe" "%TARGET_DIR%" +copy "%DIST_DIR%\OneDragon-Launcher.exe" "%TARGET_DIR%" + +rem 集成启动器: exe + .runtime 目录 +copy "%DIST_DIR%\OneDragon-RuntimeLauncher\OneDragon-RuntimeLauncher.exe" "%TARGET_DIR%" +xcopy /E /I /Y "%DIST_DIR%\OneDragon-RuntimeLauncher\.runtime" "%TARGET_DIR%\.runtime\" + +rem Copy source code for RuntimeLauncher +xcopy /E /I /Y "..\src" "%DIST_DIR%\OneDragon-RuntimeLauncher\src\" rem Copy additional resources from spec file copy "..\config\project.yml" "%TARGET_DIR%\config\" @@ -23,8 +31,10 @@ xcopy /E /I /Y "..\assets\ui" "%TARGET_DIR%\assets\ui\" copy "..\pyproject.toml" "%TARGET_DIR%\" copy "..\uv.toml" "%TARGET_DIR%\" -rem Make zip file +rem Make zip files powershell -Command "Compress-Archive -Path '%TARGET_DIR%' -DestinationPath '%DIST_DIR%\ZenlessZoneZero-OneDragon.zip' -Force" +powershell -Command "Compress-Archive -Path '%DIST_DIR%\OneDragon-Launcher.exe' -DestinationPath '%DIST_DIR%\ZenlessZoneZero-OneDragon-Launcher.zip' -Force" +powershell -Command "Compress-Archive -Path '%DIST_DIR%\OneDragon-RuntimeLauncher\OneDragon-RuntimeLauncher.exe','%DIST_DIR%\OneDragon-RuntimeLauncher\.runtime' -DestinationPath '%DIST_DIR%\ZenlessZoneZero-OneDragon-RuntimeLauncher.zip' -Force" echo Done -pause \ No newline at end of file +pause diff --git a/deploy/generate_module_manifest.py b/deploy/generate_module_manifest.py new file mode 100644 index 0000000000..478f66d3d5 --- /dev/null +++ b/deploy/generate_module_manifest.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +生成 PyInstaller 模块清单的脚本 +扫描源码目录中的所有导入,生成 module_manifest.py 用于 PyInstaller 依赖分析 +""" + +import ast +from pathlib import Path + +# 常量配置 +SEED_FILE_NAME = "module_manifest.py" # 生成的清单文件名 +SRC_DIR_NAME = "src" # 源码目录名 +DEPLOY_DIR_NAME = "deploy" # 部署目录名 + +# 项目路径 +DEPLOY_DIR = Path(__file__).parent # 当前脚本所在目录(deploy) +REPO_ROOT = DEPLOY_DIR.parent # 仓库根目录 +SRC_DIR = REPO_ROOT / SRC_DIR_NAME # 源码目录 +SEED_FILE = DEPLOY_DIR / SEED_FILE_NAME # seed 文件路径 + + +def get_src_roots(src_dir: Path) -> list[Path]: + """ + 获取需要扫描的源码目录 + 自动扫描 src/ 下所有一级子文件夹 + """ + if not src_dir.exists(): + print(f"[warn] Source directory does not exist: {src_dir}") + return [] + + src_roots = [] + for item in src_dir.iterdir(): + if item.is_dir(): + src_roots.append(item) + + return sorted(src_roots) + + +def get_local_package_names(src_roots: list[Path]) -> set[str]: + """ + 获取本地包名 + 基于实际扫描的目录 + """ + return {root.name for root in src_roots} + + +def get_top_package(name: str) -> str: + """获取模块的顶级包名""" + return name.split(".", 1)[0] if name else "" + + +def collect_imports_from_file(py: Path, repo_root: Path) -> tuple[set[str], dict[str, set[str]]]: + """ + 返回该文件中出现的导入信息 + + Returns: + (import_stmts, from_imports) + - import_stmts: 'import xxx' 语句的模块名集合 + - from_imports: {module: {name1, name2, ...}} 的字典 + """ + imports: set[str] = set() + from_imports: dict[str, set[str]] = {} + + try: + src = py.read_text(encoding="utf-8", errors="ignore") + tree = ast.parse(src, filename=str(py)) + except SyntaxError as e: + print(f"[warn] Syntax error {py.relative_to(repo_root)}: {e}") + return imports, from_imports + except Exception as e: + print(f"[warn] Parsing failed {py.relative_to(repo_root)}: {e}") + return imports, from_imports + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + if alias.name: + imports.add(alias.name) + elif isinstance(node, ast.ImportFrom): + # 跳过相对导入 + if not node.level and node.module: + module = node.module + if module not in from_imports: + from_imports[module] = set() + for alias in node.names: + if alias.name: + from_imports[module].add(alias.name) + + return imports, from_imports + + +def is_local_package(name: str, local_pkg_names: set[str]) -> bool: + """判断 name 是否属于本地包(需要排除)""" + if not name: + return False + # 检查顶级包名 + return get_top_package(name) in local_pkg_names + + +def scan_all_imports(src_roots: list[Path], repo_root: Path, local_pkg_names: set[str]) -> list[str]: + """扫描所有源码文件,收集第三方库和标准库导入""" + # 验证源码目录存在 + missing = [p for p in src_roots if not p.is_dir()] + if missing: + raise SystemExit(f"[ERROR] 源码目录不存在: {', '.join(map(str, missing))}") + + all_imports: set[str] = set() + all_from_imports: dict[str, set[str]] = {} + py_files = [] + + # 收集所有 Python 文件 + for root in src_roots: + py_files.extend(root.rglob("*.py")) + + print(f"Scanning {len(py_files)} Python files...") + + # 解析所有文件的导入 + for py in py_files: + imports, from_imports = collect_imports_from_file(py, repo_root) + all_imports |= imports + # 合并 from imports + for module, names in from_imports.items(): + if module not in all_from_imports: + all_from_imports[module] = set() + all_from_imports[module] |= names + + # 生成导入语句列表 + statements = [] + + # 处理 import 语句 + for module in sorted(all_imports): + if not is_local_package(module, local_pkg_names): + statements.append(f"import {module}") + + # 处理 from...import 语句(合并同一模块的导入) + for module in sorted(all_from_imports.keys()): + if module == "__future__": # 排除 __future__ + continue + if not is_local_package(module, local_pkg_names): + names = sorted(all_from_imports[module]) + statements.append(f"from {module} import {', '.join(names)}") + + print(f"Found {len(statements)} external dependencies") + return statements + + +def write_seed_script(seed_file: Path, mods: list[str], repo_root: Path) -> None: + """ + 生成 seed 脚本文件,用于 PyInstaller 依赖分析 + + Args: + seed_file: seed 文件路径 + mods: 完整的导入语句列表 + repo_root: 仓库根目录 + """ + if not mods: + print("[warn] No external dependencies found") + seed_file.write_text("# AUTO-GENERATED — DO NOT EDIT\npass\n", encoding="utf-8") + return + + # 生成导入语句 + content = "# AUTO-GENERATED — DO NOT EDIT\n" + content += "import sys\n" + content += "if not getattr(sys, 'frozen', False):\n" + for mod in mods: + content += f" {mod}\n" + + seed_file.write_text(content, encoding="utf-8") + print(f"Writing -> {seed_file.relative_to(repo_root)} ({len(mods)} imports)") + + +def main() -> set[str]: + """主函数,返回本地包名集合。""" + src_roots = get_src_roots(SRC_DIR) + + if not src_roots: + raise RuntimeError(f"No source directories found under {SRC_DIR}") + + print(f"Found {len(src_roots)} source packages: {', '.join(r.name for r in src_roots)}") + + local_pkg_names = get_local_package_names(src_roots) + + # 扫描并生成 + mods = scan_all_imports(src_roots, REPO_ROOT, local_pkg_names) + write_seed_script(SEED_FILE, mods, REPO_ROOT) + + print("Done!") + + return local_pkg_names + + +if __name__ == "__main__": + main() diff --git a/deploy/hook_path_inject.py b/deploy/hook_path_inject.py new file mode 100644 index 0000000000..c46e6b60c3 --- /dev/null +++ b/deploy/hook_path_inject.py @@ -0,0 +1,22 @@ +"""PyInstaller 运行时 Hook:路径注入 + +冻结的 bundle 只保留了启动器必需的最小模块集(one_dragon.launcher、one_dragon.version), +其余代码(业务逻辑、配置、工具等)均从磁盘上的 src/ 目录动态加载。 + +本 hook 在主脚本执行前运行,完成两件事: +1. 将 src/ 加入 sys.path,使 zzz_od、one_dragon_qt 等顶层包可被导入 +2. 将 src/one_dragon 追加到冻结 one_dragon 包的 __path__, + 使其能找到未冻结的子模块(envs、utils 等) +""" + +import sys +from pathlib import Path + +_src = Path(sys.executable).parent / "src" + +sys.path.insert(0, str(_src)) + +# NOTE: 此处的包名必须与 OneDragon-RuntimeLauncher.spec 中的 KEEP_TREES 顶层包一致。 +# 修改 KEEP_TREES 新增不同顶层包前缀时,需同步更新此处。 +import one_dragon +one_dragon.__path__.append(str(_src / "one_dragon")) diff --git a/deploy/module_manifest.py b/deploy/module_manifest.py new file mode 100644 index 0000000000..9d9a14156a --- /dev/null +++ b/deploy/module_manifest.py @@ -0,0 +1,129 @@ +# AUTO-GENERATED — DO NOT EDIT +import sys +if not getattr(sys, 'frozen', False): + import argparse + import ast + import atexit + import base64 + import builtins + import contextlib + import copy + import csv + import ctypes + import cv2 + import datetime + import difflib + import gettext + import glob + import hashlib + import hmac + import html + import importlib + import importlib.util + import inspect + import io + import json + import librosa + import locale + import logging + import math + import matplotlib.font_manager + import matplotlib.pyplot + import numpy + import onnxruntime + import os + import os.path + import platform + import polib + import pyautogui + import pyclipper + import pyuac + import pywintypes + import random + import re + import requests + import shutil + import signal + import smtplib + import socket + import soundcard + import string + import subprocess + import sys + import threading + import time + import traceback + import urllib.parse + import urllib.request + import uuid + import vgamepad + import warnings + import webbrowser + import win32api + import win32clipboard + import win32con + import win32gui + import win32ui + import winreg + import yaml + import zipfile + from PIL import Image, ImageDraw, ImageFont + from PySide6 import QtCore + from PySide6.QtCore import Property, QEasingCurve, QEvent, QEventLoop, QMimeData, QObject, QPoint, QPropertyAnimation, QRect, QRectF, QRegularExpression, QSize, QThread, QTimer, QUrl, Qt, Signal + from PySide6.QtGui import QBrush, QColor, QDesktopServices, QDrag, QDragEnterEvent, QDragLeaveEvent, QDragMoveEvent, QDropEvent, QFont, QFontMetrics, QIcon, QImage, QIntValidator, QKeyEvent, QMouseEvent, QPaintEvent, QPainter, QPainterPath, QPen, QPixmap, QResizeEvent, QShowEvent, QSyntaxHighlighter, QTextCharFormat, QWheelEvent, Qt + from PySide6.QtMultimedia import QMediaPlayer + from PySide6.QtMultimediaWidgets import QGraphicsVideoItem + from PySide6.QtWidgets import QAbstractButton, QAbstractItemView, QAbstractScrollArea, QApplication, QComboBox, QCompleter, QDialog, QFileDialog, QFrame, QGraphicsDropShadowEffect, QGraphicsOpacityEffect, QGraphicsScene, QGraphicsView, QGridLayout, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit, QListView, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QScrollArea, QSizePolicy, QSpacerItem, QSpinBox, QStackedWidget, QStyledItemDelegate, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget + from abc import ABC, abstractmethod + from collections import defaultdict, deque + from collections.abc import Callable + from colorama import Fore, Style, init + from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError + from contextlib import suppress + from ctypes import wintypes + from ctypes.wintypes import DWORD, HANDLE, RECT, SHORT, UINT, WCHAR, WORD + from cv2.typing import MatLike + from dataclasses import dataclass, field + from datetime import datetime, timedelta + from email.header import Header + from email.mime.image import MIMEImage + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + from email.utils import formataddr + from enum import Enum, IntEnum, StrEnum + from functools import cached_property, lru_cache, partial, wraps + from io import BytesIO + from logging import DEBUG + from logging.handlers import TimedRotatingFileHandler + from mss import mss + from mss.base import MSSBase + from packaging import version + from pathlib import Path + from pyautogui import screenshot + from pygetwindow import Win32Window + from pygit2 import Blob, Oid, Remote, Repository, Walker, discover_repository, init_repository, settings + from pygit2.enums import CheckoutStrategy, ConfigLevel, ResetMode, SortMode + from pynput import keyboard, mouse + from pynput.keyboard import Controller, Key + from qfluentwidgets import Action, BodyLabel, CaptionLabel, CardWidget, CheckBox, CheckableMenu, ColorDialog, ComboBox, Dialog, DisplayLabel, DoubleSpinBox, EditableComboBox, ExpandSettingCard, FlowLayout, FluentIcon, FluentIconBase, FluentStyleSheet, FluentThemeColor, FluentWindow, FlyoutViewBase, HorizontalFlipView, HyperlinkButton, HyperlinkCard, ImageLabel, IndeterminateProgressBar, IndicatorPosition, InfoBar, InfoBarIcon, InfoBarPosition, LargeTitleLabel, LineEdit, ListItemDelegate, ListWidget, MSFluentWindow, MaskDialogBase, MenuAnimationType, MessageBox, MessageBoxBase, NavigationBar, NavigationBarPushButton, NavigationItemPosition, PipsPager, PipsScrollButtonDisplayMode, Pivot, PixmapLabel, PlainTextEdit, PopupTeachingTip, PrimaryPushButton, ProgressBar, ProgressRing, PushButton, PushSettingCard, RoundMenu, ScrollArea, SettingCard, SettingCardGroup, SimpleCardWidget, SingleDirectionScrollArea, SpinBox, SplashScreen, SplitTitleBar, StrongBodyLabel, StyleSheetBase, SubtitleLabel, SwitchButton, TableWidget, TeachingTip, TeachingTipTailPosition, Theme, TitleLabel, ToolButton, ToolTip, ToolTipFilter, ToolTipPosition, TransparentPushButton, TransparentToolButton, VBoxLayout, isDarkTheme, qconfig, qrouter, setFont, setTheme, setThemeColor + from qfluentwidgets.common.animation import BackgroundAnimationWidget, FluentAnimation, FluentAnimationProperty, FluentAnimationType + from qfluentwidgets.common.overload import singledispatchmethod + from qfluentwidgets.components.navigation.pivot import PivotItem + from qfluentwidgets.components.settings.expand_setting_card import GroupSeparator + from qfluentwidgets.components.settings.setting_card import SettingIconWidget + from qfluentwidgets.components.widgets.frameless_window import FramelessWindow + from qfluentwidgets.window.stacked_widget import StackedWidget + from qframelesswindow import FramelessDialog + from queue import Empty, Queue + from random import random + from scipy import signal + from scipy.signal import butter, correlate, filtfilt + from scipy.spatial import KDTree + from shapely.geometry import Polygon + from sklearn.preprocessing import scale + from soundcard.mediafoundation import SoundcardRuntimeWarning + from threading import Event, Lock + from types import ModuleType + from typing import Any, Callable, ClassVar, Dict, IO, Iterable, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast + from urllib.parse import urlencode + from yaml import CSafeLoader, SafeLoader diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..8cae6d1982 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,57 @@ +# 文档 + +项目文档目录。 + +## 目录结构 + +``` +docs/ +├── README.md # 本文件 +├── develop/ # 开发文档 +│ ├── project-overview.md # 项目概述和快速开始 +│ ├── development-guide.md # 开发指引总览 +│ ├── coding-standards.md # 开发规范 +│ ├── development-environments.md # 开发环境配置 +│ ├── testing-standards.md # 测试规范 +│ ├── documentation-standards.md # 文档编写规范 +│ ├── one_dragon/ # one_dragon 框架文档 +│ └── zzz/ # zzz 游戏开发文档 +└── user/ # 用户文档 + ├── README.md # 用户文档导航 + ├── app/ # 应用使用说明 + │ └── battle-assistant.md + ├── installation.md + ├── quick-start.md + └── faq.md +``` + +## 文档分类 + +### 开发文档 (`develop/`) + +面向项目开发者,包含架构说明、开发规范和测试指南。 + +- **[项目概述](develop/project-overview.md)** - 项目介绍、技术栈、快速开始 +- **[开发指引](develop/development-guide.md)** - 开发文档总览 +- **[开发规范](develop/coding-standards.md)** - Python 代码规范和编码约定 +- **[测试规范](develop/testing-standards.md)** - 测试编写和执行规范 +- **[文档规范](develop/documentation-standards.md)** - 文档编写规范 +- **one_dragon** - 一条龙框架架构和模块文档 +- **zzz** - ZZZ 游戏特定开发文档 + +### 用户文档 (`user/`) + +面向最终用户,提供安装、配置和使用指南。 + +- **[用户文档导航](user/README.md)** - 用户文档首页 +- **[应用文档](user/app/)** - 各应用的使用说明 +- **[安装指南](user/installation.md)** - 安装步骤 +- **[快速开始](user/quick-start.md)** - 快速上手 +- **[常见问题](user/faq.md)** - 常见问题解答 + +### 运维文档 (`ops/`) + +面向运维人员,包含版本更新和运维相关文档。 + +- **[版本更新](ops/版本更新.md)** - 版本更新说明 +- **[锄大地](ops/锄大地.md)** - 大地图录制和路线说明 diff --git a/docs/develop/README.md b/docs/develop/README.md index 8f6c87fb91..778f39599c 100644 --- a/docs/develop/README.md +++ b/docs/develop/README.md @@ -63,4 +63,60 @@ uv run --env-file .env pytest zzz-od-test/ ### 推荐MCP -- [context7](https://github.com/upstash/context7) - 查询各个库的文档。 \ No newline at end of file +- [context7](https://github.com/upstash/context7) - 查询各个库的文档。 + +## 3.打包 + +进入 deploy 文件夹,运行 `build_full.bat` 可一键打包所有组件。 + +### 3.1.安装器 + +生成spec文件并打包 + +```shell +uv run pyinstaller --onefile --windowed --uac-admin --icon="../assets/ui/installer_logo.ico" --add-data "../config/project.yml;config" ../src/zzz_od/gui/zzz_installer.py -n "OneDragon-Installer" +``` + +使用spec打包 + +```shell +uv run pyinstaller --noconfirm --clean "OneDragon-Installer.spec" +``` + +### 3.2.启动器(原始) + +使用spec打包,会自动生成种子文件 + +```shell +uv run pyinstaller --noconfirm --clean "OneDragon-Launcher.spec" +``` + +### 3.3.集成启动器(RuntimeLauncher) + +> 详细设计文档见 [runtime-launcher.md](runtime-launcher.md) + +#### 架构概述 + +集成启动器将 Python 运行时直接嵌入发行包,无需用户单独安装 Python / uv。 + +- **PyInstaller 目录模式**:`contents_directory='.runtime'`,运行时文件放在 `.runtime/` 子目录 +- **最小打包**:仅打包 `one_dragon.launcher`、`one_dragon.version` 模块和二进制依赖(pygit2 等) +- **源码加载**:借助 `hook_path_inject.py` 运行时钩子,将 `/src` 注入 `sys.path`,其余模块从 `src/` 目录加载 +- **自动更新**:首次运行时自动克隆代码仓库;后续运行时根据 `auto_update` 配置自动拉取最新代码 +- **Manifest 兼容性检查**:`module_manifest.py` 记录打包时的外部依赖清单,更新代码后如新增依赖不在清单中,提示用户更新启动器 + +#### 打包命令 + +```shell +uv run pyinstaller --noconfirm --clean "OneDragon-RuntimeLauncher.spec" +``` + +#### 关键文件 + +| 文件 | 说明 | +|------|------| +| `deploy/OneDragon-RuntimeLauncher.spec` | PyInstaller 打包配置 | +| `deploy/hook_path_inject.py` | 运行时钩子,注入 `src/` 到 `sys.path` | +| `deploy/generate_module_manifest.py` | 生成外部依赖清单 | +| `deploy/module_manifest.py` | 自动生成的依赖清单(打包时生成) | +| `src/zzz_od/win_exe/runtime_launcher.py` | 集成启动器入口 | diff --git a/docs/develop/background_mode_design.md b/docs/develop/background_mode_design.md new file mode 100644 index 0000000000..58835adc1f --- /dev/null +++ b/docs/develop/background_mode_design.md @@ -0,0 +1,292 @@ +# 后台模式设计文档 + +## 概述 + +后台模式允许在游戏窗口不在前台时进行自动化操作。根据验证测试,确定了以下可行方案: + +| 场景 | 输入方式 | 技术方案 | 验证结果 | +|------|---------|---------|---------| +| UI/菜单 (非锁鼠标) | 鼠标点击 | `WM_ACTIVATE` + `PostMessage` | ✅ 后台可用 | +| gamepad_key 场景 | 手柄按键替代 | `vgamepad` 手柄按键映射 | ✅ 后台可用 | +| 大世界/战斗 (锁视角) | 手柄按键 | `vgamepad` (ViGEm 虚拟手柄) | ✅ 后台可用 | +| 键盘输入 | — | 标准 API 均不可行 | ❌ | + +## 技术原理 + +### 1. 后台鼠标点击 — WM_ACTIVATE + PostMessage + +游戏在失去焦点后会忽略 `WM_LBUTTONDOWN`/`WM_LBUTTONUP` 消息。但通过先发送 +`WM_ACTIVATE(WA_ACTIVE)` 欺骗游戏认为自己处于激活状态,后续的点击消息即可被处理。 + +**消息序列:** + +``` +1. SendMessage(hwnd, WM_ACTIVATE, WA_ACTIVE, 0) -- 假装激活 +2. sleep(10ms) +3. PostMessage(hwnd, WM_MOUSEMOVE, 0, MAKELPARAM(x, y)) -- 先移动 +4. sleep(10ms) +5. PostMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(x, y)) +6. sleep(20ms) +7. PostMessage(hwnd, WM_LBUTTONUP, 0, MAKELPARAM(x, y)) +``` + +**适用范围:** +- ✅ 所有 UI 界面、菜单、对话框 +- ✅ 非锁鼠标的交互 +- ❌ 战斗中的视角控制 + +### 1.1 gamepad_key 场景 — 手柄按键替代 + +在大世界、战斗画面等锁鼠标场景,前台模式需要 ALT 键解锁光标才能点击 UI。 +后台模式下 ALT 无法可靠传递(`keybd_event` 方案已验证失败),改用手柄按键替代: + +**方案:** 为 `ScreenArea` 添加可选 `gamepad_key` 字段,存储 `GamepadActionEnum` 动作名。 +当后台模式 `click()` 检测到 `gamepad_key` 不为空时, +通过 `gamepad_action_keys` 字典解析为实际按键列表,调用 `tap()` 或 `tap_combo()` 替代鼠标点击。 + +**数据模型:** +```python +class ScreenArea: + gamepad_key: str | None = None # GamepadActionEnum 动作名 + # 示例: 'menu'、'compendium'、'map' +``` + +**动作名解析:** +```python +# GamepadActionEnum 定义的动作: +# menu, map, minimap, compendium, function_menu +# +# 用户在设置界面配置每个动作对应的实际手柄按键 (list[str]): +# 'compendium' → ['xbox_lb', 'xbox_a'] (LB+A) +# 'menu' → ['xbox_start'] (Start) +``` + +**调用链:** +``` +find_and_click_area / round_by_click_area + └─ click(pos, gamepad_key=area.gamepad_key) + ├─ 后台 + gamepad_key → _gamepad_click(gamepad_key) + │ ├─ gamepad_action_keys['menu'] → ['xbox_start'] + │ ├─ 单键 → btn_controller.tap() + │ └─ 组合 → btn_controller.tap_combo() + ├─ 后台 → _background_click(pos, press_time) + └─ 前台 → pyautogui + ALT +``` + +**YAML 格式 (screen_info,可选字段):** +```yaml +- area_name: 菜单 + gamepad_key: menu # GamepadActionEnum 动作名 + pc_alt: true + pc_rect: [...] + +- area_name: 快捷手册 + gamepad_key: compendium # 实际按键由配置决定 + pc_alt: true + pc_rect: [...] +``` + +**涉及的 screen_info:** +- `battle.yml` — 战斗画面(结算界面 menu) +- `normal_world.yml` — 大世界(menu / map / compendium / minimap / function_menu) +- `normal_world_basic.yml` / `normal_world_investigation.yml` — 大世界变体 +- `lost_void_normal_world.yml` / `lost_void_choose_common.yml` — 迷失之地 + +### 2. 后台手柄输入 — vgamepad (ViGEm) + +ViGEm (Virtual Gamepad Emulation Bus) 在内核驱动层创建虚拟 Xbox 360 控制器。 +游戏通过 XInput API 轮询手柄状态,该 API 直接从驱动读取,不依赖窗口焦点。 + +**工作原理:** +``` +vgamepad (Python) → ViGEmBus 驱动 → 虚拟 Xbox 控制器 → XInput API → 游戏读取 +``` + +**适用范围:** +- ✅ 战斗操作(普攻、闪避、切人、大招) +- ✅ 大世界移动 +- ✅ 所有手柄支持的交互 +- ❌ 需要精确像素点击的 UI 操作 + +**依赖:** `vgamepad` Python 包 + ViGEmBus 驱动 + +**组合键支持:** +`PcButtonController.tap_combo(keys)` 在基类实现,逐个 `press(key, None)` 按住 → sleep → 逐个 `release(key)`。 + +### 3. 键盘输入 — 无后台方案 + +| 方案 | 结果 | 原因 | +|------|------|------| +| PostMessage WM_KEYDOWN/UP | ❌ | 游戏不从消息队列读键盘 | +| SendMessage WM_KEYDOWN/UP | ❌ | 同上 | +| WM_CHAR | ❌ | 同上 | +| SetKeyboardState | ❌ | 仅影响线程局部状态 | +| keybd_event / SendInput | 前台✅ 后台❌ | 硬件合成只投递到前台窗口 | +| WM_ACTIVATE + keybd_event | ❌ | keybd_event 无视消息级激活 | + +游戏键盘走 `GetAsyncKeyState` / `Raw Input`,读取硬件状态,无法通过标准 API 后台伪造。 + +## 配置架构 + +### 按键属性动态生成 + +`GameConfig` 通过 `_with_key_properties` 装饰器自动生成所有按键属性,避免手写数百行 property。 + +**两类默认值字典:** +```python +# 1. 战斗按键(存储为 str) +_KEY_DEFAULTS: dict[str, dict[str, str]] = { + 'key': {'interact': 'f', 'dodge': 'shift', ...}, # 键盘 (15 个) + 'xbox_key': {'interact': 'xbox_a', 'dodge': 'xbox_a', ...}, # Xbox + 'ds4_key': {'interact': 'ds4_cross', ...}, # DS4 +} + +# 2. 后台模式动作键(存储为 list[str]) +_ACTION_KEY_DEFAULTS: dict[str, dict[str, list[str]]] = { + 'xbox_action': {'menu': ['xbox_start'], 'compendium': ['xbox_lb', 'xbox_a'], ...}, + 'ds4_action': {'menu': ['ds4_options'], 'compendium': ['ds4_l1', 'ds4_cross'], ...}, +} +``` + +**装饰器逻辑:** 遍历两个字典 × 对应枚举,为每个组合创建 `property(getter, setter)`: +```python +# 生成 key_interact, key_dodge, ..., xbox_key_interact, ..., ds4_key_interact, ... +for prefix, defaults in _KEY_DEFAULTS.items(): + for action in GameKeyAction: # 15 个动作 + setattr(cls, f'{prefix}_{action.value.value}', property(...)) + +# 生成 xbox_action_menu, xbox_action_compendium, ..., ds4_action_menu, ... +for prefix, defaults in _ACTION_KEY_DEFAULTS.items(): + for action in GamepadActionEnum: # 5 个动作 + setattr(cls, f'{prefix}_{action.value.value}', property(...)) +``` + +### 旧版配置迁移 + +`_LEGACY_GAMEPAD_KEYS` 映射旧数字索引格式(如 `xbox_0`→`xbox_a`、`ds4_6`→`ds4_l1`)。 +`__init__` 中调用 `_migrate_legacy_gamepad_keys()` 一次性迁移 `xbox_key_*` / `ds4_key_*`。 + +### UI — GamepadActionKeyCard + +`GamepadActionKeyCard` 继承 `MultiPushSettingCard`,包含两个 `ComboBox`(修饰键 + 按钮键), +以 `list[str]` 形式读写配置。用于设置界面中后台模式动作键的配置。 + +## 架构设计 + +### 双模式控制器 + +``` +PcControllerBase +├── background_mode: bool → 全局后台模式开关 +├── click(gamepad_key=...) → 分发到下列私有方法 +│ ├── _gamepad_click() → 后台 + gamepad_key 时手柄按键替代 +│ ├── _background_click() → 后台 SetCursorPos + PostMessage 点击 +│ └── _foreground_click() → 前台 pyautogui + ALT 点击 +├── drag_to() → 分发到下列私有方法 +│ ├── _background_drag() → 后台 SetCursorPos + PostMessage 拖拽 +│ └── _foreground_drag() → 前台 pyautogui 拖拽 +├── btn_controller → keyboard_controller / xbox_controller +├── btn_tap / btn_press → 按键前 _ensure_gamepad_mode() +├── btn_release → 释放前 _ensure_gamepad_mode()(防御性) +├── _send_activate() → 发送 WM_ACTIVATE 激活消息 +└── 模式切换 + ├── enable_background_mode() → PostMessage 点击 + Xbox 手柄 + └── enable_foreground_mode() → pyautogui 点击 + 键盘 +``` +### ZPcController.init_before_context_run — 配置刷新 + +每次 `start_running()` 时自动刷新所有快照配置,保证设置界面的修改在下次运行生效: +```python +def init_before_context_run(self) -> bool: + # 根据配置启用后台/前台模式,内部调用 enable_*() 刷新 action_keys + enable_background_mode() / enable_foreground_mode() + # 刷新其他快照值 + self.turn_dx = game_config.turn_dx + self.gamepad_turn_speed = game_config.gamepad_turn_speed + self.mouse_flash_duration = game_config.mouse_flash_duration +``` +### ZPcController — 按键映射与动作方法 + +**按键映射:** `ZPcController` 使用 `action_keys: dict[str, str]` 存储当前控制方式的 +所有按键映射(如 `{'dodge': 'shift', 'interact': 'f', ...}`),由 `GameConfig.get_action_keys()` +根据控制方式返回。初始化时始终加载键盘键名,`enable_xbox()`/`enable_ds4()` 切换时同步更新。 + +**动作方法:** 15 个动作方法(`dodge()`、`normal_attack()` 等)均为一行委托: +```python +def dodge(self, press=False, press_time=None, release=False): + self._action_btn(self.action_keys['dodge'], press, press_time, release) +``` + +**`_action_btn(key, press, press_time, release)`:** 通用按键动作分发——按下/释放/点按。 + +### 转向 — _gamepad_turn + +`turn_by_distance()` 和 `turn_vertical_by_distance()` 统一委托给 `move_mouse_relative()`: +``` +move_mouse_relative(dx, dy) +├── 后台 → _gamepad_turn(dx, dy) +│ ├── gamepad_turn_speed <= 0 时跳过(速度下限保护) +│ ├── _ensure_gamepad_mode()(自包含,不依赖调用方) +│ └── 右摇杆满偏转 duration = max_d / gamepad_turn_speed +└── 前台 → _ensure_mouse_mode() + mouse_event +``` + +### _ensure_mouse_mode — 闪切键鼠模式 + +后台模式下,前台转向需要短暂切换到键鼠模式,流程: +1. `SetForegroundWindow(hwnd)` 切到前台(失败时用 ALT 技巧重试) +2. `sleep(mouse_flash_duration)` — 可配置,默认 0.05s +3. `mouse_event(MOVE)` 触发 Raw Input 切换键鼠 +4. `sleep(mouse_flash_duration)` +5. `SetForegroundWindow(prev_hwnd)` 切回原窗口 + +`mouse_flash_duration` 在设置界面可调整(后台模式风琴组内),过小可能导致切换失败。 + +### 场景切换策略 + +| 操作类型 | 后台模式 | 前台模式 | +|---------|---------|---------| +| 菜单点击 | WM_ACTIVATE + PostMessage | pyautogui | +| 锁鼠标场景 (pc_alt) | vgamepad 手柄按键 | pynput ALT + pyautogui | +| 战斗按键 | vgamepad Xbox 手柄 | keyboard (pynput) | +| 移动控制 | vgamepad 左摇杆 | keyboard WASD | +| 文本输入 | 不支持 | keyboard.type() | +| 截图 | 不受影响(已有后台截图) | 同 | + +### API + +**`PcControllerBase` 核心方法:** +- `background_mode: bool` — 全局后台模式标志 +- `click(pos, press_time, pc_alt, gamepad_key)` — 统一入口,根据模式分发 +- `drag_to(start, end, duration)` — 统一拖拽入口,根据模式分发 +- `_foreground_click(pos, press_time, pc_alt)` — 前台 pyautogui 点击,可选 ALT 解锁光标 +- `_foreground_drag(start, end, duration)` — 前台 pyautogui 拖拽 +- `_gamepad_click(gamepad_key)` — 后台 + gamepad_key 手柄替代,通过 `gamepad_action_keys` 解析动作名为实际按键 +- `_background_click(pos, press_time)` — 后台 SetCursorPos + PostMessage 点击 +- `_background_drag(start, end, duration)` — 后台 SetCursorPos + PostMessage 拖拽 +- `_send_activate()` — 发送 `WM_ACTIVATE(WA_ACTIVE)` 到游戏窗口 +- `enable_background_mode()` — 开启后台模式(PostMessage + Xbox) +- `enable_foreground_mode()` — 开启前台模式(pyautogui + 键盘) + +**`PcButtonController` 基类:** +- `tap(key)` — 单键按下释放 +- `tap_combo(keys: list[str])` — 组合键:逐个 press → sleep → 逐个 release +- `press(key, press_time)` — 按下(press_time=None 不松开) +- `release(key)` — 释放 + +**`ScreenArea` 数据模型:** +- `gamepad_key: str | None` — `GamepadActionEnum` 动作名(如 `'menu'`、`'compendium'`),默认不写入 YAML + +**`GameConfig` 核心方法:** +- `get_action_keys(control_method) -> dict[str, str]` — 返回 `{action_name: key_value}` + - `control_method`: `'keyboard'` / `'xbox'` / `'ds4'`(默认读 `config.control_method`) + - 前缀推导:`'key' if control_method == 'keyboard' else f'{control_method}_key'` + - 例如 `get_action_keys('keyboard')` → `{'dodge': 'shift', 'interact': 'f', ...}` +- `get_gamepad_action_keys(gamepad_type) -> dict[str, list[str]]` — 返回 `{action_name: [key, ...]}` + - `gamepad_type`: `'xbox'` / `'ds4'`(默认读 `config.background_gamepad_type`) + - 例如 `get_gamepad_action_keys('xbox')` → `{'menu': ['xbox_start'], 'compendium': ['xbox_lb', 'xbox_a'], ...}` + +## 前置条件 + +1. **ViGEmBus 驱动**:需要安装(安装器可集成) +2. **vgamepad Python 包**:`uv pip install vgamepad` diff --git a/docs/develop/development-environments.md b/docs/develop/development-environments.md new file mode 100644 index 0000000000..e21e39ee3c --- /dev/null +++ b/docs/develop/development-environments.md @@ -0,0 +1,19 @@ +# 开发环境说明 + +## 标准开发环境 + +- Python 环境:使用 `uv sync --group dev` 进行环境搭建 + +## Claude Code 配置 + +### 插件安装 + +在项目根目录运行以下命令安装插件: + +```bash +claude plugin marketplace add OneDragon-Anything/OneDragon-CC-Plugins +``` + +#### 所需插件列表 + +- uv-pyright-lsp - 使用uv run运行的LSP server diff --git a/docs/develop/development-guide.md b/docs/develop/development-guide.md new file mode 100644 index 0000000000..26dda471a9 --- /dev/null +++ b/docs/develop/development-guide.md @@ -0,0 +1,51 @@ +# 开发指引 + +本文档提供开发过程中的指引和参考文档。 + +## 快速链接 + +- [项目概述](project-overview.md) - 项目介绍和技术栈 +- [开发规范](standards/coding-standards.md) - Python 代码规范和编码约定 +- [开发环境](development-environments.md) - 开发环境配置说明 +- [测试规范](standards/testing-standards.md) - 测试编写和执行规范 +- [文档规范](standards/documentation-standards.md) - 文档编写规范和最佳实践 +- [开发流程规范](standards/workflow-standard.md) - 开发流程和 Submodule 使用规范 + +## 架构文档 + +### one_dragon 框架 +- [框架架构](one_dragon/one_dragon_architecture.md) - 基础框架架构 +- [模块文档](one_dragon/modules/) - 各模块详细文档 + - [操作系统](one_dragon/modules/operation.md) - 基于节点的状态机 + - [屏幕系统](one_dragon/modules/screen_system.md) - 画面识别和导航 + - [应用层](one_dragon/modules/application.md) - 高层业务逻辑 + - [条件操作](one_dragon/modules/conditional_operation.md) - 条件操作 + - [推送系统](one_dragon/modules/push.md) - 推送通知 + - [数据库操作](one_dragon/modules/sqlite.md) - SQLite 数据库 +- [应用插件系统](one_dragon/application_plugin_system.md) - 插件架构 +- [CV 管道架构](one_dragon/cv_pipeline_architecture.md) - 计算机视觉处理 +- [目标状态模块开发指南](one_dragon/target_state_module_developer_guide.md) +- [初始化流程](one_dragon/initialization.md) - 应用启动和初始化 + +### zzz 游戏 +- [应用开发](zzz/application/) - ZZZ 游戏应用开发文档 +- [MCP 服务器](zzz/mcp/) - MCP 服务器开发文档 +- [Web 服务](zzz/web/) - Web 服务架构文档 + +## 开始开发 + +### 环境配置 + +详见 [项目概述](project-overview.md) 的快速开始部分。 + +**关键环境变量**: +- `PYTHONPATH=src` - 必须设置,指定 Python 源码路径 +- 使用 `.env` 文件管理,运行时加 `--env-file .env` + +### 运行测试 + +测试相关内容见 [测试规范](standards/testing-standards.md)。 + +### 开发流程 + +详见 [开发流程规范](standards/workflow-standard.md)。 diff --git a/docs/develop/one_dragon/application_plugin_system.md b/docs/develop/one_dragon/application_plugin_system.md new file mode 100644 index 0000000000..4c32534cf6 --- /dev/null +++ b/docs/develop/one_dragon/application_plugin_system.md @@ -0,0 +1,419 @@ +# 应用插件系统设计文档 + +## 概述 + +应用插件系统提供了一种动态发现和注册应用的机制,允许在运行时刷新应用列表,而不需要在代码中硬编码应用注册逻辑。系统还支持通过 GUI 界面导入第三方插件。 + +## 插件来源 + +系统支持两种插件来源: + +| 来源 | 目录位置 | 加载方式 | 相对导入 | 导入主程序 | +|------|----------|----------|----------|------------| +| **BUILTIN** | `src/zzz_od/application/` | `spec_from_file_location` | 需完整路径 | ✅ | +| **THIRD_PARTY** | `plugins/` (项目根目录) | `spec_from_file_location` | ✅ 支持 | ✅ | + +### 第三方插件特性 + +第三方插件位于项目根目录的 `plugins/` 目录下,使用 `spec_from_file_location` 加载: + +```python +# plugins/my_plugin/utils.py +def helper(): + return "hello" + +# plugins/my_plugin/my_plugin_factory.py +from .utils import helper # ✅ 相对导入可用 +from one_dragon.xxx import yyy # ✅ 可以导入主程序模块 +from zzz_od.context.zzz_context import ZContext # ✅ 可以导入主程序模块 +``` + +## 核心组件 + +### ApplicationFactoryManager + +应用工厂管理器,负责扫描和加载应用工厂。 + +**文件位置**: `src/one_dragon/base/operation/application/application_factory_manager.py` + +**主要功能**: +- `discover_factories()`: 扫描所有插件目录,发现并加载应用工厂 +- `plugin_infos`: 获取所有已加载的插件信息 +- `third_party_plugins`: 获取第三方插件列表 +- `scan_failures`: 获取最近一次扫描的失败记录 + +> 注意:刷新/注册应用的完整流程由 `OneDragonContext.refresh_application_registration()` 编排, +> `ApplicationFactoryManager` 仅负责工厂的发现和加载。 + +**内部方法**: +- `_scan_directory()`: 扫描单个目录,检测冲突,加载工厂 +- `_load_factory_from_file()`: 从文件加载工厂类,解析模块路径 +- `_import_module_from_file()`: 统一的模块导入,自动加载所有中间包 +- `_find_factory_in_module()`: 在模块中查找并实例化工厂类(每个模块最多一个) +- `_register_plugin_metadata()`: 验证 const 字段、检测重复 APP_ID、注册到 `_plugin_infos` +- `_get_unload_prefix()`: 确定热更新时需要卸载的模块前缀 + +### PluginInfo + +插件信息数据模型,存储插件的元数据。 + +**文件位置**: `src/one_dragon/base/operation/application/plugin_info.py` + +**属性**: +- `app_id`, `app_name`, `default_group`: 核心信息 +- `author`, `homepage`, `version`, `description`: 插件元数据 +- `plugin_dir`: 插件目录路径 +- `source`: 插件来源(BUILTIN/THIRD_PARTY) +- `is_third_party`: 是否为第三方插件 + +### ApplicationFactory + +应用工厂基类。 + +**文件位置**: `src/one_dragon/base/operation/application/application_factory.py` + +**构造参数**: +- `app_id`: 应用唯一标识符 +- `app_name`: 显示名称 +- `default_group`: 是否属于默认应用组(一条龙运行列表),默认为 `True` +- `need_notify`: 是否需要通知,默认为 `False` + +## 目录结构 + +### 完整目录结构 + +``` +project_root/ +├── src/ +│ └── zzz_od/ +│ └── application/ # 内置应用(BUILTIN,版本控制) +│ ├── my_app/ +│ │ ├── my_app_const.py +│ │ └── my_app_factory.py +│ └── battle_assistant/ # 支持嵌套子目录 +│ ├── auto_battle/ +│ │ └── auto_battle_app_factory.py +│ └── dodge_assistant/ +│ └── dodge_assistant_factory.py +└── plugins/ # 第三方插件(THIRD_PARTY,gitignore) + └── my_plugin/ + ├── __init__.py # 推荐添加(无 __init__.py 时自动创建命名空间包) + ├── my_plugin_const.py + ├── my_plugin_factory.py + ├── my_plugin.py + └── sub/ # 支持嵌套子目录 + ├── __init__.py + ├── sub_feature_const.py + ├── sub_feature_factory.py + └── helpers/ + └── utils.py +``` + +### 第三方插件目录 + +第三方插件位于项目根目录的 `plugins/` 目录下,该目录被 `.gitignore` 忽略: + +``` +plugins/ +├── README.md # 说明文档 +└── my_plugin/ # 用户安装的插件 + ├── __init__.py + ├── my_plugin_const.py + ├── my_plugin_factory.py + └── my_plugin.py +``` + +## 使用方式 + +### 1. 创建新应用(内置) + +#### 步骤 1: 创建 const 文件 + +在应用目录下创建 `xxx_const.py` 文件,定义应用的基本信息: + +```python +# src/zzz_od/application/my_app/my_app_const.py + +APP_ID = "my_app" +APP_NAME = "我的应用" +DEFAULT_GROUP = True # 是否属于默认应用组(一条龙列表) +NEED_NOTIFY = True # 是否需要通知 +``` + +> 字段顺序和必须字段请参考 `app_const_schema.py`。 + +**说明**: +- `DEFAULT_GROUP = True`: 应用会出现在一条龙运行列表中 +- `DEFAULT_GROUP = False`: 应用不会出现在一条龙列表中(如工具类应用) +- `NEED_NOTIFY = True`: 应用支持发送通知 + +#### 步骤 2: 创建工厂类 + +在应用目录下创建 `xxx_factory.py` 文件: + +```python +# src/zzz_od/application/my_app/my_app_factory.py + +from one_dragon.base.operation.application.application_factory import ApplicationFactory +from zzz_od.application.my_app import my_app_const +from zzz_od.application.my_app.my_app import MyApp + +class MyAppFactory(ApplicationFactory): + + def __init__(self, ctx): + ApplicationFactory.__init__( + self, + app_id=my_app_const.APP_ID, + app_name=my_app_const.APP_NAME, + default_group=my_app_const.DEFAULT_GROUP, + need_notify=my_app_const.NEED_NOTIFY, + ) + self.ctx = ctx + + def create_application(self, instance_idx, group_id): + return MyApp(self.ctx) +``` + +**重要**: +- 文件名必须以 `_factory.py` 结尾 +- 必须在构造函数中传递 `default_group` 和 `need_notify` 参数(从 const 模块读取) + +### 2. 创建第三方插件 + +第三方插件放在项目根目录的 `plugins/` 目录下,支持相对导入和导入主程序模块: + +``` +plugins/ +└── my_plugin/ + ├── __init__.py # 推荐添加 + ├── my_plugin_const.py + ├── my_plugin_factory.py + ├── my_plugin.py + └── helpers/ + ├── __init__.py + └── utils.py +``` + +```python +# my_plugin/my_plugin_const.py + +APP_ID = "my_plugin" +APP_NAME = "我的插件" +DEFAULT_GROUP = True +NEED_NOTIFY = True + +# 插件元数据(可选,用于 GUI 显示) +PLUGIN_AUTHOR = "作者名" +PLUGIN_HOMEPAGE = "https://github.com/author/my_plugin" +PLUGIN_VERSION = "1.0.0" +PLUGIN_DESCRIPTION = "插件功能描述" +``` + +```python +# my_plugin/my_plugin_factory.py +from one_dragon.base.operation.application.application_factory import ApplicationFactory +from zzz_od.context.zzz_context import ZContext # ✅ 可以导入主程序模块 + +from .helpers.utils import calculate_damage # ✅ 相对导入可用 +from . import my_plugin_const # ✅ 相对导入 const +from .my_plugin import MyPlugin + + +class MyPluginFactory(ApplicationFactory): + def __init__(self, ctx: ZContext): + super().__init__( + app_id=my_plugin_const.APP_ID, + app_name=my_plugin_const.APP_NAME, + default_group=my_plugin_const.DEFAULT_GROUP, + need_notify=my_plugin_const.NEED_NOTIFY, + ) + self.ctx = ctx + + def create_application(self, instance_idx, group_id): + return MyPlugin(self.ctx) +``` + +**第三方插件优势**: +- ✅ 完整支持相对导入 (`from .xxx import yyy`) +- ✅ 可以导入主程序模块 (`from one_dragon.xxx`, `from zzz_od.xxx`) +- ✅ 更好的代码组织(可以有子目录) +- ✅ 独立于 src 目录,开发体验接近独立项目 + +详细的开发指南请参考 `plugins/README.md`。 + +### 3. 通过 GUI 导入插件 + +1. 打开设置 → 插件管理 +2. 点击"导入插件"按钮 +3. 选择一个或多个 `.zip` 格式的插件压缩包 +4. 插件会自动解压到 `plugins` 目录并注册 + +### 4. 运行时刷新应用 + +可以在运行时调用 `refresh_application_registration()` 方法刷新应用列表: + +```python +# 刷新应用注册 +ctx.refresh_application_registration() +``` + +这会: +1. 清空现有的应用注册 +2. 重新扫描插件目录(`application` 和 `plugins`) +3. 重新加载所有工厂模块(支持代码热更新) +4. 重新注册所有应用 +5. 更新默认应用组配置 + +## 应用分组 + +### 默认组应用 (default_group=True) + +- 会出现在"一条龙"运行列表中 +- 可以被用户排序和启用/禁用 +- 适用于:体力刷本、咖啡店、邮件等日常任务 + +### 非默认组应用 (default_group=False) + +- 不会出现在"一条龙"运行列表中 +- 作为独立工具使用 +- 适用于:自动战斗、闪避助手、截图工具等 + +## GUI 插件管理 + +### 插件管理界面 + +**文件位置**: `src/zzz_od/gui/view/setting/setting_plugin_interface.py` + +**功能**: +- 显示已安装的第三方插件列表 +- 导入插件(支持多选 zip 文件) +- 删除插件 +- 刷新插件列表 +- 打开插件目录 +- 跳转到插件主页 + +### 插件 zip 包结构 + +有效的插件 zip 包应包含以下结构: + +``` +my_plugin.zip +└── my_plugin/ + ├── __init__.py # 可选 + ├── my_plugin_const.py # 必须包含 APP_ID, APP_NAME, DEFAULT_GROUP, NEED_NOTIFY + ├── my_plugin_factory.py # 必须,工厂类 + └── my_plugin.py # 应用实现 +``` + +## 自定义插件目录 + +默认的插件目录通过 `application_plugin_dirs` 属性(`@cached_property`)自动计算。如果需要自定义,可以在子类中覆盖: + +```python +from functools import cached_property + +class MyContext(OneDragonContext): + + @cached_property + def application_plugin_dirs(self): + from pathlib import Path + from one_dragon.base.operation.application.plugin_info import PluginSource + return [ + (Path(__file__).parent.parent / 'application', PluginSource.BUILTIN), + (Path(__file__).parent.parent / 'plugins', PluginSource.THIRD_PARTY), + (Path(__file__).parent.parent / 'custom_apps', PluginSource.THIRD_PARTY), # 额外的插件目录 + ] +``` + +## 注意事项 + +1. **文件命名**: 工厂文件必须以 `_factory.py` 结尾 +2. **一模块一工厂**: 每个 `_factory.py` 文件中应只定义一个 `ApplicationFactory` 子类 +3. **const 必须字段**: 必须定义 `APP_ID`, `APP_NAME`, `DEFAULT_GROUP`, `NEED_NOTIFY`(见 `app_const_schema.py`) +4. **字段顺序**: const 文件字段顺序统一为 `APP_ID → APP_NAME → DEFAULT_GROUP → NEED_NOTIFY` +5. **模块缓存**: 刷新应用时会重新加载模块,支持代码热更新 +6. **错误处理**: 工厂实例化失败时异常会被记录到 `scan_failures` 并跳过,不会影响其他插件 +7. **APP_ID 唯一性**: 重复的 APP_ID 会被 `_register_plugin_metadata` 检测并拒绝,后来者不加载 +8. **第三方插件**: 第三方插件目录被 gitignore,用户需要自行备份 +9. **插件元数据**: 建议填写 `PLUGIN_AUTHOR`、`PLUGIN_VERSION` 等元数据以便用户识别 +10. **相对导入**: 第三方插件完整支持相对导入,建议添加 `__init__.py` 文件 +11. **导入主程序**: 第三方插件可以直接 `from one_dragon.xxx` 或 `from zzz_od.xxx` 导入主程序模块 +12. **同目录冲突**: 同一目录下不允许多个 `_factory.py` 或 `_const.py` 文件,发现时整个目录被跳过 + +## 插件加载机制 + +所有插件统一使用 `importlib.util.spec_from_file_location()` 加载。 +模块名通过 `factory_file.relative_to(module_root)` 统一计算,BUILTIN 和 THIRD_PARTY 使用相同的逻辑。 + +### 模块根目录 (module_root) + +`_load_factory_from_file()` 在加载前确定模块名的起算目录,使用共享的 `find_src_dir()` 工具函数: + +- **BUILTIN**: 调用 `find_src_dir()` 反向查找路径中最后一个 `src` 目录作为 module_root +- **THIRD_PARTY**: 使用扫描根目录(如 `plugins/`)作为 module_root + +### 内置插件 (BUILTIN) + +模块名从 `src` 目录开始计算: + +``` +src/zzz_od/application/my_app/my_app_factory.py +→ module_root: src/ +→ 模块名: zzz_od.application.my_app.my_app_factory + +src/zzz_od/application/battle_assistant/auto_battle/auto_battle_app_factory.py +→ module_root: src/ +→ 模块名: zzz_od.application.battle_assistant.auto_battle.auto_battle_app_factory +``` + +### 第三方插件 (THIRD_PARTY) + +将 `plugins/` 目录加入 `sys.path`,模块名从 plugins 目录开始计算。 +**支持嵌套子目录**,中间包会自动加载或创建为命名空间包: + +``` +plugins/my_plugin/my_plugin_factory.py +→ module_root: plugins/ +→ 模块名: my_plugin.my_plugin_factory + +plugins/my_plugin/sub/sub_feature_factory.py +→ module_root: plugins/ +→ 模块名: my_plugin.sub.sub_feature_factory +→ 中间包: my_plugin(加载 __init__.py)、my_plugin.sub(加载 __init__.py 或创建命名空间包) +``` + +### 中间包加载 + +`_import_module_from_file()` 在加载工厂模块前,会确保所有中间包都已注册到 `sys.modules`: + +1. 如果中间目录有 `__init__.py`,使用 `spec_from_file_location` 加载 +2. 如果没有 `__init__.py`,创建命名空间包(设置 `__path__` 和 `__package__`) +3. 已在 `sys.modules` 中的包会被跳过 + +### 热更新卸载策略 + +`_get_unload_prefix()` 确定模块卸载范围: + +- **THIRD_PARTY**: 卸载整个插件包(如 `my_plugin` 及其所有子模块) +- **BUILTIN**: 仅卸载 factory 所在的父包(如 `zzz_od.application.my_app` 下的模块) + +```python +# 加载过程 +# 1. 解析 module_root +module_root = find_src_dir(factory_file) if source == BUILTIN else base_dir + +# 2. 统一计算模块名 +relative_path = factory_file.relative_to(module_root) +module_name = '.'.join(relative_path.parts[:-1] + [factory_file.stem]) + +# 3. 加载所有中间包 + 工厂模块 +module = _import_module_from_file(factory_file, module_name, module_root) +``` + +**导入主程序模块**: +- 由于程序运行时 `src/` 目录已在 `sys.path` 中,插件可以直接 `from one_dragon.xxx` 或 `from zzz_od.xxx` + +**sys.path 管理**: +- `plugins/` 目录仅添加一次到 sys.path +- 使用集合跟踪已添加的路径,避免重复 +- 路径会保留以支持插件运行时的模块导入 diff --git a/docs/develop/one_dragon/cv_pipeline_architecture.md b/docs/develop/one_dragon/cv_pipeline_architecture.md new file mode 100644 index 0000000000..296be475f7 --- /dev/null +++ b/docs/develop/one_dragon/cv_pipeline_architecture.md @@ -0,0 +1,160 @@ +# 架构指南:可动态配置的计算机视觉(CV)流水线 + +## 1. 摘要与设计目标 + +本文档旨在阐述项目当前计算机视觉(CV)核心模块的架构设计。该架构的核心是一个高度可扩展、可动态配置的**CV处理流水线(CV Pipeline)**。 + +设计该系统的主要目标是: + +* **解耦核心逻辑与UI**:将底层的CV算法与上层的图形界面(Qt)彻底分离,使得CV逻辑可以被独立测试、复用和演进。 +* **流程可视化与快速调试**:提供一个可视化的开发工具,允许开发者通过拖拽和配置,快速搭建、调试和验证图像处理流程。 +* **动态参数化**:允许在流水线的任何步骤中,动态地、精细化地调整CV算法(特别是OCR)的每一个参数,以应对不同场景,最大化识别准确率和性能。 +* **代码生成与复用**:将调试好的流水线流程一键生成为可直接在生产环境中调用的Python代码,极大提高开发效率。 + +## 2. 核心概念与组件 + +系统的核心由三个主要类构成,它们共同协作,构成了整个CV流水线的基础。 + +```mermaid +graph TD + subgraph "核心组件" + A[CvPipeline] -- "管理" --> B(CvStep); + B -- "操作" --> C{CvPipelineContext}; + A -- "持有" --> C; + end + + subgraph "说明" + D[`CvPipeline`:
流水线管理器
按序执行所有步骤] + E[`CvStep`:
原子处理步骤
例如 灰度化 OCR识别
独立的 可配置的模块] + F[`CvPipelineContext`:
执行上下文
在流水线中传递数据
包含图像和中间结果] + end + + style A fill:#D6EAF8,stroke:#3498DB,stroke-width:2px + style B fill:#D1F2EB,stroke:#1ABC9C,stroke-width:2px + style C fill:#FEF9E7,stroke:#F1C40F,stroke-width:2px + +``` + +### 2.1. `CvStep` (处理步骤) + +- **定义**:一个独立的、原子的图像处理操作单元。所有步骤都继承自 `one_dragon.base.cv_process.cv_step.CvStep`。 +- **可配置性**:通过重写 `get_params()` 方法,一个`CvStep`可以向UI暴露任意数量的参数。系统会根据定义的类型(如`bool`, `int`, `float`, `enum`)自动生成对应的UI控件(复选框、滑块、下拉菜单等),并附带标签和工具提示。 +- **执行逻辑**:每个步骤的核心逻辑在 `_execute()` 方法中实现,它接收 `CvPipelineContext` 作为输入,并对其进行修改。 + +### 2.2. `CvPipeline` (流水线) + +- **定义**:`CvStep`的有序集合。它负责创建`CvPipelineContext`,并按顺序依次调用每个步骤的`execute`方法,将上下文在步骤间传递。 + +### 2.3. `CvPipelineContext` (上下文) + +- **定义**:一个数据容器,贯穿整个流水线的生命周期。它携带了`display_image`(当前用于处理和显示的图像)、`mask_image`(二值化掩码)、`contours`(轮廓列表)以及`ocr_result`等关键数据。 + +## 3. 核心服务与UI逻辑 + +### 3.1. `CvService` (后端服务) + +- **定位**: `src/one_dragon/base/cv_process/cv_service.py` +- **职责**: + - 负责加载和保存在 `assets/image_analysis_pipelines/` 目录下的 `.yml` 流水线配置文件。 + - 作为在生产环境中**运行这些流水线的统一入口**。业务代码(如 `TargetStateChecker`)通过调用 `cv_service.run_pipeline('your_pipeline_name', image)` 来执行一个完整的CV任务。 + - 维护一个可用`CvStep`的注册表,供UI和加载器使用。 + +### 3.2. 图像分析工具 (前端逻辑) + +- **定位**: `src/one_dragon_qt/view/devtools/devtools_image_analysis_interface.py` (UI界面) 和 `src/one_dragon_qt/logic/image_analysis_logic.py` (UI逻辑) +- **职责**: + - 提供一个完整的图形化界面,允许开发者加载本地图片。 + - 通过组合、排序、配置不同的`CvStep`来搭建流水线。 + - 实时预览每一步CV处理的结果。 + - 将调试好的流水线保存为 `.yml` 文件,供`CvService`在生产环境中使用。 + +## 4. 开发者指南:创建、调试与集成CV流水线 + +### 4.1. 可视化开发工作流 (如何使用) + +1. **Step 1: 打开图像分析工具**: 在开发工具中,通过“打开图片”按钮加载一张用于调试的目标截图。 +2. **Step 2: 搭建与配置流水线**: 从界面左侧的“添加步骤”下拉框中选择您需要的CV步骤(如“HSV范围过滤”),它将被添加到流水线列表中。您可以通过拖拽或使用上下箭头调整步骤的执行顺序。 +3. **Step 3: 调试与分析结果**: 选中流水线中的任意一个步骤,其所有可配置参数会立即显示在下方。实时调整参数(例如,拖动二值化的阈值滑块),主界面上的图像会**即时更新**,直观地展示出参数变化带来的效果。同时,右下角的文本框会显示该步骤的日志输出(如找到的轮廓数量、OCR识别的文本和置信度)和性能耗时。 +4. **Step 4: 保存流水线**: 当流水线调试到最佳效果时,在顶部的输入框中为其命名,并点击“保存”或“另存为”按钮。您的流水线将被保存为一个 `.yml` 文件,存放在 `assets/image_analysis_pipelines/` 目录下。 +5. **Step 5: 在代码中调用**: 在您的业务代码中(例如 `TargetStateChecker`),通过 `cv_service.run_pipeline('your_pipeline_name', image)` 来调用刚刚保存的流水线,完成一次完整的CV识别。 + +### 4.2. 如何扩展:创建全新的`CvStep` + +本节将通过一个完整的示例,指导您如何创建一个名为**“提取红色通道”**的全新CV处理步骤。 + +* **Step 1: 继承`CvStep`基类** + * 在 `one_dragon/base/cv_process/cv_step.py` 中,添加以下类定义: + + ```python + class CvStepExtractRedChannel(CvStep): + def __init__(self): + super().__init__('提取红色通道') + ``` + +* **Step 2: 实现`_execute`方法** + * 在 `CvStepExtractRedChannel` 类中,添加核心处理逻辑。红色通道是RGB图像的第一个通道(索引为0)。 + + ```python + def _execute(self, ctx: CvPipelineContext) -> bool: + # 我们的流水线约定使用RGB图像 + # 红色通道是第一个通道 + red_channel = ctx.display_image[:, :, 0] + + # 创建一个与原图大小相同的黑色图像 + h, w = red_channel.shape + new_image = np.zeros((h, w, 3), dtype=np.uint8) + + # 将红色通道的数据赋给新图像的红色通道 + new_image[:, :, 0] = red_channel + + # 更新上下文中的图像用于显示 + ctx.display_image = new_image + + ctx.add_log_entry(f"已提取红色通道") + return True + ``` + +* **Step 3: 暴露参数 `get_params` (可选)** + * 这个简单的步骤不需要参数,所以我们可以让 `get_params` 返回一个空字典。 + + ```python + def get_params(self) -> dict: + return {} # 本步骤无需配置参数 + ``` + +* **Step 4: 在`CvService`中注册** + * 打开 `one_dragon/base/cv_process/cv_service.py`。 + * 首先,从 `cv_step` 模块中导入我们新创建的类。 + + ```python + from one_dragon.base.cv_process.cv_step import ( + ..., CvStepExtractRedChannel # <-- 新增 + ) + ``` + * 然后,在 `CvService` 的 `__init__` 方法中,将其添加到 `self.available_steps` 字典里。 + + ```python + self.available_steps: Dict[str, Type[CvStep]] = { + '提取红色通道': CvStepExtractRedChannel, # <-- 新增 + '按模板裁剪': CvStepCropByTemplate, + '灰度化': CvStepGrayscale, + # ... 其他步骤 + } + ``` + +* **Step 5: 重启与验证** + * 完成以上代码修改后,重新启动程序。 + * 打开“图像分析工具”,您现在应该可以在“添加步骤”的下拉菜单中找到并使用“提取红色通道”这个新功能了。 + +### 4.3. 关键设计解析 (最佳实践) + +* **统一的RGB颜色空间**: 这是保证所有CV流程正确工作的基石。 + * **约定**: `CvService`及其所有流水线都假定处理的是**RGB图像**。 + * **实践**: + * 从实时截图获取的图像,应确保为RGB格式。 + * 通过 `cv2.imread` 加载的离线图片,**必须**在加载后立即使用 `cv2.cvtColor(img, cv2.COLOR_BGR2RGB)` 进行转换,然后再传递给`CvService`。 + +* **OCR模块的完全参数化 (`CvStepOcr`)**: + * **背景**: `OnnxOcrMatcher` 具有大量影响性能和准确率的参数。 + * **实现**: `CvStepOcr` 步骤将这些参数暴露给了UI。当执行时,它会使用这些UI上的参数临时覆盖OCR引擎的全局设置。 + * **优势**: 这使得我们可以为特定场景(如识别截图中的倾斜文字)临时启用`use_angle_cls`,或在需要高性能时开启`use_gpu`,而无需修改任何代码。这些设置仅对当前流水线生效,不会污染全局OCR配置。 \ No newline at end of file diff --git a/docs/develop/one_dragon/target_state_module_developer_guide.md b/docs/develop/one_dragon/target_state_module_developer_guide.md new file mode 100644 index 0000000000..aa146f010c --- /dev/null +++ b/docs/develop/one_dragon/target_state_module_developer_guide.md @@ -0,0 +1,238 @@ +# 架构指南:敌对目标状态检测模块 (数据驱动版) + +## 1. 摘要与设计目标 + +### 1.1. 问题陈述 + +在《绝区零》等高速动作游戏中,为了实现高效、智能的自动化战斗,程序必须能够实时、准确地获取敌方目标的多个关键状态,例如: + +* **锁定状态**: 我方是否已锁定目标? +* **敌人类型**: 当前目标是普通小怪还是强敌? +* **失衡状态**: 强敌的失衡值还剩多少? +* **异常状态**: 目标是否处于燃烧、冰冻等状态下? + +这些状态的检测涉及到不同类型的计算机视觉(CV)技术,它们的计算开销和更新频率也各不相同。 + +### 1.2. 设计目标 + +本模块旨在构建一个**高性能、可扩展、数据驱动**的敌对目标状态检测系统。 + +* **高性能**: 将高频、快速的CV检测(如锁定)与低频、慢速的OCR检测(如失衡值)在执行上分离,避免慢速任务阻塞关键决策。 +* **可扩展性 (核心)**: **通过修改纯数据文件,而非Python逻辑代码,即可添加新的状态检测。** 这是本架构最重要的设计目标。 +* **动态频率**: 允许系统根据战斗上下文,动态调整检测频率,节约CPU资源。 +* **状态自动管理**: 状态不仅能被检测和更新,还能在消失时被自动清除,确保状态的准确性。 + +## 2. 核心概念与组件 (数据驱动架构) + +本模块的核心在于**数据驱动**和**职责分离**。所有的检测任务都由数据结构(`DetectionTask`, `TargetStateDef`)来定义,由两个核心类来执行。 + +```mermaid +graph TD + subgraph "外部调用方 - AutoBattleOperator" + A["run_all_checks"] + end + + subgraph "核心模块: AutoBattleTargetContext - 通用调度器" + A --> B{遍历所有DetectionTask}; + B -- "计时器到期" --> C{提交 checker.run_task}; + C -- "异步执行" --> D[后台线程池]; + C -- "同步执行" --> E{收集结果}; + end + + subgraph "纯函数模块: TargetStateChecker - 通用解码器" + F["run_task"] -- "调用" --> G["运行CV流水线"]; + G -- "CV结果" --> H{根据 task.state_definitions 解码}; + H -- "返回 state, result 元组" --> I{返回给Context}; + end + + subgraph "数据定义 - 配置" + J["DETECTION_TASKS: List[DetectionTask]"]; + K[DetectionTask] -- "包含" --> L["List[TargetStateDef]"]; + end + + subgraph "底层服务" + M[ThreadPoolExecutor]; + N[CvService]; + O[StateRecorder]; + end + + %% 流程关联 + B --> J; + D --> M; + C --> F; + F --> G; + G --> N; + E --> O; + I --> E; + + %% 样式 + style J fill:#FFF2CC,stroke:#D6B656 + style K fill:#FFF2CC,stroke:#D6B656 + style L fill:#FFF2CC,stroke:#D6B656 + style A fill:#D6EAF8,stroke:#3498DB + style B fill:#FEF9E7,stroke:#F1C40F + style C fill:#E8DAEF,stroke:#8E44AD + style I fill:#D1F2EB,stroke:#1ABC9C +``` + +### 2.1. 核心数据结构 (驱动核心) + +- **定位**: `src/zzz_od/game_data/target_state.py` + +#### `TargetStateDef` +定义了**一个**具体的状态如何被检测。 +- `state_name`: 状态的唯一名称,如 `目标-强敌`。 +- `check_way`: 使用哪种方法来解读CV结果,例如 `TargetCheckWay.CONTOUR_COUNT_IN_RANGE`。 +- `check_params`: `check_way` 所需的参数,例如 `{'min_count': 1}`。 +- `clear_on_miss`: **一个关键布尔值。如果为 `True`,当该状态未被检测到时,系统会主动清除它;如果为 `False`,则忽略。** + +#### `DetectionTask` +将共享同一个CV流水线的一组状态定义打包在一起。 +- `task_id`: 任务的唯一ID,如 `boss_status`。 +- `pipeline_name`: 需要运行的CV流水线名称,如 `boss_stun`。 +- `state_definitions`: 一个 `TargetStateDef` 的列表,所有这些状态都将从同一个CV结果中被解码。 +- `enabled`: **(新增)** 一个布尔值,默认为 `True`。如果设为 `False`,此任务将被完全忽略,方便调试。 +- `interval`: 任务的默认执行间隔(秒)。 +- `is_async`: 是否异步执行。 + +### 2.2. `AutoBattleTargetContext` (通用调度器) + +- **定位**: `src/zzz_od/auto_battle/auto_battle_target_context.py` +- **职责**: + - **加载数据**: 从 `game_data` 加载所有 `DetectionTask`,并**过滤掉 `enabled` 为 `False` 的任务**。 + - **调度执行**: 根据每个任务的 `interval` 计时,到期后调用 `TargetStateChecker.run_task`。 + - **结果处理**: 接收 `checker` 返回的结果,并根据结果创建 `StateRecord`(包括更新、赋值、清除)。 + +### 2.3. `TargetStateChecker` (通用解码器) + +- **定位**: `src/zzz_od/auto_battle/target_state/target_state_checker.py` +- **职责**: + - **运行CV**: 根据传入的 `DetectionTask`,调用相应的CV流水线。 + - **解码结果**: 遍历 `task` 中的每个 `TargetStateDef`,使用指定的 `check_way` 和 `check_params` 来解码CV结果。 + - **返回约定**: 对每个状态,返回一个标准化的结果,通知 `Context` 如何操作。 + +#### 返回值与CheckWay约定 + +`TargetStateChecker` 的 `_check_...` 方法的返回值是驱动状态更新的核心。 + +| 返回值 | 含义 | 生成的 `StateRecord` | +| ---------------------- | -------------------------------- | --------------------------------------------- | +| `True` | 状态被检测到,只需更新时间戳 | `StateRecord(state_name, time)` | +| `(True, value)` | 状态被检测到,且需要更新一个新值 | `StateRecord(state_name, time, value=value)` | +| `None` | 状态未被检测到,且需要**清除** | `StateRecord(state_name, is_clear=True)` | +| `False` | 状态未被检测到,需要**忽略** | *不生成任何记录* | + +**这个约定,结合 `TargetStateDef` 中的 `clear_on_miss` 参数,提供了对状态生命周期管理的精确控制。** + +#### 特殊 `check_way` 说明:`MAP_CONTOUR_LENGTH_TO_PERCENT` + +这是一个特殊且强大的 `check_way`,用于将一个进度条(如血条、失衡条)的长度转换为0-100的百分比。 + +- **工作原理**: 它计算 **轮廓的外接矩形宽度** 与 **遮罩图像的宽度** 的比值。 +- **CV流水线要求**: 使用此 `check_way` 的CV流水线,必须能输出两个关键结果到 `CvPipelineContext` 中: + 1. `contours`: 通过颜色过滤等方式找到的、代表当前进度条值的轮廓。 + 2. `mask_image`: 一个二值图,其**宽度**代表了进度条100%时的总长度。通常这个 `mask_image` 是通过裁剪(Crop)操作得到的。 +- **示例**: `boss_stun_line.yml` 流水线先裁剪出失衡条区域,然后通过HSV过滤颜色得到轮廓和遮罩,最后由该方法计算出失衡值的精确百分比。 + +## 3. 开发者指南:使用与扩展 + +### 3.1. 如何使用 + +使用方法与旧版一致。在 `.yml` 决策文件中,通过 `when` 或 `until` 字段订阅本模块产生的状态即可。 + +```yaml +# in your_op.yml +- op: 释放终结技 + when: + - 目标-失衡值 <= 20 + - 目标-近距离锁定 +``` + +### 3.2. 如何扩展 (核心优势) + +得益于数据驱动的架构,添加一个新的状态检测**极其简单**,**通常只需要修改一个文件**。 + +#### 案例:添加一个新的“敌人护盾”状态检测 + +##### Step 1: 准备CV流水线 (如果需要) + +如果检测逻辑复杂,先在 `assets/image_analysis_pipelines/` 目录下创建一个新的 `enemy_shield.yml` 文件。如果只是简单的模板匹配,可以复用现有的流水线。 + +##### Step 2: 在数据定义中添加新状态 + +打开 **唯一需要修改的文件**: `src/zzz_od/game_data/target_state.py`。 + +假设我们希望这个“护盾”状态与“强敌”、“失衡值”使用同一个CV流水线 `boss_stun` 进行检测。我们只需在对应的 `DetectionTask` 中添加一个新的 `TargetStateDef` 即可。 + +```python +# in src/zzz_od/game_data/target_state.py + +# ... (省略其他定义) + +DETECTION_TASKS = [ + DetectionTask( + task_id='boss_status', + pipeline_name='boss_stun', # 复用已有的流水线 + enabled=True, # (新增) 方便地启用或禁用此任务 + interval=0.5, + is_async=True, + state_definitions=[ + TargetStateDef( + state_name='目标-强敌', + check_way=TargetCheckWay.OCR_TEXT_CONTAINS, + check_params={'contains': '强敌'}, + clear_on_miss=True + ), + # === 新增的护盾状态定义 === + TargetStateDef( + state_name='目标-有护盾', + # 假设护盾检测是通过检查轮廓数量实现的 + check_way=TargetCheckWay.CONTOUR_COUNT_IN_RANGE, + check_params={'min_count': 1}, + # 当护盾消失时,也清除这个状态 + clear_on_miss=True + ) + # ======================== + ] + ), + # ... (其他任务) +] +``` + +**完成了。** + +你不需要修改 `TargetStateChecker` 或 `AutoBattleTargetContext`。系统会自动加载这个新的定义。如果想临时禁用某个任务,只需将其 `enabled` 标志设为 `False`。 + +### 3.3. 如何维护 + +1. **调试CV问题**: 如果怀疑是CV算法(如颜色过滤)出了问题,使用“图像分析工具”加载对应的 `.yml` 文件(如 `boss_stun_line.yml`),并使用游戏截图进行离线调试。 +2. **调整检测频率 (默认关闭,显式激活)**: 本模块采用“默认关闭,显式激活”的设计模式,以提供最大的灵活性和性能控制。 + + * **核心理念**: 一个检测任务 (`DetectionTask`) 默认是**不运行**的,即使它的 `enabled` 标志为 `True`。它必须被用户的配置文件**显式激活**。 + + * **如何激活和配置 (推荐)**: + 在你的战斗配置文件(如 `config/auto_battle/.test.yml`)中,为需要启用的任务设置一个**大于0**的检测间隔。 + ```yaml + # in your_battle_config.yml + + # 设置为 0.2 秒检测一次,激活“目标锁定”任务 + target_lock_interval: 0.2 + + # 设置为 1.0 秒检测一次,激活“异常状态”任务 + abnormal_status_interval: 1.0 + + # 如果不写某一项,或者其值为0或负数,则对应的任务不会运行 + ``` + 这是**唯一推荐**的、用于配置和激活这些可选任务的方式。 + + * **代码中的默认值 (`target_state.py`)**: + 在 `src/zzz_od/game_data/target_state.py` 文件中,可配置任务(如 `lock_on`)的默认 `interval` 被设置为 `0`。 + - 这个 `0` 值意味着任务处于“可用但休眠”状态。 + - 它**不会**被执行,直到被 `.yml` 文件中的一个正数间隔所覆盖。 + +## 4. 架构优势总结 + +- **卓越的可扩展性**: 添加新状态检测的成本从“修改多个Python类”降低到“在数据文件中添加一个条目”,极大地提升了开发效率和系统的可维护性。 +- **高度解耦与内聚**: 调度逻辑(`Context`)、解码逻辑(`Checker`)和任务定义(`Data`)完全分离,职责清晰。 +- **精确的状态管理**: 通过 `clear_on_miss` 和标准化的返回值约定,开发者可以精确控制每个状态的生命周期。 +- **高性能**: 保留了原有的混合并发模型,确保关键决策的即时性。 \ No newline at end of file diff --git a/docs/develop/project-overview.md b/docs/develop/project-overview.md new file mode 100644 index 0000000000..21a9291026 --- /dev/null +++ b/docs/develop/project-overview.md @@ -0,0 +1,110 @@ +# 项目概述 + +ZenlessZoneZero-OneDragon (绝区零一条龙) 是针对游戏《绝区零》(ZenlessZoneZero) 的自动化框架。 + +## 项目目标 + +提供一套完整的自动化解决方案,支持日常任务自动执行、战斗辅助、大地图探索等功能。 + +## 技术栈 + +- **Python 3.11+** +- **PySide6** - GUI 框架 +- **OpenCV** - 图像处理 +- **YOLO** - 目标检测 +- **OCR** - 文字识别 + +## 项目结构 + +``` +ZenlessZoneZero-OneDragon/ +├── src/ +│ ├── one_dragon/ # 基础自动化框架 +│ ├── zzz_od/ # 绝区零游戏特定实现 +│ ├── zzz_mcp/ # MCP 服务器(游戏窗口检测和操作) +│ └── one_dragon_qt/ # Qt GUI 组件库 +├── zzz-od-test/ # 测试代码仓库(Git Submodule) +├── docs/ # 项目文档 +└── deploy/ # 构建和部署脚本 +``` + +## 核心模块 + +### one_dragon 框架 + +提供通用的自动化能力,包括: +- 操作系统(基于节点的状态机) +- 屏幕系统(画面识别和导航) +- 应用层(高层业务逻辑) +- 插件系统(支持第三方扩展) + +详细文档见 [one_dragon 框架](one_dragon/) + +### zzz_od 游戏 + +绝区零游戏的自动化实现,包括: +- 战斗助手 +- 每日任务 +- 空洞探索 +- 大地图探索 + +详细文档见 [zzz 游戏](zzz/) + +### zzz_mcp 服务器 + +MCP (Model Context Protocol) 服务器,提供: +- 游戏窗口检测和定位 +- 游戏截图操作 +- 跨网络远程调用支持 + +详细文档见 [MCP 服务器](zzz/mcp/) + +## 快速开始 + +### 克隆项目 + +```bash +# 克隆主仓库 +git clone https://github.com/OneDragon-Anything/ZenlessZoneZero-OneDragon.git +cd ZenlessZoneZero-OneDragon + +# 初始化并更新子模块(必须) +git submodule update --init --recursive +``` + +### 安装依赖 + +```bash +uv sync --group dev +``` + +### 环境变量 + +项目使用 `.env` 文件管理环境变量。主要变量: + +- **PYTHONPATH=src** - 必须设置,指定 Python 源码路径 +- 其他推送服务相关变量(可选) + +配置方式: +1. 复制测试仓库中的 `.env.sample` 到项目根目录 +2. 重命名为 `.env` +3. 根据需要修改配置 + +### 运行命令 + +**重要**:所有 `uv run` 命令必须加上 `--env-file .env` 参数,否则会因为找不到模块而报错。 + +```bash +# 运行应用 +uv run --env-file .env python src/zzz_od/gui/app.py + +# 运行测试 +uv run --env-file .env pytest zzz-od-test/ + +# 代码检查 +uv run ruff check src/ tests/ +uv run ruff format src/ tests/ + +# 类型检查 +uv run pyright src/ +``` diff --git a/docs/develop/runtime-launcher.md b/docs/develop/runtime-launcher.md new file mode 100644 index 0000000000..167e0946fc --- /dev/null +++ b/docs/develop/runtime-launcher.md @@ -0,0 +1,145 @@ +# 集成启动器(RuntimeLauncher)详细设计文档 + +## 概述 + +集成启动器是一种将 Python 运行时直接嵌入发行包的启动方案。与原始启动器(需要用户单独安装 Python 和 uv)不同,集成启动器解压即可运行,适合首次安装或不熟悉 Python 环境的用户。 + +## 整体架构 + +### 两种启动器对比 + +| 特性 | 原始启动器 | 集成启动器 | +|------|-----------|-----------| +| 运行方式 | exe → 子进程调用系统 Python | exe 内嵌 Python,直接 import 运行 | +| Python 环境 | 用户自行安装 | 打包在 `.runtime/` 目录中 | +| 代码加载 | 通过系统 Python 执行 `src/` | 通过运行时钩子注入 `src/` 到 `sys.path` | +| 更新机制 | 只更新源码 | 源码自动更新 + 清单兼容性检查 | +| 发行体积 | exe 很小,虚拟环境约 1GB | exe + .runtime 约 100 MB | + +### 继承关系 + +``` +LauncherBase → 基础参数解析、run() 入口 + └── ExeLauncher → 版本显示、参数构建、pyuac 管理员提升 + └── RuntimeLauncher → 集成启动器基类:代码同步、控制台隐藏、致命错误弹窗、模板方法 + └── ZLauncher → 绝区零专用入口(导入 app.py / zzz_application_launcher.py) +``` + +- `LauncherBase` 和 `ExeLauncher` 位于 `src/one_dragon/launcher/`,是框架通用代码 +- `RuntimeLauncher` 也在框架层,提供集成启动器的通用能力 +- `ZLauncher` 在 `src/zzz_od/win_exe/runtime_launcher.py`,是具体游戏的入口 + +## 打包机制 + +### PyInstaller 目录模式 + +集成启动器使用 PyInstaller 的目录模式(非单文件),运行时文件存放在 `.runtime/` 子目录中。最终目录结构如下: + +``` +安装目录/ +├── OneDragon-RuntimeLauncher.exe ← 启动入口 +├── .runtime/ ← Python 运行时 + 冻结模块 +│ ├── module_manifest.py ← 外部依赖清单 +│ ├── config/project.yml ← 项目配置 +│ └── ... ← Python DLL、so、pyd 等 +└── src/ ← 源代码目录(通过 git 同步) + ├── one_dragon/ + ├── one_dragon_qt/ + ├── zzz_od/ + └── onnxocr/ +``` + +### 最小打包策略 + +为了控制体积和避免冻结代码与 src/ 中的源码冲突,spec 文件只保留极少量必须冻结的模块,其余全部排除。 + +打包保留的模块(KEEP_TREES): +- `one_dragon.launcher` — 启动器自身需要的代码 +- `one_dragon.version` — 版本号 + +这些模块及其所有父包和子模块会被保留,其余 `one_dragon.*`、`zzz_od.*` 等全部排除。 + +### 运行时钩子与路径注入 + +`hook_path_inject.py` 是 PyInstaller 的 runtime_hook,在主脚本执行前运行,完成两件事: + +1. 将 `src/` 加入 `sys.path` — 使 `zzz_od`、`one_dragon_qt` 等未冻结的顶层包可以被导入 +2. 将 `src/one_dragon` 追加到冻结 `one_dragon` 包的 `__path__` — 使 `one_dragon.envs`、`one_dragon.utils` 等未冻结子模块能被找到 + +第 2 步之所以必要,是因为 `one_dragon` 这个包同时存在于两个位置:`.runtime/` 中有冻结的 `one_dragon.launcher` 和 `one_dragon.version`,`src/one_dragon/` 中有其余子模块。Python 的包系统需要 `__path__` 同时包含两个路径才能正确解析所有子模块。 + +**维护要点**:如果 KEEP_TREES 中新增了不同的顶层包前缀(比如某天需要冻结 `one_dragon_qt.xxx`),则 `hook_path_inject.py` 中也需要对应追加该包的 `__path__` 扩展。两个文件中都有 NOTE 注释标注了这个耦合关系。 + +## 模块清单机制 + +### 问题背景 + +集成启动器的二进制依赖(pygit2、PySide6 等)打包在 `.runtime/` 中,不会随代码更新而变化。如果代码更新后 `import` 了一个新的外部库,而 `.runtime/` 中没有该库,就会导致 `ModuleNotFoundError`。 + +### 解决方案 + +`module_manifest.py` 文件记录了打包时所有源码文件的外部依赖 import 语句。这个文件既打包进 `.runtime/`(作为本地清单),又提交到 git 仓库(作为远程清单)。 + +代码更新时,`GitService._check_manifest_compatible()` 对比本地清单和目标 commit 的清单: +- 相同 → 兼容,允许更新 +- 不同 → 不兼容,阻止更新并提示用户下载新版集成启动器 + +### 清单路径配置 + +远程清单的路径不是硬编码的,而是从目标 commit 的 `config/project.yml` 中的 `manifest_path` 字段读取。这样即使清单文件改名或移动位置,只要 `project.yml` 正确指向它就能找到。 + +### 生成时机 + +- **打包时**:spec 文件通过 `importlib` 动态加载 `generate_module_manifest.py`,在 Analysis 阶段生成 `module_manifest.py` +- **CI 自动构建**:`build-running-resources.yml` 工作流每次推送到 main 或者 pr 更新时运行 `generate_module_manifest.py`,如果清单有变化则自动提交 + +## 代码同步流程 + +集成启动器的 `_sync_code()` 方法在每次启动时执行: + +1. 记录当前 `sys.modules` 快照(用于后续清理) +2. 延迟导入 `EnvConfig`、`GitService`、`ProjectConfig`(来自 `src/`) +3. 判断是否首次运行(检查 `.git` 目录是否存在) +4. 首次运行 → 克隆仓库;非首次 → 根据 `auto_update` 配置决定是否更新 +5. 成功后清除同步过程中加载的模块(`del sys.modules[...]`),调用 `importlib.invalidate_caches()` +6. 首次克隆失败 → 退出;后续更新失败 → 打日志继续运行 + +步骤 5 的模块清理确保了主程序后续导入的是更新后的代码,而非同步过程中缓存的旧版本。 + +## 错误处理 + +### 集成启动器的 try/except + +`RuntimeLauncher.run_gui_mode()` 和 `run_onedragon_mode()` 将整个执行流程(包括 `_sync_code()` 和子类的 `_do_run_gui()` / `_do_run_onedragon()`)包裹在 try/except 中。任何未捕获的异常都会通过 `_show_fatal_error()` 弹出 Windows MessageBox 并退出,避免闪退后用户看不到错误信息。 + +### app.py 的模块级 try + +`app.py` 顶层有一个 try/except 包裹所有 import 语句。这是因为 import 阶段的错误(如依赖缺失)发生在 `main()` 被调用之前,集成启动器的 try/except 也能捕获到,但模块级 try 提供了更精确的错误信息。 + +### ZLauncher 的 src 目录检查 + +`ZLauncher` 的入口文件在模块级检查 `src/` 目录是否存在。如果用户只解压了 exe 和 .runtime 而遗漏了 src/,会立即弹出 MessageBox 提示,避免后续出现难以理解的 ImportError。 + +## 发行产物 + +CI 构建生成以下集成启动器相关产物: + +| 文件 | 内容 | 用途 | +|------|------|------| +| `{version}-WithRuntime.zip` | 集成启动器 exe + .runtime + src | 首次部署,解压即用 | +| `RuntimeLauncher.zip` | 集成启动器 exe + .runtime(不含 src) | 就地升级已有环境 | + +WithRuntime 包含 src/ 是因为首次部署时还没有 .git 目录,无法通过 git clone 获取源码。用户解压 WithRuntime 后首次启动,集成启动器会自动初始化 git 仓库并设置远程跟踪。 + +## 启动器下载卡(UI) + +`LauncherDownloadCard` 提供了一个类型下拉框,让用户在「原始启动器」和「集成启动器」之间切换。切换时会: + +1. 断开旧版本检查器的信号连接(防止竞态覆盖) +2. 创建新的版本检查器(指向对应的 exe 文件) +3. 重置版本状态并触发重新检查 + +下载时的备份/回滚机制: +- 下载前将当前 exe 重命名为 `.bak`(集成启动器还会重命名 `.runtime` 为 `.runtime.bak`) +- 下载成功 → 删除备份 +- 下载失败 → 回滚备份 diff --git a/docs/develop/standards/coding-standards.md b/docs/develop/standards/coding-standards.md new file mode 100644 index 0000000000..105aa07d75 --- /dev/null +++ b/docs/develop/standards/coding-standards.md @@ -0,0 +1,81 @@ +# 开发规范 + +本文档规定了项目开发过程中需要遵守的编码规范和最佳实践。 + +## 关键开发原则 + +1. **最小化设计** - 只实现满足需求的最小功能集,避免过度设计 +2. **文档同步** - 修改模块后必须更新对应的文档文件 + +## Python 代码规范 + +### 文档字符串 + +- 所有函数必须有 Google 风格的文档字符串 +- 文档字符串使用中文编写 +- 类和重要逻辑也需要添加注释说明 + +### 类型提示 + +- 所有类成员变量和函数签名必须包含类型提示 (Type Hints) +- 使用内置泛型类型 (`list`, `dict`) 而不是从 `typing` 模块导入 (`List`, `Dict`) +- 示例: + ```python + def process_items(items: list[str]) -> dict[str, int]: + ... + ``` + +### 导入规范 + +编写源码时需要遵循以下导入规范: + +- **使用绝对路径导入**:禁止使用相对路径导入 + ```python + # 正确 + from one_dragon.base.operation import Operation + + # 错误 + from ..operation import Operation + ``` + +- **类型注解导入**:仅用于类型注解的导入应使用 `TYPE_CHECKING` + ```python + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from one_dragon.base.operation import Operation + ``` + +### 类构造函数 + +- 类的构造函数 `__init__` 必须显式声明所有必需的和可选的参数 +- 尽量避免使用 `**kwargs` 来传递未知参数 +- 这有助于提高代码的可读性和类型检查的准确性 + +### 父类构造函数调用 + +- 在所有子类的 `__init__` 方法中,调用父类构造函数时必须显式传入所有必需参数 +- 允许使用 `super().__init__(...)`,但必须确保所有参数都显式传递 +- 示例: + ```python + super().__init__(arg1=value1, arg2=value2) + ``` + +### 代码风格 + +- **禁止特殊字符**:禁止在代码中使用表情符号和特殊 Unicode 符号(如 emoji、数学符号等) +- **显式数据结构**:应该定义一个对象,而不是使用 dict +- **不暴露任何模块**:没有收到指示的情况下,不要在 `__init__.py` 中新增暴露任何模块 + +### GUI 组件 + +- 前端组件优先使用 `pyside6-fluent-widgets` 库中现有组件 +- 如需实现新组件,需按照 Fluent Design 实现样式效果 + +### 代码格式化 + +- 使用 ruff 进行代码格式化和检查 +- 使用 pyright 进行静态类型检查 +- 运行 `uv run ruff check src/ tests/` 检查代码 +- 运行 `uv run ruff format src/ tests/` 格式化代码 +- 运行 `uv run pyright src/` 检查类型 diff --git a/docs/develop/standards/documentation-standards.md b/docs/develop/standards/documentation-standards.md new file mode 100644 index 0000000000..82f2e94796 --- /dev/null +++ b/docs/develop/standards/documentation-standards.md @@ -0,0 +1,24 @@ +# 文档规范 + +本文档规定了项目文档编写需要遵守的规范。 + +## 核心原则 + +### 方案优先 + +文档的核心目标是**将方案描述清晰**,而非提供完整的代码示例。读者应该通过理解方案来指导开发,而不是复制粘贴代码。 + +### 简洁性 + +- 避免冗长和重复的说明 +- 只包含必要的信息,去除无关内容 +- 使用简洁的语言表达 + +### 避免代码示例 + +- **禁止完整的代码实现**:文档不应包含可直接复制使用的完整代码 +- **避免使用示例**:优先使用文字描述、流程图、架构图等方式说明 +- **例外情况**:仅在以下情况允许极简代码片段 + - 说明配置格式(如 YAML、JSON 结构) + - 说明 API 签名(必需参数和返回值) + - 说明数据模型关键字段 diff --git a/docs/develop/standards/testing-standards.md b/docs/develop/standards/testing-standards.md new file mode 100644 index 0000000000..a131be1963 --- /dev/null +++ b/docs/develop/standards/testing-standards.md @@ -0,0 +1,145 @@ +# 测试规范 + +本文档规定了项目测试编写和执行的规范。 + +## 关键测试原则 + +1. **测试数据清理** - 开发/生产共用数据库,测试后必须清理数据 + +## 测试框架 + +- 使用 `pytest` 进行测试 +- 涉及异步函数时,使用 `pytest-asyncio` 进行测试 + +## 测试命令 + +```bash +uv run --env-file .env pytest zzz-od-test/ # 运行所有测试 +uv run --env-file .env pytest zzz-od-test/tests/zzz_od/ # 运行特定模块 +uv run --env-file .env pytest -k test_name # 运行特定测试 +``` + +## 测试文件组织 + +### 目录结构约定 + +测试文件的目录路径应该是被测文件的包路径 + 被测文件名的文件夹。 + +**示例:** +- 被测文件:`one_dragon/base/operation/one_dragon_context.py` +- 测试目录:`zzz-od-test/tests/one_dragon/base/operation/one_dragon_context/` +- 该目录下存放多个测试用例文件 + +### 单方法测试文件 + +每个 Python 测试文件应专门用于测试单个方法的各种场景。 + +**示例:** +- 要测试 `method_a` 方法 +- 创建一个名为 `test_method_a.py` 的文件 +- 该文件应包含专门针对 `method_a` 方法的所有测试用例 + +### 测试类组织 + +- 测试文件必须使用测试类(以 `Test` 为前缀)来组织相关的测试方法 +- 示例: + ```python + class TestScreenInfo: + def test_init(self): + ... + + def test_add_area(self): + ... + ``` + +## 测试夹具 (Fixtures) + +### 使用 Fixture 管理依赖 + +- 使用 `pytest.fixture` 来管理测试依赖和状态(如对象实例创建和清理) +- 这能提高代码的复用性和可维护性 +- 注意指定 fixture 的作用域 (scope),避免重复调用 + +### 公共 Fixture + +- 测试根目录下有一个公共夹具 `zzz-od-test/test/conftest.py` +- 里面提供基础的运行上下文 `TestContext` +- 测试文件中可以通过 `test_context: TestContext` 引入 + +**示例:** +```python +def test_something(test_context: TestContext): + ctx = test_context + # 使用 ctx 进行测试 +``` + +## 测试包规范 + +### 禁止创建测试包 + +- 测试目录下**严禁创建** `__init__.py` 文件 + +**原因:** +- 测试目录(如 `tests/zzz_mcp/`)若包含 `__init__.py` +- 会被 Python 识别为包 +- 与源代码目录(如 `src/zzz_mcp/`)产生命名冲突 +- 导致导入失败 + +**正确做法:** +- 测试目录应保持为普通目录结构,不形成 Python 包 + +## 导入约定 + +由于项目使用 `src-layout`,测试文件中的导入路径不得包含 `src` 目录。 + +**正确示例:** +```python +from one_dragon.base.operation import Operation +``` + +**错误示例:** +```python +from src.one_dragon.base.operation import Operation +``` + +## 依赖 Mock + +- Mock 外部依赖(如 `controller`、`screenshot` 等) +- 使用 `zzz-od-test/test/conftest.py` 中提供的 `TestContext` +- 该 context 已包含 mock 的 controller 和相关工具 + +## 异步测试超时 + +- 所有异步测试方法必须包含超时设置 +- 使用 `pytest.mark.timeout(3)` 防止测试无限期挂起 +- 示例: + ```python + @pytest.mark.timeout(3) + async def test_async_operation(): + ... + ``` + +## 临时文件 + +- 使用当前工作目录下的 `.temp` 目录来存储临时文件 +- 测试结束后应及时清理临时文件 + +## 测试数据管理 + +### 数据清理原则 + +- 当前项目没有区分开发和生产数据库 +- 所有环境共用同一个数据库实例 +- 因此测试数据清理尤为重要,必须确保测试后正确清理 +- 避免影响开发和使用体验 + +### 测试前准备 + +- 考虑测试过程产生脏数据的情况 +- 在测试设计阶段就需要考虑清理方案 + +## 测试覆盖要求 + +- 修改任何模块后,必须更新 `zzz-od-test/` 中的测试文件 +- 确保修改后的代码已被覆盖且所有测试都通过 +- 除非源代码逻辑有错误,否则不能因为测试不通过而修改源代码 diff --git a/docs/develop/standards/workflow-standard.md b/docs/develop/standards/workflow-standard.md new file mode 100644 index 0000000000..3007a9fc2d --- /dev/null +++ b/docs/develop/standards/workflow-standard.md @@ -0,0 +1,36 @@ +# 开发流程规范 + +本文档说明项目的开发流程规范,包括日常开发、Submodule 使用、PR 处理等。 + +## 日常开发流程 + +### 1. 开始新功能 + +1. 同步 main 分支最新代码 +2. 从 main 创建开发分支,子模块也切换到相同的分支 +3. 在分支上进行开发 + +### 2. 自测检查 + +1. 为本次修改编写对应测试 +2. 确保测试全部通过 +3. 使用 ruff 和 pyright 进行代码检测并修复 + +### 3. 提交代码 + +1. 按照提交规范编写提交信息(feat/fix/refactor/docs/test/chore) +2. 创建 PR + +### 4. PR 处理 + +1. 确保相关 action check 通过 +2. 对 reviewer 发起的 thread 进行修复或回复 +3. 确保所有 thread 都 resolve + +### 5. 合并代码 + +1. 优先使用 squash 进行合并代码 +2. 先对子模块进行合并 +3. 将子模块切换到最新的 main +4. 回到主仓库,更新 submodule 引用 +5. 合并主仓库的功能分支到 main diff --git a/docs/develop/zzz/application/intel_board.md b/docs/develop/zzz/application/intel_board.md new file mode 100644 index 0000000000..d56773f66d --- /dev/null +++ b/docs/develop/zzz/application/intel_board.md @@ -0,0 +1,41 @@ +# 情报板 + +基础流程 (每周重置记录) + +1. 发布委托 +2. 挑战 +3. 接收奖励 + +## 1.发布委托 + +### 1.1.恶名狩猎 + +1. 下述情况跳过 + - 开关未开启 + - 达到委托并行上限 + - 达到委托发布次数上限 + - 电量不足 +2. 发布选择 3选1 + - 按恶名狩猎计划发布 + - 按体力计划发布 + - 自定义次数发布 + +### 1.2.专业挑战室 + +1. 下述情况跳过 + - 开关未开启 + - 达到委托并行上限 + - 达到委托发布次数上限 + - 电量不足 +2. 发布选择 2选1 + - 按体力计划发布 + - 自定义次数发布 + +## 2.挑战 + +在未达到周期上限时,按配置选择委托进行挑战。 + +## 3.接收奖励 + +1. 接收委托奖励 +2. 按配置优先级进行兑换 \ No newline at end of file diff --git a/docs/develop/zzz/mcp/README.md b/docs/develop/zzz/mcp/README.md new file mode 100644 index 0000000000..48bbc323e7 --- /dev/null +++ b/docs/develop/zzz/mcp/README.md @@ -0,0 +1,128 @@ +# ZZZ OD MCP Server + +ZZZ OD MCP Server 是为绝区零游戏自动化项目定制的 MCP 服务器,提供游戏画面感知、截图、窗口操作等功能。 + +## 使用场景 + +### 场景 1:本地开发(默认方式)⭐ + +在游戏本机上直接使用 Claude Code,**不需要 daemon**。 + +```powershell +# 安装到 Claude Code +.\tools\mcp\install.ps1 + +# 启动 MCP Server(使用默认端口 23001) +uv run python src\zzz_mcp\zzz_mcp_server.py + +# 或指定端口 +uv run python src\zzz_mcp\zzz_mcp_server.py --port 9000 +``` + +在 Claude Code 中直接使用: +``` +请检查游戏窗口状态 +请捕获游戏画面 +``` + +### 场景 2:远程 SSH 开发(高级用法)⚙️ + +通过 SSH 远程连接到游戏电脑,**需要使用 daemon**。 + +详见:[Daemon 远程开发指南](remote-ssh.md) + +## 快速开始(本地开发) + +### 1. 安装依赖 + +```bash +cd D:\code\workspace\ZenlessZoneZero-OneDragon +uv sync --group dev +``` + +### 2. 安装 MCP Server + +```powershell +# 安装到 Claude Code(默认端口 23001) +.\tools\mcp\install.ps1 + +# 检查安装状态 +.\tools\mcp\install.ps1 -Check + +# 指定端口安装 +.\tools\mcp\install.ps1 -Port 9001 +``` + +### 3. 启动 MCP Server + +```powershell +# 使用默认端口(23001) +uv run python src\zzz_mcp\zzz_mcp_server.py + +# 指定端口 +uv run python src\zzz_mcp\zzz_mcp_server.py --port 9000 + +# 指定监听地址 +uv run python src\zzz_mcp\zzz_mcp_server.py --host 0.0.0.0 --port 9000 +``` + +启动后会显示: +``` +============================================================ +ZZZ OD MCP Server (HTTP传输方式) +============================================================ + +监听地址: http://127.0.0.1:23001/mcp + +按 Ctrl+C 停止服务器 +------------------------------------------------------------ +``` + +### 4. 重启 Claude Code + +安装完成后需要重启 Claude Code 以使配置生效。 + +### 5. 测试连接 + +在 Claude Code 中输入: +``` +请调用 check_game_window 工具测试连接 +``` + +如果成功,应该返回游戏窗口状态信息。 + +## 端口说明 + +| 服务 | 默认端口 | 说明 | +|-----|---------|------| +| ZZZ OD MCP Server | 23001 | 游戏操作服务器 | +| ZZZ OD Daemon | 23002 | 管理服务器(仅 SSH 场景) | + +## 停止 MCP Server + +### 方法 1:Ctrl+C + +如果 MCP Server 在前台运行,直接按 `Ctrl+C` 停止。 + +### 方法 2:通过端口查找并停止 + +```powershell +# 通过端口 23001 找到进程ID并停止 +$pid = (netstat -ano | Select-String ":23001.*LISTENING").ToString().Split()[-1] +Stop-Process -Id $pid -Force +``` + +## 可用工具 + +| 工具 | 说明 | 状态 | +|------|------|------| +| `check_game_window` | 检查游戏窗口状态 | ✅ | +| `capture_game_screen` | 捕获游戏画面 | ✅ | +| `open_and_enter_game` | 打开并进入游戏 | ✅ | + +## 详细文档 + +- [安装指南](installation.md) - 详细安装步骤 +- [远程 SSH 开发](remote-ssh.md) - Daemon 架构和远程开发 +- [架构设计](architecture.md) - 系统架构和设计原理 +- [故障排查](troubleshooting.md) - 常见问题解决 diff --git a/docs/develop/zzz/mcp/architecture.md b/docs/develop/zzz/mcp/architecture.md new file mode 100644 index 0000000000..f30b9649b0 --- /dev/null +++ b/docs/develop/zzz/mcp/architecture.md @@ -0,0 +1,236 @@ +# 架构设计 + +本文档介绍 ZZZ OD MCP Server 的系统架构和设计原理。 + +## 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ SSH 远程环境 │ +│ (Session 0 - 服务会话) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ │ +│ │ Claude Code │ │ +│ └──────┬──────┘ │ +│ │ │ +│ │ HTTP (MCP) │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Claude Code MCP Configuration │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ zzz_od_daemon: stdio -> HTTP://127.0.0.1:23002/mcp │ │ +│ │ zzz_od: HTTP -> HTTP://127.0.0.1:23001/mcp │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ▲ + │ HTTP + │ +┌─────────────────────────────────────────────────────────────────┐ +│ 游戏本机 (Session 1) │ +│ (交互式桌面会话 - 管理员权限) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ZZZ OD Daemon (管理服务器) │ │ +│ │ 端口: 23002 (HTTP) │ │ +│ │ 传输: streamable-http │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ 工具: │ │ +│ │ - start_zzz_od_server │ │ +│ │ - stop_zzz_od_server │ │ +│ │ - restart_zzz_od_server │ │ +│ │ - get_zzz_od_server_status │ │ +│ └──────────────────────┬──────────────────────────────────┘ │ +│ │ 启停控制 │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ZZZ OD MCP Server (游戏操作服务器) │ │ +│ │ 端口: 23001 (HTTP) │ │ +│ │ 传输: streamable-http │ │ +│ ├─────────────────────────────────────────────────────────┤ │ +│ │ 工具: │ │ +│ │ - check_game_window 检查游戏窗口状态 │ │ +│ │ - capture_game_screen 捕获游戏画面 │ │ +│ │ - open_and_enter_game 打开并进入游戏 │ │ +│ │ - ... (更多游戏操作工具) │ │ +│ └──────────────────────┬──────────────────────────────────┘ │ +│ │ │ +│ │ Windows API │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ 绝区零游戏窗口 │ │ +│ │ (Session 1 - 交互式桌面) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 核心组件 + +### 1. Daemon (管理服务器) + +**文件**: `tools/mcp/daemon/zzz_od_daemon.py` + +**职责**: +- 长期运行在 Session 1(交互式桌面会话) +- 管理游戏操作服务器的启停 +- 提供远程管理接口 +- 资源占用极小 + +**特性**: +- 端口: 23002 +- 传输方式: HTTP (streamable-http) +- 运行时长: 长期运行(开机自启) +- 工具数量: 5 个(管理工具) + +### 2. MCP Server (游戏操作服务器) + +**文件**: `src/zzz_mcp/zzz_mcp_server.py` + +**职责**: +- 执行游戏窗口操作 +- 提供游戏画面感知 +- 触发鼠标点击等操作 + +**特性**: +- 端口: 23001 +- 传输方式: HTTP (streamable-http) +- 运行时长: 按需启动 +- 工具数量: 多个(游戏操作工具) + +### 3. Claude Code 配置 + +**位置**: `~/.claude.json` + +**配置示例**: +```json +{ + "mcpServers": { + "zzz_od_daemon": { + "transport": "stdio", + "command": "uv run --directory D:/code/workspace/ZenlessZoneZero-OneDragon python tools/mcp/daemon/zzz_od_daemon.py" + }, + "zzz_od": { + "transport": "http", + "url": "http://127.0.0.1:23001/mcp", + "timeout": 30000 + } + } +} +``` + +## 设计原理 + +### Session 隔离问题 + +Windows 的会话隔离机制导致不同会话之间无法直接交互: + +| 会话 | 名称 | 说明 | +|-----|------|------| +| Session 0 | 服务会话 | SSH、系统服务运行在此会话 | +| Session 1 | 交互式桌面会话 | 用户桌面、游戏窗口运行在此会话 | + +**关键问题**: +- SSH 运行在 Session 0 +- 游戏窗口运行在 Session 1 +- Session 0 无法直接操作 Session 1 的窗口 + +### PsExec 的限制 + +尝试使用 PsExec 跨会话启动时遇到的问题: + +``` +PsExec -i 1 cmd /c "start_mcp_server.ps1" +``` + +**限制**: +1. 无法提权到管理员权限 +2. `pyautogui`、`pydirectinput` 等库需要管理员权限 +3. 跨会话启动导致权限上下文丢失 + +**结论**: PsExec 方案不可行 + +### 解决方案:Daemon 架构 + +**核心思想**: 在 Session 1 中长期运行一个轻量级管理服务器 + +**优势**: +1. ✅ Daemon 在 Session 1 启动,拥有管理员权限 +2. ✅ Daemon 启动的 MCP Server 继承管理员权限 +3. ✅ 可通过 HTTP 远程控制 +4. ✅ 资源占用小,可长期运行 +5. ✅ 支持开机自启 + +## 数据流 + +### 启动流程 + +```mermaid +sequenceDiagram + participant User as 用户 (SSH) + participant Claude as Claude Code + participant Daemon as Daemon (23002) + participant Server as MCP Server (23001) + participant Game as 游戏窗口 + + User->>Claude: 启动 zzz_od 服务器 + Claude->>Daemon: HTTP POST /start_zzz_od_server + Daemon->>Server: 启动进程 (Session 1) + Server-->>Daemon: 返回 PID + Daemon-->>Claude: 返回成功信息 + Claude-->>User: 服务器已启动 + + User->>Claude: 检查游戏窗口 + Claude->>Server: HTTP POST /check_game_window + Server->>Game: Windows API 调用 + Game-->>Server: 窗口状态 + Server-->>Claude: 返回窗口信息 + Claude-->>User: 显示窗口状态 +``` + +### 操作流程 + +```mermaid +sequenceDiagram + participant User as 用户 (SSH) + participant Claude as Claude Code + participant Server as MCP Server (23001) + participant Game as 游戏窗口 + + User->>Claude: 打开并进入游戏 + Claude->>Server: HTTP POST /open_and_enter_game + Server->>Server: 启动游戏进程 + Server->>Game: 等待窗口初始化 + Server->>Game: 执行登录操作 + Game-->>Server: 登录成功 + Server-->>Claude: 返回成功 + Claude-->>User: 游戏已启动 +``` + +## 端口分配 + +| 端口 | 服务 | 用途 | 长期运行 | +|-----|------|------|---------| +| 23001 | MCP Server | 游戏操作 | ❌ 按需 | +| 23002 | Daemon | 管理服务 | ✅ 是 | + +## 安全考虑 + +1. **本地监听**: 所有服务默认监听 127.0.0.1,不暴露到外网 +2. **权限继承**: Daemon 和 MCP Server 都需要管理员权限 +3. **环境变量**: 使用 `.env` 文件管理敏感配置 + +## 性能优化 + +1. **按需启动**: MCP Server 只在需要时启动,节省资源 +2. **轻量级 Daemon**: Daemon 资源占用极小,可长期运行 +3. **HTTP 传输**: 使用 streamable-http,性能优于 stdio + +## 扩展性 + +架构支持轻松添加新的游戏操作工具: + +1. 在 `src/zzz_mcp/tools/` 下添加新工具 +2. 在 `zzz_mcp_server.py` 中注册工具 +3. 重启 MCP Server 即可使用 diff --git a/docs/develop/zzz/mcp/dev-tools-design.md b/docs/develop/zzz/mcp/dev-tools-design.md new file mode 100644 index 0000000000..50f47552b6 --- /dev/null +++ b/docs/develop/zzz/mcp/dev-tools-design.md @@ -0,0 +1,74 @@ +# ZZZ OD MCP 开发工具集设计 + +## 设计思路 + +开发一个自动流程的完整闭环: + +``` +人工玩游戏 → 截图记录 → 创建配置 → 编写代码 → 测试调试 → 优化完善 + ↓ ↓ ↓ ↓ ↓ ↓ + 感知工具 分析工具 配置工具 (AI完成) (AI完成) (AI完成) +``` + +**核心理念**: +- MCP 工具只提供**感知能力**(截图、OCR、屏幕识别) +- **代码编写、调试、优化**由 AI 直接完成,不需要专门的工具 +- 通过实际开发示例来发现真正需要的工具 + +--- + +## 最小工具集(MVP) + +### 1. 感知类工具 - 让 AI "看到"游戏 + +#### `capture_game_screen` - 截取游戏画面 ✅ 已有 +- 返回截图路径 +- AI 可以查看截图了解当前状态 + +#### `ocr_game_screen` - OCR 识别游戏画面 +- 返回画面中的所有文本及位置 +- AI 可以分析需要点击什么 + +#### `identify_current_screen` - 识别当前画面名称 +- 返回当前屏幕名称和置信度 +- AI 可以知道现在在哪个界面 + +### 2. 交互类工具 - 让 AI "操作"游戏 + +#### `click_at_position` - 点击指定位置 +- 输入:x, y 坐标 +- AI 可以根据分析结果进行点击 + +--- + +## 使用流程示例 + +``` +用户: 帮我开发一个"随便观制造"的自动流程 + +AI: [capture_game_screen] 先截图看看当前画面 + [查看截图] 看到了"随便观-入口"界面,有"经营"和"制造"两个选项 + +用户: 点击制造 + +AI: [ocr_game_screen] 识别到"制造"在 (150, 250) + [click_at_position] 点击 (150, 250) + [capture_game_screen] 截图确认 + [identify_current_screen] 当前画面是"随便观-制造坊" + +用户: 继续 + +AI: [ocr_game_screen] 识别到"开工"按钮 + [查看现有配置] 检查是否已有这个画面的配置 + 如果没有,我会创建配置文件 + 然后编写代码... +``` + +--- + +## 待实践验证 + +通过实际开发示例来验证: +1. 这几个工具是否足够? +2. 还需要什么辅助工具? +3. 工具的返回格式是否合理? diff --git a/docs/develop/zzz/mcp/installation.md b/docs/develop/zzz/mcp/installation.md new file mode 100644 index 0000000000..97791f8189 --- /dev/null +++ b/docs/develop/zzz/mcp/installation.md @@ -0,0 +1,188 @@ +# 安装指南 + +本文档介绍如何安装和配置 ZZZ OD MCP Server。 + +## 系统要求 + +- Windows 10/11 +- Python 3.11+ +- Claude Code CLI +- 绝区零游戏已安装 + +## 本地开发安装(默认方式)⭐ + +### 步骤 1:安装依赖 + +```bash +cd D:\code\workspace\ZenlessZoneZero-OneDragon +uv sync --group dev +``` + +### 步骤 2:安装 MCP Server + +```powershell +# 安装到 Claude Code(默认端口 23001) +.\tools\mcp\install.ps1 + +# 检查安装状态 +.\tools\mcp\install.ps1 -Check + +# 卸载 +.\tools\mcp\install.ps1 -Uninstall + +# 指定端口安装 +.\tools\mcp\install.ps1 -Port 9001 +``` + +### 步骤 3:启动 MCP Server + +```powershell +# 使用默认端口(23001) +uv run python src\zzz_mcp\zzz_mcp_server.py + +# 指定端口 +uv run python src\zzz_mcp\zzz_mcp_server.py --port 9000 + +# 指定监听地址 +uv run python src\zzz_mcp\zzz_mcp_server.py --host 0.0.0.0 --port 9000 +``` + +启动后会显示: +``` +============================================================ +ZZZ OD MCP Server (HTTP传输方式) +============================================================ + +监听地址: http://127.0.0.1:23001/mcp + +按 Ctrl+C 停止服务器 +------------------------------------------------------------ +``` + +### 步骤 4:重启 Claude Code + +安装完成后需要重启 Claude Code 以使配置生效。 + +### 步骤 5:测试连接 + +在 Claude Code 中输入: +``` +请调用 check_game_window 工具测试连接 +``` + +如果成功,应该返回游戏窗口状态信息。 + +## 远程 SSH 开发安装(可选)⚙️ + +**如果你通过 SSH 远程连接到游戏电脑**,需要安装 Daemon。 + +详见:[远程 SSH 开发指南](remote-ssh.md) + +## 端口配置 + +| 服务 | 默认端口 | 说明 | 必需 | +|-----|---------|------|------| +| ZZZ OD MCP Server | 23001 | 游戏操作服务器 | ✅ 是 | +| ZZZ OD Daemon | 23002 | 管理服务器 | ❌ 仅 SSH 场景 | + +## 卸载 + +### 卸载 MCP Server + +```powershell +# 停止 MCP Server(如果正在运行) +# 按 Ctrl+C 或通过端口查找并停止 + +# 卸载 +.\tools\mcp\install.ps1 -Uninstall +``` + +### 卸载 Daemon + +```powershell +# 停止 Daemon +$pid = (netstat -ano | Select-String ":23002.*LISTENING").ToString().Split()[-1] +Stop-Process -Id $pid -Force + +# 卸载 +.\tools\mcp\daemon\install_daemon.ps1 -Uninstall +``` + +## 常见问题 + +### 问题 1:找不到 claude 命令 + +确保已安装 Claude Code CLI 并在 PATH 中: +```bash +claude --version +``` + +### 问题 2:端口已被占用 + +```powershell +# 查找占用端口的进程 +netstat -ano | findstr :23001 + +# 停止进程 +$pid = (netstat -ano | Select-String ":23001.*LISTENING").ToString().Split()[-1] +Stop-Process -Id $pid -Force +``` + +或使用其他端口: +```powershell +# 安装到其他端口 +.\tools\mcp\install.ps1 -Port 9001 + +# 启动时指定端口 +uv run python src\zzz_mcp\zzz_mcp_server.py --port 9001 +``` + +### 问题 3:模块导入失败 + +错误:`ModuleNotFoundError: No module named 'zzz_mcp'` + +解决方法: +```bash +# 确保在项目根目录 +cd D:\code\workspace\ZenlessZoneZero-OneDragon + +# 安装依赖 +uv sync --group dev +``` + +### 问题 4:配置未生效 + +1. 确认已重启 Claude Code +2. 检查配置: + ```bash + claude mcp list + ``` +3. 如果配置有问题,先卸载再重新安装: + ```powershell + .\tools\mcp\install.ps1 -Uninstall + .\tools\mcp\install.ps1 + ``` + +## 验证安装 + +### 检查 MCP Server 状态 + +```powershell +# 检查端口是否监听 +netstat -ano | findstr :23001 +``` + +### 在 Claude Code 中测试 + +``` +请调用 check_game_window 工具 +``` + +应该返回游戏窗口状态信息。 + +## 下一步 + +- 本地开发:参考 [README.md](README.md) +- 远程 SSH 开发:参考 [远程 SSH 开发指南](remote-ssh.md) +- 架构说明:参考 [架构设计](architecture.md) +- 故障排查:参考 [故障排查](troubleshooting.md) diff --git a/docs/develop/zzz/mcp/remote-ssh.md b/docs/develop/zzz/mcp/remote-ssh.md new file mode 100644 index 0000000000..0a47936268 --- /dev/null +++ b/docs/develop/zzz/mcp/remote-ssh.md @@ -0,0 +1,217 @@ +# 远程 SSH 开发指南 + +本文档介绍如何通过 SSH 远程连接到游戏电脑进行开发。 + +## 适用场景 + +**只在以下情况下需要阅读本文档**: + +- ✅ 你通过 SSH 远程连接到游戏电脑 +- ✅ 你需要在远程会话中控制游戏 + +**如果以下情况,不需要阅读本文档**: + +- ❌ 你直接在游戏本机上使用 Claude Code +- ❌ 你只需要本地开发 + +请参考:[README.md](README.md) - 本地开发快速开始 + +## 问题:SSH 远程无法触发鼠标点击 + +### 原因分析 + +**1. Session 隔离** +- SSH 运行在 Session 0(服务会话) +- 游戏窗口运行在 Session 1(交互式桌面会话) + +**2. PsExec 的限制**(已尝试的方案) +- ❌ PsExec 无法提权到管理员权限 +- ❌ 即使使用 `-i 1` 参数切换到 Session 1,仍无法获取管理员权限 +- ❌ `pyautogui`、`pydirectinput` 等鼠标点击库需要管理员权限 +- ❌ 跨会话启动会导致权限上下文丢失 + +**3. 结论** +- SSH + PsExec 方案**不可行**,无法实现鼠标点击功能 +- 必须在 Session 1 中直接以管理员权限启动进程 + +## 解决方案:Daemon 架构 + +通过在游戏本机运行一个长期运行的 Daemon(管理服务器),实现远程控制。 + +### 架构 + +``` +┌─────────────────┐ ┌──────────────────┐ +│ SSH 会话 │ │ Session 1 │ +│ (Session 0) │ │ │ +│ │ HTTP (MCP) │ Daemon │ +│ Claude Code │◄───────────────────│ (端口 23002) │ +│ │ │ 长期运行,轻量 │ +└─────────────────┘ └──────────────────┘ + │ + │ 启停控制 + ▼ + ┌──────────────────┐ + │ MCP Server │ + │ (端口 23001) │ + │ 按需启动 │ + │ 游戏操作 │ + └──────────────────┘ +``` + +**设计理念**: +- **Daemon**:轻量级 MCP,长期运行在 Session 1 +- **MCP Server**:游戏操作 MCP,按需启动/停止 + +**优势**: +- ✅ Daemon 在 Session 1 运行,拥有管理员权限 +- ✅ Daemon 启动的 MCP Server 继承管理员权限 +- ✅ 可通过 HTTP 远程控制启停 +- ✅ 资源占用小,可长期运行 +- ✅ 支持开机自启 + +## 安装和配置 + +### 步骤 1:安装 Daemon(在游戏本机) + +**重要**:Daemon 必须在游戏本机直接启动(Session 1),不能通过 SSH 启动。 + +```powershell +# 安装到 Claude Code(默认端口 23002) +.\tools\mcp\daemon\install_daemon.ps1 + +# 检查安装状态 +.\tools\mcp\daemon\install_daemon.ps1 -Check + +# 卸载 +.\tools\mcp\daemon\install_daemon.ps1 -Uninstall +``` + +### 步骤 2:启动 Daemon(在游戏本机) + +**在游戏本机上操作**(不能通过 SSH): + +```powershell +# 使用默认端口(23002) +.\tools\mcp\daemon\start_daemon.ps1 + +# 指定端口 +.\tools\mcp\daemon\start_daemon.ps1 -Port 9001 +``` + +### 步骤 3:设置开机自启(推荐) + +```powershell +# 创建开机自启快捷方式 +.\tools\mcp\daemon\create_startup_shortcut.ps1 +``` + +快捷方式将创建到启动文件夹,开机时自动启动 Daemon(后台运行,隐藏窗口)。 + +### 步骤 4:通过 SSH 远程使用 + +现在你可以通过 SSH 远程连接,在 Claude Code 中使用: + +``` +请启动 zzz_od 服务器 +请检查游戏窗口状态 +请打开并进入游戏 +``` + +## Daemon 工具 + +| 工具名称 | 说明 | +|---------|------| +| `start_zzz_od_server` | 启动 MCP Server(端口 23001)| +| `stop_zzz_od_server` | 停止 MCP Server | +| `restart_zzz_od_server` | 重启 MCP Server | +| `get_zzz_od_server_status` | 查看服务器状态(PID、内存、CPU 等)| + +## 使用流程 + +### 典型工作流程 + +1. **游戏本机**:Daemon 已启动(开机自启) +2. **SSH 远程**:连接到游戏电脑 +3. **启动服务器**: + ``` + 请启动 zzz_od 服务器 + ``` +4. **使用工具**: + ``` + 请检查游戏窗口状态 + 请打开并进入游戏 + ``` +5. **操作完成后**(可选): + ``` + 请停止 zzz_od 服务器 + ``` + +### 查看服务器状态 + +``` +请调用 get_zzz_od_server_status 查看服务器状态 +``` + +返回示例: +``` +[STATUS] ZZZ OD MCP Server 运行中 +PID: 12345 +启动时间: Wed Feb 11 08:00:22 2026 +CPU 使用: 0.5% +内存使用: 45.32 MB +子进程数: 2 +端口: 23001 +``` + +## 停止 Daemon + +### 方法 1:通过端口查找并停止 + +```powershell +# 通过端口 23002 找到进程ID并停止 +$pid = (netstat -ano | Select-String ":23002.*LISTENING").ToString().Split()[-1] +Stop-Process -Id $pid -Force +``` + +### 方法 2:Ctrl+C + +如果 Daemon 在前台运行,直接按 `Ctrl+C` 停止。 + +## 故障排查 + +### Daemon 无法连接 + +确保: +1. Daemon 已在游戏本机启动(不能通过 SSH) +2. 检查端口 23002 是否监听:`netstat -ano | findstr :23002` +3. Claude Code 配置文件包含 Daemon 配置 + +### MCP Server 点击无效 + +**关键**:MCP Server 必须由 Daemon 在游戏本机启动,不能通过 SSH + PsExec 启动。 + +### MCP Server 启动失败 + +检查: +1. 项目路径是否正确 +2. 环境变量文件 `.env` 是否存在 +3. 端口 23001 是否被占用 + +## 与本地开发的区别 + +| 特性 | 本地开发 | 远程 SSH 开发 | +|------|----------|--------------| +| 启动方式 | 直接启动 MCP Server | 通过 Daemon 管理 | +| 工作位置 | 游戏本机 | SSH 远程 | +| 鼠标点击 | ✅ 正常工作 | ✅ 通过 Daemon 正常工作 | +| 远程管理 | ❌ 不支持 | ✅ 支持 | +| 开机自启 | 不需要 | 推荐 | +| 复杂度 | 简单 | 较复杂 | + +## 总结 + +- **本地开发**:直接启动 MCP Server,不需要 Daemon +- **远程 SSH 开发**:需要使用 Daemon 架构 + +如果你不需要远程 SSH 开发,请忽略本文档,参考 [README.md](README.md) 进行本地开发即可。 diff --git a/docs/develop/zzz/mcp/troubleshooting.md b/docs/develop/zzz/mcp/troubleshooting.md new file mode 100644 index 0000000000..987e27f35d --- /dev/null +++ b/docs/develop/zzz/mcp/troubleshooting.md @@ -0,0 +1,247 @@ +# 故障排查 + +本文档介绍常见问题及其解决方法。 + +## 问题 1:无法连接 MCP 服务器 + +### 症状 + +在 Claude Code 中调用工具时提示连接失败 + +### 可能原因 + +1. MCP 服务器未启动 +2. 端口配置不一致 +3. 防火墙阻止连接 + +### 解决方法 + +#### 1. 确认 MCP Server 已启动 + +```powershell +# 检查端口 23001 是否监听 +netstat -ano | findstr :23001 +``` + +如果没有输出,MCP Server 未启动: + +```powershell +uv run python src\zzz_mcp\zzz_mcp_server.py +``` + +#### 2. 测试 HTTP 连接 + +```powershell +# 测试 MCP Server 连接 +curl http://127.0.0.1:23001/mcp +``` + +#### 3. 检查 Claude Code 配置 + +```bash +claude mcp list +``` + +确认 `zzz_od` 在列表中,且端口配置正确。 + +--- + +## 问题 2:端口已被占用 + +### 症状 + +启动时出现错误:`Address already in use` 或 `端口已被占用` + +### 解决方法 + +#### 1. 查找占用端口的进程 + +```powershell +# 查找端口 23001 +netstat -ano | findstr :23001 +``` + +输出示例: +``` +TCP 127.0.0.1:23001 0.0.0.0:0 LISTENING 12345 +``` + +最后一列是进程 ID (PID)。 + +#### 2. 停止占用端口的进程 + +```powershell +# 停止进程 +Stop-Process -Id 12345 -Force +``` + +#### 3. 使用其他端口 + +如果不想停止进程,可以修改端口配置: + +```powershell +# 安装到其他端口 +.\tools\mcp\install.ps1 -Port 9001 + +# 启动时指定端口 +uv run python src\zzz_mcp\zzz_mcp_server.py --port 9001 +``` + +--- + +## 问题 3:模块导入失败 + +### 症状 + +错误:`ModuleNotFoundError: No module named 'zzz_mcp'` + +### 原因 + +1. 不在项目根目录下运行 +2. Python 依赖未安装 + +### 解决方法 + +```bash +# 1. 确保在项目根目录 +cd D:\code\workspace\ZenlessZoneZero-OneDragon + +# 2. 安装依赖 +uv sync --group dev +``` + +--- + +## 问题 4:配置未生效 + +### 症状 + +修改配置后没有生效 + +### 解决方法 + +#### 1. 重启 Claude Code + +配置修改后必须重启 Claude Code。 + +#### 2. 检查配置 + +```bash +# 查看配置 +claude mcp list +``` + +#### 3. 重新安装 + +```powershell +# 卸载 +.\tools\mcp\install.ps1 -Uninstall + +# 重新安装 +.\tools\mcp\install.ps1 +``` + +--- + +## 问题 5:游戏窗口检测失败 + +### 症状 + +调用 `check_game_window` 返回窗口无效 + +### 原因 + +1. 游戏未启动 +2. 游戏窗口标题不匹配 +3. 游戏窗口最小化 + +### 解决方法 + +#### 1. 确认游戏已启动 + +手动启动游戏,检查窗口标题是否为"绝区零"。 + +#### 2. 检查窗口状态 + +```powershell +# 查找游戏窗口 +Get-Process | Where-Object {$_.MainWindowTitle -like "*绝区零*"} +``` + +#### 3. 使用自动启动 + +使用自动启动工具: +``` +请打开并进入游戏 +``` + +--- + +## 问题 6:权限问题 + +### 症状 + +操作失败,提示权限不足 + +### 解决方法 + +#### 1. 以管理员身份运行 + +右键 PowerShell -> 以管理员身份运行 + +#### 2. 检查 UAC 设置 + +确保 UAC 不会阻止操作 + +#### 3. 检查文件权限 + +确保项目目录有读写权限 + +--- + +## 问题 7:找不到 claude 命令 + +### 症状 + +运行安装脚本时提示找不到 claude 命令 + +### 解决方法 + +#### 1. 检查 Claude Code CLI 是否安装 + +```bash +claude --version +``` + +#### 2. 重新安装 Claude Code CLI + +参考 Claude Code 官方文档进行安装。 + +--- + +## 问题 8:SSH 远程开发问题 + +### 症状 + +通过 SSH 连接后,MCP Server 无法触发鼠标点击 + +### 说明 + +这是 Session 隔离问题,SSH 运行在 Session 0,游戏运行在 Session 1。 + +### 解决方法 + +需要使用 Daemon 架构。 + +详见:[远程 SSH 开发指南](remote-ssh.md) + +--- + +## 获取帮助 + +如果以上方法都无法解决问题: + +1. 查看详细的错误信息和堆栈跟踪 +2. 检查日志文件 +3. 搜索或提交 GitHub Issues +4. 提供详细的复现步骤和环境信息 diff --git "a/docs/ops/\347\211\210\346\234\254\346\233\264\346\226\260.md" "b/docs/ops/\347\211\210\346\234\254\346\233\264\346\226\260.md" new file mode 100644 index 0000000000..ef575d6a6d --- /dev/null +++ "b/docs/ops/\347\211\210\346\234\254\346\233\264\346\226\260.md" @@ -0,0 +1,38 @@ +## 1.新角色/皮肤 + +- agent.py AgentEnum 增加新角色/皮肤,注意新角色名称需要与空洞里呼叫增援出现的名字一致 +- 头像截图 + - 战斗画面一般需要状态判断,需要3人组队和2人组队,新角色在各位置的状态截图(3-1 3-2 3-3 2-2) + - 触发连携技 + - 触发快速支援 + - 预备编队页面,并未进行任何拖动操作的情况下,新角色位于第一个编队的第一个位置 + - 零号空洞走格子状态,新角色位于一号位 +- call_for_support.py reject_agent增加拒绝选项 +- 呼叫增援-拒绝.yml 增加拒绝选项 +- 邂逅.yml 增加空洞事件 + +### 1.1.角色状态 + +增加后 可在[测试项目](https://github.com/DoctorReid/zzz-od-test/tree/main/test/auto_battle/agent_state_checker)增加对应测试 + +#### 1.1.1.原理 + +通常情况下,角色的状态都有一个固定的位置,在此基础上显示长条、圆点之类的内容。 + +因此,识别的最简单方法就是对固定位置进行颜色判断。 + +增加一个角色状态,需要做以下内容 + +- 在AgentEnum增加状态定义 +- 每个状态,需要截图,3人组队和2人组队各个位置的截图,根据截图在模板管理中增加对应的模板 +- 在测试项目中,增加对应的测试样例 + +#### 1.1.1.长条类-1 + +例子:青衣、莱特 + +可以通过长条的背景色,来判断未满的状态条长度,从而反推当前的状态条长度。 + +在模板管理中,增加高度为1的模板,模板记录的位置就是需要识颜色的位置。 + +识别代码见 agent_state_checker.check_length_by_background_gray diff --git "a/docs/ops/\351\224\204\345\244\247\345\234\260.md" "b/docs/ops/\351\224\204\345\244\247\345\234\260.md" new file mode 100644 index 0000000000..599e8411c5 --- /dev/null +++ "b/docs/ops/\351\224\204\345\244\247\345\234\260.md" @@ -0,0 +1,30 @@ +# 大地图录制 + +当前录制主要是为了补充大地图上的特殊图标信息,这些图标可以辅助坐标识别,并且也是当前最有效的计算坐标方法。 + +为什么靠黑白地图计算坐标的效果不好: + +1. 黑白图上信息太少,使用模板匹配很容易歪。 +2. 小地图的半透明显示,很容易受背景污染,导致没有有效的颜色范围可以处理所有情况,最终结果就是小地图的道理提取效果不稳定。 + +## 地图来源 + +待暗檬补充 + +## 录制过程 + +0. 原图是黑白+透明3通道,需要先使用 large_map_recorder_utils.__debug_read_extract 转化成 黑白2通道。 +1. 复制大地图到对应文件夹中 `assets/game_data/world_patrol/xx/yy`,取名 `road_mask.png`。 +2. 进入 `大地图录制` 界面,选择对应区域加载。 +3. 游戏里,找一个小地图上看到道路形状很不规则的位置,越不规则越好。转动视角让小地图背景尽量暗,然后截图。 +4. 调整大地图的缩放,通常是 40% 左右,但每张地图不一样。 +5. 使用 `定位` 和 `重叠`,看小地图能否和大地图完全重叠。如果不能重叠,则按取消重新加载。 +6. 重复4~5,直到找一个合适的缩放比例。保存。 +7. 到各个特殊点的地方截图,然后 `定位` -> `合并`。 +8. 如果出现未识别的地图图标,则需要截图后,从小地图上扣图,保存到 `assets/template/map/map_icon_xx/raw.png`。然后使用 `map_icon_utils` 按颜色过滤。 +9. `编辑图标` -> 给传送点(mm_icon_01)的填上具体的传送点名称。 +10. 所有图标增加后就完成了。 + +# 路线录制 + +目前路线只能尽量走有图标的地方,对于没有图标的地方,只能看运气。 diff --git a/docs/user/app/battle-assistant.md b/docs/user/app/battle-assistant.md new file mode 100644 index 0000000000..dbd70100ab --- /dev/null +++ b/docs/user/app/battle-assistant.md @@ -0,0 +1,46 @@ +# 战斗助手 + +战斗助手提供自动战斗辅助功能。 + +## 使用建议 + +为了让您的自动战斗体验更加顺畅,这里有一些使用建议: + +### 1. 画面设置 + +建议您将游戏帧数限制在 30 帧,并适当降低画质设置。如果您的电脑配置非常强大,尝试 60 帧也是可以的,但请务必不要开启「无限帧率」。 + +### 2. 释放电脑性能 + +自动战斗功能依托于 AI 视觉识别技术,需要消耗一定的计算资源。建议您在使用时尽量关闭不必要的后台程序,电脑运行越流畅,AI 的表现也会越出色。 + +### 3. 截图间隔设置 + +为了不让 AI 错过每一个精彩瞬间,请确保截图频率至少达到每秒 20 次。毕竟,我们不能强求 AI 在 1 帧的画面里完成电竞级的操作呀。 + +### 4. 按键设置建议 + +强烈建议您将连携技的「左」、「右」和「取消」设置为独立的按键,避免与普攻、闪避或终结技共用键位。特别需要强调的是,几乎所有的连携技释放错误都源自于使用了同一个按键。默认键位下,连携技出现时很容易被普攻误触,导致操作失误,分开设置会让战斗更精准。 + +例如: +- 连携左设置为 1 +- 连携右设置为 3 +- 取消设置为 2 + +### 5. 应对特殊情况 + +目前的 AI 还在学习中,暂时无法完美应对 BOSS 的转阶段或特殊状态攻击。遇到这种情况时,可能需要您稍微费心,暂停自动战斗并手动应对一下。 + +### 6. 最佳显示环境 + +为了保证识别准确,请将游戏分辨率设置为 1920x1080,并关闭 HDR 功能。同时,请确保游戏画面没有被其他窗口遮挡,让 AI 能看清全局。 + +## 功能说明 + +待补充... + +## 配置选项 + +待补充... + +祝您游戏愉快! diff --git a/pyproject.toml b/pyproject.toml index 3ee5dabcad..8de7720c66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ dev = [ "pytest-asyncio>=1.1.0", "ruff>=0.12.10", "pyuac==0.0.3", + "mcp>=1.0.0", + "psutil>=7.2.2", + "pyright>=1.1.408", ] gamepad = [ diff --git a/src/one_dragon/base/conditional_operation/loader.py b/src/one_dragon/base/conditional_operation/loader.py index 60e5d7b4ad..6c525cf589 100644 --- a/src/one_dragon/base/conditional_operation/loader.py +++ b/src/one_dragon/base/conditional_operation/loader.py @@ -6,7 +6,7 @@ from one_dragon.base.conditional_operation.operation_def import OperationDef from one_dragon.base.conditional_operation.scene import Scene from one_dragon.base.conditional_operation.state_handler import StateHandler -from one_dragon.utils import os_utils +from one_dragon.utils import os_utils, yaml_utils from one_dragon.utils.log_utils import log @@ -262,7 +262,7 @@ def load_yaml_config(sub_dir: list[str], template_name: str, read_from_merged: b with open(file_path, 'r', encoding='utf-8') as file: log.debug(f"加载yaml: {file_path}") - data = yaml.safe_load(file) + data = yaml_utils.safe_load(file) return data diff --git a/src/one_dragon/base/config/custom_config.py b/src/one_dragon/base/config/custom_config.py index a894390bca..c302b98e70 100644 --- a/src/one_dragon/base/config/custom_config.py +++ b/src/one_dragon/base/config/custom_config.py @@ -17,11 +17,6 @@ class ThemeEnum(Enum): DARK = ConfigItem('深色', 'Dark') -class ThemeColorModeEnum(Enum): - - AUTO = ConfigItem('自动', 'auto') - CUSTOM = ConfigItem('自定义', 'custom') - class BackgroundTypeEnum(Enum): @@ -129,25 +124,13 @@ def last_dynamic_background_fetch_time(self, new_value: str) -> None: self.update('last_dynamic_background_fetch_time', new_value) @property - def theme_color_mode(self) -> str: - """ - 主题色模式 - """ - return self.get('theme_color_mode', ThemeColorModeEnum.AUTO.value.value) - - @theme_color_mode.setter - def theme_color_mode(self, new_value: str) -> None: - """ - 主题色模式 - """ - self.update('theme_color_mode', new_value) + def custom_theme_color(self) -> bool: + """是否使用自定义主题色""" + return self.get('custom_theme_color', False) - @property - def is_custom_theme_color(self) -> bool: - """ - 是否使用自定义主题色 - """ - return self.theme_color_mode == ThemeColorModeEnum.CUSTOM.value.value + @custom_theme_color.setter + def custom_theme_color(self, value: bool) -> None: + self.update('custom_theme_color', value) @property def theme_color_str(self) -> str: diff --git a/src/one_dragon/base/config/one_dragon_config.py b/src/one_dragon/base/config/one_dragon_config.py index 004ea9eff8..3316a3fe58 100644 --- a/src/one_dragon/base/config/one_dragon_config.py +++ b/src/one_dragon/base/config/one_dragon_config.py @@ -1,7 +1,6 @@ import os import shutil from enum import Enum -from typing import List, Optional from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.config.yaml_config import YamlConfig @@ -39,12 +38,12 @@ class InstanceRun(Enum): class OneDragonConfig(YamlConfig): def __init__(self): - YamlConfig.__init__(self, 'one_dragon', backup_model_name='zzz_one_dragon') # TODO 2025.12时可以删掉 backup_model_name - self.instance_list: List[OneDragonInstance] = [] - self._temp_instance_indices: Optional[List[int]] = None + YamlConfig.__init__(self, 'one_dragon') + self.instance_list: list[OneDragonInstance] = [] + self._temp_instance_indices: list[int] | None = None self._init_instance_list() - def set_temp_instance_indices(self, instance_indices: Optional[List[int]]): + def set_temp_instance_indices(self, instance_indices: list[int] | None): """设置临时实例索引列表""" self._temp_instance_indices = instance_indices @@ -145,15 +144,15 @@ def delete_instance(self, instance_idx: int): self._init_instance_list() @property - def dict_instance_list(self) -> List[dict]: + def dict_instance_list(self) -> list[dict]: return self.get('instance_list', []) @dict_instance_list.setter - def dict_instance_list(self, new_list: List[dict]): + def dict_instance_list(self, new_list: list[dict]): self.update('instance_list', new_list) @property - def current_active_instance(self) -> Optional[OneDragonInstance]: + def current_active_instance(self) -> OneDragonInstance | None: """ 获取当前激活使用的账号 :return: @@ -164,7 +163,7 @@ def current_active_instance(self) -> Optional[OneDragonInstance]: return None @property - def instance_list_in_od(self) -> List[OneDragonInstance]: + def instance_list_in_od(self) -> list[OneDragonInstance]: """ 需要在一条龙中运行的实例列表 如果设置了临时实例索引,则使用临时配置 diff --git a/src/one_dragon/base/config/yaml_config.py b/src/one_dragon/base/config/yaml_config.py index 155fb4643a..2507f748eb 100644 --- a/src/one_dragon/base/config/yaml_config.py +++ b/src/one_dragon/base/config/yaml_config.py @@ -11,7 +11,7 @@ class YamlConfig(YamlOperator): def __init__( self, module_name: str, - backup_model_name: str | None = None, + backup_module_name: str | None = None, instance_idx: Optional[int] = None, sub_dir: Optional[List[str]] = None, sample: bool = False, copy_from_sample: bool = False, @@ -27,7 +27,7 @@ def __init__( self.module_name: str = module_name """配置文件名称""" - self.backup_model_name: str = backup_model_name + self.backup_module_name: str = backup_module_name """备用的配置文件名称 主要用于配置文件改名时做迁移使用""" self.is_mock: bool = is_mock @@ -72,7 +72,7 @@ def _get_yaml_file_path(self) -> Optional[str]: return yml_path # 备用文件存在时 复制使用 - backup_yml_path = os.path.join(dir_path, f'{self.backup_model_name}.yml') + backup_yml_path = os.path.join(dir_path, f'{self.backup_module_name}.yml') if os.path.exists(backup_yml_path): shutil.copyfile(backup_yml_path, yml_path) return yml_path diff --git a/src/one_dragon/base/config/yaml_operator.py b/src/one_dragon/base/config/yaml_operator.py index a715cdc06b..be7b34e2cb 100644 --- a/src/one_dragon/base/config/yaml_operator.py +++ b/src/one_dragon/base/config/yaml_operator.py @@ -4,6 +4,7 @@ import yaml +from one_dragon.utils import yaml_utils from one_dragon.utils.log_utils import log cached_yaml_data: dict[str, tuple[float, dict]] = {} @@ -28,7 +29,7 @@ def read_cache_or_load(file_path: str): with open(file_path, 'r', encoding='utf-8') as file: log.debug(f"加载yaml: {file_path}") - data = yaml.safe_load(file) + data = yaml_utils.safe_load(file) cached_yaml_data[file_path] = (last_modify, data) return data diff --git a/src/one_dragon/base/controller/controller_base.py b/src/one_dragon/base/controller/controller_base.py index 5e3f8f16b3..9643684f1f 100644 --- a/src/one_dragon/base/controller/controller_base.py +++ b/src/one_dragon/base/controller/controller_base.py @@ -45,12 +45,13 @@ def is_game_window_ready(self) -> bool: """ return False - def click(self, pos: Point = None, press_time: float = 0, pc_alt: bool = False) -> bool: + def click(self, pos: Point = None, press_time: float = 0, pc_alt: bool = False, gamepad_key: str | None = None) -> bool: """ 点击位置 :param pos: 点击位置 (x,y) 默认分辨率下的游戏窗口里的坐标 :param press_time: 大于0时长按若干秒 - :param pc_alt: 只在PC端有用 使用ALT键进行点击 + :param pc_alt: 只在PC端有用 使用ALT键进行点击(仅前台模式) + :param gamepad_key: 后台模式下用手柄按键替代点击的动作名 :return: 不在窗口区域时不点击 返回False """ pass diff --git a/src/one_dragon/base/controller/pc_button/ds4_button_controller.py b/src/one_dragon/base/controller/pc_button/ds4_button_controller.py index 5dee14b34d..4c096dcdc4 100644 --- a/src/one_dragon/base/controller/pc_button/ds4_button_controller.py +++ b/src/one_dragon/base/controller/pc_button/ds4_button_controller.py @@ -1,279 +1,107 @@ -import time - from enum import Enum -from typing import Callable, List, Optional from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.controller.pc_button import pc_button_utils -from one_dragon.base.controller.pc_button.pc_button_controller import PcButtonController +from one_dragon.base.controller.pc_button.virtual_gamepad_controller import ( + VirtualGamepadController, +) class Ds4ButtonEnum(Enum): - CROSS = ConfigItem('X', 'ds4_0') - CIRCLE = ConfigItem('○', 'ds4_1') - SQUARE = ConfigItem('□', 'ds4_2') - TRIANGLE = ConfigItem('△', 'ds4_3') - L2 = ConfigItem('L2', 'ds4_4') - R2 = ConfigItem('R2', 'ds4_5') - L1 = ConfigItem('L1', 'ds4_6') - R1 = ConfigItem('R1', 'ds4_7') - L_STICK_W = ConfigItem('左摇杆-上', 'ds4_8') - L_STICK_S = ConfigItem('左摇杆-下', 'ds4_9') - L_STICK_A = ConfigItem('左摇杆-左', 'ds4_10') - L_STICK_D = ConfigItem('左摇杆-右', 'ds4_11') - L_THUMB = ConfigItem('左摇杆-按下', 'ds4_12') - R_THUMB = ConfigItem('右摇杆-按下', 'ds4_13') - - -class Ds4ButtonController(PcButtonController): - - def __init__(self): - PcButtonController.__init__(self) - self.pad = None - if pc_button_utils.is_vgamepad_installed(): - import vgamepad as vg - self.pad = vg.VDS4Gamepad() - self._btn = vg.DS4_BUTTONS - - self._tap_handler: List[Callable[[Optional[bool], Optional[float]], None]] = [ - self.tap_a, - self.tap_b, - self.tap_x, - self.tap_y, - self.tap_lt, - self.tap_rt, - self.tap_lb, - self.tap_rb, - self.tap_l_stick_w, - self.tap_l_stick_s, - self.tap_l_stick_a, - self.tap_l_stick_d, - self.tap_l_thumb, - self.tap_r_thumb, - ] - - self.release_handler: List[Callable[[], None]] = [ - self.release_a, - self.release_b, - self.release_x, - self.release_y, - self.release_lt, - self.release_rt, - self.release_lb, - self.release_rb, - self.release_l_stick, - self.release_l_stick, - self.release_l_stick, - self.release_l_stick, - self.release_l_thumb, - self.release_r_thumb, - ] - - def tap(self, key: str) -> None: - """ - 触发按键 - :param key: - :return: - """ - if key is None: # 部分按键不支持 - return - self._tap_handler[int(key.split('_')[-1])](False, None) - - def tap_a(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_CROSS, press=press, press_time=press_time) - - def tap_b(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_CIRCLE, press=press, press_time=press_time) - - def tap_x(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_SQUARE, press=press, press_time=press_time) - - def tap_y(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_TRIANGLE, press=press, press_time=press_time) - - def tap_lt(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_trigger(value=255) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_trigger(value=0) - self.pad.update() - - def tap_rt(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.right_trigger(value=255) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.right_trigger(value=0) - self.pad.update() - - def tap_lb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_SHOULDER_LEFT, press=press, press_time=press_time) - - def tap_rb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_SHOULDER_RIGHT, press=press, press_time=press_time) - - def tap_l_stick_w(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(0, 1) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_stick_s(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(0, -1) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_stick_a(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(-1, 0) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_stick_d(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(1, 0) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_thumb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_THUMB_LEFT, press=press, press_time=press_time) - - def tap_r_thumb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.DS4_BUTTON_THUMB_RIGHT, press=press, press_time=press_time) - - def _press_button(self, btn, press: bool = False, press_time: Optional[float] = None): - """ - 按键 - :param btn: 键 - :param press: 是否按下 - :param press_time: 按下时间。如果 press=False press_time=None,则使用key_press_time;如果 press=True press=None 则不放开 - :return: - """ - self.pad.press_button(btn) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.release_button(btn) - self.pad.update() - - def reset(self): - self.pad.reset() - self.pad.update() - - def press(self, key: str, press_time: Optional[float] = None) -> None: - """ - :param key: 按键 - :param press_time: 持续按键时间 - :return: - """ - if key is None: # 部分按键不支持 + CROSS = ConfigItem('✕', 'ds4_cross') + CIRCLE = ConfigItem('○', 'ds4_circle') + SQUARE = ConfigItem('□', 'ds4_square') + TRIANGLE = ConfigItem('△', 'ds4_triangle') + L2 = ConfigItem('L2', 'ds4_l2') + R2 = ConfigItem('R2', 'ds4_r2') + L1 = ConfigItem('L1', 'ds4_l1') + R1 = ConfigItem('R1', 'ds4_r1') + L_STICK_W = ConfigItem('左摇杆-上', 'ds4_ls_up') + L_STICK_S = ConfigItem('左摇杆-下', 'ds4_ls_down') + L_STICK_A = ConfigItem('左摇杆-左', 'ds4_ls_left') + L_STICK_D = ConfigItem('左摇杆-右', 'ds4_ls_right') + L_THUMB = ConfigItem('左摇杆-按下', 'ds4_l_thumb') + R_THUMB = ConfigItem('右摇杆-按下', 'ds4_r_thumb') + DPAD_UP = ConfigItem('十字键-上', 'ds4_dpad_up') + DPAD_DOWN = ConfigItem('十字键-下', 'ds4_dpad_down') + DPAD_LEFT = ConfigItem('十字键-左', 'ds4_dpad_left') + DPAD_RIGHT = ConfigItem('十字键-右', 'ds4_dpad_right') + OPTIONS = ConfigItem('OPTIONS', 'ds4_options') + SHARE = ConfigItem('SHARE', 'ds4_share') + TOUCHPAD = ConfigItem('触控板', 'ds4_touchpad') + R_STICK_W = ConfigItem('右摇杆-上', 'ds4_rs_up') + R_STICK_S = ConfigItem('右摇杆-下', 'ds4_rs_down') + R_STICK_A = ConfigItem('右摇杆-左', 'ds4_rs_left') + R_STICK_D = ConfigItem('右摇杆-右', 'ds4_rs_right') + PS = ConfigItem('PS', 'ds4_ps') + + +class Ds4ButtonController(VirtualGamepadController): + + def __init__(self) -> None: + VirtualGamepadController.__init__(self) + if not pc_button_utils.is_vgamepad_installed(): return - self._tap_handler[int(key.split('_')[-1])](True, press_time) - - def release(self, key: str) -> None: - if key is None: # 部分按键不支持 - return - self.release_handler[int(key.split('_')[-1])]() - - def release_a(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_CROSS) - - def release_b(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_CIRCLE) - - def release_x(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_SQUARE) - - def release_y(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_TRIANGLE) - - def release_lt(self) -> None: - self.pad.left_trigger(value=0) - self.pad.update() - - def release_rt(self) -> None: - self.pad.right_trigger(value=0) - self.pad.update() - - def release_lb(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_SHOULDER_LEFT) - - def release_rb(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_SHOULDER_RIGHT) - - def release_l_stick(self) -> None: - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def release_l_thumb(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_THUMB_LEFT) - - def release_r_thumb(self) -> None: - self._release_btn(self._btn.DS4_BUTTON_THUMB_RIGHT) - def _release_btn(self, btn) -> None: - """ - 释放具体按键 - """ - self.pad.release_button(btn) - self.pad.update() + import vgamepad as vg + self.pad = vg.VDS4Gamepad() + btn = vg.DS4_BUTTONS + dpad = vg.DS4_DPAD_DIRECTIONS + special = vg.DS4_SPECIAL_BUTTONS + + # 普通按钮 + for key, const in [ + ('ds4_cross', btn.DS4_BUTTON_CROSS), + ('ds4_circle', btn.DS4_BUTTON_CIRCLE), + ('ds4_square', btn.DS4_BUTTON_SQUARE), + ('ds4_triangle', btn.DS4_BUTTON_TRIANGLE), + ('ds4_l1', btn.DS4_BUTTON_SHOULDER_LEFT), + ('ds4_r1', btn.DS4_BUTTON_SHOULDER_RIGHT), + ('ds4_l_thumb', btn.DS4_BUTTON_THUMB_LEFT), + ('ds4_r_thumb', btn.DS4_BUTTON_THUMB_RIGHT), + ('ds4_options', btn.DS4_BUTTON_OPTIONS), + ('ds4_share', btn.DS4_BUTTON_SHARE), + ]: + self._register_button(key, const) + + # 扳机 + self._register_trigger('ds4_l2', left=True) + self._register_trigger('ds4_r2', left=False) + + # DPAD(DS4 需用 directional_pad API) + none_dir = dpad.DS4_BUTTON_DPAD_NONE + for key, direction in [ + ('ds4_dpad_up', dpad.DS4_BUTTON_DPAD_NORTH), + ('ds4_dpad_down', dpad.DS4_BUTTON_DPAD_SOUTH), + ('ds4_dpad_left', dpad.DS4_BUTTON_DPAD_WEST), + ('ds4_dpad_right', dpad.DS4_BUTTON_DPAD_EAST), + ]: + self._key_bindings[key] = ( + lambda d=direction: self.pad.directional_pad(direction=d), + lambda n=none_dir: self.pad.directional_pad(direction=n), + ) + + # 特殊按钮 (PS / 触控板) + for key, sb in [ + ('ds4_ps', special.DS4_SPECIAL_BUTTON_PS), + ('ds4_touchpad', special.DS4_SPECIAL_BUTTON_TOUCHPAD), + ]: + self._key_bindings[key] = ( + lambda s=sb: self.pad.press_special_button(special_button=s), + lambda s=sb: self.pad.release_special_button(special_button=s), + ) + + # 左摇杆 + for key, x, y in [ + ('ds4_ls_up', 0, 1), ('ds4_ls_down', 0, -1), + ('ds4_ls_left', -1, 0), ('ds4_ls_right', 1, 0), + ]: + self._register_stick(key, stick='left', x=x, y=y) + + # 右摇杆 + for key, x, y in [ + ('ds4_rs_up', 0, 1), ('ds4_rs_down', 0, -1), + ('ds4_rs_left', -1, 0), ('ds4_rs_right', 1, 0), + ]: + self._register_stick(key, stick='right', x=x, y=y) diff --git a/src/one_dragon/base/controller/pc_button/keyboard_mouse_controller.py b/src/one_dragon/base/controller/pc_button/keyboard_mouse_controller.py index e0d7b2fa0f..692c03e62f 100644 --- a/src/one_dragon/base/controller/pc_button/keyboard_mouse_controller.py +++ b/src/one_dragon/base/controller/pc_button/keyboard_mouse_controller.py @@ -1,7 +1,6 @@ import time from pynput import keyboard, mouse -from typing import Optional from one_dragon.base.controller.pc_button import pc_button_utils from one_dragon.base.controller.pc_button.pc_button_controller import PcButtonController @@ -13,6 +12,7 @@ def __init__(self): PcButtonController.__init__(self) self.keyboard = keyboard.Controller() self.mouse = mouse.Controller() + self._pressed_keys: set[str] = set() # 当前按下的键 def tap(self, key: str) -> None: """ @@ -25,7 +25,7 @@ def tap(self, key: str) -> None: else: self.keyboard.tap(pc_button_utils.get_keyboard_button(key)) - def press(self, key: str, press_time: Optional[float] = None) -> None: + def press(self, key: str, press_time: float | None = None) -> None: """ :param key: 按键 :param press_time: 持续按键时间 @@ -37,6 +37,8 @@ def press(self, key: str, press_time: Optional[float] = None) -> None: self.mouse.press(real_key) else: self.keyboard.press(real_key) + if press_time is None: + self._pressed_keys.add(key) if press_time is not None: time.sleep(press_time) @@ -47,16 +49,23 @@ def press(self, key: str, press_time: Optional[float] = None) -> None: self.keyboard.release(real_key) def release(self, key: str) -> None: + if key not in self._pressed_keys: + return + self._pressed_keys.discard(key) is_mouse = pc_button_utils.is_mouse_button(key) if is_mouse: self.mouse.release(pc_button_utils.get_mouse_button(key)) else: self.keyboard.release(pc_button_utils.get_keyboard_button(key)) + def reset(self) -> None: + for key in list(self._pressed_keys): + self.release(key) + if __name__ == '__main__': _c = KeyboardMouseController() t1 = time.time() _c.press('a') _c.release('a') - print('%.4f' % (time.time() - t1)) \ No newline at end of file + print('%.4f' % (time.time() - t1)) diff --git a/src/one_dragon/base/controller/pc_button/pc_button_controller.py b/src/one_dragon/base/controller/pc_button/pc_button_controller.py index 02bdaf6184..c7543dff53 100644 --- a/src/one_dragon/base/controller/pc_button/pc_button_controller.py +++ b/src/one_dragon/base/controller/pc_button/pc_button_controller.py @@ -1,10 +1,12 @@ -from typing import Optional +import contextlib +import time class PcButtonController: - def __init__(self): + def __init__(self) -> None: self.key_press_time: float = 0.02 + self.combo_press_time: float = 0.5 def tap(self, key: str) -> None: """ @@ -12,7 +14,31 @@ def tap(self, key: str) -> None: """ pass - def press(self, key: str, press_time: Optional[float] = None) -> None: + def tap_combo(self, keys: list[str]) -> None: + """顺序按下组合键:先按修饰键等待轮盘,再按动作键,最后全部释放。 + + 例如 ['xbox_lb', 'xbox_a']: + 1) 按住 LB(不释放)→ 等待 combo_press_time(轮盘出现) + 2) 按住 A(不释放)→ 等待 key_press_time + 3) 逐个释放 + """ + if not keys: + return + pressed: list[str] = [] + try: + for i, key in enumerate(keys): + if key is not None: + self.press(key, press_time=None) # 按住不放 + pressed.append(key) + if i < len(keys) - 1: + time.sleep(self.combo_press_time) + time.sleep(self.key_press_time) + finally: + for key in reversed(pressed): + with contextlib.suppress(Exception): + self.release(key) + + def press(self, key: str, press_time: float | None = None) -> None: """ :param key: 按键 :param press_time: 持续按键时间。不传入时 代表不松开 diff --git a/src/one_dragon/base/controller/pc_button/pc_button_listener.py b/src/one_dragon/base/controller/pc_button/pc_button_listener.py index c8f4b5b081..e4587c6cf0 100644 --- a/src/one_dragon/base/controller/pc_button/pc_button_listener.py +++ b/src/one_dragon/base/controller/pc_button/pc_button_listener.py @@ -1,12 +1,10 @@ -from concurrent.futures import ThreadPoolExecutor, Future +from collections.abc import Callable +from concurrent.futures import Future, ThreadPoolExecutor from pynput import keyboard, mouse -from typing import Callable from one_dragon.utils import thread_utils -_key_mouse_btn_listener_executor = ThreadPoolExecutor(thread_name_prefix='od_key_mouse_btn_listener', max_workers=8) - class PcButtonListener: @@ -26,6 +24,8 @@ def __init__(self, self.listen_mouse: bool = listen_mouse self.listen_gamepad: bool = listen_gamepad + self._executor = ThreadPoolExecutor(thread_name_prefix='od_key_mouse_btn_listener', max_workers=8) + def _on_keyboard_press(self, event): if isinstance(event, keyboard.Key): k = event.name @@ -68,7 +68,7 @@ def _on_mouse_click(self, x, y, button: mouse.Button, pressed): def _call_button_tap_callback(self, key: str) -> None: if self.on_button_tap is not None: - future: Future = _key_mouse_btn_listener_executor.submit(self.on_button_tap, key) + future: Future = self._executor.submit(self.on_button_tap, key) future.add_done_callback(thread_utils.handle_future_result) def start(self): @@ -80,3 +80,4 @@ def start(self): def stop(self): self.keyboard_listener.stop() self.mouse_listener.stop() + self._executor.shutdown(wait=False, cancel_futures=True) diff --git a/src/one_dragon/base/controller/pc_button/virtual_gamepad_controller.py b/src/one_dragon/base/controller/pc_button/virtual_gamepad_controller.py new file mode 100644 index 0000000000..b705de2b5d --- /dev/null +++ b/src/one_dragon/base/controller/pc_button/virtual_gamepad_controller.py @@ -0,0 +1,102 @@ +"""虚拟手柄控制器基类 —— Xbox / DS4 的公共逻辑。 + +子类只需提供 ``pad`` 实例并调用 ``_register_*`` 系列方法注册按键, +基类自动构建 ``tap / press / release`` 的字符串分发。 +""" + +import time +from collections.abc import Callable + +from one_dragon.base.controller.pc_button.pc_button_controller import PcButtonController + + +class VirtualGamepadController(PcButtonController): + """数据驱动的虚拟手柄控制器。 + + 内部维护 ``_key_bindings: dict[str, tuple[activate, deactivate]]``, + ``activate()`` 将手柄状态设为"按下",``deactivate()`` 将其重置。 + ``pad.update()`` 由基类统一调用。 + """ + + def __init__(self) -> None: + PcButtonController.__init__(self) + self.pad = None + self._key_bindings: dict[str, tuple[Callable[[], None], Callable[[], None]]] = {} + + def _register_button(self, key: str, btn_const: int) -> None: + """注册普通按钮(press_button / release_button)。""" + self._key_bindings[key] = ( + lambda b=btn_const: self.pad.press_button(b), + lambda b=btn_const: self.pad.release_button(b), + ) + + def _register_trigger(self, key: str, *, left: bool) -> None: + """注册扳机(left_trigger / right_trigger)。""" + trigger = 'left_trigger' if left else 'right_trigger' + self._key_bindings[key] = ( + lambda t=trigger: getattr(self.pad, t)(value=255), + lambda t=trigger: getattr(self.pad, t)(value=0), + ) + + def _register_stick( + self, key: str, *, stick: str, x: float, y: float + ) -> None: + """注册摇杆方向。 + + Args: + key: 按键标识 + stick: ``'left'`` 或 ``'right'`` + x: 偏转 x + y: 偏转 y + """ + fn_name = f'{stick}_joystick_float' + self._key_bindings[key] = ( + lambda f=fn_name, _x=x, _y=y: getattr(self.pad, f)(_x, _y), + lambda f=fn_name: getattr(self.pad, f)(0, 0), + ) + + def _do_action( + self, + activate: Callable[[], None], + deactivate: Callable[[], None], + *, + press: bool, + press_time: float | None, + ) -> None: + """执行按键动作,统一处理按下时长和释放逻辑。""" + activate() + self.pad.update() + + if press: + if press_time is None: # 按住不放 + return + else: + if press_time is None: + press_time = self.key_press_time + + time.sleep(max(self.key_press_time, press_time)) + deactivate() + self.pad.update() + + def tap(self, key: str) -> None: + if key is None: + return + activate, deactivate = self._key_bindings[key] + self._do_action(activate, deactivate, press=False, press_time=None) + + def press(self, key: str, press_time: float | None = None) -> None: + if key is None: + return + activate, deactivate = self._key_bindings[key] + self._do_action(activate, deactivate, press=True, press_time=press_time) + + def release(self, key: str) -> None: + if key is None: + return + _, deactivate = self._key_bindings[key] + deactivate() + self.pad.update() + + def reset(self) -> None: + self.pad.reset() + self.pad.update() diff --git a/src/one_dragon/base/controller/pc_button/xbox_button_controller.py b/src/one_dragon/base/controller/pc_button/xbox_button_controller.py index b7cd52ff87..9473596d49 100644 --- a/src/one_dragon/base/controller/pc_button/xbox_button_controller.py +++ b/src/one_dragon/base/controller/pc_button/xbox_button_controller.py @@ -1,273 +1,86 @@ -import time - from enum import Enum -from typing import Callable, List, Optional from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.controller.pc_button import pc_button_utils -from one_dragon.base.controller.pc_button.pc_button_controller import PcButtonController +from one_dragon.base.controller.pc_button.virtual_gamepad_controller import ( + VirtualGamepadController, +) class XboxButtonEnum(Enum): - A = ConfigItem('A', 'xbox_0') - B = ConfigItem('B', 'xbox_1') - X = ConfigItem('X', 'xbox_2') - Y = ConfigItem('Y', 'xbox_3') - LT = ConfigItem('LT', 'xbox_4') - RT = ConfigItem('RT', 'xbox_5') - LB = ConfigItem('LB', 'xbox_6') - RB = ConfigItem('RB', 'xbox_7') - L_STICK_W = ConfigItem('左摇杆-上', 'xbox_8') - L_STICK_S = ConfigItem('左摇杆-下', 'xbox_9') - L_STICK_A = ConfigItem('左摇杆-左', 'xbox_10') - L_STICK_D = ConfigItem('左摇杆-右', 'xbox_11') - L_THUMB = ConfigItem('左摇杆-按下', 'xbox_12') - R_THUMB = ConfigItem('右摇杆-按下', 'xbox_13') - - -class XboxButtonController(PcButtonController): - - def __init__(self): - PcButtonController.__init__(self) - self.pad = None - if pc_button_utils.is_vgamepad_installed(): - import vgamepad as vg - self.pad = vg.VX360Gamepad() - self._btn = vg.XUSB_BUTTON - - self._tap_handler: List[Callable[[Optional[bool], Optional[float]], None]] = [ - self.tap_a, - self.tap_b, - self.tap_x, - self.tap_y, - self.tap_lt, - self.tap_rt, - self.tap_lb, - self.tap_rb, - self.tap_l_stick_w, - self.tap_l_stick_s, - self.tap_l_stick_a, - self.tap_l_stick_d, - self.tap_l_thumb, - self.tap_r_thumb, - ] - - self.release_handler: List[Callable[[], None]] = [ - self.release_a, - self.release_b, - self.release_x, - self.release_y, - self.release_lt, - self.release_rt, - self.release_lb, - self.release_rb, - self.release_l_stick, - self.release_l_stick, - self.release_l_stick, - self.release_l_stick, - self.release_l_thumb, - self.release_r_thumb, - ] - - def tap(self, key: str) -> None: - """ - 触发按键 - :param key: - :return: - """ - if key is None: # 部分按键不支持 - return - self._tap_handler[int(key.split('_')[-1])](False, None) - - def tap_a(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_A, press=press, press_time=press_time) - - def tap_b(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_B, press=press, press_time=press_time) - - def tap_x(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_X, press=press, press_time=press_time) - - def tap_y(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_Y, press=press, press_time=press_time) - - def tap_lt(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_trigger(value=255) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_trigger(value=0) - self.pad.update() - - def tap_rt(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.right_trigger(value=255) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.right_trigger(value=0) - self.pad.update() - - def tap_lb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_LEFT_SHOULDER, press=press, press_time=press_time) - - def tap_rb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_RIGHT_SHOULDER, press=press, press_time=press_time) - - def tap_l_stick_w(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(0, 1) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_stick_s(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(0, -1) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_stick_a(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(-1, 0) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_stick_d(self, press: bool = False, press_time: Optional[float] = None) -> None: - self.pad.left_joystick_float(1, 0) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def tap_l_thumb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_LEFT_THUMB, press=press, press_time=press_time) - - def tap_r_thumb(self, press: bool = False, press_time: Optional[float] = None) -> None: - self._press_button(self._btn.XUSB_GAMEPAD_RIGHT_THUMB, press=press, press_time=press_time) - - def _press_button(self, btn, press: bool = False, press_time: Optional[float] = None): - """ - :param btn: 按键 - :param press: 是否按下 - :param press_time: 按下时间。如果 press=False press_time=None,则使用key_press_time;如果 press=True press=None 则不放开 - :return: - """ - self.pad.press_button(btn) - self.pad.update() - - if press: - if press_time is None: # 不放开 - return - else: - if press_time is None: - press_time = self.key_press_time - - time.sleep(max(self.key_press_time, press_time)) - self.pad.release_button(btn) - self.pad.update() - - def reset(self): - self.pad.reset() - self.pad.update() - - def press(self, key: str, press_time: Optional[float] = None) -> None: - if key is None: # 部分按键不支持 + A = ConfigItem('A', 'xbox_a') + B = ConfigItem('B', 'xbox_b') + X = ConfigItem('X', 'xbox_x') + Y = ConfigItem('Y', 'xbox_y') + LT = ConfigItem('LT', 'xbox_lt') + RT = ConfigItem('RT', 'xbox_rt') + LB = ConfigItem('LB', 'xbox_lb') + RB = ConfigItem('RB', 'xbox_rb') + L_STICK_W = ConfigItem('左摇杆-上', 'xbox_ls_up') + L_STICK_S = ConfigItem('左摇杆-下', 'xbox_ls_down') + L_STICK_A = ConfigItem('左摇杆-左', 'xbox_ls_left') + L_STICK_D = ConfigItem('左摇杆-右', 'xbox_ls_right') + L_THUMB = ConfigItem('左摇杆-按下', 'xbox_l_thumb') + R_THUMB = ConfigItem('右摇杆-按下', 'xbox_r_thumb') + DPAD_UP = ConfigItem('十字键-上', 'xbox_dpad_up') + DPAD_DOWN = ConfigItem('十字键-下', 'xbox_dpad_down') + DPAD_LEFT = ConfigItem('十字键-左', 'xbox_dpad_left') + DPAD_RIGHT = ConfigItem('十字键-右', 'xbox_dpad_right') + START = ConfigItem('START', 'xbox_start') + BACK = ConfigItem('BACK', 'xbox_back') + R_STICK_W = ConfigItem('右摇杆-上', 'xbox_rs_up') + R_STICK_S = ConfigItem('右摇杆-下', 'xbox_rs_down') + R_STICK_A = ConfigItem('右摇杆-左', 'xbox_rs_left') + R_STICK_D = ConfigItem('右摇杆-右', 'xbox_rs_right') + GUIDE = ConfigItem('GUIDE', 'xbox_guide') + + +class XboxButtonController(VirtualGamepadController): + + def __init__(self) -> None: + VirtualGamepadController.__init__(self) + if not pc_button_utils.is_vgamepad_installed(): return - self._tap_handler[int(key.split('_')[-1])](True, press_time) - - def release(self, key: str) -> None: - if key is None: # 部分按键不支持 - return - self.release_handler[int(key.split('_')[-1])]() - - def release_a(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_A) - - def release_b(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_B) - - def release_x(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_X) - - def release_y(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_Y) - - def release_lt(self) -> None: - self.pad.left_trigger(value=0) - self.pad.update() - - def release_rt(self) -> None: - self.pad.right_trigger(value=0) - self.pad.update() - - def release_lb(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_LEFT_SHOULDER) - - def release_rb(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_RIGHT_SHOULDER) - - def release_l_stick(self) -> None: - self.pad.left_joystick_float(0, 0) - self.pad.update() - - def release_l_thumb(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_LEFT_THUMB) - - def release_r_thumb(self) -> None: - self._release_btn(self._btn.XUSB_GAMEPAD_RIGHT_THUMB) - def _release_btn(self, btn) -> None: - """ - 释放具体按键 - """ - self.pad.release_button(btn) - self.pad.update() + import vgamepad as vg + self.pad = vg.VX360Gamepad() + btn = vg.XUSB_BUTTON + + # 普通按钮 + for key, const in [ + ('xbox_a', btn.XUSB_GAMEPAD_A), + ('xbox_b', btn.XUSB_GAMEPAD_B), + ('xbox_x', btn.XUSB_GAMEPAD_X), + ('xbox_y', btn.XUSB_GAMEPAD_Y), + ('xbox_lb', btn.XUSB_GAMEPAD_LEFT_SHOULDER), + ('xbox_rb', btn.XUSB_GAMEPAD_RIGHT_SHOULDER), + ('xbox_l_thumb', btn.XUSB_GAMEPAD_LEFT_THUMB), + ('xbox_r_thumb', btn.XUSB_GAMEPAD_RIGHT_THUMB), + ('xbox_dpad_up', btn.XUSB_GAMEPAD_DPAD_UP), + ('xbox_dpad_down', btn.XUSB_GAMEPAD_DPAD_DOWN), + ('xbox_dpad_left', btn.XUSB_GAMEPAD_DPAD_LEFT), + ('xbox_dpad_right', btn.XUSB_GAMEPAD_DPAD_RIGHT), + ('xbox_start', btn.XUSB_GAMEPAD_START), + ('xbox_back', btn.XUSB_GAMEPAD_BACK), + ('xbox_guide', btn.XUSB_GAMEPAD_GUIDE), + ]: + self._register_button(key, const) + + # 扳机 + self._register_trigger('xbox_lt', left=True) + self._register_trigger('xbox_rt', left=False) + + # 左摇杆 + for key, x, y in [ + ('xbox_ls_up', 0, 1), ('xbox_ls_down', 0, -1), + ('xbox_ls_left', -1, 0), ('xbox_ls_right', 1, 0), + ]: + self._register_stick(key, stick='left', x=x, y=y) + + # 右摇杆 + for key, x, y in [ + ('xbox_rs_up', 0, 1), ('xbox_rs_down', 0, -1), + ('xbox_rs_left', -1, 0), ('xbox_rs_right', 1, 0), + ]: + self._register_stick(key, stick='right', x=x, y=y) diff --git a/src/one_dragon/base/controller/pc_controller_base.py b/src/one_dragon/base/controller/pc_controller_base.py index 9e591fe77b..3dc35f1310 100644 --- a/src/one_dragon/base/controller/pc_controller_base.py +++ b/src/one_dragon/base/controller/pc_controller_base.py @@ -1,8 +1,12 @@ +import contextlib import ctypes import time from functools import lru_cache import pyautogui +import win32api +import win32con +import win32gui from cv2.typing import MatLike from pynput import keyboard @@ -48,6 +52,10 @@ def __init__(self, self.btn_controller: PcButtonController = self.keyboard_controller self.screenshot_controller: PcScreenshotController = PcScreenshotController(self.game_win, standard_width, standard_height) self.screenshot_method: str = screenshot_method + self.background_mode: bool = False + self.mouse_flash_duration: float = 0.05 # 闪切键鼠模式时每步等待时长 + self.gamepad_action_keys: dict[str, list[str]] = {} + self._game_input_mode: str = 'keyboard_mouse' # 游戏当前识别的输入设备 def init_game_win(self) -> bool: """ @@ -65,13 +73,15 @@ def init_game_win(self) -> bool: def init_before_context_run(self) -> bool: pyautogui.FAILSAFE = False # 禁用 Fail-Safe,防止鼠标接近屏幕的边缘或角落时报错 self.init_game_win() - self.game_win.active() + if not self.background_mode: + self.game_win.active() return True def cleanup_after_app_shutdown(self) -> None: """ 清理资源 """ + self.btn_controller.reset() self.screenshot_controller.cleanup() def active_window(self) -> None: @@ -79,12 +89,14 @@ def active_window(self) -> None: 前置窗口 """ self.game_win.init_win() - self.game_win.active() + if not self.background_mode: + self.game_win.active() def set_window_title(self, new_title: str) -> None: - """ - 设置窗口标题 - :param new_title: 新的窗口标题 + """设置窗口标题。 + + Args: + new_title: 新的窗口标题 """ self.game_win.update_win_title(new_title) @@ -105,21 +117,195 @@ def enable_ds4(self): def enable_keyboard(self): self.btn_controller = self.keyboard_controller + def btn_tap(self, key: str) -> None: + """按键(tap)。后台模式下先发 WM_ACTIVATE 再确保手柄输入模式。""" + if self.background_mode: + self._send_activate() + self._ensure_gamepad_mode() + self.btn_controller.tap(key) + + def btn_press(self, key: str, press_time: float | None = None) -> None: + """按住键。后台模式下先发 WM_ACTIVATE 再确保手柄输入模式。""" + if self.background_mode: + self._send_activate() + self._ensure_gamepad_mode() + self.btn_controller.press(key, press_time) + + def btn_release(self, key: str) -> None: + """释放键。""" + self.btn_controller.release(key) + @property def is_game_window_ready(self) -> bool: + """游戏窗口是否已经准备好了。""" + return self.game_win.is_win_valid + + def close_game(self) -> None: + win = self.game_win.get_win() + if win is None: + return + try: + win.close() + log.info('关闭游戏成功') + except Exception: + log.error('关闭游戏失败', exc_info=True) + + def get_screenshot(self, independent: bool = False) -> MatLike | None: + if self.is_game_window_ready: + # 确保截图器已初始化 + if not independent and self.screenshot_controller.active_strategy_name is None: + self.screenshot_controller.init_screenshot(self.screenshot_method) + return self.screenshot_controller.get_screenshot(independent) + else: + raise RuntimeError('游戏窗口未就绪') + + def enable_foreground_mode(self) -> None: """ - 游戏窗口是否已经准备好了 - :return: + 启用前台模式 (默认): + - 鼠标点击 → pyautogui + - 按键操作 → 键盘 (pynput) """ - return self.game_win.is_win_valid + self.background_mode = False + self._game_input_mode = 'keyboard_mouse' + self.enable_keyboard() + log.info('已启用前台模式: pyautogui 点击 + 键盘') + + def enable_background_mode(self, gamepad_type: str = 'xbox') -> None: + """ + 启用后台模式: + - 鼠标点击 → PostMessage (WM_ACTIVATE + PostMessage) + - 按键操作 → 虚拟手柄 (vgamepad) + - gamepad_key 场景 → 手柄按键替代 + 需要先安装 ViGEmBus 驱动和 vgamepad 包。 + + Args: + gamepad_type: 'xbox' 或 'ds4' + """ + if not pc_button_utils.is_vgamepad_installed(): + log.error('启用后台模式失败: 未检测到 vgamepad/ViGEmBus') + self.background_mode = False + self._game_input_mode = 'keyboard_mouse' + self.enable_keyboard() + return + + self.background_mode = True + if gamepad_type == 'ds4': + self.enable_ds4() + log.info('已启用后台模式: PostMessage 点击 + DS4 手柄') + else: + self.enable_xbox() + log.info('已启用后台模式: PostMessage 点击 + Xbox 手柄') + + def _ensure_mouse_mode(self) -> None: + """确保游戏处于键鼠输入模式。 + + 游戏使用 Raw Input 检测设备类型,只有窗口在前台时才处理鼠标输入。 + 因此需要极短暂地将游戏窗口切到前台、发送鼠标移动、再切回。 + """ + if self._game_input_mode == 'keyboard_mouse': + return + + hwnd = self.game_win.get_hwnd() + if hwnd is None: + return - def click(self, pos: Point = None, press_time: float = 0, pc_alt: bool = False) -> bool: + user32 = ctypes.windll.user32 + prev_hwnd = win32gui.GetForegroundWindow() + + try: + win32gui.SetForegroundWindow(hwnd) + except Exception: + try: + # ALT 技巧: 先按 ALT 解除系统对 SetForegroundWindow 的限制 + user32.keybd_event(0x12, 0, 0, 0) # VK_MENU down + user32.keybd_event(0x12, 0, 2, 0) # VK_MENU up + win32gui.SetForegroundWindow(hwnd) + except Exception: + log.warning('切换前台失败,无法切回键鼠模式') + return + time.sleep(self.mouse_flash_duration) + + # mouse_event 鼠标移动,触发 Raw Input 让游戏切回键鼠 + user32.mouse_event(self.MOUSEEVENTF_MOVE, 2, 0, 0, 0) + time.sleep(self.mouse_flash_duration) + + # 切回原来的前台窗口 + if prev_hwnd and prev_hwnd != hwnd: + with contextlib.suppress(Exception): + win32gui.SetForegroundWindow(prev_hwnd) + time.sleep(0.1) + self._game_input_mode = 'keyboard_mouse' + + def _ensure_gamepad_mode(self) -> None: + """确保游戏处于手柄输入模式。若当前为键鼠模式,先发一次手柄按键让游戏切换。""" + if self._game_input_mode == 'gamepad': + return + self.btn_controller.tap(self._get_switch_gamepad_key()) + time.sleep(0.1) + self._game_input_mode = 'gamepad' + + def _get_switch_gamepad_key(self) -> str: + """获取用于切换到手柄模式的按键(使用右摇杆上推,最不易产生副作用)。""" + if isinstance(self.btn_controller, Ds4ButtonController): + return 'ds4_rs_up' + return 'xbox_rs_up' + + def _send_activate(self) -> bool: + """发送 WM_ACTIVATE(WA_ACTIVE) 到游戏窗口。 + + 让游戏认为自己被激活,但不实际改变前台窗口。 + 后台 PostMessage 点击前必须调用。 + + Returns: + 是否成功 + """ + hwnd = self.game_win.get_hwnd() + if hwnd is None: + log.error('游戏窗口未就绪,无法发送 WM_ACTIVATE') + return False + try: + win32gui.SendMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) + time.sleep(0.01) + return True + except Exception: + log.error('发送 WM_ACTIVATE 失败', exc_info=True) + return False + + def _set_cursor_to(self, hwnd: int, cx: int, cy: int) -> None: + """将物理光标移动到窗口客户区坐标 (cx, cy) 对应的屏幕位置。""" + screen_x, screen_y = win32gui.ClientToScreen(hwnd, (cx, cy)) + win32api.SetCursorPos((screen_x, screen_y)) + + def click(self, pos: Point = None, press_time: float = 0, pc_alt: bool = False, gamepad_key: str | None = None) -> bool: + """点击位置。 + + Args: + pos: 游戏中的位置 (x,y) + press_time: 大于0时长按若干秒 + pc_alt: 只在PC端有用 使用ALT键进行点击 + gamepad_key: 后台模式下用手柄按键替代点击的动作名 + + Returns: + 不在窗口区域时不点击 返回False """ - 点击位置 - :param pos: 游戏中的位置 (x,y) - :param press_time: 大于0时长按若干秒 - :param pc_alt: 只在PC端有用 使用ALT键进行点击 - :return: 不在窗口区域时不点击 返回False + if self.background_mode: + if gamepad_key: + return self._gamepad_click(gamepad_key) + return self._background_click(pos, press_time) + + return self._foreground_click(pos, press_time, pc_alt) + + + def _foreground_click(self, pos: Point | None, press_time: float = 0, pc_alt: bool = False) -> bool: + """前台点击:通过 pyautogui 点击,可选 ALT 解锁光标。 + + Args: + pos: 游戏中的位置 (x,y),None 时使用当前鼠标位置 + press_time: 大于0时长按 + pc_alt: 是否先按住 ALT 再点击 + + Returns: + 是否成功 """ click_pos: Point if pos is not None: @@ -138,46 +324,106 @@ def click(self, pos: Point = None, press_time: float = 0, pc_alt: bool = False) self.keyboard_controller.keyboard.release(keyboard.Key.alt) return True - def get_screenshot(self, independent: bool = False) -> MatLike | None: - if self.is_game_window_ready: - # 确保截图器已初始化 - if not independent and self.screenshot_controller.active_strategy_name is None: - self.screenshot_controller.init_screenshot(self.screenshot_method) - return self.screenshot_controller.get_screenshot(independent) - else: - raise RuntimeError('游戏窗口未就绪') + def _gamepad_click(self, gamepad_key: str | None) -> bool: + """后台模式下使用手柄按键替代点击。 + + 仅在后台模式且 gamepad_key 不为空时执行手柄按键。 + gamepad_key 是 GamepadActionEnum 的动作名 (如 'compendium'), + 通过 self.gamepad_action_keys 解析为实际按键列表 (如 ['xbox_lb', 'xbox_a'])。 - def scroll(self, down: int, pos: Point = None): + Args: + gamepad_key: GamepadActionEnum 的存储值(动作名),由控制器解析为实际按键 + + Returns: + True 表示已用手柄替代,False 表示未替代 """ - 向下滚动 - :param down: 负数时为相上滚动 - :param pos: 滚动位置 默认分辨率下的游戏窗口里的坐标 - :return: + if not self.background_mode or not gamepad_key: + return False + + raw_keys = self.gamepad_action_keys.get(gamepad_key, []) + if not raw_keys: + log.error(f'后台模式: 未找到动作 {gamepad_key} 的手柄键映射') + return False + + self._ensure_gamepad_mode() + self._send_activate() + + try: + if len(raw_keys) == 1: + self.btn_controller.tap(raw_keys[0]) + else: + self.btn_controller.tap_combo(raw_keys) + return True + except KeyError: + log.error(f'后台模式: 动作 {gamepad_key} 包含未注册手柄键 {raw_keys}', exc_info=True) + return False + + def _background_click(self, pos: Point | None, press_time: float = 0) -> bool: + """后台点击:用 SetCursorPos 移动光标,再 PostMessage WM_LBUTTONDOWN/UP。 + + Args: + pos: 游戏中的位置 (x,y),None 时不移动光标 + press_time: 大于0时长按 + + Returns: + 是否成功 """ - if pos is None: - pos = get_current_mouse_pos() - win_pos = self.game_win.game2win_pos(pos) - if win_pos is None: - log.error('滚动位置不在游戏窗口区域 (%s)', pos) - return - win_scroll(down, win_pos) + self._ensure_mouse_mode() - def drag_to(self, end: Point, start: Point = None, duration: float = 0.5): + hwnd = self.game_win.get_hwnd() + if hwnd is None: + log.error('游戏窗口未就绪,无法后台点击') + return False + + if pos is not None: + scaled_pos = self.game_win.get_scaled_game_pos(pos) + if scaled_pos is None: + log.error('点击非游戏窗口区域 (%s)', pos) + return False + self._set_cursor_to(hwnd, int(scaled_pos.x), int(scaled_pos.y)) + time.sleep(0.01) + + try: + win32gui.SendMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) + time.sleep(0.01) + + win32gui.PostMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, 0) + if press_time > 0: + time.sleep(press_time) + else: + time.sleep(0.02) + + win32gui.PostMessage(hwnd, win32con.WM_LBUTTONUP, 0, 0) + return True + except Exception: + log.error('后台点击失败', exc_info=True) + return False + + def drag_to(self, start: Point, end: Point, duration: float = 0.5) -> None: + """按住拖拽。 + + Args: + end: 拖拽目的点 + start: 拖拽开始点 + duration: 拖拽持续时间 """ - 按住拖拽 - :param end: 拖拽目的点 - :param start: 拖拽开始点 - :param duration: 拖拽持续时间 - :return: + if self.background_mode: + return self._background_drag(start, end, duration) + + return self._foreground_drag(start, end, duration) + + def _foreground_drag(self, start: Point, end: Point, duration: float = 0.5) -> None: + """前台拖拽:通过 pyautogui 按住拖动。 + + Args: + start: 拖拽起点(游戏坐标) + end: 拖拽终点(游戏坐标) + duration: 拖拽持续时间 """ - from_pos: Point - if start is None: - from_pos = get_current_mouse_pos() - else: - from_pos = self.game_win.game2win_pos(start) - if from_pos is None: - log.error('拖拽起点不在游戏窗口区域 (%s)', start) - return + from_pos = self.game_win.game2win_pos(start) + if from_pos is None: + log.error('拖拽起点不在游戏窗口区域 (%s)', start) + return to_pos = self.game_win.game2win_pos(end) if to_pos is None: @@ -185,29 +431,86 @@ def drag_to(self, end: Point, start: Point = None, duration: float = 0.5): return drag_mouse(from_pos, to_pos, duration=duration) - def close_game(self): - """ - 关闭游戏 - :return: + def _background_drag(self, start: Point, end: Point, duration: float = 0.5) -> None: + """后台拖拽:用 SetCursorPos 移动光标,配合 PostMessage WM_LBUTTONDOWN/UP。 + + Args: + start: 拖拽起点(游戏坐标) + end: 拖拽终点(游戏坐标) + duration: 拖拽持续时间 + + Returns: + 是否成功 """ - win = self.game_win.get_win() - if win is None: + self._ensure_mouse_mode() + + hwnd = self.game_win.get_hwnd() + if hwnd is None: + log.error('游戏窗口未就绪,无法后台拖拽') + return + + # 转换起点坐标 + scaled_start = self.game_win.get_scaled_game_pos(start) + if scaled_start is None: + log.error('拖拽起点不在游戏窗口区域 (%s)', start) return + sx, sy = int(scaled_start.x), int(scaled_start.y) + + # 转换终点坐标 + scaled_end = self.game_win.get_scaled_game_pos(end) + if scaled_end is None: + log.error('拖拽终点不在游戏窗口区域 (%s)', end) + return + ex, ey = int(scaled_end.x), int(scaled_end.y) + try: - win.close() - log.info('关闭游戏成功') + win32gui.SendMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0) + time.sleep(0.01) + + # 移动到起点并按下 + self._set_cursor_to(hwnd, sx, sy) + time.sleep(0.01) + win32gui.PostMessage(hwnd, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, 0) + time.sleep(0.02) + + # 分步移动到终点 + steps = max(int(duration / 0.02), 5) + for i in range(1, steps + 1): + t = i / steps + cx = int(sx + (ex - sx) * t) + cy = int(sy + (ey - sy) * t) + self._set_cursor_to(hwnd, cx, cy) + time.sleep(duration / steps) + + # 松开 + win32gui.PostMessage(hwnd, win32con.WM_LBUTTONUP, 0, 0) except Exception: - log.error('关闭游戏失败', exc_info=True) + log.error('后台拖拽失败', exc_info=True) - def input_str(self, to_input: str, interval: float = 0.1): + def scroll(self, down: int, pos: Point = None) -> None: + """向下滚动。 + + Args: + down: 负数时为向上滚动 + pos: 滚动位置 默认分辨率下的游戏窗口里的坐标 """ - 输入文本 需要自己先选择好输入框 - :param to_input: 文本 - :return: + if pos is None: + pos = get_current_mouse_pos() + win_pos = self.game_win.game2win_pos(pos) + if win_pos is None: + log.error('滚动位置不在游戏窗口区域 (%s)', pos) + return + win_scroll(down, win_pos) + + def input_str(self, to_input: str, interval: float = 0.1) -> None: + """输入文本 需要自己先选择好输入框。 + + Args: + to_input: 文本 """ self.keyboard_controller.keyboard.type(to_input) - def mouse_move(self, game_pos: Point): + def mouse_move(self, game_pos: Point) -> None: """ 鼠标移动到指定的位置 """ @@ -222,12 +525,12 @@ def center_point(self) -> Point: def win_click(pos: Point = None, press_time: float = 0, primary: bool = True): - """ - 点击鼠标 - :param pos: 屏幕坐标 - :param press_time: 按住时间 - :param primary: 是否点击鼠标主要按键(通常是左键) - :return: + """点击鼠标。 + + Args: + pos: 屏幕坐标 + press_time: 按住时间 + primary: 是否点击鼠标主要按键(通常是左键) """ btn = pyautogui.PRIMARY if primary else pyautogui.SECONDARY if pos is None: @@ -242,11 +545,11 @@ def win_click(pos: Point = None, press_time: float = 0, primary: bool = True): def win_scroll(clicks: int, pos: Point = None): - """ - 向下滚动 - :param clicks: 负数时为相上滚动 - :param pos: 滚动位置 不传入时为鼠标当前位置 - :return: + """向下滚动。 + + Args: + clicks: 负数时为向上滚动 + pos: 滚动位置 不传入时为鼠标当前位置 """ if pos is not None: pyautogui.moveTo(pos.x, pos.y) @@ -256,10 +559,7 @@ def win_scroll(clicks: int, pos: Point = None): @lru_cache def get_mouse_sensitivity(): - """ - 获取鼠标灵敏度 - :return: - """ + """获取鼠标灵敏度。""" user32 = ctypes.windll.user32 speed = ctypes.c_int() user32.SystemParametersInfoA(0x0070, 0, ctypes.byref(speed), 0) @@ -267,21 +567,18 @@ def get_mouse_sensitivity(): def drag_mouse(start: Point, end: Point, duration: float = 0.5): - """ - 按住鼠标左键进行画面拖动 - :param start: 原位置 - :param end: 拖动位置 - :param duration: 拖动鼠标到目标位置,持续秒数 - :return: + """按住鼠标左键进行画面拖动。 + + Args: + start: 原位置 + end: 拖动位置 + duration: 拖动鼠标到目标位置,持续秒数 """ pyautogui.moveTo(start.x, start.y) # 将鼠标移动到起始位置 pyautogui.dragTo(end.x, end.y, duration=duration) def get_current_mouse_pos() -> Point: - """ - 获取鼠标当前坐标 - :return: - """ + """获取鼠标当前坐标。""" pos = pyautogui.position() return Point(pos.x, pos.y) diff --git a/src/one_dragon/base/cv_process/cv_service.py b/src/one_dragon/base/cv_process/cv_service.py index b81f956cd4..5770eb4a61 100644 --- a/src/one_dragon/base/cv_process/cv_service.py +++ b/src/one_dragon/base/cv_process/cv_service.py @@ -16,7 +16,7 @@ CvStepCropByArea, CvStepCropToAnnulus, CvTemplateMatchingStep ) from one_dragon.base.operation.one_dragon_context import OneDragonContext -from one_dragon.utils import os_utils +from one_dragon.utils import os_utils, yaml_utils class CvService: @@ -122,7 +122,7 @@ def load_pipeline(self, name: str) -> CvPipeline | None: with open(file_path, 'r', encoding='utf-8') as f: try: - pipeline_data = yaml.safe_load(f) + pipeline_data = yaml_utils.safe_load(f) except yaml.YAMLError: return None @@ -218,4 +218,4 @@ def rename_template_contour(self, old_name: str, new_name: str): old_file_path = os.path.join(self.TEMPLATE_DIR, f"{old_name}.npy") new_file_path = os.path.join(self.TEMPLATE_DIR, f"{new_name}.npy") if os.path.exists(old_file_path) and not os.path.exists(new_file_path): - os.rename(old_file_path, new_file_path) \ No newline at end of file + os.rename(old_file_path, new_file_path) diff --git a/src/one_dragon/base/operation/application/application_run_context.py b/src/one_dragon/base/operation/application/application_run_context.py index db09f80a37..1a3197e566 100644 --- a/src/one_dragon/base/operation/application/application_run_context.py +++ b/src/one_dragon/base/operation/application/application_run_context.py @@ -472,3 +472,15 @@ def check_and_update_all_run_record(self, instance_idx: int) -> None: except Exception: # 部分应用没有运行记录 跳过即可 pass + + def after_app_shutdown(self) -> None: + """ + 整个脚本运行结束后的清理 + + 关闭应用运行上下文,包括停止当前运行任务、清除运行状态。 + """ + # 首先停止当前运行的应用,清除运行状态 + self.stop_running() + + # 关闭执行器 + self._executor.shutdown(wait=False, cancel_futures=True) diff --git a/src/one_dragon/base/operation/application_base.py b/src/one_dragon/base/operation/application_base.py index 959d678a3b..651325b9e1 100644 --- a/src/one_dragon/base/operation/application_base.py +++ b/src/one_dragon/base/operation/application_base.py @@ -116,3 +116,10 @@ def next_execution_desc(self) -> str: @staticmethod def get_preheat_executor() -> ThreadPoolExecutor: return _app_preheat_executor + + @staticmethod + def after_app_shutdown() -> None: + """ + 整个脚本运行结束后的清理 + """ + _app_preheat_executor.shutdown(wait=False, cancel_futures=True) diff --git a/src/one_dragon/base/operation/one_dragon_context.py b/src/one_dragon/base/operation/one_dragon_context.py index ab2b8ece6d..ced50e5f82 100644 --- a/src/one_dragon/base/operation/one_dragon_context.py +++ b/src/one_dragon/base/operation/one_dragon_context.py @@ -256,11 +256,13 @@ def init(self) -> None: self.push_service.init_push_channels() - self.gh_proxy_service.update_proxy_url() + # 只有在配置了 ghproxy 代理时才更新代理地址 + if self.env_config.is_gh_proxy: + self.gh_proxy_service.update_proxy_url() self.init_others() except Exception: - log.error('识别连携技出错', exc_info=True) + log.error('初始化出错', exc_info=True) finally: self._init_lock.release() @@ -409,3 +411,7 @@ def after_app_shutdown(self) -> None: StateRecordService.after_app_shutdown() from one_dragon.utils import gpu_executor gpu_executor.shutdown(wait=False) + from one_dragon.base.operation.application_base import Application + Application.after_app_shutdown() + self.run_context.after_app_shutdown() + self.push_service.after_app_shutdown() diff --git a/src/one_dragon/base/operation/operation.py b/src/one_dragon/base/operation/operation.py index f716aa6c69..7245c9b403 100644 --- a/src/one_dragon/base/operation/operation.py +++ b/src/one_dragon/base/operation/operation.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from one_dragon.base.operation.one_dragon_context import OneDragonContext + class NodeStateProxy: """ 一个代理类,用于安全、便捷地访问节点的静态信息和动态执行结果。 @@ -775,6 +776,7 @@ def round_by_find_and_click_area( screen: MatLike | None = None, screen_name: str | None = None, area_name: str | None = None, + pre_delay: float = 0.3, success_wait: float | None = None, success_wait_round: float | None = None, retry_wait: float | None = None, @@ -789,6 +791,7 @@ def round_by_find_and_click_area( screen: 截图图像。默认为None(将截取新截图)。 screen_name: 屏幕名称。默认为None。 area_name: 区域名称。默认为None。 + pre_delay: 点击前等待时间(秒)。默认为0.3秒。 success_wait: 成功后等待时间(秒)。默认为None。 success_wait_round: 成功后等待直到轮次时间达到此值,如果设置了success_wait则忽略。默认为None。 retry_wait: 失败后等待时间(秒)。默认为None。 @@ -840,6 +843,7 @@ def round_by_find_and_click_area( if not any_found: return self.round_success(status=area_name, wait=success_wait, wait_round_time=success_wait_round) + time.sleep(pre_delay) click = screen_utils.find_and_click_area( ctx=self.ctx, screen=screen, @@ -948,8 +952,35 @@ def round_by_find_area_binary( else: return self.round_retry(status=f'未找到 {area_name}', wait=retry_wait, wait_round_time=retry_wait_round) + def scroll_area( + self, + screen_name: str | None = None, + area_name: str | None = None, + direction: str = 'down', + start_ratio: float = 0.9, + end_ratio: float = 0.1, + ) -> None: + """在指定区域内滚动屏幕。 + + Args: + screen_name: 屏幕名称。默认为None。 + area_name: 区域名称。默认为None。 + direction: 滚动方向,'down' 表示往下滚(从下往上滑),'up' 表示往上滚(从上往下滑) + start_ratio: 起始位置比例(距顶部的比例)。默认0.9,即区域底部10%处 + end_ratio: 结束位置比例(距顶部的比例)。默认0.1,即区域顶部10%处 + """ + if screen_name is None or area_name is None: + return + + area = self.ctx.screen_loader.get_area(screen_name, area_name) + if area is None: + return + + screen_utils.scroll_area(self.ctx, area, direction, start_ratio, end_ratio) + def round_by_click_area( self, screen_name: str, area_name: str, click_left_top: bool = False, + pre_delay: float = 0.0, success_wait: float | None = None, success_wait_round: float | None = None, retry_wait: float | None = None, retry_wait_round: float | None = None ) -> OperationRoundResult: @@ -959,6 +990,7 @@ def round_by_click_area( screen_name: 屏幕名称。 area_name: 区域名称。 click_left_top: 是否点击左上角。默认为False。 + pre_delay: 点击前等待时间(秒)。默认为0.0秒。 success_wait: 成功后等待时间(秒)。默认为None。 success_wait_round: 成功后等待直到轮次时间达到此值,如果设置了success_wait则忽略。默认为None。 retry_wait: 失败后等待时间(秒)。默认为None。 @@ -975,7 +1007,8 @@ def round_by_click_area( to_click = area.left_top else: to_click = area.center - click = self.ctx.controller.click(pos=to_click, pc_alt=area.pc_alt) + time.sleep(pre_delay) + click = self.ctx.controller.click(pos=to_click, pc_alt=area.pc_alt, gamepad_key=area.gamepad_key) if click: self.update_screen_after_operation(screen_name, area_name) return self.round_success(status=area_name, wait=success_wait, wait_round_time=success_wait_round) @@ -988,6 +1021,7 @@ def round_by_ocr_and_click( target_cn: str, area: Optional[ScreenArea] = None, lcs_percent: float = 0.5, + pre_delay: float = 0.3, success_wait: float | None = None, success_wait_round: float | None = None, retry_wait: float | None = None, @@ -995,6 +1029,7 @@ def round_by_ocr_and_click( color_range: list[list[int]] | None = None, offset: Point | None = None, crop_first: bool = True, + remove_whitespace: bool = False, ) -> OperationRoundResult: """使用OCR在区域内查找目标文本并点击。 @@ -1004,12 +1039,14 @@ def round_by_ocr_and_click( area: 要搜索的目标区域。默认为None(搜索整个屏幕)。 crop_first: 在传入区域时 是否先裁剪再进行文本识别 lcs_percent: 文本匹配阈值。默认为0.5。 + pre_delay: 点击前等待时间(秒)。默认为0.3秒。 success_wait: 成功后等待时间(秒)。默认为None。 success_wait_round: 成功后等待直到轮次时间达到此值,如果设置了success_wait则忽略。默认为None。 retry_wait: 失败后等待时间(秒)。默认为None。 retry_wait_round: 失败后等待直到轮次时间达到此值,如果设置了retry_wait则忽略。默认为None。 color_range: 文本匹配的颜色范围。默认为None。 offset: 点击位置的偏移量。默认为None。 + remove_whitespace: 文本匹配前是否清洗空白字符。默认为False。 Returns: OperationRoundResult: 点击结果。 @@ -1022,6 +1059,15 @@ def round_by_ocr_and_click( color_range=color_range, crop_first=crop_first, ) + if remove_whitespace: + # 移除空白字符,提升文本匹配兼容性 + target_cn = str_utils.remove_whitespace(target_cn) + # 清理OCR结果字典的键:移除所有键中的空白字符 + # 若清理后出现重复键,后遍历到的键值对会覆盖先遍历到的 + ocr_result_map = { + str_utils.remove_whitespace(key): val + for key, val in ocr_result_map.items() + } to_click: Point | None = None ocr_result_list: list[str] = [] @@ -1051,6 +1097,7 @@ def round_by_ocr_and_click( if offset is not None: to_click = to_click + offset + time.sleep(pre_delay) click = self.ctx.controller.click(to_click) if click: return self.round_success(target_cn, wait=success_wait, wait_round_time=success_wait_round) @@ -1063,6 +1110,7 @@ def round_by_ocr_and_click_by_priority( screen: MatLike | None = None, ignore_cn_list: list[str] | None = None, area: Optional[ScreenArea] = None, + pre_delay: float = 0.3, success_wait: float | None = None, success_wait_round: float | None = None, retry_wait: float | None = None, @@ -1079,6 +1127,7 @@ def round_by_ocr_and_click_by_priority( ignore_cn_list: 要忽略的文本列表。目标列表中的某些元素仅用于防止匹配错误,例如["领取", "已领取"]可以防止"已领取*1"匹配到"领取",而"已领取"不需要实际匹配。默认为None。 area: 要搜索的目标区域。默认为None。 crop_first: 在传入区域时 是否先裁剪再进行文本识别 + pre_delay: 点击前等待时间(秒)。默认为0.3秒。 success_wait: 成功后等待时间(秒)。默认为None。 success_wait_round: 成功后等待直到轮次时间达到此值,如果设置了success_wait则忽略。默认为None。 retry_wait: 失败后等待时间(秒)。默认为None。 @@ -1112,11 +1161,91 @@ def round_by_ocr_and_click_by_priority( if offset is not None: to_click = to_click + offset + time.sleep(pre_delay) self.ctx.controller.click(to_click) return self.round_success(status=match_word, wait=success_wait, wait_round_time=success_wait_round) return self.round_retry(status='未匹配到目标文本', wait=retry_wait, wait_round_time=retry_wait_round) + def round_by_ocr_and_click_with_action( + self, + target_action_list: list[tuple[str, OperationRoundResultEnum]], + screen: MatLike | None = None, + area: ScreenArea | None = None, + pre_delay: float = 0.3, + success_wait: float | None = None, + success_wait_round: float | None = None, + wait_wait: float | None = None, + wait_wait_round: float | None = None, + retry_wait: float | None = None, + retry_wait_round: float | None = None, + color_range: list[list[int]] | None = None, + offset: Point | None = None, + crop_first: bool = True, + ) -> OperationRoundResult: + """使用OCR按优先级查找文本并点击,支持为不同目标指定不同的返回动作。 + + Args: + target_action_list: 目标文本和动作的元组列表。列表顺序决定优先级。 + 每个元组为 (目标文本, OperationRoundResultEnum)。 + 支持的动作: SUCCESS(进入下一节点)、WAIT(继续当前节点)、RETRY(重试)。 + 示例: [('出战', OperationRoundResultEnum.SUCCESS), ('下一步', OperationRoundResultEnum.WAIT)] + screen: 游戏截图。默认为None(使用 last_screenshot)。 + area: 要搜索的目标区域。默认为None(搜索整个屏幕)。 + crop_first: 在传入区域时 是否先裁剪再进行文本识别。默认为True。 + pre_delay: 点击前等待时间(秒)。默认为0.3秒。 + success_wait: 匹配到 SUCCESS 动作后等待时间(秒)。默认为None。 + success_wait_round: 匹配到 SUCCESS 动作后等待直到轮次时间达到此值。默认为None。 + wait_wait: 匹配到 WAIT 动作后等待时间(秒)。默认为None。 + wait_wait_round: 匹配到 WAIT 动作后等待直到轮次时间达到此值。默认为None。 + retry_wait: 未匹配到任何目标时等待时间(秒)。默认为None。 + retry_wait_round: 未匹配到任何目标时等待直到轮次时间达到此值。默认为None。 + color_range: 文本匹配的颜色范围。默认为None。 + offset: 点击位置的偏移量。默认为None。 + + Returns: + OperationRoundResult: 根据匹配目标返回对应的结果类型。 + """ + if screen is None: + screen = self.last_screenshot + + if color_range is None and area is not None: + color_range = area.color_range + + ocr_result_map = self.ctx.ocr_service.get_ocr_result_map( + image=screen, + rect=area.rect if area is not None else None, + color_range=color_range, + crop_first=crop_first, + ) + + # 从元组列表构建目标列表和动作映射 + target_cn_list = [target for target, _ in target_action_list] + action_map = dict(target_action_list) + + match_word, match_word_mrl = ocr_utils.match_word_list_by_priority( + ocr_result_map, + target_cn_list, + ) + + if match_word is not None and match_word_mrl is not None and match_word_mrl.max is not None: + to_click = match_word_mrl.max.center + if offset is not None: + to_click = to_click + offset + + time.sleep(pre_delay) + self.ctx.controller.click(to_click) + + action = action_map.get(match_word, OperationRoundResultEnum.SUCCESS) + if action == OperationRoundResultEnum.WAIT: + return self.round_wait(status=match_word, wait=wait_wait, wait_round_time=wait_wait_round) + elif action == OperationRoundResultEnum.RETRY: + return self.round_retry(status=match_word, wait=retry_wait, wait_round_time=retry_wait_round) + else: # SUCCESS + return self.round_success(status=match_word, wait=success_wait, wait_round_time=success_wait_round) + + return self.round_retry(status='未匹配到目标文本', wait=retry_wait, wait_round_time=retry_wait_round) + def round_by_ocr( self, screen: np.ndarray, diff --git a/src/one_dragon/base/push/channel/server_chan.py b/src/one_dragon/base/push/channel/server_chan.py index 70d8c06814..83ac41b40e 100644 --- a/src/one_dragon/base/push/channel/server_chan.py +++ b/src/one_dragon/base/push/channel/server_chan.py @@ -1,8 +1,13 @@ +import re + import requests from cv2.typing import MatLike from one_dragon.base.push.push_channel import PushChannel -from one_dragon.base.push.push_channel_config import PushChannelConfigField, FieldTypeEnum +from one_dragon.base.push.push_channel_config import ( + FieldTypeEnum, + PushChannelConfigField, +) class ServerChan(PushChannel): @@ -48,14 +53,22 @@ def push( tuple[bool, str]: 是否成功、错误信息 """ try: - push_key = config.get('PUSH_KEY', '') + sendkey = config.get('PUSH_KEY', '') ok, msg = self.validate_config(config) if not ok: return False, msg - # Server酱 API 地址 - api_url = f"https://sctapi.ftqq.com/{push_key}.send" + # 判断 sendkey 是否以 'sctp' 开头,并提取数字构造 URL + if sendkey.startswith('sctp'): + match = re.match(r'sctp(\d+)t', sendkey) + if match: + num = match.group(1) + url = f'https://{num}.push.ft07.com/send/{sendkey}.send' + else: + raise ValueError('无效的 sendkey 格式') + else: + url = f'https://sctapi.ftqq.com/{sendkey}.send' # 构建请求数据 message_data = { @@ -64,8 +77,8 @@ def push( } # 发送请求 - headers = {'Content-Type': 'application/json'} - response = requests.post(api_url, json=message_data, headers=headers, timeout=10) + headers = {'Content-Type': 'application/json;charset=utf-8'} + response = requests.post(url, json=message_data, headers=headers, timeout=10) if response.status_code == 200: result = response.json() @@ -94,4 +107,4 @@ def validate_config(self, config: dict[str, str]) -> tuple[bool, str]: if len(push_key) == 0: return False, "PUSH_KEY 不能为空" - return True, "配置验证通过" \ No newline at end of file + return True, "配置验证通过" diff --git a/src/one_dragon/base/push/push_config.py b/src/one_dragon/base/push/push_config.py index c9b0ec0fa7..19297c8933 100644 --- a/src/one_dragon/base/push/push_config.py +++ b/src/one_dragon/base/push/push_config.py @@ -1,11 +1,8 @@ -import os -import shutil from enum import Enum from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.config.yaml_config import YamlConfig from one_dragon.base.push.push_channel_config import PushChannelConfigField -from one_dragon.utils import os_utils class PushProxy(Enum): @@ -17,67 +14,8 @@ class PushProxy(Enum): class PushConfig(YamlConfig): def __init__(self): - """ - 推送配置 - 应该是一个全局配置 - """ - # 执行配置文件路径层面的迁移 - self._migrate_legacy_config_file_path() - YamlConfig.__init__(self, 'push') - # 执行配置文件数据内容层面的迁移 - self._migrate_legacy_qywx_am_param() - - def _migrate_legacy_config_file_path(self) -> None: - """ - 迁移旧版本配置文件路径:将单实例(如 'config/01')目录下的 push.yml - 复制到全局配置目录 'config/'。预计 2026-01-01 可删除这部分兼容代码。 - """ - instance_config_file_path = os.path.join( - os_utils.get_path_under_work_dir('config', '01'), - 'push.yml' - ) - global_config_file_path = os.path.join( - os_utils.get_path_under_work_dir('config'), - 'push.yml' - ) - if not os.path.exists(global_config_file_path) and os.path.exists(instance_config_file_path): - shutil.copy(instance_config_file_path, global_config_file_path) - - def _migrate_legacy_qywx_am_param(self) -> None: - """ - 迁移旧的 'qywx_am' 参数,将其拆分为 'qywx_app_corp_id' 等新字段。 - """ - old_am_key = 'qywx_am' - - # 检查旧的 qywx_am 配置是否存在且有值 - if self.data and isinstance(self.data.get(old_am_key), str) and self.data.get(old_am_key, '').strip(): - am_value = self.data.get(old_am_key) - - parts = [part.strip() for part in am_value.split(',')] - - # 确认参数个数正确 - if len(parts) >= 4: - migration_map = { - 'qywx_app_corp_id': parts[0], - 'qywx_app_corp_secret': parts[1], - 'qywx_app_to_user': parts[2], - 'qywx_app_agent_id': parts[3] - } - # 可选的media id - if len(parts) >= 5: - migration_map['qywx_app_media_id'] = parts[4] - - for new_key, new_value in migration_map.items(): - # 只有当新key不存在或为空时,才进行迁移,避免覆盖用户的新设置 - if not self.data.get(new_key): - self.data[new_key] = new_value - - # 迁移完成,删除旧key并保存 - del self.data[old_am_key] - self.save() - @property def send_image(self) -> bool: """ 是否发送图片 """ diff --git a/src/one_dragon/base/push/push_service.py b/src/one_dragon/base/push/push_service.py index 28343ed36d..083860f705 100644 --- a/src/one_dragon/base/push/push_service.py +++ b/src/one_dragon/base/push/push_service.py @@ -29,8 +29,8 @@ from one_dragon.base.push.channel.telegram import Telegram from one_dragon.base.push.channel.we_plus_bot import WePlusBot from one_dragon.base.push.channel.webhook import Webhook -from one_dragon.base.push.channel.work_weixin_bot import WorkWeixinBot from one_dragon.base.push.channel.work_weixin_app import WorkWeixinApp +from one_dragon.base.push.channel.work_weixin_bot import WorkWeixinBot from one_dragon.base.push.channel.wx_pusher import WxPusher from one_dragon.base.push.push_channel import PushChannel from one_dragon.base.push.push_channel_config import PushChannelConfigField @@ -254,3 +254,9 @@ def get_proxy(self) -> str | None: return self.ctx.env_config.personal_proxy return None + + def after_app_shutdown(self) -> None: + """ + 整个脚本运行结束后的清理 + """ + self._executor.shutdown(wait=True) diff --git a/src/one_dragon/base/screen/screen_area.py b/src/one_dragon/base/screen/screen_area.py index 8044a3f253..e010da7954 100644 --- a/src/one_dragon/base/screen/screen_area.py +++ b/src/one_dragon/base/screen/screen_area.py @@ -1,5 +1,3 @@ -from typing import Optional - import numpy as np from one_dragon.base.geometry.point import Point @@ -12,27 +10,29 @@ def __init__( self, area_name: str = '', pc_rect: Rect | None = None, - text: Optional[str] = '', + text: str = '', lcs_percent: float = 0.5, - template_id: Optional[str] = '', - template_sub_dir: Optional[str] = '', + template_id: str = '', + template_sub_dir: str = '', template_match_threshold: float = 0.7, pc_alt: bool = False, id_mark: bool = False, - goto_list: Optional[list[str]] = None, - color_range: Optional[list[list[int]]] = None, + goto_list: list[str] | None = None, + color_range: list[list[int]] | None = None, + gamepad_key: str | None = None, ): - self.area_name: str = area_name + self.area_name: str = area_name or '' self.pc_rect: Rect = pc_rect if pc_rect is not None else Rect(0, 0, 0, 0) - self.text: Optional[str] = text + self.text: str = text or '' self.lcs_percent: float = lcs_percent - self.template_id: Optional[str] = template_id - self.template_sub_dir: Optional[str] = template_sub_dir + self.template_id: str = template_id or '' + self.template_sub_dir: str = template_sub_dir or '' self.template_match_threshold: float = template_match_threshold self.pc_alt: bool = pc_alt # PC端需要使用ALT后才能点击 self.id_mark: bool = id_mark # 是否用于画面的唯一标识 self.goto_list: list[str] = [] if goto_list is None else goto_list # 交互后 可能会跳转的画面名称列表 - self.color_range: Optional[list[list[int]]] = color_range # 识别时候的筛选的颜色范围 文本时候有效 + self.color_range: list[list[int]] | None = color_range # 识别时候的筛选的颜色范围 文本时候有效 + self.gamepad_key: str | None = gamepad_key # GamepadActionEnum 动作名 如 'menu', 'compendium' @property def rect(self) -> Rect: @@ -74,34 +74,13 @@ def width(self) -> int: def height(self) -> int: return self.rect.height - @property - def template_id_display_text(self) -> str: - if len(self.template_sub_dir) == 0: - return self.template_id - else: - return f'{self.template_sub_dir}.{self.template_id}' - - @property - def goto_list_display_text(self) -> str: - if self.goto_list is None: - return '' - else: - return ','.join(self.goto_list) - - @property - def color_range_display_text(self) -> str: - if self.color_range is None: - return '' - else: - return str(self.color_range) - @property def is_text_area(self) -> bool: """ 是否文本区域 :return: """ - return self.text is not None and len(self.text) > 0 + return len(self.text) > 0 @property def is_template_area(self) -> bool: @@ -109,7 +88,7 @@ def is_template_area(self) -> bool: 是否模板区域 :return: """ - return self.template_id is not None and len(self.template_id) > 0 + return len(self.template_id) > 0 @property def color_range_lower(self) -> np.ndarray: @@ -126,7 +105,7 @@ def color_range_upper(self) -> np.ndarray: return np.array(self.color_range[1], dtype=np.uint8) def to_dict(self) -> dict: - order_dict = dict() + order_dict = {} order_dict['area_name'] = self.area_name order_dict['id_mark'] = self.id_mark order_dict['pc_rect'] = [self.pc_rect.x1, self.pc_rect.y1, self.pc_rect.x2, self.pc_rect.y2] @@ -137,5 +116,7 @@ def to_dict(self) -> dict: order_dict['template_match_threshold'] = self.template_match_threshold order_dict['color_range'] = self.color_range order_dict['goto_list'] = self.goto_list + if self.gamepad_key: + order_dict['gamepad_key'] = self.gamepad_key return order_dict diff --git a/src/one_dragon/base/screen/screen_info.py b/src/one_dragon/base/screen/screen_info.py index 13e22b8b5d..d30875c545 100644 --- a/src/one_dragon/base/screen/screen_info.py +++ b/src/one_dragon/base/screen/screen_info.py @@ -1,12 +1,10 @@ +from typing import Any + import cv2 -import os from cv2.typing import MatLike -from typing import List, Optional, Any -from one_dragon.base.config.yaml_operator import YamlOperator from one_dragon.base.geometry.rectangle import Rect from one_dragon.base.screen.screen_area import ScreenArea -from one_dragon.utils import os_utils, cv2_utils class ScreenInfo: @@ -19,7 +17,7 @@ def __init__(self, data: dict[str, Any]): self.screen_image: MatLike | None = None self.pc_alt: bool = data.get('pc_alt', False) # PC端点击是否需要使用ALT键 - self.area_list: List[ScreenArea] = [] # 画面中包含的区域 + self.area_list: list[ScreenArea] = [] # 画面中包含的区域 data_area_list = data.get('area_list', []) for data_area in data_area_list: @@ -36,10 +34,11 @@ def __init__(self, data: dict[str, Any]): pc_alt=self.pc_alt, id_mark=data_area.get('id_mark', False), goto_list=data_area.get('goto_list', []), + gamepad_key=data_area.get('gamepad_key', ''), ) self.area_list.append(area) - def get_image_to_show(self, highlight_area_idx: Optional[int] = None) -> MatLike: + def get_image_to_show(self, highlight_area_idx: int | None = None) -> MatLike: """ 用于显示的图片 :param highlight_area_idx: 高亮区域索引 diff --git a/src/one_dragon/base/screen/screen_loader.py b/src/one_dragon/base/screen/screen_loader.py index 463e068e42..085cc1baa2 100644 --- a/src/one_dragon/base/screen/screen_loader.py +++ b/src/one_dragon/base/screen/screen_loader.py @@ -5,7 +5,7 @@ from one_dragon.base.screen.screen_area import ScreenArea from one_dragon.base.screen.screen_info import ScreenInfo -from one_dragon.utils import os_utils +from one_dragon.utils import os_utils, yaml_utils from one_dragon.utils.log_utils import log @@ -95,7 +95,10 @@ def reload(self, from_memory: bool = False, from_separated_files: bool = False) file_path = os.path.join(self.yml_file_dir, file_name) with open(file_path, 'r', encoding='utf-8') as file: log.debug(f"加载yaml: {file_path}") - data = yaml.safe_load(file) + data = yaml_utils.safe_load(file) + if not isinstance(data, dict): + log.warning(f"画面配置格式错误,已跳过: {file_path}") + continue screen_info = ScreenInfo(data) self.screen_info_list.append(screen_info) @@ -109,8 +112,15 @@ def reload(self, from_memory: bool = False, from_separated_files: bool = False) file_path = self.merge_yml_file_path with open(file_path, 'r', encoding='utf-8') as file: log.debug(f"加载yaml: {file_path}") - yaml_data = yaml.safe_load(file) + yaml_data = yaml_utils.safe_load(file) + if not isinstance(yaml_data, list): + if yaml_data is not None: + log.warning(f"合并画面配置格式错误,已忽略: {file_path}") + yaml_data = [] for data in yaml_data: + if not isinstance(data, dict): + log.warning(f"合并画面配置中存在非字典条目,已跳过: {file_path}") + continue screen_info = ScreenInfo(data) self.screen_info_list.append(screen_info) self.screen_info_map[screen_info.screen_name] = screen_info diff --git a/src/one_dragon/base/screen/screen_utils.py b/src/one_dragon/base/screen/screen_utils.py index c183494e34..3a3dff4d95 100644 --- a/src/one_dragon/base/screen/screen_utils.py +++ b/src/one_dragon/base/screen/screen_utils.py @@ -262,7 +262,7 @@ def find_and_click_area( for ocr_result in ocr_result_list: if str_utils.find_by_lcs(gt(area.text, 'game'), ocr_result.data, percent=area.lcs_percent): - if ctx.controller.click(ocr_result.center, pc_alt=area.pc_alt): + if ctx.controller.click(ocr_result.center, pc_alt=area.pc_alt, gamepad_key=area.gamepad_key): return OcrClickResultEnum.OCR_CLICK_SUCCESS else: return OcrClickResultEnum.OCR_CLICK_FAIL @@ -276,15 +276,60 @@ def find_and_click_area( threshold=area.template_match_threshold) if mrl.max is None: return OcrClickResultEnum.OCR_CLICK_NOT_FOUND - elif ctx.controller.click(mrl.max.center + rect.left_top, pc_alt=area.pc_alt): + if ctx.controller.click(mrl.max.center + rect.left_top, pc_alt=area.pc_alt, gamepad_key=area.gamepad_key): return OcrClickResultEnum.OCR_CLICK_SUCCESS else: return OcrClickResultEnum.OCR_CLICK_FAIL else: - ctx.controller.click(area.center, pc_alt=area.pc_alt) + ctx.controller.click(area.center, pc_alt=area.pc_alt, gamepad_key=area.gamepad_key) return OcrClickResultEnum.OCR_CLICK_SUCCESS +def scroll_area( + ctx: OneDragonContext, + area: ScreenArea, + direction: str = 'down', + start_ratio: float = 0.9, + end_ratio: float = 0.1, +) -> None: + """ + 在指定区域内滚动屏幕 + + Args: + ctx: 运行上下文 + area: 区域 + direction: 滚动方向,'down' 表示往下滚(从下往上滑),'up' 表示往上滚(从上往下滑) + start_ratio: 起始位置比例(距顶部的比例)。默认0.9,即区域底部10%处 + end_ratio: 结束位置比例(距顶部的比例)。默认0.1,即区域顶部10%处 + """ + rect = area.rect + height = rect.height + + # 统一按“距顶部比例”计算位置,避免 start/end 计算成同一点 + start_ratio = max(0.0, min(1.0, start_ratio)) + end_ratio = max(0.0, min(1.0, end_ratio)) + y_start = rect.y1 + int(height * start_ratio) + y_end = rect.y1 + int(height * end_ratio) + + if direction == 'up': + # 往上滚:手势从上往下划 + start = Point(rect.center.x, y_end) + end = Point(rect.center.x, y_start) + else: + # 往下滚:手势从下往上划 + start = Point(rect.center.x, y_start) + end = Point(rect.center.x, y_end) + + # 防止极端情况下 start/end 重合导致“看起来没有滚动” + if start.y == end.y: + if start.y >= rect.center.y: + end = Point(end.x, max(rect.y1 + 1, end.y - 1)) + else: + end = Point(end.x, min(rect.y2 - 1, end.y + 1)) + + ctx.controller.drag_to(start=start, end=end) + + def get_match_screen_name( ctx: OneDragonContext, screen: MatLike, diff --git a/src/one_dragon/envs/ghproxy_service.py b/src/one_dragon/envs/ghproxy_service.py index 0b4f88dff9..d5ec72f1af 100644 --- a/src/one_dragon/envs/ghproxy_service.py +++ b/src/one_dragon/envs/ghproxy_service.py @@ -1,5 +1,6 @@ import re -import urllib.request + +import requests from one_dragon.envs.env_config import EnvConfig from one_dragon.utils.log_utils import log @@ -16,9 +17,14 @@ def update_proxy_url(self) -> None: :return: """ url = 'https://ghproxy.link/js/src_views_home_HomeView_vue.js' # 打开 https://ghproxy.link/ 后找到的js文件 + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Referer': 'https://ghproxy.link/' + } try: - with urllib.request.urlopen(url, timeout=10) as response: - js_content: str = response.read().decode('utf-8') + response = requests.get(url, headers=headers, timeout=10) + response.raise_for_status() + js_content = response.text except Exception: log.error('自动获取免费代理地址失败', exc_info=True) return @@ -59,4 +65,4 @@ def __debug(): if __name__ == '__main__': - __debug() \ No newline at end of file + __debug() diff --git a/src/one_dragon/envs/git_service.py b/src/one_dragon/envs/git_service.py index 16294904b6..7a9f27a1e9 100644 --- a/src/one_dragon/envs/git_service.py +++ b/src/one_dragon/envs/git_service.py @@ -1,11 +1,14 @@ import contextlib +import sys import time from collections.abc import Callable from dataclasses import dataclass from pathlib import Path +import yaml from packaging import version from pygit2 import ( + Blob, Oid, Remote, Repository, @@ -279,6 +282,110 @@ def _get_commit_walker(self, sort_mode: SortMode = SortMode.TOPOLOGICAL) -> Walk log.error('获取commit遍历器失败', exc_info=True) return None + def _get_file_at_commit(self, commit_oid: Oid, file_path: str) -> bytes | None: + """获取指定 commit 中某文件的内容 + + Args: + commit_oid: 提交 OID + file_path: 相对于仓库根目录的文件路径(如 'deploy/module_manifest.py') + + Returns: + 文件内容的字节,文件不存在时返回 None + """ + try: + repo = self._open_repo() + obj = repo.revparse_single(f'{commit_oid}:{file_path}') + if isinstance(obj, Blob): + return obj.data + return None + except (KeyError, ValueError): + return None + + # ================== 模块清单检查 ================== + + def _check_manifest_compatible(self, target_oid: Oid) -> tuple[bool, str]: + """检查模块清单是否与当前运行环境兼容 + + 本地清单从 .runtime/module_manifest.py 读取(打包时写入), + 远程清单路径从目标 commit 的 project.yml 的 manifest_path 字段获取。 + 仅在 frozen 环境(PyInstaller 打包后)下执行检查。 + + Args: + target_oid: 目标提交 OID + + Returns: + (是否兼容, 提示消息) + """ + if not getattr(sys, 'frozen', False): + return True, '' + + # 读取本地 manifest(打包进 .runtime/ 的文件) + runtime_dir = Path(getattr(sys, '_MEIPASS', '')) + local_manifest_path = runtime_dir / 'module_manifest.py' + if not local_manifest_path.is_file(): + return True, '' + + try: + local_manifest = local_manifest_path.read_bytes() + except Exception: + log.warning('读取本地模块清单失败,跳过检查', exc_info=True) + return True, '' + + # 从目标 commit 的 project.yml 获取清单路径 + manifest_git_path = self._get_manifest_path_from_commit(target_oid) + if not manifest_git_path: + return True, '' + + # 读取目标 commit 中的 manifest + remote_manifest = self._get_file_at_commit(target_oid, manifest_git_path) + if remote_manifest is None: + return True, '' + + if local_manifest == remote_manifest: + return True, '' + + msg = gt('目标版本的运行环境与当前不兼容') + log.warning(f'模块清单已变更,阻止代码更新。目标: {str(target_oid)[:7]}') + return False, msg + + def _get_manifest_path_from_commit(self, commit_oid: Oid) -> str | None: + """从指定 commit 的 project.yml 中读取 manifest_path + + Args: + commit_oid: 目标提交 OID + + Returns: + 清单文件的仓库路径,读取失败时返回 None + """ + raw = self._get_file_at_commit(commit_oid, 'config/project.yml') + if raw is None: + return None + try: + data = yaml.safe_load(raw) + path = data.get('manifest_path') if isinstance(data, dict) else None + return path if isinstance(path, str) and path else None + except Exception: + return None + + def _check_remote_manifest_compatible(self) -> tuple[bool, str]: + """检查远程分支的模块清单是否与当前运行环境兼容 + + 封装远程 OID 解析 + 清单比对,异常时跳过检查。 + + Returns: + (是否兼容, 提示消息) + """ + remote_ref = f'refs/remotes/{self.env_config.git_remote}/{self.env_config.git_branch}' + try: + repo = self._open_repo() + if remote_ref not in repo.references: + return True, '' + remote_oid = repo.references[remote_ref].target + return self._check_manifest_compatible(remote_oid) + except Exception: + log.warning('检查模块清单时出错,跳过检查', exc_info=True) + return True, '' + def _checkout_branch(self) -> bool: """切换到指定分支 @@ -440,14 +547,22 @@ def _fetch_and_checkout_latest_branch(self, progress_callback: Callable[[float, # 获取远程代码 if progress_callback: - progress_callback(1/5, gt('获取远程代码')) + progress_callback(1/6, gt('获取远程代码')) if not self._fetch_remote(): return False, gt('获取远程代码失败') + # 检查模块清单兼容性(仅 frozen 环境) + if progress_callback: + progress_callback(2/6, gt('检查运行环境兼容性')) + + compatible, msg = self._check_remote_manifest_compatible() + if not compatible: + return False, msg + # 检查工作区状态 if progress_callback: - progress_callback(2/5, gt('检查工作区状态')) + progress_callback(3/6, gt('检查工作区状态')) success, message = self._validate_working_directory() if not success: @@ -455,21 +570,21 @@ def _fetch_and_checkout_latest_branch(self, progress_callback: Callable[[float, # 切换到目标分支 if progress_callback: - progress_callback(3/5, gt('切换到目标分支')) + progress_callback(4/6, gt('切换到目标分支')) if not self._checkout_branch(): return False, gt('切换到目标分支失败') # 同步远程分支 if progress_callback: - progress_callback(4/5, gt('同步远程分支')) + progress_callback(5/6, gt('同步远程分支')) success, message = self._sync_with_remote(self.env_config.force_update) if not success: return False, message if progress_callback: - progress_callback(5/5, message) + progress_callback(6/6, message) return True, message @@ -575,11 +690,28 @@ def update_remote(self) -> None: except Exception: log.error('更新远程仓库地址失败', exc_info=True) - def reset_to_commit(self, commit_id: str) -> bool: + def reset_to_commit(self, commit_id: str) -> tuple[bool, str]: """ - 回滚到特定commit + 回滚到特定commit,会先检查模块清单兼容性 + + Returns: + (是否成功, 提示消息) """ - return self._reset_hard(commit_id) + try: + repo = self._open_repo() + obj = repo.revparse_single(commit_id) + target_oid = obj.id + except Exception: + log.error(f'解析提交ID失败: {commit_id}', exc_info=True) + return False, gt('解析提交ID失败') + + compatible, msg = self._check_manifest_compatible(target_oid) + if not compatible: + return False, msg + + if self._reset_hard(target_oid): + return True, '' + return False, gt('回滚失败') def get_current_version(self) -> str | None: """ diff --git a/src/one_dragon/launcher/exe_launcher.py b/src/one_dragon/launcher/exe_launcher.py index 401b07eb12..c4b7ae4006 100644 --- a/src/one_dragon/launcher/exe_launcher.py +++ b/src/one_dragon/launcher/exe_launcher.py @@ -53,7 +53,7 @@ def main(self, args) -> None: sys.exit(1) if not pyuac.isUserAdmin(): - pyuac.runAsAdmin(sys.argv) + pyuac.runAsAdmin(sys.argv, wait=False) sys.exit(0) else: if args.onedragon: diff --git a/src/one_dragon/launcher/runtime_launcher.py b/src/one_dragon/launcher/runtime_launcher.py new file mode 100644 index 0000000000..9f15296451 --- /dev/null +++ b/src/one_dragon/launcher/runtime_launcher.py @@ -0,0 +1,92 @@ +import ctypes +import importlib +import sys + +from one_dragon.launcher.exe_launcher import ExeLauncher + + +class RuntimeLauncher(ExeLauncher): + """集成启动器基类 + + 将 Python 运行时嵌入到应用目录中的启动器。 + 提供代码同步、控制台隐藏等通用功能。 + """ + + def __init__(self, description: str, version: str) -> None: + ExeLauncher.__init__(self, description, version) + + def _sync_code(self) -> None: + """同步代码:首次运行时克隆,后续运行时自动更新""" + pre_modules = set(sys.modules) + + from one_dragon.envs.env_config import EnvConfig + from one_dragon.envs.git_service import GitService + from one_dragon.envs.project_config import ProjectConfig + from one_dragon.utils.i18_utils import gt + from one_dragon.utils.log_utils import log + + env_config = EnvConfig() + git_service = GitService(ProjectConfig(), env_config) + first_run = not git_service.check_repo_exists() + + if not first_run and not env_config.auto_update: + log.info(gt('未开启代码自动更新,跳过')) + return + + log.info(gt('首次运行,正在同步代码仓库...') if first_run else gt('正在检查代码更新...')) + success, msg = git_service.fetch_latest_code() + + if success: + log.info(gt('代码同步完成') if first_run else gt('代码已是最新')) + # 清除同步过程中加载的模块,避免主程序使用旧版本 + for name in set(sys.modules) - pre_modules: + del sys.modules[name] + importlib.invalidate_caches() + elif first_run: + log.info(f"{gt('代码同步失败')}: {msg}") + sys.exit(1) + else: + log.info(f"{gt('代码更新失败')}: {msg}") + + @staticmethod + def _hide_console() -> None: + """隐藏控制台窗口,用于 GUI 模式""" + hwnd = ctypes.windll.kernel32.GetConsoleWindow() + if hwnd: + ctypes.windll.user32.ShowWindow(hwnd, 0) # SW_HIDE + + @staticmethod + def _show_fatal_error(error_info: str) -> None: + """显示致命错误对话框并退出""" + ctypes.windll.user32.MessageBoxW( + None, + f"启动失败,报错信息如下:\n{error_info}", + "OneDragon 集成启动器", + 0x10, # MB_ICONERROR + ) + sys.exit(1) + + def run_onedragon_mode(self, launch_args: list[str]) -> None: + try: + self._sync_code() + self._do_run_onedragon(launch_args) + except Exception: + import traceback + self._show_fatal_error(traceback.format_exc()) + + def run_gui_mode(self) -> None: + try: + self._sync_code() + self._hide_console() + self._do_run_gui() + except Exception: + import traceback + self._show_fatal_error(traceback.format_exc()) + + def _do_run_onedragon(self, launch_args: list[str]) -> None: + """运行一条龙模式,子类实现""" + pass + + def _do_run_gui(self) -> None: + """运行GUI模式,子类实现""" + pass diff --git a/src/one_dragon/utils/app_utils.py b/src/one_dragon/utils/app_utils.py index cbcf48b34a..91a4567d78 100644 --- a/src/one_dragon/utils/app_utils.py +++ b/src/one_dragon/utils/app_utils.py @@ -1,7 +1,6 @@ -import sys - -import os import subprocess +import sys +from pathlib import Path from one_dragon.utils import os_utils @@ -12,26 +11,47 @@ def start_one_dragon(restart: bool) -> None: :param restart: 是否重启 :return: 是否成功 """ - launcher_path = os.path.join(os_utils.get_work_dir(), 'OneDragon-Launcher.exe') + if getattr(sys, 'frozen', False): + launcher_path = Path(sys.executable) + else: + launcher_path = Path(os_utils.get_work_dir()) / 'OneDragon-Launcher.exe' subprocess.Popen(f'cmd /c "start "" "{launcher_path}""', shell=True) if restart: sys.exit(0) -def get_launcher_version() -> str: +def get_exe_version(exe_path: str) -> str: """ - 检查启动器版本 - :return: 版本号 + 获取指定 exe 的版本号(通过 --version 参数) + Args: + exe_path: exe 文件路径 + Returns: + str: 版本号,失败返回空字符串 """ - launcher_path = os.path.join(os_utils.get_work_dir(), 'OneDragon-Launcher.exe') try: - result = subprocess.run(f'"{launcher_path}" --version', capture_output=True, text=True) + result = subprocess.run( + f'"{exe_path}" --version', + capture_output=True, text=True, + creationflags=subprocess.CREATE_NO_WINDOW, + ) version_output = result.stdout.strip() - parts = version_output.split('v', 1) - return f"v{parts[1]}" if len(parts) > 1 else version_output + return version_output.rsplit(maxsplit=1)[-1] if version_output else "" except Exception: return "" +def get_launcher_version() -> str: + """ + 检查当前启动器版本 + Returns: + str: 版本号 + """ + if getattr(sys, 'frozen', False): + launcher_path = Path(sys.executable) + else: + launcher_path = Path(os_utils.get_work_dir()) / 'OneDragon-Launcher.exe' + return get_exe_version(str(launcher_path)) + + if __name__ == '__main__': print(get_launcher_version()) diff --git a/src/one_dragon/utils/cv2_utils.py b/src/one_dragon/utils/cv2_utils.py index b6bca6f9d0..df7e121dd0 100644 --- a/src/one_dragon/utils/cv2_utils.py +++ b/src/one_dragon/utils/cv2_utils.py @@ -1191,3 +1191,46 @@ def to_binary(img: MatLike, threshold: int = 127) -> MatLike: gray = img _, binary = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY) return binary + + +def is_colorful(img: MatLike, saturation_threshold: int = 30, color_ratio_threshold: float = 0.1) -> bool: + """ + 判断图像是否为彩色(不是灰度) + + 彩色图像的判断依据: + 1. 饱和度:HSV空间中的S通道平均值高于阈值 + 2. 颜色占比:具有明显饱和度的像素占比高于阈值 + + Args: + img: 输入图像(RGB格式) + saturation_threshold: 饱和度阈值,低于此值认为是不饱和的(灰度)。默认30 + color_ratio_threshold: 彩色像素占比阈值。默认0.1(10%) + + Returns: + bool: True 表示是彩色的,False 表示接近灰度 + """ + if img is None or img.size == 0: + return False + + # 确保是彩色图像 + if len(img.shape) != 3 or img.shape[2] != 3: + return False + + # 转换为HSV颜色空间 + hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) + + # 获取饱和度通道(S通道) + s_channel = hsv[:, :, 1] + + # 方法1:计算饱和度均值 + mean_saturation = np.mean(s_channel) + + # 方法2:计算具有明显饱和度的像素比例 + saturated_pixels = np.sum(s_channel > saturation_threshold) + total_pixels = s_channel.size + color_ratio = saturated_pixels / total_pixels + + # 判断条件:饱和度均值高于阈值 且 彩色像素占比高于阈值 + is_colorful = mean_saturation > saturation_threshold and color_ratio > color_ratio_threshold + + return is_colorful diff --git a/src/one_dragon/utils/str_utils.py b/src/one_dragon/utils/str_utils.py index 8a4a0dc766..57fd783336 100644 --- a/src/one_dragon/utils/str_utils.py +++ b/src/one_dragon/utils/str_utils.py @@ -280,3 +280,15 @@ def is_target_after_ocr_list( found_before_target = True return found_target and found_before_target + +def remove_whitespace(v: str | None) -> str: + """ + 移除字符串中的所有空白字符 + :param v: 原始字符串 + :return: 清理空白字符后的字符串 + """ + if v is None: + return "" + + # 移除空格 + return re.sub(r'\s', '', v) \ No newline at end of file diff --git a/src/one_dragon/utils/yaml_utils.py b/src/one_dragon/utils/yaml_utils.py new file mode 100644 index 0000000000..5255e62982 --- /dev/null +++ b/src/one_dragon/utils/yaml_utils.py @@ -0,0 +1,12 @@ +import yaml +from typing import Any, IO + +try: + from yaml import CSafeLoader as SafeLoader +except ImportError: + from yaml import SafeLoader + + +def safe_load(stream: str | bytes | IO[str] | IO[bytes]) -> Any: + """Safely parse YAML via CSafeLoader when available, else SafeLoader.""" + return yaml.load(stream, Loader=SafeLoader) diff --git a/src/one_dragon_qt/app/directory_picker.py b/src/one_dragon_qt/app/directory_picker.py index 1734e4329d..7c7a0a7fef 100644 --- a/src/one_dragon_qt/app/directory_picker.py +++ b/src/one_dragon_qt/app/directory_picker.py @@ -1,12 +1,34 @@ -import os import locale -from PySide6.QtCore import Qt, QEventLoop, QSize -from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QFileDialog, QApplication, QWidget +import os + +from PySide6.QtCore import QEventLoop, QSize, Qt, QTimer from PySide6.QtGui import QPixmap -from qfluentwidgets import (FluentIcon, PrimaryPushButton, ToolButton, LineEdit, MessageBox, - SplitTitleBar, SubtitleLabel, PixmapLabel) +from PySide6.QtWidgets import ( + QApplication, + QFileDialog, + QHBoxLayout, + QSizePolicy, + QStackedWidget, + QVBoxLayout, + QWidget, +) +from qfluentwidgets import ( + BodyLabel, + CaptionLabel, + FluentIcon, + IndeterminateProgressBar, + LineEdit, + MessageBox, + PixmapLabel, + PrimaryPushButton, + ProgressBar, + SplitTitleBar, + SubtitleLabel, + ToolButton, +) from one_dragon_qt.services.styles_manager import OdQtStyleSheet +from one_dragon_qt.services.unpack_runner import UnpackResourceRunner from one_dragon_qt.utils.image_utils import scale_pixmap_for_high_dpi from one_dragon_qt.windows.window import PhosWindow @@ -29,7 +51,12 @@ def __init__(self, language='zh'): 'directory_not_empty_warning': '所选目录不为空,里面的内容将被覆盖:\n{path}\n\n是否继续使用此目录?', 'i_know': '我知道了', 'continue_use': '继续使用', - 'select_other': '选择其他目录' + 'select_other': '选择其他目录', + 'preparing': '正在准备安装文件...', + 'copying': '正在复制 {current}/{total}', + 'cleaning': '正在清理源目录...', + 'unpack_failed_title': '搬运失败', + 'unpack_failed_body': '安装文件搬运失败,请重新运行安装器。\n\n{detail}', }, 'en': { 'title': 'Please Select Installation Path', @@ -43,7 +70,12 @@ def __init__(self, language='zh'): 'directory_not_empty_warning': 'The selected directory is not empty, its contents will be overwritten:\n{path}\n\nDo you want to continue using this directory?', 'i_know': 'I Know', 'continue_use': 'Continue', - 'select_other': 'Select Other' + 'select_other': 'Select Other', + 'preparing': 'Preparing installation files...', + 'copying': 'Copying {current}/{total}', + 'cleaning': 'Cleaning source directory...', + 'unpack_failed_title': 'Migration Failed', + 'unpack_failed_body': 'Installation file migration failed. Please re-run the installer.\n\n{detail}', } } @@ -63,19 +95,31 @@ def detect_language(): return 'zh' else: return 'en' - except: + except Exception: return 'zh' class DirectoryPickerInterface(QWidget): """路径选择器界面""" - def __init__(self, parent=None, icon_path=None): + def __init__(self, parent=None, icon_path=None, installer_dir: str = ""): QWidget.__init__(self, parent=parent) self.setObjectName("directory_picker_interface") self.selected_path = "" self.icon_path = icon_path + self.installer_dir = installer_dir + self.translator = DirectoryPickerTranslator(DirectoryPickerTranslator.detect_language()) + self._runner: UnpackResourceRunner | None = None + self._last_log: str = "" + self._pending_log: str = "" + + # 节流计时器:最多每 250 ms 刷新一次文件名,避免闪烁 + self._log_timer = QTimer(self) + self._log_timer.setSingleShot(True) + self._log_timer.setInterval(250) + self._log_timer.timeout.connect(self._flush_log) + self._init_ui() def _init_ui(self): @@ -109,7 +153,6 @@ def _init_ui(self): # 标题 self.title_label = SubtitleLabel(self.translator.get_text('title')) self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - main_layout.addWidget(self.title_label) # 路径显示区域 path_layout = QHBoxLayout() @@ -124,20 +167,63 @@ def _init_ui(self): self.browse_btn.clicked.connect(self._on_browse_clicked) path_layout.addWidget(self.browse_btn) - main_layout.addLayout(path_layout) - - # 按钮区域 + # 确认按钮 button_layout = QHBoxLayout() button_layout.addStretch(1) self.confirm_btn = PrimaryPushButton(self.translator.get_text('confirm')) self.confirm_btn.setIcon(FluentIcon.ACCEPT) - self.confirm_btn.setMinimumSize(120, 36) # 设置最小尺寸使按钮变大 + self.confirm_btn.setMinimumSize(120, 36) self.confirm_btn.clicked.connect(self._on_confirm_clicked) self.confirm_btn.setEnabled(False) button_layout.addWidget(self.confirm_btn) button_layout.addStretch(1) - main_layout.addLayout(button_layout) + # 选路页(page 0):标题 + 路径输入 + 确认 + pick_page = QWidget() + pick_layout = QVBoxLayout(pick_page) + pick_layout.setContentsMargins(0, 0, 0, 0) + pick_layout.setSpacing(20) + pick_layout.addWidget(self.title_label) + pick_layout.addLayout(path_layout) + pick_layout.addLayout(button_layout) + pick_layout.addStretch(1) + + # 进度页(page 1):count → bar → status + stretch + progress_page = QWidget() + pg_layout = QVBoxLayout(progress_page) + pg_layout.setContentsMargins(0, 0, 0, 0) + pg_layout.setSpacing(0) + self.progress_bar = ProgressBar() + self.progress_bar.setRange(0, 1) + self.progress_bar.setValue(0) + self.indet_progress_bar = IndeterminateProgressBar() + # 用内层 stack 切换两种进度条,保证布局高度稳定 + self.bar_stack = QStackedWidget() + self.bar_stack.addWidget(self.progress_bar) # index 0: 复制阶段(确定进度) + self.bar_stack.addWidget(self.indet_progress_bar) # index 1: 清理阶段(不确定进度) + self.bar_stack.setCurrentIndex(0) + # 第一行:正在复制 xx/xx(BodyLabel,字号稍大) + self.count_label = BodyLabel(self.translator.get_text('preparing')) + self.count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + # 第二行:具体文件路径(CaptionLabel,省略过长路径) + self.status_label = CaptionLabel('') + self.status_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + self.status_label.setWordWrap(False) + # 禁止 status_label 撑宽窗口;文本过长时 _on_unpack_log 会做 ElideMiddle + self.status_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred) + self.status_label.setMinimumWidth(0) + pg_layout.addWidget(self.count_label) + pg_layout.addSpacing(12) + pg_layout.addWidget(self.bar_stack) + pg_layout.addSpacing(8) + pg_layout.addWidget(self.status_label) + pg_layout.addStretch(1) + + # 展示切换器 + self.picker_stack = QStackedWidget() + self.picker_stack.addWidget(pick_page) + self.picker_stack.addWidget(progress_page) + main_layout.addWidget(self.picker_stack) # 添加弹性空间 main_layout.addStretch(1) @@ -205,12 +291,74 @@ def _on_browse_clicked(self): def _on_confirm_clicked(self): """确认按钮点击事件""" - if self.selected_path: - # 获取顶层窗口 - window = self.window() - if isinstance(window, DirectoryPickerWindow): - window.selected_directory = self.selected_path - window.close() + if not self.selected_path: + return + + window = self.window() + if not isinstance(window, DirectoryPickerWindow): + return + + # 启动解包,切换到进度页 + self.picker_stack.setCurrentIndex(1) + + self._runner = UnpackResourceRunner(self.installer_dir, self.selected_path) + self._runner.log_message.connect(self._on_unpack_log) + self._runner.progress_changed.connect(self._on_unpack_progress) + self._runner.unpack_done.connect(self._on_unpack_finished) + self._runner.start() + + def _on_unpack_log(self, message: str) -> None: + """缓存最后一条日志;由节流计时器决定何时真正刷新 status_label。""" + self._last_log = message + self._pending_log = message + if not self._log_timer.isActive(): + self._log_timer.start() + + def _flush_log(self) -> None: + """节流计时器触发时,将最新日志写入 status_label(ElideMiddle截断)。""" + message = self._pending_log + avail = self.status_label.width() + if avail > 0: + elided = self.status_label.fontMetrics().elidedText( + message, Qt.TextElideMode.ElideMiddle, avail + ) + else: + elided = message + self.status_label.setText(elided) + + def _on_unpack_progress(self, current: int, total: int) -> None: + """更新进度条与计数行。current=-1 表示进入清理阶段。""" + if current == -1: + # 清理阶段:切换到动画进度条,计数行提示清理,文件行置空 + self.bar_stack.setCurrentIndex(1) + self.indet_progress_bar.start() + self.count_label.setText(self.translator.get_text('cleaning')) + self.status_label.setText("") + else: + self.bar_stack.setCurrentIndex(0) + self.progress_bar.setRange(0, total) + self.progress_bar.setValue(current) + self.count_label.setText(self.translator.get_text('copying', current=current, total=total)) + + def _on_unpack_finished(self, success: bool) -> None: + """解包完毕:成功时设定目标目录并关窗;失败时弹错误提示。""" + window = self.window() + if not isinstance(window, DirectoryPickerWindow): + return + if success: + window.selected_directory = self.selected_path + window.close() + else: + detail = self._last_log or "-" + w = MessageBox( + self.translator.get_text('unpack_failed_title'), + self.translator.get_text('unpack_failed_body', detail=detail), + parent=window, + ) + w.yesButton.setText(self.translator.get_text('i_know')) + w.cancelButton.setVisible(False) + w.exec() + window.close() def _on_language_switch(self): """语言切换按钮点击事件""" @@ -231,7 +379,9 @@ class DirectoryPickerWindow(PhosWindow): def __init__(self, parent=None, - icon_path=None): + icon_path=None, + installer_dir: str = ""): + self.installer_dir = installer_dir PhosWindow.__init__(self, parent=parent) self.setTitleBar(SplitTitleBar(self)) self._last_stack_idx: int = 0 @@ -253,7 +403,7 @@ def exec(self): """模态执行窗口,等待窗口关闭""" self._event_loop = QEventLoop() self._event_loop.exec() - return True if self.selected_directory else False + return bool(self.selected_directory) def closeEvent(self, event): """窗口关闭事件处理""" @@ -272,8 +422,8 @@ def create_sub_interface(self) -> None: 创建子页面 :return: """ - # 创建路径选择器界面,传入图标路径 - self.picker_interface = DirectoryPickerInterface(self, self.icon_path) + # 创建路径选择器界面,传入图标路径和安装器目录 + self.picker_interface = DirectoryPickerInterface(self, self.icon_path, self.installer_dir) self.addSubInterface(self.picker_interface, FluentIcon.FOLDER_ADD, "") def init_window(self): diff --git a/src/one_dragon_qt/services/unpack_runner.py b/src/one_dragon_qt/services/unpack_runner.py new file mode 100644 index 0000000000..c01c075d07 --- /dev/null +++ b/src/one_dragon_qt/services/unpack_runner.py @@ -0,0 +1,249 @@ +import contextlib +import hashlib +import json +import shutil +import sys +from pathlib import Path + +from PySide6.QtCore import QThread, Signal + + +class UnpackResourceRunner(QThread): + """资源解包线程:读取安装清单,将安装器目录中的文件逐一校验后搬运至工作目录。""" + + unpack_done = Signal(bool) # 搬运完成信号,参数为是否成功 + log_message = Signal(str) # 当前文件名日志信号 + progress_changed = Signal(int, int) # (current, total) 复制进度信号 + + def __init__(self, installer_dir: str, work_dir: str, parent=None) -> None: + """ + Args: + installer_dir: 安装器所在目录(含 install_manifest.json) + work_dir: 目标工作目录 + """ + super().__init__(parent) + self.installer_dir = installer_dir + self.work_dir = work_dir + self.is_done: bool = False + self.is_success: bool = False + + @staticmethod + def _copy_and_hash(src: Path, dst: Path) -> tuple[int, str]: + """流式复制并计算哈希,返回 (大小, sha256)""" + hasher = hashlib.sha256() + size = 0 + with src.open('rb') as fsrc, dst.open('wb') as fdst: + while True: + buf = fsrc.read(1024 * 1024) # 1MB chunk + if not buf: + break + fdst.write(buf) + hasher.update(buf) + size += len(buf) + # 复制元数据(如修改时间),保持与 copy2 行为一致 + shutil.copystat(src, dst) + return size, hasher.hexdigest().upper() + + def _copy_by_manifest_then_cleanup(self, src_root: Path, dst_root: Path) -> bool: + """ + 按照安装清单将 src_root 下的文件复制到 dst_root,校验通过后删除源文件。 + + 流程: + 1. 读取并解析 install_manifest.json + 2. 预检磁盘空间(留20%余量) + 3. 逐文件流式复制,校验 size / sha256 + 4. 删除已复制的源文件(跳过正在运行的安装器 exe) + 5. 清理因搬运变空的目录 + 6. 单独搬运清单文件本身 + """ + manifest_path = src_root / 'install_manifest.json' + if not manifest_path.exists(): + return False + + try: + manifest = json.loads(manifest_path.read_text(encoding='utf-8')) + except Exception as e: + self.log_message.emit(f"读取安装清单失败: {e}") + return False + + # 清单格式: {"version": "...", "generated_at": "...", "files": [...]} + # 其中 files 为文件条目列表,每项包含 path / size / sha256 + if not isinstance(manifest, dict) or not isinstance(manifest.get('files'), list): + self.log_message.emit("安装清单格式不正确") + return False + + files: list[dict] = manifest['files'] + total = len(files) + + # 尝试获取正在运行的安装器 exe 路径,避免搬运过程中删除自己 + running_exe: Path | None = None + try: + running_exe = Path(sys.executable).resolve() + except Exception: + running_exe = None + + # 预计算总大小并检查磁盘空间(留20%余量),避免搬运过程中途失败导致残留 + # 沿父路径向上查找第一个存在的目录(防御 dst_root 尚未创建的边缘情况) + total_size = sum(item.get('size', 0) for item in files if isinstance(item, dict)) + _check_path = dst_root + while not _check_path.exists() and _check_path != _check_path.parent: + _check_path = _check_path.parent + try: + free_space = shutil.disk_usage(_check_path).free + except Exception: + free_space = None + if free_space is not None and free_space < total_size * 1.2: # 留20%余量 + msg = f"磁盘空间不足: 需要 {total_size/(1024**3):.2f}GB, 可用 {free_space/(1024**3):.2f}GB" + self.log_message.emit(msg) + return False + + copied_files: list[tuple[Path, Path, dict]] = [] + for idx, item in enumerate(files, 1): + if not isinstance(item, dict): + continue + rel = item.get('path') + if not rel or not isinstance(rel, str): + continue + rel_norm = rel.replace('\\', '/') + src_path = (src_root / rel_norm) + dst_path = (dst_root / rel_norm) + + # 安全:只允许搬运 src_root 下的内容 + try: + if not src_path.resolve().is_relative_to(src_root.resolve()): + continue + except Exception: + continue + + if src_path.is_dir(): + dst_path.mkdir(parents=True, exist_ok=True) + continue + + if not src_path.exists(): + # 允许清单中包含不存在项(例如不同包型差异),跳过即可 + continue + + dst_path.parent.mkdir(parents=True, exist_ok=True) + + self.log_message.emit(rel_norm) + self.progress_changed.emit(idx, total) + + try: + actual_size, actual_sha = self._copy_and_hash(src_path, dst_path) + except Exception as e: + self.log_message.emit(f"复制文件失败,跳过: {rel} err={e}") + with contextlib.suppress(Exception): + dst_path.unlink(missing_ok=True) + continue + + expected_size = item.get('size') + if isinstance(expected_size, int) and expected_size >= 0: + if actual_size != expected_size: + self.log_message.emit(f"文件大小校验失败,跳过: {rel}") + with contextlib.suppress(Exception): + dst_path.unlink(missing_ok=True) + continue + + expected_sha = item.get('sha256') + if isinstance(expected_sha, str) and expected_sha: + if actual_sha != expected_sha.upper(): + self.log_message.emit(f"文件哈希校验失败,跳过: {rel}") + with contextlib.suppress(Exception): + dst_path.unlink(missing_ok=True) + continue + + copied_files.append((src_path, dst_path, item)) + + # 进入清理阶段:emit (-1, -1) 当清理阶段的双行状态标志 + self.progress_changed.emit(-1, -1) + + # 复制成功后删除源文件(跳过正在运行的安装器 exe) + for src_path, _, _ in copied_files: + with contextlib.suppress(Exception): + if running_exe is not None: + try: + if src_path.resolve() == running_exe: + continue + except Exception: + if str(src_path).lower() == str(running_exe).lower(): + continue + src_path.unlink(missing_ok=True) + + # 尝试清理空目录:仅清理本次搬运涉及到的路径链,避免误删用户原本存在但为空的目录 + dirs_to_try: set[Path] = set() + for src_path, _, _ in copied_files: + parent = src_path.parent + while True: + if parent == src_root: + break + # 只处理 src_root 下的目录 + try: + if not parent.resolve().is_relative_to(src_root.resolve()): + break + except Exception: + # resolve 失败时,退化为字符串前缀判断 + if not str(parent).lower().startswith(str(src_root).lower()): + break + + dirs_to_try.add(parent) + if parent.parent == parent: + break + parent = parent.parent + + for p in sorted(dirs_to_try, key=lambda x: len(str(x)), reverse=True): + with contextlib.suppress(Exception): + p.rmdir() + + # 清单文件本身不在清单列表中(避免自引用 sha 问题),这里单独搬运 + manifest_src = src_root / 'install_manifest.json' + manifest_dst = dst_root / 'install_manifest.json' + if manifest_src.exists(): + try: + manifest_dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(manifest_src, manifest_dst) + with contextlib.suppress(Exception): + manifest_src.unlink(missing_ok=True) + except Exception as e: + self.log_message.emit(f"搬运清单文件失败: {e}") + + return True + + def run(self) -> None: + """线程入口:若安装器目录与工作目录相同则视为已就位,否则执行清单搬运。""" + src_root = Path(self.installer_dir) + dst_root = Path(self.work_dir) + + # 安装器目录与工作目录相同,无需搬运,直接视为成功 + if self._is_same_dir(src_root, dst_root): + self._finish(True) + return + + # 无清单说明安装目录不含待搬运资源(开发环境 / 在线安装等),视为无需解包 + if not (src_root / 'install_manifest.json').exists(): + self._finish(True) + return + + self.log_message.emit("正在读取安装清单...") + + # 逐文件复制+校验,成功后删除源文件;异常视为失败 + try: + ok = self._copy_by_manifest_then_cleanup(src_root, dst_root) + self._finish(ok) + except Exception as e: + self.log_message.emit(f"解包资源失败: {e}") + self._finish(False) + + def _finish(self, success: bool) -> None: + """记录结果并发射完成信号。""" + self.is_done = True + self.is_success = success + self.unpack_done.emit(success) + + @staticmethod + def _is_same_dir(a: Path, b: Path) -> bool: + """判断两个路径是否指向同一目录(通过 resolve 消除符号链接和相对路径差异)。""" + try: + return a.resolve() == b.resolve() + except Exception: + # 路径 resolve 失败时降级为字符串比较(不区分大小写) + return str(a).lower() == str(b).lower() diff --git a/src/one_dragon_qt/utils/layout_utils.py b/src/one_dragon_qt/utils/layout_utils.py index 00d92d9397..98f10ec62a 100644 --- a/src/one_dragon_qt/utils/layout_utils.py +++ b/src/one_dragon_qt/utils/layout_utils.py @@ -1,15 +1,13 @@ -class Margins(): - def __init__(self, - left:int = 0, - top:int = 0, - right:int = 0, - bottom:int = 0): - self.left = left - self.top = top - self.right = right - self.bottom = bottom - -class IconSize(): - def __init__(self, width:int, height:int): - self.width = width - self.height = height \ No newline at end of file +from typing import NamedTuple + + +class Margins(NamedTuple): + left: int = 0 + top: int = 0 + right: int = 0 + bottom: int = 0 + + +class IconSize(NamedTuple): + width: int = 0 + height: int = 0 diff --git a/src/one_dragon_qt/view/code_interface.py b/src/one_dragon_qt/view/code_interface.py index 4be9954d89..f83198d7ce 100644 --- a/src/one_dragon_qt/view/code_interface.py +++ b/src/one_dragon_qt/view/code_interface.py @@ -231,12 +231,17 @@ def on_reset_commit_clicked(self) -> None: """ btn = self.sender() commit_id = btn.property('commit') - success = self.ctx.git_service.reset_to_commit(commit_id) + success, msg = self.ctx.git_service.reset_to_commit(commit_id) if success: self.code_card.updated = True self.code_card.check_and_update_display() self.page_num = -1 self.start_fetch_total() + elif msg: + dialog = Dialog(gt('回滚失败'), msg, self) + dialog.setTitleBarVisible(False) + dialog.cancelButton.hide() + dialog.exec() def _on_custom_branch_edited(self) -> None: text = self.custom_git_branch_lineedit.text() diff --git a/src/one_dragon_qt/view/devtools/devtools_image_analysis_interface.py b/src/one_dragon_qt/view/devtools/devtools_image_analysis_interface.py index bbbacd27e1..f079bc72e4 100644 --- a/src/one_dragon_qt/view/devtools/devtools_image_analysis_interface.py +++ b/src/one_dragon_qt/view/devtools/devtools_image_analysis_interface.py @@ -78,6 +78,7 @@ def _init_signal_connections(self): self.down_btn.clicked.connect(self._on_move_step_down) self.add_step_combo.currentIndexChanged.connect(self._on_add_step_by_combo) self.run_btn.clicked.connect(self._on_run_pipeline) + self.screenshot_btn.clicked.connect(self._on_screenshot_clicked) self.toggle_view_btn.clicked.connect(self._on_toggle_view) self.color_channel_btn.clicked.connect(self._on_color_channel_clicked) self.pipeline_list_widget.currentItemChanged.connect(self._on_pipeline_selection_changed) @@ -171,6 +172,8 @@ def _init_op_buttons(self) -> QWidget: layout.addStretch(1) self.open_btn = PushButton(text=gt('打开图片'), icon=FluentIcon.DOCUMENT) layout.addWidget(self.open_btn) + self.screenshot_btn = PushButton(text=gt('截图'), icon=FluentIcon.CAMERA) + layout.addWidget(self.screenshot_btn) self.toggle_view_btn = PushButton(text=gt('切换视图')) layout.addWidget(self.toggle_view_btn) self.run_btn = PushButton(text=gt('执行'), icon=FluentIcon.PLAY_SOLID) @@ -661,6 +664,16 @@ def _on_open_image(self): self._display_image(self.logic.get_display_image()) self._update_toggle_button_text() + def _on_screenshot_clicked(self) -> None: + """ + 响应截图按钮 + """ + _, screen = self.ctx.controller.screenshot() + if screen is not None: + if self.logic.load_image_from_array(screen): + self._display_image(self.logic.get_display_image()) + self._update_toggle_button_text() + def _on_image_pasted(self, image_data) -> None: """ 通过拖放或粘贴加载图片后的回调 diff --git a/src/one_dragon_qt/view/devtools/devtools_screen_manage_interface.py b/src/one_dragon_qt/view/devtools/devtools_screen_manage_interface.py index 4d31ae4d4a..badd43a69f 100644 --- a/src/one_dragon_qt/view/devtools/devtools_screen_manage_interface.py +++ b/src/one_dragon_qt/view/devtools/devtools_screen_manage_interface.py @@ -1,10 +1,14 @@ +import ast import os +from collections.abc import Callable from contextlib import suppress +from dataclasses import dataclass from typing import Any from PySide6.QtCore import QObject, Qt, Signal from PySide6.QtGui import QKeyEvent from PySide6.QtWidgets import ( + QDialog, QFileDialog, QHBoxLayout, QTableWidgetItem, @@ -15,6 +19,7 @@ BodyLabel, CheckBox, FluentIcon, + InfoBarIcon, LineEdit, PushButton, ScrollArea, @@ -39,6 +44,7 @@ from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log from one_dragon_qt.mixins.history_mixin import HistoryMixin +from one_dragon_qt.utils.layout_utils import Margins from one_dragon_qt.widgets.cv2_image import Cv2Image from one_dragon_qt.widgets.editable_combo_box import EditableComboBox from one_dragon_qt.widgets.row import Row @@ -56,22 +62,58 @@ class ScreenInfoWorker(QObject): signal = Signal() -AREA_FIELD_2_COLUMN: dict[str, int] = { - '操作': 0, - '标识': 1, - '区域名称': 2, - '位置': 3, - '文本': 4, - '阈值1': 5, - '模板': 6, - '阈值2': 7, - '颜色范围': 8, - '前往画面': 9, -} +@dataclass +class ColumnMeta: + """表格列元数据""" + display_name: str + attr_name: str | None = None + parser: Callable[[str], Any] | None = None + width: int | None = None # None = 自动宽度 + formatter: Callable[[Any], str] | None = None # 属性值 → 显示文本,None = str() + + +def _parse_rect(text: str) -> Rect: + """解析矩形,校验为 (x1, y1, x2, y2) 结构。""" + stripped = text.strip().strip('()[]') + parts = [p.strip() for p in stripped.split(',')] + if len(parts) != 4: + raise ValueError(f'需要 4 个坐标值,实际: {len(parts)} 个') + return Rect(*(int(p) for p in parts)) + + +def _parse_color_range(text: str) -> list[list[int]] | None: + """解析颜色范围,校验为 [[r,g,b],[r,g,b]] 结构。""" + if not text.strip(): + return None + val = ast.literal_eval(text) + if (isinstance(val, list) and len(val) == 2 + and all(isinstance(v, list) and len(v) == 3 for v in val)): + return val + raise ValueError(f'需要 [[r,g,b],[r,g,b]],实际: {val}') class DevtoolsScreenManageInterface(VerticalScrollInterface, HistoryMixin): + AREA_COLUMNS: list[ColumnMeta] = [ + ColumnMeta('操作', width=40), + ColumnMeta('标识', width=40), + ColumnMeta('区域名称', 'area_name', lambda x: x), + ColumnMeta('位置', 'pc_rect', _parse_rect, 200), + ColumnMeta('OCR文本', 'text', lambda x: x), + ColumnMeta('OCR阈值', 'lcs_percent', lambda x: float(x) if x else 0.5, 70), + ColumnMeta('模板目录', 'template_sub_dir', lambda x: x), + ColumnMeta('模板ID', 'template_id', lambda x: x), + ColumnMeta('模板阈值', 'template_match_threshold', lambda x: float(x) if x else 0.7, 70), + ColumnMeta('颜色范围', 'color_range', _parse_color_range, + formatter=lambda v: '' if v is None else str(v)), + ColumnMeta('前往画面', 'goto_list', lambda x: [i.strip() for i in x.split(',') if i.strip()], + formatter=lambda v: ','.join(v) if v else ''), + ColumnMeta('手柄键', 'gamepad_key', lambda x: x.strip() or None, 120, + formatter=lambda v: '' if v is None else str(v)), + ] + + AREA_FIELD_2_COLUMN: dict[str, int] = {col.display_name: idx for idx, col in enumerate(AREA_COLUMNS)} + def __init__(self, ctx: OneDragonContext, parent=None): VerticalScrollInterface.__init__( self, @@ -130,7 +172,7 @@ def _init_left_part(self) -> QWidget: self.merge_opt.clicked.connect(self._on_merge_clicked) control_layout.addWidget(self.merge_opt) - btn_row = Row(spacing=6, margins=(0, 0, 0, 0)) + btn_row = Row(spacing=6, margins=Margins(0, 0, 0, 0)) control_layout.addWidget(btn_row) self.existed_yml_btn = EditableComboBox() @@ -157,7 +199,7 @@ def _init_left_part(self) -> QWidget: btn_row.add_stretch(1) - img_btn_row = Row(spacing=6, margins=(0, 0, 0, 0)) + img_btn_row = Row(spacing=6, margins=Margins(0, 0, 0, 0)) control_layout.addWidget(img_btn_row) self.pc_alt_opt = CheckBox(text=gt('PC 点击需 Alt')) @@ -170,6 +212,10 @@ def _init_left_part(self) -> QWidget: self.choose_image_btn.clicked.connect(self.choose_existed_image) img_btn_row.add_widget(self.choose_image_btn) + self.screenshot_btn = PushButton(text=gt('截图')) + self.screenshot_btn.clicked.connect(self._on_screenshot_clicked) + img_btn_row.add_widget(self.screenshot_btn) + self.choose_template_btn = PushButton(text=gt('导入模板区域')) self.choose_template_btn.clicked.connect(self.choose_existed_template) img_btn_row.add_widget(self.choose_template_btn) @@ -194,9 +240,16 @@ def _init_left_part(self) -> QWidget: ) control_layout.addWidget(self.screen_info_opt) + self._control_layout = control_layout self.table_widget = self._init_area_table_widget() control_layout.addWidget(self.table_widget, stretch=1) + self.popup_table_btn = PushButton(text=gt('弹出表格')) + self.popup_table_btn.clicked.connect(self._on_popup_table) + control_layout.addWidget(self.popup_table_btn) + + self._popup_win: QDialog | None = None + scroll_area.setWidget(control_widget) scroll_area.setWidgetResizable(True) @@ -219,21 +272,21 @@ def _init_area_table_widget(self) -> QWidget: self.area_table = TableWidget() self.area_table.cellChanged.connect(self._on_area_table_cell_changed) - self.area_table.setMinimumWidth(980) + self.area_table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.area_table.setBorderVisible(True) self.area_table.setBorderRadius(8) self.area_table.setWordWrap(True) - self.area_table.setColumnCount(len(AREA_FIELD_2_COLUMN)) + self.area_table.setColumnCount(len(self.AREA_COLUMNS)) self.area_table.verticalHeader().hide() - self.area_table.setHorizontalHeaderLabels([ - gt(key) - for key in AREA_FIELD_2_COLUMN - ]) - self.area_table.setColumnWidth(AREA_FIELD_2_COLUMN['操作'], 40) - self.area_table.setColumnWidth(AREA_FIELD_2_COLUMN['标识'], 40) - self.area_table.setColumnWidth(AREA_FIELD_2_COLUMN['位置'], 200) - self.area_table.setColumnWidth(AREA_FIELD_2_COLUMN['阈值1'], 70) - self.area_table.setColumnWidth(AREA_FIELD_2_COLUMN['阈值2'], 70) + self.area_table.setHorizontalHeaderLabels([gt(col.display_name) for col in self.AREA_COLUMNS]) + for idx, col in enumerate(self.AREA_COLUMNS): + if col.width is not None: + self.area_table.setColumnWidth(idx, col.width) + + # 让表格宽度始终等于所有列宽之和 + self._sync_table_width() + self.area_table.horizontalHeader().sectionResized.connect(self._on_table_column_resized) + # table的行被选中时 触发 self.area_table_row_selected: int = -1 # 选中的行 self.area_table.cellClicked.connect(self.on_area_table_cell_clicked) @@ -244,11 +297,64 @@ def _init_area_table_widget(self) -> QWidget: return widget + def _sync_table_width(self) -> None: + """同步表格宽度为所有列宽之和。""" + total = sum( + self.area_table.columnWidth(c) + for c in range(self.area_table.columnCount()) + ) + self.area_table.setFixedWidth(total + 2) + + def _on_table_column_resized(self, _index: int, _old: int, _new: int) -> None: + """列宽变化时同步表格整体宽度。""" + self._sync_table_width() + + def _on_popup_table(self) -> None: + """弹出区域表格到独立窗口。""" + if self._popup_win is not None: + self._popup_win.activateWindow() + return + + self._popup_win = QDialog(self) + self._popup_win.setWindowTitle(gt('区域表格编辑')) + self._popup_win.setWindowFlags( + self._popup_win.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint + ) + self._popup_win.setMinimumSize(1200, 600) + self._popup_win.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + self._popup_win.destroyed.connect(self._on_popup_closed) + + popup_layout = QVBoxLayout(self._popup_win) + popup_layout.setContentsMargins(4, 4, 4, 4) + + # 将表格卡片移到弹出窗口 + self.table_widget.setParent(self._popup_win) + popup_layout.addWidget(self.table_widget) + self.table_widget.show() + self.popup_table_btn.hide() + + # 占位拉伸,把其他控件挤到顶部 + self._control_layout.addStretch(1) + + self._popup_win.show() + + def _on_popup_closed(self) -> None: + """弹出窗口关闭后,将表格放回原位。""" + # 移除占位拉伸(layout 最后一个 item) + last = self._control_layout.count() - 1 + spacer = self._control_layout.itemAt(last) + if spacer is not None and spacer.widget() is None: + self._control_layout.removeItem(spacer) + + # 插到弹出按钮前面 + btn_idx = self._control_layout.indexOf(self.popup_table_btn) + self._control_layout.insertWidget(btn_idx, self.table_widget, stretch=1) + self.table_widget.show() + self.popup_table_btn.show() + self._popup_win = None + def _update_existed_yml_options(self) -> None: - """ - 更新已有的yml选项 - :return: - """ + """更新已有的yml选项。""" self.existed_yml_btn.set_items([ ConfigItem(i.screen_name) for i in self.ctx.screen_loader.screen_info_list @@ -285,20 +391,13 @@ def _init_right_part(self) -> QWidget: return widget - def on_interface_shown(self) -> None: - """ - 子界面显示时 进行初始化 - :return: - """ + """子界面显示时 进行初始化。""" VerticalScrollInterface.on_interface_shown(self) self._update_display_by_screen() def _update_display_by_screen(self) -> None: - """ - 根据画面图片,统一更新界面的显示 - :return: - """ + """根据画面图片,统一更新界面的显示。""" chosen = self.chosen_screen is not None self.merge_opt.setDisabled(chosen) @@ -327,10 +426,7 @@ def _update_display_by_screen(self) -> None: self._update_area_table_display() def _update_area_table_display(self): - """ - 更新区域表格的显示 - :return: - """ + """更新区域表格的显示。""" self.area_table.blockSignals(True) area_list = [] if self.chosen_screen is None else self.chosen_screen.area_list area_cnt = len(area_list) @@ -338,6 +434,7 @@ def _update_area_table_display(self): for idx in range(area_cnt): area_item = area_list[idx] + del_btn = ToolButton(FluentIcon.DELETE, parent=None) del_btn.setFixedSize(32, 32) del_btn.clicked.connect(self._on_row_delete_clicked) @@ -346,42 +443,31 @@ def _update_area_table_display(self): id_check.setChecked(area_item.id_mark) id_check.setProperty('area_name', area_item.area_name) id_check.stateChanged.connect(self.on_area_id_check_changed) + id_check.setFixedSize(32, 32) + id_check.setStyleSheet(id_check.styleSheet() + 'CheckBox { margin-left: 8px; }') self.area_table.setCellWidget(idx, 0, del_btn) self.area_table.setCellWidget(idx, 1, id_check) - self.area_table.setItem(idx, 2, QTableWidgetItem(area_item.area_name)) - self.area_table.setItem(idx, 3, QTableWidgetItem(str(area_item.pc_rect))) - self.area_table.setItem(idx, 4, QTableWidgetItem(area_item.text)) - self.area_table.setItem(idx, 5, QTableWidgetItem(str(area_item.lcs_percent))) - self.area_table.setItem(idx, 6, QTableWidgetItem(area_item.template_id_display_text)) - self.area_table.setItem(idx, 7, QTableWidgetItem(str(area_item.template_match_threshold))) - self.area_table.setItem(idx, 8, QTableWidgetItem(str(area_item.color_range_display_text))) - self.area_table.setItem(idx, 9, QTableWidgetItem(area_item.goto_list_display_text)) + for col_idx, col in enumerate(self.AREA_COLUMNS): + if col.attr_name is None: + continue + val = getattr(area_item, col.attr_name) + text = col.formatter(val) if col.formatter else str(val) + self.area_table.setItem(idx, col_idx, QTableWidgetItem(text)) # 最后一行 只保留一个新增按钮 add_btn = ToolButton(FluentIcon.ADD, parent=None) add_btn.setFixedSize(32, 32) add_btn.clicked.connect(self._on_area_add_clicked) self.area_table.setCellWidget(area_cnt, 0, add_btn) - self.area_table.setItem(area_cnt, 1, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 2, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 3, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 4, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 5, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 6, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 7, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 8, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 9, QTableWidgetItem('')) - self.area_table.setItem(area_cnt, 10, QTableWidgetItem('')) + for col_idx in range(1, len(self.AREA_COLUMNS)): + self.area_table.setItem(area_cnt, col_idx, QTableWidgetItem('')) self.area_table.blockSignals(False) def _update_image_display(self): - """ - 更新图片显示 - :return: - """ + """更新图片显示。""" image_to_show = None if self.chosen_screen is None else self.chosen_screen.get_image_to_show(self.area_table_row_selected) if image_to_show is not None: image = Cv2Image(image_to_show) @@ -394,11 +480,7 @@ def _update_image_display(self): self.image_label.setImage(None) def _on_choose_existed_yml(self, screen_name: str): - """ - 选择了已有的yml - :param screen_name: - :return: - """ + """选择了已有的yml。""" self.chosen_screen = None # 搜索时 输入了一半时候会找到对应的画面 with suppress(Exception): @@ -411,10 +493,7 @@ def _on_choose_existed_yml(self, screen_name: str): self._whole_update.signal.emit() def _on_create_clicked(self): - """ - 创建一个新的 - :return: - """ + """创建一个新的。""" if self.chosen_screen is not None: return @@ -424,10 +503,7 @@ def _on_create_clicked(self): self._whole_update.signal.emit() def _on_save_clicked(self) -> None: - """ - 保存 - :return: - """ + """保存。""" if self.chosen_screen is None: return @@ -435,10 +511,7 @@ def _on_save_clicked(self) -> None: self._existed_yml_update.signal.emit() def _on_delete_clicked(self) -> None: - """ - 删除 - :return: - """ + """删除。""" if self.chosen_screen is None: return self.ctx.screen_loader.delete_screen(self.chosen_screen.screen_id) @@ -447,10 +520,7 @@ def _on_delete_clicked(self) -> None: self._existed_yml_update.signal.emit() def _on_cancel_clicked(self) -> None: - """ - 取消编辑 - :return: - """ + """取消编辑。""" self.chosen_screen = None self.existed_yml_btn.blockSignals(True) self.existed_yml_btn.setCurrentIndex(-1) @@ -463,10 +533,7 @@ def _on_cancel_clicked(self) -> None: self._whole_update.signal.emit() def choose_existed_image(self) -> None: - """ - 选择已有的环图片 - :return: - """ + """选择已有的环图片。""" default_dir = os_utils.get_path_under_work_dir('.debug', 'images') if self.last_screen_dir is not None: default_dir = self.last_screen_dir @@ -488,23 +555,38 @@ def choose_existed_image(self) -> None: self._on_image_chosen(fix_file_path) def _on_image_chosen(self, image_file_path: str) -> None: - """ - 选择图片之后的回调 - :param image_file_path: - :return: - """ + """选择图片之后的回调。""" if self.chosen_screen is None: return self.chosen_screen.screen_image = cv2_utils.read_image(image_file_path) self._image_update.signal.emit() - def _on_image_pasted(self, image_data) -> None: + def _on_screenshot_clicked(self) -> None: """ - 通过拖放或粘贴加载图片后的回调,等同于"选择图片" - :param image_data: 文件路径 (str) 或 numpy 数组 (RGB 格式) + 截图按钮点击 :return: """ + _, screen = self.ctx.controller.screenshot() + if screen is None: + return + + if self.chosen_screen is None: + # 没有选中画面时,自动创建一个新的 + self.chosen_screen = ScreenInfo({}) + # 清除撤回记录 + self._clear_history() + self._whole_update.signal.emit() + + self.chosen_screen.screen_image = screen + self._image_update.signal.emit() + + def _on_image_pasted(self, image_data) -> None: + """通过拖放或粘贴加载图片后的回调,等同于“选择图片”。 + + Args: + image_data: 文件路径 (str) 或 numpy 数组 (RGB 格式) + """ if self.chosen_screen is None: return @@ -540,10 +622,10 @@ def choose_existed_template(self) -> None: self._on_template_chosen(fix_file_path) def _on_template_chosen(self, template_file_path: str) -> None: - """ - 选择模板后 导入模板对应的区域 - :param template_file_path: 模板文件路径 - :return: + """选择模板后 导入模板对应的区域。 + + Args: + template_file_path: 模板文件路径 """ if self.chosen_screen is None: return @@ -589,10 +671,7 @@ def _on_pc_alt_changed(self, checked: bool) -> None: self.chosen_screen.pc_alt = self.pc_alt_opt.isChecked() def _on_area_add_clicked(self) -> None: - """ - 新增一个区域 - :return: - """ + """新增一个区域。""" if self.chosen_screen is None: return @@ -600,10 +679,7 @@ def _on_area_add_clicked(self) -> None: self._area_table_update.signal.emit() def _on_row_delete_clicked(self): - """ - 删除一行 - :return: - """ + """删除一行。""" if self.chosen_screen is None: return @@ -615,12 +691,7 @@ def _on_row_delete_clicked(self): self._image_update.signal.emit() def _on_area_table_cell_changed(self, row: int, column: int) -> None: - """ - 表格内容改变 - :param row: - :param column: - :return: - """ + """表格内容改变。""" if self.chosen_screen is None: return if row < 0 or row >= len(self.chosen_screen.area_list): @@ -628,44 +699,33 @@ def _on_area_table_cell_changed(self, row: int, column: int) -> None: area_item = self.chosen_screen.area_list[row] text = self.area_table.item(row, column).text().strip() - # 列映射:列索引 -> (属性名, 处理函数) - column_handlers = { - AREA_FIELD_2_COLUMN['区域名称']: ('area_name', lambda x: x), - AREA_FIELD_2_COLUMN['位置']: ('pc_rect', self._parse_rect_from_text), - AREA_FIELD_2_COLUMN['文本']: ('text', lambda x: x), - AREA_FIELD_2_COLUMN['阈值1']: ('lcs_percent', lambda x: float(x) if len(x) > 0 else 0.5), - AREA_FIELD_2_COLUMN['模板']: ('template', self._parse_template_from_text), - AREA_FIELD_2_COLUMN['阈值2']: ('template_match_threshold', lambda x: float(x) if len(x) > 0 else 0.7), - AREA_FIELD_2_COLUMN['颜色范围']: ('color_range', self._parse_color_range_from_text), - AREA_FIELD_2_COLUMN['前往画面']: ('goto_list', lambda x: [i.strip() for i in x.split(',') if i.strip()]) - } - if column not in column_handlers: + # 直接从 AREA_COLUMNS 获取属性名和解析器 + if column >= len(self.AREA_COLUMNS): return - - attr_name, handler = column_handlers[column] + col_meta = self.AREA_COLUMNS[column] + if col_meta.attr_name is None: + return + attr_name = col_meta.attr_name + handler = col_meta.parser # 记录修改前的状态 - if attr_name == 'template': - old_value = f"{area_item.template_sub_dir}.{area_item.template_id}" if area_item.template_sub_dir else area_item.template_id - elif attr_name == 'pc_rect': - old_value = Rect(area_item.pc_rect.x1, area_item.pc_rect.y1, area_item.pc_rect.x2, area_item.pc_rect.y2) - elif attr_name == 'goto_list': - old_value = area_item.goto_list.copy() if area_item.goto_list else [] - else: - old_value = getattr(area_item, attr_name) + old_value = getattr(area_item, attr_name) # 应用新值 try: new_value = handler(text) - if attr_name == 'template': - area_item.template_sub_dir, area_item.template_id = new_value - elif attr_name == 'pc_rect': - area_item.pc_rect = new_value + setattr(area_item, attr_name, new_value) + if attr_name == 'pc_rect': self._image_update.signal.emit() - else: - setattr(area_item, attr_name, new_value) - except Exception: + except Exception as e: # 如果解析失败,不进行修改 + log.error('解析失败', exc_info=True) + self.show_info_bar( + '解析失败', + f'{col_meta.display_name}: {e}', + icon=InfoBarIcon.ERROR, + duration=5000, + ) return # 添加到撤回历史记录 @@ -678,40 +738,12 @@ def _on_area_table_cell_changed(self, row: int, column: int) -> None: } self._add_history_record(table_change) - def _parse_rect_from_text(self, text: str) -> Rect: - """解析文本为矩形对象""" - num_list = [int(i) for i in text[1:-1].split(',')] - while len(num_list) < 4: - num_list.append(0) - return Rect(num_list[0], num_list[1], num_list[2], num_list[3]) - - def _parse_template_from_text(self, text: str) -> tuple[str, str]: - """解析模板文本为 (sub_dir, template_id)""" - if len(text) == 0: - return '', '' - template_list = text.split('.') - if len(template_list) > 1: - return template_list[0], template_list[1] - else: - return '', template_list[0] - - def _parse_color_range_from_text(self, text: str): - """解析颜色范围文本""" - try: - import json - arr = json.loads(text) - if isinstance(arr, list): - return arr - except Exception: - pass - return None - def _on_image_left_clicked(self, x: int, y: int) -> None: - """ - 图片上左键单击后显示坐标 - :param x: 点击的x坐标 - :param y: 点击的y坐标 - :return: + """图片上左键单击后显示坐标。 + + Args: + x: 点击的x坐标 + y: 点击的y坐标 """ if self.chosen_screen is None or self.chosen_screen.screen_image is None: return @@ -720,14 +752,7 @@ def _on_image_left_clicked(self, x: int, y: int) -> None: self.y_pos_label.setText(str(y)) def _on_image_rect_selected(self, x1: int, y1: int, x2: int, y2: int) -> None: - """ - 在图片上选择一个区域后的回调 - :param x1: - :param y1: - :param x2: - :param y2: - :return: - """ + """在图片上选择一个区域后的回调。""" if self.chosen_screen is None or self.area_table_row_selected is None: return if self.area_table_row_selected < 0 or self.area_table_row_selected >= len(self.chosen_screen.area_list): @@ -746,7 +771,7 @@ def _on_image_rect_selected(self, x1: int, y1: int, x2: int, y2: int) -> None: self._add_history_record(rect_change) self.area_table.blockSignals(True) - self.area_table.item(self.area_table_row_selected, AREA_FIELD_2_COLUMN['位置']).setText(f'({x1}, {y1}, {x2}, {y2})') + self.area_table.item(self.area_table_row_selected, self.AREA_FIELD_2_COLUMN['位置']).setText(f'({x1}, {y1}, {x2}, {y2})') self.area_table.blockSignals(False) area_item.pc_rect = Rect(x1, y1, x2, y2) @@ -815,16 +840,7 @@ def _apply_undo(self, change_record: dict[str, Any]) -> None: area_item = self.chosen_screen.area_list[row_index] # 根据修改类型恢复原值 - if change_type == 'template': - if '.' in old_value: - template_list = old_value.split('.') - area_item.template_sub_dir = template_list[0] - area_item.template_id = template_list[1] - else: - area_item.template_sub_dir = '' - area_item.template_id = old_value - else: - setattr(area_item, change_type, old_value) + setattr(area_item, change_type, old_value) # 如果是坐标修改,需要更新图像显示 if change_type == 'pc_rect': @@ -848,7 +864,7 @@ def _apply_undo(self, change_record: dict[str, Any]) -> None: # 更新表格显示 self.area_table.blockSignals(True) - self.area_table.item(row_index, AREA_FIELD_2_COLUMN['位置']).setText(f'({old_rect.x1}, {old_rect.y1}, {old_rect.x2}, {old_rect.y2})') + self.area_table.item(row_index, self.AREA_FIELD_2_COLUMN['位置']).setText(f'({old_rect.x1}, {old_rect.y1}, {old_rect.x2}, {old_rect.y2})') self.area_table.blockSignals(False) # 更新图像显示 @@ -873,30 +889,13 @@ def _apply_redo(self, change_record: dict[str, Any]) -> None: area_item = self.chosen_screen.area_list[row_index] - # 根据修改类型恢复新值 - if change_type == 'template': - if len(new_value) == 0: - area_item.template_sub_dir = '' - area_item.template_id = '' - else: - template_list = new_value.split('.') - if len(template_list) > 1: - area_item.template_sub_dir = template_list[0] - area_item.template_id = template_list[1] - else: - area_item.template_sub_dir = '' - area_item.template_id = template_list[0] - elif change_type == 'pc_rect': - rect_value = self._parse_rect_from_text(new_value) - area_item.pc_rect = rect_value + # 将文本恢复为正确类型 + parser = next((col.parser for col in self.AREA_COLUMNS if col.attr_name == change_type), None) + parsed = parser(new_value) if parser else new_value + setattr(area_item, change_type, parsed) + + if change_type == 'pc_rect': self._image_update.signal.emit() - else: - if change_type == 'lcs_percent' or change_type == 'template_match_threshold': - setattr(area_item, change_type, float(new_value) if len(new_value) > 0 else (0.5 if change_type == 'lcs_percent' else 0.7)) - elif change_type == 'goto_list': - setattr(area_item, change_type, new_value.split(',')) - else: - setattr(area_item, change_type, new_value) # 更新表格显示 self._area_table_update.signal.emit() @@ -916,7 +915,7 @@ def _apply_redo(self, change_record: dict[str, Any]) -> None: # 更新表格显示 self.area_table.blockSignals(True) - self.area_table.item(row_index, AREA_FIELD_2_COLUMN['位置']).setText(f'({new_rect.x1}, {new_rect.y1}, {new_rect.x2}, {new_rect.y2})') + self.area_table.item(row_index, self.AREA_FIELD_2_COLUMN['位置']).setText(f'({new_rect.x1}, {new_rect.y1}, {new_rect.x2}, {new_rect.y2})') self.area_table.blockSignals(False) # 更新图像显示 diff --git a/src/one_dragon_qt/view/devtools/devtools_template_helper_interface.py b/src/one_dragon_qt/view/devtools/devtools_template_helper_interface.py index 902077538c..5dc4fb0563 100644 --- a/src/one_dragon_qt/view/devtools/devtools_template_helper_interface.py +++ b/src/one_dragon_qt/view/devtools/devtools_template_helper_interface.py @@ -1,26 +1,48 @@ import os +from typing import Any + import cv2 -from PySide6.QtWidgets import QWidget, QSizePolicy, QFileDialog, QTableWidgetItem, QMessageBox, QVBoxLayout, QHBoxLayout from PySide6.QtCore import Qt from PySide6.QtGui import QKeyEvent -from qfluentwidgets import (FluentIcon, InfoBarIcon, PushButton, ToolButton, CaptionLabel, LineEdit, - SingleDirectionScrollArea, TableWidget, TeachingTip, TeachingTipTailPosition) -from typing import Optional, Any +from PySide6.QtWidgets import ( + QFileDialog, + QHBoxLayout, + QMessageBox, + QSizePolicy, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) +from qfluentwidgets import ( + CaptionLabel, + FluentIcon, + InfoBarIcon, + LineEdit, + PushButton, + SingleDirectionScrollArea, + TableWidget, + TeachingTip, + TeachingTipTailPosition, + ToolButton, +) from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.geometry.point import Point from one_dragon.base.operation.one_dragon_context import OneDragonContext from one_dragon.base.screen.template_info import TemplateInfo, TemplateShapeEnum -from one_dragon.utils import os_utils, cv2_utils +from one_dragon.utils import cv2_utils, os_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log from one_dragon_qt.mixins.history_mixin import HistoryMixin +from one_dragon_qt.utils.layout_utils import Margins from one_dragon_qt.widgets.combo_box import ComboBox from one_dragon_qt.widgets.cv2_image import Cv2Image from one_dragon_qt.widgets.editable_combo_box import EditableComboBox from one_dragon_qt.widgets.fixed_size_image_label import FixedSizeImageLabel from one_dragon_qt.widgets.row import Row -from one_dragon_qt.widgets.setting_card.multi_push_setting_card import MultiPushSettingCard +from one_dragon_qt.widgets.setting_card.multi_push_setting_card import ( + MultiPushSettingCard, +) from one_dragon_qt.widgets.setting_card.switch_setting_card import SwitchSettingCard from one_dragon_qt.widgets.setting_card.text_setting_card import TextSettingCard from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface @@ -40,8 +62,8 @@ def __init__(self, ctx: OneDragonContext, parent=None): self._init_history() # 初始化历史记录功能 self.ctx: OneDragonContext = ctx - self.chosen_template: Optional[TemplateInfo] = None - self.last_screen_dir: Optional[str] = None # 上一次选择的图片路径 + self.chosen_template: TemplateInfo | None = None + self.last_screen_dir: str | None = None # 上一次选择的图片路径 def get_content_widget(self) -> QWidget: @@ -68,7 +90,7 @@ def _init_left_part(self) -> QWidget: control_layout.setContentsMargins(12, 0, 12, 0) control_layout.setSpacing(6) - btn_row = Row(spacing=6, margins=(0, 0, 0, 0)) + btn_row = Row(spacing=6, margins=Margins(0, 0, 0, 0)) control_layout.addWidget(btn_row) self.existed_yml_btn = EditableComboBox() @@ -92,7 +114,7 @@ def _init_left_part(self) -> QWidget: self.cancel_btn.clicked.connect(self._on_cancel_clicked) btn_row.add_widget(self.cancel_btn) - save_row = Row(spacing=6, margins=(0, 0, 0, 0)) + save_row = Row(spacing=6, margins=Margins(0, 0, 0, 0)) control_layout.addWidget(save_row) save_row.add_stretch(1) @@ -101,6 +123,10 @@ def _init_left_part(self) -> QWidget: self.choose_image_btn.clicked.connect(self.choose_existed_image) save_row.add_widget(self.choose_image_btn) + self.screenshot_btn = PushButton(text=gt('截图')) + self.screenshot_btn.clicked.connect(self._on_screenshot_clicked) + save_row.add_widget(self.screenshot_btn) + self.save_config_btn = PushButton(text=gt('保存配置')) self.save_config_btn.clicked.connect(self._on_save_config_clicked) save_row.add_widget(self.save_config_btn) @@ -359,7 +385,7 @@ def _update_point_table_display(self): del_btn.clicked.connect(self._on_row_delete_clicked) self.point_table.setCellWidget(idx, 0, del_btn) - self.point_table.setItem(idx, 1, QTableWidgetItem('%d, %d' % (point_item.x, point_item.y))) + self.point_table.setItem(idx, 1, QTableWidgetItem(f'{point_item.x}, {point_item.y}')) # 根据行数调整表格高度 row_height = self.point_table.rowHeight(0) if self.point_table.rowCount() > 0 else 32 @@ -627,6 +653,24 @@ def _on_image_chosen(self, image_file_path: str) -> None: self.chosen_template.point_updated = True self._update_all_image_display() + def _on_screenshot_clicked(self) -> None: + """ + 截图按钮点击 + :return: + """ + _, screen = self.ctx.controller.screenshot() + if screen is None: + return + + if self.chosen_template is None: + # 没有选中模板时,自动创建一个新的 + self.chosen_template = TemplateInfo('', '') + self._update_whole_display() + + self.chosen_template.screen_image = screen + self.chosen_template.point_updated = True + self._update_all_image_display() + def _on_image_pasted(self, image_data) -> None: """ 通过拖放或粘贴加载图片后的回调 diff --git a/src/one_dragon_qt/view/installer_interface.py b/src/one_dragon_qt/view/installer_interface.py index 5873e472b3..9b77c4abe8 100644 --- a/src/one_dragon_qt/view/installer_interface.py +++ b/src/one_dragon_qt/view/installer_interface.py @@ -1,16 +1,30 @@ -import shutil import webbrowser -from pathlib import Path -from PySide6.QtCore import Qt, QThread, QTimer, QSize, Signal +from PySide6.QtCore import QSize, Qt, QTimer, Signal from PySide6.QtGui import QPixmap -from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QStackedWidget, QFrame -from qfluentwidgets import (FluentIcon, ProgressRing, ProgressBar, IndeterminateProgressBar, - PushButton, PrimaryPushButton, HyperlinkButton, - TitleLabel, SubtitleLabel, BodyLabel) +from PySide6.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QStackedWidget, + QVBoxLayout, + QWidget, +) +from qfluentwidgets import ( + BodyLabel, + FluentIcon, + HyperlinkButton, + IndeterminateProgressBar, + PrimaryPushButton, + ProgressBar, + ProgressRing, + PushButton, + SubtitleLabel, + TitleLabel, +) from one_dragon.base.operation.one_dragon_env_context import OneDragonEnvContext -from one_dragon.utils import app_utils, os_utils +from one_dragon.utils import app_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log from one_dragon_qt.utils.image_utils import scale_pixmap_for_high_dpi @@ -24,33 +38,9 @@ from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface -class UnpackResourceRunner(QThread): - """资源解包线程""" - finished = Signal(bool) - def __init__(self, installer_dir: str | None, work_dir: str, parent=None): - super().__init__(parent) - self.installer_dir = installer_dir - self.work_dir = work_dir - - def run(self): - if self.installer_dir is None: - self.finished.emit(False) - return - - uv_zip_dir = Path(self.installer_dir) / '.install' / 'uv-x86_64-pc-windows-msvc.zip' - if Path(self.installer_dir) != Path(self.work_dir) and uv_zip_dir.exists(): - try: - shutil.copytree(self.installer_dir, self.work_dir, dirs_exist_ok=True) - self.finished.emit(True) - except Exception as e: - log.error(f"解包资源失败: {e}") - self.finished.emit(False) - else: - self.finished.emit(True) - - class ClickableStepCircle(QLabel): """可点击的步骤圆圈""" + clicked = Signal(int) def __init__(self, step_index: int, parent=None): @@ -366,9 +356,6 @@ def __init__(self, ctx: OneDragonEnvContext, extra_install_cards: list | None = self.is_all_completed = False self.is_advanced_mode = False - self.unpack_resource_runner = UnpackResourceRunner(self.ctx.installer_dir, os_utils.get_work_dir()) - self.unpack_resource_runner.finished.connect(self.on_unpack_finished) - def get_content_widget(self) -> QWidget: content_widget = QWidget() @@ -712,16 +699,7 @@ def update_step_display(self): self.is_all_completed = False # 根据当前步骤状态更新按钮 - if current_step_widget.is_completed: - self.install_step_btn.setVisible(False) - self.skip_current_btn.setVisible(False) - self.next_btn.setVisible(True) - self.next_btn.setEnabled(True) - if self.current_step == len(self.install_steps) - 1: - self.next_btn.setText(gt('完成')) - else: - self.next_btn.setText(gt('下一步')) - elif current_step_widget.is_skipped: + if current_step_widget.is_completed or current_step_widget.is_skipped: self.install_step_btn.setVisible(False) self.skip_current_btn.setVisible(False) self.next_btn.setVisible(True) @@ -914,59 +892,12 @@ def format_log_with_line_breaks(log_text): formatted_latest_log = format_log_with_line_breaks(latest_log) self.log_display_label.setText(formatted_latest_log) - def on_unpack_finished(self, success: bool): - """资源解压完成回调""" - self.stop_placebo_progress() - self.show_install_options(success) - - def start_placebo_progress(self): - """启动占位进度动画""" - self.progress_ring.setVisible(True) - self.progress_label.setVisible(True) - self.progress_label.setText(gt('正在解压资源...')) - - self.placebo_timer = QTimer(self) - self.placebo_progress = 0 - - def update_placebo_progress(): - # 使用非线性增长,让进度看起来更自然 - if self.placebo_progress < 80: - increment = 1 - elif self.placebo_progress < 95: - increment = 0.5 - else: - increment = 0.1 - - # 最大99%,避免在真正完成前到达100% - self.placebo_progress = min(99, self.placebo_progress + increment) - self.progress_ring.setValue(int(self.placebo_progress)) - - self.placebo_timer.timeout.connect(update_placebo_progress) - self.placebo_timer.start(100) - - def stop_placebo_progress(self): - """停止占位进度动画并清理资源""" - if hasattr(self, 'placebo_timer') and self.placebo_timer: - self.placebo_timer.stop() - self.placebo_timer.deleteLater() - self.placebo_timer = None - - # 设置完成状态 - self.placebo_progress = 100 - self.progress_ring.setValue(100) - - def show_install_options(self, success: bool): + def show_install_options(self): """显示安装选项""" - self.install_btn.setVisible(success) - self.advanced_btn.setVisible(success) + self.install_btn.setVisible(True) + self.advanced_btn.setVisible(True) self.progress_ring.setVisible(False) - self.progress_label.setVisible(not success) - if not success: - # 资源解压失败时自动打开帮助文档 - webbrowser.open(self.ctx.project_config.doc_link) - log.info("资源解压失败,已自动打开帮助文档") - self.progress_label.setText(gt('资源解压失败!已自动打开排障文档')) - self.progress_label.setStyleSheet("color: #d13438;") + self.progress_label.setVisible(False) def update_all_install_cards(self): """更新所有安装卡的状态""" @@ -977,9 +908,7 @@ def update_all_install_cards(self): def on_interface_shown(self) -> None: super().on_interface_shown() - # 启动资源解压和进度动画 - self.unpack_resource_runner.start() - self.start_placebo_progress() + self.show_install_options() # 更新所有安装卡的状态 self.update_all_install_cards() diff --git a/src/one_dragon_qt/view/one_dragon/one_dragon_run_interface.py b/src/one_dragon_qt/view/one_dragon/one_dragon_run_interface.py index 0707209505..80ed59be28 100644 --- a/src/one_dragon_qt/view/one_dragon/one_dragon_run_interface.py +++ b/src/one_dragon_qt/view/one_dragon/one_dragon_run_interface.py @@ -103,20 +103,14 @@ def _get_left_layout(self) -> QVBoxLayout: layout = QVBoxLayout() scroll_area = SingleDirectionScrollArea() - scroll_content = QWidget() - scroll_layout = QVBoxLayout(scroll_content) - scroll_layout.setContentsMargins(0, 0, 16, 0) - # 使用 AppRunList 管理应用列表 + # 使用 AppRunList 管理应用列表,直接作为滚动内容 self.app_run_list = AppRunList(self.ctx) self.app_run_list.app_list_changed.connect(self._on_app_list_changed) self.app_run_list.app_run_clicked.connect(self._on_app_card_run) self.app_run_list.app_switch_changed.connect(self.on_app_switch_run) self.app_run_list.app_setting_clicked.connect(self.on_app_setting_clicked) - scroll_layout.addWidget(self.app_run_list) - scroll_layout.addStretch(1) - - scroll_area.setWidget(scroll_content) + scroll_area.setWidget(self.app_run_list) scroll_area.setWidgetResizable(True) layout.addWidget(scroll_area) diff --git a/src/one_dragon_qt/view/setting/setting_custom_interface.py b/src/one_dragon_qt/view/setting/setting_custom_interface.py index a8c0b1d4a2..a128fb4d5e 100644 --- a/src/one_dragon_qt/view/setting/setting_custom_interface.py +++ b/src/one_dragon_qt/view/setting/setting_custom_interface.py @@ -1,45 +1,41 @@ +import ctypes import os import shutil -import ctypes -import hashlib from ctypes import wintypes -import base64 -import uuid -from PySide6.QtWidgets import QWidget, QFileDialog, QVBoxLayout, QInputDialog, QLineEdit, QHBoxLayout from PySide6.QtGui import QColor -from qfluentwidgets import Dialog, FluentIcon, PrimaryPushButton, SettingCardGroup, setTheme, Theme, ColorDialog, LineEdit, ToolButton - -from one_dragon.base.config.custom_config import ThemeEnum, UILanguageEnum, ThemeColorModeEnum, BackgroundTypeEnum +from PySide6.QtWidgets import QFileDialog, QWidget +from qfluentwidgets import ( + ColorDialog, + Dialog, + FluentIcon, + PrimaryPushButton, + SettingCardGroup, + Theme, + setTheme, +) + +from one_dragon.base.config.custom_config import ( + BackgroundTypeEnum, + ThemeEnum, + UILanguageEnum, +) from one_dragon.base.operation.one_dragon_context import OneDragonContext +from one_dragon.utils import app_utils, os_utils +from one_dragon.utils.i18_utils import gt from one_dragon_qt.services.theme_manager import ThemeManager from one_dragon_qt.widgets.column import Column +from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ( + ComboBoxSettingCard, +) +from one_dragon_qt.widgets.setting_card.password_switch_setting_card import ( + PasswordSwitchSettingCard, +) from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface -from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ComboBoxSettingCard -from one_dragon_qt.widgets.setting_card.password_switch_setting_card import PasswordSwitchSettingCard -from one_dragon_qt.widgets.setting_card.switch_setting_card import SwitchSettingCard -from one_dragon.utils import app_utils, os_utils -from one_dragon.utils.i18_utils import gt class SettingCustomInterface(VerticalScrollInterface): - @property - def theme_color_password_salt(self) -> str: - _e = os.environ.get('THEME_COLOR_SALT') - if _e: - return _e - try: - import platform - _m = f"{platform.node()}-{platform.machine()}" - return str(uuid.uuid5(uuid.NAMESPACE_DNS, _m)) - except Exception: - return str(uuid.uuid4()) - - def _get_pwd(self): - _x = [103, 114, 101, 101, 100, 105, 115, 103, 111, 111, 100] - return ''.join(chr(i) for i in _x) - def __init__(self, ctx: OneDragonContext, parent=None): self.ctx: OneDragonContext = ctx @@ -74,41 +70,23 @@ def _init_basic_group(self) -> SettingCardGroup: self.theme_opt.value_changed.connect(self._on_theme_changed) basic_group.addSettingCard(self.theme_opt) - # 主题色模式选择 - self.theme_color_mode_opt = ComboBoxSettingCard( - icon=FluentIcon.PALETTE, - title='主题色模式', - content='选择主题色的获取方式', - options_enum=ThemeColorModeEnum - ) - self.theme_color_mode_opt.value_changed.connect(self._on_theme_color_mode_changed) - # 自定义主题色按钮 self.custom_theme_color_btn = PrimaryPushButton(icon=FluentIcon.PALETTE, text=gt('自定义主题色')) self.custom_theme_color_btn.clicked.connect(self._on_custom_theme_color_clicked) - self.theme_color_mode_opt.hBoxLayout.addWidget(self.custom_theme_color_btn, 0) - self.theme_color_mode_opt.hBoxLayout.addSpacing(16) - self.custom_theme_color_btn.setEnabled(self.ctx.custom_config.is_custom_theme_color) - - # 主题色密码输入框 - self.theme_color_password = LineEdit() - self.theme_color_password.setPlaceholderText(gt('请输入密码')) - self.theme_color_password.setEchoMode(LineEdit.EchoMode.Password) - self.theme_color_password.setMinimumWidth(150) - self.theme_color_password.setMaximumWidth(self.theme_color_password.maximumWidth() - 45) - - # 切换显示/隐藏密码按钮 - self.theme_color_password_toggle = ToolButton(FluentIcon.HIDE) - self.theme_color_password_toggle.setCheckable(True) - self.theme_color_password_toggle.clicked.connect(self._toggle_theme_color_password_visibility) - - # 创建密码布局 - self.theme_color_password_layout = QHBoxLayout() - self.theme_color_password_layout.setContentsMargins(0, 0, 0, 0) - self.theme_color_password_layout.addWidget(self.theme_color_password) - self.theme_color_password_layout.addSpacing(5) - self.theme_color_password_layout.addWidget(self.theme_color_password_toggle) - self.theme_color_mode_opt.hBoxLayout.insertLayout(4, self.theme_color_password_layout, 0) + + # 主题色模式(密码保护) + self.theme_color_mode_opt = PasswordSwitchSettingCard( + icon=FluentIcon.PALETTE, + title='自定义主题色', + content='开启后可自定义主题色', + extra_btn=self.custom_theme_color_btn, + password_hint='使用此功能需要密码哦~', + password_hash='b0cd76b7d7829362d581b739c0b295abf53182792609078bb17a9dd917ffba7c', + dialog_title='嘻嘻~', + dialog_content='密码不对哦~', + dialog_button_text='再试试吧', + ) + self.theme_color_mode_opt.value_changed.connect(self._on_theme_color_mode_changed) basic_group.addSettingCard(self.theme_color_mode_opt) @@ -142,7 +120,7 @@ def on_interface_shown(self) -> None: VerticalScrollInterface.on_interface_shown(self) self.ui_language_opt.init_with_adapter(self.ctx.custom_config.get_prop_adapter('ui_language')) self.theme_opt.init_with_adapter(self.ctx.custom_config.get_prop_adapter('theme')) - self.theme_color_mode_opt.init_with_adapter(self.ctx.custom_config.get_prop_adapter('theme_color_mode')) + self.theme_color_mode_opt.init_with_adapter(self.ctx.custom_config.get_prop_adapter('custom_theme_color')) self.custom_banner_opt.init_with_adapter(self.ctx.custom_config.get_prop_adapter('custom_banner')) self.background_type_opt.init_with_adapter(self.ctx.custom_config.get_prop_adapter('background_type')) @@ -159,39 +137,11 @@ def _on_ui_language_changed(self, index: int, value: str) -> None: def _on_theme_changed(self, index: int, value: str) -> None: setTheme(Theme[self.ctx.custom_config.theme.upper()],lazy=True) - def _toggle_theme_color_password_visibility(self): - if self.theme_color_password_toggle.isChecked(): - self.theme_color_password.setEchoMode(LineEdit.EchoMode.Normal) - self.theme_color_password_toggle.setIcon(FluentIcon.VIEW) - else: - self.theme_color_password.setEchoMode(LineEdit.EchoMode.Password) - self.theme_color_password_toggle.setIcon(FluentIcon.HIDE) - - def _verify_theme_color_password(self) -> bool: - _p = self.theme_color_password.text() - _h = hashlib.sha256((_p + self.theme_color_password_salt).encode()).hexdigest() - _expected = hashlib.sha256((self._get_pwd() + self.theme_color_password_salt).encode()).hexdigest() - if _h == _expected: - return True - else: - _d = Dialog(gt('密码错误'), gt('密码不对哦~'), self) - _d.yesButton.setText(gt('再试试吧')) - _d.cancelButton.hide() - _d.exec() - return False - - def _on_theme_color_mode_changed(self, index: int, value: str) -> None: - if value == ThemeColorModeEnum.CUSTOM.value.value: - if not self._verify_theme_color_password(): - self.theme_color_mode_opt.setValue(ThemeColorModeEnum.AUTO.value.value) - return - if value == ThemeColorModeEnum.AUTO.value.value: + def _on_theme_color_mode_changed(self, value: bool) -> None: + if not value: self.ctx.signal.reload_banner = True - self.custom_theme_color_btn.setEnabled(value == ThemeColorModeEnum.CUSTOM.value.value) def _on_custom_theme_color_clicked(self) -> None: - if not self._verify_theme_color_password(): - return _c = self.ctx.custom_config.theme_color _d = ColorDialog(QColor(_c[0], _c[1], _c[2]), gt('请选择主题色'), self) _d.colorChanged.connect(self._update_custom_theme_color) diff --git a/src/one_dragon_qt/view/setting/setting_push_interface.py b/src/one_dragon_qt/view/setting/setting_push_interface.py index dc7e3f6d6f..0a7d8ed756 100644 --- a/src/one_dragon_qt/view/setting/setting_push_interface.py +++ b/src/one_dragon_qt/view/setting/setting_push_interface.py @@ -1,23 +1,39 @@ import json from PySide6.QtWidgets import QWidget -from qfluentwidgets import FluentIcon, PushButton, InfoBar, InfoBarPosition, SettingCard +from qfluentwidgets import FluentIcon, InfoBar, InfoBarPosition, PushButton, SettingCard from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.controller.pc_clipboard import PcClipboard +from one_dragon.base.operation.one_dragon_context import OneDragonContext from one_dragon.base.push.curl_generator import CurlGenerator +from one_dragon.base.push.push_channel_config import ( + FieldTypeEnum, + PushChannelConfigField, +) from one_dragon.base.push.push_config import PushProxy from one_dragon.base.push.push_email_services import PushEmailServices -from one_dragon.base.operation.one_dragon_context import OneDragonContext -from one_dragon.base.push.push_channel_config import PushChannelConfigField, FieldTypeEnum from one_dragon.utils.i18_utils import gt from one_dragon_qt.utils.config_utils import get_prop_adapter from one_dragon_qt.widgets.column import Column -from one_dragon_qt.widgets.setting_card.code_editor_setting_card import CodeEditorSettingCard -from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ComboBoxSettingCard -from one_dragon_qt.widgets.setting_card.editable_combo_box_setting_card import EditableComboBoxSettingCard -from one_dragon_qt.widgets.setting_card.key_value_setting_card import KeyValueSettingCard -from one_dragon_qt.widgets.setting_card.multi_push_setting_card import MultiPushSettingCard +from one_dragon_qt.widgets.setting_card.code_editor_setting_card import ( + CodeEditorSettingCard, +) +from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ( + ComboBoxSettingCard, +) +from one_dragon_qt.widgets.setting_card.editable_combo_box_setting_card import ( + EditableComboBoxSettingCard, +) +from one_dragon_qt.widgets.setting_card.expand_setting_card_group import ( + ExpandSettingCardGroup, +) +from one_dragon_qt.widgets.setting_card.key_value_setting_card import ( + KeyValueSettingCard, +) +from one_dragon_qt.widgets.setting_card.multi_push_setting_card import ( + MultiPushSettingCard, +) from one_dragon_qt.widgets.setting_card.switch_setting_card import SwitchSettingCard from one_dragon_qt.widgets.setting_card.text_setting_card import TextSettingCard from one_dragon_qt.widgets.setting_card.yaml_config_adapter import YamlConfigAdapter @@ -77,7 +93,7 @@ def get_content_widget(self) -> QWidget: ) content_widget.add_widget(self.test_notification_card) - # 通知方式选择 + # 通知方式 — 手风琴组:下拉框作为头部,渠道配置项作为子卡片 self.notification_method_opt = ComboBoxSettingCard( icon=FluentIcon.MESSAGE, title='通知方式', @@ -87,17 +103,18 @@ def get_content_widget(self) -> QWidget: ] ) self.notification_method_opt.value_changed.connect(self._update_notification_ui) - content_widget.add_widget(self.notification_method_opt) + channel_group = ExpandSettingCardGroup(icon=FluentIcon.MESSAGE, title='通知方式') + channel_group.addHeaderWidget(self.notification_method_opt.combo_box) + content_widget.add_widget(channel_group) + + # 预创建特殊卡片(稍后按渠道分配) self.pwsh_curl_btn = PushButton(text='PowerShell 风格') self.pwsh_curl_btn.clicked.connect(lambda: self._generate_curl('pwsh')) - self.unix_curl_btn = PushButton(text='Unix 风格') self.unix_curl_btn.clicked.connect(lambda: self._generate_curl('unix')) - self.curl_btn = MultiPushSettingCard(icon=FluentIcon.CODE, title='生成 cURL 命令', btn_list=[self.pwsh_curl_btn, self.unix_curl_btn]) self.curl_btn.setVisible(False) - content_widget.add_widget(self.curl_btn) email_services = PushEmailServices.load_services() service_options = [ConfigItem(label=name, value=name, desc="") for name in email_services] @@ -109,23 +126,28 @@ def get_content_widget(self) -> QWidget: ) self.email_service_opt.value_changed.connect(lambda idx, val: self._on_email_service_selected(val)) self.email_service_opt.combo_box.setFixedWidth(320) - self.email_service_opt.combo_box.setCurrentIndex(-1) # 设置为无选中状态 - self.email_service_opt.setVisible(False) # 默认隐藏,SMTP方式时显示 - content_widget.add_widget(self.email_service_opt) + self.email_service_opt.combo_box.setCurrentIndex(-1) + self.email_service_opt.setVisible(False) + # 按渠道组织卡片 self.push_channel_cards: dict[str, list] = {} - all_cards_widget = Column() for channel in self.ctx.push_service.channels: - channel_cards = [] + channel_cards: list[QWidget] = [] + + if channel.channel_id == 'SMTP': + channel_cards.append(self.email_service_opt) + channel_group.addSettingCard(self.email_service_opt) + elif channel.channel_id == 'WEBHOOK': + channel_cards.append(self.curl_btn) + channel_group.addSettingCard(self.curl_btn) for field in channel.config_schema: card = self._create_card(channel.channel_id, field) channel_cards.append(card) - all_cards_widget.add_widget(card) + channel_group.addSettingCard(card) self.push_channel_cards[channel.channel_id] = channel_cards - content_widget.add_widget(all_cards_widget) content_widget.add_stretch(1) return content_widget @@ -269,16 +291,11 @@ def _update_notification_ui(self): """根据选择的通知方式更新界面""" selected_method = self.notification_method_opt.getValue() - # 隐藏所有卡片 for method_name, method_cards in self.push_channel_cards.items(): is_selected = (method_name == selected_method) for card in method_cards: card.setVisible(is_selected) - # 特殊按钮 - self.email_service_opt.setVisible(selected_method == "SMTP") - self.curl_btn.setVisible(selected_method == "WEBHOOK") - def _set_proxy_input_visibility(self): """设置代理输入框的可见性""" self.proxy_input_opt.setVisible(self.proxy_opt.getValue() == PushProxy.PERSONAL.value.value) diff --git a/src/one_dragon_qt/widgets/app_run_list.py b/src/one_dragon_qt/widgets/app_run_list.py index 7d7b4a8e36..a12221285b 100644 --- a/src/one_dragon_qt/widgets/app_run_list.py +++ b/src/one_dragon_qt/widgets/app_run_list.py @@ -98,7 +98,7 @@ def _update_existing_cards( for idx, app in enumerate(app_list): if idx < len(self._app_cards): card = self._app_cards[idx] - card.index = idx + card.update_item(app, idx) run_record = self.ctx.run_context.get_run_record( app_id=app.app_id, instance_idx=instance_idx @@ -175,7 +175,7 @@ def _on_app_move_top(self, app_id: str) -> None: # 更新索引 for i, c in enumerate(self._app_cards): - c.index = i + c.update_item(c.data, i) # 重新构建布局 self._rebuild_layout() @@ -203,7 +203,7 @@ def _handle_order_changed(self, new_data_list: list) -> None: # 更新所有卡片的索引 for idx, card in enumerate(self._app_cards): - card.index = idx + card.update_item(card.data, idx) # 触发应用列表改变信号 new_app_list = [card.app for card in self._app_cards] diff --git a/src/one_dragon_qt/widgets/base_interface.py b/src/one_dragon_qt/widgets/base_interface.py index 7b04fc9087..6b783f6b20 100644 --- a/src/one_dragon_qt/widgets/base_interface.py +++ b/src/one_dragon_qt/widgets/base_interface.py @@ -1,28 +1,16 @@ from PySide6.QtGui import QIcon, Qt from PySide6.QtWidgets import QWidget -from qfluentwidgets import FluentIconBase, InfoBarIcon, InfoBarPosition, InfoBar -from typing import Union +from qfluentwidgets import FluentIconBase, InfoBar, InfoBarIcon, InfoBarPosition from one_dragon.utils.i18_utils import gt -try: - from zzz_od.telemetry.auto_telemetry import TelemetryInterfaceMixin, auto_telemetry_method -except ImportError: - class TelemetryInterfaceMixin: - def track_interface_shown(self): pass - def track_interface_hidden(self): pass - def auto_telemetry_method(*args, **kwargs): - def decorator(func): - return func - return decorator - -class BaseInterface(QWidget, TelemetryInterfaceMixin): +class BaseInterface(QWidget): def __init__(self, object_name: str, nav_text_cn: str, - nav_icon: Union[FluentIconBase, QIcon, str] = None, + nav_icon: FluentIconBase | QIcon | str = None, parent=None): """ 包装一个子页面需要有的内容 @@ -32,24 +20,22 @@ def __init__(self, """ QWidget.__init__(self, parent=parent) self.nav_text: str = gt(nav_text_cn) - self.nav_icon: Union[FluentIconBase, QIcon, str] = nav_icon + self.nav_icon: FluentIconBase | QIcon | str = nav_icon self.setObjectName(object_name) - @auto_telemetry_method("interface_shown") def on_interface_shown(self) -> None: """ 子界面显示时 进行初始化 :return: """ - self.track_interface_shown() + pass - @auto_telemetry_method("interface_hidden") def on_interface_hidden(self) -> None: """ 子界面隐藏时的回调 :return: """ - self.track_interface_hidden() + pass def show_info_bar( self, diff --git a/src/one_dragon_qt/widgets/column.py b/src/one_dragon_qt/widgets/column.py index cdcefd92f8..ae8601d122 100644 --- a/src/one_dragon_qt/widgets/column.py +++ b/src/one_dragon_qt/widgets/column.py @@ -1,28 +1,19 @@ from PySide6.QtCore import Qt -from PySide6.QtWidgets import QWidget, QVBoxLayout, QSizePolicy +from PySide6.QtWidgets import QVBoxLayout, QWidget + +from one_dragon_qt.utils.layout_utils import Margins class Column(QWidget): """ 垂直布局容器组件,用于将多个组件在垂直方向上排列。 - 支持在创建时自定义间距和边距。 Usage: - # 基本用法 - column = Column() + column = Column(spacing=8, margins=Margins(10, 5, 10, 5)) column.add_widget(button1) - column.add_widget(button2) - - # 自定义间距和边距 - column = Column(spacing=8, margins=(10, 5, 10, 5)) - - Args: - parent: 父组件,默认为None - spacing: 组件之间的间距,单位为像素 - margins: 容器的边距,支持4个值(left,top,right,bottom)、2个值(horizontal,vertical)或1个值 """ - def __init__(self, parent=None, spacing: int | None = None, margins: tuple | int | None = None): + def __init__(self, parent=None, spacing: int | None = None, margins: Margins | None = None): QWidget.__init__(self, parent=parent) self.v_layout = QVBoxLayout(self) @@ -31,23 +22,7 @@ def __init__(self, parent=None, spacing: int | None = None, margins: tuple | int self.v_layout.setSpacing(spacing) if margins is not None: - # 支持 int、tuple、list,避免对 int 执行 len() 触发 TypeError - if isinstance(margins, int): - l = t = r = b = int(margins) - elif isinstance(margins, (tuple, list)): - if len(margins) == 4: - l, t, r, b = map(int, margins) - elif len(margins) == 2: - h, v = map(int, margins) - l = r = h - t = b = v - elif len(margins) == 1: - l = t = r = b = int(margins[0]) - else: - raise ValueError("margins 只能为 1/2/4 个整数") - else: - raise TypeError("margins 类型应为 int 或 tuple/list[int]") - self.v_layout.setContentsMargins(l, t, r, b) + self.v_layout.setContentsMargins(margins.left, margins.top, margins.right, margins.bottom) def add_widget(self, widget: QWidget, stretch: int = 0, alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignTop): self.v_layout.addWidget(widget, stretch=stretch, alignment=alignment) diff --git a/src/one_dragon_qt/widgets/download_card/launcher_download_card.py b/src/one_dragon_qt/widgets/download_card/launcher_download_card.py index 565e208b3d..35e7e44715 100644 --- a/src/one_dragon_qt/widgets/download_card/launcher_download_card.py +++ b/src/one_dragon_qt/widgets/download_card/launcher_download_card.py @@ -1,11 +1,11 @@ -import os +import shutil from contextlib import suppress from pathlib import Path from packaging import version -from PySide6.QtCore import QThread, Signal +from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QIcon -from qfluentwidgets import FluentIcon, FluentThemeColor +from qfluentwidgets import ComboBox, FluentIcon, FluentThemeColor from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.operation.one_dragon_env_context import OneDragonEnvContext @@ -18,9 +18,17 @@ ZipDownloaderSettingCard, ) -LAUNCHER_NAME = 'OneDragon-Launcher' -LAUNCHER_EXE_NAME = LAUNCHER_NAME + '.exe' -LAUNCHER_BACKUP_NAME = LAUNCHER_NAME + '.bak' + '.exe' +# 原始启动器 +LAUNCHER_EXE = 'OneDragon-Launcher.exe' +LAUNCHER_BACKUP = 'OneDragon-Launcher.bak.exe' +LAUNCHER_ZIP_SUFFIX = 'Launcher.zip' + +# 集成启动器 +RUNTIME_LAUNCHER_EXE = 'OneDragon-RuntimeLauncher.exe' +RUNTIME_LAUNCHER_BACKUP = 'OneDragon-RuntimeLauncher.bak.exe' +RUNTIME_LAUNCHER_ZIP_SUFFIX = 'RuntimeLauncher.zip' +RUNTIME_DIR = '.runtime' +RUNTIME_DIR_BACKUP = '.runtime.bak' class LauncherVersionChecker(QThread): @@ -35,33 +43,24 @@ class LauncherVersionChecker(QThread): """ check_finished = Signal(str, str, str) - def __init__(self, ctx: OneDragonEnvContext): + def __init__(self, ctx: OneDragonEnvContext, exe_name: str): super().__init__() self.ctx = ctx + self.exe_name = exe_name - def run(self): - # 检查当前版本 - launcher_path = Path(os_utils.get_work_dir()) / LAUNCHER_EXE_NAME - if launcher_path.exists(): - current_version = app_utils.get_launcher_version() - else: - current_version = "" - - # 检查最新版本 + def run(self) -> None: + exe_path = Path(os_utils.get_work_dir()) / self.exe_name + current_version = app_utils.get_exe_version(str(exe_path)) if exe_path.exists() else "" latest_stable, latest_beta = self.ctx.git_service.get_latest_tag() - - self.check_finished.emit( - current_version, - latest_stable, - latest_beta, - ) + self.check_finished.emit(current_version, latest_stable, latest_beta) class LauncherDownloadCard(ZipDownloaderSettingCard): def __init__(self, ctx: OneDragonEnvContext): self.ctx: OneDragonEnvContext = ctx - self.version_checker = LauncherVersionChecker(ctx) + self._launcher_type: str = 'launcher' # 'launcher' | 'runtime' + self.version_checker = LauncherVersionChecker(ctx, LAUNCHER_EXE) self.version_checker.check_finished.connect(self._on_version_check_finished) self.target_version = "latest" self.current_version: str | None = None @@ -76,19 +75,54 @@ def __init__(self, ctx: OneDragonEnvContext): content=gt('检查中...'), ) - # 设置下拉框选项:稳定版和测试版 + # 启动器类型下拉框 + self.type_combo = ComboBox() + self.type_combo.addItem(gt('原始启动器'), userData='launcher') + self.type_combo.addItem(gt('集成启动器'), userData='runtime') + self.type_combo.currentIndexChanged.connect(self._on_type_changed) + self.btn_layout.insertWidget(1, self.type_combo, alignment=Qt.AlignmentFlag.AlignRight) + + # 通道选项:稳定版 / 测试版 self.set_options_by_list([ ConfigItem('稳定版', 'stable'), ConfigItem('测试版', 'beta') ]) + # ---- 启动器类型 ---- + + @property + def _exe_name(self) -> str: + return RUNTIME_LAUNCHER_EXE if self._launcher_type == 'runtime' else LAUNCHER_EXE + + @property + def _backup_name(self) -> str: + return RUNTIME_LAUNCHER_BACKUP if self._launcher_type == 'runtime' else LAUNCHER_BACKUP + + @property + def _zip_suffix(self) -> str: + return RUNTIME_LAUNCHER_ZIP_SUFFIX if self._launcher_type == 'runtime' else LAUNCHER_ZIP_SUFFIX + + @property + def _is_runtime(self) -> bool: + return self._launcher_type == 'runtime' + + def _on_type_changed(self, _index: int) -> None: + self._launcher_type = self.type_combo.currentData() + # 断开旧检查器信号,避免竞态覆盖 + old_checker = self.version_checker + old_checker.check_finished.disconnect(self._on_version_check_finished) + # 重建版本检查器(指向不同 exe) + self.version_checker = LauncherVersionChecker(self.ctx, self._exe_name) + self.version_checker.check_finished.connect(self._on_version_check_finished) + # 重置版本状态,触发重新检查 + self.current_version = None + self.latest_stable = None + self.latest_beta = None + self.check_and_update_display() + def _get_downloader_param(self, _idx = None) -> CommonDownloaderParam: - """ - 动态生成下载器参数 - :return: CommonDownloaderParam - """ - zip_file_name = f'{self.ctx.project_config.project_name}-Launcher.zip' - launcher_path = os.path.join(os_utils.get_work_dir(), LAUNCHER_EXE_NAME) + zip_file_name = f'{self.ctx.project_config.project_name}-{self._zip_suffix}' + exe_path = str(Path(os_utils.get_work_dir()) / self._exe_name) base = ( 'latest/download' @@ -101,7 +135,7 @@ def _get_downloader_param(self, _idx = None) -> CommonDownloaderParam: save_file_path=DEFAULT_ENV_PATH, save_file_name=zip_file_name, github_release_download_url=download_url, - check_existed_list=[launcher_path], + check_existed_list=[exe_path], unzip_dir_path=os_utils.get_work_dir() ) @@ -190,10 +224,12 @@ def check_and_update_display(self) -> None: self.download_btn.setText(gt('下载中')) self.download_btn.setDisabled(True) self.combo_box.setDisabled(True) + self.type_combo.setDisabled(True) return # 启用下拉框 self.combo_box.setEnabled(True) + self.type_combo.setEnabled(True) # 检查版本检查线程状态 is_checking = self.version_checker.isRunning() @@ -261,44 +297,41 @@ def _select_channel_by_version(self) -> None: self.combo_box.setCurrentIndex(0) def _on_download_click(self) -> None: - """ - 下载前的准备工作:备份旧启动器文件并删除遗留文件 - :return: - """ - # 备份需要更新的启动器文件 - self._swap_launcher_and_backup(backup=True) - - # 删除旧版本遗留的文件 + self._swap_backup(backup=True) self._delete_legacy_files() - - # 调用父类方法执行下载 ZipDownloaderSettingCard._on_download_click(self) - def _swap_launcher_and_backup(self, backup: bool) -> None: - """ - 在启动器文件和备份文件之间进行交换 - :param backup: True=备份,False=回滚 - """ + def _swap_backup(self, backup: bool) -> None: + """备份或回滚启动器文件。""" work_dir = Path(os_utils.get_work_dir()) - launcher_path = work_dir / LAUNCHER_EXE_NAME - backup_path = work_dir / LAUNCHER_BACKUP_NAME - src, dst, action = ( - (launcher_path, backup_path, '备份') - if backup else - (backup_path, launcher_path, '回滚') - ) - - if not src.exists(): - return # 没有可操作文件,直接返回 + action = '备份' if backup else '回滚' - try: - # 仅在备份时需要清理旧备份 - if backup and dst.exists(): - dst.unlink() - os.replace(str(src), str(dst)) - log.info(f'{action}文件: {src.name} -> {dst.name}') - except Exception as e: - log.error(f'{action}文件失败 {src.name}: {e}') + # exe + exe_path = work_dir / self._exe_name + bak_path = work_dir / self._backup_name + src, dst = (exe_path, bak_path) if backup else (bak_path, exe_path) + if src.exists(): + try: + if backup and dst.exists(): + dst.unlink() + src.replace(dst) + log.info(f'{action}文件: {src.name} -> {dst.name}') + except Exception as e: + log.error(f'{action}文件失败 {src.name}: {e}') + + # 集成启动器额外处理 .runtime 目录 + if self._is_runtime: + rt_path = work_dir / RUNTIME_DIR + rt_bak = work_dir / RUNTIME_DIR_BACKUP + src_d, dst_d = (rt_path, rt_bak) if backup else (rt_bak, rt_path) + if src_d.exists(): + try: + if dst_d.exists(): + shutil.rmtree(dst_d) + src_d.rename(dst_d) + log.info(f'{action}目录: {src_d.name} -> {dst_d.name}') + except Exception as e: + log.error(f'{action}目录失败 {src_d.name}: {e}') def _delete_legacy_files(self) -> None: """ @@ -321,37 +354,35 @@ def _delete_legacy_files(self) -> None: except Exception as e: log.error(f'删除旧版本遗留文件失败 {legacy_file}: {e}') - def _cleanup_backup_launcher(self) -> None: - """ - 删除备份的启动器文件 - :return: - """ - work_dir = os_utils.get_work_dir() - backup_path = Path(work_dir) / LAUNCHER_BACKUP_NAME + def _cleanup_backup(self) -> None: + """删除备份的启动器文件(以及集成启动器的 .runtime.bak 目录)。""" + work_dir = Path(os_utils.get_work_dir()) - if backup_path.exists(): + bak_path = work_dir / self._backup_name + if bak_path.exists(): try: - backup_path.unlink() - log.info(f'删除备份文件: {LAUNCHER_BACKUP_NAME}') + bak_path.unlink() + log.info(f'删除备份文件: {self._backup_name}') except Exception as e: - log.error(f'删除备份文件失败 {LAUNCHER_BACKUP_NAME}: {e}') + log.error(f'删除备份文件失败 {self._backup_name}: {e}') + + if self._is_runtime: + rt_bak = work_dir / RUNTIME_DIR_BACKUP + if rt_bak.exists(): + try: + shutil.rmtree(rt_bak) + log.info(f'删除备份目录: {RUNTIME_DIR_BACKUP}') + except Exception as e: + log.error(f'删除备份目录失败 {RUNTIME_DIR_BACKUP}: {e}') def _on_download_finish(self, success: bool, message: str) -> None: - """ - 下载完成后处理备份:成功则删除备份,失败则回滚 - :param success: 是否成功 - :param message: 消息 - :return: - """ + """下载完成后处理备份:成功则删除备份,失败则回滚。""" if success: - # 下载成功,删除备份文件 - self._cleanup_backup_launcher() + self._cleanup_backup() - # 使用后台线程更新版本号 if not self.version_checker.isRunning(): self.version_checker.start() else: - # 下载失败,回滚到备份文件 - self._swap_launcher_and_backup(backup=False) + self._swap_backup(backup=False) ZipDownloaderSettingCard._on_download_finish(self, success, message) diff --git a/src/one_dragon_qt/widgets/draggable_list.py b/src/one_dragon_qt/widgets/draggable_list.py index 2ee1a1e80c..1e470239df 100644 --- a/src/one_dragon_qt/widgets/draggable_list.py +++ b/src/one_dragon_qt/widgets/draggable_list.py @@ -186,6 +186,22 @@ def __init__(self, data: Any, index: int, content_widget: QWidget, parent=None, ) layout.addWidget(self.content_widget) + def update_item(self, data: Any, index: int) -> None: + """ + 更新列表项关联的数据和索引 + :param data: 关联的数据 + :param index: 列表项的索引 + """ + self.data = data + self.index = index + self.after_update_item() + + def after_update_item(self) -> None: + """ + 更新后的钩子函数,子类可以重写以实现特定逻辑 + """ + pass + def _ensure_opacity_effect(self) -> None: """ 确保 QGraphicsOpacityEffect 已创建(延迟创建) @@ -377,7 +393,7 @@ def __init__(self, parent=None, enable_opacity_effect: bool = True): # 创建主布局 self._layout = QVBoxLayout(self) self._layout.setSpacing(FluentDesignConst.LAYOUT_SPACING) - self._layout.setContentsMargins(0, 0, 0, 0) + self._layout.setContentsMargins(0, 0, 16, 16) # 右边距16适配滚动条,底部边距16留出空间 # 创建 Fluent Design 风格的位置指示器 self._drop_indicator = FluentDropIndicator(self) diff --git a/src/one_dragon_qt/widgets/row.py b/src/one_dragon_qt/widgets/row.py index 4ef8b9b5fd..c58b9ba784 100644 --- a/src/one_dragon_qt/widgets/row.py +++ b/src/one_dragon_qt/widgets/row.py @@ -1,28 +1,19 @@ from PySide6.QtCore import Qt -from PySide6.QtWidgets import QWidget, QHBoxLayout, QSizePolicy +from PySide6.QtWidgets import QHBoxLayout, QWidget + +from one_dragon_qt.utils.layout_utils import Margins class Row(QWidget): """ 水平布局容器组件,用于将多个组件在水平方向上排列。 - 支持在创建时自定义间距和边距。 Usage: - # 基本用法 - row = Row() + row = Row(spacing=8, margins=Margins(10, 5, 10, 5)) row.add_widget(button1) - row.add_widget(button2) - - # 自定义间距和边距 - row = Row(spacing=8, margins=(10, 5, 10, 5)) - - Args: - parent: 父组件,默认为None - spacing: 组件之间的间距,单位为像素 - margins: 容器的边距,支持4个值(left,top,right,bottom)、2个值(horizontal,vertical)或1个值 """ - def __init__(self, parent=None, spacing: int | None = None, margins: tuple | int | None = None): + def __init__(self, parent=None, spacing: int | None = None, margins: Margins | None = None): QWidget.__init__(self, parent=parent) self.h_layout = QHBoxLayout(self) @@ -31,23 +22,7 @@ def __init__(self, parent=None, spacing: int | None = None, margins: tuple | int self.h_layout.setSpacing(spacing) if margins is not None: - # 支持 int、tuple、list,避免对 int 执行 len() 触发 TypeError - if isinstance(margins, int): - l = t = r = b = int(margins) - elif isinstance(margins, (tuple, list)): - if len(margins) == 4: - l, t, r, b = map(int, margins) - elif len(margins) == 2: - h, v = map(int, margins) - l = r = h - t = b = v - elif len(margins) == 1: - l = t = r = b = int(margins[0]) - else: - raise ValueError("margins 只能为 1/2/4 个整数") - else: - raise TypeError("margins 类型应为 int 或 tuple/list[int]") - self.h_layout.setContentsMargins(l, t, r, b) + self.h_layout.setContentsMargins(margins.left, margins.top, margins.right, margins.bottom) def add_widget(self, widget: QWidget, stretch: int = 0, alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignLeft): self.h_layout.addWidget(widget, stretch=stretch, alignment=alignment) diff --git a/src/one_dragon_qt/widgets/scroll_credits.py b/src/one_dragon_qt/widgets/scroll_credits.py index e9797bcb80..232cbe290e 100644 --- a/src/one_dragon_qt/widgets/scroll_credits.py +++ b/src/one_dragon_qt/widgets/scroll_credits.py @@ -3,8 +3,7 @@ from qfluentwidgets import qconfig, Theme, isDarkTheme from one_dragon.base.operation.one_dragon_env_context import OneDragonEnvContext from one_dragon.utils.log_utils import log -from one_dragon.utils import os_utils -import yaml +from one_dragon.utils import os_utils, yaml_utils import os @@ -163,7 +162,7 @@ def _load_commit_data(self): # 读取并解析YAML文件 with open(contributors_file, 'r', encoding='utf-8') as f: - contributors_data = yaml.safe_load(f) + contributors_data = yaml_utils.safe_load(f) # 整合所有贡献者信息 - 按类别显示,支持自定义分组 all_contributors = [] diff --git a/src/one_dragon_qt/widgets/setting_card/expand_setting_card_group.py b/src/one_dragon_qt/widgets/setting_card/expand_setting_card_group.py new file mode 100644 index 0000000000..3a3f38451a --- /dev/null +++ b/src/one_dragon_qt/widgets/setting_card/expand_setting_card_group.py @@ -0,0 +1,66 @@ +from PySide6.QtCore import QEvent, QObject +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QWidget +from qfluentwidgets import ExpandSettingCard, FluentIconBase +from qfluentwidgets.components.settings.expand_setting_card import GroupSeparator + +from one_dragon.utils.i18_utils import gt + + +class ExpandSettingCardGroup(ExpandSettingCard): + """可展开设置卡片组(手风琴式) + + 与 SettingCardGroup 有一致的 addSettingCard API。 + """ + + def __init__( + self, + icon: FluentIconBase | QIcon | str, + title: str, + content: str | None = None, + parent: QWidget | None = None, + ): + super().__init__(icon, gt(title), parent=parent) + if content: + self.card.setContent(gt(content)) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self._card_sep_pairs: list[tuple[QWidget, GroupSeparator | None]] = [] + + def addHeaderWidget(self, widget: QWidget) -> None: + """在头部 expandButton 左侧添加操作组件""" + self.card.addWidget(widget) + + def addSettingCard(self, card: QWidget) -> None: + """添加设置卡片(自动插入分隔线,去除子卡自身边框)""" + sep: GroupSeparator | None = None + if self._card_sep_pairs: + sep = GroupSeparator(self.view) + self.viewLayout.addWidget(sep) + + card.paintEvent = lambda _e: None + card.setParent(self.view) + self.viewLayout.addWidget(card) + self._card_sep_pairs.append((card, sep)) + card.installEventFilter(self) + self._adjustViewSize() + + def addSettingCards(self, cards: list[QWidget]) -> None: + """批量添加设置卡片""" + for card in cards: + self.addSettingCard(card) + + def eventFilter(self, obj: QObject, event: QEvent) -> bool: + if event.type() in (QEvent.Type.Show, QEvent.Type.Hide): + self._update_separators() + return super().eventFilter(obj, event) + + def _update_separators(self) -> None: + """根据卡片可见性更新分隔线:仅当当前卡片可见且前面存在可见卡片时才显示分隔线""" + has_visible_before = False + for card, sep in self._card_sep_pairs: + if sep is not None: + sep.setVisible(card.isVisible() and has_visible_before) + if card.isVisible(): + has_visible_before = True + self._adjustViewSize() diff --git a/src/one_dragon_qt/widgets/setting_card/gamepad_action_key_card.py b/src/one_dragon_qt/widgets/setting_card/gamepad_action_key_card.py new file mode 100644 index 0000000000..f1a64276e7 --- /dev/null +++ b/src/one_dragon_qt/widgets/setting_card/gamepad_action_key_card.py @@ -0,0 +1,97 @@ +from enum import Enum + +from PySide6.QtCore import Signal +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import QWidget +from qfluentwidgets import FluentIconBase + +from one_dragon.base.config.config_item import ConfigItem +from one_dragon_qt.utils.layout_utils import IconSize, Margins +from one_dragon_qt.widgets.adapter_init_mixin import AdapterInitMixin +from one_dragon_qt.widgets.combo_box import ComboBox +from one_dragon_qt.widgets.setting_card.multi_push_setting_card import ( + MultiPushSettingCard, +) + + +class GamepadActionKeyCard(MultiPushSettingCard, AdapterInitMixin): + """手柄动作键卡片:修饰键 + 按钮。 + + 用于后台模式手柄动作键配置,一张卡片包含两个下拉框(修饰键、按钮), + 组合值以 list[str] 格式存储,如 ['xbox_lb', 'xbox_a'] 表示 LB+A。 + """ + + value_changed = Signal(list) + + def __init__( + self, + icon: str | QIcon | FluentIconBase, + title: str, + modifier_enum: type[Enum], + button_enum: type[Enum], + content: str | None = None, + icon_size: IconSize = IconSize(16, 16), + margins: Margins = Margins(16, 16, 0, 16), + parent: QWidget = None, + ) -> None: + self.modifier_combo = ComboBox() + self.modifier_combo.addItem('无', userData='') + for item in modifier_enum: + ci: ConfigItem = item.value + self.modifier_combo.addItem(ci.ui_text, userData=ci.value) + self.modifier_combo.setCurrentIndex(0) + + self.button_combo = ComboBox() + for item in button_enum: + ci: ConfigItem = item.value + self.button_combo.addItem(ci.ui_text, userData=ci.value) + self.button_combo.setCurrentIndex(0) + + MultiPushSettingCard.__init__( + self, icon=icon, title=title, content=content, + icon_size=icon_size, margins=margins, + btn_list=[self.modifier_combo, self.button_combo], + parent=parent, + ) + AdapterInitMixin.__init__(self) + + self._updating = False + self.modifier_combo.currentIndexChanged.connect(self._on_combo_changed) + self.button_combo.currentIndexChanged.connect(self._on_combo_changed) + + def _on_combo_changed(self, _index: int) -> None: + """任一下拉框变化时,组合值并同步 adapter。""" + if self._updating: + return + value = self.getValue() + + if self.adapter is not None: + self.adapter.set_value(value) + + self.value_changed.emit(value) + + def setValue(self, value: object, emit_signal: bool = True) -> None: + """从存储值设置两个下拉框。""" + self._updating = True + keys = value if isinstance(value, list) else [] + modifier_val = keys[0] if len(keys) >= 2 else '' + button_val = keys[-1] if keys else '' + + idx = self.modifier_combo.findData(modifier_val) + self.modifier_combo.setCurrentIndex(idx if idx >= 0 else 0) + + idx = self.button_combo.findData(button_val) + self.button_combo.setCurrentIndex(idx if idx >= 0 else 0) + + self._updating = False + + if emit_signal: + self.value_changed.emit(self.getValue()) + + def getValue(self) -> list[str]: + """获取当前组合值。""" + modifier = self.modifier_combo.currentData() or '' + button = self.button_combo.currentData() or '' + if modifier: + return [modifier, button] + return [button] diff --git a/src/one_dragon_qt/widgets/setting_card/setting_card_base.py b/src/one_dragon_qt/widgets/setting_card/setting_card_base.py index ff0545d248..b149c64995 100644 --- a/src/one_dragon_qt/widgets/setting_card/setting_card_base.py +++ b/src/one_dragon_qt/widgets/setting_card/setting_card_base.py @@ -1,20 +1,16 @@ from PySide6.QtCore import Qt from PySide6.QtGui import QIcon -from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QFrame -from qfluentwidgets import SettingCard, FluentIconBase -from qfluentwidgets.components.settings.setting_card import ( - SettingIconWidget, - FluentStyleSheet, -) -from typing import Union +from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout +from qfluentwidgets import FluentIconBase, FluentStyleSheet, SettingCard +from qfluentwidgets.components.settings.setting_card import SettingIconWidget from one_dragon.utils.i18_utils import gt -from one_dragon_qt.utils.layout_utils import Margins, IconSize +from one_dragon_qt.utils.layout_utils import IconSize, Margins class SettingCardBase(SettingCard): - def __init__(self, icon: Union[str, QIcon, FluentIconBase], title, content=None, + def __init__(self, icon: str | QIcon | FluentIconBase, title, content=None, icon_size: IconSize = IconSize(16, 16), margins: Margins = Margins(16, 16, 0, 16), parent=None): diff --git a/src/one_dragon_qt/windows/window.py b/src/one_dragon_qt/windows/window.py index d4f44ed35e..be1bb1da2b 100644 --- a/src/one_dragon_qt/windows/window.py +++ b/src/one_dragon_qt/windows/window.py @@ -1,36 +1,35 @@ -from PySide6.QtCore import Qt, Signal, QRect, QRectF, QUrl -from PySide6.QtGui import QIcon, QPainter, QColor, QFont, QDesktopServices +from PySide6.QtCore import QRect, QRectF, Qt, QUrl, Signal +from PySide6.QtGui import QColor, QDesktopServices, QIcon, QPainter from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QLabel, - QSpacerItem, - QSizePolicy, + QAbstractScrollArea, + QApplication, QHBoxLayout, + QLabel, QPushButton, - QApplication, - QAbstractScrollArea, + QSizePolicy, + QSpacerItem, + QVBoxLayout, + QWidget, ) from qfluentwidgets import ( + FluentIconBase, FluentStyleSheet, - isDarkTheme, - setFont, - SplitTitleBar, - NavigationBarPushButton, + InfoBar, + InfoBarPosition, MSFluentWindow, - SingleDirectionScrollArea, NavigationBar, - qrouter, - FluentIconBase, + NavigationBarPushButton, NavigationItemPosition, - InfoBar, - InfoBarPosition, + SingleDirectionScrollArea, + SplitTitleBar, + isDarkTheme, + qconfig, + qrouter, + setFont, ) from qfluentwidgets.common.animation import BackgroundAnimationWidget -from qfluentwidgets.common.config import qconfig from qfluentwidgets.components.widgets.frameless_window import FramelessWindow from qfluentwidgets.window.stacked_widget import StackedWidget -from typing import Union # 伪装父类 (替换 FluentWindowBase 初始化) @@ -171,7 +170,7 @@ def insertItem( self, index: int, routeKey: str, - icon: Union[str, QIcon, FluentIconBase], + icon: str | QIcon | FluentIconBase, text: str, onClick=None, selectable=True, diff --git a/src/zzz_mcp/__init__.py b/src/zzz_mcp/__init__.py new file mode 100644 index 0000000000..57577794ef --- /dev/null +++ b/src/zzz_mcp/__init__.py @@ -0,0 +1,7 @@ +""" +ZZZ MCP Server - 绝区零游戏画面感知MCP服务器 + +提供游戏截图、画面识别等功能,用于游戏内容更新后的适配工作。 +""" + +__version__ = "0.1.0" diff --git a/src/zzz_mcp/context.py b/src/zzz_mcp/context.py new file mode 100644 index 0000000000..7087d93b2b --- /dev/null +++ b/src/zzz_mcp/context.py @@ -0,0 +1,61 @@ +""" +MCP 服务器上下文管理 + +管理 ZContext 的生命周期,为所有 MCP 工具提供共享的游戏上下文。 +""" +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass + +from mcp.server.fastmcp import FastMCP +from zzz_od.context.zzz_context import ZContext + +# 全局 ZContext 实例 +_zzz_context: ZContext | None = None + + +@dataclass +class McpContext: + """MCP 服务器应用上下文,包装 ZContext 实例""" + zzz: ZContext + + +def get_zzz_context() -> ZContext | None: + """ + 获取全局 ZContext 实例 + + Returns: + ZContext | None: 全局 ZContext 实例,如果未初始化则返回 None + """ + return _zzz_context + + +@asynccontextmanager +async def zzz_lifespan(server: FastMCP) -> AsyncIterator[McpContext]: + """ + 管理 ZContext 的生命周期 + + 在服务器启动时初始化 ZContext,在服务器关闭时清理资源。 + + Args: + server: FastMCP 服务器实例 + + Yields: + McpContext: 包含初始化后的 ZContext 的应用上下文 + """ + global _zzz_context + from one_dragon.utils.log_utils import log + log.info("ZZZ MCP Server: Initializing ZContext...") + ctx = ZContext() + _zzz_context = ctx + try: + # 初始化 ZContext(参考 app.py 的使用方式) + ctx.init() + log.info(f"ZZZ MCP Server: ZContext initialized. Ready: {ctx.ready_for_application}") + yield McpContext(zzz=ctx) + finally: + # 清理资源 + log.info("ZZZ MCP Server: Shutting down ZContext...") + _zzz_context = None + ctx.after_app_shutdown() + log.info("ZZZ MCP Server: ZContext shutdown complete") diff --git a/src/zzz_mcp/tools/__init__.py b/src/zzz_mcp/tools/__init__.py new file mode 100644 index 0000000000..d6c5d793a1 --- /dev/null +++ b/src/zzz_mcp/tools/__init__.py @@ -0,0 +1,19 @@ +from mcp.server.fastmcp import FastMCP + +from .base import register_base_tools +from .game_operation import register_game_tools +from .screenshot import register_screenshot_tools +from .screen_analysis import register_screen_analysis_tools + + +def register_all_tools(mcp: FastMCP) -> None: + """ + 注册所有工具模块 + + Args: + mcp: FastMCP 服务器实例 + """ + register_base_tools(mcp) + register_screenshot_tools(mcp) + register_game_tools(mcp) + register_screen_analysis_tools(mcp) diff --git a/src/zzz_mcp/tools/base.py b/src/zzz_mcp/tools/base.py new file mode 100644 index 0000000000..3d20c471e5 --- /dev/null +++ b/src/zzz_mcp/tools/base.py @@ -0,0 +1,45 @@ +""" +基础工具模块 + +提供基础的 MCP 工具,如 ping、status 等。 +""" +from mcp.server.fastmcp import FastMCP + +from ..context import get_zzz_context + + +def register_base_tools(mcp: FastMCP) -> None: + """注册基础工具""" + + @mcp.tool() + def check_game_window() -> str: + """ + 检查绝区零游戏窗口状态 + + Returns: + str: 游戏窗口状态信息,包括窗口标题、有效性、激活状态、位置和大小 + """ + zzz = get_zzz_context() + if zzz is None: + return "错误: ZContext 未初始化" + + if zzz.controller is None: + return "错误: 控制器未初始化" + + game_win = zzz.controller.game_win + if game_win is None: + return "错误: 游戏窗口未初始化" + + status_lines = [ + "游戏窗口状态:", + f" 窗口标题: {game_win.win_title}", + f" 窗口有效: {game_win.is_win_valid}", + f" 窗口激活: {game_win.is_win_active}", + f" 窗口缩放: {game_win.is_win_scale}", + ] + + if game_win.win_rect is not None: + rect = game_win.win_rect + status_lines.append(f" 窗口位置: x={rect.x1}, y={rect.y1}, 宽={rect.width}, 高={rect.height}") + + return "\n".join(status_lines) diff --git a/src/zzz_mcp/tools/game_operation.py b/src/zzz_mcp/tools/game_operation.py new file mode 100644 index 0000000000..a56b899f45 --- /dev/null +++ b/src/zzz_mcp/tools/game_operation.py @@ -0,0 +1,69 @@ +""" +游戏操作工具模块 + +提供游戏启动、操作相关的 MCP 工具。 +""" +from mcp.server.fastmcp import FastMCP + +from ..context import get_zzz_context + + +def register_game_tools(mcp: FastMCP) -> None: + """注册游戏操作工具""" + + @mcp.tool() + def open_and_enter_game() -> str: + """ + 打开并进入绝区零游戏 + + 注意: + 1. 此操作需要较长时间(可能需要1-2分钟) + 2. 执行步骤:启动运行上下文 → 打开游戏客户端 → 等待窗口初始化 → 自动登录 + 3. ⚠️ 环境要求: + - 不支持远程桌面/SSH 会话环境(如 RDP、frps、SSH隧道) + - 游戏会检测运行环境,在远程会话下无法创建可见窗口 + - 建议在本地交互式会话中使用,或先手动启动游戏再使用其他工具 + + 操作流程: + 1. 关闭自动HDR + 2. 启动游戏进程 + 3. 等待游戏窗口就绪(最多60秒) + 4. 恢复HDR设置 + 5. 执行登录操作 + + Returns: + str: 操作结果信息 + """ + zzz = get_zzz_context() + if zzz is None: + return "错误: ZContext 未初始化" + + from zzz_od.operation.enter_game.open_and_enter_game import OpenAndEnterGame + from one_dragon.utils.log_utils import log + + try: + log.info("MCP: 开始执行打开并进入游戏操作") + + # 启动运行上下文 + if not zzz.run_context.start_running(): + return "错误: 无法启动运行上下文,请检查控制器是否已初始化" + + try: + # 设置运行上下文属性(参考 suibian_temple_craft_dispatch.py 的示例) + zzz.run_context.current_instance_idx = zzz.current_instance_idx + + # 执行打开并进入游戏操作 + op = OpenAndEnterGame(zzz) + result = op.execute() + + if result.success: + return "成功打开并进入绝区零游戏" + else: + return f"打开游戏失败: {result.status}" + finally: + # 停止运行上下文(重要:确保资源正确释放) + zzz.run_context.stop_running() + + except Exception as e: + log.error(f"MCP: 打开游戏时发生错误: {e}", exc_info=True) + return f"打开游戏时发生错误: {str(e)}" diff --git a/src/zzz_mcp/tools/screen_analysis.py b/src/zzz_mcp/tools/screen_analysis.py new file mode 100644 index 0000000000..91d7d882b9 --- /dev/null +++ b/src/zzz_mcp/tools/screen_analysis.py @@ -0,0 +1,116 @@ +""" +画面分析工具模块 + +提供游戏画面分析相关的 MCP 工具。 +""" + +from dataclasses import dataclass + +from mcp.server.fastmcp import FastMCP + +from one_dragon.utils.log_utils import log +from zzz_mcp.context import get_zzz_context + + +@dataclass +class OcrText: + """OCR 识别的文本""" + text: str # 识别的文本内容 + x: int # 文本区域左上角 X 坐标 + y: int # 文本区域左上角 Y 坐标 + width: int # 文本区域宽度 + height: int # 文本区域高度 + + +@dataclass +class AnalyzeScreenResult: + """画面分析结果""" + success: bool # 是否成功 + ocr_texts: list[OcrText] # OCR 识别的文本列表 + error: str | None = None # 错误信息 + + +def register_screen_analysis_tools(mcp: FastMCP) -> None: + """注册画面分析相关工具""" + + @mcp.tool() + def analyze_screen() -> AnalyzeScreenResult: + """ + 分析绝区零游戏当前画面 + + 返回当前画面的详细信息,包括 OCR 识别结果。 + 后续会扩展到模板匹配、屏幕识别等内容。 + + Returns: + AnalyzeScreenResult: 画面分析结果 + """ + zzz = get_zzz_context() + if zzz is None: + return AnalyzeScreenResult( + success=False, + ocr_texts=[], + error="ZContext 未初始化" + ) + + if zzz.controller is None: + return AnalyzeScreenResult( + success=False, + ocr_texts=[], + error="控制器未初始化" + ) + + if not zzz.controller.is_game_window_ready: + return AnalyzeScreenResult( + success=False, + ocr_texts=[], + error="游戏窗口未就绪" + ) + + if zzz.ocr_service is None: + return AnalyzeScreenResult( + success=False, + ocr_texts=[], + error="OCR 服务未初始化" + ) + + try: + # 1. 截图 + image = zzz.controller.get_screenshot(independent=False) + if image is None: + return AnalyzeScreenResult( + success=False, + ocr_texts=[], + error="截图失败" + ) + + # 2. OCR 识别 + ocr_result_list = zzz.ocr_service.get_ocr_result_list( + image=image, + ) + + # 3. 构建返回结果 + ocr_texts = [] + for ocr_result in ocr_result_list: + ocr_texts.append(OcrText( + text=ocr_result.data, + x=int(ocr_result.x), + y=int(ocr_result.y), + width=int(ocr_result.w), + height=int(ocr_result.h), + )) + + log.info(f"画面分析完成,识别到 {len(ocr_texts)} 个文本") + + return AnalyzeScreenResult( + success=True, + ocr_texts=ocr_texts, + error=None, + ) + + except Exception as e: + log.error(f"画面分析失败: {e}", exc_info=True) + return AnalyzeScreenResult( + success=False, + ocr_texts=[], + error=f"画面分析失败 - {str(e)}" + ) diff --git a/src/zzz_mcp/tools/screenshot.py b/src/zzz_mcp/tools/screenshot.py new file mode 100644 index 0000000000..f1f808c88e --- /dev/null +++ b/src/zzz_mcp/tools/screenshot.py @@ -0,0 +1,82 @@ +""" +截图工具模块 + +提供游戏截图相关的 MCP 工具。 +""" + +import time +from pathlib import Path + +from mcp.server.fastmcp import FastMCP + +from one_dragon.utils import os_utils +from one_dragon.utils.log_utils import log +from zzz_mcp.context import get_zzz_context + + +def get_screenshot_dir() -> Path: + """ + 获取截图保存目录 + 如果目录不存在则自动创建 + + Returns: + Path: 截图保存目录的绝对路径 + """ + dir_path = os_utils.get_path_under_work_dir(".debug", "zzz_od_mcp", "screenshot") + return Path(dir_path).absolute() + + +def register_screenshot_tools(mcp: FastMCP) -> None: + """注册截图相关工具""" + + @mcp.tool() + def capture_game_screen() -> str: + """ + 捕获绝区零游戏当前画面 + + Returns: + str: 截图保存的绝对路径 + """ + zzz = get_zzz_context() + if zzz is None: + return "错误: ZContext 未初始化" + + if zzz.controller is None: + return "错误: 控制器未初始化" + + if not zzz.controller.is_game_window_ready: + return "错误: 游戏窗口未就绪" + + try: + # 执行截图 - 使用正确的 API + image = zzz.controller.get_screenshot(independent=False) + + if image is None: + return "错误: 截图失败,返回值为 None" + + # 生成带时间戳的文件名 + timestamp = time.strftime("%Y%m%d_%H%M%S") + filename = f"screenshot_{timestamp}.png" + + # 确保目录存在 + screenshot_dir = get_screenshot_dir() + screenshot_dir.mkdir(parents=True, exist_ok=True) + + # 保存截图 + img_path = screenshot_dir / filename + + # 将 OpenCV 图像(RGB)保存为 PNG + import cv2 + + # image 是 RGB 格式,需要转为 BGR 格式才能被 cv2.imwrite 正确保存 + bgr_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + cv2.imwrite(str(img_path), bgr_image) + + log.info(f"截图已保存到: {img_path}") + + # 返回绝对路径 + return str(img_path) + + except Exception as e: + log.error(f"截图失败: {e}", exc_info=True) + return f"错误: 截图失败 - {str(e)}" diff --git a/src/zzz_mcp/zzz_mcp_server.py b/src/zzz_mcp/zzz_mcp_server.py new file mode 100644 index 0000000000..fcd1540f71 --- /dev/null +++ b/src/zzz_mcp/zzz_mcp_server.py @@ -0,0 +1,70 @@ +""" +ZZZ MCP Server - 绝区零游戏画面感知MCP服务器 + +提供游戏截图、画面识别等功能,用于游戏内容更新后的适配工作。 +""" +import sys + +import uvicorn +from mcp.server.fastmcp import FastMCP + +from zzz_mcp.context import zzz_lifespan +from zzz_mcp.tools import register_all_tools + +# 创建MCP服务器实例,传入 lifespan 管理 ZContext +mcp = FastMCP("zzz_od", lifespan=zzz_lifespan) + +# 注册所有工具 +register_all_tools(mcp) + + +def main(host: str = "127.0.0.1", port: int = 8000): + """ + 启动MCP服务器 + + Args: + host: 监听地址 + port: 监听端口 + """ + print("=" * 60) + print("ZZZ OD MCP Server (HTTP传输方式)") + print("=" * 60) + print(f"\n监听地址: http://{host}:{port}/mcp") + print("\n按 Ctrl+C 停止服务器\n") + print("-" * 60) + + # 获取Starlette应用并使用uvicorn运行 + app = mcp.streamable_http_app() + uvicorn.run(app, host=host, port=port) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="启动ZZZ OD MCP服务器(HTTP传输方式)" + ) + parser.add_argument( + "--host", + default="127.0.0.1", + help="监听地址 (默认: 127.0.0.1)" + ) + parser.add_argument( + "--port", + type=int, + default=8000, + help="监听端口 (默认: 8000)" + ) + + args = parser.parse_args() + + try: + main(host=args.host, port=args.port) + except KeyboardInterrupt: + print("\n\n服务器已停止") + sys.exit(0) + except Exception as e: + print(f"\n[ERROR] 服务器启动失败: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/src/zzz_mcp/zzz_od_server_manage.py b/src/zzz_mcp/zzz_od_server_manage.py new file mode 100644 index 0000000000..65c4520792 --- /dev/null +++ b/src/zzz_mcp/zzz_od_server_manage.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" +ZZZ OD Server Management MCP Server + +轻量级管理服务器,长期运行在 Session 1 中,用于管理 zzz_od MCP 服务器的启停。 +""" +import subprocess +import time +import psutil +from typing import Optional +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("ZZZ OD Server Manage") + +# 配置 +PROJECT_ROOT = r"D:\code\workspace\ZenlessZoneZero-OneDragon" +MAIN_SERVER_SCRIPT = "src/zzz_mcp/zzz_mcp_server.py" +MAIN_SERVER_PORT = 8000 + + +def find_main_server_process() -> Optional[psutil.Process]: + """查找 zzz_od MCP 主服务器进程""" + for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']): + try: + cmdline = proc.info['cmdline'] + if cmdline and any('zzz_mcp_server.py' in arg for arg in cmdline): + return proc + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + return None + + +def is_port_in_use(port: int) -> bool: + """检查端口是否被占用""" + for conn in psutil.net_connections(): + if conn.laddr.port == port and conn.status == 'LISTEN': + return True + return False + + +@mcp.tool() +def start_zzz_od_server() -> str: + """ + 启动 ZZZ OD MCP 主服务器 + + 在 Session 1 中启动游戏操作 MCP 服务器,用于游戏窗口检测和操作。 + + Returns: + str: 启动结果信息 + """ + # 检查是否已经在运行 + existing_proc = find_main_server_process() + if existing_proc: + return f"[OK] ZZZ OD MCP Server 已在运行 (PID: {existing_proc.pid})" + + if is_port_in_use(MAIN_SERVER_PORT): + return f"[WARN] 端口 {MAIN_SERVER_PORT} 已被占用,可能有其他程序在使用" + + # 启动服务器 + try: + cmd = f'cd /d "{PROJECT_ROOT}" && uv run --env-file .env python {MAIN_SERVER_SCRIPT}' + + # 使用 POPEN 启动,不阻塞 + process = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8' + ) + + # 等待一下确保启动成功 + time.sleep(2) + + # 检查进程是否还在运行 + if process.poll() is None: + return f"[SUCCESS] ZZZ OD MCP Server 启动成功 (PID: {process.pid})\n端口: {MAIN_SERVER_PORT}" + else: + stdout, stderr = process.communicate() + error_msg = stderr if stderr else "未知错误" + return f"[ERROR] 启动失败: {error_msg}" + + except Exception as e: + return f"[ERROR] 启动异常: {str(e)}" + + +@mcp.tool() +def stop_zzz_od_server() -> str: + """ + 停止 ZZZ OD MCP 主服务器 + + 停止正在运行的 zzz_od MCP 服务器进程。 + + Returns: + str: 停止结果信息 + """ + proc = find_main_server_process() + + if not proc: + # 检查端口是否被占用 + if is_port_in_use(MAIN_SERVER_PORT): + return f"[WARN] 未找到 zzz_od_server 进程,但端口 {MAIN_SERVER_PORT} 被占用" + return "[OK] ZZZ OD MCP Server 未运行" + + try: + # 终止进程及其子进程 + children = proc.children(recursive=True) + for child in children: + child.terminate() + proc.terminate() + + # 等待进程结束 + gone, alive = psutil.wait_procs([proc] + children, timeout=5) + + # 如果还有存活进程,强制杀掉 + if alive: + for p in alive: + p.kill() + + return f"[SUCCESS] ZZZ OD MCP Server 已停止 (PID: {proc.pid})" + + except psutil.NoSuchProcess: + return "[OK] ZZZ OD MCP Server 已停止" + except Exception as e: + return f"[ERROR] 停止失败: {str(e)}" + + +@mcp.tool() +def restart_zzz_od_server() -> str: + """ + 重启 ZZZ OD MCP 主服务器 + + 先停止当前运行的服务器,然后重新启动。 + + Returns: + str: 重启结果信息 + """ + stop_result = stop_zzz_od_server() + + if "[ERROR]" in stop_result: + return f"[ERROR] 重启失败 - 停止阶段出错:\n{stop_result}" + + # 等待端口释放 + time.sleep(2) + + start_result = start_zzz_od_server() + + return f"[RESTART]\n{stop_result}\n{start_result}" + + +@mcp.tool() +def get_zzz_od_server_status() -> str: + """ + 查看 ZZZ OD MCP 主服务器状态 + + Returns: + str: 服务器状态信息 + """ + proc = find_main_server_process() + + if not proc: + port_status = "占用" if is_port_in_use(MAIN_SERVER_PORT) else "空闲" + return f"[STATUS] ZZZ OD MCP Server 未运行\n端口 {MAIN_SERVER_PORT}: {port_status}" + + try: + # 获取进程信息 + with proc.oneshot(): + pid = proc.pid + create_time = time.ctime(proc.create_time()) + cpu_percent = proc.cpu_percent(interval=0.1) + memory_info = proc.memory_info() + + # 检查子进程数量 + children = len(proc.children(recursive=True)) + + status = f"""[STATUS] ZZZ OD MCP Server 运行中 +PID: {pid} +启动时间: {create_time} +CPU 使用: {cpu_percent}% +内存使用: {memory_info.rss / 1024 / 1024:.2f} MB +子进程数: {children} +端口: {MAIN_SERVER_PORT}""" + + return status + + except Exception as e: + return f"[STATUS] ZZZ OD MCP Server 运行中 (PID: {proc.pid})\n[ERROR] 无法获取详细信息: {str(e)}" + + +@mcp.tool() +def ping() -> str: + """ + Ping 管理服务器 + + 用于测试管理服务器是否正常运行。 + + Returns: + str: pong 响应 + """ + return "ZZZ OD Server Manage: pong" + + +if __name__ == "__main__": + # 运行管理服务器(HTTP stream,端口 8001) + import argparse + + parser = argparse.ArgumentParser(description='ZZZ OD Server Management MCP Server') + parser.add_argument('--host', default='127.0.0.1', help='Host to bind to') + parser.add_argument('--port', type=int, default=8001, help='Port to listen on') + + args = parser.parse_args() + + print("=" * 60) + print("ZZZ OD Server Management MCP Server") + print("=" * 60) + print(f"Host: {args.host}") + print(f"Port: {args.port}") + print(f"\n管理服务器地址: http://{args.host}:{args.port}/mcp") + print("\n可用工具:") + print(" - start_zzz_od_server: 启动主服务器") + print(" - stop_zzz_od_server: 停止主服务器") + print(" - restart_zzz_od_server: 重启主服务器") + print(" - get_zzz_od_server_status: 查看状态") + print(" - ping: 测试连接") + print("\n" + "=" * 60) + + mcp.run(transport="streamable-http", host=args.host, port=args.port) diff --git a/src/zzz_od/application/battle_assistant/auto_battle/auto_battle_app.py b/src/zzz_od/application/battle_assistant/auto_battle/auto_battle_app.py index 37ec54acd3..89cda02534 100644 --- a/src/zzz_od/application/battle_assistant/auto_battle/auto_battle_app.py +++ b/src/zzz_od/application/battle_assistant/auto_battle/auto_battle_app.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import ClassVar, TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from one_dragon.base.controller.pc_button import pc_button_utils from one_dragon.base.operation.operation_edge import node_from @@ -8,7 +8,7 @@ from one_dragon.base.operation.operation_round_result import OperationRoundResult from zzz_od.application.battle_assistant.auto_battle import auto_battle_const from zzz_od.application.zzz_application import ZApplication -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum if TYPE_CHECKING: from zzz_od.context.zzz_context import ZContext @@ -42,18 +42,18 @@ def check_gamepad(self) -> OperationRoundResult: 检测手柄 :return: """ - gamepad_type = self.ctx.battle_assistant_config.gamepad_type - if gamepad_type == GamepadTypeEnum.NONE.value.value: + gamepad_type = self.ctx.battle_assistant_config.control_method + if gamepad_type == ControlMethodEnum.KEYBOARD.value.value: self.ctx.controller.enable_keyboard() return self.round_success(status='无需手柄') elif not pc_button_utils.is_vgamepad_installed(): self.ctx.controller.enable_keyboard() return self.round_fail(status='未安装虚拟手柄依赖') - elif self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.XBOX.value.value: + elif self.ctx.battle_assistant_config.control_method == ControlMethodEnum.XBOX.value.value: self.ctx.controller.enable_xbox() self.ctx.controller.btn_controller.set_key_press_time(self.ctx.game_config.xbox_key_press_time) - elif self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.DS4.value.value: + elif self.ctx.battle_assistant_config.control_method == ControlMethodEnum.DS4.value.value: self.ctx.controller.enable_ds4() self.ctx.controller.btn_controller.set_key_press_time(self.ctx.game_config.ds4_key_press_time) return self.round_success(status='已安装虚拟手柄依赖') diff --git a/src/zzz_od/application/battle_assistant/battle_assistant_config.py b/src/zzz_od/application/battle_assistant/battle_assistant_config.py index a73be63a29..5a326690b6 100644 --- a/src/zzz_od/application/battle_assistant/battle_assistant_config.py +++ b/src/zzz_od/application/battle_assistant/battle_assistant_config.py @@ -1,5 +1,5 @@ from one_dragon.base.config.yaml_config import YamlConfig -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum class BattleAssistantConfig(YamlConfig): @@ -32,12 +32,12 @@ def screenshot_interval(self, new_value: float) -> None: self.update('screenshot_interval', new_value) @property - def gamepad_type(self) -> str: - return self.get('gamepad_type', GamepadTypeEnum.NONE.value.value) + def control_method(self) -> str: + return self.get('control_method', ControlMethodEnum.KEYBOARD.value.value) - @gamepad_type.setter - def gamepad_type(self, new_value: str) -> None: - self.update('gamepad_type', new_value) + @control_method.setter + def control_method(self, new_value: str) -> None: + self.update('control_method', new_value) @property def auto_battle_config(self) -> str: diff --git a/src/zzz_od/application/battle_assistant/dodge_assitant/dodge_assistant_app.py b/src/zzz_od/application/battle_assistant/dodge_assitant/dodge_assistant_app.py index fd80fc0883..9ce4999cb9 100644 --- a/src/zzz_od/application/battle_assistant/dodge_assitant/dodge_assistant_app.py +++ b/src/zzz_od/application/battle_assistant/dodge_assitant/dodge_assistant_app.py @@ -8,7 +8,7 @@ from zzz_od.application.battle_assistant.dodge_assitant import dodge_assistant_const from zzz_od.application.zzz_application import ZApplication from zzz_od.auto_battle import auto_battle_utils -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum from zzz_od.context.zzz_context import ZContext @@ -38,16 +38,16 @@ def check_gamepad(self) -> OperationRoundResult: 检测手柄 :return: """ - if self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.NONE.value.value: + if self.ctx.battle_assistant_config.control_method == ControlMethodEnum.KEYBOARD.value.value: self.ctx.controller.enable_keyboard() return self.round_success(status='无需手柄') elif not pc_button_utils.is_vgamepad_installed(): self.ctx.controller.enable_keyboard() return self.round_fail(status='未安装虚拟手柄依赖') - elif self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.XBOX.value.value: + elif self.ctx.battle_assistant_config.control_method == ControlMethodEnum.XBOX.value.value: self.ctx.controller.enable_xbox() self.ctx.controller.btn_controller.set_key_press_time(self.ctx.game_config.xbox_key_press_time) - elif self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.DS4.value.value: + elif self.ctx.battle_assistant_config.control_method == ControlMethodEnum.DS4.value.value: self.ctx.controller.enable_ds4() self.ctx.controller.btn_controller.set_key_press_time(self.ctx.game_config.ds4_key_press_time) return self.round_success(status='已安装虚拟手柄依赖') diff --git a/src/zzz_od/application/battle_assistant/operation_debug/operation_debug_app.py b/src/zzz_od/application/battle_assistant/operation_debug/operation_debug_app.py index ae93148bad..c3e773c9f1 100644 --- a/src/zzz_od/application/battle_assistant/operation_debug/operation_debug_app.py +++ b/src/zzz_od/application/battle_assistant/operation_debug/operation_debug_app.py @@ -11,7 +11,7 @@ from zzz_od.application.battle_assistant.operation_debug import operation_debug_const from zzz_od.application.zzz_application import ZApplication from zzz_od.auto_battle.auto_battle_operator import AutoBattleOperator -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum from zzz_od.context.zzz_context import ZContext @@ -37,16 +37,16 @@ def check_gamepad(self) -> OperationRoundResult: 检测手柄 :return: """ - if self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.NONE.value.value: + if self.ctx.battle_assistant_config.control_method == ControlMethodEnum.KEYBOARD.value.value: self.ctx.controller.enable_keyboard() return self.round_success(status='无需手柄') elif not pc_button_utils.is_vgamepad_installed(): self.ctx.controller.enable_keyboard() return self.round_fail(status='未安装虚拟手柄依赖') - elif self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.XBOX.value.value: + elif self.ctx.battle_assistant_config.control_method == ControlMethodEnum.XBOX.value.value: self.ctx.controller.enable_xbox() self.ctx.controller.btn_controller.set_key_press_time(self.ctx.game_config.xbox_key_press_time) - elif self.ctx.battle_assistant_config.gamepad_type == GamepadTypeEnum.DS4.value.value: + elif self.ctx.battle_assistant_config.control_method == ControlMethodEnum.DS4.value.value: self.ctx.controller.enable_ds4() self.ctx.controller.btn_controller.set_key_press_time(self.ctx.game_config.ds4_key_press_time) return self.round_success(status='已安装虚拟手柄依赖') diff --git a/src/zzz_od/application/charge_plan/charge_plan_app.py b/src/zzz_od/application/charge_plan/charge_plan_app.py index 9ae8364262..d17039c41b 100644 --- a/src/zzz_od/application/charge_plan/charge_plan_app.py +++ b/src/zzz_od/application/charge_plan/charge_plan_app.py @@ -12,6 +12,7 @@ ChargePlanConfig, ChargePlanItem, ) +from zzz_od.application.charge_plan.charge_plan_run_record import ChargePlanRunRecord from zzz_od.application.zzz_application import ZApplication from zzz_od.context.zzz_context import ZContext from zzz_od.operation.back_to_normal_world import BackToNormalWorld @@ -42,15 +43,21 @@ def __init__(self, ctx: ZContext): instance_idx=self.ctx.current_instance_idx, group_id=application_const.DEFAULT_GROUP_ID, ) + self.run_record: ChargePlanRunRecord = self.ctx.run_context.get_run_record( + app_id=charge_plan_const.APP_ID, + instance_idx=self.ctx.current_instance_idx, + ) self.charge_power: int = 0 # 剩余电量 self.required_charge: int = 0 # 需要的电量 self.last_tried_plan: ChargePlanItem | None = None - self.next_plan: ChargePlanItem | None = None + self.current_plan: ChargePlanItem | None = None @operation_node(name='开始体力计划', is_start_node=True) def start_charge_plan(self) -> OperationRoundResult: self.last_tried_plan = None + for plan in self.config.plan_list: + plan.skipped = False return self.round_success() @node_from(from_name='挑战完成') @@ -75,6 +82,7 @@ def check_charge_power(self) -> OperationRoundResult: return self.round_retry('未识别到电量', wait=1) self.charge_power = digit + self.run_record.record_current_charge_power(digit) return self.round_success(f'剩余电量 {digit}') @node_from(from_name='识别电量') @@ -129,51 +137,51 @@ def find_and_select_next_plan(self) -> OperationRoundResult: continue else: # 设置下一个计划,然后触发恢复电量 - self.next_plan = candidate_plan + self.current_plan = candidate_plan self.required_charge = need_charge_power - self.charge_power return self.round_success(ChargePlanApp.STATUS_TRY_RESTORE_CHARGE) # 设置下一个计划并返回成功 - self.next_plan = candidate_plan + self.current_plan = candidate_plan return self.round_success() @node_from(from_name='查找并选择下一个可执行任务') @operation_node(name='传送') def transport(self) -> OperationRoundResult: - # 使用已经在查找并选择下一个可执行任务节点中设置好的self.next_plan + # 使用已经在查找并选择下一个可执行任务节点中设置好的self.current_plan op = TransportByCompendium(self.ctx, - self.next_plan.tab_name, - self.next_plan.category_name, - self.next_plan.mission_type_name) + self.current_plan.tab_name, + self.current_plan.category_name, + self.current_plan.mission_type_name) return self.round_by_op_result(op.execute()) @node_from(from_name='传送') @operation_node(name='识别副本分类') def check_mission_type(self) -> OperationRoundResult: - return self.round_success(self.next_plan.category_name) + return self.round_success(self.current_plan.category_name) @node_from(from_name='识别副本分类', status='实战模拟室') @operation_node(name='实战模拟室') def combat_simulation(self) -> OperationRoundResult: - op = CombatSimulation(self.ctx, self.next_plan) + op = CombatSimulation(self.ctx, self.current_plan) return self.round_by_op_result(op.execute()) @node_from(from_name='识别副本分类', status='区域巡防') @operation_node(name='区域巡防') def area_patrol(self) -> OperationRoundResult: - op = AreaPatrol(self.ctx, self.next_plan) + op = AreaPatrol(self.ctx, self.current_plan) return self.round_by_op_result(op.execute()) @node_from(from_name='识别副本分类', status='专业挑战室') @operation_node(name='专业挑战室') def expert_challenge(self) -> OperationRoundResult: - op = ExpertChallenge(self.ctx, self.next_plan) + op = ExpertChallenge(self.ctx, self.current_plan) return self.round_by_op_result(op.execute()) @node_from(from_name='识别副本分类', status='恶名狩猎') @operation_node(name='恶名狩猎') def notorious_hunt(self) -> OperationRoundResult: - op = NotoriousHunt(self.ctx, self.next_plan, use_charge_power=True) + op = NotoriousHunt(self.ctx, self.current_plan, use_charge_power=True) return self.round_by_op_result(op.execute()) @node_from(from_name='实战模拟室', success=True) @@ -198,9 +206,11 @@ def challenge_complete(self) -> OperationRoundResult: @node_from(from_name='传送', success=False, status='找不到 代理人方案培养') @operation_node(name='电量不足') def charge_not_enough(self) -> OperationRoundResult: - if self.config.skip_plan or self.next_plan.mission_type_name == '代理人方案培养': - # 跳过当前计划,继续尝试下一个 - self.last_tried_plan = self.next_plan + is_agent_plan = self.current_plan.is_agent_plan + if self.config.skip_plan or is_agent_plan: + # 标记当前计划为跳过,继续尝试下一个 + self.current_plan.skipped = True + self.last_tried_plan = self.current_plan return self.round_success() else: # 不跳过,直接结束本轮计划 diff --git a/src/zzz_od/application/charge_plan/charge_plan_config.py b/src/zzz_od/application/charge_plan/charge_plan_config.py index 02eea48198..6e8fa9f5e6 100644 --- a/src/zzz_od/application/charge_plan/charge_plan_config.py +++ b/src/zzz_od/application/charge_plan/charge_plan_config.py @@ -55,15 +55,20 @@ def __init__( self.predefined_team_idx: int = predefined_team_idx # 预备配队下标 -1为使用当前配队 self.notorious_hunt_buff_num: int = notorious_hunt_buff_num # 恶名狩猎 选择的buff self.plan_id: str = plan_id if plan_id else str(uuid.uuid4()) # 计划的唯一标识符 + self.skipped: bool = False # 单次运行中是否跳过(不持久化) + + @property + def is_agent_plan(self) -> bool: + return self.mission_type_name == '代理人方案培养' @property def uid(self) -> str: - return '%s_%s_%s_%s' % ( - self.tab_name if self.tab_name is not None else '', - self.category_name if self.category_name is not None else '', - self.mission_type_name if self.mission_type_name is not None else '', - self.mission_name if self.mission_name is not None else '', - ) + + tab_name = self.tab_name or '' + category_name = self.category_name or '' + mission_type_name = self.mission_type_name or '' + mission_name = self.mission_name or '' + return f'{tab_name}_{category_name}_{mission_type_name}_{mission_name}' class ChargePlanConfig(ApplicationConfig): @@ -78,20 +83,6 @@ def __init__(self, instance_idx: int, group_id: str): self.plan_list: list[ChargePlanItem] = [] - # 迁移旧名称 2025/12/31 移除 - migration_list = [] - migration_list.extend(self.data.get('plan_list', [])) - migration_list.extend(self.data.get('history_list', [])) - - is_changed = False - for item in migration_list: - if item.get('category_name') == '定期清剿': - item['category_name'] = '区域巡防' - is_changed = True - - if is_changed: - YamlConfig.save(self) - for plan_item in self.data.get('plan_list', []): self.plan_list.append(ChargePlanItem(**plan_item)) @@ -173,32 +164,31 @@ def move_top(self, idx: int) -> None: def reset_plans(self) -> None: """ - 根据运行次数 重置运行计划 - :return: + 根据运行次数 重置运行计划(跳过 skipped 的计划) """ if len(self.plan_list) == 0: return - while True: - all_finish: bool = True - for plan in self.plan_list: - if plan.run_times < plan.plan_times: - all_finish = False + eligible = [p for p in self.plan_list if not p.skipped] + if not eligible: + return - if not all_finish: + while True: + if any(p.run_times < p.plan_times for p in eligible): break - for plan in self.plan_list: + for plan in eligible: plan.run_times -= plan.plan_times self.save() def get_next_plan(self, last_tried_plan: ChargePlanItem | None = None) -> ChargePlanItem | None: """ - 获取下一个未完成的计划任务。 + 获取下一个未完成的计划任务(跳过 skipped 的计划)。 如果提供了 last_tried_plan,则从该任务之后开始查找。 如果未提供,则从列表的开头查找第一个未完成任务。 - 不再在此方法内调用 reset_plans,重置逻辑由调用方(ChargePlanApp)管理。 + Args: + last_tried_plan: 上次尝试的计划 """ if len(self.plan_list) == 0: return None @@ -224,6 +214,8 @@ def get_next_plan(self, last_tried_plan: ChargePlanItem | None = None) -> Charge # 3. 从指定位置开始遍历查找符合条件的计划 for i in range(start_index, len(self.plan_list)): plan = self.plan_list[i] + if plan.skipped: + continue if plan.run_times < plan.plan_times: return plan @@ -232,14 +224,17 @@ def get_next_plan(self, last_tried_plan: ChargePlanItem | None = None) -> Charge def all_plan_finished(self) -> bool: """ - 是否全部计划已完成 - :return: + 是否全部计划已完成(跳过 skipped 的计划) """ if self.plan_list is None: return True - return all(plan.run_times >= plan.plan_times for plan in self.plan_list) - + for plan in self.plan_list: + if plan.skipped: + continue + if plan.run_times < plan.plan_times: + return False + return True def add_plan_run_times(self, to_add: ChargePlanItem) -> None: """ diff --git a/src/zzz_od/application/charge_plan/charge_plan_run_record.py b/src/zzz_od/application/charge_plan/charge_plan_run_record.py index 69eadcc999..0ca49b3c64 100644 --- a/src/zzz_od/application/charge_plan/charge_plan_run_record.py +++ b/src/zzz_od/application/charge_plan/charge_plan_run_record.py @@ -1,11 +1,12 @@ -from typing import Optional +import time from one_dragon.base.operation.application_run_record import AppRunRecord class ChargePlanRunRecord(AppRunRecord): + MAX_CHARGE_POWER = 240 - def __init__(self, instance_idx: Optional[int] = None, game_refresh_hour_offset: int = 0): + def __init__(self, instance_idx: int | None = None, game_refresh_hour_offset: int = 0): AppRunRecord.__init__( self, 'charge_plan', @@ -13,5 +14,35 @@ def __init__(self, instance_idx: Optional[int] = None, game_refresh_hour_offset: game_refresh_hour_offset=game_refresh_hour_offset ) - def check_and_update_status(self): # 每次都运行 - self.reset_record() \ No newline at end of file + def check_and_update_status(self) -> None: # 每次都运行 + self.reset_record() + + def reset_record(self) -> None: + AppRunRecord.reset_record(self) + self.charge_power_snapshot = [0, -1] + + @property + def charge_power_snapshot(self) -> list[int]: + return self.get('current_charge_power_snapshot', [0, -1]) + + @charge_power_snapshot.setter + def charge_power_snapshot(self, new_value: list[int]) -> None: + charge_power, record_time = new_value + self.update('current_charge_power_snapshot', [charge_power, record_time]) + + def record_current_charge_power(self, charge_power: int) -> None: + self.charge_power_snapshot = [charge_power, int(time.time())] + + def get_estimated_charge_power(self) -> int: + charge_power, record_time = self.charge_power_snapshot + if record_time == -1: + return -1 + + current_time = int(time.time()) + elapsed_seconds = max(0, current_time - record_time) + recovered = int(elapsed_seconds // 360) # 每6分钟恢复1点体力 + + return min( + charge_power + recovered, + ChargePlanRunRecord.MAX_CHARGE_POWER, + ) diff --git a/src/zzz_od/application/city_fund/city_fund_app.py b/src/zzz_od/application/city_fund/city_fund_app.py index 9aa8f904f7..4a6e5cfa1d 100644 --- a/src/zzz_od/application/city_fund/city_fund_app.py +++ b/src/zzz_od/application/city_fund/city_fund_app.py @@ -35,6 +35,7 @@ def click_fund(self) -> OperationRoundResult: success_wait=1, retry_wait=1) @node_from(from_name='点击丽都城募') + @node_from(from_name='点击成长任务', status='按钮-确认') @operation_node(name='点击成长任务') def click_task(self) -> OperationRoundResult: result = self.round_by_find_and_click_area(self.last_screenshot, '丽都城募', '开启丽都城募') @@ -64,10 +65,17 @@ def click_level(self) -> OperationRoundResult: @node_notify(when=NotifyTiming.CURRENT_SUCCESS) @operation_node(name='等级全部领取') def click_level_claim(self) -> OperationRoundResult: - return self.round_by_find_and_click_area(self.last_screenshot, '丽都城募', '等级-全部领取', - success_wait=1, retry_wait=1) + # 2.6版本更新,等级回馈领取,已领取过全部领取按钮是会消失的 + for screen_name, area_name in [ + ('丽都城募', '等级-全部领取'), + ('丽都城募', '按钮-确认'), + ]: + result = self.round_by_find_and_click_area(self.last_screenshot, screen_name, area_name, success_wait=1) + if result.is_success: + return self.round_retry(status=result.status, wait=1) + + return self.round_success() # 两个按钮都未找到,说明已领取完毕 - @node_from(from_name='点击成长任务', status='按钮-确认') @node_from(from_name='等级全部领取') @node_from(from_name='等级全部领取', success=False) @operation_node(name='返回大世界') @@ -78,7 +86,7 @@ def back_to_world(self) -> OperationRoundResult: def __debug(): ctx = ZContext() - ctx.init_by_config() + ctx.init() app = CityFundApp(ctx) app.execute() diff --git a/src/zzz_od/application/engagement_reward/engagement_reward_app.py b/src/zzz_od/application/engagement_reward/engagement_reward_app.py index 198f09dba3..52599a04d1 100644 --- a/src/zzz_od/application/engagement_reward/engagement_reward_app.py +++ b/src/zzz_od/application/engagement_reward/engagement_reward_app.py @@ -14,6 +14,7 @@ class EngagementRewardApp(ZApplication): STATUS_NO_REWARD: ClassVar[str] = '无奖励可领取' + STATUS_CLAIM_SUCCESS: ClassVar[str] = '日常奖励领取成功' def __init__(self, ctx: ZContext): """ @@ -26,13 +27,6 @@ def __init__(self, ctx: ZContext): op_name=engagement_reward_const.APP_NAME, ) - def handle_init(self) -> None: - """ - 执行前的初始化 由子类实现 - 注意初始化要全面 方便一个指令重复使用 - """ - self.idx: int = 4 - @operation_node(name='返回大世界', is_start_node=True) def back_at_first(self) -> OperationRoundResult: op = BackToNormalWorld(self.ctx) @@ -44,48 +38,44 @@ def goto_compendium_daily(self) -> OperationRoundResult: return self.round_by_goto_screen(screen_name='快捷手册-日常') @node_from(from_name='快捷手册-日常') - @operation_node(name='识别活跃度') - def check_engagement(self) -> OperationRoundResult: - area = self.ctx.screen_loader.get_area('快捷手册', '今日最大活跃度') - part = cv2_utils.crop_image_only(self.last_screenshot, area.rect) - - ocr_result = self.ctx.ocr.run_ocr_single_line(part) - num = str_utils.get_positive_digits(ocr_result, None) - if num is None: - return self.round_retry('识别活跃度失败', wait_round_time=1) - - self.idx = 4 # 只需要点最后一个就可以领取 - - return self.round_success() - - @node_from(from_name='识别活跃度') @operation_node(name='点击奖励') def click_reward(self) -> OperationRoundResult: - if self.idx > 1: - area_name = f'活跃度奖励-{self.idx}' - return self.round_by_click_area('快捷手册', area_name, success_wait=1, retry_wait=1) - else: - return self.round_fail(EngagementRewardApp.STATUS_NO_REWARD) + return self.round_by_find_and_click_area(self.last_screenshot, '快捷手册', '今日最大活跃度', success_wait=1, retry_wait=1) @node_from(from_name='点击奖励') @operation_node(name='查看奖励结果') def check_reward(self) -> OperationRoundResult: - return self.round_by_find_and_click_area(self.last_screenshot, '快捷手册', '活跃度奖励-确认', success_wait=1, retry_wait=1) + result = self.round_by_find_and_click_area(self.last_screenshot, '快捷手册', '活跃度奖励-确认', success_wait=1, retry_wait=1) + if result.is_success: + return self.round_success('日常奖励领取成功') + + result = self.round_by_find_area(self.last_screenshot, '快捷手册', '活跃度奖励-奖励预览') + if result.is_success: + result = self.round_by_find_and_click_area(self.last_screenshot, '画面-通用', '关闭', success_wait=1, retry_wait=1) + if result.is_success: + return self.round_success('日常奖励已领取或活跃度未满') + + return self.round_success('未找到确认按钮或奖励预览') - @node_from(from_name='查看奖励结果', success=False) @node_from(from_name='查看奖励结果') - @node_from(from_name='识别活跃度', status=STATUS_NO_REWARD) - @node_notify(when=NotifyTiming.PREVIOUS_DONE) + @node_notify(when=NotifyTiming.CURRENT_DONE, detail=True) + @operation_node(name='识别活跃度') + def check_engagement(self) -> OperationRoundResult: + result = self.round_by_find_area(self.last_screenshot, '快捷手册', '活跃度奖励-4') + return self.round_success('活跃度已满') if result.is_success else self.round_fail('活跃度未满') + + @node_from(from_name='识别活跃度') + @node_from(from_name='识别活跃度', success=False) @operation_node(name='完成后返回大世界') def back_afterwards(self) -> OperationRoundResult: op = BackToNormalWorld(self.ctx) - return self.round_by_op_result(op.execute()) + op.execute() + return self.round_success() if self.previous_node.is_success else self.round_fail() def __debug(): ctx = ZContext() - ctx.init_by_config() - ctx.init_ocr() + ctx.init() ctx.run_context.start_running() op = EngagementRewardApp(ctx) op.execute() diff --git a/src/zzz_od/application/game_config_checker/mouse_sensitivity_checker/mouse_sensitivity_checker.py b/src/zzz_od/application/game_config_checker/mouse_sensitivity_checker/mouse_sensitivity_checker.py index 7004966bde..ce14255247 100644 --- a/src/zzz_od/application/game_config_checker/mouse_sensitivity_checker/mouse_sensitivity_checker.py +++ b/src/zzz_od/application/game_config_checker/mouse_sensitivity_checker/mouse_sensitivity_checker.py @@ -25,11 +25,16 @@ def __init__(self, ctx: ZContext): op_name=mouse_sensitivity_checker_const.APP_NAME, ) - self.turn_distance: int = 500 # 转向时鼠标移动的距离 + self.turn_distance: int = 500 # 鼠标模式:转向时鼠标移动的距离 + self.gamepad_test_duration: float = 0.3 # 手柄模式:右摇杆推动时长(秒) self.angle_check_times: int = 0 self.last_angle: float = 0 self.angle_diff_list: list[float] = [] + @property + def _is_gamepad_mode(self) -> bool: + return self.ctx.controller.background_mode + @operation_node(name='返回大世界') def back_at_first(self) -> OperationRoundResult: op = BackToNormalWorld(self.ctx) @@ -44,6 +49,9 @@ def transport(self) -> OperationRoundResult: @node_from(from_name='传送') @operation_node(name='转向检测', is_start_node=False) def check(self) -> OperationRoundResult: + if self._is_gamepad_mode and self.ctx.game_config.turn_dx == 0: + return self.round_fail(status='手柄灵敏度检测需先完成鼠标灵敏度检测 (turn_dx)') + mini_map = self.ctx.world_patrol_service.cut_mini_map(self.last_screenshot) angle = mini_map.view_angle @@ -63,16 +71,45 @@ def check(self) -> OperationRoundResult: return self.round_success() self.last_angle = angle - self.ctx.controller.turn_by_distance(self.turn_distance) + + if self._is_gamepad_mode: + self._gamepad_turn_test() + else: + self.ctx.controller.turn_by_distance(self.turn_distance) + return self.round_wait(status='转向继续下一轮识别', wait=2) + def _gamepad_turn_test(self) -> None: + """直接推右摇杆固定时长,用于校准 gamepad_turn_speed。""" + pad = self.ctx.controller.btn_controller.pad + pad.right_joystick_float(1.0, 0) # 满偏转向右 + pad.update() + time.sleep(self.gamepad_test_duration) + pad.right_joystick_float(0, 0) + pad.update() + @node_from(from_name='转向检测') @operation_node(name='结果统计') def calculate(self) -> OperationRoundResult: - dx = self.turn_distance / float(np.mean(self.angle_diff_list)) - self.ctx.game_config.turn_dx = dx - self.ctx.controller.turn_dx = self.ctx.game_config.turn_dx - log.info(f'转向系数={dx:0.6f}') + mean_diff = float(np.mean(self.angle_diff_list)) + + if abs(mean_diff) < 1e-6: + return self.round_fail(status='平均角度差过小,检测结果不可靠') + + if self._is_gamepad_mode: + # gamepad_turn_speed = |turn_dx| * |mean_angle_diff| / test_duration + turn_dx = self.ctx.game_config.turn_dx + speed = abs(turn_dx * mean_diff) / self.gamepad_test_duration + self.ctx.game_config.gamepad_turn_speed = speed + self.ctx.controller.gamepad_turn_speed = speed + log.info(f'手柄转速 gamepad_turn_speed={speed:.2f} (turn_dx={turn_dx:.4f}, ' + f'平均角度差={mean_diff:.2f}°, 测试时长={self.gamepad_test_duration}s)') + else: + dx = self.turn_distance / mean_diff + self.ctx.game_config.turn_dx = dx + self.ctx.controller.turn_dx = dx + log.info(f'转向系数 turn_dx={dx:.6f}') + return self.round_success('完成检测') diff --git a/src/zzz_od/application/hollow_zero/lost_void/context/lost_void_context.py b/src/zzz_od/application/hollow_zero/lost_void/context/lost_void_context.py index b144aab625..d9f683541b 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/context/lost_void_context.py +++ b/src/zzz_od/application/hollow_zero/lost_void/context/lost_void_context.py @@ -1,6 +1,7 @@ import os import time from typing import Optional, List, Tuple +import re from cv2.typing import MatLike @@ -280,7 +281,9 @@ def match_artifact_by_ocr_full(self, name_full_str: str) -> Optional[LostVoidArt def check_artifact_priority_input(self, input_str: str) -> Tuple[List[str], str]: """ 校验优先级的文本输入 - 错误的输入会被过滤掉 + 当前采用“文本驱动”策略: + - 只做去空行清洗 + - 不再强依赖本地藏品清单进行合法性过滤 :param input_str: :return: 匹配的藏品和错误信息 """ @@ -288,39 +291,13 @@ def check_artifact_priority_input(self, input_str: str) -> Tuple[List[str], str] return [], '' input_arr = [i.strip() for i in input_str.split('\n')] - filter_result_list = [] - error_msg = '' + filter_result_list: list[str] = [] for i in input_arr: if len(i) == 0: continue - split_idx = i.find(' ') - if split_idx != -1: - cate_name = i[:split_idx] - item_name = i[split_idx+1:] - else: - cate_name = i - item_name = '' - - is_valid: bool = False - - if cate_name in self.cate_2_artifact: - - if item_name == '': # 整个分类 - is_valid = True - elif item_name in ['S', 'A', 'B']: # 按等级 - is_valid = True - else: - for art in self.cate_2_artifact[cate_name]: - if item_name == art.name: - is_valid = True - break - - if not is_valid: - error_msg += f'输入非法 {i}' - else: - filter_result_list.append(i) + filter_result_list.append(i) - return filter_result_list, error_msg + return filter_result_list, '' def check_region_type_priority_input(self, input_str: str) -> Tuple[List[str], str]: """ @@ -346,7 +323,10 @@ def check_region_type_priority_input(self, input_str: str) -> Tuple[List[str], s return filter_result_list, error_msg def get_artifact_pos( - self, screen: MatLike, to_choose_gear_branch: bool = False + self, + screen: MatLike, + to_choose_gear_branch: bool = False, + screen_name: str = '迷失之地-通用选择', ) -> list[LostVoidArtifactPos]: """ 识别画面中出现的藏品 @@ -354,12 +334,9 @@ def get_artifact_pos( - 邦布商店 :param screen: 游戏画面 :param to_choose_gear_branch: 是否识别战术棱镜 + :param screen_name: 当前界面名称,用于读取“区域-藏品名称” :return: """ - artifact_name_list: list[str] = [] - for art in self.ctx.lost_void.all_artifact_list: - artifact_name_list.append(gt(art.display_name, 'game')) - # 识别其它标识 title_word_list = [ gt('有同流派武备', 'game'), @@ -368,22 +345,7 @@ def get_artifact_pos( gt('NEW!', 'game') ] - # 其它标识也要一起匹配 防止部分鸣徽名称和这些很相似 - artifact_name_list.extend(title_word_list) - - artifact_pos_list: list[LostVoidArtifactPos] = [] - ocr_result_map = self.ctx.ocr.run_ocr(screen) - for ocr_result, mrl in ocr_result_map.items(): - title_idx: int = str_utils.find_best_match_by_difflib(ocr_result, artifact_name_list) - if title_idx is None or title_idx < 0: - continue - - if title_idx >= len(self.ctx.lost_void.all_artifact_list): - continue - - artifact = self.ctx.lost_void.all_artifact_list[title_idx] - artifact_pos = LostVoidArtifactPos(artifact, mrl.max.rect) - artifact_pos_list.append(artifact_pos) + artifact_pos_list = self._build_artifact_candidates_from_name_ocr(screen, screen_name) # 识别武备分支 if to_choose_gear_branch: @@ -413,11 +375,17 @@ def get_artifact_pos( if closest_artifact_pos is not None: original_artifact = closest_artifact_pos.artifact - branch_artifact_name: str = f'{original_artifact.display_name}-{branch}' - branch_artifact = self.ctx.lost_void.get_artifact_by_full_name(branch_artifact_name) - if branch_artifact is not None: - closest_artifact_pos.artifact = branch_artifact - + # 分支标识按OCR候选直接派生,不依赖本地藏品库 + closest_artifact_pos.artifact = LostVoidArtifact( + category=original_artifact.category, + name=f'{original_artifact.name}-{branch}', + level=original_artifact.level, + is_gear=original_artifact.is_gear, + template_id=original_artifact.template_id, + ) + + # 标题标识(已选择/NEW/齿轮硬币不足)仍按全图OCR做空间关联 + ocr_result_map = self.ctx.ocr.run_ocr(screen) for ocr_result, mrl in ocr_result_map.items(): title_idx: int = str_utils.find_best_match_by_difflib(ocr_result, title_word_list) if title_idx is None or title_idx < 0: @@ -438,7 +406,12 @@ def get_artifact_pos( closest_artifact_pos = artifact_pos if closest_artifact_pos is not None: - if title_idx == 1: # 已选择 + if title_idx == 0: # 有同流派武备 + closest_artifact_pos.has_same_style = True + # “有同流派武备”在该场景可视作已选状态,避免重复点击同一项。 + closest_artifact_pos.chosen = True + closest_artifact_pos.can_choose = False + elif title_idx == 1: # 已选择 closest_artifact_pos.chosen = True closest_artifact_pos.can_choose = False elif title_idx == 2: # 齿轮硬币不足 @@ -449,10 +422,212 @@ def get_artifact_pos( # artifact_pos_list = [i for i in artifact_pos_list if i.can_choose] # 这行导致了chosen_list只会是空的 display_text = ', '.join([i.artifact.display_name for i in artifact_pos_list]) if len(artifact_pos_list) > 0 else '无' - log.info(f'当前识别藏品 {display_text}') + primary_cnt = len([i for i in artifact_pos_list if i.is_primary_name]) + secondary_cnt = len(artifact_pos_list) - primary_cnt + log.info(f'当前识别藏品 主选={primary_cnt} 次选={secondary_cnt} {display_text}') return artifact_pos_list + def _build_artifact_candidates_from_name_ocr( + self, + screen: MatLike, + screen_name: str, + ) -> list[LostVoidArtifactPos]: + """ + 从“区域-藏品名称”OCR结果构建候选: + 1. []/【】结构 => 主选 + 2. 其他文本 => 次选 + 并按X坐标聚合为每卡一个候选,保留坐标用于点击。 + """ + try: + area = self.ctx.screen_loader.get_area(screen_name, '区域-藏品名称') + except Exception as e: + log.warning(f'获取区域失败 screen={screen_name} area=区域-藏品名称 err={e}') + return [] + + ocr_result_map = self.ctx.ocr_service.get_ocr_result_map( + image=screen, + rect=area.rect, + crop_first=True, + ) + + raw_candidates: list[LostVoidArtifactPos] = [] + for ocr_text, mrl in ocr_result_map.items(): + text = ocr_text.strip() + if len(text) == 0: + continue + + artifact, is_primary_name = self._create_artifact_from_ocr_text(text) + if artifact is None: + continue + + for mr in mrl: + raw_candidates.append( + LostVoidArtifactPos( + art=artifact, + rect=mr.rect, + ocr_text=text, + is_primary_name=is_primary_name, + ) + ) + + if len(raw_candidates) == 0: + return [] + + raw_candidates.sort(key=lambda i: (i.rect.center.x, i.rect.center.y)) + + # 按x坐标聚合同一卡片,优先保留主选文本 + merged_candidates: list[LostVoidArtifactPos] = [] + for candidate in raw_candidates: + merged = False + for idx, existed in enumerate(merged_candidates): + if abs(existed.rect.center.x - candidate.rect.center.x) < 90: + merged_candidates[idx] = self._pick_better_candidate(existed, candidate) + merged = True + break + if not merged: + merged_candidates.append(candidate) + + merged_candidates.sort(key=lambda i: (i.rect.center.x, i.rect.center.y)) + return merged_candidates + + def _create_artifact_from_ocr_text(self, ocr_text: str) -> Tuple[Optional[LostVoidArtifact], bool]: + """ + 从OCR文本提取候选藏品信息 + :return: (artifact, is_primary_name) + """ + if ocr_text is None: + return None, False + + text = ocr_text.strip() + if len(text) < 2: + return None, False + + normalized = text.replace('【', '[').replace('】', ']') + match = re.match(r'^\[(.+?)\](.+)$', normalized) + if match is not None: + raw_category = match.group(1).strip() + raw_name = match.group(2).strip() + if len(raw_name) == 0: + return None, False + + # 例如 “击破: 叩击” -> “击破”,便于和配置里的分类文本做匹配 + category = raw_category.split(':', 1)[0].split(':', 1)[0].strip() + if len(category) == 0: + category = raw_category + + return LostVoidArtifact(category=category, name=raw_name, level='?'), True + + # 卡牌界面常见主标题样式:`「xxx」yyy` + # 该结构应视为主选名称,而不是无详情说明文本。 + quote_match = re.match(r'^「(.+?)」\s*(.+)$', text) + if quote_match is not None: + title = quote_match.group(1).strip() + suffix = quote_match.group(2).strip() + if len(title) > 0: + name = f'{title} {suffix}'.strip() if len(suffix) > 0 else title + return LostVoidArtifact(category='卡牌', name=name, level='?'), True + + # 没有[]结构,归为次选,直接保留原文。 + return LostVoidArtifact(category='无详情', name=text, level='?'), False + + @staticmethod + def _pick_better_candidate(left: LostVoidArtifactPos, right: LostVoidArtifactPos) -> LostVoidArtifactPos: + """ + 同x聚合时的候选优先级: + 1. 主选([])优先 + 2. 已知等级(S/A/B)优先 + 3. OCR文本更长优先 + 4. y更小(更靠上)优先 + """ + left_known = left.artifact.level in ['S', 'A', 'B'] + right_known = right.artifact.level in ['S', 'A', 'B'] + + left_score = ( + 1 if left.is_primary_name else 0, + 1 if left_known else 0, + len(left.ocr_text), + -left.rect.center.y, + ) + right_score = ( + 1 if right.is_primary_name else 0, + 1 if right_known else 0, + len(right.ocr_text), + -right.rect.center.y, + ) + return right if right_score > left_score else left + + @staticmethod + def _normalize_category_text(category: str) -> str: + if category is None: + return '' + text = category.strip() + for ch in [' ', ' ', '·', ':', ':', '[', ']', '【', '】']: + text = text.replace(ch, '') + + # 常见别名归一 + if text == '击破': + return '异常击破' + return text + + @classmethod + def _is_category_match(cls, artifact_category: str, priority_category: str) -> bool: + if artifact_category == priority_category: + return True + + normalized_artifact = cls._normalize_category_text(artifact_category) + normalized_priority = cls._normalize_category_text(priority_category) + if len(normalized_artifact) == 0 or len(normalized_priority) == 0: + return False + + if normalized_artifact == normalized_priority: + return True + + # 允许“异常·击破”与“击破”这类前后缀兼容 + return normalized_artifact in normalized_priority or normalized_priority in normalized_artifact + + def _is_priority_rule_match(self, artifact_pos: LostVoidArtifactPos, priority_rule: str) -> bool: + """ + 判断某个候选是否命中优先级规则 + 支持: + 1. 分类:`通用` + 2. 分类 + 名称:`通用 喷水枪` + 3. 分类 + 等级:`通用 A` + 4. 纯文本(用于次选):`啦啦啦` + """ + if priority_rule is None: + return False + + rule = priority_rule.strip() + if len(rule) == 0: + return False + + artifact = artifact_pos.artifact + split_idx = rule.find(' ') + if split_idx == -1: + # 单词条:优先按分类匹配,次选文本可按名称/原文匹配 + if self._is_category_match(artifact.category, rule): + return True + if artifact.name == rule: + return True + return artifact_pos.ocr_text == rule + + cate_name = rule[:split_idx].strip() + item_name = rule[split_idx + 1:].strip() + + if not self._is_category_match(artifact.category, cate_name): + return False + + if len(item_name) == 0: + return True + + if item_name in ['S', 'A', 'B']: + return artifact.level == item_name + + if artifact.name == item_name or artifact_pos.ocr_text.endswith(item_name): + return True + return str_utils.find_by_lcs(item_name, artifact.name, percent=0.6) or str_utils.find_by_lcs(item_name, artifact_pos.ocr_text, percent=0.6) + def get_artifact_by_priority( self, artifact_list: List[LostVoidArtifactPos], choose_num: int, consider_priority_1: bool = True, consider_priority_2: bool = True, @@ -471,99 +646,100 @@ def get_artifact_by_priority( :param consider_priority_new: 是否优先选择NEW类型 最高优先级 :return: 按优先级选择的结果 """ + def fmt_artifact(pos: LostVoidArtifactPos, idx: int | None = None) -> str: + prefix = f'#{idx} ' if idx is not None else '' + return ( + f'{prefix}{pos.artifact.display_name}' + f' [分类={pos.artifact.category} 等级={pos.artifact.level} 主选={pos.is_primary_name} NEW={pos.is_new}]' + f' [坐标=({pos.rect.center.x},{pos.rect.center.y})]' + ) + + raw_artifact_list = list(artifact_list) + raw_text = '; '.join([fmt_artifact(pos, idx) for idx, pos in enumerate(raw_artifact_list)]) if len(raw_artifact_list) > 0 else '无' + log.debug(f'优先级输入候选(去重前) 共{len(raw_artifact_list)}个: {raw_text}') + artifact_list = self.remove_overlapping_artifacts(artifact_list) + artifact_list = sorted(artifact_list, key=lambda i: (i.rect.center.x, i.rect.center.y)) + + log.debug(f'当前考虑优先级 数量={choose_num} NEW!={consider_priority_new} 第一优先级={consider_priority_1} 第二优先级={consider_priority_2} 其他={consider_not_in_priority}') + dedup_text = '; '.join([fmt_artifact(pos, idx) for idx, pos in enumerate(artifact_list)]) if len(artifact_list) > 0 else '无' + log.debug(f'优先级输入候选(去重后) 共{len(artifact_list)}个: {dedup_text}') - log.info(f'当前考虑优先级 数量={choose_num} NEW!={consider_priority_new} 第一优先级={consider_priority_1} 第二优先级={consider_priority_2} 其他={consider_not_in_priority}') - # 合并动态优先级和静态优先级 priority_list_to_consider = [] - + final_priority_list_1 = self.dynamic_priority_list.copy() if consider_priority_1 and self.challenge_config.artifact_priority: final_priority_list_1.extend(self.challenge_config.artifact_priority) priority_list_to_consider.append(final_priority_list_1) - + if consider_priority_2 and self.challenge_config.artifact_priority_2: priority_list_to_consider.append(self.challenge_config.artifact_priority_2) if len(priority_list_to_consider) == 0: # 两个优先级都是空的时候 强制考虑非优先级的 consider_not_in_priority = True - priority_idx_list: List[int] = [] # 优先级排序的下标 + p1_text = ', '.join(final_priority_list_1) if len(final_priority_list_1) > 0 else '空' + p2_text = ', '.join(self.challenge_config.artifact_priority_2) if consider_priority_2 and len(self.challenge_config.artifact_priority_2) > 0 else '空' + log.debug(f'优先级规则 第一优先级={p1_text}') + log.debug(f'优先级规则 第二优先级={p2_text}') - # 优先选择NEW类型 最高优先级 - if consider_priority_new: - for level in ['S', 'A', 'B']: - for idx in range(len(artifact_list)): - if ignore_idx_list is not None and idx in ignore_idx_list: # 需要忽略的下标 - continue - - if idx in priority_idx_list: # 已经加入过了 - continue - - pos = artifact_list[idx] - if pos.artifact.level != level: - continue - - if not pos.is_new: - continue - - priority_idx_list.append(idx) - - # 按优先级顺序 将匹配的藏品下标加入 - # 同时 优先考虑等级高的 - for target_level in ['S', 'A', 'B']: - for priority_list in priority_list_to_consider: - for priority in priority_list: - split_idx = priority.find(' ') - if split_idx != -1: - cate_name = priority[:split_idx] - item_name = priority[split_idx+1:] - else: - cate_name = priority - item_name = '' - - for idx in range(len(artifact_list)): - if ignore_idx_list is not None and idx in ignore_idx_list: # 需要忽略的下标 + priority_idx_list: List[int] = [] # 优先级排序的下标 + choose_reason_map: dict[int, str] = {} + ignored_idx_set = set(ignore_idx_list) if ignore_idx_list is not None else set() + all_idx_list = [i for i in range(len(artifact_list)) if i not in ignored_idx_set] + primary_idx_list = [i for i in all_idx_list if artifact_list[i].is_primary_name] + secondary_idx_list = [i for i in all_idx_list if not artifact_list[i].is_primary_name] + ignored_text = ', '.join([str(i) for i in sorted(list(ignored_idx_set))]) if len(ignored_idx_set) > 0 else '无' + log.debug(f'优先级分组 忽略下标={ignored_text} 主选下标={primary_idx_list} 次选下标={secondary_idx_list}') + + def add_idx_if_absent(target_idx: int, reason: str) -> None: + if target_idx in priority_idx_list: + return + priority_idx_list.append(target_idx) + choose_reason_map[target_idx] = reason + log.debug(f'候选入队 {fmt_artifact(artifact_list[target_idx], target_idx)} 原因={reason}') + + # 规则:先主选,再次选 + for group_name, group_idx_list in [('主选', primary_idx_list), ('次选', secondary_idx_list)]: + # 1) 主次组内先考虑NEW + if consider_priority_new: + for level in ['S', 'A', 'B', '?']: + for idx in group_idx_list: + if idx in priority_idx_list: continue - - artifact: LostVoidArtifact = artifact_list[idx].artifact - - if artifact.level != target_level: + pos = artifact_list[idx] + if not pos.is_new: continue - - if idx in priority_idx_list: # 已经加入过了 + if level != '?' and pos.artifact.level != level: continue - - if artifact.category != cate_name: # 不符合分类 + if level == '?' and pos.artifact.level in ['S', 'A', 'B']: continue - - if item_name == '': - priority_idx_list.append(idx) + add_idx_if_absent(idx, f'{group_name}-NEW优先 命中等级={level}') + + # 2) 按优先级文本匹配(坐标顺序作为同优先级稳定序) + for list_idx, priority_list in enumerate(priority_list_to_consider): + list_name = '第一优先级' if list_idx == 0 else f'第二优先级{list_idx}' + for priority_rule in priority_list: + matched_idx_list: list[int] = [] + for idx in group_idx_list: + if idx in priority_idx_list: continue + if self._is_priority_rule_match(artifact_list[idx], priority_rule): + matched_idx_list.append(idx) + add_idx_if_absent(idx, f'{group_name}-{list_name} 命中规则="{priority_rule}"') + if len(matched_idx_list) > 0: + hit_text = ', '.join([fmt_artifact(artifact_list[idx], idx) for idx in matched_idx_list]) + log.debug(f'规则命中 {group_name}-{list_name} 规则="{priority_rule}" 命中={hit_text}') + else: + log.debug(f'规则未命中 {group_name}-{list_name} 规则="{priority_rule}"') - if item_name in ['S', 'A', 'B']: - if artifact.level == item_name: - priority_idx_list.append(idx) - continue - - if item_name == artifact.name: - priority_idx_list.append(idx) - - # 将剩余的 按等级加入 - if consider_not_in_priority: - for level in ['S', 'A', 'B']: - for idx in range(len(artifact_list)): - if ignore_idx_list is not None and idx in ignore_idx_list: # 需要忽略的下标 - continue - - if idx in priority_idx_list: # 已经加入过了 + # 3) 其余候选按坐标顺序补齐 + if consider_not_in_priority: + for idx in group_idx_list: + if idx in priority_idx_list: continue - - artifact: LostVoidArtifact = artifact_list[idx].artifact - - if artifact.level == level: - priority_idx_list.append(idx) + add_idx_if_absent(idx, f'{group_name}-非优先级补位') result_list: List[LostVoidArtifactPos] = [] for i in range(choose_num): @@ -572,7 +748,16 @@ def get_artifact_by_priority( result_list.append(artifact_list[priority_idx_list[i]]) display_text = ','.join([i.artifact.display_name for i in result_list]) if len(result_list) > 0 else '无' - log.info(f'当前符合优先级列表 {display_text}') + selected_detail = [] + for i, pos in enumerate(result_list): + idx = priority_idx_list[i] + reason = choose_reason_map.get(idx, '未知原因') + selected_detail.append(f'{fmt_artifact(pos, idx)} 原因={reason}') + selected_text = '; '.join(selected_detail) if len(selected_detail) > 0 else '无' + queue_text = ', '.join([str(i) for i in priority_idx_list]) if len(priority_idx_list) > 0 else '空' + log.debug(f'优先级入队顺序 下标={queue_text}') + log.debug(f'当前符合优先级列表 {display_text}') + log.debug(f'最终选择明细 {selected_text}') return result_list @@ -601,7 +786,7 @@ def remove_overlapping_artifacts(self, artifact_list: List[LostVoidArtifactPos]) next_art = sorted_artifacts[j] x_distance = abs(current_art.rect.center.x - next_art.rect.center.x) - if x_distance < 200: # 横坐标太近 + if x_distance < 100: # 横坐标太近 overlapping_arts.append(next_art) log.debug(f'发现重叠藏品: {current_art.artifact.display_name} 和 {next_art.artifact.display_name}, 距离: {x_distance}') j += 1 diff --git a/src/zzz_od/application/hollow_zero/lost_void/lost_void_app.py b/src/zzz_od/application/hollow_zero/lost_void/lost_void_app.py index 5d8522e7a3..b1a94ce3a8 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/lost_void_app.py +++ b/src/zzz_od/application/hollow_zero/lost_void/lost_void_app.py @@ -4,12 +4,12 @@ import cv2 from one_dragon.base.geometry.point import Point -from one_dragon.base.matcher.match_result import MatchResult +from one_dragon.base.matcher.match_result import MatchResult, MatchResultList from one_dragon.base.operation.application import application_const from one_dragon.base.operation.operation import Operation from one_dragon.base.operation.operation_edge import node_from from one_dragon.base.operation.operation_node import operation_node -from one_dragon.base.operation.operation_notify import node_notify, NotifyTiming +from one_dragon.base.operation.operation_notify import NotifyTiming, node_notify from one_dragon.base.operation.operation_round_result import OperationRoundResult from one_dragon.utils import cv2_utils, str_utils from one_dragon.utils.i18_utils import gt @@ -29,8 +29,8 @@ from zzz_od.context.zzz_context import ZContext from zzz_od.game_data.agent import Agent, AgentEnum from zzz_od.operation.back_to_normal_world import BackToNormalWorld -from zzz_od.operation.compendium.tp_by_compendium import TransportByCompendium from zzz_od.operation.choose_predefined_team import ChoosePredefinedTeam +from zzz_od.operation.compendium.tp_by_compendium import TransportByCompendium from zzz_od.operation.deploy import Deploy @@ -38,6 +38,7 @@ class LostVoidApp(ZApplication): STATUS_ENOUGH_TIMES: ClassVar[str] = '完成通关次数' STATUS_AGAIN: ClassVar[str] = '继续挑战' + STATUS_AGAIN_MATRIX: ClassVar[str] = '继续挑战-矩阵行动' def __init__(self, ctx: ZContext): ZApplication.__init__( @@ -60,9 +61,12 @@ def __init__(self, ctx: ZContext): self.priority_agent_list: list[Agent] = [] # 优先选择的代理人列表 self.use_priority_agent: bool = False # 本次挑战是否使用了UP代理人 + self._entry_nav_click_cooldown_sec: float = 1.0 + self._entry_nav_last_click_at: float = 0.0 @operation_node(name='初始化加载', is_start_node=True) def init_for_lost_void(self) -> OperationRoundResult: + self._reset_entry_nav_click_cooldown() # 检查分配给今天的任务是否完成 if self.run_record.is_finished_by_day: return self.round_success(LostVoidApp.STATUS_ENOUGH_TIMES) @@ -122,6 +126,13 @@ def wait_lost_void_entry(self) -> OperationRoundResult: if result.is_success: return self.round_retry(result.status, wait=0.5) + # 新入口UI:战线肃清/特遣调查需要先在“矩阵探索”页点击“常规”再点目标副本 + # 到达入口的判定只认“常规”,后续分流由OCR导航节点按副本目标处理 + if self.config.mission_name in ['战线肃清', '特遣调查']: + ocr_result_map = self.ctx.ocr.run_ocr(self.last_screenshot) + if self._find_ocr_text_mr(ocr_result_map, '常规') is not None: + return self.round_success(status='迷失之地-入口') + screen_name = self.check_and_update_current_screen(self.last_screenshot, screen_name_list=['迷失之地-入口']) if screen_name != '迷失之地-入口': return self.round_wait(status='等待画面加载', wait=1) @@ -143,6 +154,8 @@ def check_bounty_commission_before(self) -> OperationRoundResult: if not self.config.is_bounty_commission_mode: if self.run_record.is_finished_by_day: return self.round_success(LostVoidApp.STATUS_ENOUGH_TIMES) + if self.config.mission_name == '矩阵行动': + return self.round_success(LostVoidApp.STATUS_AGAIN_MATRIX) return self.round_success(LostVoidApp.STATUS_AGAIN) TARGET_SCORE = '8000' # 目标分数文本 @@ -160,6 +173,8 @@ def check_bounty_commission_before(self) -> OperationRoundResult: # 根据次数判断完成状态 if target_count == 1: # 只有一个 8000,表示 xxxx/8000 (未完成) + if self.config.mission_name == '矩阵行动': + return self.round_success(LostVoidApp.STATUS_AGAIN_MATRIX) return self.round_success(LostVoidApp.STATUS_AGAIN) elif target_count == 2: # 两个 8000,表示 8000/8000 (已完成) @@ -170,13 +185,229 @@ def check_bounty_commission_before(self) -> OperationRoundResult: # 未识别到预期的结果(0次或3次以上),返回重试 return self.round_retry(wait=0.5) + # ========== 矩阵行动入口流程节点 ========== + + @node_from(from_name='识别悬赏委托完成进度', status=STATUS_AGAIN_MATRIX) + @operation_node(name='矩阵行动-前往入口') + def matrix_goto_entry(self) -> OperationRoundResult: + return self.round_by_goto_screen(screen_name='迷失之地-入口') + + @node_from(from_name='矩阵行动-前往入口') + @operation_node(name='矩阵行动-前往挑战') + def matrix_goto_challenge(self) -> OperationRoundResult: + return self.round_by_find_and_click_area( + self.last_screenshot, + '迷失之地-入口', + '按钮-前往挑战', + success_wait=1, + ) + + @node_from(from_name='矩阵行动-前往挑战') + @operation_node(name='矩阵行动-点击下一步') + def matrix_click_next_step(self) -> OperationRoundResult: + return self.round_by_find_and_click_area( + self.last_screenshot, + '迷失之地-入口', + '按钮-下一步', + success_wait=1, + ) + + @node_from(from_name='矩阵行动-点击下一步') + @operation_node(name='矩阵行动-点击预备编队') + def matrix_click_preset_team(self) -> OperationRoundResult: + area = self.ctx.screen_loader.get_area('迷失之地-矩阵行动', '预备编队') + if area is not None: + part = cv2_utils.crop_image_only(self.last_screenshot, area.rect) + if cv2_utils.is_colorful(part): + # 按钮已变成彩色,说明加载完成 + return self.round_success(status='预备编队已加载', wait=1) + + # 按钮还是灰度,需要点击 + result = self.round_by_find_and_click_area( + self.last_screenshot, + '迷失之地-矩阵行动', + '预备编队', + success_wait=1, + ) + if result.is_success: + return self.round_wait(status='预备编队加载中', wait=0.5) + + return self.round_retry(status='点击预备编队失败', wait=0.5) + + @node_from(from_name='矩阵行动-点击预备编队') + @operation_node(name='矩阵行动-选择配队', node_max_retry_times=7) + def matrix_select_team(self) -> OperationRoundResult: + # 初始为较高的匹配阈值,如果超过5次匹配失败则改用0.5的阈值兜底 + lcs_percent = 0.7 if self.node_retry_times < 5 else 0.5 + + area = self.ctx.screen_loader.get_area('迷失之地-矩阵行动', '编队列表') + main_team_area = self.ctx.screen_loader.get_area('迷失之地-矩阵行动', '主战编队槽') + + # 获取目标编队名称 + predefined_idx = self.ctx.lost_void.challenge_config.predefined_team_idx + if predefined_idx == -1: + predefined_idx = 0 + self.ctx.lost_void.predefined_team_idx = predefined_idx + team_name = self.ctx.team_config.team_list[predefined_idx].name + + # 查找并点击目标配队 + team_match_result = self.round_by_ocr_and_click( + screen=self.last_screenshot, + target_cn=team_name, + lcs_percent=lcs_percent, + remove_whitespace=True, # 去除空白字符提高匹配兼容性 + ) + + if team_match_result.is_success: + # 等待画面更新 + time.sleep(0.5) + self.screenshot() + # 在主战编队槽区域检测是否出现"主战" + ocr_result_list = self.ctx.ocr_service.get_ocr_result_list( + image=self.last_screenshot, + rect=main_team_area.rect, + ) + success_msg = '已选择配队' + if self.node_retry_times >= 5: + success_msg += '(随机)' + for ocr_text in ocr_result_list: + if '主战' in ocr_text.data: + return self.round_success(success_msg, wait=1) + return self.round_retry('未找到主战', wait=0.5) + + self.scroll_area(screen_name='迷失之地-矩阵行动', area_name='编队列表', direction='down') + return self.round_retry(f'未找到{team_name}, 尝试向下滚动', wait=0.3) + + @node_from(from_name='矩阵行动-选择配队') + @operation_node(name='矩阵行动-点击协助代理人') + def matrix_click_support_agent(self) -> OperationRoundResult: + return self.round_by_find_and_click_area( + self.last_screenshot, + '迷失之地-矩阵行动', + '协战代理人', + success_wait=1, + ) + + @node_from(from_name='矩阵行动-点击协助代理人') + @operation_node(name='矩阵行动-等待代理人列表', node_max_retry_times=300) + def matrix_wait_support_panel(self) -> OperationRoundResult: + ocr_result_map = self.ctx.ocr.run_ocr(self.last_screenshot) + if self._find_ocr_text_mr(ocr_result_map, '代理人') is not None: + return self.round_success('已出现代理人列表') + return self.round_retry('等待代理人列表', wait=0.1) + + @node_from(from_name='矩阵行动-等待代理人列表') + @operation_node(name='矩阵行动-选择协助代理人') + def matrix_select_support_agent(self) -> OperationRoundResult: + area = self.ctx.screen_loader.get_area('迷失之地-矩阵行动', '代理人列表') + support_team_area = self.ctx.screen_loader.get_area('迷失之地-矩阵行动', '协战编队槽') + ocr_result_list = self.ctx.ocr_service.get_ocr_result_list( + image=self.last_screenshot, + rect=area.rect, + ) + + # 先点击UP代理人 + clicked = False + for ocr_text in ocr_result_list: + if 'up' in ocr_text.data.lower() and ocr_text.center.x < self.ctx.controller.standard_width // 2: + self.ctx.controller.click(ocr_text.center) + clicked = True + break + + # 找不到UP,点击第一个 + if not clicked: + if len(ocr_result_list) > 0: + self.ctx.controller.click(ocr_result_list[0].center) + else: + return self.round_retry('未找到代理人', wait=0.1) + + # 等待画面更新,重新截图OCR + time.sleep(0.5) + self.screenshot() + # 在协战编队槽区域检测是否出现"协战" + ocr_result_list = self.ctx.ocr_service.get_ocr_result_list( + image=self.last_screenshot, + rect=support_team_area.rect, + ) + + for ocr_text in ocr_result_list: + if '协战' in ocr_text.data: + return self.round_success('已选择协助代理人') + + return self.round_retry('未找到协战', wait=0.5) + + @node_from(from_name='矩阵行动-选择协助代理人') + @operation_node(name='矩阵行动-开始挑战') + def matrix_start_challenge(self) -> OperationRoundResult: + return self.round_by_find_and_click_area( + self.last_screenshot, + '迷失之地-矩阵行动', + '按钮-开始挑战', + success_wait=1, + ) + + # ========== 常规副本入口流程节点 ========== + @node_from(from_name='识别悬赏委托完成进度', status=STATUS_AGAIN) @operation_node(name='前往副本画面', node_max_retry_times=60) def goto_mission_screen(self) -> OperationRoundResult: mission_name = self.config.mission_name + if mission_name in ['战线肃清', '特遣调查']: + return self.round_success('需OCR入口导航') return self.round_by_goto_screen(screen_name=f'迷失之地-{mission_name}') + @node_from(from_name='前往副本画面', status='需OCR入口导航') + @operation_node(name='入口OCR-点击常规', node_max_retry_times=300) + def click_regular_in_matrix_explore(self) -> OperationRoundResult: + mission_name = self.config.mission_name + ocr_result_map = self.ctx.ocr.run_ocr(self.last_screenshot) + + # 条件通过:检测到下一步按钮文字(战线肃清/特遣调查) + if self._find_ocr_text_mr(ocr_result_map, mission_name) is not None: + self._reset_entry_nav_click_cooldown() + return self.round_success('已显示目标副本入口') + + regular_mr = self._find_ocr_text_mr(ocr_result_map, '常规') + if regular_mr is None: + return self.round_retry('未识别到常规', wait=0.1) + + if self._is_entry_nav_click_on_cooldown(): + return self.round_retry('点击常规冷却', wait=0.1) + + if self.ctx.controller.click(regular_mr.center): + self._record_entry_nav_click() + return self.round_wait('点击常规', wait=0.3) + return self.round_retry('点击常规失败', wait=0.1) + + @node_from(from_name='入口OCR-点击常规', status='已显示目标副本入口') + @operation_node(name='入口OCR-点击目标副本', node_max_retry_times=300) + def click_target_mission_in_matrix_explore(self) -> OperationRoundResult: + mission_name = self.config.mission_name + target_screen_name = f'迷失之地-{mission_name}' + + screen_name = self.check_and_update_current_screen( + self.last_screenshot, + screen_name_list=[target_screen_name], + ) + if screen_name == target_screen_name: + self._reset_entry_nav_click_cooldown() + return self.round_success('已进入目标副本') + + ocr_result_map = self.ctx.ocr.run_ocr(self.last_screenshot) + mission_mr = self._find_ocr_text_mr(ocr_result_map, mission_name) + if mission_mr is None: + return self.round_retry('未识别到目标副本入口', wait=0.1) + + if self._is_entry_nav_click_on_cooldown(): + return self.round_retry('点击目标副本冷却', wait=0.1) + + if self.ctx.controller.click(mission_mr.center): + self._record_entry_nav_click() + return self.round_wait('点击目标副本', wait=0.5) + return self.round_retry('点击目标副本失败', wait=0.1) + @node_from(from_name='前往副本画面') + @node_from(from_name='入口OCR-点击目标副本', status='已进入目标副本') @operation_node(name='副本画面识别') def check_for_mission(self) -> OperationRoundResult: """ @@ -346,7 +577,7 @@ def _choose_strategy_by_chase_new_mode(self) -> OperationRoundResult: return self.round_fail("追新模式失败:未找到任何可选择的调查战略") - def _swipe_strategy_list(self): + def _swipe_strategy_list(self) -> None: """ 滑动调查战略列表 """ @@ -478,6 +709,7 @@ def deploy(self) -> OperationRoundResult: @node_from(from_name='识别初始画面', status='迷失之地-大世界') @node_from(from_name='出战') + @node_from(from_name='矩阵行动-开始挑战') @operation_node(name='加载自动战斗配置') def load_auto_op(self) -> OperationRoundResult: self.ctx.auto_battle_context.init_auto_op( @@ -500,6 +732,8 @@ def run_level(self) -> OperationRoundResult: self.next_region_type = LostVoidRegionType.from_value(op_result.data) else: self.next_region_type = LostVoidRegionType.ENTRY + elif op_result.status == LostVoidRunLevel.STATUS_COMPLETE: + self.next_region_type = LostVoidRegionType.ENTRY return self.round_by_op_result(op_result) @@ -509,12 +743,38 @@ def after_complete(self) -> OperationRoundResult: screen_name = self.check_and_update_current_screen(self.last_screenshot, screen_name_list=['迷失之地-入口']) if screen_name != '迷失之地-入口': return self.round_wait('等待画面加载', wait=1) + self.run_record.add_complete_times() if self.use_priority_agent: self.run_record.complete_task_force_with_up = True return self.round_success() + def _find_ocr_text_mr(self, ocr_result_map: dict[str, MatchResultList], target_text: str) -> MatchResult | None: + target = gt(target_text, 'game') + ocr_word_list = list(ocr_result_map.keys()) + + idx = str_utils.find_best_match_by_difflib(target, ocr_word_list, cutoff=0.5) + if idx is not None and idx >= 0: + mrl = ocr_result_map[ocr_word_list[idx]] + if mrl.max is not None: + return mrl.max + + for ocr_word, mrl in ocr_result_map.items(): + if str_utils.find_by_lcs(target, ocr_word, percent=0.6) and mrl.max is not None: + return mrl.max + + return None + + def _reset_entry_nav_click_cooldown(self) -> None: + self._entry_nav_last_click_at = 0.0 + + def _is_entry_nav_click_on_cooldown(self) -> bool: + return time.monotonic() - self._entry_nav_last_click_at < self._entry_nav_click_cooldown_sec + + def _record_entry_nav_click(self) -> None: + self._entry_nav_last_click_at = time.monotonic() + @node_from(from_name='识别悬赏委托完成进度', status=STATUS_ENOUGH_TIMES) @operation_node(name='打开悬赏委托') def open_reward_list(self) -> OperationRoundResult: diff --git a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_artifact_pos.py b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_artifact_pos.py index b995c19484..8890971426 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_artifact_pos.py +++ b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_artifact_pos.py @@ -6,12 +6,21 @@ class LostVoidArtifactPos: - def __init__(self, art: LostVoidArtifact, rect: Rect): + def __init__( + self, + art: LostVoidArtifact, + rect: Rect, + ocr_text: str = '', + is_primary_name: bool = True, + ): self.artifact: LostVoidArtifact = art self.rect: Rect = rect + self.ocr_text: str = ocr_text # OCR原始文本 + self.is_primary_name: bool = is_primary_name # 是否为 [] / 【】 结构的主选名称 self.can_choose: bool = True self.chosen: bool = False # 已经被选择了 + self.has_same_style: bool = False # 有同流派武备 self.store_price: Optional[int] = None self.store_buy_rect: Optional[Rect] = None self.is_new: bool = False @@ -38,4 +47,4 @@ def add_buy(self, rect: Rect) -> bool: return False self.store_buy_rect = rect - return True \ No newline at end of file + return True diff --git a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_bangboo_store.py b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_bangboo_store.py index 17d474c448..124aee6d68 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_bangboo_store.py +++ b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_bangboo_store.py @@ -128,7 +128,10 @@ def get_artifact_pos(self, screen: MatLike) -> List[LostVoidArtifactPos]: @param screen: 游戏画面 @return: 识别到的藏品 """ - artifact_pos_list: list[LostVoidArtifactPos] = self.ctx.lost_void.get_artifact_pos(screen) + artifact_pos_list: list[LostVoidArtifactPos] = self.ctx.lost_void.get_artifact_pos( + screen, + screen_name='迷失之地-邦布商店' + ) # 识别价格 area = self.ctx.screen_loader.get_area('迷失之地-邦布商店', '区域-价格') diff --git a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_common.py b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_common.py index c70f80e5a4..0754811ddc 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_common.py +++ b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_common.py @@ -1,12 +1,14 @@ import time +import re from cv2.typing import MatLike -from typing import List, Optional, Tuple +from typing import Optional from one_dragon.base.geometry.point import Point from one_dragon.base.matcher.match_result import MatchResult from one_dragon.base.operation.operation_node import operation_node from one_dragon.base.operation.operation_round_result import OperationRoundResult +from one_dragon.base.screen import screen_utils from one_dragon.utils import cv2_utils, str_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log @@ -27,8 +29,10 @@ def __init__(self, ctx: ZContext): self.to_choose_artifact: bool = False # 需要选择普通藏品 self.to_choose_gear: bool = False # 需要选择武备 self.to_choose_gear_branch: bool = False # 需要选择武备分支 - self.to_choose_num: int = 1 # 需要选择的数量 + self.to_choose_num: int = 1 # 需选数量(本轮总目标) self.chosen_idx_list: list[int] = [] # 已经选择过的下标 + self.fallback_click_count: int = 0 # 按钮计数缺失时,使用本轮点击次数作为兜底 + self.last_choose_target_num: int = 0 # 记录上一轮目标数量,用于重置点击计数 @operation_node(name='选择', is_start_node=True) def choose_artifact(self) -> OperationRoundResult: @@ -36,85 +40,373 @@ def choose_artifact(self) -> OperationRoundResult: self.ctx.controller.mouse_move(area.center + Point(0, 100)) time.sleep(0.1) - result = self.round_by_find_area(self.last_screenshot, '迷失之地-通用选择', '按钮-刷新') - can_refresh = result.is_success - art_list, chosen_list = self.get_artifact_pos(self.last_screenshot) - art: Optional[LostVoidArtifactPos] = None + if self.to_choose_num <= 0: + self.fallback_click_count = 0 + self.last_choose_target_num = 0 if self.to_choose_num > 0: - if len(art_list) == 0 and len(chosen_list) == 0: # 已选和可选都没有才算没有 - # 如果初始可选=0 已选=1 即自动战斗结束不及时导致误点屏幕中心选到了卡牌 且正好为单选 会一直无法识别而卡住 而不是去直接点确认 这种小概率情况会出现在秘宝猎人战略 - return self.round_retry(status='无法识别藏品', wait=1) - - priority_list: list[LostVoidArtifactPos] = self.ctx.lost_void.get_artifact_by_priority( - art_list, self.to_choose_num, - consider_priority_1=True, consider_priority_2=not can_refresh, - consider_not_in_priority=not can_refresh, - consider_priority_new=self.ctx.lost_void.challenge_config.artifact_priority_new + if self.last_choose_target_num != self.to_choose_num: + self.fallback_click_count = 0 + self.last_choose_target_num = self.to_choose_num + chosen_cnt = self.get_effective_chosen_count(self.last_screenshot, chosen_list, self.to_choose_num) + log.info( + f'选择概览 需选数量={self.to_choose_num} 已选数量={chosen_cnt} 可选数量={len(art_list)}' ) + if chosen_cnt >= self.to_choose_num: + return self.click_confirm() + + # 四层流程:NEW -> 同流派 -> 优先级 -> 兜底 + # 每层都按“还需选择数量”补齐,补满即确认。 + if len(art_list) > 0 and self.select_by_layers(self.to_choose_num): + return self.click_confirm() + + # 兜底:无条件点击可见文本块后直接确认,避免卡死。 + self.try_choose_by_click_name_text(target_num=self.to_choose_num) + + # 关键修正:多选场景必须选满后才能确认,避免“1/2就点确定”导致卡死/退出。 + _, latest_screen = self.ctx.controller.screenshot() + _, final_chosen_list = self.get_artifact_pos(latest_screen) + final_chosen_cnt = self.get_effective_chosen_count(latest_screen, final_chosen_list, self.to_choose_num) + if final_chosen_cnt < self.to_choose_num: + return self.round_retry( + status=f'未选满 目标={self.to_choose_num} 当前={final_chosen_cnt}', + wait=0.5 + ) + + return self.click_confirm() - # 如果需要选择多个 则有任意一个符合优先级即可 剩下的用优先级以外的补上 - if 0 < len(priority_list) < self.to_choose_num: + def select_by_layers(self, target_num: int) -> bool: + # 选择策略: + # 1) 先取NEW候选(无需试探点击) + # 2) 再取优先级候选 + # 3) 合并去重后仅点击第一个,然后重算 + for _ in range(12): + _, current_screen = self.ctx.controller.screenshot() + can_choose_list, chosen_list = self.get_artifact_pos(current_screen) + chosen_cnt = self.get_effective_chosen_count(current_screen, chosen_list, target_num) + if chosen_cnt >= target_num: + return True + + remain_cnt = target_num - chosen_cnt + available_list = [i for i in can_choose_list if i.can_choose] + if len(available_list) == 0: + continue + + new_list: list[LostVoidArtifactPos] = [] + if self.ctx.lost_void.challenge_config.artifact_priority_new: + new_list = self.sort_candidates([i for i in available_list if i.is_new]) + + priority_list: list[LostVoidArtifactPos] = [] + if self.has_priority_rule(): + # 用当前排序规则产出优先级候选顺序 priority_list = self.ctx.lost_void.get_artifact_by_priority( - art_list, self.to_choose_num, - consider_priority_1=True, consider_priority_2=True, - consider_not_in_priority=True, - consider_priority_new=self.ctx.lost_void.challenge_config.artifact_priority_new + available_list, len(available_list), + consider_priority_1=True, + consider_priority_2=True, + consider_not_in_priority=False, + consider_priority_new=False, + ) + + merged_list: list[LostVoidArtifactPos] = [] + used_center_set: set[tuple[int, int]] = set() + for item in new_list + priority_list: + key = (item.rect.center.x, item.rect.center.y) + if key in used_center_set: + continue + used_center_set.add(key) + merged_list.append(item) + + if len(merged_list) == 0: + log.info( + f'组合选择 优先级层无可点击候选 需选={target_num} 已选={chosen_cnt} ' + f'可选={len(available_list)} 将进入兜底层' ) + return False - # 注意最后筛选优先级的长度一定要符合需求的选择数量 - # 不然在选择2个情况下会一直选择1个 导致无法继续 - if len(priority_list) == self.to_choose_num: - for chosen in chosen_list: - self.ctx.controller.click(chosen.rect.center + Point(0, 100)) - time.sleep(0.5) - - for art in priority_list: - self.ctx.controller.click(art.rect.center) - time.sleep(0.5) - elif can_refresh: - result = self.round_by_find_and_click_area(self.last_screenshot, '迷失之地-通用选择', '按钮-刷新') - if result.is_success: - return self.round_wait(result.status, wait=1) - else: - return self.round_retry(result.status, wait=1) - - result = self.round_by_find_and_click_area(screen=self.last_screenshot, screen_name='迷失之地-通用选择', area_name='按钮-确定', - success_wait=1, retry_wait=1) + new_text = ', '.join([i.artifact.display_name for i in new_list]) if len(new_list) > 0 else '无' + priority_text = ', '.join([i.artifact.display_name for i in priority_list]) if len(priority_list) > 0 else '无' + merged_text = ', '.join([i.artifact.display_name for i in merged_list]) if len(merged_list) > 0 else '无' + log.info( + f'组合选择 需选={target_num} 已选={chosen_cnt} 还需={remain_cnt} 可选={len(available_list)} ' + f'NEW={new_text} 优先级={priority_text} 合并后={merged_text}' + ) + + target = merged_list[0] + log.info(f'组合选择 本轮点击 {target.artifact.display_name} @({target.rect.center.x},{target.rect.center.y})') + self.ctx.controller.click(target.rect.center) + self.fallback_click_count += 1 + time.sleep(0.3) + if self._reached_target_choose_num(target_num): + return True + + _, final_screen = self.ctx.controller.screenshot() + _, final_chosen_list = self.get_artifact_pos(final_screen) + final_chosen_cnt = self.get_effective_chosen_count(final_screen, final_chosen_list, target_num) + return final_chosen_cnt >= target_num + + def _reached_target_choose_num(self, target_num: int) -> bool: + for _ in range(3): + _, latest_screen = self.ctx.controller.screenshot() + _, chosen_list = self.get_artifact_pos(latest_screen) + chosen_cnt = self.get_effective_chosen_count(latest_screen, chosen_list, target_num) + if chosen_cnt >= target_num: + return True + time.sleep(0.2) + return False + + def has_priority_rule(self) -> bool: + cfg = self.ctx.lost_void.challenge_config + if cfg is None: + return False + if len(self.ctx.lost_void.dynamic_priority_list) > 0: + return True + if len(cfg.artifact_priority) > 0: + return True + if len(cfg.artifact_priority_2) > 0: + return True + return False + + def sort_candidates(self, candidate_list: list[LostVoidArtifactPos]) -> list[LostVoidArtifactPos]: + if len(candidate_list) <= 1: + return candidate_list + + level_rank = {'S': 0, 'A': 1, 'B': 2} + return sorted( + candidate_list, + key=lambda i: ( + 0 if i.is_primary_name else 1, + level_rank.get(i.artifact.level, 9), + i.rect.center.x, + i.rect.center.y, + ) + ) + + def click_confirm(self) -> OperationRoundResult: + _, latest_screen = self.ctx.controller.screenshot() + result = self.round_by_find_and_click_area( + screen=latest_screen, + screen_name='迷失之地-通用选择', + area_name='按钮-确定', + success_wait=1, + retry_wait=1 + ) if result.is_success: self.ctx.lost_void.priority_updated = False log.info("藏品选择成功,已设置优先级更新标志") - status = result.status if art is None else f'选择 {art.artifact.name}' - return self.round_success(status) + return self.round_success(result.status) else: return self.round_retry(result.status, wait=1) - def get_artifact_pos(self, screen: MatLike) -> Tuple[List[LostVoidArtifactPos], List[LostVoidArtifactPos]]: + def try_choose_by_click_name_text(self, target_num: int | None = None) -> bool: + """ + 兜底选择: + 1. 获取“区域-藏品名称”的OCR文本块 + 2. 第一轮逐个点击,每次点击后检查“有同流派武备” + 3. 若第一轮未命中,第二轮逐个点击,每次点击后检查“已选择” + """ + if target_num is not None: + return self.try_fill_by_can_choose(target_num) + + _, current_screen = self.ctx.controller.screenshot() + click_target_list = self.get_name_text_click_target_list(current_screen) + if len(click_target_list) == 0: + log.info('无法识别藏品 兜底点击未识别到藏品名称文本块') + return False + + clicked_any = False + + log.info(f'无法识别藏品 兜底第一轮 文本块数量={len(click_target_list)}') + for target_idx, target in enumerate(click_target_list): + self.ctx.controller.click(target.center) + clicked_any = True + time.sleep(0.3) + + _, clicked_screen = self.ctx.controller.screenshot() + if self.has_same_style_selected(clicked_screen): + log.info(f'兜底点击藏品成功 第一轮命中同流派武备 第{target_idx + 1}/{len(click_target_list)}个') + return True + + log.info(f'无法识别藏品 兜底第二轮 文本块数量={len(click_target_list)}') + for target_idx, target in enumerate(click_target_list): + self.ctx.controller.click(target.center) + clicked_any = True + time.sleep(0.3) + + _, clicked_screen = self.ctx.controller.screenshot() + if self.has_selected(clicked_screen): + log.info(f'兜底点击藏品成功 第二轮命中已选择 第{target_idx + 1}/{len(click_target_list)}个') + return True + + _, final_screen = self.ctx.controller.screenshot() + if self.has_selected(final_screen): + log.info('兜底点击藏品成功 第二轮结束后检测到已选择') + return True + + log.info('无法识别藏品 兜底两轮结束仍未检测到目标标志') + return False + + def try_fill_by_can_choose(self, target_num: int) -> bool: + """ + 选择数量场景下的兜底: + 仅点击当前可选(can_choose)候选,避免点到已选导致覆盖。 + 每次点击后都重新截图和重算,直到达到需选数量或候选耗尽。 + """ + tried_center_list: list[Point] = [] + + for _ in range(12): + _, current_screen = self.ctx.controller.screenshot() + can_choose_list, chosen_list = self.get_artifact_pos(current_screen) + chosen_cnt = self.get_effective_chosen_count(current_screen, chosen_list, target_num) + if chosen_cnt >= target_num: + log.info('兜底点击藏品成功 通过can_choose达到目标数量') + return True + + candidate_list = self.sort_candidates([i for i in can_choose_list if i.can_choose]) + target = None + for candidate in candidate_list: + duplicated = False + for tried_center in tried_center_list: + if abs(candidate.rect.center.x - tried_center.x) < 40 and abs(candidate.rect.center.y - tried_center.y) < 40: + duplicated = True + break + if not duplicated: + target = candidate + break + + if target is None: + break + + self.ctx.controller.click(target.rect.center) + tried_center_list.append(target.rect.center) + self.fallback_click_count += 1 + time.sleep(0.3) + + _, final_screen = self.ctx.controller.screenshot() + _, chosen_after = self.get_artifact_pos(final_screen) + chosen_after_cnt = self.get_effective_chosen_count(final_screen, chosen_after, target_num) + if chosen_after_cnt >= target_num: + log.info('兜底点击藏品成功 can_choose结束后达到目标数量') + return True + + log.info('兜底点击藏品结束 can_choose候选耗尽仍未达到目标数量') + return False + + def get_effective_chosen_count( + self, + screen: MatLike, + chosen_list: list[LostVoidArtifactPos], + target_num: int | None = None, + ) -> int: + """ + 获取已选数量: + 1. 优先使用按钮“确定(x/y)”中的x计数 + 2. 按钮计数无法识别时,使用本轮点击次数兜底 + """ + confirm_cnt = self._get_chosen_count_from_confirm_button(screen, target_num) + if confirm_cnt is not None: + # 以按钮计数为准,同时同步兜底计数,避免后续切换来源时出现突变。 + self.fallback_click_count = confirm_cnt + return confirm_cnt + log.debug(f'按钮计数未命中,使用点击次数兜底={self.fallback_click_count}') + return self.fallback_click_count + + def _get_chosen_count_from_confirm_button(self, screen: MatLike, target_num: int | None = None) -> int | None: + area = self.ctx.screen_loader.get_area('迷失之地-通用选择', '按钮-确定') + ocr_result_map = self.ctx.ocr_service.get_ocr_result_map( + image=screen, + rect=area.rect, + crop_first=True + ) + + chosen_cnt: int | None = None + for text in ocr_result_map.keys(): + normalized = text.strip().replace('(', '(').replace(')', ')') + match = re.search(r'(\d+)\s*/\s*(\d+)', normalized) + if match is None: + continue + current_cnt = int(match.group(1)) + total_cnt = int(match.group(2)) + if target_num is not None and total_cnt != target_num: + log.debug(f'按钮计数忽略 文本={text} 解析={current_cnt}/{total_cnt} 目标={target_num}') + continue + if chosen_cnt is None or current_cnt > chosen_cnt: + chosen_cnt = current_cnt + + if chosen_cnt is not None: + log.debug(f'按钮计数命中 已选={chosen_cnt} 目标={target_num}') + return chosen_cnt + + def get_name_text_click_target_list(self, screen: MatLike) -> list[MatchResult]: + area = self.ctx.screen_loader.get_area('迷失之地-通用选择', '区域-藏品名称') + ocr_result_map = self.ctx.ocr_service.get_ocr_result_map( + image=screen, + rect=area.rect, + crop_first=True + ) + + all_result_list: list[MatchResult] = [] + for text, mrl in ocr_result_map.items(): + if len(text.strip()) == 0: + continue + for mr in mrl: + all_result_list.append(mr) + + all_result_list.sort(key=lambda i: (i.center.x, i.center.y)) + + # 同一卡片名称可能被OCR拆成多个文本块,按X坐标做一次聚合,避免重复点同一张 + result_list: list[MatchResult] = [] + for mr in all_result_list: + duplicated = False + for existed in result_list: + if abs(existed.center.x - mr.center.x) < 90: + duplicated = True + break + if not duplicated: + result_list.append(mr) + + return result_list + + def has_same_style_selected(self, screen: MatLike) -> bool: + selected_area = self.ctx.screen_loader.get_area('迷失之地-通用选择', '区域-藏品已选择') + return screen_utils.find_by_ocr( + self.ctx, + screen, + target_cn='有同流派武备', + area=selected_area + ) + + def has_selected(self, screen: MatLike) -> bool: + selected_area = self.ctx.screen_loader.get_area('迷失之地-通用选择', '区域-藏品已选择') + return screen_utils.find_by_ocr( + self.ctx, + screen, + target_cn='已选择', + area=selected_area + ) + + def get_artifact_pos(self, screen: MatLike) -> tuple[list[LostVoidArtifactPos], list[LostVoidArtifactPos]]: """ 获取藏品的位置 @param screen: 游戏画面 - @return: Tuple[识别到的武备的位置, 已经选择的位置] + @return: tuple[识别到的武备的位置, 已经选择的位置] """ self.check_choose_title(screen) if self.to_choose_num == 0: # 不需要选择的 return [], [] - artifact_name_list: list[str] = [] - for art in self.ctx.lost_void.all_artifact_list: - artifact_name_list.append(gt(art.display_name, 'game')) - artifact_pos_list: list[LostVoidArtifactPos] = self.ctx.lost_void.get_artifact_pos( screen, - to_choose_gear_branch=self.to_choose_gear_branch + to_choose_gear_branch=self.to_choose_gear_branch, + screen_name='迷失之地-通用选择' ) can_choose_list = [i for i in artifact_pos_list if i.can_choose] - display_text = ', '.join([i.artifact.display_name for i in can_choose_list]) if len(can_choose_list) > 0 else '无' - log.info(f'当前可选择藏品 {display_text}') + can_choose_text = ', '.join([i.artifact.display_name for i in can_choose_list]) if len(can_choose_list) > 0 else '无' + log.info(f'当前可选藏品 数量={len(can_choose_list)} {can_choose_text}') chosen_list = [i for i in artifact_pos_list if i.chosen] - display_text = ', '.join([i.artifact.display_name for i in chosen_list]) if len(chosen_list) > 0 else '无' - log.info(f'当前已选择藏品 {display_text}') + chosen_text = ', '.join([i.artifact.display_name for i in chosen_list]) if len(chosen_list) > 0 else '无' + log.info(f'当前已选藏品 数量={len(chosen_list)} {chosen_text}') return can_choose_list, chosen_list @@ -131,51 +423,120 @@ def check_choose_title(self, screen: MatLike) -> None: part = cv2_utils.crop_image_only(screen, area.rect) ocr_result = self.ctx.ocr.run_ocr(part) - target_result_list = [ - gt('请选择1项', 'game'), - gt('请选择2项', 'game'), - gt('请选择1个武备', 'game'), - gt('获得武备', 'game'), - gt('武备已升级', 'game'), - gt('获得战利品', 'game'), - gt('请选择1张卡牌', 'game'), - gt('请选择战术棱镜方案强化的方向', 'game'), - ] - - result = self.round_by_find_area(screen, '迷失之地-通用选择', '区域-武备标识') # 下方的GEAR - if result.is_success: - self.to_choose_gear = True - self.to_choose_num = 1 + title_words = [w.strip() for w in ocr_result.keys() if len(w.strip()) > 0] + title_text = ''.join(title_words) + normalized_text = ( + title_text.replace('(', '(').replace(')', ')') + .replace(' ', '').replace(' ', '') + ) - for ocr_word in ocr_result.keys(): - idx = str_utils.find_best_match_by_difflib(ocr_word, target_result_list) - if idx is None: - self.to_choose_num = 0 - elif idx == 0: # 请选择1项 - # 1.5 更新后 武备和普通鸣徽都是这个标题 - self.to_choose_num = 1 - elif idx == 1: # 请选择2项 - self.to_choose_artifact = True - self.to_choose_num = 2 - elif idx == 2: # 请选择1个武备 - self.to_choose_gear = True - self.to_choose_num = 1 - elif idx == 3: # 获得武备 + def apply_rule(rule_id: str) -> None: + if rule_id == 'GEAR_GAIN': self.to_choose_gear = True self.to_choose_num = 0 - elif idx == 4: # 武备已升级 + elif rule_id == 'GEAR_UPGRADE': self.to_choose_gear = True self.to_choose_num = 0 - elif idx == 5: # 获得战利品 + elif rule_id == 'ARTIFACT_GAIN': self.to_choose_artifact = True self.to_choose_num = 0 - elif idx == 6: # 请选择1张卡牌 + elif rule_id == 'GEAR_BRANCH': + self.to_choose_gear = True + self.to_choose_gear_branch = True + self.to_choose_num = 1 + elif rule_id == 'CHOOSE_2': + self.to_choose_artifact = True + self.to_choose_num = 2 + elif rule_id == 'CHOOSE_1_GEAR': + self.to_choose_gear = True + self.to_choose_num = 1 + elif rule_id == 'CHOOSE_1_CARD': self.to_choose_artifact = True self.to_choose_num = 1 - elif idx == 7: # 请选择战术棱镜方案强化的方向 + elif rule_id == 'CHOOSE_1': + # 1.5 更新后 武备和普通鸣徽都可能是这个标题 + self.to_choose_num = 1 + + # 第一轮:精准匹配(避免“1项”被模糊吸附到“2项”) + exact_rule_list: list[tuple[str, list[str]]] = [ + ('GEAR_GAIN', ['获得武备']), + ('GEAR_UPGRADE', ['武备已升级']), + ('ARTIFACT_GAIN', ['获得战利品']), + ('GEAR_BRANCH', ['请选择战术棱镜方案强化的方向']), + # 兼容 2枚/两枚 变体 + ('CHOOSE_2', ['请选择2项', '请选择2枚鸣徽', '请选择两枚鸣徽']), + ('CHOOSE_1_GEAR', ['请选择1个武备']), + ('CHOOSE_1_CARD', ['请选择1张卡牌']), + # 你指出的文案修正:1枚鸣徽 + ('CHOOSE_1', ['请选择1项', '请选择1枚鸣徽', '请选择一枚鸣徽']), + ] + + matched_rule = None + for rule_id, phrase_list in exact_rule_list: + if any(phrase in normalized_text for phrase in phrase_list): + apply_rule(rule_id) + matched_rule = f'exact:{rule_id}' + break + + # 第二轮:模糊匹配兜底(只有第一轮没命中才使用) + if matched_rule is None and len(title_words) > 0: + fuzzy_target_list = [ + '获得武备', + '武备已升级', + '获得战利品', + '请选择战术棱镜方案强化的方向', + '请选择2项', + '请选择2枚鸣徽', + '请选择两枚鸣徽', + '请选择1个武备', + '请选择1张卡牌', + '请选择1项', + '请选择1枚鸣徽', + '请选择一枚鸣徽', + ] + target_2_rule = { + '获得武备': 'GEAR_GAIN', + '武备已升级': 'GEAR_UPGRADE', + '获得战利品': 'ARTIFACT_GAIN', + '请选择战术棱镜方案强化的方向': 'GEAR_BRANCH', + '请选择2项': 'CHOOSE_2', + '请选择2枚鸣徽': 'CHOOSE_2', + '请选择两枚鸣徽': 'CHOOSE_2', + '请选择1个武备': 'CHOOSE_1_GEAR', + '请选择1张卡牌': 'CHOOSE_1_CARD', + '请选择1项': 'CHOOSE_1', + '请选择1枚鸣徽': 'CHOOSE_1', + '请选择一枚鸣徽': 'CHOOSE_1', + } + for ocr_word in title_words: + idx = str_utils.find_best_match_by_difflib( + ocr_word, + fuzzy_target_list, + cutoff=0.9, + ) + if idx is None: + continue + target_phrase = fuzzy_target_list[idx] + apply_rule(target_2_rule[target_phrase]) + matched_rule = f'fuzzy:{target_2_rule[target_phrase]}:{ocr_word}->{target_phrase}' + break + + # 兜底:标题仍未命中时,仍可用下方GEAR标识判断为武备单选 + if matched_rule is None: + result = self.round_by_find_area(screen, '迷失之地-通用选择', '区域-武备标识') + if result.is_success: self.to_choose_gear = True - self.to_choose_gear_branch = True self.to_choose_num = 1 + matched_rule = 'fallback:gear_marker' + else: + matched_rule = 'fallback:none' + + log.info( + f'标题判定 OCR={title_words if len(title_words) > 0 else ["无"]} ' + f'规则={matched_rule} ' + f'需选数量={self.to_choose_num} 选择藏品={self.to_choose_artifact} ' + f'选择武备={self.to_choose_gear} 武备分支={self.to_choose_gear_branch}' + ) def __debug(): ctx = ZContext() @@ -205,4 +566,4 @@ def __get_get_artifact_pos(): if __name__ == '__main__': - __debug() \ No newline at end of file + __debug() diff --git a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_gear.py b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_gear.py index 6e2a910565..acca28b233 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_gear.py +++ b/src/zzz_od/application/hollow_zero/lost_void/operation/interact/lost_void_choose_gear.py @@ -1,16 +1,17 @@ import time +import re import cv2 from cv2.typing import MatLike -from typing import List, Tuple +from typing import Any from one_dragon.base.geometry.point import Point from one_dragon.base.geometry.rectangle import Rect -from one_dragon.base.matcher.match_result import MatchResult +from one_dragon.base.matcher.match_result import MatchResult, MatchResultList from one_dragon.base.operation.operation_edge import node_from from one_dragon.base.operation.operation_node import operation_node from one_dragon.base.operation.operation_round_result import OperationRoundResult -from one_dragon.utils import cv2_utils, cal_utils, str_utils +from one_dragon.utils import cv2_utils, cal_utils from one_dragon.utils.log_utils import log from zzz_od.application.hollow_zero.lost_void.context.lost_void_artifact import LostVoidArtifact from zzz_od.application.hollow_zero.lost_void.operation.interact.lost_void_artifact_pos import LostVoidArtifactPos @@ -44,43 +45,39 @@ def choose_gear(self) -> OperationRoundResult: return self.round_retry(status=f'当前画面 {screen_name}', wait=1) choose_new: bool = False - if self.ctx.lost_void.challenge_config.chase_new_mode: - gear_contours, gear_context = self._find_gears_with_status() + gear_contours, gear_context = self._find_gears_with_status() + if not gear_contours: + return self.round_retry(status='无法识别武备槽位') - if not gear_contours: - return self.round_retry(status='【武备追新】无法识别任何武备') - - unlocked_gears = [g for g, has_level in gear_contours if not has_level] + gear_list, has_level_list = self.get_gear_pos_by_click_ocr(gear_contours, gear_context) + if len(gear_list) == 0: + return self.round_retry(status='无法识别武备名称') + if self.ctx.lost_void.challenge_config.chase_new_mode: + unlocked_gears: list[LostVoidArtifactPos] = [ + gear_list[i] + for i in range(min(len(gear_list), len(has_level_list))) + if not has_level_list[i] + ] if unlocked_gears: - target_contour = unlocked_gears[0] - log.debug("【武备追新】找到一个未获取的武备,准备点击") - - M = cv2.moments(target_contour) - center_x = int(M["m10"] / M["m00"]) - center_y = int(M["m01"] / M["m00"]) - offset_x, offset_y = gear_context.crop_offset - click_pos = Point(center_x + offset_x, center_y + offset_y) - log.debug(f"【武备追新】 点击目标坐标: {click_pos} (相对: ({center_x}, {center_y}), 偏移: {gear_context.crop_offset})") - self.ctx.controller.click(click_pos) + priority_new = self.ctx.lost_void.get_artifact_by_priority(unlocked_gears, 1) + target = priority_new[0] if len(priority_new) > 0 else unlocked_gears[0] + self.ctx.controller.click(target.rect.center) time.sleep(0.5) choose_new = True + log.info(f'【武备追新】当前可选未获取武备 {",".join([i.artifact.display_name for i in unlocked_gears])}') else: - log.debug("【武备追新】所有武备都已获取,回退至原优先级") + log.info('【武备追新】所有武备都已获取,回退至优先级') if not choose_new: - gear_list = self.get_gear_pos_by_feature(screen_list) - if len(gear_list) == 0: - return self.round_retry(status='无法识别武备') - - priority_list: List[LostVoidArtifactPos] = self.ctx.lost_void.get_artifact_by_priority(gear_list, 1) - if priority_list: - self.ctx.controller.click(priority_list[0].rect.center) - time.sleep(0.5) + priority_list: list[LostVoidArtifactPos] = self.ctx.lost_void.get_artifact_by_priority(gear_list, 1) + target = priority_list[0] if len(priority_list) > 0 else gear_list[0] + self.ctx.controller.click(target.rect.center) + time.sleep(0.5) return self.round_success(wait=0.5) - def _find_gears_with_status(self) -> Tuple[List[tuple[any, bool]], any]: + def _find_gears_with_status(self) -> tuple[list[tuple[Any, bool]], Any]: """ 使用CV流水线查找武备及其状态 :return: (武备轮廓, 是否有等级) @@ -141,58 +138,116 @@ def _find_gears_with_status(self) -> Tuple[List[tuple[any, bool]], any]: # cv2_utils.show_image(self.last_screenshot, debug_rects, wait=0) return gear_with_status, gear_context - def get_gear_pos_by_feature(self, screen_list: List[MatLike]) -> List[LostVoidArtifactPos]: + def get_gear_pos_by_click_ocr( + self, + gear_with_status: list[tuple[Any, bool]], + gear_context: Any, + ) -> tuple[list[LostVoidArtifactPos], list[bool]]: """ - 获取武备的位置 - @param screen_list: 游戏截图列表 由于武备的图像是动态的 需要多张识别后合并结果 - @param only_no_level: 只获取无等级的 - @return: 识别到的武备的位置 + 逐个点击武备,等待1秒截图,裁剪“武备名称”区域,按实际数量纵向拼图后统一OCR。 + 按从上到下的OCR结果回填到从左到右的武备槽位。 """ - area = self.ctx.screen_loader.get_area('迷失之地-武备选择', '武备列表') - to_check_list: List[LostVoidArtifact] = [ - i - for i in self.ctx.lost_void.all_artifact_list - if i.template_id is not None - ] - - result_list: List[LostVoidArtifactPos] = [] - - for screen in screen_list: - part = cv2_utils.crop_image_only(screen, area.rect) - - source_kps, source_desc = cv2_utils.feature_detect_and_compute(part) - for gear in to_check_list: - template = self.ctx.template_loader.get_template('lost_void', gear.template_id) - if template is None: - continue - - template_kps, template_desc = template.features - mr = cv2_utils.feature_match_for_one( - source_kps, source_desc, - template_kps, template_desc, - template_width=template.raw.shape[1], template_height=template.raw.shape[0], - knn_distance_percent=0.5 + if len(gear_with_status) == 0: + return [], [] + + name_area = self.ctx.screen_loader.get_area('迷失之地-武备选择', '武备名称') + gear_abs_pairs = gear_context.get_absolute_rect_pairs() + gear_abs_pairs.sort(key=lambda item: item[1][0]) + sorted_status_list = sorted(gear_with_status, key=lambda item: cv2.boundingRect(item[0])[0]) + + slice_list: list[MatLike] = [] + click_rect_list: list[Rect] = [] + has_level_list: list[bool] = [] + + for i, (_, (x1, y1, x2, y2)) in enumerate(gear_abs_pairs): + click_pos = Point((x1 + x2) // 2, (y1 + y2) // 2) + self.ctx.controller.click(click_pos) + time.sleep(1) + _, current_screen = self.ctx.controller.screenshot() + slice_list.append(cv2_utils.crop_image_only(current_screen, name_area.rect)) + click_rect_list.append(Rect(x1, y1, x2, y2)) + + if i < len(sorted_status_list): + has_level_list.append(sorted_status_list[i][1]) + else: + has_level_list.append(True) + + if len(slice_list) == 0: + return [], [] + + stitched = cv2.vconcat(slice_list) + ocr_map = self.ctx.ocr_service.get_ocr_result_map( + image=stitched, + crop_first=False, + ) + name_list = self._extract_names_from_stitched_ocr(ocr_map, len(slice_list), slice_list[0].shape[0]) + + result_list: list[LostVoidArtifactPos] = [] + total_cnt = min(len(click_rect_list), len(name_list)) + for i in range(total_cnt): + ocr_text = name_list[i] + if len(ocr_text) == 0: + continue + + art, is_primary = self._build_artifact_from_ocr_name(ocr_text) + if art is None: + continue + + result_list.append( + LostVoidArtifactPos( + art=art, + rect=click_rect_list[i], + ocr_text=ocr_text, + is_primary_name=is_primary, ) - - if mr is None: - continue - - mr.add_offset(area.left_top) - mr.data = gear - - existed = False - for existed_result in result_list: - if cal_utils.distance_between(existed_result.rect.center, mr.center) < existed_result.rect.width // 2: - existed = True - break - - if not existed: - result_list.append(LostVoidArtifactPos(gear, mr.rect)) + ) display_text = ','.join([i.artifact.display_name for i in result_list]) if len(result_list) > 0 else '无' - log.info(f'当前识别藏品 {display_text}') - - return result_list + log.info(f'当前识别武备 {display_text}') + return result_list, has_level_list + + def _extract_names_from_stitched_ocr( + self, + ocr_map: dict[str, MatchResultList], + slot_cnt: int, + slot_height: int, + ) -> list[str]: + slot_tokens: list[list[tuple[int, str]]] = [[] for _ in range(slot_cnt)] + for text, mrl in ocr_map.items(): + token = text.strip() + if len(token) == 0: + continue + for mr in mrl: + slot_idx = mr.center.y // slot_height + if slot_idx < 0 or slot_idx >= slot_cnt: + continue + slot_tokens[slot_idx].append((mr.center.x, token)) + + name_list: list[str] = [] + for token_list in slot_tokens: + token_list.sort(key=lambda item: item[0]) + text = ''.join([i[1] for i in token_list]).strip() + name_list.append(text) + return name_list + + def _build_artifact_from_ocr_name(self, ocr_text: str) -> tuple[LostVoidArtifact | None, bool]: + normalized = ocr_text.strip().replace('【', '[').replace('】', ']') + if len(normalized) == 0: + return None, False + + match = re.search(r'\[(.+?)\](.+)$', normalized) + if match is not None: + raw_category = match.group(1).strip() + raw_name = match.group(2).strip() + if len(raw_name) == 0: + return None, False + category = raw_category.split(':', 1)[0].split(':', 1)[0].strip() + if len(category) == 0: + category = raw_category + return LostVoidArtifact(category=category, name=raw_name, level='?', is_gear=True), True + + # 武备选择仅接受 [分类]名称 结构,其他OCR结果直接丢弃。 + return None, False @node_from(from_name='选择武备') @operation_node(name='点击携带') @@ -224,4 +279,4 @@ def __debug(): if __name__ == '__main__': - __debug() \ No newline at end of file + __debug() diff --git a/src/zzz_od/application/hollow_zero/lost_void/operation/lost_void_run_level.py b/src/zzz_od/application/hollow_zero/lost_void/operation/lost_void_run_level.py index e6b450bada..75c53b83fd 100644 --- a/src/zzz_od/application/hollow_zero/lost_void/operation/lost_void_run_level.py +++ b/src/zzz_od/application/hollow_zero/lost_void/operation/lost_void_run_level.py @@ -670,10 +670,11 @@ def in_battle(self) -> OperationRoundResult: if self.current_frame_in_battle: # 当前回到可战斗画面 if (not self.last_frame_in_battle # 之前在非战斗画面 - or self.last_screenshot_time - self.last_det_time >= 1 # 1秒识别一次 + or self.last_screenshot_time - self.last_det_time >= 0.8 # 0.8秒识别一次 or (self.no_in_battle_times > 0 and self.last_screenshot_time - self.last_check_finish_time >= 0.1) # 之前也识别到脱离战斗 0.1秒识别一次 ): no_in_battle = False + found_next_region_hint = False # 尝试识别下层入口 (道中危机 和 终结之役 不需要识别) if self.region_type not in [LostVoidRegionType.ELITE, LostVoidRegionType.BOSS]: @@ -717,8 +718,15 @@ def in_battle(self) -> OperationRoundResult: found = screen_utils.find_by_ocr(self.ctx, self.last_screenshot, target_cn='前往下一个区域', area=area) if found: + found_next_region_hint = True no_in_battle = True + # "前往下一个区域" 单次命中即判脱战 + if found_next_region_hint: + self.ctx.auto_battle_context.stop_auto_battle() + self.no_in_battle_times = 0 + return self.round_success('识别需移动交互') + if no_in_battle: self.no_in_battle_times += 1 else: diff --git a/src/zzz_od/application/intel_board/__init__.py b/src/zzz_od/application/intel_board/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/zzz_od/application/intel_board/intel_board_app.py b/src/zzz_od/application/intel_board/intel_board_app.py new file mode 100644 index 0000000000..6a66bb17da --- /dev/null +++ b/src/zzz_od/application/intel_board/intel_board_app.py @@ -0,0 +1,358 @@ +from one_dragon.base.geometry.point import Point +from one_dragon.base.operation.application import application_const +from one_dragon.base.operation.operation_edge import node_from +from one_dragon.base.operation.operation_node import operation_node +from one_dragon.base.operation.operation_notify import NotifyTiming, node_notify +from one_dragon.base.operation.operation_round_result import ( + OperationRoundResult, + OperationRoundResultEnum, +) +from one_dragon.utils import cv2_utils +from zzz_od.application.intel_board import intel_board_const +from zzz_od.application.intel_board.intel_board_config import IntelBoardConfig +from zzz_od.application.intel_board.intel_board_run_record import IntelBoardRunRecord +from zzz_od.application.zzz_application import ZApplication +from zzz_od.context.zzz_context import ZContext +from zzz_od.operation.back_to_normal_world import BackToNormalWorld +from zzz_od.operation.choose_predefined_team import ChoosePredefinedTeam +from zzz_od.operation.compendium.notorious_hunt_move import NotoriousHuntMove + + +class IntelBoardApp(ZApplication): + def __init__(self, ctx: ZContext, instance_idx: int = 0, game_refresh_hour_offset: int = 0): + ZApplication.__init__( + self, + ctx=ctx, + app_id=intel_board_const.APP_ID, + op_name=intel_board_const.APP_NAME, + run_record=IntelBoardRunRecord( + instance_idx=instance_idx, + game_refresh_hour_offset=game_refresh_hour_offset + ) + ) + self.config: IntelBoardConfig = self.ctx.run_context.get_config( + app_id=intel_board_const.APP_ID, + instance_idx=self.ctx.current_instance_idx, + group_id=application_const.DEFAULT_GROUP_ID, + ) + self.run_record: IntelBoardRunRecord = self.run_record + self.scroll_times: int = 0 + self.current_commission_type: str | None = None + self.has_filtered: bool = False + + @operation_node(name='返回大世界', is_start_node=True) + def back_to_world(self) -> OperationRoundResult: + op = BackToNormalWorld(self.ctx, ensure_normal_world=True) + return self.round_by_op_result(op.execute()) + + @node_from(from_name='返回大世界') + @operation_node(name='打开情报板') + def open_board(self) -> OperationRoundResult: + if self.config.exp_grind_mode: + if self.run_record.exp_complete: + return self.round_success('本周期已完成') + elif self.run_record.progress_complete: + return self.round_success('本周期已完成') + + # 1. 识别并点击大世界-功能导览按钮 + return self.round_by_find_and_click_area( + screen_name='大世界-普通', + area_name='功能导览', + success_wait=1, + retry_wait=1 + ) + + @node_from(from_name='打开情报板') + @operation_node(name='点击情报板') + def click_board(self) -> OperationRoundResult: + # 2. OCR 点击情报板 + return self.round_by_ocr_and_click(self.last_screenshot, '情报板', success_wait=1, retry_wait=1) + + @node_from(from_name='检查进度', success=False) + @node_from(from_name='接取委托', success=False) + @operation_node(name='刷新委托') + def refresh_commission(self) -> OperationRoundResult: + self.scroll_times = 0 + if self.has_filtered: + return self.round_by_find_and_click_area( + screen_name='情报板', area_name='刷新按钮', + success_wait=1, retry_wait=1 + ) + + return self.round_success('未筛选') + + @node_from(from_name='刷新委托', status='未筛选') + @operation_node(name='打开筛选', node_max_retry_times=60) + def open_filter(self) -> OperationRoundResult: + result = self.round_by_find_area(self.last_screenshot, '情报板', '点数兑换') + if result.is_success: + return self.round_by_click_area( + screen_name='情报板', area_name='筛选按钮', + success_wait=0.5, retry_wait=0.5 + ) + + return self.round_retry('未找到筛选按钮', wait=1) + + @node_from(from_name='打开筛选') + @operation_node(name='重置筛选') + def reset_filter(self) -> OperationRoundResult: + area = self.ctx.screen_loader.get_area('情报板', '重置按钮') + return self.round_by_ocr_and_click(self.last_screenshot, '重置', area, success_wait=0.5, retry_wait=0.5) + + @node_from(from_name='重置筛选') + @operation_node(name='选择恶名狩猎') + def select_notorious_hunt(self) -> OperationRoundResult: + search_area = self.ctx.screen_loader.get_area('情报板', '搜索区域') + return self.round_by_ocr_and_click(self.last_screenshot, '恶名狩猎', area=search_area, + success_wait=0.5, retry_wait=0.5) + + @node_from(from_name='选择恶名狩猎') + @operation_node(name='选择专业挑战室') + def select_expert_challenge(self) -> OperationRoundResult: + search_area = self.ctx.screen_loader.get_area('情报板', '搜索区域') + return self.round_by_ocr_and_click(self.last_screenshot, '专业挑战室', area=search_area, + success_wait=0.5, retry_wait=0.5) + + @node_from(from_name='选择专业挑战室') + @operation_node(name='关闭筛选') + def close_filter(self) -> OperationRoundResult: + self.has_filtered = True + return self.round_by_click_area('情报板', '关闭筛选', success_wait=1) + + @node_from(from_name='刷新委托') + @node_from(from_name='关闭筛选') + @node_from(from_name='寻找委托', status='翻页') + @operation_node(name='寻找委托') + def find_commission(self) -> OperationRoundResult: + # 4. Ocr 专业挑战室/恶名狩猎,找不到就往下翻到找到为止 + result = self.round_by_ocr_and_click_by_priority( + target_cn_list=['专业挑战室', '恶名狩猎'], + success_wait=0.5, + ) + if result.is_success: + commission_map = {'专业挑战室': 'expert_challenge', '恶名狩猎': 'notorious_hunt'} + self.current_commission_type = commission_map.get(result.status) + return result + + # 翻页 + if self.scroll_times >= 5: + return self.round_success(status='无委托') + + self.scroll_times += 1 + self.scroll_area('情报板', '搜索区域') + return self.round_wait(status='翻页', wait=1) + + @node_from(from_name='寻找委托') + @operation_node(name='接取委托') + def accept_commission(self) -> OperationRoundResult: + + return self.round_by_ocr_and_click_with_action( + target_action_list=[ + ('接取委托', OperationRoundResultEnum.WAIT), + ('前往', OperationRoundResultEnum.SUCCESS), + ], + success_wait=0.5, + wait_wait=0.5, + retry_wait=0.5, + ) + + @node_from(from_name='接取委托') + @operation_node(name='下一步') + def next_step(self) -> OperationRoundResult: + # 7. 持续点击下一步直到出现预备编队页面 需要先选编队再出战 + result = self.round_by_ocr(self.last_screenshot, '预备编队') + if result.is_success: + return self.round_success() + return self.round_by_ocr_and_click_with_action( + target_action_list=[ + ('下一步', OperationRoundResultEnum.WAIT), + ('无报酬模式', OperationRoundResultEnum.WAIT), + ], + wait_wait=1, + retry_wait=1 + ) + + @node_from(from_name='下一步') + @operation_node(name='选择预备编队') + def choose_predefined_team(self) -> OperationRoundResult: + # 8. 选择预备编队 无需选择时直接跳过 + if self.config.predefined_team_idx == -1: + return self.round_success('无需选择预备编队') + op = ChoosePredefinedTeam(self.ctx, [self.config.predefined_team_idx]) + return self.round_by_op_result(op.execute()) + + @node_from(from_name='选择预备编队') + @operation_node(name='点击出战') + def click_deploy(self) -> OperationRoundResult: + # 9. 编队选择完成后点击出战进入战斗 + return self.round_by_ocr_and_click(self.last_screenshot, '出战', success_wait=1, retry_wait=1) + + @node_from(from_name='点击出战') + @operation_node(name='委托代行中弹窗') + def click_commission_agent(self) -> OperationRoundResult: + # 点击委托代行中弹窗的确定按钮(如果有的话) + result = self.round_by_ocr(self.last_screenshot, '委托代行中') + if result.is_success: + return self.round_by_ocr_and_click(self.last_screenshot, '确认') + return self.round_success('无弹窗') + + @node_from(from_name='委托代行中弹窗') + @operation_node(name='加载自动战斗指令') + def init_auto_battle(self) -> OperationRoundResult: + # 10. 加载自动战斗指令 根据编队配置或默认配置 + if self.config.predefined_team_idx == -1: + auto_battle = self.config.auto_battle_config + else: + team_list = self.ctx.team_config.team_list + auto_battle = team_list[self.config.predefined_team_idx].auto_battle + self.ctx.auto_battle_context.init_auto_op(op_name=auto_battle) + return self.round_success() + + @node_from(from_name='加载自动战斗指令') + @operation_node(name='等待战斗画面加载', node_max_retry_times=60) + def wait_battle_screen(self) -> OperationRoundResult: + # 11. 等待战斗画面加载完成 + result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-普通攻击') + if result.is_success: + return self.round_success() + + result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') + if result.is_success: + return self.round_success() + + return self.round_retry(result.status, wait=1) + + @node_from(from_name='等待战斗画面加载') + @operation_node(name='战斗前移动') + def pre_battle_move(self) -> OperationRoundResult: + # 12. 根据委托类型选择移动方式 + if self.current_commission_type == 'notorious_hunt': + op = NotoriousHuntMove(self.ctx, 3) + return self.round_by_op_result(op.execute()) + else: + # expert_challenge: 向前走一段距离 确保能开怪 + self.ctx.controller.move_w(press=True, press_time=1.5, release=True) + return self.round_success() + + @node_from(from_name='战斗前移动') + @operation_node(name='开始自动战斗') + def start_auto_battle(self) -> OperationRoundResult: + # 13. 启动自动战斗 + self.ctx.auto_battle_context.start_auto_battle() + return self.round_success() + + @node_from(from_name='开始自动战斗') + @operation_node(name='战斗中', mute=True, timeout_seconds=600) + def auto_battle(self) -> OperationRoundResult: + if self.ctx.auto_battle_context.last_check_end_result is not None: + self.ctx.auto_battle_context.stop_auto_battle() + return self.round_success(status=self.ctx.auto_battle_context.last_check_end_result) + + self.ctx.auto_battle_context.check_battle_state( + self.last_screenshot, self.last_screenshot_time, + check_battle_end_normal_result=True, + ) + + return self.round_wait(wait=self.ctx.battle_assistant_config.screenshot_interval) + + @node_from(from_name='战斗中') + @node_from(from_name='点击结算按钮') + @operation_node(name='检查回到委托列表') + def check_back_to_list(self) -> OperationRoundResult: + result = self.round_by_ocr(self.last_screenshot, '周期内可获取') + if result.is_success: + if self.current_commission_type == 'expert_challenge': + self.run_record.expert_challenge_count += 1 + elif self.current_commission_type == 'notorious_hunt': + self.run_record.notorious_hunt_count += 1 + self.current_commission_type = None + return self.round_success('结算完成') + return self.round_fail('未回到列表') + + @node_from(from_name='检查回到委托列表', success=False) + @operation_node(name='点击结算按钮', node_max_retry_times=60) + def click_settlement_button(self) -> OperationRoundResult: + result = self.round_by_ocr_and_click_with_action( + target_action_list=[ + ('完成', OperationRoundResultEnum.WAIT), + ('下一步', OperationRoundResultEnum.WAIT), + ('确认', OperationRoundResultEnum.WAIT), + ], + wait_wait=1, + retry_wait=1, + ) + if result.result != OperationRoundResultEnum.RETRY: + return self.round_success(result.status, wait=1) + return result + + @node_from(from_name='检查回到委托列表') + @node_from(from_name='点击情报板') + @operation_node(name='检查进度') + def check_progress(self) -> OperationRoundResult: + # 刷经验模式:先检查已有经验是否够了 + if self.config.exp_grind_mode and self.run_record.exp_complete: + return self.round_success('完成') + + # OCR 读取进度代币值 + rect = self.ctx.screen_loader.get_area('情报板', '进度文本').rect + screen = self.last_screenshot + part = cv2_utils.crop_image_only(screen, rect) + ocr_result = self.ctx.ocr.run_ocr_single_line(part) + + current = 0 + try: + normalized = ocr_result.replace('/', '/') + clean_text = ''.join([c for c in normalized if c.isdigit() or c == '/']) + if '/' in clean_text: + current = int(clean_text.split('/')[0]) + + if not self.config.exp_grind_mode: + # 普通模式:代币值满即完成 + if current >= 1000: + self.run_record.progress_complete = True + return self.round_success('完成') + except (ValueError, IndexError) as e: + return self.round_fail(f'解析进度文本失败: {ocr_result}, 错误: {e}') + + # 刷经验模式:计数都为0时,根据代币值估算最低经验 + if self.config.exp_grind_mode: + if (self.run_record.notorious_hunt_count == 0 + and self.run_record.expert_challenge_count == 0 + and self.run_record.base_exp == 0 + and current > 0): + # 专业挑战一次70代币=250经验,按此比例估算最低经验 + self.run_record.base_exp = ((current + 69) // 70) * 250 + if self.run_record.exp_complete: + return self.round_success('完成') + + return self.round_fail('继续') + + @node_from(from_name='打开情报板', status='本周期已完成') + @node_from(from_name='检查进度') + @node_from(from_name='寻找委托', status='无委托') + @node_notify(when=NotifyTiming.CURRENT_DONE, detail=True) + @operation_node(name='结束处理') + def finish_processing(self) -> OperationRoundResult: + status = (f'完成 恶名狩猎: {self.run_record.notorious_hunt_count}, ' + f'专业挑战室: {self.run_record.expert_challenge_count}, ' + f'累计经验: {self.run_record.total_exp}') + + return self.round_success(status) + + def handle_pause(self, e=None): + self.ctx.auto_battle_context.stop_auto_battle() + + def handle_resume(self, e=None): + if self.current_node.node is not None and self.current_node.node.cn == '战斗中': + self.ctx.auto_battle_context.resume_auto_battle() + + +def __debug(): + ctx = ZContext() + ctx.init() + ctx.run_context.start_running() + app = IntelBoardApp(ctx) + app.execute() + +if __name__ == '__main__': + __debug() diff --git a/src/zzz_od/application/intel_board/intel_board_app_factory.py b/src/zzz_od/application/intel_board/intel_board_app_factory.py new file mode 100644 index 0000000000..71b9aa0711 --- /dev/null +++ b/src/zzz_od/application/intel_board/intel_board_app_factory.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from one_dragon.base.operation.application.application_config import ApplicationConfig +from one_dragon.base.operation.application.application_factory import ApplicationFactory +from one_dragon.base.operation.application_base import Application +from zzz_od.application.intel_board import intel_board_const +from zzz_od.application.intel_board.intel_board_app import ( + IntelBoardApp, +) +from zzz_od.application.intel_board.intel_board_config import ( + IntelBoardConfig, +) +from zzz_od.application.intel_board.intel_board_run_record import ( + IntelBoardRunRecord, +) +from one_dragon.base.operation.application_run_record import AppRunRecord + +if TYPE_CHECKING: + from zzz_od.context.zzz_context import ZContext + + +class IntelBoardAppFactory(ApplicationFactory): + + def __init__(self, ctx: ZContext): + ApplicationFactory.__init__( + self, + app_id=intel_board_const.APP_ID, + app_name=intel_board_const.APP_NAME, + ) + self.ctx: ZContext = ctx + + def create_application(self, instance_idx: int, group_id: str) -> Application: + return IntelBoardApp( + self.ctx, + instance_idx=instance_idx, + game_refresh_hour_offset=self.ctx.game_account_config.game_refresh_hour_offset + ) + + def create_config( + self, instance_idx: int, group_id: str + ) -> ApplicationConfig: + return IntelBoardConfig( + instance_idx=instance_idx, + group_id=group_id, + ) + + def create_run_record(self, instance_idx: int) -> AppRunRecord: + return IntelBoardRunRecord( + instance_idx=instance_idx, + game_refresh_hour_offset=self.ctx.game_account_config.game_refresh_hour_offset, + ) diff --git a/src/zzz_od/application/intel_board/intel_board_config.py b/src/zzz_od/application/intel_board/intel_board_config.py new file mode 100644 index 0000000000..a415a05c0b --- /dev/null +++ b/src/zzz_od/application/intel_board/intel_board_config.py @@ -0,0 +1,40 @@ +from one_dragon.base.operation.application.application_config import ApplicationConfig +from zzz_od.application.intel_board import intel_board_const + + +class IntelBoardConfig(ApplicationConfig): + + def __init__(self, instance_idx: int, group_id: str): + ApplicationConfig.__init__( + self, + app_id=intel_board_const.APP_ID, + instance_idx=instance_idx, + group_id=group_id, + ) + + @property + def predefined_team_idx(self) -> int: + """预备编队下标,-1 代表不选择""" + return self.get('predefined_team_idx', -1) + + @predefined_team_idx.setter + def predefined_team_idx(self, new_value: int) -> None: + self.update('predefined_team_idx', new_value) + + @property + def auto_battle_config(self) -> str: + """自动战斗配置名称""" + return self.get('auto_battle_config', '全配队通用') + + @auto_battle_config.setter + def auto_battle_config(self, new_value: str) -> None: + self.update('auto_battle_config', new_value) + + @property + def exp_grind_mode(self) -> bool: + """是否开启刷满经验模式,开启后按经验值判断完成而非情报板进度""" + return self.get('exp_grind_mode', False) + + @exp_grind_mode.setter + def exp_grind_mode(self, new_value: bool) -> None: + self.update('exp_grind_mode', new_value) diff --git a/src/zzz_od/application/intel_board/intel_board_const.py b/src/zzz_od/application/intel_board/intel_board_const.py new file mode 100644 index 0000000000..7819a7ce35 --- /dev/null +++ b/src/zzz_od/application/intel_board/intel_board_const.py @@ -0,0 +1,4 @@ +APP_ID = 'intel_board' +APP_NAME = '情报板' +DEFAULT_GROUP = True +NEED_NOTIFY = True diff --git a/src/zzz_od/application/intel_board/intel_board_run_record.py b/src/zzz_od/application/intel_board/intel_board_run_record.py new file mode 100644 index 0000000000..07555f2ff8 --- /dev/null +++ b/src/zzz_od/application/intel_board/intel_board_run_record.py @@ -0,0 +1,85 @@ +from one_dragon.base.operation.application_run_record import ( + AppRunRecord, + AppRunRecordPeriod, +) +from zzz_od.application.intel_board import intel_board_const + +# 每次战斗获得的经验值 +EXP_PER_NOTORIOUS_HUNT = 500 +EXP_PER_EXPERT_CHALLENGE = 250 +EXP_TARGET = 5000 + + +class IntelBoardRunRecord(AppRunRecord): + + def __init__(self, instance_idx: int | None = None, game_refresh_hour_offset: int = 0): + AppRunRecord.__init__( + self, + intel_board_const.APP_ID, + instance_idx=instance_idx, + game_refresh_hour_offset=game_refresh_hour_offset, + record_period=AppRunRecordPeriod.WEEKLY + ) + + @property + def progress_complete(self) -> bool: + """本周期进度是否已满 (1000/1000)""" + return self.get('progress_complete', False) + + @progress_complete.setter + def progress_complete(self, value: bool) -> None: + self.update('progress_complete', value) + + @property + def notorious_hunt_count(self) -> int: + """本周期恶名狩猎完成次数""" + return self.get('notorious_hunt_count', 0) + + @notorious_hunt_count.setter + def notorious_hunt_count(self, value: int) -> None: + self.update('notorious_hunt_count', value) + + @property + def expert_challenge_count(self) -> int: + """本周期专业挑战室完成次数""" + return self.get('expert_challenge_count', 0) + + @expert_challenge_count.setter + def expert_challenge_count(self, value: int) -> None: + self.update('expert_challenge_count', value) + + @property + def base_exp(self) -> int: + """根据OCR进度估算的基础经验值(计数为0时的历史经验)""" + return self.get('base_exp', 0) + + @base_exp.setter + def base_exp(self, value: int) -> None: + self.update('base_exp', value) + + @property + def total_exp(self) -> int: + """本周期已累计经验值""" + return (self.base_exp + + self.notorious_hunt_count * EXP_PER_NOTORIOUS_HUNT + + self.expert_challenge_count * EXP_PER_EXPERT_CHALLENGE) + + @property + def exp_complete(self) -> bool: + """经验值是否已刷满""" + return self.total_exp >= EXP_TARGET + + def reset_record(self): + AppRunRecord.reset_record(self) + self.progress_complete = False + self.notorious_hunt_count = 0 + self.expert_challenge_count = 0 + self.base_exp = 0 + + @property + def run_status_under_now(self) -> int: + if self._should_reset_by_dt(): + return AppRunRecord.STATUS_WAIT + if self.progress_complete or self.exp_complete: + return AppRunRecord.STATUS_SUCCESS + return self.run_status diff --git a/src/zzz_od/application/notify/notify_app.py b/src/zzz_od/application/notify/notify_app.py index 3b15c1db04..2bfd819d4f 100644 --- a/src/zzz_od/application/notify/notify_app.py +++ b/src/zzz_od/application/notify/notify_app.py @@ -3,6 +3,7 @@ from one_dragon.base.operation.application_run_record import AppRunRecord from one_dragon.base.operation.operation_node import operation_node from one_dragon.base.operation.operation_round_result import OperationRoundResult +from zzz_od.application.charge_plan.charge_plan_run_record import ChargePlanRunRecord from zzz_od.application.notify import notify_const from zzz_od.application.zzz_application import ZApplication from zzz_od.context.zzz_context import ZContext @@ -44,6 +45,7 @@ def notify(self) -> OperationRoundResult: def format_message(self) -> str: success = [] failure = [] + charge_power_text = None group_config = self.ctx.app_group_manager.get_one_dragon_group_config(instance_idx=self.ctx.current_instance_idx) for app_config in group_config.app_list: @@ -55,29 +57,37 @@ def format_message(self) -> str: continue if not self.is_within_time(run_record.run_time): continue + if isinstance(run_record, ChargePlanRunRecord): + charge_power = run_record.get_estimated_charge_power() + if charge_power >= 0: + charge_power_text = ( + f'当前体力:{charge_power}/{ChargePlanRunRecord.MAX_CHARGE_POWER}' + ) if run_record.run_status_under_now == AppRunRecord.STATUS_SUCCESS: success.append(app_config.app_name) if run_record.run_status_under_now == AppRunRecord.STATUS_FAIL: failure.append(app_config.app_name) self.exist_failure = True - parts = [f"一条龙运行完成:"] + parts = ["一条龙运行完成:"] + if charge_power_text is not None: + parts.append(charge_power_text) has_failure = bool(failure) has_success = bool(success) if has_failure: parts.append(f"❌ 失败指令:{', '.join(failure)}") elif has_success: - parts.append(f"全部成功✅") + parts.append("全部成功✅") if has_success: parts.append(f"✅ 成功指令:{', '.join(success)}") elif not has_failure: - parts.append(f"全部失败❌") + parts.append("全部失败❌") return "\n".join(parts) - def is_within_time(self, time_str) -> bool: + def is_within_time(self, time_str: str) -> bool: end_time = datetime.now() try: # 解析输入的时间字符串,格式为月-日 时:分 @@ -97,11 +107,8 @@ def is_within_time(self, time_str) -> bool: continue start_time = end_time - timedelta(hours=3) - for candidate in candidates: - # 检查候选时间是否在最近三小时内且不超过当前时间 - if start_time <= candidate <= end_time: - return True - return False + # 检查候选时间是否在最近三小时内且不超过当前时间 + return any(start_time <= candidate <= end_time for candidate in candidates) def __debug(): diff --git a/src/zzz_od/application/notorious_hunt/notorious_hunt_config.py b/src/zzz_od/application/notorious_hunt/notorious_hunt_config.py index 402067faee..ad003a2ac5 100644 --- a/src/zzz_od/application/notorious_hunt/notorious_hunt_config.py +++ b/src/zzz_od/application/notorious_hunt/notorious_hunt_config.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import List, Optional from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.config.yaml_config import YamlConfig @@ -35,7 +34,7 @@ def __init__(self, instance_idx: int, group_id: str): group_id=group_id, ) - self.plan_list: List[ChargePlanItem] = [] + self.plan_list: list[ChargePlanItem] = [] if 'plan_list' in self.data: for plan_item in self.data.get('plan_list', []): @@ -55,7 +54,7 @@ def __init__(self, instance_idx: int, group_id: str): if plan.mission_type_name not in existed_missions: self.plan_list.append(plan) - def _get_default_plan(self) -> List[ChargePlanItem]: + def _get_default_plan(self) -> list[ChargePlanItem]: """ 默认的周本计划 """ @@ -107,6 +106,15 @@ def move_up(self, idx: int) -> None: self.save() + def move_top(self, idx: int) -> None: + """移动到顶部""" + if idx <= 0 or idx >= len(self.plan_list): + return + + plan = self.plan_list.pop(idx) + self.plan_list.insert(0, plan) + self.save() + def reset_plans(self) -> None: if len(self.plan_list) == 0: return @@ -125,7 +133,7 @@ def reset_plans(self) -> None: self.save() - def get_next_plan(self) -> Optional[ChargePlanItem]: + def get_next_plan(self) -> ChargePlanItem | None: if len(self.plan_list) == 0: return None diff --git a/src/zzz_od/application/random_play/random_play_app.py b/src/zzz_od/application/random_play/random_play_app.py index 6213180299..bee68d7245 100644 --- a/src/zzz_od/application/random_play/random_play_app.py +++ b/src/zzz_od/application/random_play/random_play_app.py @@ -1,6 +1,6 @@ import difflib import time -from typing import ClassVar, List, Optional +from typing import ClassVar from cv2.typing import MatLike @@ -9,8 +9,11 @@ from one_dragon.base.operation.application import application_const from one_dragon.base.operation.operation_edge import node_from from one_dragon.base.operation.operation_node import operation_node -from one_dragon.base.operation.operation_notify import node_notify, NotifyTiming -from one_dragon.base.operation.operation_round_result import OperationRoundResult +from one_dragon.base.operation.operation_notify import NotifyTiming, node_notify +from one_dragon.base.operation.operation_round_result import ( + OperationRoundResult, + OperationRoundResultEnum, +) from one_dragon.utils import cv2_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log @@ -50,11 +53,11 @@ def handle_init(self) -> None: 执行前的初始化 由子类实现 注意初始化要全面 方便一个指令重复使用 """ - self._all_video_themes: List[str] = [ + self._all_video_themes: list[str] = [ '纪实', '怀旧', '冒险', '幻想', '喜剧', '动作', '惊悚', '悬疑', '访谈', '都市', '时尚', '灾难', '悲剧', '亲情', '广告', '爱情', ] - self._need_video_themes: List[str] = [] + self._need_video_themes: list[str] = [] self._current_idx: int = 0 @operation_node(name='传送', is_start_node=True) @@ -73,7 +76,6 @@ def move_and_interact(self) -> OperationRoundResult: time.sleep(1) self.ctx.controller.interact(press=True, press_time=0.2, release=True) - time.sleep(5) return self.round_success() @@ -82,11 +84,12 @@ def move_and_interact(self) -> OperationRoundResult: def wait_run(self) -> OperationRoundResult: result = self.round_by_find_area(self.last_screenshot, '影像店营业', '昨日账本') if result.is_success: - return self.round_by_click_area('影像店营业', '返回', - success_wait=1, retry_wait=1) - # 看看经营状况 - return self.round_by_find_area(self.last_screenshot, '影像店营业', '经营状况', - success_wait=1, retry_wait=1) + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '按钮-关闭', + retry_wait=1) + # 看看经营状况,识别到就点击一下,保证在"经营状况"分支 + # 因为二次运行时,有极低概率"无人咨询"变成"咨询中"并被默认跳转 + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '经营状况', + retry_wait=1) @node_from(from_name='等待经营画面加载') @operation_node(name='识别营业状态') @@ -94,7 +97,7 @@ def check_running(self) -> OperationRoundResult: # 防止上一步跳过了昨日账本 result = self.round_by_find_area(self.last_screenshot, '影像店营业', '昨日账本') if result.is_success: - self.round_by_click_area('影像店营业', '返回') + self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '按钮-关闭') return self.round_retry(wait=1) result = self.round_by_find_area(self.last_screenshot, '影像店营业', '正在营业') @@ -103,6 +106,12 @@ def check_running(self) -> OperationRoundResult: else: return self.round_success() + @node_from(from_name='识别营业状态', status=STATUS_ALREADY_RUNNING) + @operation_node(name='关闭经营页面') + def close_business_page(self) -> OperationRoundResult: + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '返回', + retry_wait=1) + @node_from(from_name='识别营业状态') @operation_node(name='点击宣传员入口') def click_promoter_entry(self) -> OperationRoundResult: @@ -111,7 +120,7 @@ def click_promoter_entry(self) -> OperationRoundResult: :return: """ return self.round_by_click_area('影像店营业', '宣传员入口', - success_wait=1, retry_wait=1) + retry_wait=1) @node_from(from_name='点击宣传员入口') @operation_node(name='选择宣传员') @@ -124,53 +133,68 @@ def choose_promoter(self) -> OperationRoundResult: if not result.is_success: return self.round_retry(status=result.status, wait_round_time=1) + # 已经选择过了 直接返回 + result = self.round_by_find_area(self.last_screenshot, '影像店营业', '换下') + if result.is_success: + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '返回') + target_agent_name_1 = self.config.agent_name_1 target_agent_name_2 = self.config.agent_name_2 dt = self.run_record.get_current_dt() idx = (int(dt[-1]) % 2) + 1 - if (RANDOM_AGENT_NAME == target_agent_name_1 - or RANDOM_AGENT_NAME == target_agent_name_2): + if RANDOM_AGENT_NAME in (target_agent_name_1, target_agent_name_2): # 随机选择 - self.round_by_click_area('影像店营业', '宣传员-%d' % idx) - time.sleep(0.5) - - return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '确认', success_wait=1, retry_wait=1) + self.round_by_click_area('影像店营业', f'宣传员-{idx}', pre_delay=1) + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '确认', + retry_wait=1) area = self.ctx.screen_loader.get_area('影像店营业', '宣传员列表') if idx == 1: - target_agent_name = target_agent_name_1 + candidates = [target_agent_name_1, target_agent_name_2] else: - target_agent_name = target_agent_name_2 - - # 使用名称匹配 - result = self.round_by_ocr_and_click(self.last_screenshot, target_agent_name, area=area, + candidates = [target_agent_name_2, target_agent_name_1] + + # 依次尝试匹配每个候选代理人(名称 OCR → 头像匹配) + for agent_name in candidates: + if self._try_select_agent(agent_name, area): + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '确认', + retry_wait=1) + log.info(f'代理人匹配失败: {agent_name}') + + if self.node_retry_times >= 2: + # 滚动多次仍未找到, 兜底选第一个位置 + log.info('滚动多次仍未找到, 选择默认位置') + self.round_by_click_area('影像店营业', '宣传员-1') + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '确认', + retry_wait=1) + # 向下滚动并重试 + log.info('所有候选代理人匹配失败, 向下滚动重试') + self.scroll_area('影像店营业', '宣传员列表') + return self.round_retry(wait=0.5) + + def _try_select_agent(self, agent_name: str, area) -> bool: + """尝试通过名称 OCR 或头像匹配选择代理人""" + result = self.round_by_ocr_and_click(self.last_screenshot, agent_name, area=area, color_range=[(230, 230, 230), (255, 255, 255)]) if result.is_success: - time.sleep(0.5) - return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '确认', success_wait=1, retry_wait=1) + return True - # 使用头像匹配 - mr = self.get_pos_by_avatar(self.last_screenshot, target_agent_name) + mr = self.get_pos_by_avatar(self.last_screenshot, agent_name) if mr is not None: self.ctx.controller.click(mr.center) - time.sleep(0.5) - return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '确认', success_wait=1, retry_wait=1) + return True - # 找不到时 向下滚动 - start_point = area.center - end_point = start_point + Point(0, -100) - self.ctx.controller.drag_to(start=start_point, end=end_point) - return self.round_retry(result.status, wait=0.5) + return False - def get_pos_by_avatar(self, screen: MatLike, target_agent_name: str) -> Optional[MatchResult]: + def get_pos_by_avatar(self, screen: MatLike, target_agent_name: str) -> MatchResult | None: """ 根据头像匹配 @param screen: 游戏画面 @param target_agent_name: 需要选择的代理人名称 @return: """ - agent: Optional[Agent] = None + agent: Agent | None = None for agent_enum in AgentEnum: if agent_enum.value.agent_name == target_agent_name: agent = agent_enum.value @@ -190,7 +214,6 @@ def get_pos_by_avatar(self, screen: MatLike, target_agent_name: str) -> Optional mr.add_offset(area.left_top) return mr - @node_from(from_name='选择宣传员') @operation_node(name='识别录像带主题') def check_video_theme(self) -> OperationRoundResult: @@ -211,7 +234,7 @@ def check_video_theme(self) -> OperationRoundResult: results = difflib.get_close_matches(ocr_result, target_list, n=1) - if results is not None and len(results) > 0: + if results: idx = target_list.index(results[0]) self._need_video_themes.append(self._all_video_themes[idx]) @@ -223,7 +246,7 @@ def check_video_theme(self) -> OperationRoundResult: continue self._need_video_themes.append(theme) - log.info('所需主题 %s' % self._need_video_themes) + log.info(f'所需主题 {self._need_video_themes}') return self.round_success() @node_from(from_name='识别录像带主题') @@ -234,7 +257,7 @@ def click_video_entry(self) -> OperationRoundResult: :return: """ return self.round_by_click_area('影像店营业', '录像带入口', - success_wait=1, retry_wait=1) + retry_wait=1) @node_from(from_name='点击录像带入口') @operation_node(name='识别推荐上架') @@ -284,7 +307,7 @@ def choose_theme(self) -> OperationRoundResult: results = difflib.get_close_matches(ocr_str, target_list, n=1) - if results is None or len(results) == 0: + if not results: continue idx = target_list.index(results[0]) @@ -302,7 +325,7 @@ def choose_theme(self) -> OperationRoundResult: start = area.center end = start + Point(0, -100) self.ctx.controller.drag_to(start=start, end=end) - return self.round_retry(status='未找到%s' % current_target, wait=1) + return self.round_retry(status=f'未找到{current_target}', wait=1) @node_from(from_name='选择主题') @operation_node(name='上架') @@ -321,9 +344,7 @@ def choose_onshelf(self) -> OperationRoundResult: # 这个点击是为了关闭筛选 click1 = self.round_by_click_area('影像店营业', '上架') - time.sleep(0.5) - click2 = self.round_by_click_area('影像店营业', '上架') - time.sleep(0.5) + click2 = self.round_by_click_area('影像店营业', '上架', pre_delay=0.5) if click1.is_success and click2.is_success: return self.round_wait(wait=1) @@ -338,20 +359,29 @@ def back(self) -> OperationRoundResult: if result.is_success: return self.round_success() - return self.round_by_click_area('影像店营业', '返回', success_wait=1, retry_wait=1) + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '返回', + retry_wait=1) @node_from(from_name='返回') @operation_node(name='开始营业') def start(self) -> OperationRoundResult: - return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '开始营业', success_wait=1, retry_wait=1) + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '开始营业', + retry_wait=1) @node_from(from_name='开始营业') - @operation_node(name='开始营业确认') + @operation_node(name='确认营业') + def confirm_business(self) -> OperationRoundResult: + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '开始营业-确认', + retry_wait=1) + + @node_from(from_name='确认营业') + @operation_node(name='营业后确认') def confirm(self) -> OperationRoundResult: - return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '开始营业-确认', success_wait=1, retry_wait=1) + return self.round_by_find_and_click_area(self.last_screenshot, '影像店营业', '营业后确认', + retry_wait=1) - @node_from(from_name='开始营业确认') - @node_from(from_name='识别营业状态', status=STATUS_ALREADY_RUNNING) + @node_from(from_name='营业后确认') + @node_from(from_name='关闭经营页面') @node_notify(when=NotifyTiming.PREVIOUS_DONE) @operation_node(name='返回大世界') def back_to_world(self) -> OperationRoundResult: @@ -361,7 +391,7 @@ def back_to_world(self) -> OperationRoundResult: def __debug(): ctx = ZContext() - ctx.init_by_config() + ctx.init() app = RandomPlayApp(ctx) app.execute() diff --git a/src/zzz_od/application/world_patrol/operation/transport_by_3d_map.py b/src/zzz_od/application/world_patrol/operation/transport_by_3d_map.py index 5a535cd4e9..3bf64027f7 100644 --- a/src/zzz_od/application/world_patrol/operation/transport_by_3d_map.py +++ b/src/zzz_od/application/world_patrol/operation/transport_by_3d_map.py @@ -53,6 +53,8 @@ def open_map(self) -> OperationRoundResult: else: return self.round_retry(status='未发现地图', wait=1) + @node_from(from_name='选择子区域', success=False) # 区域有子区域但找不到 说明选择区域错误 + @node_from(from_name='关闭区域信息弹窗') # 搜索失败 → 关闭弹窗 → 重新选区域 @node_from(from_name='初始回到大世界', status='3D地图') @node_from(from_name='打开地图') @operation_node(name='选择区域', node_max_retry_times=20) @@ -88,26 +90,27 @@ def choose_area(self) -> OperationRoundResult: start_point = area.center end_point = start_point + Point(0, 400 * (-1 if is_target_after else 1)) self.ctx.controller.drag_to(start=start_point, end=end_point) - return self.round_retry() + # 等待滚动动画稳定 避免动画中OCR识别到目标但点击位置偏移 + return self.round_retry(wait=1) @node_from(from_name='选择区域') - @operation_node(name='选择子区域', node_max_retry_times=6) - def choose_sub_area(self) -> OperationRoundResult: + @operation_node(name='展开子区域列表') + def expand_sub_area(self) -> OperationRoundResult: if self.target_area.parent_area is None: return self.round_success(status='无需选择') - self.round_by_click_area('3D地图', '按钮-当前子区域', - success_wait=1) + return self.round_by_click_area('3D地图', '按钮-当前子区域') - self.screenshot() + @node_from(from_name='展开子区域列表') + @operation_node(name='选择子区域', node_max_retry_times=6) + def choose_sub_area(self) -> OperationRoundResult: return self.round_by_ocr_and_click( - screen=self.last_screenshot, - target_cn=self.target_area.area_name, - area=self.ctx.screen_loader.get_area('3D地图', '区域-子区域列表'), - success_wait=1, + self.last_screenshot, self.target_area.area_name, + self.ctx.screen_loader.get_area('3D地图', '区域-子区域列表'), retry_wait=1, ) + @node_from(from_name='展开子区域列表', status='无需选择') @node_from(from_name='选择子区域') @operation_node(name='打开筛选') def open_filter(self) -> OperationRoundResult: @@ -127,10 +130,8 @@ def choose_filter(self) -> OperationRoundResult: target_word = '传送' return self.round_by_ocr_and_click( - screen=self.last_screenshot, - target_cn=target_word, - area=self.ctx.screen_loader.get_area('3D地图', '区域-筛选选项'), - success_wait=1, + self.last_screenshot, target_word, + self.ctx.screen_loader.get_area('3D地图', '区域-筛选选项'), retry_wait=1, ) @@ -182,7 +183,7 @@ def init_tp_search(self) -> OperationRoundResult: return self.round_success() @node_from(from_name='初始化传送点搜索') - @operation_node(name='搜索传送点循环', node_max_retry_times=20) + @operation_node(name='搜索传送点循环', node_max_retry_times=8) def search_tp_icon_loop(self) -> OperationRoundResult: """ 传送点搜索主循环: @@ -334,6 +335,13 @@ def _perform_random_drag(self, map_area: ScreenArea): log.debug(f'执行随机拖动:{direction}') self.ctx.controller.drag_to(start=start_point, end=end_point) + @node_from(from_name='搜索传送点循环', success=False) # 搜索失败后关闭残留弹窗 + @operation_node(name='关闭区域信息弹窗') + def close_area_info_popup(self) -> OperationRoundResult: + """搜索失败后关闭残留的传送点信息弹窗""" + self.round_by_find_and_click_area(self.last_screenshot, '3D地图', '按钮-区域信息-关闭') + return self.round_success() + @node_from(from_name='搜索传送点循环') @operation_node(name='点击前往') def click_go(self) -> OperationRoundResult: @@ -341,14 +349,14 @@ def click_go(self) -> OperationRoundResult: self.last_screenshot, '3D地图', '按钮-前往', until_not_find_all=[('3D地图', '按钮-前往')], - success_wait=1, retry_wait=1, ) @node_from(from_name='点击前往') @operation_node(name='等待画面加载') def back_at_last(self) -> OperationRoundResult: - op = BackToNormalWorld(self.ctx) + # allow_battle=True: 传送落地即进入战斗时直接返回,由调用方(如锄大地)处理战斗 + op = BackToNormalWorld(self.ctx, allow_battle=True) return self.round_by_op_result(op.execute()) def __debug(): diff --git a/src/zzz_od/application/world_patrol/operation/world_patrol_run_route.py b/src/zzz_od/application/world_patrol/operation/world_patrol_run_route.py index 35c5a3eadb..3bb3132e6b 100644 --- a/src/zzz_od/application/world_patrol/operation/world_patrol_run_route.py +++ b/src/zzz_od/application/world_patrol/operation/world_patrol_run_route.py @@ -147,6 +147,7 @@ def __init__( @operation_node(name='初始回到大世界', is_start_node=True) def back_at_first(self) -> OperationRoundResult: + """运行路线前:确保当前在大世界画面,再进行后续传送""" if self.current_idx != 0: return self.round_success(status='DEBUG') @@ -156,6 +157,7 @@ def back_at_first(self) -> OperationRoundResult: @node_from(from_name='初始回到大世界') @operation_node(name='传送') def transport(self) -> OperationRoundResult: + """传送到目标点:内部最后一步(等待画面加载)也会调用 BackToNormalWorld 等待传送加载完成""" op = TransportBy3dMap(self.ctx, self.route.tp_area, self.route.tp_name) return self.round_by_op_result(op.execute()) diff --git a/src/zzz_od/application/world_patrol/world_patrol_app.py b/src/zzz_od/application/world_patrol/world_patrol_app.py index d941151cc3..aec8d17270 100644 --- a/src/zzz_od/application/world_patrol/world_patrol_app.py +++ b/src/zzz_od/application/world_patrol/world_patrol_app.py @@ -14,7 +14,6 @@ from zzz_od.application.zzz_application import ZApplication from zzz_od.context.zzz_context import ZContext from zzz_od.operation.back_to_normal_world import BackToNormalWorld -from zzz_od.operation.goto.goto_menu import GotoMenu class WorldPatrolApp(ZApplication): @@ -79,21 +78,29 @@ def back_at_first(self) -> OperationRoundResult: return self.round_by_op_result(op.execute()) @node_from(from_name='开始前返回大世界') - @operation_node(name='打开菜单') - def open_menu(self) -> OperationRoundResult: - op = GotoMenu(self.ctx) - return self.round_by_op_result(op.execute()) - - @node_from(from_name='打开菜单') @operation_node(name='前往绳网') def goto_inter_knot(self) -> OperationRoundResult: - return self.round_by_goto_screen(screen_name='绳网', success_wait=1, retry_wait=1) + # 无任务追踪 → 跳过 + result = self.round_by_find_area(self.last_screenshot, '大世界', '任务追踪') + if result.is_success: + return self.round_success(status='无任务追踪') + + # 有任务追踪 + return self.round_by_goto_screen(screen_name='绳网', retry_wait=1) @node_from(from_name='前往绳网') @operation_node(name='停止追踪') def stop_tracking(self) -> OperationRoundResult: - return self.round_by_find_and_click_area(screen_name='绳网', area_name='按钮-停止追踪', - success_wait=1, retry_wait=1) + # 找到"停止追踪" → 点击 → 成功 → 返回大世界 + click_result = self.round_by_find_and_click_area(self.last_screenshot, '绳网', '按钮-停止追踪') + if click_result.is_success: + return click_result + # 没找到"停止追踪"但找到"追踪" → 直接成功跳过 → 返回大世界 + find_result = self.round_by_find_area(self.last_screenshot, '绳网', '按钮-追踪') + if find_result.is_success: + return self.round_success(status='无需停止追踪') + # 都没找到(不可能) → retry 几次 → 失败 → 仍然返回大世界 + return self.round_retry(status='未找到追踪按钮', wait=1) @node_from(from_name='停止追踪') @node_from(from_name='停止追踪', success=False) @@ -102,6 +109,7 @@ def back_after_stop_tracking(self) -> OperationRoundResult: op = BackToNormalWorld(self.ctx) return self.round_by_op_result(op.execute()) + @node_from(from_name='前往绳网', status='无任务追踪') @node_from(from_name='停止追踪后返回大世界') @node_notify(when=NotifyTiming.CURRENT_DONE, detail=True) @operation_node(name='执行路线') diff --git a/src/zzz_od/application/world_patrol/world_patrol_service.py b/src/zzz_od/application/world_patrol/world_patrol_service.py index 7ac28e0a50..2690e6247d 100644 --- a/src/zzz_od/application/world_patrol/world_patrol_service.py +++ b/src/zzz_od/application/world_patrol/world_patrol_service.py @@ -12,7 +12,7 @@ from one_dragon.base.geometry.rectangle import Rect from one_dragon.base.matcher.match_result import MatchResult from one_dragon.base.screen.screen_utils import find_template_coord_in_area -from one_dragon.utils import os_utils, cv2_utils, cal_utils +from one_dragon.utils import os_utils, cv2_utils, cal_utils, yaml_utils from one_dragon.utils.log_utils import log from zzz_od.application.world_patrol.mini_map_wrapper import MiniMapWrapper from zzz_od.application.world_patrol.world_patrol_area import WorldPatrolArea, WorldPatrolEntry, WorldPatrolLargeMap, \ @@ -258,7 +258,7 @@ def get_world_patrol_routes_by_area(self, area: WorldPatrolArea) -> list[WorldPa try: file_path = os.path.join(route_dir, filename) with open(file_path, 'r', encoding='utf-8') as f: - data = yaml.safe_load(f) + data = yaml_utils.safe_load(f) route = WorldPatrolRoute.from_dict(data, area) routes.append(route) except Exception as e: @@ -325,7 +325,7 @@ def get_world_patrol_route_lists(self) -> list[WorldPatrolRouteList]: try: file_path = os.path.join(list_dir, filename) with open(file_path, 'r', encoding='utf-8') as f: - data = yaml.safe_load(f) + data = yaml_utils.safe_load(f) route_list = WorldPatrolRouteList.from_dict(data) route_lists.append(route_list) except Exception as e: diff --git a/src/zzz_od/application/zzz_application_launcher.py b/src/zzz_od/application/zzz_application_launcher.py index 09a29a1105..20e17624a2 100644 --- a/src/zzz_od/application/zzz_application_launcher.py +++ b/src/zzz_od/application/zzz_application_launcher.py @@ -1,3 +1,5 @@ +import sys + from one_dragon.launcher.application_launcher import ApplicationLauncher from zzz_od.context.zzz_context import ZContext @@ -12,6 +14,12 @@ def create_context(self): return ZContext() -if __name__ == '__main__': +def main(args: list[str] | None = None) -> None: + if args is not None: + sys.argv = [sys.argv[0]] + args launcher = ZApplicationLauncher() launcher.run() + + +if __name__ == '__main__': + main() diff --git a/src/zzz_od/auto_battle/auto_battle_operator.py b/src/zzz_od/auto_battle/auto_battle_operator.py index a5717a6b7e..6dd3d8eb70 100644 --- a/src/zzz_od/auto_battle/auto_battle_operator.py +++ b/src/zzz_od/auto_battle/auto_battle_operator.py @@ -79,6 +79,7 @@ def __init__( # 停止事件 self._stop_event = Event() + self._periodic_generation: int = 0 # 会话代际计数器 def load_other_info(self, data: dict[str, Any]) -> None: """ @@ -113,8 +114,8 @@ def init_before_running(self) -> tuple[bool, str]: ConditionalOperator.init(self) log.info(f'自动战斗配置加载成功 {self.get_template_name()}') return True, '' - except Exception as e: - log.error('自动战斗初始化失败 共享配队文件请在群内提醒对应作者修复', exc_info=True) + except Exception: + log.error('自动战斗初始化失败 如果是共享配队文件请在群内提醒对应作者修复', exc_info=True) return False, '初始化失败' def get_atomic_op(self, op_def: OperationDef) -> AtomicOp: @@ -139,12 +140,15 @@ def dispose(self) -> None: def start_running_async(self) -> bool: success = ConditionalOperator.start_running_async(self) if success: - lock_f = _auto_battle_operator_executor.submit(self.operate_periodically) + self._periodic_generation += 1 + self._stop_event.clear() + gen = self._periodic_generation + lock_f = _auto_battle_operator_executor.submit(self.operate_periodically, gen) lock_f.add_done_callback(thread_utils.handle_future_result) return success - def operate_periodically(self) -> None: + def operate_periodically(self, generation: int) -> None: """ 周期性完成动作 @@ -154,10 +158,9 @@ def operate_periodically(self) -> None: """ if self.auto_lock_interval <= 0 and self.auto_turn_interval <= 0: # 不开启自动锁定 和 自动转向 return - self._stop_event.clear() lock_op = AtomicBtnLock(self.ctx) turn_op = AtomicTurn(self.ctx, 100) - while self.is_running: + while self.is_running and self._periodic_generation == generation: now = time.time() if not self.ctx.last_check_in_battle: # 当前画面不是战斗画面 就不运行了 @@ -188,7 +191,7 @@ def stop_running(self) -> None: 停止执行 """ self._stop_event.set() - super().stop_running() + ConditionalOperator.stop_running(self) @staticmethod def after_app_shutdown() -> None: diff --git a/src/zzz_od/config/game_config.py b/src/zzz_od/config/game_config.py index 780598ee76..2f67fec355 100644 --- a/src/zzz_od/config/game_config.py +++ b/src/zzz_od/config/game_config.py @@ -6,144 +6,212 @@ from one_dragon.base.controller.pc_button.xbox_button_controller import XboxButtonEnum -class GamepadTypeEnum(Enum): +class ControlMethodEnum(Enum): - NONE = ConfigItem('无', 'none') + KEYBOARD = ConfigItem('键鼠', 'keyboard') XBOX = ConfigItem('Xbox', 'xbox') DS4 = ConfigItem('DS4', 'ds4') -class GameConfig(BasicGameConfig): - - @property - def key_normal_attack(self) -> str: - return self.get('key_normal_attack', 'mouse_left') - - @key_normal_attack.setter - def key_normal_attack(self, new_value: str) -> None: - self.update('key_normal_attack', new_value) - - @property - def key_dodge(self) -> str: - return self.get('key_dodge', 'shift') - - @key_dodge.setter - def key_dodge(self, new_value: str) -> None: - self.update('key_dodge', new_value) - - @property - def key_switch_next(self) -> str: - return self.get('key_switch_next', 'space') - - @key_switch_next.setter - def key_switch_next(self, new_value: str) -> None: - self.update('key_switch_next', new_value) - - @property - def key_switch_prev(self) -> str: - return self.get('key_switch_prev', 'c') - - @key_switch_prev.setter - def key_switch_prev(self, new_value: str) -> None: - self.update('key_switch_prev', new_value) - - @property - def key_special_attack(self) -> str: - return self.get('key_special_attack', 'e') - - @key_special_attack.setter - def key_special_attack(self, new_value: str) -> None: - self.update('key_special_attack', new_value) - - @property - def key_ultimate(self) -> str: - """爆发技""" - return self.get('key_ultimate', 'q') - - @key_ultimate.setter - def key_ultimate(self, new_value: str) -> None: - self.update('key_ultimate', new_value) - - @property - def key_interact(self) -> str: - """交互""" - return self.get('key_interact', 'f') - - @key_interact.setter - def key_interact(self, new_value: str) -> None: - self.update('key_interact', new_value) - - @property - def key_chain_left(self) -> str: - return self.get('key_chain_left', 'q') - - @key_chain_left.setter - def key_chain_left(self, new_value: str) -> None: - self.update('key_chain_left', new_value) - - @property - def key_chain_right(self) -> str: - return self.get('key_chain_right', 'e') - - @key_chain_right.setter - def key_chain_right(self, new_value: str) -> None: - self.update('key_chain_right', new_value) - - @property - def key_move_w(self) -> str: - return self.get('key_move_w', 'w') - - @key_move_w.setter - def key_move_w(self, new_value: str) -> None: - self.update('key_move_w', new_value) - - @property - def key_move_s(self) -> str: - return self.get('key_move_s', 's') - - @key_move_s.setter - def key_move_s(self, new_value: str) -> None: - self.update('key_move_s', new_value) - - @property - def key_move_a(self) -> str: - return self.get('key_move_a', 'a') - - @key_move_a.setter - def key_move_a(self, new_value: str) -> None: - self.update('key_move_a', new_value) - - @property - def key_move_d(self) -> str: - return self.get('key_move_d', 'd') - - @key_move_d.setter - def key_move_d(self, new_value: str) -> None: - self.update('key_move_d', new_value) - - @property - def key_lock(self) -> str: - return self.get('key_lock', 'mouse_middle') - - @key_lock.setter - def key_lock(self, new_value: str) -> None: - self.update('key_lock', new_value) +class GamepadTypeEnum(Enum): - @property - def key_chain_cancel(self) -> str: - return self.get('key_chain_cancel', 'mouse_middle') + XBOX = ConfigItem('Xbox', 'xbox') + DS4 = ConfigItem('DS4', 'ds4') - @key_chain_cancel.setter - def key_chain_cancel(self, new_value: str) -> None: - self.update('key_chain_cancel', new_value) - @property - def gamepad_type(self) -> str: - return self.get('gamepad_type', GamepadTypeEnum.NONE.value.value) +class GamepadActionEnum(Enum): + """后台模式下用手柄按键替代点击的逻辑动作。 + + value 是 ConfigItem(显示名, 存储值)。 + screen 区域的 gamepad_key 引用存储值。 + """ + + MENU = ConfigItem('菜单', 'menu') + MAP = ConfigItem('地图', 'map') + MINIMAP = ConfigItem('小地图', 'minimap') + COMPENDIUM = ConfigItem('快捷手册', 'compendium') + GUIDE = ConfigItem('功能导览', 'function_menu') + + +class GameKeyAction(Enum): + """游戏按键动作""" + + INTERACT = ConfigItem('交互', 'interact') + NORMAL_ATTACK = ConfigItem('普通攻击', 'normal_attack') + DODGE = ConfigItem('闪避', 'dodge') + SWITCH_NEXT = ConfigItem('角色切换-下一个', 'switch_next') + SWITCH_PREV = ConfigItem('角色切换-上一个', 'switch_prev') + SPECIAL_ATTACK = ConfigItem('特殊攻击', 'special_attack') + ULTIMATE = ConfigItem('终结技', 'ultimate') + CHAIN_LEFT = ConfigItem('连携技-左', 'chain_left') + CHAIN_RIGHT = ConfigItem('连携技-右', 'chain_right') + MOVE_W = ConfigItem('移动-前', 'move_w') + MOVE_S = ConfigItem('移动-后', 'move_s') + MOVE_A = ConfigItem('移动-左', 'move_a') + MOVE_D = ConfigItem('移动-右', 'move_d') + LOCK = ConfigItem('锁定敌人', 'lock') + CHAIN_CANCEL = ConfigItem('连携技-取消', 'chain_cancel') + + +# 按键默认值:{prefix: {action_value: default}} +_KEY_DEFAULTS: dict[str, dict[str, str]] = { + 'key': { + 'interact': 'f', + 'normal_attack': 'mouse_left', + 'dodge': 'shift', + 'switch_next': 'space', + 'switch_prev': 'c', + 'special_attack': 'e', + 'ultimate': 'q', + 'chain_left': 'q', + 'chain_right': 'e', + 'move_w': 'w', + 'move_s': 's', + 'move_a': 'a', + 'move_d': 'd', + 'lock': 'mouse_middle', + 'chain_cancel': 'mouse_middle', + }, + 'xbox_key': { + 'interact': XboxButtonEnum.A.value.value, + 'normal_attack': XboxButtonEnum.X.value.value, + 'dodge': XboxButtonEnum.A.value.value, + 'switch_next': XboxButtonEnum.RB.value.value, + 'switch_prev': XboxButtonEnum.LB.value.value, + 'special_attack': XboxButtonEnum.Y.value.value, + 'ultimate': XboxButtonEnum.RT.value.value, + 'chain_left': XboxButtonEnum.LB.value.value, + 'chain_right': XboxButtonEnum.RB.value.value, + 'move_w': XboxButtonEnum.L_STICK_W.value.value, + 'move_s': XboxButtonEnum.L_STICK_S.value.value, + 'move_a': XboxButtonEnum.L_STICK_A.value.value, + 'move_d': XboxButtonEnum.L_STICK_D.value.value, + 'lock': XboxButtonEnum.R_THUMB.value.value, + 'chain_cancel': XboxButtonEnum.A.value.value, + }, + 'ds4_key': { + 'interact': Ds4ButtonEnum.CROSS.value.value, + 'normal_attack': Ds4ButtonEnum.SQUARE.value.value, + 'dodge': Ds4ButtonEnum.CROSS.value.value, + 'switch_next': Ds4ButtonEnum.R1.value.value, + 'switch_prev': Ds4ButtonEnum.L1.value.value, + 'special_attack': Ds4ButtonEnum.TRIANGLE.value.value, + 'ultimate': Ds4ButtonEnum.R2.value.value, + 'chain_left': Ds4ButtonEnum.L1.value.value, + 'chain_right': Ds4ButtonEnum.R1.value.value, + 'move_w': Ds4ButtonEnum.L_STICK_W.value.value, + 'move_s': Ds4ButtonEnum.L_STICK_S.value.value, + 'move_a': Ds4ButtonEnum.L_STICK_A.value.value, + 'move_d': Ds4ButtonEnum.L_STICK_D.value.value, + 'lock': Ds4ButtonEnum.R_THUMB.value.value, + 'chain_cancel': Ds4ButtonEnum.CROSS.value.value, + }, +} + +# 后台模式手柄动作键默认值:{prefix: {action_value: default}} +_ACTION_KEY_DEFAULTS: dict[str, dict[str, list[str]]] = { + 'xbox_action': { + 'menu': [XboxButtonEnum.START.value.value], + 'map': [XboxButtonEnum.DPAD_RIGHT.value.value], + 'minimap': [XboxButtonEnum.BACK.value.value], + 'compendium': [XboxButtonEnum.LT.value.value, XboxButtonEnum.A.value.value], + 'function_menu': [XboxButtonEnum.LT.value.value, XboxButtonEnum.START.value.value], + }, + 'ds4_action': { + 'menu': [Ds4ButtonEnum.OPTIONS.value.value], + 'map': [Ds4ButtonEnum.DPAD_RIGHT.value.value], + 'minimap': [Ds4ButtonEnum.TOUCHPAD.value.value], + 'compendium': [Ds4ButtonEnum.L2.value.value, Ds4ButtonEnum.CROSS.value.value], + 'function_menu': [Ds4ButtonEnum.L2.value.value, Ds4ButtonEnum.OPTIONS.value.value], + }, +} + + +def _with_key_properties(cls): + """根据 _KEY_DEFAULTS 和 _ACTION_KEY_DEFAULTS 动态生成按键 property""" + + def _create_getter(name: str, default_value: str): + def getter(self) -> str: + return self.get(name, default_value) + return getter + + def _create_setter(name: str): + def setter(self, new_value: str) -> None: + self.update(name, new_value) + return setter + + for prefix, defaults in _KEY_DEFAULTS.items(): + for action in GameKeyAction: + prop_name = f'{prefix}_{action.value.value}' + default = defaults[action.value.value] + prop = property(_create_getter(prop_name, default), _create_setter(prop_name)) + setattr(cls, prop_name, prop) + + for prefix, defaults in _ACTION_KEY_DEFAULTS.items(): + for action in GamepadActionEnum: + prop_name = f'{prefix}_{action.value.value}' + default = defaults[action.value.value] + prop = property(_create_getter(prop_name, default), _create_setter(prop_name)) + setattr(cls, prop_name, prop) + + return cls + + +@_with_key_properties +class GameConfig(BasicGameConfig): - @gamepad_type.setter - def gamepad_type(self, new_value: str) -> None: - self.update('gamepad_type', new_value) + # 旧数字索引 → 新描述性键名映射(兼容旧版配置) + _LEGACY_GAMEPAD_KEYS: dict[str, str] = { + **{f'xbox_{i}': k for i, k in enumerate([ + 'xbox_a', 'xbox_b', 'xbox_x', 'xbox_y', + 'xbox_lt', 'xbox_rt', 'xbox_lb', 'xbox_rb', + 'xbox_ls_up', 'xbox_ls_down', 'xbox_ls_left', 'xbox_ls_right', + 'xbox_l_thumb', 'xbox_r_thumb', + ])}, + **{f'ds4_{i}': k for i, k in enumerate([ + 'ds4_cross', 'ds4_circle', 'ds4_square', 'ds4_triangle', + 'ds4_l2', 'ds4_r2', 'ds4_l1', 'ds4_r1', + 'ds4_ls_up', 'ds4_ls_down', 'ds4_ls_left', 'ds4_ls_right', + 'ds4_l_thumb', 'ds4_r_thumb', + ])}, + } + + def __init__(self, instance_idx: int): + BasicGameConfig.__init__(self, instance_idx) + # TODO 迁移旧配置 2026-9 删除 + self._migrate_legacy_keys() + self._migrate_legacy_gamepad_keys() + + def _migrate_legacy_keys(self) -> None: + """迁移旧键名到新键名。""" + _RENAMES = {'gamepad_type': 'control_method'} + for old_key, new_key in _RENAMES.items(): + old_val = self.get(old_key) + if old_val is not None and self.get(new_key) is None: + self.update(new_key, old_val) + self.update(old_key, None) + + def _migrate_legacy_gamepad_keys(self) -> None: + """初始化时一次性迁移所有旧数字格式的手柄按键配置。""" + for prefix in ('xbox_key', 'ds4_key'): + for action in GameKeyAction: + prop = f'{prefix}_{action.value.value}' + value = self.get(prop, '') + if not value: + continue + migrated = '+'.join( + self._LEGACY_GAMEPAD_KEYS.get(p, p) for p in value.split('+') + ) + if migrated != value: + self.update(prop, migrated) + + @property + def control_method(self) -> str: + return self.get('control_method', ControlMethodEnum.KEYBOARD.value.value) + + @control_method.setter + def control_method(self, new_value: str) -> None: + self.update('control_method', new_value) @property def xbox_key_press_time(self) -> float: @@ -153,128 +221,6 @@ def xbox_key_press_time(self) -> float: def xbox_key_press_time(self, new_value: float) -> None: self.update('xbox_key_press_time', new_value) - @property - def xbox_key_normal_attack(self) -> str: - return self.get('xbox_key_normal_attack', XboxButtonEnum.X.value.value) - - @xbox_key_normal_attack.setter - def xbox_key_normal_attack(self, new_value: str) -> None: - self.update('xbox_key_normal_attack', new_value) - - @property - def xbox_key_dodge(self) -> str: - return self.get('xbox_key_dodge', XboxButtonEnum.A.value.value) - - @xbox_key_dodge.setter - def xbox_key_dodge(self, new_value: str) -> None: - self.update('xbox_key_dodge', new_value) - - @property - def xbox_key_switch_next(self) -> str: - return self.get('xbox_key_switch_next', XboxButtonEnum.RB.value.value) - - @xbox_key_switch_next.setter - def xbox_key_switch_next(self, new_value: str) -> None: - self.update('xbox_key_switch_next', new_value) - - @property - def xbox_key_switch_prev(self) -> str: - return self.get('xbox_key_switch_prev', XboxButtonEnum.LB.value.value) - - @xbox_key_switch_prev.setter - def xbox_key_switch_prev(self, new_value: str) -> None: - self.update('xbox_key_switch_prev', new_value) - - @property - def xbox_key_special_attack(self) -> str: - return self.get('xbox_key_special_attack', XboxButtonEnum.Y.value.value) - - @xbox_key_special_attack.setter - def xbox_key_special_attack(self, new_value: str) -> None: - self.update('xbox_key_special_attack', new_value) - - @property - def xbox_key_ultimate(self) -> str: - """爆发技""" - return self.get('xbox_key_ultimate', XboxButtonEnum.RT.value.value) - - @xbox_key_ultimate.setter - def xbox_key_ultimate(self, new_value: str) -> None: - self.update('xbox_key_ultimate', new_value) - - @property - def xbox_key_interact(self) -> str: - """交互""" - return self.get('xbox_key_interact', XboxButtonEnum.A.value.value) - - @xbox_key_interact.setter - def xbox_key_interact(self, new_value: str) -> None: - self.update('xbox_key_interact', new_value) - - @property - def xbox_key_chain_left(self) -> str: - return self.get('xbox_key_chain_left', XboxButtonEnum.LB.value.value) - - @xbox_key_chain_left.setter - def xbox_key_chain_left(self, new_value: str) -> None: - self.update('xbox_key_chain_left', new_value) - - @property - def xbox_key_chain_right(self) -> str: - return self.get('xbox_key_chain_right', XboxButtonEnum.RB.value.value) - - @xbox_key_chain_right.setter - def xbox_key_chain_right(self, new_value: str) -> None: - self.update('xbox_key_chain_right', new_value) - - @property - def xbox_key_move_w(self) -> str: - return self.get('xbox_key_move_w', XboxButtonEnum.L_STICK_W.value.value) - - @xbox_key_move_w.setter - def xbox_key_move_w(self, new_value: str) -> None: - self.update('xbox_key_move_w', new_value) - - @property - def xbox_key_move_s(self) -> str: - return self.get('xbox_key_move_s', XboxButtonEnum.L_STICK_S.value.value) - - @xbox_key_move_s.setter - def xbox_key_move_s(self, new_value: str) -> None: - self.update('xbox_key_move_s', new_value) - - @property - def xbox_key_move_a(self) -> str: - return self.get('xbox_key_move_a', XboxButtonEnum.L_STICK_A.value.value) - - @xbox_key_move_a.setter - def xbox_key_move_a(self, new_value: str) -> None: - self.update('xbox_key_move_a', new_value) - - @property - def xbox_key_move_d(self) -> str: - return self.get('xbox_key_move_d', XboxButtonEnum.L_STICK_D.value.value) - - @xbox_key_move_d.setter - def xbox_key_move_d(self, new_value: str) -> None: - self.update('xbox_key_move_d', new_value) - - @property - def xbox_key_lock(self) -> str: - return self.get('xbox_key_lock', XboxButtonEnum.R_THUMB.value.value) - - @xbox_key_lock.setter - def xbox_key_lock(self, new_value: str) -> None: - self.update('xbox_key_lock', new_value) - - @property - def xbox_key_chain_cancel(self) -> str: - return self.get('xbox_key_chain_cancel', XboxButtonEnum.A.value.value) - - @xbox_key_chain_cancel.setter - def xbox_key_chain_cancel(self, new_value: str) -> None: - self.update('xbox_key_chain_cancel', new_value) - @property def ds4_key_press_time(self) -> float: return self.get('ds4_key_press_time', 0.02) @@ -284,126 +230,67 @@ def ds4_key_press_time(self, new_value: float) -> None: self.update('ds4_key_press_time', new_value) @property - def ds4_key_normal_attack(self) -> str: - return self.get('ds4_key_normal_attack', Ds4ButtonEnum.SQUARE.value.value) - - @ds4_key_normal_attack.setter - def ds4_key_normal_attack(self, new_value: str) -> None: - self.update('ds4_key_normal_attack', new_value) - - @property - def ds4_key_dodge(self) -> str: - return self.get('ds4_key_dodge', Ds4ButtonEnum.CROSS.value.value) - - @ds4_key_dodge.setter - def ds4_key_dodge(self, new_value: str) -> None: - self.update('ds4_key_dodge', new_value) - - @property - def ds4_key_switch_next(self) -> str: - return self.get('ds4_key_switch_next', Ds4ButtonEnum.R1.value.value) - - @ds4_key_switch_next.setter - def ds4_key_switch_next(self, new_value: str) -> None: - self.update('ds4_key_switch_next', new_value) - - @property - def ds4_key_switch_prev(self) -> str: - return self.get('ds4_key_switch_prev', Ds4ButtonEnum.L1.value.value) + def background_mode(self) -> bool: + return self.get('background_mode', False) - @ds4_key_switch_prev.setter - def ds4_key_switch_prev(self, new_value: str) -> None: - self.update('ds4_key_switch_prev', new_value) + @background_mode.setter + def background_mode(self, new_value: bool) -> None: + self.update('background_mode', new_value) @property - def ds4_key_special_attack(self) -> str: - return self.get('ds4_key_special_attack', Ds4ButtonEnum.TRIANGLE.value.value) + def background_gamepad_type(self) -> str: + return self.get('background_gamepad_type', GamepadTypeEnum.XBOX.value.value) - @ds4_key_special_attack.setter - def ds4_key_special_attack(self, new_value: str) -> None: - self.update('ds4_key_special_attack', new_value) + @background_gamepad_type.setter + def background_gamepad_type(self, new_value: str) -> None: + self.update('background_gamepad_type', new_value) @property - def ds4_key_ultimate(self) -> str: - """爆发技""" - return self.get('ds4_key_ultimate', Ds4ButtonEnum.R2.value.value) + def mouse_flash_duration(self) -> float: + """后台模式闪切键鼠模式时每步等待时长(秒)""" + return self.get('mouse_flash_duration', 0.05) - @ds4_key_ultimate.setter - def ds4_key_ultimate(self, new_value: str) -> None: - self.update('ds4_key_ultimate', new_value) + @mouse_flash_duration.setter + def mouse_flash_duration(self, new_value: float) -> None: + self.update('mouse_flash_duration', new_value) - @property - def ds4_key_interact(self) -> str: - """交互""" - return self.get('ds4_key_interact', Ds4ButtonEnum.CROSS.value.value) - - @ds4_key_interact.setter - def ds4_key_interact(self, new_value: str) -> None: - self.update('ds4_key_interact', new_value) - - @property - def ds4_key_chain_left(self) -> str: - return self.get('ds4_key_chain_left', Ds4ButtonEnum.L1.value.value) + def get_action_keys(self, control_method: str) -> dict[str, str]: + """获取指定控制方式的所有按键映射。 - @ds4_key_chain_left.setter - def ds4_key_chain_left(self, new_value: str) -> None: - self.update('ds4_key_chain_left', new_value) - - @property - def ds4_key_chain_right(self) -> str: - return self.get('ds4_key_chain_right', Ds4ButtonEnum.R1.value.value) - - @ds4_key_chain_right.setter - def ds4_key_chain_right(self, new_value: str) -> None: - self.update('ds4_key_chain_right', new_value) - - @property - def ds4_key_move_w(self) -> str: - return self.get('ds4_key_move_w', Ds4ButtonEnum.L_STICK_W.value.value) - - @ds4_key_move_w.setter - def ds4_key_move_w(self, new_value: str) -> None: - self.update('ds4_key_move_w', new_value) - - @property - def ds4_key_move_s(self) -> str: - return self.get('ds4_key_move_s', Ds4ButtonEnum.L_STICK_S.value.value) + Args: + control_method: ControlMethodEnum 的值,如 'keyboard' / 'xbox' / 'ds4'。 - @ds4_key_move_s.setter - def ds4_key_move_s(self, new_value: str) -> None: - self.update('ds4_key_move_s', new_value) - - @property - def ds4_key_move_a(self) -> str: - return self.get('ds4_key_move_a', Ds4ButtonEnum.L_STICK_A.value.value) - - @ds4_key_move_a.setter - def ds4_key_move_a(self, new_value: str) -> None: - self.update('ds4_key_move_a', new_value) - - @property - def ds4_key_move_d(self) -> str: - return self.get('ds4_key_move_d', Ds4ButtonEnum.L_STICK_D.value.value) - - @ds4_key_move_d.setter - def ds4_key_move_d(self, new_value: str) -> None: - self.update('ds4_key_move_d', new_value) - - @property - def ds4_key_lock(self) -> str: - return self.get('ds4_key_lock', Ds4ButtonEnum.R_THUMB.value.value) + Returns: + {action_name: key_value},如 {'dodge': 'shift', 'interact': 'f', ...} + """ + prefix = 'key' if control_method == 'keyboard' else f'{control_method}_key' + return { + action.value.value: getattr(self, f'{prefix}_{action.value.value}') + for action in GameKeyAction + } - @ds4_key_lock.setter - def ds4_key_lock(self, new_value: str) -> None: - self.update('ds4_key_lock', new_value) + def get_gamepad_action_keys(self, gamepad_type: str | None = None) -> dict[str, list[str]]: + """获取指定手柄类型的后台模式动作 → 实际按键映射。 - @property - def ds4_key_chain_cancel(self) -> str: - return self.get('ds4_key_chain_cancel', Ds4ButtonEnum.CROSS.value.value) + Args: + gamepad_type: GamepadTypeEnum 的值,如 'xbox' / 'ds4'。 + 为 None 时使用当前配置的 background_gamepad_type。 - @ds4_key_chain_cancel.setter - def ds4_key_chain_cancel(self, new_value: str) -> None: - self.update('ds4_key_chain_cancel', new_value) + Returns: + {action_name: [key, ...]} + """ + if gamepad_type is None: + gamepad_type = self.background_gamepad_type + result: dict[str, list[str]] = {} + for action in GamepadActionEnum: + action_name: str = action.value.value + if not action_name: + continue + prop_name = f'{gamepad_type}_action_{action_name}' + value = getattr(self, prop_name, []) + if value: + result[action_name] = value + return result @property def original_hdr_value(self) -> str: @@ -415,12 +302,18 @@ def original_hdr_value(self, new_value: str) -> None: @property def turn_dx(self) -> float: - """ - 转向时 每度所需要移动的像素距离 - :return: - """ + """转向时 每度所需要移动的像素距离。""" return self.get('turn_dx', 0) @turn_dx.setter def turn_dx(self, new_value: float): self.update('turn_dx', new_value) + + @property + def gamepad_turn_speed(self) -> float: + """后台手柄模式下,右摇杆满偏转对应的 每秒等效鼠标像素距离。""" + return self.get('gamepad_turn_speed', 1000) + + @gamepad_turn_speed.setter + def gamepad_turn_speed(self, new_value: float): + self.update('gamepad_turn_speed', new_value) diff --git a/src/zzz_od/context/zzz_context.py b/src/zzz_od/context/zzz_context.py index 0d114b73a3..db263b76b0 100644 --- a/src/zzz_od/context/zzz_context.py +++ b/src/zzz_od/context/zzz_context.py @@ -134,13 +134,15 @@ def after_app_shutdown(self) -> None: if hasattr(self, 'telemetry') and self.telemetry: self.telemetry.shutdown() - OneDragonContext.after_app_shutdown(self) + # 上层清理依赖框架服务(如 StateRecordService),必须先于框架清理 self.withered_domain.after_app_shutdown() self.auto_battle_context.after_app_shutdown() from zzz_od.auto_battle.auto_battle_operator import AutoBattleOperator AutoBattleOperator.after_app_shutdown() + OneDragonContext.after_app_shutdown(self) + @cached_property def shared_dialog_manager(self): """ diff --git a/src/zzz_od/controller/zzz_pc_controller.py b/src/zzz_od/controller/zzz_pc_controller.py index 6ef61ffd4c..d20997b12b 100644 --- a/src/zzz_od/controller/zzz_pc_controller.py +++ b/src/zzz_od/controller/zzz_pc_controller.py @@ -1,5 +1,5 @@ import ctypes -from typing import Optional +import time from cv2.typing import MatLike @@ -25,24 +25,24 @@ def __init__( standard_height=standard_height) self.game_config: GameConfig = game_config - self.key_dodge: str = self.game_config.key_dodge - self.key_switch_next: str = self.game_config.key_switch_next - self.key_switch_prev: str = self.game_config.key_switch_prev - self.key_normal_attack: str = self.game_config.key_normal_attack - self.key_special_attack: str = self.game_config.key_special_attack - self.key_ultimate: str = self.game_config.key_ultimate - self.key_chain_left: str = self.game_config.key_chain_left - self.key_chain_right: str = self.game_config.key_chain_right - self.key_move_w: str = self.game_config.key_move_w - self.key_move_s: str = self.game_config.key_move_s - self.key_move_a: str = self.game_config.key_move_a - self.key_move_d: str = self.game_config.key_move_d - self.key_interact: str = self.game_config.key_interact - self.key_lock: str = self.game_config.key_lock - self.key_chain_cancel: str = self.game_config.key_chain_cancel + self.action_keys = self.game_config.get_action_keys('keyboard') + self.gamepad_action_keys = self.game_config.get_gamepad_action_keys() + self.mouse_flash_duration: float = game_config.mouse_flash_duration self.is_moving: bool = False # 是否正在移动 self.turn_dx: float = game_config.turn_dx + self.gamepad_turn_speed: float = game_config.gamepad_turn_speed + + def init_before_context_run(self) -> bool: + """运行前根据配置启用后台/前台模式,刷新快照配置""" + if self.game_config.background_mode: + self.enable_background_mode(self.game_config.background_gamepad_type) + else: + self.enable_foreground_mode() + self.turn_dx = self.game_config.turn_dx + self.gamepad_turn_speed = self.game_config.gamepad_turn_speed + self.mouse_flash_duration = self.game_config.mouse_flash_duration + return PcControllerBase.init_before_context_run(self) def fill_uid_black(self, screen: MatLike) -> MatLike: """ @@ -59,215 +59,111 @@ def fill_uid_black(self, screen: MatLike) -> MatLike: def enable_keyboard(self): PcControllerBase.enable_keyboard(self) - - self.key_dodge = self.game_config.key_dodge - self.key_switch_next = self.game_config.key_switch_next - self.key_switch_prev = self.game_config.key_switch_prev - self.key_normal_attack = self.game_config.key_normal_attack - self.key_special_attack = self.game_config.key_special_attack - self.key_ultimate: str = self.game_config.key_ultimate - self.key_chain_left: str = self.game_config.key_chain_left - self.key_chain_right: str = self.game_config.key_chain_right - self.key_move_w: str = self.game_config.key_move_w - self.key_move_s: str = self.game_config.key_move_s - self.key_move_a: str = self.game_config.key_move_a - self.key_move_d: str = self.game_config.key_move_d - self.key_interact: str = self.game_config.key_interact - self.key_lock: str = self.game_config.key_lock - self.key_chain_cancel: str = self.game_config.key_chain_cancel + self.action_keys = self.game_config.get_action_keys('keyboard') def enable_xbox(self): PcControllerBase.enable_xbox(self) - - self.key_dodge = self.game_config.xbox_key_dodge - self.key_switch_next = self.game_config.xbox_key_switch_next - self.key_switch_prev = self.game_config.xbox_key_switch_prev - self.key_normal_attack = self.game_config.xbox_key_normal_attack - self.key_special_attack = self.game_config.xbox_key_special_attack - self.key_ultimate: str = self.game_config.xbox_key_ultimate - self.key_chain_left: str = self.game_config.xbox_key_chain_left - self.key_chain_right: str = self.game_config.xbox_key_chain_right - self.key_move_w: str = self.game_config.xbox_key_move_w - self.key_move_s: str = self.game_config.xbox_key_move_s - self.key_move_a: str = self.game_config.xbox_key_move_a - self.key_move_d: str = self.game_config.xbox_key_move_d - self.key_interact: str = self.game_config.xbox_key_interact - self.key_lock: str = self.game_config.xbox_key_lock - self.key_chain_cancel: str = self.game_config.xbox_key_chain_cancel + self.action_keys = self.game_config.get_action_keys('xbox') + self.gamepad_action_keys = self.game_config.get_gamepad_action_keys('xbox') def enable_ds4(self): PcControllerBase.enable_ds4(self) + self.action_keys = self.game_config.get_action_keys('ds4') + self.gamepad_action_keys = self.game_config.get_gamepad_action_keys('ds4') - self.key_dodge = self.game_config.ds4_key_dodge - self.key_switch_next = self.game_config.ds4_key_switch_next - self.key_switch_prev = self.game_config.ds4_key_switch_prev - self.key_normal_attack = self.game_config.ds4_key_normal_attack - self.key_special_attack = self.game_config.ds4_key_special_attack - self.key_ultimate: str = self.game_config.ds4_key_ultimate - self.key_chain_left: str = self.game_config.ds4_key_chain_left - self.key_chain_right: str = self.game_config.ds4_key_chain_right - self.key_move_w: str = self.game_config.ds4_key_move_w - self.key_move_s: str = self.game_config.ds4_key_move_s - self.key_move_a: str = self.game_config.ds4_key_move_a - self.key_move_d: str = self.game_config.ds4_key_move_d - self.key_interact: str = self.game_config.ds4_key_interact - self.key_lock: str = self.game_config.ds4_key_lock - self.key_chain_cancel: str = self.game_config.ds4_key_chain_cancel - - def dodge(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 闪避 - :return: - """ + def _action_btn(self, key: str, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """通用按键动作:按下/释放/点按""" if press: - self.btn_controller.press(self.key_dodge, press_time) + self.btn_press(key, press_time) elif release: - self.btn_controller.release(self.key_dodge) + self.btn_release(key) else: - self.btn_controller.tap(self.key_dodge) + self.btn_tap(key) - def switch_next(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 切换角色-下一个 - :return: - """ - if press: - self.btn_controller.press(self.key_switch_next, press_time) - elif release: - self.btn_controller.release(self.key_switch_next) - else: - self.btn_controller.tap(self.key_switch_next) + def dodge(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """闪避""" + self._action_btn(self.action_keys['dodge'], press, press_time, release) - def switch_prev(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 切换角色-上一个 - :return: - """ - if press: - self.btn_controller.press(self.key_switch_prev, press_time) - elif release: - self.btn_controller.release(self.key_switch_prev) - else: - self.btn_controller.tap(self.key_switch_prev) + def switch_next(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """切换角色-下一个""" + self._action_btn(self.action_keys['switch_next'], press, press_time, release) - def normal_attack(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 普通攻击 - """ - if press: - self.btn_controller.press(self.key_normal_attack, press_time) - elif release: - self.btn_controller.release(self.key_normal_attack) - else: - self.btn_controller.tap(self.key_normal_attack) + def switch_prev(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """切换角色-上一个""" + self._action_btn(self.action_keys['switch_prev'], press, press_time, release) - def special_attack(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 特殊攻击 - """ - if press: - self.btn_controller.press(self.key_special_attack, press_time) - elif release: - self.btn_controller.release(self.key_special_attack) - else: - self.btn_controller.tap(self.key_special_attack) + def normal_attack(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """普通攻击""" + self._action_btn(self.action_keys['normal_attack'], press, press_time, release) - def ultimate(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 终结技 - """ - if press: - self.btn_controller.press(self.key_ultimate, press_time) - elif release: - self.btn_controller.release(self.key_ultimate) - else: - self.btn_controller.tap(self.key_ultimate) + def special_attack(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """特殊攻击""" + self._action_btn(self.action_keys['special_attack'], press, press_time, release) - def chain_left(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 连携技-左 - """ - if press: - self.btn_controller.press(self.key_chain_left, press_time) - elif release: - self.btn_controller.release(self.key_chain_left) - else: - self.btn_controller.tap(self.key_chain_left) + def ultimate(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """终结技""" + self._action_btn(self.action_keys['ultimate'], press, press_time, release) - def chain_right(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 连携技-右 - """ - if press: - self.btn_controller.press(self.key_chain_right, press_time) - elif release: - self.btn_controller.release(self.key_chain_right) - else: - self.btn_controller.tap(self.key_chain_right) + def chain_left(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """连携技-左""" + self._action_btn(self.action_keys['chain_left'], press, press_time, release) - def move_w(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 向前移动 - :return: - """ - if press: - self.btn_controller.press(self.key_move_w, press_time) - elif release: - self.btn_controller.release(self.key_move_w) - else: - self.btn_controller.tap(self.key_move_w) + def chain_right(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """连携技-右""" + self._action_btn(self.action_keys['chain_right'], press, press_time, release) - def move_s(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 向后移动 - """ - if press: - self.btn_controller.press(self.key_move_s, press_time) - elif release: - self.btn_controller.release(self.key_move_s) - else: - self.btn_controller.tap(self.key_move_s) + def move_w(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """向前移动""" + self._action_btn(self.action_keys['move_w'], press, press_time, release) - def move_a(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 向左移动 - """ - if press: - self.btn_controller.press(self.key_move_a, press_time) - elif release: - self.btn_controller.release(self.key_move_a) - else: - self.btn_controller.tap(self.key_move_a) + def move_s(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """向后移动""" + self._action_btn(self.action_keys['move_s'], press, press_time, release) + + def move_a(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """向左移动""" + self._action_btn(self.action_keys['move_a'], press, press_time, release) + + def move_d(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """向右移动""" + self._action_btn(self.action_keys['move_d'], press, press_time, release) + + def interact(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """交互""" + self._action_btn(self.action_keys['interact'], press, press_time, release) + + def lock(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """锁定敌人""" + self._action_btn(self.action_keys['lock'], press, press_time, release) - def move_d(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: + def chain_cancel(self, press: bool = False, press_time: float | None = None, release: bool = False) -> None: + """取消连携""" + self._action_btn(self.action_keys['chain_cancel'], press, press_time, release) + + def start_moving_forward(self) -> None: """ - 向右移动 + 开始向前移动 """ - if press: - self.btn_controller.press(self.key_move_d, press_time) - elif release: - self.btn_controller.release(self.key_move_d) - else: - self.btn_controller.tap(self.key_move_d) + if self.is_moving: + return + self.is_moving = True + self.move_w(press=True) - def interact(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: + def stop_moving_forward(self) -> None: """ - 交互 + 停止向前移动 """ - if press: - self.btn_controller.press(self.key_interact, press_time) - elif release: - self.btn_controller.release(self.key_interact) - else: - self.btn_controller.tap(self.key_interact) + self.is_moving = False + self.move_w(release=True) def turn_by_distance(self, d: float): """ 横向转向 按距离转 - :param d: 正数往右转 负数往左转 - :return: + + Args: + d: 正数往右转 负数往左转 """ - ctypes.windll.user32.mouse_event(0x0001, int(d), 0) + self.move_mouse_relative(d, 0) def turn_by_angle_diff(self, angle_diff: float) -> None: """ @@ -281,58 +177,60 @@ def turn_by_angle_diff(self, angle_diff: float) -> None: """ self.turn_by_distance(self.turn_dx * angle_diff) - def lock(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 锁定敌人 + def turn_vertical_by_distance(self, d: float): """ - if press: - self.btn_controller.press(self.key_lock, press_time) - elif release: - self.btn_controller.release(self.key_lock) - else: - self.btn_controller.tap(self.key_lock) + 纵向转向 按距离转 - def chain_cancel(self, press: bool = False, press_time: Optional[float] = None, release: bool = False) -> None: - """ - 取消连携 + Args: + d: 正数往下转 负数往上转 """ - if press: - self.btn_controller.press(self.key_chain_cancel, press_time) - elif release: - self.btn_controller.release(self.key_chain_cancel) - else: - self.btn_controller.tap(self.key_chain_cancel) + self.move_mouse_relative(0, d) - def start_moving_forward(self) -> None: + def move_mouse_relative(self, dx: float, dy: float): """ - 开始向前移动 + 相对移动鼠标 + + Args: + dx: 横向移动距离,正数向右 + dy: 纵向移动距离,正数向下 """ - if self.is_moving: + if dx == 0 and dy == 0: return - self.is_moving = True - self.move_w(press=True) + if self.background_mode: + self._gamepad_turn(dx, dy) + else: + self._ensure_mouse_mode() + ctypes.windll.user32.mouse_event(0x0001, int(dx), int(dy)) - def stop_moving_forward(self) -> None: + def _gamepad_turn(self, dx: float, dy: float) -> None: """ - 停止向前移动 - """ - self.is_moving = False - self.move_w(release=True) + 手柄右摇杆模拟鼠标转向 - def turn_vertical_by_distance(self, d: float): - """ - 纵向转向 按距离转 - :param d: 正数往下转 负数往上转 - :return: - """ - ctypes.windll.user32.mouse_event(0x0001, 0, int(d)) + 将鼠标像素距离换算为右摇杆满偏转持续时间。 + Y 轴取反:鼠标向下(+dy)对应摇杆向下(-y)。 - def move_mouse_relative(self, dx: float, dy: float): - """ - 相对移动鼠标 - :param dx: 横向移动距离,正数向右 - :param dy: 纵向移动距离,正数向下 + Args: + dx: 水平像素距离,正数向右 + dy: 垂直像素距离,正数向下 """ if dx == 0 and dy == 0: return - ctypes.windll.user32.mouse_event(0x0001, int(dx), int(dy)) \ No newline at end of file + if self.gamepad_turn_speed <= 0: + return + + self._ensure_gamepad_mode() + + max_d = max(abs(dx), abs(dy)) + stick_x = dx / max_d + stick_y = -dy / max_d # 鼠标下(+) → 摇杆下(-y) + + duration = max_d / self.gamepad_turn_speed + + pad = self.btn_controller.pad + try: + pad.right_joystick_float(stick_x, stick_y) + pad.update() + time.sleep(duration) + finally: + pad.right_joystick_float(0, 0) + pad.update() diff --git a/src/zzz_od/game_data/agent.py b/src/zzz_od/game_data/agent.py index 79ef1a7d6a..dfe658cfda 100644 --- a/src/zzz_od/game_data/agent.py +++ b/src/zzz_od/game_data/agent.py @@ -272,8 +272,10 @@ class AgentEnum(Enum): CAESAR_KING = Agent('caesar_king', '凯撒', RareTypeEnum.S, AgentTypeEnum.DEFENSE, DmgTypeEnum.PHYSICAL, ['caesar_king']) BURNICE_WHITE = Agent('burnice_white', '柏妮思', RareTypeEnum.S, AgentTypeEnum.ANOMALY, DmgTypeEnum.FIRE, ['burnice_white'], - state_list=[AgentStateDef('柏妮思-燃点', AgentStateCheckWay.BACKGROUND_GRAY_RANGE_LENGTH, - 'burnice_white', lower_color=0, upper_color=70) + state_list=[AgentStateDef('柏妮思-燃点', AgentStateCheckWay.FOREGROUND_COLOR_RANGE_LENGTH, + template_id='burnice_white', + hsv_color=(0, 255, 255), hsv_color_diff=(90, 200, 100), + max_length=100) ]) YANAGI = Agent('yanagi', '柳', RareTypeEnum.S, AgentTypeEnum.ANOMALY, DmgTypeEnum.ELECTRIC, ['yanagi']) @@ -339,7 +341,8 @@ class AgentEnum(Enum): max_length=120) ]) - PANYINHU = Agent('panyinhu', '潘引壶', RareTypeEnum.A, AgentTypeEnum.DEFENSE, DmgTypeEnum.PHYSICAL, ['panyinhu']) + PANYINHU = Agent('panyinhu', '潘引壶', RareTypeEnum.A, AgentTypeEnum.DEFENSE, DmgTypeEnum.PHYSICAL, + ['panyinhu', 'panyinhu_culinary_jewel']) JU_FUFU = Agent('ju_fufu', '橘福福', RareTypeEnum.S, AgentTypeEnum.STUN, DmgTypeEnum.FIRE, ['ju_fufu'], state_list=[ @@ -442,7 +445,7 @@ class AgentEnum(Enum): RareTypeEnum.S, AgentTypeEnum.SUPPORT, DmgTypeEnum.PHYSICAL, - ["sunna"], + ["sunna", "sunna_afternoon_tea_break"], ) YESHUNGUANG = Agent( @@ -479,3 +482,9 @@ class AgentEnum(Enum): ), ], ) + + ARIA = Agent('aria', '爱芮', RareTypeEnum.S, AgentTypeEnum.ANOMALY, DmgTypeEnum.ETHER, ['aria', 'aria_discordant_note'], + state_list=[AgentStateDef('爱芮-应援能量', AgentStateCheckWay.COLOR_RANGE_CONNECT, + template_id='aria_cheer_energy', + hsv_color=(90, 255, 255), hsv_color_diff=(90, 200, 100), + connect_cnt=2)]) diff --git a/src/zzz_od/game_data/compendium.py b/src/zzz_od/game_data/compendium.py index 55751f719f..e1b8ec74d7 100644 --- a/src/zzz_od/game_data/compendium.py +++ b/src/zzz_od/game_data/compendium.py @@ -1,18 +1,15 @@ import os -from typing import List, Optional - -import yaml from one_dragon.base.config.config_item import ConfigItem -from one_dragon.utils import os_utils +from one_dragon.utils import os_utils, yaml_utils from one_dragon.utils.log_utils import log class CompendiumTab: - def __init__(self, tab_name: str, category_list: List = None): + def __init__(self, tab_name: str, category_list: list = None): self.tab_name: str = tab_name - self.category_list: List[CompendiumCategory] = [] + self.category_list: list[CompendiumCategory] = [] if category_list is not None: for category_list_item in category_list: category_item = CompendiumCategory(**category_list_item) @@ -22,10 +19,10 @@ def __init__(self, tab_name: str, category_list: List = None): class CompendiumCategory: - def __init__(self, category_name: str, mission_type_list: List = None): - self.tab: Optional[CompendiumTab] = None + def __init__(self, category_name: str, mission_type_list: list = None): + self.tab: CompendiumTab | None = None self.category_name: str = category_name - self.mission_type_list: List[CompendiumMissionType] = [] + self.mission_type_list: list[CompendiumMissionType] = [] if mission_type_list is not None: for mission_type_item in mission_type_list: mission_type = CompendiumMissionType(**mission_type_item) @@ -38,15 +35,15 @@ def set_tab(self, tab: CompendiumTab): class CompendiumMissionType: - def __init__(self, mission_type_name: str, mission_type_name_display: Optional[str] = None, - mission_list: List = None, alias_list: List[str] = None): - self.category: Optional[CompendiumCategory] = None + def __init__(self, mission_type_name: str, mission_type_name_display: str | None = None, + mission_list: list = None, alias_list: list[str] = None): + self.category: CompendiumCategory | None = None self.mission_type_name: str = mission_type_name self.mission_type_name_display: str = mission_type_name if mission_type_name_display is not None: self.mission_type_name_display = mission_type_name_display - self.alias_list: List[str] = alias_list if alias_list is not None else [] - self.mission_list: List[CompendiumMission] = [] + self.alias_list: list[str] = alias_list if alias_list is not None else [] + self.mission_list: list[CompendiumMission] = [] if mission_list is not None: for mission_item in mission_list: mission = CompendiumMission(**mission_item) @@ -56,11 +53,15 @@ def __init__(self, mission_type_name: str, mission_type_name_display: Optional[s def set_category(self, category: CompendiumCategory): self.category = category + @property + def is_agent_plan(self) -> bool: + return self.mission_type_name == '代理人方案培养' + class CompendiumMission: - def __init__(self, mission_name: str, mission_name_display: Optional[str] = None): - self.mission_type: Optional[CompendiumMissionType] = None + def __init__(self, mission_name: str, mission_name_display: str | None = None): + self.mission_type: CompendiumMissionType | None = None self.mission_name: str = mission_name self.mission_name_display: str = mission_name if mission_name_display is None else mission_name_display @@ -70,8 +71,8 @@ def set_mission_type(self, mission_type: CompendiumMissionType): class CompendiumData: - def __init__(self, tab_list: List = None): - self.tab_list: List[CompendiumTab] = [] + def __init__(self, tab_list: list = None): + self.tab_list: list[CompendiumTab] = [] if tab_list is not None: for tab_item in tab_list: self.tab_list.append(CompendiumTab(**tab_item)) @@ -80,10 +81,10 @@ def __init__(self, tab_list: List = None): class Coffee: def __init__(self, coffee_name: str, - tab: Optional[CompendiumTab], - category: Optional[CompendiumCategory], - mission_type: Optional[CompendiumMissionType], - mission: Optional[CompendiumMission], + tab: CompendiumTab | None, + category: CompendiumCategory | None, + mission_type: CompendiumMissionType | None, + mission: CompendiumMission | None, extra: bool = False): self.coffee_name: str = coffee_name self.tab: CompendiumTab = tab @@ -114,9 +115,9 @@ class CompendiumService: def __init__(self): self.data: CompendiumData = CompendiumData() - self.coffee_list: List[Coffee] = [] + self.coffee_list: list[Coffee] = [] self.name_2_coffee: dict[str, Coffee] = {} - self.coffee_schedule: dict[int, List[Coffee]] = {} + self.coffee_schedule: dict[int, list[Coffee]] = {} def reload(self) -> None: """ @@ -140,19 +141,19 @@ def _load_all_compendium(self) -> None: return try: - with open(file_path, 'r', encoding='utf-8') as file: - tab_list: List[dict] = yaml.safe_load(file) + with open(file_path, encoding='utf-8') as file: + tab_list: list[dict] = yaml_utils.safe_load(file) self.data = CompendiumData(tab_list) except Exception: log.error(f'文件读取失败 {file_path}', exc_info=True) - def get_tab_data(self, tab_name: str) -> Optional[CompendiumTab]: + def get_tab_data(self, tab_name: str) -> CompendiumTab | None: for tab_item in self.data.tab_list: if tab_item.tab_name == tab_name: return tab_item return None - def get_category_list_data(self, tab_name: str) -> List[CompendiumCategory]: + def get_category_list_data(self, tab_name: str) -> list[CompendiumCategory]: tab = self.get_tab_data(tab_name) if tab is None: @@ -160,7 +161,7 @@ def get_category_list_data(self, tab_name: str) -> List[CompendiumCategory]: return tab.category_list - def get_category_data(self, tab_name: str, category_name: str) -> Optional[CompendiumCategory]: + def get_category_data(self, tab_name: str, category_name: str) -> CompendiumCategory | None: category_list = self.get_category_list_data(tab_name) for category_item in category_list: @@ -169,14 +170,14 @@ def get_category_data(self, tab_name: str, category_name: str) -> Optional[Compe return None - def get_mission_type_list_data(self, tab_name: str, category_name: str) -> List[CompendiumMissionType]: + def get_mission_type_list_data(self, tab_name: str, category_name: str) -> list[CompendiumMissionType]: category: CompendiumCategory = self.get_category_data(tab_name, category_name) if category is not None: return category.mission_type_list else: return [] - def get_mission_type_data(self, tab_name: str, category_name: str, mission_type_name: str) -> Optional[CompendiumMissionType]: + def get_mission_type_data(self, tab_name: str, category_name: str, mission_type_name: str) -> CompendiumMissionType | None: mission_type_list = self.get_mission_type_list_data(tab_name, category_name) for mission_type in mission_type_list: @@ -185,14 +186,14 @@ def get_mission_type_data(self, tab_name: str, category_name: str, mission_type_ return None - def get_mission_list_data(self, tab_name: str, category_name: str, mission_type_name: str) -> List[CompendiumMission]: + def get_mission_list_data(self, tab_name: str, category_name: str, mission_type_name: str) -> list[CompendiumMission]: mission_type = self.get_mission_type_data(tab_name, category_name, mission_type_name) if mission_type is not None: return mission_type.mission_list else: return [] - def get_mission_data(self, tab_name: str, category_name: str, mission_type_name: str, mission_name: str) -> Optional[CompendiumMission]: + def get_mission_data(self, tab_name: str, category_name: str, mission_type_name: str, mission_name: str) -> CompendiumMission | None: mission_list = self.get_mission_list_data(tab_name, category_name, mission_type_name) for mission in mission_list: if mission.mission_name == mission_name: @@ -200,8 +201,8 @@ def get_mission_data(self, tab_name: str, category_name: str, mission_type_name: return None - def get_charge_plan_category_list(self) -> List[ConfigItem]: - category_config_list: List[ConfigItem] = [] + def get_charge_plan_category_list(self) -> list[ConfigItem]: + category_config_list: list[ConfigItem] = [] category_list = self.get_category_list_data('训练') for category_item in category_list: @@ -220,8 +221,8 @@ def get_charge_plan_category_list(self) -> List[ConfigItem]: return category_config_list - def get_charge_plan_mission_type_list(self, category_name: str) -> List[ConfigItem]: - config_list: List[ConfigItem] = [] + def get_charge_plan_mission_type_list(self, category_name: str) -> list[ConfigItem]: + config_list: list[ConfigItem] = [] mission_type_list = self.get_mission_type_list_data('训练', category_name) for mission_type_item in mission_type_list: @@ -232,8 +233,8 @@ def get_charge_plan_mission_type_list(self, category_name: str) -> List[ConfigIt return config_list - def get_charge_plan_mission_list(self, category_name: str, mission_type: str) -> List[ConfigItem]: - config_list: List[ConfigItem] = [] + def get_charge_plan_mission_list(self, category_name: str, mission_type: str) -> list[ConfigItem]: + config_list: list[ConfigItem] = [] mission_list = self.get_mission_list_data('训练', category_name, mission_type) for mission_item in mission_list: @@ -244,7 +245,7 @@ def get_charge_plan_mission_list(self, category_name: str, mission_type: str) -> return config_list - def get_same_category_mission_type_list(self, mission_type_name: str) -> Optional[List[CompendiumMissionType]]: + def get_same_category_mission_type_list(self, mission_type_name: str) -> list[CompendiumMissionType] | None: """ 获取与副本相同分类的全部列表 """ @@ -256,8 +257,8 @@ def get_same_category_mission_type_list(self, mission_type_name: str) -> Optiona return None - def get_notorious_hunt_plan_mission_type_list(self, category_name: str) -> List[ConfigItem]: - config_list: List[ConfigItem] = [] + def get_notorious_hunt_plan_mission_type_list(self, category_name: str) -> list[ConfigItem]: + config_list: list[ConfigItem] = [] mission_type_list = self.get_mission_type_list_data('训练', category_name) for mission_type_item in mission_type_list: @@ -268,7 +269,7 @@ def get_notorious_hunt_plan_mission_type_list(self, category_name: str) -> List[ return config_list - def get_hollow_zero_mission_name_list(self) -> List[str]: + def get_hollow_zero_mission_name_list(self) -> list[str]: mission_type_list = self.get_mission_type_list_data('作战', '零号空洞') return [ mission.mission_name @@ -291,8 +292,8 @@ def _load_coffee(self) -> None: return try: - with open(file_path, 'r', encoding='utf-8') as file: - data = yaml.safe_load(file) + with open(file_path, encoding='utf-8') as file: + data = yaml_utils.safe_load(file) self.coffee_list = [] self.name_2_coffee = {} @@ -312,10 +313,10 @@ def _load_coffee(self) -> None: log.error(f'文件读取失败 {file_path}', exc_info=True) def _construct_coffee(self, coffee_name: str, - tab_name: Optional[str] = None, - category_name: Optional[str] = None, - mission_type_name: Optional[str] = None, - mission_name: Optional[str] = None, + tab_name: str | None = None, + category_name: str | None = None, + mission_type_name: str | None = None, + mission_name: str | None = None, extra: bool = False ) -> Coffee: tab = self.get_tab_data(tab_name) @@ -325,19 +326,19 @@ def _construct_coffee(self, coffee_name: str, return Coffee(coffee_name, tab, category, mission_type, mission, extra=extra) - def get_coffee_config_list_by_day(self, day: int) -> List[ConfigItem]: + def get_coffee_config_list_by_day(self, day: int) -> list[ConfigItem]: return [ConfigItem(i.display_name, i.coffee_name) for i in self.coffee_schedule.get(day, [])] - def get_extra_coffee_list(self) -> List[Coffee]: + def get_extra_coffee_list(self) -> list[Coffee]: return [i for i in self.coffee_list if i.extra] - def get_lost_void_mission_name_list(self) -> List[str]: + def get_lost_void_mission_name_list(self) -> list[str]: """ 迷失之地的关卡名称列表 :return: """ - mission_name_list: List[str] = [] + mission_name_list: list[str] = [] mission_list = self.get_mission_list_data('作战', '零号空洞', '迷失之地') for mission in mission_list: mission_name_list.append(mission.mission_name_display) - return mission_name_list \ No newline at end of file + return mission_name_list diff --git a/src/zzz_od/game_data/map_area.py b/src/zzz_od/game_data/map_area.py index f6dada3c1b..51ac1144a3 100644 --- a/src/zzz_od/game_data/map_area.py +++ b/src/zzz_od/game_data/map_area.py @@ -1,10 +1,9 @@ import difflib import os -import yaml from typing import List, Optional -from one_dragon.utils import os_utils +from one_dragon.utils import os_utils, yaml_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log @@ -41,7 +40,7 @@ def reload(self) -> None: ) try: with open(file_path, 'r', encoding='utf-8') as file: - area_list: List[dict] = yaml.safe_load(file) + area_list: List[dict] = yaml_utils.safe_load(file) self.area_list = [] self.area_name_map = {} for area_data in area_list: diff --git a/src/zzz_od/gui/app.py b/src/zzz_od/gui/app.py index d583a19fc1..e8c9cb876e 100644 --- a/src/zzz_od/gui/app.py +++ b/src/zzz_od/gui/app.py @@ -1,9 +1,9 @@ try: import sys - from typing import Tuple - from PySide6.QtCore import Qt, QThread, Signal, QTimer + + from PySide6.QtCore import Qt, QThread, QTimer, Signal from PySide6.QtWidgets import QApplication - from qfluentwidgets import NavigationItemPosition, setTheme, Theme + from qfluentwidgets import NavigationItemPosition, Theme, setTheme from one_dragon.base.operation.one_dragon_context import ContextInstanceEventEnum from one_dragon.utils import app_utils @@ -224,7 +224,7 @@ def _on_instance_active_signal(self) -> None: ) ) - def _update_version(self, versions: Tuple[str, str]) -> None: + def _update_version(self, versions: tuple[str, str]) -> None: """ 更新版本显示 @param ver: @@ -314,7 +314,7 @@ def closeEvent(self, event): # 初始化应用程序,并启动主窗口 -if __name__ == "__main__": +def main() -> None: if _init_error is not None: # 显示错误弹窗,询问用户是否打开排障文档 error_message = f"启动一条龙失败,报错信息如下:\n{stack_trace}\n\n是否打开排障文档查看解决方案?" @@ -350,6 +350,12 @@ def closeEvent(self, event): init_runner.start() # 启动应用程序事件循环 - app.exec() + quit_code = app.exec() _ctx.after_app_shutdown() + + sys.exit(quit_code) + + +if __name__ == "__main__": + main() diff --git a/src/zzz_od/gui/dialog/charge_plan_setting_dialog.py b/src/zzz_od/gui/dialog/charge_plan_setting_dialog.py index bb525b2a3a..6769ebf4d4 100644 --- a/src/zzz_od/gui/dialog/charge_plan_setting_dialog.py +++ b/src/zzz_od/gui/dialog/charge_plan_setting_dialog.py @@ -125,8 +125,7 @@ def update_plan_list_display(self): self.content_widget.add_widget(self.plus_btn, stretch=1) for idx, plan in enumerate(plan_list): - card = self.card_list[idx] - card.init_with_plan(plan, self.config) + self.card_list[idx].update_item(plan, idx) while len(self.card_list) > len(plan_list): self.drag_list.remove_item(len(self.card_list) - 1) @@ -205,5 +204,4 @@ def _on_order_changed(self, new_data_list: list) -> None: # 更新所有卡片的索引 for idx, card in enumerate(self.card_list): - card.idx = idx - card.index = idx + card.update_item(card.data, idx) diff --git a/src/zzz_od/gui/dialog/intel_board_setting_dialog.py b/src/zzz_od/gui/dialog/intel_board_setting_dialog.py new file mode 100644 index 0000000000..484a6d1bdc --- /dev/null +++ b/src/zzz_od/gui/dialog/intel_board_setting_dialog.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget +from qfluentwidgets import ( + CaptionLabel, + FlyoutViewBase, + PopupTeachingTip, + PushButton, + SwitchButton, + TeachingTipTailPosition, +) + +from one_dragon.base.config.config_item import ConfigItem +from one_dragon.utils.i18_utils import gt +from one_dragon_qt.widgets.combo_box import ComboBox +from zzz_od.application.battle_assistant.auto_battle_config import ( + get_auto_battle_op_config_list, +) +from zzz_od.application.intel_board import intel_board_const +from zzz_od.application.intel_board.intel_board_config import IntelBoardConfig +from zzz_od.application.intel_board.intel_board_run_record import IntelBoardRunRecord + +if TYPE_CHECKING: + from zzz_od.context.zzz_context import ZContext + + +class IntelBoardSettingFlyout(FlyoutViewBase): + """情报板配置弹出框""" + + _current_tip: PopupTeachingTip | None = None + + def __init__(self, ctx: ZContext, group_id: str, parent: QWidget | None = None): + super().__init__(parent) + self.ctx = ctx + self.group_id = group_id + self.intel_board_config: IntelBoardConfig | None = None + self._setup_ui() + + def _setup_ui(self): + """设置UI布局""" + layout = QVBoxLayout(self) + layout.setContentsMargins(16, 12, 16, 12) + layout.setSpacing(8) + + # 预备编队行 + team_row = QHBoxLayout() + team_row.setSpacing(8) + team_label = CaptionLabel(gt('预备编队')) + team_label.setFixedWidth(60) + self.predefined_team_opt = ComboBox() + self.predefined_team_opt.setFixedWidth(120) + self.predefined_team_opt.currentIndexChanged.connect(self._on_team_changed) + team_row.addWidget(team_label) + team_row.addWidget(self.predefined_team_opt) + team_row.addStretch(1) + layout.addLayout(team_row) + + # 自动战斗行 + self.auto_battle_row = QHBoxLayout() + self.auto_battle_row.setSpacing(8) + self.auto_battle_label = CaptionLabel(gt('自动战斗')) + self.auto_battle_label.setFixedWidth(60) + self.auto_battle_opt = ComboBox() + self.auto_battle_opt.setFixedWidth(120) + self.auto_battle_opt.currentIndexChanged.connect(self._on_auto_battle_changed) + self.auto_battle_row.addWidget(self.auto_battle_label) + self.auto_battle_row.addWidget(self.auto_battle_opt) + self.auto_battle_row.addStretch(1) + layout.addLayout(self.auto_battle_row) + + # 刷满经验模式行 + exp_grind_row = QHBoxLayout() + exp_grind_row.setSpacing(8) + exp_grind_label = CaptionLabel(gt('刷满经验')) + exp_grind_label.setFixedWidth(60) + self.exp_grind_switch = SwitchButton() + self.exp_grind_switch.setOnText('') + self.exp_grind_switch.setOffText('') + self.exp_grind_switch.checkedChanged.connect(self._on_exp_grind_changed) + exp_grind_row.addWidget(exp_grind_label) + exp_grind_row.addWidget(self.exp_grind_switch) + exp_grind_row.addStretch(1) + layout.addLayout(exp_grind_row) + + # 重置进度按钮行 + reset_row = QHBoxLayout() + reset_row.setSpacing(8) + self.reset_btn = PushButton(gt('重置进度')) + self.reset_btn.clicked.connect(self._on_reset_progress) + reset_row.addWidget(self.reset_btn) + layout.addLayout(reset_row) + + def init_config(self): + """初始化配置""" + self.intel_board_config = self.ctx.run_context.get_config( + app_id=intel_board_const.APP_ID, + instance_idx=self.ctx.current_instance_idx, + group_id=self.group_id, + ) + + # 初始化预备编队下拉框 + team_list = ([ConfigItem('游戏内配队', -1)] + + [ConfigItem(team.name, team.idx) for team in self.ctx.team_config.team_list]) + self.predefined_team_opt.set_items(team_list, self.intel_board_config.predefined_team_idx) + + # 初始化自动战斗下拉框 + auto_battle_list = get_auto_battle_op_config_list(sub_dir='auto_battle') + self.auto_battle_opt.set_items(auto_battle_list, self.intel_board_config.auto_battle_config) + + # 初始化刷满经验模式开关 + self.exp_grind_switch.setChecked(self.intel_board_config.exp_grind_mode) + + # 根据当前配队设置自动战斗选项的可见性 + self._update_auto_battle_visibility() + + def _on_team_changed(self, idx: int) -> None: + value = self.predefined_team_opt.currentData() + if self.intel_board_config: + self.intel_board_config.predefined_team_idx = value + self._update_auto_battle_visibility() + + def _update_auto_battle_visibility(self): + """更新自动战斗选项的可见性""" + visible = self.intel_board_config and self.intel_board_config.predefined_team_idx == -1 + self.auto_battle_label.setVisible(visible) + self.auto_battle_opt.setVisible(visible) + + def _on_auto_battle_changed(self, idx: int) -> None: + if self.intel_board_config: + self.intel_board_config.auto_battle_config = self.auto_battle_opt.currentData() + + def _on_exp_grind_changed(self, checked: bool) -> None: + if self.intel_board_config: + self.intel_board_config.exp_grind_mode = checked + + def _on_reset_progress(self) -> None: + """重置情报板进度完成标记""" + run_record = IntelBoardRunRecord( + instance_idx=self.ctx.current_instance_idx, + ) + run_record.progress_complete = False + run_record.notorious_hunt_count = 0 + run_record.expert_challenge_count = 0 + run_record.base_exp = 0 + self.reset_btn.setText(gt('已重置')) + self.reset_btn.setEnabled(False) + + @classmethod + def show_flyout(cls, ctx: ZContext, group_id: str, target: QWidget, parent: QWidget | None = None) -> PopupTeachingTip: + """显示配置弹出框""" + # 如果已经有弹出框显示,直接返回(防止重复点击) + if cls._current_tip is not None: + try: + # 检查对象是否有效 + cls._current_tip.isVisible() + return cls._current_tip # 已有有效的 tip,不重复创建 + except RuntimeError: + cls._current_tip = None # C++ 对象已销毁,清理引用 + + # 创建弹出框视图 + content_view = IntelBoardSettingFlyout(ctx, group_id, parent) + content_view.init_config() + + # 创建并显示 PopupTeachingTip(点击空白区域自动关闭) + tip = PopupTeachingTip.make( + view=content_view, + target=target, + duration=-1, + tailPosition=TeachingTipTailPosition.RIGHT, + parent=parent, + ) + + cls._current_tip = tip + # 当 tip 关闭时清理引用 + tip.destroyed.connect(lambda: setattr(cls, '_current_tip', None)) + + return tip diff --git a/src/zzz_od/gui/dialog/notorious_hunt_setting_dialog.py b/src/zzz_od/gui/dialog/notorious_hunt_setting_dialog.py index 3a4459fded..e1096f751b 100644 --- a/src/zzz_od/gui/dialog/notorious_hunt_setting_dialog.py +++ b/src/zzz_od/gui/dialog/notorious_hunt_setting_dialog.py @@ -1,16 +1,17 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from PySide6.QtWidgets import QWidget from one_dragon_qt.widgets.column import Column +from one_dragon_qt.widgets.draggable_list import DraggableList from zzz_od.application.charge_plan.charge_plan_config import ( ChargePlanItem, ) from zzz_od.application.notorious_hunt import notorious_hunt_const from zzz_od.gui.dialog.app_setting_dialog import AppSettingDialog -from zzz_od.gui.view.one_dragon.notorious_hunt_interface import ChargePlanCard +from zzz_od.gui.view.one_dragon.notorious_hunt_interface import NotoriousHuntCard if TYPE_CHECKING: from zzz_od.context.zzz_context import ZContext @@ -24,30 +25,38 @@ def __init__(self, ctx: ZContext, parent: QWidget | None = None): def get_content_widget(self) -> QWidget: self.content_widget = Column() - self.card_list: List[ChargePlanCard] = [] - self.last_empty_widget: QWidget = QWidget() + # 创建可拖动的列表容器 + self.drag_list = DraggableList() + self.drag_list.order_changed.connect(self._on_order_changed) + self.content_widget.add_widget(self.drag_list) + + self.card_list: list[NotoriousHuntCard] = [] return self.content_widget - def update_plan_list_display(self): + def update_plan_list_display(self) -> None: plan_list = self.config.plan_list if len(plan_list) > len(self.card_list): - self.content_widget.remove_widget(self.last_empty_widget) - + # 需要添加新的卡片 while len(self.card_list) < len(plan_list): idx = len(self.card_list) - card = ChargePlanCard(self.ctx, idx, self.config.plan_list[idx]) + card = NotoriousHuntCard(self.ctx, idx, self.config.plan_list[idx]) card.changed.connect(self._on_plan_item_changed) + card.move_top.connect(self._on_plan_item_move_top) self.card_list.append(card) - self.content_widget.add_widget(card) + self.drag_list.add_list_item(card) - self.content_widget.add_widget(self.last_empty_widget, stretch=1) + elif len(plan_list) < len(self.card_list): + # 需要移除多余的卡片 + while len(self.card_list) > len(plan_list): + self.drag_list.remove_item(len(self.card_list) - 1) + self.card_list.pop(-1) + # 更新所有卡片的显示 for idx, plan in enumerate(plan_list): - card = self.card_list[idx] - card.init_with_plan(plan) + self.card_list[idx].update_item(plan, idx) def on_dialog_shown(self) -> None: super().on_dialog_shown() @@ -62,3 +71,32 @@ def on_dialog_shown(self) -> None: def _on_plan_item_changed(self, idx: int, plan: ChargePlanItem) -> None: self.config.update_plan(idx, plan) + + def _on_plan_item_move_top(self, idx: int) -> None: + self.config.move_top(idx) + self.update_plan_list_display() + + def _on_order_changed(self, new_data_list: list[ChargePlanItem]) -> None: + """ + 拖拽改变顺序后的回调 + + Args: + new_data_list: 新顺序的数据列表 + """ + # 更新配置中的 plan_list 顺序 + self.config.plan_list = new_data_list + self.config.save() + + # 重新构建 card_list 的顺序 + new_card_list: list[NotoriousHuntCard] = [] + for data in new_data_list: + # 找到对应数据的 card + for card in self.card_list: + if card.data == data: + new_card_list.append(card) + break + self.card_list = new_card_list + + # 更新所有卡片的索引 + for idx, card in enumerate(self.card_list): + card.update_item(card.data, idx) diff --git a/src/zzz_od/gui/dialog/shared_dialog_manager.py b/src/zzz_od/gui/dialog/shared_dialog_manager.py index 29deedc675..71c9b7153a 100644 --- a/src/zzz_od/gui/dialog/shared_dialog_manager.py +++ b/src/zzz_od/gui/dialog/shared_dialog_manager.py @@ -6,7 +6,10 @@ from zzz_od.gui.dialog.charge_plan_setting_dialog import ChargePlanSettingDialog from zzz_od.gui.dialog.coffee_setting_dialog import CoffeeSettingDialog -from zzz_od.gui.dialog.drive_disc_dismantle_setting_dialog import DriveDiscDismantleSettingDialog +from zzz_od.gui.dialog.drive_disc_dismantle_setting_dialog import ( + DriveDiscDismantleSettingDialog, +) +from zzz_od.gui.dialog.intel_board_setting_dialog import IntelBoardSettingFlyout from zzz_od.gui.dialog.lost_void_setting_dialog import LostVoidSettingDialog from zzz_od.gui.dialog.notorious_hunt_setting_dialog import NotoriousHuntSettingDialog from zzz_od.gui.dialog.random_play_setting_dialog import RandomPlaySettingDialog @@ -162,3 +165,16 @@ def show_redemption_code_setting_dialog( group_id=group_id, parent=parent, ) + + def show_intel_board_setting_flyout( + self, + target: QWidget, + parent: QWidget, + group_id: str, + ): + IntelBoardSettingFlyout.show_flyout( + ctx=self.ctx, + group_id=group_id, + target=target, + parent=parent, + ) diff --git a/src/zzz_od/gui/view/battle_assistant/auto_battle_interface.py b/src/zzz_od/gui/view/battle_assistant/auto_battle_interface.py index 7ef73c796e..b4e0d2d4c0 100644 --- a/src/zzz_od/gui/view/battle_assistant/auto_battle_interface.py +++ b/src/zzz_od/gui/view/battle_assistant/auto_battle_interface.py @@ -29,7 +29,7 @@ get_auto_battle_op_config_list, ) from zzz_od.application.zzz_application import ZApplication -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum from zzz_od.context.zzz_context import ZContext from zzz_od.gui.view.battle_assistant.battle_state_display import ( BattleStateDisplay, @@ -110,9 +110,9 @@ def get_widget_at_top(self) -> QWidget: top_widget.add_widget(self.screenshot_interval_opt) self.gamepad_type_opt = ComboBoxSettingCard( - icon=FluentIcon.GAME, title='手柄类型', - content='需先安装虚拟手柄依赖,参考文档或使用安装器。仅在战斗助手生效。', - options_enum=GamepadTypeEnum + icon=FluentIcon.GAME, title='操作方式', + content='仅影响自动战斗。如需使用手柄,请先安装虚拟手柄依赖。', + options_enum=ControlMethodEnum ) self.gamepad_type_opt.value_changed.connect(self._on_gamepad_type_changed) top_widget.add_widget(self.gamepad_type_opt) @@ -158,7 +158,7 @@ def on_interface_shown(self) -> None: self.auto_ultimate_opt.init_with_adapter(get_prop_adapter(self.ctx.battle_assistant_config, 'auto_ultimate_enabled')) self.merged_opt.init_with_adapter(get_prop_adapter(self.ctx.battle_assistant_config, 'use_merged_file')) self.screenshot_interval_opt.init_with_adapter(get_prop_adapter(self.ctx.battle_assistant_config, 'screenshot_interval')) - self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.gamepad_type) + self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.control_method) self.ctx.listen_event(AutoBattleApp.EVENT_OP_LOADED, self._on_auto_op_loaded_event) def on_interface_hidden(self) -> None: @@ -229,7 +229,7 @@ def _on_del_clicked(self) -> None: self._update_auto_battle_config_opts() def _on_gamepad_type_changed(self, idx: int, value: str) -> None: - self.ctx.battle_assistant_config.gamepad_type = value + self.ctx.battle_assistant_config.control_method = value def _on_key_press(self, event: ContextEventItem) -> None: """ @@ -279,4 +279,4 @@ def _refresh_interface(self): self.config_opt.setValue(self.ctx.battle_assistant_config.auto_battle_config) self.gpu_opt.init_with_adapter(self.ctx.model_config.get_prop_adapter('flash_classifier_gpu')) self.screenshot_interval_opt.setValue(str(self.ctx.battle_assistant_config.screenshot_interval)) - self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.gamepad_type) + self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.control_method) diff --git a/src/zzz_od/gui/view/battle_assistant/dodge_assistant_interface.py b/src/zzz_od/gui/view/battle_assistant/dodge_assistant_interface.py index 8e6f4a8d0b..9d1eca27e0 100644 --- a/src/zzz_od/gui/view/battle_assistant/dodge_assistant_interface.py +++ b/src/zzz_od/gui/view/battle_assistant/dodge_assistant_interface.py @@ -23,7 +23,7 @@ get_auto_battle_op_config_list, ) from zzz_od.application.battle_assistant.dodge_assitant import dodge_assistant_const -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum from zzz_od.context.zzz_context import ZContext from zzz_od.gui.view.battle_assistant.battle_state_display import BattleStateDisplay @@ -74,9 +74,9 @@ def get_widget_at_top(self) -> QWidget: top_widget.add_widget(self.screenshot_interval_opt) self.gamepad_type_opt = ComboBoxSettingCard( - icon=FluentIcon.GAME, title='手柄类型', - content='需先安装虚拟手柄依赖,参考文档或使用安装器。仅在战斗助手生效。', - options_enum=GamepadTypeEnum + icon=FluentIcon.GAME, title='操作方式', + content='仅影响自动战斗。如需使用手柄,请先安装虚拟手柄依赖。', + options_enum=ControlMethodEnum ) self.gamepad_type_opt.value_changed.connect(self._on_gamepad_type_changed) top_widget.add_widget(self.gamepad_type_opt) @@ -117,7 +117,7 @@ def on_interface_shown(self) -> None: self.dodge_opt.init_with_adapter(self.ctx.battle_assistant_config.get_prop_adapter('dodge_assistant_config')) self.gpu_opt.init_with_adapter(self.ctx.model_config.get_prop_adapter('flash_classifier_gpu')) self.screenshot_interval_opt.init_with_adapter(self.ctx.battle_assistant_config.get_prop_adapter('screenshot_interval')) - self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.gamepad_type) + self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.control_method) self.ctx.listen_event(AutoBattleApp.EVENT_OP_LOADED, self._on_auto_op_loaded_event) # # 调试用 @@ -155,7 +155,7 @@ def _on_del_clicked(self) -> None: self._update_dodge_way_opts() def _on_gamepad_type_changed(self, idx: int, value: str) -> None: - self.ctx.battle_assistant_config.gamepad_type = value + self.ctx.battle_assistant_config.control_method = value def on_context_state_changed(self) -> None: """ diff --git a/src/zzz_od/gui/view/battle_assistant/operation_debug_interface.py b/src/zzz_od/gui/view/battle_assistant/operation_debug_interface.py index 608732c0ec..36ee6ca0c9 100644 --- a/src/zzz_od/gui/view/battle_assistant/operation_debug_interface.py +++ b/src/zzz_od/gui/view/battle_assistant/operation_debug_interface.py @@ -20,7 +20,7 @@ from zzz_od.application.battle_assistant.operation_template_config import ( get_operation_template_config_list, ) -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ControlMethodEnum from zzz_od.context.zzz_context import ZContext @@ -63,9 +63,9 @@ def get_widget_at_top(self) -> QWidget: top_widget.add_widget(self.repeat_opt) self.gamepad_type_opt = ComboBoxSettingCard( - icon=FluentIcon.GAME, title='手柄类型', - content='需先安装虚拟手柄依赖,参考文档或使用安装器。仅在战斗助手生效。', - options_enum=GamepadTypeEnum + icon=FluentIcon.GAME, title='操作方式', + content='仅影响自动战斗。如需使用手柄,请先安装虚拟手柄依赖。', + options_enum=ControlMethodEnum ) self.gamepad_type_opt.value_changed.connect(self._on_gamepad_type_changed) top_widget.add_widget(self.gamepad_type_opt) @@ -80,7 +80,7 @@ def on_interface_shown(self) -> None: AppRunInterface.on_interface_shown(self) self._update_auto_battle_config_opts() self.config_opt.setValue(self.ctx.battle_assistant_config.debug_operation_config) - self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.gamepad_type) + self.gamepad_type_opt.setValue(self.ctx.battle_assistant_config.control_method) self.repeat_opt.setValue(self.ctx.battle_assistant_config.debug_operation_repeat) def _update_auto_battle_config_opts(self) -> None: @@ -119,4 +119,4 @@ def _on_del_clicked(self) -> None: self._update_auto_battle_config_opts() def _on_gamepad_type_changed(self, idx: int, value: str) -> None: - self.ctx.battle_assistant_config.gamepad_type = value + self.ctx.battle_assistant_config.control_method = value diff --git a/src/zzz_od/gui/view/devtools/agent_template_generator_interface.py b/src/zzz_od/gui/view/devtools/agent_template_generator_interface.py new file mode 100644 index 0000000000..15851b318d --- /dev/null +++ b/src/zzz_od/gui/view/devtools/agent_template_generator_interface.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +import re +from pathlib import Path + +from cv2.typing import MatLike +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QVBoxLayout, QWidget +from qfluentwidgets import ( + BodyLabel, + CaptionLabel, + FluentIcon, + InfoBarIcon, + LineEdit, + PrimaryPushButton, + SimpleCardWidget, + SubtitleLabel, +) + +from one_dragon.base.geometry.point import Point +from one_dragon.base.screen.template_info import TemplateInfo +from one_dragon.utils import cv2_utils, os_utils +from one_dragon.utils.i18_utils import gt +from one_dragon_qt.utils.layout_utils import Margins +from one_dragon_qt.widgets.column import Column +from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface +from zzz_od.context.zzz_context import ZContext +from zzz_od.gui.view.devtools.template_card_widget import TemplateCardWidget + +TEMPLATE_CONFIGS = [ + { + 'name': '1号位大头像', + 'sub_dir': 'battle', + 'template_id': 'avatar_1_{agent_id}', + 'template_ref': 'avatar_1_template', + }, + { + 'name': '2号位小头像', + 'sub_dir': 'battle', + 'template_id': 'avatar_2_{agent_id}', + 'template_ref': 'avatar_2_template', + }, + { + 'name': '连携头像', + 'sub_dir': 'battle', + 'template_id': 'avatar_chain_{agent_id}', + 'template_ref': 'avatar_chain_template', + }, + { + 'name': '快速支援头像', + 'sub_dir': 'battle', + 'template_id': 'avatar_quick_{agent_id}', + 'template_ref': 'avatar_quick_template', + }, + { + 'name': '零号空洞头像', + 'sub_dir': 'hollow', + 'template_id': 'avatar_{agent_id}', + 'template_ref': 'avatar_hollow_template', + }, + { + 'name': '组队预设头像', + 'sub_dir': 'predefined_team', + 'template_id': 'avatar_{agent_id}', + 'template_ref': 'avatar_template_team', + }, +] + + +class AgentTemplateGeneratorInterface(VerticalScrollInterface): + + def __init__(self, ctx: ZContext, parent=None): + VerticalScrollInterface.__init__( + self, + content_widget=None, + object_name='agent_template_generator_interface', + nav_text_cn='代理人模板生成', + nav_icon=FluentIcon.PEOPLE, + parent=parent, + ) + self.ctx: ZContext = ctx + self.agent_id: str | None = None + self.last_screen_dir: str | None = None + self._template_ref_cache: dict[str, TemplateInfo] = {} + self._agent_id_pattern = re.compile(r'^[a-z][a-z0-9_]*$') + + def get_content_widget(self) -> QWidget: + # 创建居中容器 + center_widget = QWidget() + center_layout = QHBoxLayout(center_widget) + center_layout.setContentsMargins(0, 0, 0, 0) + + # 左侧列:标题 + 输入 + left_column = Column(spacing=16, margins=Margins(0, 0, 0, 0)) + left_column.setFixedWidth(300) + center_layout.addWidget(left_column, alignment=Qt.AlignmentFlag.AlignTop) + + # 标题区域 + title_card = SimpleCardWidget() + title_layout = Column(spacing=8, margins=Margins(16, 16, 16, 16)) + QVBoxLayout(title_card).addWidget(title_layout) + + title_label = SubtitleLabel(text=gt('代理人模板生成')) + title_layout.add_widget(title_label) + + hint_label = CaptionLabel(text=gt('为指定角色生成6个头像模板')) + hint_label.setWordWrap(True) + title_layout.add_widget(hint_label) + + left_column.add_widget(title_card) + + # 输入区域 + input_card = SimpleCardWidget() + input_layout = Column(spacing=16, margins=Margins(16, 16, 16, 16)) + QVBoxLayout(input_card).addWidget(input_layout) + + # 输入框 + self.agent_id_edit = LineEdit() + self.agent_id_edit.setPlaceholderText(gt('输入代理人英文名')) + self.agent_id_edit.textChanged.connect(self._on_agent_id_changed) + input_layout.add_widget(self.agent_id_edit) + + # 一键生成按钮 + self.btn_generate_all = PrimaryPushButton(text=gt('一键生成'), icon=FluentIcon.PLAY) + self.btn_generate_all.clicked.connect(self._on_generate_all_clicked) + input_layout.add_widget(self.btn_generate_all) + + left_column.add_widget(input_card) + + # 截图说明卡片(紧贴输入卡片) + hint_card = SimpleCardWidget() + hint_layout = Column(spacing=12, margins=Margins(16, 16, 16, 16)) + QVBoxLayout(hint_card).addWidget(hint_layout) + + hint_title = BodyLabel(text=gt('截图方法')) + hint_layout.add_widget(hint_title) + + hint_items = [ + '1号大头像:3人组队,目标角色放前台', + '2号小头像:目标角色放后台-1', + '连携头像:2人队,触发目标角色连携', + '快速支援:触发目标快速支援', + '空洞头像:进入空洞后,目标角色在编队1号位', + '预设编队:编队在第一个队伍第一位', + ] + + for item in hint_items: + item_label = CaptionLabel(text=item) + hint_layout.add_widget(item_label) + + left_column.add_widget(hint_card) + left_column.add_stretch(1) + + # 右侧列:6个模板卡片 - 垂直排列 + right_column = Column(spacing=16, margins=Margins(8, 0, 8, 16)) + right_column.setFixedWidth(650) + center_layout.addWidget(right_column, alignment=Qt.AlignmentFlag.AlignTop) + + # 模板卡片 + self.template_cards: list[TemplateCardWidget] = [] + for _, config in enumerate(TEMPLATE_CONFIGS, start=1): + title = config["name"] + card = TemplateCardWidget(title=title, template_config=config) + card.btn_choose_screenshot.clicked.connect(lambda _, c=card: self._on_choose_screenshot(c)) + card.btn_capture_game.clicked.connect(lambda _, c=card: self._on_capture_game(c)) + card.btn_save.clicked.connect(lambda _, c=card: self._on_save_template(c)) + card.set_buttons_enabled(False) + self.template_cards.append(card) + right_column.add_widget(card) + + return center_widget + + def on_interface_shown(self) -> None: + VerticalScrollInterface.on_interface_shown(self) + + def _set_agent_input_error(self, is_error: bool) -> None: + if is_error: + self.agent_id_edit.setProperty('error', True) + self.agent_id_edit.setStyle(self.agent_id_edit.style()) + else: + self.agent_id_edit.setProperty('error', False) + self.agent_id_edit.setStyle(self.agent_id_edit.style()) + + def _on_agent_id_changed(self, text: str) -> None: + self._apply_agent_id(text) + + def _apply_agent_id(self, text: str) -> None: + agent_id = text.strip().lower() + previous_agent_id = self.agent_id + if not agent_id: + self.agent_id = None + self._set_agent_input_error(False) + self._set_cards_enabled(False) + return + + if self._agent_id_pattern.fullmatch(agent_id) is None: + self.agent_id = None + self._set_agent_input_error(True) + self._set_cards_enabled(False) + return + + self.agent_id = agent_id + self._set_agent_input_error(False) + self._set_cards_enabled(True) + if previous_agent_id != agent_id: + for card in self.template_cards: + card.reset_preview() + + def _set_cards_enabled(self, enabled: bool) -> None: + for card in self.template_cards: + card.set_buttons_enabled(enabled) + if not enabled: + card.reset_preview() + + def _get_template_ref(self, template_ref: str) -> TemplateInfo | None: + if template_ref in self._template_ref_cache: + return self._template_ref_cache[template_ref] + + template = TemplateInfo('template', template_ref) + if not template.is_file_exists: + return None + self._template_ref_cache[template_ref] = template + return template + + def _preview_template_crop(self, template_ref: str, screen_image: MatLike) -> MatLike | None: + template = self._get_template_ref(template_ref) + if template is None: + return None + template.screen_image = screen_image + return template.get_template_raw_by_screen_point() + + def _save_template(self, agent_id: str, template_config: dict, screen_image: MatLike) -> bool: + template_ref = template_config['template_ref'] + template_ref_info = self._get_template_ref(template_ref) + if template_ref_info is None: + return False + + sub_dir = template_config['sub_dir'] + template_id = template_config['template_id'].format(agent_id=agent_id) + + template = TemplateInfo(sub_dir, template_id) + template.screen_image = screen_image + template.template_shape = template_ref_info.template_shape + template.point_list = [Point(p.x, p.y) for p in template_ref_info.point_list] + template.auto_mask = template_ref_info.auto_mask + + try: + template.save_raw() + template.save_mask() + return True + except Exception: + return False + + + + def _choose_screenshot(self) -> str | None: + default_dir = os_utils.get_path_under_work_dir('.debug', 'images') + if self.last_screen_dir is not None: + default_dir = self.last_screen_dir + + file_path, _ = QFileDialog.getOpenFileName( + self, + gt('选择截图'), + dir=default_dir, + filter="PNG (*.png)", + ) + if file_path: + fix_file_path = str(Path(file_path).resolve()) + self.last_screen_dir = str(Path(fix_file_path).parent) + return fix_file_path + return None + + def _on_choose_screenshot(self, card: TemplateCardWidget) -> None: + if self.agent_id is None: + self.show_info_bar(gt('提示'), gt('请先输入角色ID'), icon=InfoBarIcon.WARNING) + return + + file_path = self._choose_screenshot() + if file_path is None: + return + + screen_image = cv2_utils.read_image(file_path) + if screen_image is None: + self.show_info_bar(gt('失败'), gt('截图读取失败'), icon=InfoBarIcon.ERROR) + return + self._preview_and_update(card, screen_image) + + def _on_capture_game(self, card: TemplateCardWidget) -> None: + if self.agent_id is None: + self.show_info_bar(gt('提示'), gt('请先输入角色ID'), icon=InfoBarIcon.WARNING) + return + + _, screen = self.ctx.controller.screenshot() + if screen is None: + self.show_info_bar(gt('失败'), gt('游戏截图失败'), icon=InfoBarIcon.ERROR) + return + self._preview_and_update(card, screen) + + def _preview_and_update(self, card: TemplateCardWidget, screen_image: MatLike) -> None: + preview_image = self._preview_template_crop(card.template_config['template_ref'], screen_image) + if preview_image is None: + self.show_info_bar(gt('失败'), gt('裁剪失败,请检查模板配置'), icon=InfoBarIcon.ERROR) + return + + card.screen_image = screen_image + card.set_preview_image(preview_image) + card.is_saved = False + card.btn_save.setEnabled(True) + + def _on_save_template(self, card: TemplateCardWidget) -> None: + if self.agent_id is None: + self.show_info_bar(gt('提示'), gt('请先输入角色ID'), icon=InfoBarIcon.WARNING) + return + if card.screen_image is None: + self.show_info_bar(gt('提示'), gt('请先选择截图或游戏截图'), icon=InfoBarIcon.WARNING) + return + + success = self._save_template(self.agent_id, card.template_config, card.screen_image) + if success: + card.set_saved(True) + else: + self.show_info_bar(gt('失败'), gt('模板保存失败'), icon=InfoBarIcon.ERROR) + + def _on_generate_all_clicked(self) -> None: + if self.agent_id is None: + self.show_info_bar(gt('提示'), gt('请先输入角色ID'), icon=InfoBarIcon.WARNING) + return + + failed: list[str] = [] + for card in self.template_cards: + screen = card.screen_image + if screen is None: + _, screen = self.ctx.controller.screenshot() + if screen is None: + failed.append(card.template_config['name']) + continue + + preview_image = self._preview_template_crop(card.template_config['template_ref'], screen) + if preview_image is not None: + card.set_preview_image(preview_image) + + if self._save_template(self.agent_id, card.template_config, screen): + card.set_saved(True) + else: + failed.append(card.template_config['name']) + + if failed: + self.show_info_bar(gt('部分失败'), f"{gt('失败模板')}: {', '.join(failed)}", icon=InfoBarIcon.WARNING) + else: + self.show_info_bar(gt('成功'), gt('全部模板已生成'), icon=InfoBarIcon.SUCCESS) diff --git a/src/zzz_od/gui/view/devtools/app_devtools_interface.py b/src/zzz_od/gui/view/devtools/app_devtools_interface.py index 9c7aeb3370..aa78077efd 100644 --- a/src/zzz_od/gui/view/devtools/app_devtools_interface.py +++ b/src/zzz_od/gui/view/devtools/app_devtools_interface.py @@ -5,6 +5,9 @@ from one_dragon_qt.view.devtools.devtools_template_helper_interface import DevtoolsTemplateHelperInterface from one_dragon_qt.widgets.pivot_navi_interface import PivotNavigatorInterface from zzz_od.context.zzz_context import ZContext +from zzz_od.gui.view.devtools.agent_template_generator_interface import ( + AgentTemplateGeneratorInterface, +) from zzz_od.gui.view.devtools.devtools_screenshot_helper_interface import DevtoolsScreenshotHelperInterface @@ -24,5 +27,6 @@ def create_sub_interface(self): """ self.add_sub_interface(DevtoolsImageAnalysisInterface(self.ctx)) self.add_sub_interface(DevtoolsTemplateHelperInterface(self.ctx)) + self.add_sub_interface(AgentTemplateGeneratorInterface(self.ctx)) self.add_sub_interface(DevtoolsScreenManageInterface(self.ctx)) self.add_sub_interface(DevtoolsScreenshotHelperInterface(self.ctx)) diff --git a/src/zzz_od/gui/view/devtools/template_card_widget.py b/src/zzz_od/gui/view/devtools/template_card_widget.py new file mode 100644 index 0000000000..2e7d4c5588 --- /dev/null +++ b/src/zzz_od/gui/view/devtools/template_card_widget.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from cv2.typing import MatLike +from PySide6.QtWidgets import QFrame, QHBoxLayout +from qfluentwidgets import ( + BodyLabel, + FluentIcon, + PrimaryPushButton, + PushButton, + SimpleCardWidget, +) + +from one_dragon_qt.utils.layout_utils import Margins +from one_dragon_qt.widgets.cv2_image import Cv2Image +from one_dragon_qt.widgets.fixed_size_image_label import FixedSizeImageLabel +from one_dragon_qt.widgets.row import Row + + +class TemplateCardWidget(SimpleCardWidget): + """单个模板的卡片组件""" + + def __init__(self, title: str, template_config: dict, parent=None): + SimpleCardWidget.__init__(self, parent=parent) + + self.template_config = template_config + self.screen_image: MatLike | None = None + self.preview_image: MatLike | None = None + self.is_saved: bool = False + + # 单行布局:标题 | 三个按钮 | 预览框 + layout = QHBoxLayout(self) + layout.setContentsMargins(16, 12, 16, 12) + layout.setSpacing(16) + + # 标题 + self.title_label = BodyLabel(text=title) + layout.addWidget(self.title_label) + + # 分割线 + line = QFrame() + line.setFrameShape(QFrame.Shape.VLine) + line.setFrameShadow(QFrame.Shadow.Sunken) + layout.addWidget(line) + + # 三个按钮 + btn_row = Row(spacing=10, margins=Margins(0, 0, 0, 0)) + layout.addWidget(btn_row) + + self.btn_choose_screenshot = PushButton(text='选择截图', icon=FluentIcon.FOLDER) + btn_row.add_widget(self.btn_choose_screenshot) + + self.btn_capture_game = PushButton(text='游戏截图', icon=FluentIcon.CAMERA) + btn_row.add_widget(self.btn_capture_game) + + self.btn_save = PrimaryPushButton(text='保存', icon=FluentIcon.SAVE) + self.btn_save.setEnabled(False) + btn_row.add_widget(self.btn_save) + + # 分割线 + line2 = QFrame() + line2.setFrameShape(QFrame.Shape.VLine) + line2.setFrameShadow(QFrame.Shadow.Sunken) + layout.addWidget(line2) + + # 预览框 + self.preview_label = FixedSizeImageLabel(120) + layout.addWidget(self.preview_label) + + def set_preview_image(self, image: MatLike | None) -> None: + self.preview_image = image + if image is None: + self.preview_label.setImage(None) + else: + self.preview_label.setImage(Cv2Image(image)) + + def set_saved(self, saved: bool) -> None: + self.is_saved = saved + if saved: + self.btn_save.setEnabled(False) + + def set_buttons_enabled(self, enabled: bool) -> None: + self.btn_choose_screenshot.setEnabled(enabled) + self.btn_capture_game.setEnabled(enabled) + if not enabled: + self.btn_save.setEnabled(False) + + def reset_preview(self) -> None: + self.screen_image = None + self.preview_image = None + self.is_saved = False + self.preview_label.setImage(None) + self.btn_save.setEnabled(False) diff --git a/src/zzz_od/gui/view/home/home_interface.py b/src/zzz_od/gui/view/home/home_interface.py index b89e084665..24faa5dce8 100644 --- a/src/zzz_od/gui/view/home/home_interface.py +++ b/src/zzz_od/gui/view/home/home_interface.py @@ -840,7 +840,7 @@ def _update_start_button_style_from_banner(self) -> None: def _get_theme_color(self) -> tuple[int, int, int]: """获取主题色,优先使用缓存,否则从图片提取""" # 如果是自定义模式,直接返回自定义颜色 - if self.ctx.custom_config.is_custom_theme_color: + if self.ctx.custom_config.custom_theme_color: return self.ctx.custom_config.theme_color current_banner_path = self.choose_banner_media() diff --git a/src/zzz_od/gui/view/one_dragon/charge_plan_interface.py b/src/zzz_od/gui/view/one_dragon/charge_plan_interface.py index 3fa9503366..561b8db6c7 100644 --- a/src/zzz_od/gui/view/one_dragon/charge_plan_interface.py +++ b/src/zzz_od/gui/view/one_dragon/charge_plan_interface.py @@ -129,6 +129,10 @@ def __init__(self, ctx: ZContext, self.init_with_plan(plan, config) + def after_update_item(self) -> None: + self.idx = self.index + self.init_with_plan(self.data, self.config) + def init_category_combo_box(self) -> None: config_list = self.ctx.compendium_service.get_charge_plan_category_list() self.category_combo_box.set_items(config_list, self.plan.category_name) @@ -394,14 +398,11 @@ def update_plan_list_display(self): # 更新所有卡片的显示 for idx, plan in enumerate(plan_list): - card = self.card_list[idx] - card.idx = idx - card.index = idx # 更新 DraggableListItem 的索引 - card.init_with_plan(plan, self.config) + self.card_list[idx].update_item(plan, idx) def _on_add_clicked(self) -> None: from zzz_od.gui.view.one_dragon.charge_plan_dialog import ChargePlanDialog - dialog = ChargePlanDialog(self.ctx, self.config, parent=self) + dialog = ChargePlanDialog(self.ctx, self.config, parent=self.window()) result = dialog.exec() if result: self.config.add_plan(dialog.plan) @@ -472,5 +473,4 @@ def _on_order_changed(self, new_data_list: list) -> None: # 更新所有卡片的索引 for idx, card in enumerate(self.card_list): - card.idx = idx - card.index = idx + card.update_item(card.data, idx) diff --git a/src/zzz_od/gui/view/one_dragon/notorious_hunt_interface.py b/src/zzz_od/gui/view/one_dragon/notorious_hunt_interface.py index dc92a90fd3..143752b1b4 100644 --- a/src/zzz_od/gui/view/one_dragon/notorious_hunt_interface.py +++ b/src/zzz_od/gui/view/one_dragon/notorious_hunt_interface.py @@ -1,26 +1,34 @@ from PySide6.QtCore import Signal from PySide6.QtWidgets import QWidget -from qfluentwidgets import FluentIcon, CaptionLabel, LineEdit -from typing import List, Optional +from qfluentwidgets import CaptionLabel, FluentIcon, LineEdit, ToolButton from one_dragon.base.config.config_item import ConfigItem from one_dragon.base.operation.application import application_const from one_dragon.utils.i18_utils import gt from one_dragon_qt.widgets.column import Column from one_dragon_qt.widgets.combo_box import ComboBox -from one_dragon_qt.widgets.setting_card.multi_push_setting_card import MultiLineSettingCard +from one_dragon_qt.widgets.draggable_list import DraggableList, DraggableListItem +from one_dragon_qt.widgets.setting_card.multi_push_setting_card import ( + MultiLineSettingCard, +) from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface -from zzz_od.application.battle_assistant.auto_battle_config import get_auto_battle_op_config_list +from zzz_od.application.battle_assistant.auto_battle_config import ( + get_auto_battle_op_config_list, +) from zzz_od.application.charge_plan.charge_plan_config import ChargePlanItem from zzz_od.application.notorious_hunt import notorious_hunt_const -from zzz_od.application.notorious_hunt.notorious_hunt_config import NotoriousHuntLevelEnum, NotoriousHuntBuffEnum, \ - NotoriousHuntConfig +from zzz_od.application.notorious_hunt.notorious_hunt_config import ( + NotoriousHuntBuffEnum, + NotoriousHuntConfig, + NotoriousHuntLevelEnum, +) from zzz_od.context.zzz_context import ZContext -class ChargePlanCard(MultiLineSettingCard): +class NotoriousHuntCard(DraggableListItem): changed = Signal(int, ChargePlanItem) + move_top = Signal(int) def __init__(self, ctx: ZContext, idx: int, plan: ChargePlanItem): @@ -52,8 +60,10 @@ def __init__(self, ctx: ZContext, self.plan_times_input = LineEdit() self.plan_times_input.textChanged.connect(self._on_plan_times_changed) - MultiLineSettingCard.__init__( - self, + self.move_top_btn = ToolButton(FluentIcon.PIN, None) + self.move_top_btn.clicked.connect(self._on_move_top_clicked) + + content_widget = MultiLineSettingCard( icon=FluentIcon.CALENDAR, title='', line_list=[ @@ -69,10 +79,27 @@ def __init__(self, ctx: ZContext, self.run_times_input, plan_times_label, self.plan_times_input, + self.move_top_btn, ] ] ) + DraggableListItem.__init__( + self, + data=plan, + index=idx, + content_widget=content_widget + ) + + self.init_with_plan(plan) + + def after_update_item(self) -> None: + self.idx = self.index + self.init_with_plan(self.data) + + def _on_move_top_clicked(self) -> None: + self.move_top.emit(self.idx) + def init_with_plan(self, plan: ChargePlanItem) -> None: """ 以一个体力计划进行初始化 @@ -174,35 +201,43 @@ def __init__(self, ctx: ZContext, parent=None): nav_text_cn='恶名狩猎计划' ) - self.config: Optional[NotoriousHuntConfig] = None + self.config: NotoriousHuntConfig | None = None def get_content_widget(self) -> QWidget: self.content_widget = Column() - self.card_list: List[ChargePlanCard] = [] - self.last_empty_widget: QWidget = QWidget() + # 创建可拖动的列表容器 + self.drag_list = DraggableList() + self.drag_list.order_changed.connect(self._on_order_changed) + self.content_widget.add_widget(self.drag_list) + + self.card_list: list[NotoriousHuntCard] = [] return self.content_widget - def update_plan_list_display(self): + def update_plan_list_display(self) -> None: plan_list = self.config.plan_list if len(plan_list) > len(self.card_list): - self.content_widget.remove_widget(self.last_empty_widget) - + # 需要添加新的卡片 while len(self.card_list) < len(plan_list): idx = len(self.card_list) - card = ChargePlanCard(self.ctx, idx, self.config.plan_list[idx]) + card = NotoriousHuntCard(self.ctx, idx, self.config.plan_list[idx]) card.changed.connect(self._on_plan_item_changed) + card.move_top.connect(self._on_plan_item_move_top) self.card_list.append(card) - self.content_widget.add_widget(card) + self.drag_list.add_list_item(card) - self.content_widget.add_widget(self.last_empty_widget, stretch=1) + elif len(plan_list) < len(self.card_list): + # 需要移除多余的卡片 + while len(self.card_list) > len(plan_list): + self.drag_list.remove_item(len(self.card_list) - 1) + self.card_list.pop(-1) + # 更新所有卡片的显示 for idx, plan in enumerate(plan_list): - card = self.card_list[idx] - card.init_with_plan(plan) + self.card_list[idx].update_item(plan, idx) def on_interface_shown(self) -> None: VerticalScrollInterface.on_interface_shown(self) @@ -220,3 +255,32 @@ def on_interface_hidden(self) -> None: def _on_plan_item_changed(self, idx: int, plan: ChargePlanItem) -> None: self.config.update_plan(idx, plan) + + def _on_plan_item_move_top(self, idx: int) -> None: + self.config.move_top(idx) + self.update_plan_list_display() + + def _on_order_changed(self, new_data_list: list[ChargePlanItem]) -> None: + """ + 拖拽改变顺序后的回调 + + Args: + new_data_list: 新顺序的数据列表 + """ + # 更新配置中的 plan_list 顺序 + self.config.plan_list = new_data_list + self.config.save() + + # 重新构建 card_list 的顺序 + new_card_list: list[NotoriousHuntCard] = [] + for data in new_data_list: + # 找到对应数据的 card + for card in self.card_list: + if card.data == data: + new_card_list.append(card) + break + self.card_list = new_card_list + + # 更新所有卡片的索引 + for idx, card in enumerate(self.card_list): + card.update_item(card.data, idx) diff --git a/src/zzz_od/gui/view/one_dragon/zzz_one_dragon_run_interface.py b/src/zzz_od/gui/view/one_dragon/zzz_one_dragon_run_interface.py index 1ad7b522a2..8641782235 100644 --- a/src/zzz_od/gui/view/one_dragon/zzz_one_dragon_run_interface.py +++ b/src/zzz_od/gui/view/one_dragon/zzz_one_dragon_run_interface.py @@ -5,6 +5,7 @@ from zzz_od.application.drive_disc_dismantle import drive_disc_dismantle_const from zzz_od.application.hollow_zero.lost_void import lost_void_const from zzz_od.application.hollow_zero.withered_domain import withered_domain_const +from zzz_od.application.intel_board import intel_board_const from zzz_od.application.notorious_hunt import notorious_hunt_const from zzz_od.application.random_play import random_play_const from zzz_od.application.redemption_code import redemption_code_const @@ -77,9 +78,25 @@ def on_app_setting_clicked(self, app_id: str) -> None: parent=self, group_id=group_id ) + elif app_id == intel_board_const.APP_ID: + # 找到对应的卡片作为弹出框的锚点 + target = self._find_app_card_setting_btn(app_id) + if target: + self.ctx.shared_dialog_manager.show_intel_board_setting_flyout( + target=target, + parent=self, + group_id=group_id + ) else: self.show_info_bar( title=f'{app_name} 暂不支持设置', content='', duration=3000, ) + + def _find_app_card_setting_btn(self, app_id: str): + """找到对应 app_id 的卡片的设置按钮""" + for card in self.app_run_list._app_cards: + if card.app.app_id == app_id: + return card.setting_btn + return None diff --git a/src/zzz_od/gui/view/setting/setting_game_interface.py b/src/zzz_od/gui/view/setting/setting_game_interface.py index 3f642ea09a..a2b4877321 100644 --- a/src/zzz_od/gui/view/setting/setting_game_interface.py +++ b/src/zzz_od/gui/view/setting/setting_game_interface.py @@ -1,20 +1,44 @@ from PySide6.QtWidgets import QWidget -from qfluentwidgets import FluentIcon, SettingCardGroup, PushButton - -from one_dragon.base.config.basic_game_config import TypeInputWay, ScreenSizeEnum, FullScreenEnum, MonitorEnum +from qfluentwidgets import FluentIcon, InfoBar, PushButton, SettingCardGroup + +from one_dragon.base.config.basic_game_config import ( + FullScreenEnum, + MonitorEnum, + ScreenSizeEnum, + TypeInputWay, +) +from one_dragon.base.controller.pc_button import pc_button_utils from one_dragon.base.controller.pc_button.ds4_button_controller import Ds4ButtonEnum from one_dragon.base.controller.pc_button.xbox_button_controller import XboxButtonEnum from one_dragon.utils import cmd_utils from one_dragon.utils.i18_utils import gt from one_dragon_qt.widgets.column import Column -from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ComboBoxSettingCard +from one_dragon_qt.widgets.combo_box import ComboBox +from one_dragon_qt.widgets.setting_card.combo_box_setting_card import ( + ComboBoxSettingCard, +) +from one_dragon_qt.widgets.setting_card.expand_setting_card_group import ( + ExpandSettingCardGroup, +) +from one_dragon_qt.widgets.setting_card.gamepad_action_key_card import ( + GamepadActionKeyCard, +) from one_dragon_qt.widgets.setting_card.key_setting_card import KeySettingCard -from one_dragon_qt.widgets.setting_card.multi_push_setting_card import MultiPushSettingCard -from one_dragon_qt.widgets.setting_card.spin_box_setting_card import DoubleSpinBoxSettingCard +from one_dragon_qt.widgets.setting_card.multi_push_setting_card import ( + MultiPushSettingCard, +) +from one_dragon_qt.widgets.setting_card.spin_box_setting_card import ( + DoubleSpinBoxSettingCard, +) from one_dragon_qt.widgets.setting_card.switch_setting_card import SwitchSettingCard from one_dragon_qt.widgets.setting_card.text_setting_card import TextSettingCard from one_dragon_qt.widgets.vertical_scroll_interface import VerticalScrollInterface -from zzz_od.config.game_config import GamepadTypeEnum +from zzz_od.config.game_config import ( + ControlMethodEnum, + GameKeyAction, + GamepadActionEnum, + GamepadTypeEnum, +) from zzz_od.context.zzz_context import ZContext @@ -35,9 +59,7 @@ def get_content_widget(self) -> QWidget: content_widget = Column() content_widget.add_widget(self._get_basic_group()) - content_widget.add_widget(self._get_launch_argument_group()) - content_widget.add_widget(self._get_key_group()) - content_widget.add_widget(self._get_gamepad_group()) + content_widget.add_widget(self._get_key_settings_group()) content_widget.add_stretch(1) return content_widget @@ -49,6 +71,8 @@ def _get_basic_group(self) -> QWidget: options_enum=TypeInputWay) basic_group.addSettingCard(self.input_way_opt) + basic_group.addSettingCard(self._get_background_mode_group()) + self.hdr_btn_enable = PushButton(text=gt('启用 HDR'), icon=FluentIcon.SETTING, parent=self) self.hdr_btn_enable.clicked.connect(self._on_hdr_enable_clicked) self.hdr_btn_disable = PushButton(text=gt('禁用 HDR'), icon=FluentIcon.SETTING, parent=self) @@ -58,14 +82,75 @@ def _get_basic_group(self) -> QWidget: btn_list=[self.hdr_btn_disable, self.hdr_btn_enable]) basic_group.addSettingCard(self.hdr_btn) + basic_group.addSettingCard(self._get_launch_argument_group()) + return basic_group + def _get_background_mode_group(self) -> ExpandSettingCardGroup: + """后台模式开关 + 手柄动作键配置组(Xbox 和 DS4 各一套)。""" + background_group = ExpandSettingCardGroup( + icon=FluentIcon.ROBOT, + title='后台模式(测试版)', + content='需要虚拟手柄驱动和 PrintWindow 截图方式。运行时会短暂抢占鼠标进行点击操作,无需游戏窗口置顶。请勿在前台玩支持手柄的游戏。', + ) + + self.background_mode_switch = SwitchSettingCard( + icon=FluentIcon.ROBOT, title='后台模式(测试版)', + ) + self.background_mode_switch.value_changed.connect(self._on_background_mode_changed) + background_group.addHeaderWidget(self.background_mode_switch.btn) + + self.background_gamepad_type_opt = ComboBoxSettingCard( + icon=FluentIcon.GAME, title='后台手柄类型', + options_enum=GamepadTypeEnum, + ) + self.background_gamepad_type_opt.value_changed.connect(self._toggle_action_cards) + background_group.addHeaderWidget(self.background_gamepad_type_opt.combo_box) + + self.mouse_flash_duration_opt = DoubleSpinBoxSettingCard( + icon=FluentIcon.SPEED_HIGH, title='闪切时长(秒)', + content='后台模式切换键鼠输入时的前台停留时长,过小可能切换失败', + minimum=0.01, maximum=0.2, step=0.01, + ) + background_group.addSettingCard(self.mouse_flash_duration_opt) + + # Xbox 动作键卡片 + self._xbox_action_cards: dict[str, GamepadActionKeyCard] = {} + for action in GamepadActionEnum: + action_name: str = action.value.value + if not action_name: + continue + card = GamepadActionKeyCard( + icon=FluentIcon.GAME, + title=action.value.ui_text, + modifier_enum=XboxButtonEnum, + button_enum=XboxButtonEnum, + ) + self._xbox_action_cards[action_name] = card + background_group.addSettingCard(card) + + # DS4 动作键卡片 + self._ds4_action_cards: dict[str, GamepadActionKeyCard] = {} + for action in GamepadActionEnum: + action_name: str = action.value.value + if not action_name: + continue + card = GamepadActionKeyCard( + icon=FluentIcon.GAME, + title=action.value.ui_text, + modifier_enum=Ds4ButtonEnum, + button_enum=Ds4ButtonEnum, + ) + self._ds4_action_cards[action_name] = card + background_group.addSettingCard(card) + + return background_group + def _get_launch_argument_group(self) -> QWidget: - launch_argument_group = SettingCardGroup(gt('启动参数')) + launch_argument_group = ExpandSettingCardGroup(icon=FluentIcon.SETTING, title='启动参数') - self.launch_argument_switch = SwitchSettingCard(icon=FluentIcon.SETTING, title='启用') - self.launch_argument_switch.value_changed.connect(self._on_launch_argument_switch_changed) - launch_argument_group.addSettingCard(self.launch_argument_switch) + self.launch_argument_switch = SwitchSettingCard(icon=FluentIcon.SETTING, title='启动参数') + launch_argument_group.addHeaderWidget(self.launch_argument_switch.btn) self.screen_size_opt = ComboBoxSettingCard(icon=FluentIcon.FIT_PAGE, title='窗口尺寸', options_enum=ScreenSizeEnum) launch_argument_group.addSettingCard(self.screen_size_opt) @@ -88,166 +173,64 @@ def _get_launch_argument_group(self) -> QWidget: return launch_argument_group - def _get_key_group(self) -> QWidget: - key_group = SettingCardGroup(gt('游戏按键')) - - self.key_normal_attack_opt = KeySettingCard(icon=FluentIcon.GAME, title='普通攻击') - key_group.addSettingCard(self.key_normal_attack_opt) - - self.key_dodge_opt = KeySettingCard(icon=FluentIcon.GAME, title='闪避') - key_group.addSettingCard(self.key_dodge_opt) - - self.key_switch_next_opt = KeySettingCard(icon=FluentIcon.GAME, title='角色切换-下一个') - key_group.addSettingCard(self.key_switch_next_opt) - - self.key_switch_prev_opt = KeySettingCard(icon=FluentIcon.GAME, title='角色切换-上一个') - key_group.addSettingCard(self.key_switch_prev_opt) + def _get_key_settings_group(self) -> QWidget: + key_settings_group = SettingCardGroup(gt('按键设置')) - self.key_special_attack_opt = KeySettingCard(icon=FluentIcon.GAME, title='特殊攻击') - key_group.addSettingCard(self.key_special_attack_opt) + self.control_method_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='操控方式', + content='仅影响自动战斗。如需使用手柄,请先安装虚拟手柄依赖。', + options_enum=ControlMethodEnum) + self.control_method_opt.value_changed.connect(self._on_control_method_changed) + key_settings_group.addSettingCard(self.control_method_opt) - self.key_ultimate_opt = KeySettingCard(icon=FluentIcon.GAME, title='终结技') - key_group.addSettingCard(self.key_ultimate_opt) + self._keyboard_group = self._get_keyboard_group() + self._gamepad_group = self._get_gamepad_group() + key_settings_group.addSettingCard(self._keyboard_group) + key_settings_group.addSettingCard(self._gamepad_group) - self.key_interact_opt = KeySettingCard(icon=FluentIcon.GAME, title='交互') - key_group.addSettingCard(self.key_interact_opt) + return key_settings_group - self.key_chain_left_opt = KeySettingCard(icon=FluentIcon.GAME, title='连携技-左') - key_group.addSettingCard(self.key_chain_left_opt) + def _get_keyboard_group(self) -> ExpandSettingCardGroup: + key_group = ExpandSettingCardGroup(icon=FluentIcon.GAME, title='键盘按键') - self.key_chain_right_opt = KeySettingCard(icon=FluentIcon.GAME, title='连携技-右') - key_group.addSettingCard(self.key_chain_right_opt) - - self.key_move_w_opt = KeySettingCard(icon=FluentIcon.GAME, title='移动-前') - key_group.addSettingCard(self.key_move_w_opt) - - self.key_move_s_opt = KeySettingCard(icon=FluentIcon.GAME, title='移动-后') - key_group.addSettingCard(self.key_move_s_opt) - - self.key_move_a_opt = KeySettingCard(icon=FluentIcon.GAME, title='移动-左') - key_group.addSettingCard(self.key_move_a_opt) - - self.key_move_d_opt = KeySettingCard(icon=FluentIcon.GAME, title='移动-右') - key_group.addSettingCard(self.key_move_d_opt) - - self.key_lock_opt = KeySettingCard(icon=FluentIcon.GAME, title='锁定敌人') - key_group.addSettingCard(self.key_lock_opt) - - self.key_chain_cancel_opt = KeySettingCard(icon=FluentIcon.GAME, title='连携技-取消') - key_group.addSettingCard(self.key_chain_cancel_opt) + self._key_cards: dict[GameKeyAction, KeySettingCard] = {} + for action in GameKeyAction: + card = KeySettingCard(icon=FluentIcon.GAME, title=action.value.label) + key_group.addSettingCard(card) + self._key_cards[action] = card return key_group - def _get_gamepad_group(self) -> QWidget: - gamepad_group = SettingCardGroup(gt('手柄按键')) + def _get_gamepad_group(self) -> ExpandSettingCardGroup: + gamepad_group = ExpandSettingCardGroup(icon=FluentIcon.GAME, title='手柄按键') - self.gamepad_type_opt = ComboBoxSettingCard( - icon=FluentIcon.GAME, title='手柄类型', - content='需先安装虚拟手柄依赖,参考文档或使用安装器。仅在闪避助手生效。', - options_enum=GamepadTypeEnum - ) - self.gamepad_type_opt.value_changed.connect(self._on_gamepad_type_changed) - gamepad_group.addSettingCard(self.gamepad_type_opt) + gamepad_display_combo = ComboBox() + gamepad_display_combo.set_items([GamepadTypeEnum.XBOX.value, GamepadTypeEnum.DS4.value]) + gamepad_display_combo.currentIndexChanged.connect(self._toggle_gamepad_cards) + gamepad_group.addHeaderWidget(gamepad_display_combo) # xbox self.xbox_key_press_time_opt = DoubleSpinBoxSettingCard(icon=FluentIcon.GAME, title='单次按键持续时间(秒)', content='自行调整,过小可能按键被吞,过大可能影响操作') gamepad_group.addSettingCard(self.xbox_key_press_time_opt) - self.xbox_key_normal_attack_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='普通攻击', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_normal_attack_opt) - - self.xbox_key_dodge_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='闪避', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_dodge_opt) - - self.xbox_key_switch_next_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='角色切换-下一个', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_switch_next_opt) - - self.xbox_key_switch_prev_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='角色切换-上一个', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_switch_prev_opt) - - self.xbox_key_special_attack_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='特殊攻击', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_special_attack_opt) - - self.xbox_key_ultimate_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='终结技', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_ultimate_opt) - - self.xbox_key_interact_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='交互', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_interact_opt) - - self.xbox_key_chain_left_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='连携技-左', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_chain_left_opt) - - self.xbox_key_chain_right_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='连携技-右', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_chain_right_opt) - - self.xbox_key_move_w_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-前', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_move_w_opt) - - self.xbox_key_move_s_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-后', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_move_s_opt) - - self.xbox_key_move_a_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-左', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_move_a_opt) - - self.xbox_key_move_d_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-右', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_move_d_opt) - - self.xbox_key_lock_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='锁定敌人', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_lock_opt) - - self.xbox_key_chain_cancel_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='连携技-取消', options_enum=XboxButtonEnum) - gamepad_group.addSettingCard(self.xbox_key_chain_cancel_opt) + self._xbox_cards: dict[GameKeyAction, ComboBoxSettingCard] = {} + for action in GameKeyAction: + card = ComboBoxSettingCard(icon=FluentIcon.GAME, title=action.value.label, options_enum=XboxButtonEnum) + gamepad_group.addSettingCard(card) + self._xbox_cards[action] = card # ds4 self.ds4_key_press_time_opt = DoubleSpinBoxSettingCard(icon=FluentIcon.GAME, title='单次按键持续时间(秒)', content='自行调整,过小可能按键被吞,过大可能影响操作') gamepad_group.addSettingCard(self.ds4_key_press_time_opt) - self.ds4_key_normal_attack_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='普通攻击', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_normal_attack_opt) - - self.ds4_key_dodge_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='闪避', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_dodge_opt) - - self.ds4_key_switch_next_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='角色切换-下一个', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_switch_next_opt) - - self.ds4_key_switch_prev_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='角色切换-上一个', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_switch_prev_opt) - - self.ds4_key_special_attack_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='特殊攻击', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_special_attack_opt) - - self.ds4_key_ultimate_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='终结技', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_ultimate_opt) - - self.ds4_key_interact_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='交互', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_interact_opt) - - self.ds4_key_chain_left_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='连携技-左', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_chain_left_opt) - - self.ds4_key_chain_right_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='连携技-右', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_chain_right_opt) + self._ds4_cards: dict[GameKeyAction, ComboBoxSettingCard] = {} + for action in GameKeyAction: + card = ComboBoxSettingCard(icon=FluentIcon.GAME, title=action.value.label, options_enum=Ds4ButtonEnum) + gamepad_group.addSettingCard(card) + self._ds4_cards[action] = card - self.ds4_key_move_w_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-前', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_move_w_opt) - - self.ds4_key_move_s_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-后', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_move_s_opt) - - self.ds4_key_move_a_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-左', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_move_a_opt) - - self.ds4_key_move_d_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='移动-右', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_move_d_opt) - - self.ds4_key_lock_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='锁定敌人', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_lock_opt) - - self.ds4_key_chain_cancel_opt = ComboBoxSettingCard(icon=FluentIcon.GAME, title='连携技-取消', options_enum=Ds4ButtonEnum) - gamepad_group.addSettingCard(self.ds4_key_chain_cancel_opt) + gamepad_display_combo.setCurrentIndex(0) return gamepad_group @@ -256,113 +239,76 @@ def on_interface_shown(self) -> None: self.input_way_opt.init_with_adapter(self.ctx.game_config.type_input_way_adapter) + self.background_mode_switch.init_with_adapter(self.ctx.game_config.get_prop_adapter('background_mode')) + self.background_gamepad_type_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('background_gamepad_type')) + self.mouse_flash_duration_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('mouse_flash_duration')) + for action_name, card in self._xbox_action_cards.items(): + card.init_with_adapter(self.ctx.game_config.get_prop_adapter(f'xbox_action_{action_name}')) + for action_name, card in self._ds4_action_cards.items(): + card.init_with_adapter(self.ctx.game_config.get_prop_adapter(f'ds4_action_{action_name}')) + self._toggle_action_cards() + self.launch_argument_switch.init_with_adapter(self.ctx.game_config.get_prop_adapter('launch_argument')) self.screen_size_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('screen_size')) self.full_screen_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('full_screen')) self.popup_window_switch.init_with_adapter(self.ctx.game_config.get_prop_adapter('popup_window')) self.monitor_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('monitor')) self.launch_argument_advance.init_with_adapter(self.ctx.game_config.get_prop_adapter('launch_argument_advance')) - self._update_launch_argument_part() - - self.key_normal_attack_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_normal_attack')) - self.key_dodge_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_dodge')) - self.key_switch_next_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_switch_next')) - self.key_switch_prev_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_switch_prev')) - self.key_special_attack_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_special_attack')) - self.key_ultimate_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_ultimate')) - self.key_interact_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_interact')) - self.key_chain_left_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_chain_left')) - self.key_chain_right_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_chain_right')) - self.key_move_w_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_move_w')) - self.key_move_s_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_move_s')) - self.key_move_a_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_move_a')) - self.key_move_d_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_move_d')) - self.key_lock_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_lock')) - self.key_chain_cancel_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('key_chain_cancel')) - - self._update_gamepad_part() - - def _update_gamepad_part(self) -> None: - """ - 手柄部分更新显示 - :return: - """ - self.gamepad_type_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('gamepad_type')) - - is_xbox = self.ctx.game_config.gamepad_type == GamepadTypeEnum.XBOX.value.value - self.xbox_key_press_time_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_press_time')) - self.xbox_key_normal_attack_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_normal_attack')) - self.xbox_key_dodge_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_dodge')) - self.xbox_key_switch_next_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_switch_next')) - self.xbox_key_switch_prev_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_switch_prev')) - self.xbox_key_special_attack_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_special_attack')) - self.xbox_key_ultimate_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_ultimate')) - self.xbox_key_interact_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_interact')) - self.xbox_key_chain_left_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_chain_left')) - self.xbox_key_chain_right_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_chain_right')) - self.xbox_key_move_w_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_move_w')) - self.xbox_key_move_s_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_move_s')) - self.xbox_key_move_a_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_move_a')) - self.xbox_key_move_d_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_move_d')) - self.xbox_key_lock_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_lock')) - self.xbox_key_chain_cancel_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_chain_cancel')) + self.control_method_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('control_method')) - self.xbox_key_press_time_opt.setVisible(is_xbox) - self.xbox_key_normal_attack_opt.setVisible(is_xbox) - self.xbox_key_dodge_opt.setVisible(is_xbox) - self.xbox_key_switch_next_opt.setVisible(is_xbox) - self.xbox_key_switch_prev_opt.setVisible(is_xbox) - self.xbox_key_special_attack_opt.setVisible(is_xbox) - self.xbox_key_ultimate_opt.setVisible(is_xbox) - self.xbox_key_interact_opt.setVisible(is_xbox) - self.xbox_key_chain_left_opt.setVisible(is_xbox) - self.xbox_key_chain_right_opt.setVisible(is_xbox) - self.xbox_key_move_w_opt.setVisible(is_xbox) - self.xbox_key_move_s_opt.setVisible(is_xbox) - self.xbox_key_move_a_opt.setVisible(is_xbox) - self.xbox_key_move_d_opt.setVisible(is_xbox) - self.xbox_key_lock_opt.setVisible(is_xbox) - self.xbox_key_chain_cancel_opt.setVisible(is_xbox) - - is_ds4 = self.ctx.game_config.gamepad_type == GamepadTypeEnum.DS4.value.value + for action, card in self._key_cards.items(): + card.init_with_adapter(self.ctx.game_config.get_prop_adapter(f'key_{action.value.value}')) + + self.xbox_key_press_time_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('xbox_key_press_time')) + for action, card in self._xbox_cards.items(): + card.init_with_adapter(self.ctx.game_config.get_prop_adapter(f'xbox_key_{action.value.value}')) self.ds4_key_press_time_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_press_time')) - self.ds4_key_normal_attack_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_normal_attack')) - self.ds4_key_dodge_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_dodge')) - self.ds4_key_switch_next_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_switch_next')) - self.ds4_key_switch_prev_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_switch_prev')) - self.ds4_key_special_attack_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_special_attack')) - self.ds4_key_ultimate_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_ultimate')) - self.ds4_key_interact_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_interact')) - self.ds4_key_chain_left_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_chain_left')) - self.ds4_key_chain_right_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_chain_right')) - self.ds4_key_move_w_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_move_w')) - self.ds4_key_move_s_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_move_s')) - self.ds4_key_move_a_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_move_a')) - self.ds4_key_move_d_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_move_d')) - self.ds4_key_lock_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_lock')) - self.ds4_key_chain_cancel_opt.init_with_adapter(self.ctx.game_config.get_prop_adapter('ds4_key_chain_cancel')) - - self.ds4_key_press_time_opt.setVisible(is_ds4) - self.ds4_key_normal_attack_opt.setVisible(is_ds4) - self.ds4_key_dodge_opt.setVisible(is_ds4) - self.ds4_key_switch_next_opt.setVisible(is_ds4) - self.ds4_key_switch_prev_opt.setVisible(is_ds4) - self.ds4_key_special_attack_opt.setVisible(is_ds4) - self.ds4_key_ultimate_opt.setVisible(is_ds4) - self.ds4_key_interact_opt.setVisible(is_ds4) - self.ds4_key_chain_left_opt.setVisible(is_ds4) - self.ds4_key_chain_right_opt.setVisible(is_ds4) - self.ds4_key_move_w_opt.setVisible(is_ds4) - self.ds4_key_move_s_opt.setVisible(is_ds4) - self.ds4_key_move_a_opt.setVisible(is_ds4) - self.ds4_key_move_d_opt.setVisible(is_ds4) - self.ds4_key_lock_opt.setVisible(is_ds4) - self.ds4_key_chain_cancel_opt.setVisible(is_ds4) - - def _on_gamepad_type_changed(self, idx: int, value: str) -> None: - self._update_gamepad_part() + for action, card in self._ds4_cards.items(): + card.init_with_adapter(self.ctx.game_config.get_prop_adapter(f'ds4_key_{action.value.value}')) + + def _toggle_gamepad_cards(self, index: int) -> None: + """根据头部下拉框切换 Xbox/DS4 卡片可见性""" + is_xbox = index == 0 + + self.xbox_key_press_time_opt.setVisible(is_xbox) + for card in self._xbox_cards.values(): + card.setVisible(is_xbox) + + self.ds4_key_press_time_opt.setVisible(not is_xbox) + for card in self._ds4_cards.values(): + card.setVisible(not is_xbox) + + def _toggle_action_cards(self) -> None: + """根据配置切换 Xbox/DS4 动作键卡片可见性。""" + is_xbox = self.ctx.game_config.background_gamepad_type == GamepadTypeEnum.XBOX.value.value + for card in self._xbox_action_cards.values(): + card.setVisible(is_xbox) + for card in self._ds4_action_cards.values(): + card.setVisible(not is_xbox) + + def _on_background_mode_changed(self, value: bool) -> None: + """后台模式开关变更时检查 vgamepad 是否可用。""" + if value and not pc_button_utils.is_vgamepad_installed(): + self.background_mode_switch.setValue(False, emit_signal=False) + self.ctx.game_config.background_mode = False + InfoBar.warning( + title='后台模式不可用', + content='未检测到 vgamepad / ViGEmBus,请先安装虚拟手柄驱动', + parent=self, duration=5000, + ) + + def _on_control_method_changed(self, _index: int, value: str) -> None: + """操控方式变更时检查 vgamepad 是否可用。""" + if value != ControlMethodEnum.KEYBOARD.value.value and not pc_button_utils.is_vgamepad_installed(): + self.control_method_opt.setValue(ControlMethodEnum.KEYBOARD.value.value, emit_signal=False) + self.ctx.game_config.control_method = ControlMethodEnum.KEYBOARD.value.value + InfoBar.warning( + title='手柄操控不可用', + content='未检测到 vgamepad / ViGEmBus,请先安装虚拟手柄驱动', + parent=self, duration=5000, + ) def _on_hdr_enable_clicked(self) -> None: self.hdr_btn_enable.setEnabled(False) @@ -375,18 +321,3 @@ def _on_hdr_disable_clicked(self) -> None: self.hdr_btn_enable.setEnabled(True) cmd_utils.run_command(['reg', 'add', 'HKCU\\Software\\Microsoft\\DirectX\\UserGpuPreferences', '/v', self.ctx.game_account_config.game_path, '/d', 'AutoHDREnable=2096;', '/f']) - - def _update_launch_argument_part(self) -> None: - """ - 启动参数部分更新显示 - :return: - """ - value = self.ctx.game_config.launch_argument - self.screen_size_opt.setVisible(value) - self.full_screen_opt.setVisible(value) - self.popup_window_switch.setVisible(value) - self.monitor_opt.setVisible(value) - self.launch_argument_advance.setVisible(value) - - def _on_launch_argument_switch_changed(self) -> None: - self._update_launch_argument_part() diff --git a/src/zzz_od/gui/zzz_installer.py b/src/zzz_od/gui/zzz_installer.py index 32425ff659..b73fa3596a 100644 --- a/src/zzz_od/gui/zzz_installer.py +++ b/src/zzz_od/gui/zzz_installer.py @@ -18,7 +18,7 @@ icon_path = Path.cwd() / 'assets/ui/logo.ico' installer_dir = Path(sys.argv[0]).resolve().parent - picker_window = DirectoryPickerWindow(icon_path=icon_path) + picker_window = DirectoryPickerWindow(icon_path=icon_path, installer_dir=str(installer_dir)) picker_window.exec() work_dir = picker_window.selected_directory if not work_dir: diff --git a/src/zzz_od/hollow_zero/hollow_zero_data_service.py b/src/zzz_od/hollow_zero/hollow_zero_data_service.py index 05a5c1e907..2290832d1d 100644 --- a/src/zzz_od/hollow_zero/hollow_zero_data_service.py +++ b/src/zzz_od/hollow_zero/hollow_zero_data_service.py @@ -2,9 +2,7 @@ import os from typing import List, Optional, Tuple -import yaml - -from one_dragon.utils import os_utils +from one_dragon.utils import os_utils, yaml_utils from one_dragon.utils.i18_utils import gt from one_dragon.utils.log_utils import log from zzz_od.hollow_zero.game_data.hollow_zero_event import HallowZeroEvent, HollowZeroEntry @@ -40,7 +38,7 @@ def _load_normal_events(self): file_path = os.path.join(dir_path, file_name) try: with open(file_path, 'r', encoding='utf-8') as file: - event_list: List[dict] = yaml.safe_load(file) + event_list: list[dict] = yaml_utils.safe_load(file) events = [HallowZeroEvent(**i) for i in event_list] for e in events: e.on_the_right = True @@ -66,7 +64,7 @@ def _load_entry_list(self): try: with open(file_path, 'r', encoding='utf-8') as file: - entry_list: List[dict] = yaml.safe_load(file) + entry_list: list[dict] = yaml_utils.safe_load(file) for i in entry_list: entry = HollowZeroEntry(**i) self.entry_list.append(entry) @@ -88,7 +86,7 @@ def _load_resonium(self) -> None: try: with open(file_path, 'r', encoding='utf-8') as file: - entry_list: List[dict] = yaml.safe_load(file) + entry_list: list[dict] = yaml_utils.safe_load(file) for i in entry_list: item = Resonium(**i) self.resonium_list.append(item) @@ -323,4 +321,4 @@ def get_no_battle_list(self) -> List[str]: if __name__ == '__main__': _data = HallowZeroDataService() - _data.match_resonium_by_ocr_full('[强聚合徽标') \ No newline at end of file + _data.match_resonium_by_ocr_full('[强聚合徽标') diff --git a/src/zzz_od/operation/back_to_normal_world.py b/src/zzz_od/operation/back_to_normal_world.py index 5816cc9a41..f561f453aa 100644 --- a/src/zzz_od/operation/back_to_normal_world.py +++ b/src/zzz_od/operation/back_to_normal_world.py @@ -15,29 +15,45 @@ class BackToNormalWorld(ZOperation): - def __init__(self, ctx: ZContext): + def __init__(self, ctx: ZContext, ensure_normal_world: bool = False, allow_battle: bool = False): """ 需要保证在任何情况下调用,都能返回大世界,让后续的应用可执行 - :param ctx: + + Args: + ctx (ZContext): 上下文 + ensure_normal_world (bool): 是否回到普通大世界 + allow_battle (bool): 是否允许在战斗状态直接返回成功(锄大地传送后用,让调用方处理战斗)。 + 启用时调用方必须处理返回的 status='大世界-战斗',否则角色将卡在战斗画面。 """ ZOperation.__init__(self, ctx, op_name=gt('返回大世界')) + self.ensure_normal_world: bool = ensure_normal_world # 是否回到普通大世界 + self.allow_battle: bool = allow_battle # 是否允许战斗状态直接返回 + self.handle_init() + + def handle_init(self) -> None: self.last_dialog_idx: int = -1 # 上次选择的对话选项下标 self.click_exit_battle: bool = False # 是否点击了退出战斗 - self.click_escape_stuck: bool = False # 是否点击了脱离卡死 + self.prefer_dialog_confirm: bool = False # 第一次优先取消,后续确认/取消轮流点击 @node_from(from_name='打开地图', success=False) + @node_from(from_name='执行传送') + @node_from(from_name='执行传送', success=False) + @node_from(from_name='确认脱离卡死', success=False) @operation_node(name='画面识别', is_start_node=True, node_max_retry_times=60) def check_screen_and_run(self) -> OperationRoundResult: """ 识别游戏画面 :return: """ + screen_name_list = ['大世界-普通', '大世界-勘域'] current_screen = self.check_and_update_current_screen() - if current_screen in ['大世界-普通', '大世界-勘域']: - if self.click_escape_stuck: # 脱离卡死后到达大世界,立即打开地图传送到录像店 - self.click_escape_stuck = False - return self.round_success('脱离卡死-传送') + if current_screen in screen_name_list: + if current_screen == '大世界-勘域': + already_transport = self.previous_node.name == '执行传送' and self.previous_node.is_success + if self.ensure_normal_world and not already_transport: + return self.round_success('传送到录像店') + return self.round_success(status=current_screen) result = self.round_by_goto_screen(screen=self.last_screenshot, screen_name='大世界-普通', retry_wait=None) @@ -48,14 +64,6 @@ def check_screen_and_run(self) -> OperationRoundResult: and self.ctx.screen_loader.current_screen_name is not None): return self.round_wait(result.status, wait=1) - result = self.round_by_find_area_binary(self.last_screenshot, '大世界', '信息') - if result.is_success: - return self.round_success(result.status) - - result = self.round_by_find_area(self.last_screenshot, '大世界', '星期') - if result.is_success: - return self.round_success(result.status) - mini_map = self.ctx.world_patrol_service.cut_mini_map(self.last_screenshot) if mini_map.play_mask_found: return self.round_success(status='发现地图') @@ -65,10 +73,6 @@ def check_screen_and_run(self) -> OperationRoundResult: if result.is_success: return self.round_retry(result.status, wait=1) - # 部分画面有关闭按钮(置前,插件场景"关闭"和"合成(完成)"可能同时存在) - result = self.round_by_find_and_click_area(self.last_screenshot, '画面-通用', '关闭') - if result.is_success: - return self.round_retry(result.status, wait=1) # 战斗菜单-退出战斗(完全通用,包括但不限于危局强袭战!) result = self.round_by_find_and_click_area(self.last_screenshot, '战斗-菜单', '按钮-退出战斗') @@ -84,14 +88,22 @@ def check_screen_and_run(self) -> OperationRoundResult: # 战斗菜单-脱离卡死(大世界-勘域不慎进入战斗状态时使用) result = self.round_by_find_and_click_area(self.last_screenshot, '战斗-菜单', '按钮-脱离卡死') if result.is_success: - self.click_escape_stuck = True + return self.round_success('脱离卡死', wait=1) + + # 通用返回按钮(识别点击型) + # 需要在"完成"前面,某些插件场景可能会识别到'返回'和"完成"同时存在 + result = self.round_by_find_and_click_area(self.last_screenshot, '画面-通用', '返回') + if result.is_success: + return self.round_retry(result.status, wait=1) + + # 部分画面有关闭按钮 + result = self.round_by_find_and_click_area(self.last_screenshot, '画面-通用', '关闭') + if result.is_success: return self.round_retry(result.status, wait=1) - if self.click_escape_stuck: # 必须置前,因为会被通用的"取消"误判 - result = self.round_by_find_and_click_area(self.last_screenshot, '战斗-菜单', '按钮-脱离卡死-确认') - if result.is_success: - return self.round_retry(result.status, wait=1) # 等待游戏传送回大世界 - # 通用完成按钮(置后,避免插件场景"合成"被误匹配为"完成") + # 通用完成按钮 + # 某些插件场景"合成"可能会被误匹配为"完成" + # 需要在'返回'后面,购买大月卡后返回大世界一直点击“已完成购买” issue #2005 result = self.round_by_find_and_click_area(self.last_screenshot, '画面-通用', '完成') if result.is_success: return self.round_retry(result.status, wait=1) @@ -103,10 +115,16 @@ def check_screen_and_run(self) -> OperationRoundResult: op.execute() return self.round_retry(result, wait=1) - # 通用的取消按钮(例如进入游戏时,空洞继弹出来的继续对话框) - # 必须在“确认”后面,因为“确认”和“取消”一般是成对出现 - result = self.round_by_find_and_click_area(self.last_screenshot, '大世界', '对话框取消') + # 通用对话框:确认/取消轮流优先点击,避免卡在只点一种按钮 + first_area = '对话框确认' if self.prefer_dialog_confirm else '对话框取消' + second_area = '对话框取消' if self.prefer_dialog_confirm else '对话框确认' + result = self.round_by_find_and_click_area(self.last_screenshot, '大世界', first_area) + if result.is_success: + self.prefer_dialog_confirm = not self.prefer_dialog_confirm + return self.round_retry(result.status, wait=1) + result = self.round_by_find_and_click_area(self.last_screenshot, '大世界', second_area) if result.is_success: + self.prefer_dialog_confirm = not self.prefer_dialog_confirm return self.round_retry(result.status, wait=1) # 这是领取完活跃度奖励的情况 @@ -121,10 +139,14 @@ def check_screen_and_run(self) -> OperationRoundResult: # 判断在战斗画面 result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-普通攻击') if result.is_success: + if self.allow_battle: + # 锄大地传送后落地即进入战斗,直接返回成功让调用方处理战斗 + return self.round_success(status='大世界-战斗') self.round_by_click_area('战斗画面', '菜单') return self.round_retry(result.status, wait=0.5) - # 兜底的无条件返回(左上角的红色返回按钮,很多画面共有,故不能提前使用) + # 通用返回按钮(兜底点击型) + # 区域位于左上角的红色返回按钮,不进行识别,相当于点击空白区域,故不能提前使用 click_back = self.round_by_click_area('画面-通用', '返回') if click_back.is_success: # 由于上方识别可能耗时较长 @@ -136,14 +158,18 @@ def check_screen_and_run(self) -> OperationRoundResult: else: return self.round_fail() - @node_from(from_name='画面识别', status='脱离卡死-传送') + @node_from(from_name='画面识别', status='脱离卡死') + @operation_node(name='确认脱离卡死') + def confirm_escape_stuck(self) -> OperationRoundResult: + """确认脱离卡死""" + return self.round_by_find_and_click_area(self.last_screenshot, '战斗-菜单', '按钮-脱离卡死-确认', retry_wait=0.5) + + @node_from(from_name='画面识别', status='传送到录像店') + @node_from(from_name='确认脱离卡死') @operation_node(name='打开地图', node_max_retry_times=60) def open_map(self) -> OperationRoundResult: """脱离卡死后,识别到大世界立即点击地图按钮""" - result = self.round_by_find_and_click_area(self.last_screenshot, '大世界', '地图') - if result.is_success: - return self.round_success(result.status) - return self.round_retry(result.status, wait=0.5) + return self.round_by_find_and_click_area(self.last_screenshot, '大世界', '地图', retry_wait=0.5) @node_from(from_name='打开地图') @operation_node(name='执行传送') diff --git a/src/zzz_od/operation/compendium/combat_simulation.py b/src/zzz_od/operation/compendium/combat_simulation.py index e7600f4afe..91a218c4fb 100644 --- a/src/zzz_od/operation/compendium/combat_simulation.py +++ b/src/zzz_od/operation/compendium/combat_simulation.py @@ -6,7 +6,7 @@ from one_dragon.base.operation.operation import Operation from one_dragon.base.operation.operation_edge import node_from from one_dragon.base.operation.operation_node import operation_node -from one_dragon.base.operation.operation_notify import node_notify, NotifyTiming +from one_dragon.base.operation.operation_notify import NotifyTiming, node_notify from one_dragon.base.operation.operation_round_result import OperationRoundResult from one_dragon.utils import cv2_utils, str_utils from one_dragon.utils.i18_utils import gt @@ -45,7 +45,7 @@ def __init__(self, ctx: ZContext, plan: ChargePlanItem): """ ZOperation.__init__( self, ctx, - op_name='%s %s' % ( + op_name='{} {}'.format( gt('实战模拟室', 'game'), gt(plan.mission_name, 'game') ) @@ -94,7 +94,7 @@ def is_in_category_screen(self, screen) -> bool: return False target_word_list: list[str] = [gt(i.mission_type_name, 'game') for i in category.mission_type_list] match_type_cnt: int = 0 - for ocr_result in ocr_result_map.keys(): + for ocr_result in ocr_result_map: match_idx: int = str_utils.find_best_match_by_difflib(ocr_result, target_word_list) if match_idx is not None and match_idx >= 0: match_type_cnt += 1 @@ -118,7 +118,7 @@ def choose_mission(self) -> OperationRoundResult: self.scroll_count = 0 return self.round_success(status=CombatSimulation.STATUS_CHOOSE_FAIL) - if self.plan.mission_name == '代理人方案培养': + if self.plan.is_agent_plan: target_point: Point | None = None area = self.ctx.screen_loader.get_area('实战模拟室', '副本名称列表顶部') diff --git a/src/zzz_od/operation/compendium/compendium_choose_mission_type.py b/src/zzz_od/operation/compendium/compendium_choose_mission_type.py index 3fdf2e4afe..b7fd88f1b5 100644 --- a/src/zzz_od/operation/compendium/compendium_choose_mission_type.py +++ b/src/zzz_od/operation/compendium/compendium_choose_mission_type.py @@ -1,5 +1,4 @@ import difflib -from typing import Optional, ClassVar, List from one_dragon.base.geometry.point import Point from one_dragon.base.geometry.rectangle import Rect @@ -22,8 +21,6 @@ def __init__(self): class CompendiumChooseMissionType(ZOperation): - AGENT_PLAN: ClassVar[str] = '代理人方案培养' - def __init__(self, ctx: ZContext, mission_type: CompendiumMissionType): """ 已经打开了快捷手册了 选择了 Tab 和 分类 @@ -32,7 +29,7 @@ def __init__(self, ctx: ZContext, mission_type: CompendiumMissionType): """ ZOperation.__init__( self, ctx, - op_name='%s %s %s' % ( + op_name='{} {} {}'.format( gt('快捷手册', 'game'), gt('选择副本类型', 'game'), gt(mission_type.mission_type_name, 'game') @@ -43,15 +40,15 @@ def __init__(self, ctx: ZContext, mission_type: CompendiumMissionType): @operation_node(name='选择副本', is_start_node=True, node_max_retry_times=20) def choose_mission_type(self) -> OperationRoundResult: - if self.mission_type.mission_type_name == CompendiumChooseMissionType.AGENT_PLAN: - return self.round_success(status=CompendiumChooseMissionType.AGENT_PLAN) + if self.mission_type.is_agent_plan: + return self.round_success(status='代理人方案培养') area = self.ctx.screen_loader.get_area('快捷手册', '副本列表') part = cv2_utils.crop_image_only(self.last_screenshot, area.rect) - mission_type_list: List[CompendiumMissionType] = self.ctx.compendium_service.get_same_category_mission_type_list(self.mission_type.mission_type_name) + mission_type_list: list[CompendiumMissionType] = self.ctx.compendium_service.get_same_category_mission_type_list(self.mission_type.mission_type_name) if mission_type_list is None: - return self.round_fail('非法的副本分类 %s' % self.mission_type.mission_type_name) + return self.round_fail(f'非法的副本分类 {self.mission_type.mission_type_name}') before_target_cnt: int = 0 # 在目标副本前面的数量 target_idx: int = -1 @@ -70,9 +67,9 @@ def choose_mission_type(self) -> OperationRoundResult: name_to_idx[alias_name] = idx if target_idx == -1: - return self.round_fail('非法的副本分类 %s' % self.mission_type.mission_type_name) + return self.round_fail(f'非法的副本分类 {self.mission_type.mission_type_name}') - target_point: Optional[Point] = None + target_point: Point | None = None ocr_results = self.ctx.ocr.run_ocr(part) for ocr_result, mrl in ocr_results.items(): if mrl.max is None: @@ -95,7 +92,7 @@ def choose_mission_type(self) -> OperationRoundResult: return self.handle_go_button(self.last_screenshot, target_point) - @node_from(from_name='选择副本', status=AGENT_PLAN) + @node_from(from_name='选择副本', status='代理人方案培养') @operation_node(name='选择代理人方案', node_max_retry_times=10) def choose_mission_type_by_agent(self) -> OperationRoundResult: """ @@ -128,7 +125,7 @@ def handle_scroll(self, area: Rect) -> OperationRoundResult: start = area.center + Point(-100, 0) end = start + Point(0, 300 * -1) self.ctx.controller.drag_to(start=start, end=end) - return self.round_retry(status='找不到 %s' % self.mission_type.mission_type_name, wait=1) + return self.round_retry(status=f'找不到 {self.mission_type.mission_type_name}', wait=1) def handle_go_button(self, screen, target_point: Point) -> OperationRoundResult: """ @@ -139,7 +136,7 @@ def handle_go_button(self, screen, target_point: Point) -> OperationRoundResult: part = cv2_utils.crop_image_only(screen, go_rect) ocr_results = self.ctx.ocr.run_ocr(part) - target_go_point: Optional[Point] = None + target_go_point: Point | None = None for ocr_result, mrl in ocr_results.items(): if mrl.max is None: continue @@ -157,9 +154,9 @@ def handle_go_button(self, screen, target_point: Point) -> OperationRoundResult: start = area.center end = start + Point(0, -200) self.ctx.controller.drag_to(start=start, end=end) - return self.round_retry(status='找不到 %s' % '前往', wait=1) + return self.round_retry(status='找不到 前往', wait=1) - click = self.ctx.controller.click(target_go_point) + self.ctx.controller.click(target_go_point) return self.round_success(wait=1) @node_from(from_name='选择副本') diff --git a/src/zzz_od/operation/compendium/expert_challenge.py b/src/zzz_od/operation/compendium/expert_challenge.py index 225369a110..8658bc4d4a 100644 --- a/src/zzz_od/operation/compendium/expert_challenge.py +++ b/src/zzz_od/operation/compendium/expert_challenge.py @@ -229,12 +229,11 @@ def __debug_charge(): def __debug(): ctx = ZContext() - ctx.init_by_config() - ctx.init_ocr() + ctx.init() ctx.run_context.start_running() op = ExpertChallenge(ctx, ChargePlanItem( category_name='专业挑战室', - mission_type_name='恶名·杜拉罕', + mission_type_name='牲鬼·卫律使者', auto_battle_config='全配队通用', predefined_team_idx=-1 )) @@ -242,4 +241,4 @@ def __debug(): if __name__ == '__main__': - __debug_charge() + __debug() diff --git a/src/zzz_od/operation/compendium/notorious_hunt.py b/src/zzz_od/operation/compendium/notorious_hunt.py index 64c688b4fa..db2730449d 100644 --- a/src/zzz_od/operation/compendium/notorious_hunt.py +++ b/src/zzz_od/operation/compendium/notorious_hunt.py @@ -2,21 +2,14 @@ from typing import ClassVar from one_dragon.base.geometry.point import Point -from one_dragon.base.matcher.match_result import MatchResult from one_dragon.base.operation.application import application_const from one_dragon.base.operation.operation import Operation -from one_dragon.base.operation.operation_base import OperationResult from one_dragon.base.operation.operation_edge import node_from from one_dragon.base.operation.operation_node import operation_node -from one_dragon.base.operation.operation_notify import node_notify, NotifyTiming -from one_dragon.base.operation.operation_round_result import ( - OperationRoundResult, - OperationRoundResultEnum, -) +from one_dragon.base.operation.operation_notify import NotifyTiming, node_notify +from one_dragon.base.operation.operation_round_result import OperationRoundResult from one_dragon.utils import cv2_utils, str_utils from one_dragon.utils.i18_utils import gt -from one_dragon.utils.log_utils import log -from one_dragon.yolo.detect_utils import DetectFrameResult from zzz_od.application.charge_plan import charge_plan_const from zzz_od.application.charge_plan.charge_plan_config import ( ChargePlanConfig, @@ -30,13 +23,13 @@ from zzz_od.application.notorious_hunt.notorious_hunt_run_record import ( NotoriousHuntRunRecord, ) -from zzz_od.auto_battle import auto_battle_utils -from zzz_od.auto_battle.auto_battle_operator import AutoBattleOperator from zzz_od.context.zzz_context import ZContext -from zzz_od.operation.challenge_mission.check_next_after_battle import ChooseNextOrFinishAfterBattle +from zzz_od.operation.challenge_mission.check_next_after_battle import ( + ChooseNextOrFinishAfterBattle, +) from zzz_od.operation.challenge_mission.exit_in_battle import ExitInBattle -from zzz_od.operation.challenge_mission.restart_in_battle import RestartInBattle from zzz_od.operation.choose_predefined_team import ChoosePredefinedTeam +from zzz_od.operation.compendium.notorious_hunt_move import NotoriousHuntMove from zzz_od.operation.restore_charge import RestoreCharge from zzz_od.operation.zzz_operation import ZOperation from zzz_od.screen_area.screen_normal_world import ScreenNormalWorldEnum @@ -58,7 +51,7 @@ def __init__(self, ctx: ZContext, plan: ChargePlanItem, """ ZOperation.__init__( self, ctx, - op_name='%s %s' % ( + op_name='{} {}'.format( gt('恶名狩猎', 'game'), gt(plan.mission_type_name, 'game') ) @@ -84,10 +77,6 @@ def __init__(self, ctx: ZContext, plan: ChargePlanItem, self.use_charge_power: bool = use_charge_power # 是否使用电量 深度追猎 self.can_run_times: int = -1 - self.move_times: int = 0 # 移动次数 - self.no_dis_times: int = 0 # 识别不到距离的次数 - self.restart_times: int = 0 # 重新开始战斗次数 - def _match_mission_type(self, target_name: str, ocr_result: str) -> bool: """ 匹配任务类型名称(支持别名双向查找) @@ -118,7 +107,7 @@ def wait_entry_load(self) -> OperationRoundResult: @node_from(from_name='等待入口加载', status='按钮-街区') @operation_node(name='判断副本名称') def check_mission(self) -> OperationRoundResult: - if self.plan.mission_type_name == '代理人方案培养': + if self.plan.is_agent_plan: # 通过代理人进入则跳过重新选择副本 return self.round_success() area = self.ctx.screen_loader.get_area('恶名狩猎', '标题-副本名称') @@ -126,7 +115,7 @@ def check_mission(self) -> OperationRoundResult: ocr_result_map = self.ctx.ocr.run_ocr(part) is_target_mission: bool = False # 当前是否目标副本 - for ocr_result in ocr_result_map.keys(): + for ocr_result in ocr_result_map: if self._match_mission_type(self.plan.mission_type_name, ocr_result): is_target_mission = True break @@ -159,7 +148,7 @@ def choose_mission(self) -> OperationRoundResult: break find: bool = False # 当前画面有没有识别到 mission_type - for ocr_result, mrl in ocr_result_map.items(): + for ocr_result, _ in ocr_result_map.items(): if str_utils.find_by_lcs(gt(mission_type.mission_type_name, 'game'), ocr_result, percent=0.5): find = True break @@ -304,8 +293,6 @@ def init_auto_battle(self) -> OperationRoundResult: team_list = self.ctx.team_config.team_list auto_battle = team_list[self.plan.predefined_team_idx].auto_battle - self.ctx.lost_void.init_lost_void_det_model() # 借用迷失之地的模型来识别距离白点 - self.ctx.auto_battle_context.init_auto_op( sub_dir='auto_battle', op_name=auto_battle, @@ -313,7 +300,6 @@ def init_auto_battle(self) -> OperationRoundResult: return self.round_success() @node_from(from_name='加载自动战斗指令') - @node_from(from_name='主动重新开始') @operation_node(name='等待战斗画面加载', node_max_retry_times=60, is_start_node=False) def wait_battle_screen(self) -> OperationRoundResult: result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-普通攻击') @@ -327,165 +313,12 @@ def wait_battle_screen(self) -> OperationRoundResult: return self.round_retry(result.status, wait=1) @node_from(from_name='等待战斗画面加载') - @operation_node(name='移动靠近交互', node_max_retry_times=10) - def first_move(self) -> OperationRoundResult: - result = self._move_by_hint() - if result.is_success: - self.no_dis_times = 0 - elif result.result == OperationRoundResultEnum.RETRY: - self.no_dis_times += 1 - return result - - def _move_by_hint(self) -> OperationRoundResult: - """ - 根据画面显示的距离进行移动 - 出现交互的按钮时候就可以停止了 - """ - if self.move_times >= 10: - return self.round_fail() - - result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') - if result.is_success: - self.ctx.controller.interact(press=True, press_time=0.2, release=True) - return self.round_success(status=result.status, wait=2) # 按键后 等待一段时间选择鸣徽界面出现 - - det_result: DetectFrameResult = self.ctx.lost_void.detector.run(self.last_screenshot, label_list=['0001-距离']) - self.ctx.auto_battle_context.check_battle_distance(self.last_screenshot) - distance_pos = None - if len(det_result.results) > 0: - distance_pos = Point(det_result.results[0].center[0], det_result.results[0].center[1]) - - battle_result = self.round_by_find_area(self.last_screenshot, '恶名狩猎', '标识-BOSS血条') - if battle_result.is_success: - return self.round_success(battle_result.status) - - if distance_pos is None: - return self.round_retry(wait=1) - - current_distance = self.ctx.auto_battle_context.last_check_distance - - turn_result = self.turn_to_target(distance_pos) - if turn_result: - return self.round_wait(wait=0.5) - else: - self.last_distance = current_distance - log.info(f'识别距离: {current_distance}') - press_time = self.ctx.auto_battle_context.last_check_distance / 7.2 # 朱鸢测出来的速度 - # 有可能识别错距离 设置一个最大的移动时间 - press_time = min(press_time, 5) - if press_time > 0: - self.ctx.controller.move_w(press=True, press_time=press_time, release=True) - self.move_times += 1 - return self.round_wait(wait=0.5) - else: - return self.round_retry(status='识别距离失败', wait=1) - - def turn_to_target(self, target: Point) -> bool: - """ - 根据目标的位置 进行转动 - @param target: 目标位置 - @return: 是否进行了转动 - """ - if target.x < 760: - self.ctx.controller.turn_by_distance(-100) - return True - elif target.x < 860: - self.ctx.controller.turn_by_distance(-50) - return True - elif target.x < 910: - self.ctx.controller.turn_by_distance(-25) - return True - elif target.x > 1160: - self.ctx.controller.turn_by_distance(+100) - return True - elif target.x > 1060: - self.ctx.controller.turn_by_distance(+50) - return True - elif target.x > 1010: - self.ctx.controller.turn_by_distance(+25) - return True - else: - return False - - @node_from(from_name='移动靠近交互', status='按键-交互') - @operation_node(name='交互') - def move_and_interact(self) -> OperationRoundResult: - result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') - if result.is_success: - self.ctx.controller.interact(press=True, press_time=0.2, release=True) - time.sleep(2) - return self.round_retry() - else: - return self.round_success() - - @node_from(from_name='交互') - @operation_node(name='选择') - def choose_buff(self) -> OperationRoundResult: - result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-普通攻击') - if result.is_success: - return self.round_success(self.plan.mission_type_name) - - result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') - if result.is_success: - return self.round_success(self.plan.mission_type_name) - - ocr_result_map = self.ctx.ocr.run_ocr(self.last_screenshot) - choose_mr_list: list[MatchResult] = [] - - for ocr_result, mrl in ocr_result_map.items(): - if str_utils.find_by_lcs(gt('选择', 'game'), ocr_result, percent=1): - for mr in mrl: - choose_mr_list.append(mr) - - log.info('当前识别鸣徽选项数量 %d', len(choose_mr_list)) - - if len(choose_mr_list) == 0: - return self.round_retry('未识别到鸣徽选择', wait=1) - - # 按横坐标从左往右排序 - choose_mr_list.sort(key=lambda x: x.left_top.x) - - to_choose_idx = self.plan.notorious_hunt_buff_num - 1 - if to_choose_idx >= len(choose_mr_list): - to_choose_idx = 0 - - to_click = choose_mr_list[to_choose_idx].center - self.ctx.controller.click(to_click) - - return self.round_wait(wait=1) - - @node_from(from_name='选择') - @operation_node(name='选择后移动', node_max_retry_times=18) - def move_after_buff(self) -> OperationRoundResult: - """ - 选择buff后 向白色提示点移动 - :return: - """ - direction_arr = [ - 2, 3, 3, 2, 0, 1, 1, 0, # 左右右左 前后后前 往四个方向都走一段试试 - 2, 0, 3, 3, 1, 1, 2, 2, 0, 3, # 左上 右右 下下 左左 上右 在附近转一圈 - ] - result = self._move_by_hint() - if result.result == OperationRoundResultEnum.RETRY: - # 识别不到白点 可能是有因为没吃到红心 在周围随机移动 - direction = direction_arr[self.no_dis_times % len(direction_arr)] - if direction == 0: - self.ctx.controller.move_w(press=True, press_time=0.5, release=True) - elif direction == 1: - self.ctx.controller.move_s(press=True, press_time=0.5, release=True) - elif direction == 2: - self.ctx.controller.move_a(press=True, press_time=0.5, release=True) - elif direction == 3: - self.ctx.controller.move_d(press=True, press_time=0.5, release=True) - self.no_dis_times += 1 - else: - self.no_dis_times = 0 - - return result + @operation_node(name='战斗前移动') + def run_battle(self) -> OperationRoundResult: + op = NotoriousHuntMove(self.ctx, self.plan.notorious_hunt_buff_num) + return self.round_by_op_result(op.execute()) - @node_from(from_name='移动靠近交互', status='标识-BOSS血条') - @node_from(from_name='选择', success=False) # 2.1版本更新后 基本进不到选buff环节 加一个兜底 - @node_from(from_name='选择后移动') + @node_from(from_name='战斗前移动') @node_from(from_name='战斗失败', status='战斗结果-倒带') @operation_node(name='开始自动战斗') def start_auto_op(self) -> OperationRoundResult: @@ -531,6 +364,25 @@ def battle_fail_exit(self) -> OperationRoundResult: else: return self.round_retry(result.status, wait=1) + @node_from(from_name='战斗前移动', success=False) + @node_from(from_name='自动战斗', success=False, status=Operation.STATUS_TIMEOUT) + @operation_node(name='退出战斗') + def exit_battle(self) -> OperationRoundResult: + self.ctx.auto_battle_context.stop_auto_battle() + op = ExitInBattle(self.ctx, '战斗-挑战结果-失败', '按钮-退出') + return self.round_by_op_result(op.execute()) + + @node_from(from_name='退出战斗') + @operation_node(name='点击挑战结果退出') + def click_result_exit(self) -> OperationRoundResult: + result = self.round_by_find_and_click_area(screen_name='战斗-挑战结果-失败', area_name='按钮-退出', + until_not_find_all=[('战斗-挑战结果-失败', '按钮-退出')], + success_wait=1, retry_wait=1) + if result.is_success: + return self.round_fail(status=NotoriousHunt.STATUS_FIGHT_TIMEOUT) + else: + return self.round_retry(status=result.status, wait=1) + @node_from(from_name='自动战斗') @node_notify(when=NotifyTiming.CURRENT_SUCCESS, detail=True) @operation_node(name='战斗结束') @@ -578,45 +430,6 @@ def wait_back_to_entry(self) -> OperationRoundResult: return self.round_retry(result.status, wait=1) - @node_from(from_name='移动靠近交互', success=False) - @node_from(from_name='自动战斗', success=False, status=Operation.STATUS_TIMEOUT) - @operation_node(name='退出战斗') - def exit_battle(self) -> OperationRoundResult: - self.ctx.auto_battle_context.stop_auto_battle() - op = ExitInBattle(self.ctx, '战斗-挑战结果-失败', '按钮-退出') - return self.round_by_op_result(op.execute()) - - @node_from(from_name='退出战斗') - @operation_node(name='点击挑战结果退出') - def click_result_exit(self) -> OperationRoundResult: - result = self.round_by_find_and_click_area(screen_name='战斗-挑战结果-失败', area_name='按钮-退出', - until_not_find_all=[('战斗-挑战结果-失败', '按钮-退出')], - success_wait=1, retry_wait=1) - if result.is_success: - return self.round_fail(status=NotoriousHunt.STATUS_FIGHT_TIMEOUT) - else: - return self.round_retry(status=result.status, wait=1) - - @node_from(from_name='选择后移动', success=False) - @operation_node(name='主动重新开始') - def restart_battle(self) -> OperationRoundResult: - """ - 吃不到红心的情况 重新开始 - :return: - """ - if self.restart_times == 0: - op = RestartInBattle(self.ctx) - op_result = op.execute() - self.restart_times += 1 - return self.round_by_op_result(op_result) - else: - op = ExitInBattle(self.ctx) - op_result = op.execute() - if op_result.success: - return self.round_fail(status='无法开启战斗') - else: - return self.round_retry(op_result.status, wait=1) - def handle_pause(self, e=None): self.ctx.auto_battle_context.stop_auto_battle() diff --git a/src/zzz_od/operation/compendium/notorious_hunt_move.py b/src/zzz_od/operation/compendium/notorious_hunt_move.py new file mode 100644 index 0000000000..2860598edb --- /dev/null +++ b/src/zzz_od/operation/compendium/notorious_hunt_move.py @@ -0,0 +1,195 @@ +import time + +from one_dragon.base.geometry.point import Point +from one_dragon.base.matcher.match_result import MatchResult +from one_dragon.base.operation.operation_edge import node_from +from one_dragon.base.operation.operation_node import operation_node +from one_dragon.base.operation.operation_round_result import ( + OperationRoundResult, + OperationRoundResultEnum, +) +from one_dragon.utils import str_utils +from one_dragon.utils.i18_utils import gt +from one_dragon.utils.log_utils import log +from one_dragon.yolo.detect_utils import DetectFrameResult +from zzz_od.context.zzz_context import ZContext +from zzz_od.operation.zzz_operation import ZOperation + + +class NotoriousHuntMove(ZOperation): + """ + 从 NotoriousHunt 提取的移动靠近机制。 + 仅处理战斗前的移动:靠近目标 → 交互/选buff → 完成。 + + 调用方需要自行处理:加载自动战斗指令、等待画面加载、开始自动战斗。 + """ + + def __init__(self, ctx: ZContext, buff_num: int = 3): + """恶名狩猎战斗前移动操作 + + Args: + ctx: 上下文 + buff_num: 鸣徽选择编号 1~3,从左到右 + """ + ZOperation.__init__( + self, ctx, + op_name=gt('恶名狩猎战斗', 'game'), + ) + self.buff_num: int = buff_num + self.move_times: int = 0 + self.no_dis_times: int = 0 + + @operation_node(name='初始化模型', is_start_node=True) + def init_model(self) -> OperationRoundResult: + """加载迷失之地检测模型 用于识别距离白点""" + self.ctx.lost_void.init_lost_void_det_model() + return self.round_success() + + @node_from(from_name='初始化模型') + @operation_node(name='移动靠近交互', node_max_retry_times=10) + def first_move(self) -> OperationRoundResult: + result = self._move_by_hint() + if result.is_success: + self.no_dis_times = 0 + elif result.result == OperationRoundResultEnum.RETRY: + self.no_dis_times += 1 + return result + + def _move_by_hint(self) -> OperationRoundResult: + """根据画面显示的距离进行移动,出现交互按钮时停止。""" + if self.move_times >= 10: + return self.round_fail() + + result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') + if result.is_success: + self.ctx.controller.interact(press=True, press_time=0.2, release=True) + return self.round_success(status=result.status, wait=2) + + det_result: DetectFrameResult = self.ctx.lost_void.detector.run( + self.last_screenshot, label_list=['0001-距离'] + ) + self.ctx.auto_battle_context.check_battle_distance(self.last_screenshot) + distance_pos = None + if len(det_result.results) > 0: + distance_pos = Point( + det_result.results[0].center[0], + det_result.results[0].center[1], + ) + + battle_result = self.round_by_find_area( + self.last_screenshot, '恶名狩猎', '标识-BOSS血条' + ) + if battle_result.is_success: + return self.round_success(battle_result.status) + + if distance_pos is None: + return self.round_retry(wait=1) + + current_distance = self.ctx.auto_battle_context.last_check_distance + + if self._turn_to_target(distance_pos): + return self.round_wait(wait=0.5) + else: + log.info(f'识别距离: {current_distance}') + press_time = current_distance / 7.2 + press_time = min(press_time, 5) + if press_time > 0: + self.ctx.controller.move_w( + press=True, press_time=press_time, release=True + ) + self.move_times += 1 + return self.round_wait(wait=0.5) + else: + return self.round_retry(status='识别距离失败', wait=1) + + def _turn_to_target(self, target: Point) -> bool: + """根据目标的位置进行转动。""" + if target.x < 760: + self.ctx.controller.turn_by_distance(-100) + elif target.x < 860: + self.ctx.controller.turn_by_distance(-50) + elif target.x < 910: + self.ctx.controller.turn_by_distance(-25) + elif target.x > 1160: + self.ctx.controller.turn_by_distance(+100) + elif target.x > 1060: + self.ctx.controller.turn_by_distance(+50) + elif target.x > 1010: + self.ctx.controller.turn_by_distance(+25) + else: + return False + return True + + @node_from(from_name='移动靠近交互', status='按键-交互') + @operation_node(name='交互') + def move_and_interact(self) -> OperationRoundResult: + result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') + if result.is_success: + self.ctx.controller.interact(press=True, press_time=0.2, release=True) + time.sleep(2) + return self.round_retry() + return self.round_success() + + @node_from(from_name='交互') + @operation_node(name='选择') + def choose_buff(self) -> OperationRoundResult: + result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-普通攻击') + if result.is_success: + return self.round_success() + + result = self.round_by_find_area(self.last_screenshot, '战斗画面', '按键-交互') + if result.is_success: + return self.round_success() + + ocr_result_map = self.ctx.ocr.run_ocr(self.last_screenshot) + choose_mr_list: list[MatchResult] = [] + + for ocr_result, mrl in ocr_result_map.items(): + if str_utils.find_by_lcs(gt('选择', 'game'), ocr_result, percent=1): + for mr in mrl: + choose_mr_list.append(mr) + + log.info('当前识别鸣徽选项数量 %d', len(choose_mr_list)) + + if len(choose_mr_list) == 0: + return self.round_retry('未识别到鸣徽选择', wait=1) + + choose_mr_list.sort(key=lambda x: x.left_top.x) + + to_choose_idx = self.buff_num - 1 + if to_choose_idx >= len(choose_mr_list): + to_choose_idx = 0 + + self.ctx.controller.click(choose_mr_list[to_choose_idx].center) + return self.round_wait(wait=1) + + @node_from(from_name='选择') + @operation_node(name='选择后移动', node_max_retry_times=18) + def move_after_buff(self) -> OperationRoundResult: + """选择buff后向白色提示点移动。""" + direction_arr = [ + 2, 3, 3, 2, 0, 1, 1, 0, + 2, 0, 3, 3, 1, 1, 2, 2, 0, 3, + ] + result = self._move_by_hint() + if result.result == OperationRoundResultEnum.RETRY: + direction = direction_arr[self.no_dis_times % len(direction_arr)] + if direction == 0: + self.ctx.controller.move_w(press=True, press_time=0.5, release=True) + elif direction == 1: + self.ctx.controller.move_s(press=True, press_time=0.5, release=True) + elif direction == 2: + self.ctx.controller.move_a(press=True, press_time=0.5, release=True) + elif direction == 3: + self.ctx.controller.move_d(press=True, press_time=0.5, release=True) + self.no_dis_times += 1 + else: + self.no_dis_times = 0 + return result + + @node_from(from_name='移动靠近交互', status='标识-BOSS血条') + @node_from(from_name='选择', success=False) + @node_from(from_name='选择后移动') + @operation_node(name='移动完成') + def move_done(self) -> OperationRoundResult: + return self.round_success() diff --git a/src/zzz_od/operation/compendium/tp_by_compendium.py b/src/zzz_od/operation/compendium/tp_by_compendium.py index 58de0aae48..f8fa61df95 100644 --- a/src/zzz_od/operation/compendium/tp_by_compendium.py +++ b/src/zzz_od/operation/compendium/tp_by_compendium.py @@ -4,9 +4,12 @@ from one_dragon.utils.i18_utils import gt from zzz_od.context.zzz_context import ZContext from zzz_od.operation.back_to_normal_world import BackToNormalWorld -from zzz_od.operation.transport import Transport -from zzz_od.operation.compendium.compendium_choose_category import CompendiumChooseCategory -from zzz_od.operation.compendium.compendium_choose_mission_type import CompendiumChooseMissionType +from zzz_od.operation.compendium.compendium_choose_category import ( + CompendiumChooseCategory, +) +from zzz_od.operation.compendium.compendium_choose_mission_type import ( + CompendiumChooseMissionType, +) from zzz_od.operation.zzz_operation import ZOperation @@ -19,7 +22,7 @@ def __init__(self, ctx: ZContext, tab_name: str, category_name: str, mission_typ """ ZOperation.__init__( self, ctx, - op_name='%s %s %s-%s-%s' % ( + op_name='{} {} {}-{}-{}'.format( gt('传送'), gt('快捷手册', 'game'), gt(tab_name, 'game'), gt(category_name, 'game'), gt(mission_type_name or '', 'game') @@ -36,14 +39,8 @@ def __init__(self, ctx: ZContext, tab_name: str, category_name: str, mission_typ @operation_node(name='返回大世界', is_start_node=True) def back_to_world(self) -> OperationRoundResult: # 先回到大世界 - op = BackToNormalWorld(self.ctx) - result = op.execute() - if result.success and result.status == '大世界-勘域': - # 仅在需要使用快捷手册的场景下,并且返回后不处于大世界-普通时, - # 先传送到不成文规定的统一起点,避免传送确认弹窗差异 - tp = Transport(self.ctx, '录像店', '房间') # 不成文规定:先前的鼠标校准功能选择的传送点 - return self.round_by_op_result(tp.execute()) - return self.round_by_op_result(result) + op = BackToNormalWorld(self.ctx, ensure_normal_world=True) + return self.round_by_op_result(op.execute()) @node_from(from_name='返回大世界') @operation_node(name='快捷手册') diff --git a/src/zzz_od/operation/map_transport.py b/src/zzz_od/operation/map_transport.py index 17f1bc5f35..eefb17169f 100644 --- a/src/zzz_od/operation/map_transport.py +++ b/src/zzz_od/operation/map_transport.py @@ -18,9 +18,15 @@ def __init__(self, ctx: ZContext, area_name: str, tp_name: str) -> None: self.area_name: str = area_name self.tp_name: str = tp_name + self._reselect_area_times: int = 0 + @node_from(from_name='选择传送点', success=False) @operation_node(name='选择区域', is_start_node=True) def choose_area(self) -> OperationRoundResult: + self._reselect_area_times += 1 + if self._reselect_area_times > 3: + return self.round_fail(self.previous_node.result.status) + area_name_list: list[str] = [] for area in self.ctx.map_service.area_list: area_name_list.append(gt(area.area_name, 'game')) diff --git a/src/zzz_od/telemetry/config.py b/src/zzz_od/telemetry/config.py index e47d807e2c..a064b41874 100644 --- a/src/zzz_od/telemetry/config.py +++ b/src/zzz_od/telemetry/config.py @@ -2,11 +2,12 @@ 遥测配置管理 """ import os -import yaml import logging +import yaml from pathlib import Path from typing import Dict, Any, Optional +from one_dragon.utils import yaml_utils from .models import TelemetryConfig, PrivacySettings @@ -47,7 +48,7 @@ def _load_from_env(self, config: TelemetryConfig) -> None: if env_yml_path.exists(): try: with open(env_yml_path, 'r', encoding='utf-8') as f: - env_config = yaml.safe_load(f) + env_config = yaml_utils.safe_load(f) if env_config: # 加载Loki认证信息 if 'loki_tenant_id' in env_config: @@ -75,7 +76,7 @@ def _load_from_file(self, config: TelemetryConfig) -> None: """从配置文件加载配置""" try: with open(self.config_file, 'r', encoding='utf-8') as f: - yaml_data = yaml.safe_load(f) + yaml_data = yaml_utils.safe_load(f) if yaml_data and 'telemetry' in yaml_data: telemetry_config = yaml_data['telemetry'] @@ -232,7 +233,7 @@ def load_privacy_settings(self) -> PrivacySettings: try: if self.privacy_file.exists(): with open(self.privacy_file, 'r', encoding='utf-8') as f: - yaml_data = yaml.safe_load(f) + yaml_data = yaml_utils.safe_load(f) if yaml_data and 'privacy' in yaml_data: privacy_data = yaml_data['privacy'] diff --git a/src/zzz_od/win_exe/runtime_launcher.py b/src/zzz_od/win_exe/runtime_launcher.py new file mode 100644 index 0000000000..a0355e5ffb --- /dev/null +++ b/src/zzz_od/win_exe/runtime_launcher.py @@ -0,0 +1,37 @@ +import ctypes +import sys +from pathlib import Path + +from one_dragon.launcher.runtime_launcher import RuntimeLauncher +from one_dragon.version import __version__ + +# src/ 目录检查 +_SRC_DIR = Path(sys.executable).parent / "src" +if not _SRC_DIR.is_dir(): + ctypes.windll.user32.MessageBoxW( + None, + f"缺少 src 目录:\n{_SRC_DIR}\n\n请重新解压完整的 WithRuntime 压缩包。", + "OneDragon 集成启动器", + 0x10, # MB_ICONERROR + ) + sys.exit(1) + + +class ZLauncher(RuntimeLauncher): + """绝区零启动器""" + + def __init__(self) -> None: + RuntimeLauncher.__init__(self, "绝区零 一条龙 启动器", __version__) + + def _do_run_onedragon(self, launch_args: list[str]) -> None: + from zzz_od.application.zzz_application_launcher import main + main(launch_args) + + def _do_run_gui(self) -> None: + from zzz_od.gui.app import main + main() + + +if __name__ == '__main__': + launcher = ZLauncher() + launcher.run() diff --git a/tests/zzz_mcp/conftest.py b/tests/zzz_mcp/conftest.py new file mode 100644 index 0000000000..61242c003c --- /dev/null +++ b/tests/zzz_mcp/conftest.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +""" +ZZZ MCP 测试配置 +""" diff --git a/tests/zzz_mcp/test_game_operation.py b/tests/zzz_mcp/test_game_operation.py new file mode 100644 index 0000000000..aefb6dac17 --- /dev/null +++ b/tests/zzz_mcp/test_game_operation.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +""" +测试游戏启动工具功能 + +用于诊断 MCP 启动游戏工具失败的问题。 +""" + +import unittest +import asyncio +from unittest.mock import MagicMock, patch + + +class TestGameOperationBasic(unittest.TestCase): + """基础模块导入测试""" + + def test_import_zzz_mcp_modules(self): + """测试能否导入 zzz_mcp 模块""" + from zzz_mcp.context import get_zzz_context + self.assertIsNotNone(get_zzz_context) + + def test_import_zzz_od_modules(self): + """测试能否导入 zzz_od 模块""" + from zzz_od.context.zzz_context import ZContext + self.assertIsNotNone(ZContext) + + def test_get_zzz_context_when_not_initialized(self): + """测试未初始化时获取上下文""" + from zzz_mcp.context import get_zzz_context + ctx = get_zzz_context() + self.assertIsNone(ctx, "未初始化时应返回 None") + + +class TestGameOperationContext(unittest.TestCase): + """测试 ZContext 初始化和状态""" + + def setUp(self): + """每个测试前重置全局上下文""" + import zzz_mcp.context as ctx_module + ctx_module._zzz_context = None + + def tearDown(self): + """每个测试后清理全局上下文""" + import zzz_mcp.context as ctx_module + ctx_module._zzz_context = None + + def test_zzz_lifespan_initialization(self): + """测试 ZContext 通过 lifespan 初始化""" + from zzz_mcp.context import zzz_lifespan, McpContext + from mcp.server.fastmcp import FastMCP + + async def test(): + mock_mcp = MagicMock() + + async with zzz_lifespan(mock_mcp) as context: + self.assertIsInstance(context, McpContext) + self.assertIsNotNone(context.zzz) + print(f"ZContext 初始化成功,ready_for_application: {context.zzz.ready_for_application}") + print(f"Controller 类型: {type(context.zzz.controller).__name__}") + print(f"RunContext 类型: {type(context.zzz.run_context).__name__}") + + asyncio.run(test()) + + def test_zzz_context_controller(self): + """测试 Controller 是否正确初始化""" + from zzz_mcp.context import zzz_lifespan + from mcp.server.fastmcp import FastMCP + + async def test(): + mock_mcp = MagicMock() + + async with zzz_lifespan(mock_mcp) as context: + ctx = context.zzz + self.assertIsNotNone(ctx.controller, "Controller 不应为 None") + print(f"Controller 实例: {ctx.controller}") + + # 检查关键方法 + self.assertTrue(hasattr(ctx.controller, 'init_game_win'), "应有 init_game_win 方法") + self.assertTrue(hasattr(ctx.controller, 'is_game_window_ready'), "应有 is_game_window_ready 属性") + + asyncio.run(test()) + + def test_zzz_context_run_context(self): + """测试 RunContext 是否正确初始化""" + from zzz_mcp.context import zzz_lifespan + from mcp.server.fastmcp import FastMCP + + async def test(): + mock_mcp = MagicMock() + + async with zzz_lifespan(mock_mcp) as context: + ctx = context.zzz + self.assertIsNotNone(ctx.run_context, "RunContext 不应为 None") + print(f"RunContext 实例: {ctx.run_context}") + + # 检查关键方法 + self.assertTrue(hasattr(ctx.run_context, 'start_running'), "应有 start_running 方法") + self.assertTrue(hasattr(ctx.run_context, 'stop_running'), "应有 stop_running 方法") + + asyncio.run(test()) + + +class TestGameOperationTool(unittest.TestCase): + """测试游戏启动工具""" + + def setUp(self): + """每个测试前重置全局上下文""" + import zzz_mcp.context as ctx_module + ctx_module._zzz_context = None + + def tearDown(self): + """每个测试后清理全局上下文""" + import zzz_mcp.context as ctx_module + ctx_module._zzz_context = None + + def test_register_game_tools(self): + """测试游戏工具注册""" + from zzz_mcp.tools.game_operation import register_game_tools + from mcp.server.fastmcp import FastMCP + + mcp = FastMCP("test_server") + register_game_tools(mcp) + + tools = mcp._tool_manager._tools + self.assertIn("open_and_enter_game", tools) + + def test_open_and_enter_game_without_context(self): + """测试在没有 ZContext 时调用打开游戏""" + from zzz_mcp.tools.game_operation import register_game_tools + from mcp.server.fastmcp import FastMCP + + mcp = FastMCP("test_server") + register_game_tools(mcp) + + tools = mcp._tool_manager._tools + open_game_tool = tools["open_and_enter_game"] + + result = open_game_tool.fn() + self.assertIn("错误", result) + self.assertIn("未初始化", result) + + @patch('one_dragon.base.operation.application.application_run_context.ApplicationRunContext.start_running') + def test_open_and_enter_game_run_context_fails(self, mock_start_running): + """测试 run_context 启动失败""" + from zzz_mcp.context import zzz_lifespan + from zzz_mcp.tools.game_operation import register_game_tools + from mcp.server.fastmcp import FastMCP + + # 模拟 start_running 返回 False + mock_start_running.return_value = False + + async def test(): + mock_mcp = MagicMock() + + async with zzz_lifespan(mock_mcp) as context: + # 注册工具(使用新的 MCP 实例) + test_mcp = FastMCP("test_server") + register_game_tools(test_mcp) + + tools = test_mcp._tool_manager._tools + open_game_tool = tools["open_and_enter_game"] + + result = open_game_tool.fn() + print(f"结果: {result}") + + self.assertIn("错误", result) + self.assertIn("启动运行上下文", result) + + asyncio.run(test()) + + @unittest.skip("集成测试:需要实际的游戏环境,跳过自动运行") + def test_open_and_enter_game_integration(self): + """集成测试:实际测试打开游戏流程""" + from zzz_mcp.context import zzz_lifespan + from zzz_mcp.tools.game_operation import register_game_tools + from mcp.server.fastmcp import FastMCP + import time + + async def test(): + mock_mcp = MagicMock() + + async with zzz_lifespan(mock_mcp) as context: + # 注册工具 + test_mcp = FastMCP("test_server") + register_game_tools(test_mcp) + + tools = test_mcp._tool_manager._tools + open_game_tool = tools["open_and_enter_game"] + + # 实际执行打开游戏(这将花费较长时间) + print("开始执行打开游戏操作...") + start_time = time.time() + result = open_game_tool.fn() + elapsed_time = time.time() - start_time + + print(f"操作结果: {result}") + print(f"耗时: {elapsed_time:.2f} 秒") + + # 验证结果 + self.assertNotIn("错误", result, "不应该有错误") + + asyncio.run(test()) diff --git a/tools/ci/generate_install_manifest.py b/tools/ci/generate_install_manifest.py new file mode 100644 index 0000000000..29ae1476be --- /dev/null +++ b/tools/ci/generate_install_manifest.py @@ -0,0 +1,104 @@ +import argparse +import hashlib +import json +import os +from datetime import UTC, datetime +from pathlib import Path + + +def sha256(path: Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest().upper() + + +_DEFAULT_EXCLUDE_PREFIXES: list[str] = ["dist/", "deploy/build/", ".venv/", ".install/uv_cache/"] + + +def main() -> int: + parser = argparse.ArgumentParser(description="Generate install manifest for offline installer move/verify.") + parser.add_argument("--root", default=".", help="Root directory to scan (default: .)") + parser.add_argument("--output", default="install_manifest.json", help="Output manifest path") + parser.add_argument( + "--exclude-prefix", + action="append", + default=None, + metavar="PREFIX", + help=( + "Exclude path prefix (posix) relative to root. " + "Can be specified multiple times. " + f"Defaults to: {', '.join(_DEFAULT_EXCLUDE_PREFIXES)}" + ), + ) + parser.add_argument( + "--ignore-read-errors", + action="store_true", + help="Skip files that cannot be read/hashed (NOT recommended for CI)", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + output_path = Path(args.output) + output_abs = output_path.resolve() if output_path.is_absolute() else (root / output_path).resolve() + + raw_prefixes: list[str] = args.exclude_prefix if args.exclude_prefix is not None else _DEFAULT_EXCLUDE_PREFIXES + exclude_prefixes: list[str] = [] + for p in raw_prefixes: + if not p: + continue + norm = p.replace("\\", "/") + if not norm.endswith("/"): + norm += "/" + exclude_prefixes.append(norm) + + version = os.environ.get("RELEASE_VERSION") or os.environ.get("GITHUB_REF_NAME") or "" + generated_at = datetime.now(UTC).isoformat() + + entries: list[dict] = [] + for file_path in root.rglob("*"): + if not file_path.is_file(): + continue + + # 永远不要把输出文件本身纳入清单,否则二次生成时会出现 sha 与最终文件内容不一致 + try: + if file_path.resolve() == output_abs: + continue + except Exception: + # resolve 失败时退化为字符串比较 + if str(file_path).lower() == str(output_abs).lower(): + continue + + rel = file_path.relative_to(root).as_posix() + if any(rel.startswith(prefix) for prefix in exclude_prefixes): + continue + + try: + entries.append( + { + "path": rel, + "size": file_path.stat().st_size, + "sha256": sha256(file_path), + } + ) + except OSError: + if args.ignore_read_errors: + continue + raise + + entries.sort(key=lambda x: x["path"]) + + manifest = { + "version": version, + "generated_at": generated_at, + "files": entries, + } + + output_abs.parent.mkdir(parents=True, exist_ok=True) + output_abs.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ci/get_version.py b/tools/ci/get_version.py new file mode 100644 index 0000000000..1bd4eab009 --- /dev/null +++ b/tools/ci/get_version.py @@ -0,0 +1,77 @@ +import os +import re +import subprocess + + +def main() -> int: + github_ref = os.environ.get('GITHUB_REF', '') + github_output = os.environ.get('GITHUB_OUTPUT') + create_release = os.environ.get('CREATE_RELEASE', 'false').lower() == 'true' + + version = "" + should_push_tag = False + + if github_ref.startswith('refs/tags/'): + # 已由 tag 推送触发,直接使用该 tag 作为版本 + version = github_ref[10:] + elif create_release: + # 手动触发且要求创建 release:生成新的 beta 版本 + # 获取远程 tag 列表并按版本排序 + cmd = ['git', 'ls-remote', '--refs', '--tags', '--sort=-version:refname', 'origin', 'v*'] + result = subprocess.run(cmd, capture_output=True, text=True) + + latest_tag = None + if result.returncode == 0 and result.stdout.strip(): + for line in result.stdout.strip().splitlines(): + match = re.search(r'refs/tags/(v\d+\.\d+\.\d+(?:-beta\.\d+)?)$', line) + if match: + latest_tag = match.group(1) + break + + if not latest_tag: + # 仓库还没有任何符合语义版本的 tag,初始化 + version = "v0.1.0-beta.1" + else: + # 根据最新 tag 递增 + beta_match = re.match(r'^(v\d+\.\d+\.\d+)-beta\.(\d+)$', latest_tag) + if beta_match: + # 最新即为 beta,在编号上 +1 + version = f"{beta_match.group(1)}-beta.{int(beta_match.group(2)) + 1}" + else: + # 最新为稳定版本,从该稳定版本的下一位开始新的 beta 序列 + stable_match = re.match(r'^(v\d+\.\d+\.)(\d+)$', latest_tag) + if stable_match: + version = f"{stable_match.group(1)}{int(stable_match.group(2)) + 1}-beta.1" + else: + version = "v0.1.0-beta.1" + + should_push_tag = True + else: + # PR 或非发布构建 + short_hash = subprocess.run( + ['git', 'rev-parse', '--short', 'HEAD'], + capture_output=True, text=True, + ).stdout.strip() or 'unknown' + + pr_match = re.match(r'^refs/pull/(\d+)/', github_ref) + if pr_match: + version = f"pr{pr_match.group(1)}+{short_hash}" + else: + version = f"dev+{short_hash}" + + print(f"Version: {version}") + + if github_output: + with open(github_output, 'a') as f: + f.write(f"version={version}\n") + + if should_push_tag: + print(f"Creating and pushing new tag: {version}") + subprocess.run(['git', '-c', 'user.name=GitHub Actions', '-c', 'user.email=actions@github.com', 'tag', version], check=True) + subprocess.run(['git', 'push', 'origin', version], check=True) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ci/prepare_release_assets.py b/tools/ci/prepare_release_assets.py new file mode 100644 index 0000000000..7aa8a35e57 --- /dev/null +++ b/tools/ci/prepare_release_assets.py @@ -0,0 +1,356 @@ +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import time +import urllib.request +import zipfile +from pathlib import Path + + +def _log(msg: str) -> None: + print(msg, flush=True) + + +def _run(cmd: list[str], cwd: Path) -> None: + subprocess.run(cmd, cwd=str(cwd), check=True) + + +def _download(url: str, dest: Path, *, token: str | None = None, retries: int = 3, timeout: int = 60) -> None: + dest.parent.mkdir(parents=True, exist_ok=True) + + headers = { + "User-Agent": "ZenlessZoneZero-OneDragon CI", + "Accept": "application/octet-stream", + } + if token: + headers["Authorization"] = f"Bearer {token}" + + for attempt in range(1, retries + 1): + try: + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as resp, dest.open("wb") as f: + shutil.copyfileobj(resp, f) + return + except Exception as e: + _log(f"[attempt {attempt}/{retries}] Download failed: {url} — {e}") + if attempt < retries: + time.sleep(2 * attempt) + else: + raise + + +def _fetch_json(url: str, *, token: str | None = None, retries: int = 3, timeout: int = 60) -> object: + headers = { + "User-Agent": "ZenlessZoneZero-OneDragon CI", + "Accept": "application/vnd.github+json", + } + if token: + headers["Authorization"] = f"Bearer {token}" + + for attempt in range(1, retries + 1): + try: + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return json.loads(resp.read().decode("utf-8")) + except Exception as e: + _log(f"[attempt {attempt}/{retries}] JSON fetch failed: {url} — {e}") + if attempt < retries: + time.sleep(2 * attempt) + else: + raise + + +def _get_latest_model_asset(repo: str, pattern: str, *, token: str | None = None) -> dict | None: + api_url = f"https://api.github.com/repos/{repo}/releases" + data = _fetch_json(api_url, token=token) + if not isinstance(data, list): + return None + + rx = re.compile(pattern) + best: dict | None = None + max_number = -1 + + for release in data: + if not isinstance(release, dict): + continue + assets = release.get("assets") + if not isinstance(assets, list): + continue + + for asset in assets: + if not isinstance(asset, dict): + continue + name = asset.get("name") + if not isinstance(name, str): + continue + if not rx.search(name): + continue + + url = asset.get("browser_download_url") + if not isinstance(url, str) or not url: + continue + + m = re.search(r"(\d{8})\.zip$", name) + if m: + number = int(m.group(1)) + if number > max_number: + max_number = number + best = { + "url": url, + "name": name, + "version_number": number, + } + elif best is None: + best = { + "url": url, + "name": name, + "version_number": 0, + } + + return best + + +def _extract_zip(zip_path: Path, dest_dir: Path) -> None: + dest_dir.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(zip_path, "r") as zf: + zf.extractall(dest_dir) + + +def _zip_dir_contents( + root_dir: Path, + zip_path: Path, + *, + root_prefix: str, + exclude_prefixes: set[str] | None = None, +) -> None: + """把 root_dir 下的内容打进 zip,可选 root_prefix 和排除前缀。""" + zip_path.parent.mkdir(parents=True, exist_ok=True) + if zip_path.exists(): + zip_path.unlink() + + prefix = root_prefix.strip("/\\") + + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf: + for p in root_dir.rglob("*"): + if p.is_file(): + rel = p.relative_to(root_dir).as_posix() + if exclude_prefixes and any(rel.startswith(ep) for ep in exclude_prefixes): + continue + arcname = f"{prefix}/{rel}" if prefix else rel + zf.write(p, arcname) + + +def _zip_single_file(file_path: Path, zip_path: Path) -> None: + zip_path.parent.mkdir(parents=True, exist_ok=True) + if zip_path.exists(): + zip_path.unlink() + + with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zf: + zf.write(file_path, file_path.name) + + +# ---- 模型资源配置 ---- + +_MODEL_CONFIGS: list[dict[str, str]] = [ + { + "label": "ppocrv5", + "repo": "OneDragon-Anything/OneDragon-Env", + "pattern": r"ppocrv5\.zip$", + "dest_folder": "onnx_ocr", + "fallback_url": "https://github.com/OneDragon-Anything/OneDragon-Env/releases/download/ppocrv5/ppocrv5.zip", + "fallback_name": "ppocrv5", + }, + { + "label": "flash", + "repo": "OneDragon-Anything/OneDragon-YOLO", + "pattern": r"flash.*\.zip$", + "dest_folder": "flash_classifier", + "fallback_url": "https://github.com/OneDragon-Anything/OneDragon-YOLO/releases/download/zzz_model/yolov8n-640-flash-0127.zip", + "fallback_name": "yolov8n-640-flash-0127", + }, + { + "label": "hollow", + "repo": "OneDragon-Anything/OneDragon-YOLO", + "pattern": r"hollow.*\.zip$", + "dest_folder": "hollow_zero_event", + "fallback_url": "https://github.com/OneDragon-Anything/OneDragon-YOLO/releases/download/zzz_model/yolov8s-736-hollow-zero-event-0126.zip", + "fallback_name": "yolov8s-736-hollow-zero-event-0126", + }, + { + "label": "lost_void_det", + "repo": "OneDragon-Anything/OneDragon-YOLO", + "pattern": r"lost.*\.zip$", + "dest_folder": "lost_void_det", + "fallback_url": "https://github.com/OneDragon-Anything/OneDragon-YOLO/releases/download/zzz_model/yolov8n-736-lost-void-det-20250612.zip", + "fallback_name": "yolov8n-736-lost-void-det-20250612", + }, +] + + +def _download_models(model_base: Path, temp_dir: Path, *, token: str | None) -> None: + """根据 _MODEL_CONFIGS 下载并解压所有模型到 assets/models/。""" + for cfg in _MODEL_CONFIGS: + dest = model_base / cfg["dest_folder"] + dest.mkdir(parents=True, exist_ok=True) + + _log(f"Resolve {cfg['label']} model") + asset = _get_latest_model_asset(cfg["repo"], cfg["pattern"], token=token) + + tmp_zip = temp_dir / f"{cfg['label']}.zip" + if asset: + folder_name = asset["name"].removesuffix(".zip") + url = asset["url"] + else: + folder_name = cfg["fallback_name"] + url = cfg["fallback_url"] + + _log(f"Download: {url}") + _download(url, tmp_zip, token=token) + target = dest / folder_name + target.mkdir(parents=True, exist_ok=True) + _extract_zip(tmp_zip, target) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Prepare release assets and package Full/Full-Environment.") + parser.add_argument("--repo-root", default=".", help="Repository root (default: .)") + parser.add_argument("--release-version", required=True, help="Release version") + parser.add_argument("--dist-src", default="deploy/dist", help="Downloaded dist artifact directory") + parser.add_argument("--dist-name", default="dist", help="Name of dist directory after moving to parent") + parser.add_argument("--env-dir", default=".install", help="Offline env directory in repo root") + args = parser.parse_args() + + repo_root = Path(args.repo_root).resolve() + release_version = args.release_version + + # token通过环境变量注入 + token = os.environ.get("GITHUB_TOKEN", "") + + dist_src = (repo_root / args.dist_src).resolve() + if not dist_src.exists(): + raise SystemExit(f"dist-src not found: {dist_src}") + + parent_dir = repo_root.parent + dist_dir = (parent_dir / args.dist_name).resolve() + + # 1. 先把 deploy/dist 挪到 repo 外,避免后续 git clean 把产物删掉 + if dist_dir.exists(): + shutil.rmtree(dist_dir) + _log(f"Move dist: {dist_src} -> {dist_dir}") + shutil.move(str(dist_src), str(dist_dir)) + + # 2. 清理工作区(仅清理 repo 内),确保打包内容可控 + _log("Clean repo via git reset/clean") + _run(["git", "reset", "--hard", "HEAD"], cwd=repo_root) + _run(["git", "clean", "-fd"], cwd=repo_root) + + # 3. 准备 .install 离线资源 + env_dir = repo_root / args.env_dir + env_dir.mkdir(parents=True, exist_ok=True) + + _log("Download offline uv + cpython") + _download( + "https://github.com/OneDragon-Anything/OneDragon-Env/releases/download/ZenlessZoneZero-OneDragon/uv-x86_64-pc-windows-msvc.zip", + env_dir / "uv-x86_64-pc-windows-msvc.zip", + token=token or None, + ) + _download( + "https://github.com/OneDragon-Anything/OneDragon-Env/releases/download/ZenlessZoneZero-OneDragon/cpython-3.11.zip", + env_dir / "cpython-3.11.zip", + token=token or None, + ) + + # 4. 检查是否有已签名的可执行文件,如果有则替换 + signed_dir = dist_dir / "signed" + if signed_dir.exists(): + _log("Found signed executables, replacing...") + for exe_name, parent in [ + ("OneDragon-Installer.exe", dist_dir), + ("OneDragon-Launcher.exe", dist_dir), + ("OneDragon-RuntimeLauncher.exe", dist_dir / "OneDragon-RuntimeLauncher"), + ]: + signed = signed_dir / exe_name + if signed.exists(): + _log(f" Replacing {exe_name}") + shutil.copy2(signed, parent / exe_name) + + # 5. 打包启动器 + installer_exe = dist_dir / "OneDragon-Installer.exe" + launcher_exe = dist_dir / "OneDragon-Launcher.exe" + runtime_launcher_dir = dist_dir / "OneDragon-RuntimeLauncher" + runtime_launcher_exe = runtime_launcher_dir / "OneDragon-RuntimeLauncher.exe" + + if not installer_exe.exists(): + raise SystemExit(f"Missing {installer_exe}") + if not launcher_exe.exists(): + raise SystemExit(f"Missing {launcher_exe}") + if not runtime_launcher_dir.exists(): + raise SystemExit(f"Missing {runtime_launcher_dir}") + if not runtime_launcher_exe.exists(): + raise SystemExit(f"Missing {runtime_launcher_exe}") + + shutil.copy2(installer_exe, repo_root / "OneDragon-Installer.exe") + shutil.copy2(launcher_exe, repo_root / "OneDragon-Launcher.exe") + + # 打包两个启动器(集成启动器不复制到 repo_root,避免混入 Full/Full-Environment) + launcher_zip = dist_dir / "ZenlessZoneZero-OneDragon-Launcher.zip" + _zip_single_file(launcher_exe, launcher_zip) + + runtime_launcher_zip = dist_dir / "ZenlessZoneZero-OneDragon-RuntimeLauncher.zip" + _zip_dir_contents(runtime_launcher_dir, runtime_launcher_zip, root_prefix="", exclude_prefixes={"src/"}) + + # WithRuntime: 集成启动器 + src(首次安装用,无需 git clone) + with_runtime_zip = dist_dir / f"ZenlessZoneZero-OneDragon-{release_version}-WithRuntime.zip" + _log(f"Create WithRuntime zip: {with_runtime_zip}") + _zip_dir_contents(runtime_launcher_dir, with_runtime_zip, root_prefix="") + + # 6. 下载并解压模型到 assets/models + temp_dir = repo_root / "temp_models" + if temp_dir.exists(): + shutil.rmtree(temp_dir) + temp_dir.mkdir(parents=True, exist_ok=True) + + _download_models(repo_root / "assets/models", temp_dir, token=token or None) + + # 清理临时模型目录(避免打包进 Full/Full-Environment) + shutil.rmtree(temp_dir, ignore_errors=True) + + # 7. Full 包清单 + 打包(在 repo_root 下打包全部内容;zip 输出在 dist_dir 以避免自包含) + _log("Generate install manifest (Full)") + _run([sys.executable, "tools/ci/generate_install_manifest.py"], cwd=repo_root) + + full_zip = dist_dir / f"ZenlessZoneZero-OneDragon-{release_version}-Full.zip" + _log(f"Create Full zip: {full_zip}") + _zip_dir_contents(repo_root, full_zip, root_prefix=f"ZenlessZoneZero-OneDragon-{release_version}-Full") + + # 8. Full-Environment:把环境包放入 .install 后重新生成清单并打包 + env_zip = dist_dir / "ZenlessZoneZero-OneDragon-Environment.zip" + if not env_zip.exists(): + raise SystemExit(f"Missing {env_zip}") + + shutil.copy2(env_zip, env_dir / "ZenlessZoneZero-OneDragon-Environment.zip") + + _log("Generate install manifest (Full-Environment)") + _run([sys.executable, "tools/ci/generate_install_manifest.py"], cwd=repo_root) + + full_env_zip = dist_dir / f"ZenlessZoneZero-OneDragon-{release_version}-Full-Environment.zip" + _log(f"Create Full-Environment zip: {full_env_zip}") + _zip_dir_contents(repo_root, full_env_zip, root_prefix=f"ZenlessZoneZero-OneDragon-{release_version}-Full-Environment") + + # 9. 复制安装器到版本化文件名 + shutil.copy2(repo_root / "OneDragon-Installer.exe", repo_root / f"ZenlessZoneZero-OneDragon-{release_version}-Installer.exe") + + # 10. 把 dist_dir 下所有 zip 移到 repo_root,供 release step 上传 + for z in dist_dir.glob("*.zip"): + _log(f"Move zip to repo root: {z.name}") + shutil.move(str(z), str(repo_root / z.name)) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/mcp/README.md b/tools/mcp/README.md new file mode 100644 index 0000000000..0d649521c9 --- /dev/null +++ b/tools/mcp/README.md @@ -0,0 +1,75 @@ +# ZZZ OD MCP 工具 + +ZZZ OD MCP Server 安装和启动工具。 + +## 快速开始(本地开发)⭐ + +### 1. 安装 MCP Server + +```powershell +# 安装到 Claude Code +.\tools\mcp\install.ps1 + +# 检查安装状态 +.\tools\mcp\install.ps1 -Check +``` + +### 2. 启动 MCP Server + +```powershell +# 使用默认端口(23001) +uv run python src\zzz_mcp\zzz_mcp_server.py +``` + +### 3. 使用 + +在 Claude Code 中: +``` +请检查游戏窗口状态 +请捕获游戏画面 +``` + +## 远程 SSH 开发(可选)⚙️ + +**如果你通过 SSH 远程连接到游戏电脑**,需要使用 Daemon。 + +```powershell +# 安装 Daemon +.\tools\mcp\daemon\install_daemon.ps1 + +# 启动 Daemon(在游戏本机) +.\tools\mcp\daemon\start_daemon.ps1 + +# 设置开机自启 +.\tools\mcp\daemon\create_startup_shortcut.ps1 +``` + +详见:[docs/develop/zzz/mcp/remote-ssh.md](../../docs/develop/zzz/mcp/remote-ssh.md) + +## 文件说明 + +### 主服务器相关 +- `install.ps1` - 安装 MCP Server 到 Claude + +### Daemon 相关(仅 SSH 场景) +- `daemon/install_daemon.ps1` - 安装 Daemon 到 Claude +- `daemon/start_daemon.ps1` - 启动 Daemon +- `daemon/create_startup_shortcut.ps1` - 创建开机自启快捷方式 +- `daemon/zzz_od_daemon.py` - Daemon 服务器 + +## 端口说明 + +| 服务 | 端口 | 必需 | +|-----|-----|------| +| ZZZ OD MCP Server | 23001 | ✅ 是 | +| ZZZ OD Daemon | 23002 | ❌ 仅 SSH 场景 | + +## 详细文档 + +完整文档请查看:[docs/develop/zzz/mcp/](../../docs/develop/zzz/mcp/) + +- [README](../../docs/develop/zzz/mcp/README.md) - 总览 +- [安装指南](../../docs/develop/zzz/mcp/installation.md) - 安装步骤 +- [远程 SSH 开发](../../docs/develop/zzz/mcp/remote-ssh.md) - Daemon 架构 +- [架构设计](../../docs/develop/zzz/mcp/architecture.md) - 系统架构 +- [故障排查](../../docs/develop/zzz/mcp/troubleshooting.md) - 常见问题 diff --git a/tools/mcp/daemon/create_startup_shortcut.ps1 b/tools/mcp/daemon/create_startup_shortcut.ps1 new file mode 100644 index 0000000000..ada68db48b --- /dev/null +++ b/tools/mcp/daemon/create_startup_shortcut.ps1 @@ -0,0 +1,55 @@ +# Create ZZZ OD Daemon Startup Shortcut +# +# This script creates a shortcut in the Windows Startup folder +# to automatically start ZZZ OD Daemon when user logs in. + +$ErrorActionPreference = "Stop" + +# Get paths +$ProjectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent +$StartDaemonScript = Join-Path $ProjectRoot "tools\mcp\daemon\start_daemon.ps1" + +# Startup folder +$StartupFolder = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" +$ShortcutPath = Join-Path $StartupFolder "ZZZ OD Daemon.lnk" + +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "ZZZ OD Daemon - Startup Shortcut Creator" -ForegroundColor Cyan +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Project Root: $ProjectRoot" +Write-Host "Start Script: $StartDaemonScript" +Write-Host "Shortcut Path: $ShortcutPath" +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "" + +# Check if start_daemon.ps1 exists +if (-not (Test-Path $StartDaemonScript)) { + Write-Host "[ERROR] start_daemon.ps1 not found: $StartDaemonScript" -ForegroundColor Red + exit 1 +} + +# Create WScript.Shell object +$WshShell = New-Object -ComObject WScript.Shell + +# Create shortcut +$Shortcut = $WshShell.CreateShortcut($ShortcutPath) +$Shortcut.TargetPath = "powershell.exe" +$Shortcut.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$StartDaemonScript`"" +$Shortcut.WorkingDirectory = $ProjectRoot +$Shortcut.Description = "ZZZ OD MCP Server Daemon - Manages game operation server lifecycle" +$Shortcut.Save() + +# Release COM object +[System.Runtime.Interopservices.Marshal]::ReleaseComObject($WshShell) | Out-Null + +Write-Host "[SUCCESS] Shortcut created!" -ForegroundColor Green +Write-Host "" +Write-Host "Shortcut location: $ShortcutPath" -ForegroundColor Cyan +Write-Host "" +Write-Host "ZZZ OD Daemon will now automatically start when you log in." -ForegroundColor Green +Write-Host "" +Write-Host "To remove:" -ForegroundColor Yellow +Write-Host " Delete the shortcut file: $ShortcutPath" +Write-Host "" +Write-Host "============================================================" -ForegroundColor Cyan diff --git a/tools/mcp/daemon/install_daemon.ps1 b/tools/mcp/daemon/install_daemon.ps1 new file mode 100644 index 0000000000..f89a9b9281 --- /dev/null +++ b/tools/mcp/daemon/install_daemon.ps1 @@ -0,0 +1,164 @@ +# ZZZ OD Daemon Installation Script +# +# Usage: +# .\install_daemon.ps1 # Install (default port 23002) +# .\install_daemon.ps1 -Port 9001 # Specify port +# .\install_daemon.ps1 -Uninstall # Uninstall +# .\install_daemon.ps1 -Check # Check status + +param( + [string]$HostName = "127.0.0.1", + [int]$Port = 23002, + [switch]$Uninstall, + [switch]$Check +) + +$ErrorActionPreference = "Stop" + +# Get project root +$ProjectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent +$DaemonScript = Join-Path $ProjectRoot "tools\mcp\daemon\zzz_od_daemon.py" + +$DaemonKey = "zzz_od_daemon" + +function Install-Daemon { + param([string]$HostName, [int]$Port) + + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "ZZZ OD Daemon MCP Server Installation" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Project Root: $ProjectRoot" + Write-Host "Daemon Script: $DaemonScript" + Write-Host "Listen URL: http://${HostName}:${Port}/mcp" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + # Check script file + if (-not (Test-Path $DaemonScript)) { + Write-Host "[ERROR] Daemon script not found: $DaemonScript" -ForegroundColor Red + return $false + } + + # Use claude mcp command to add daemon + $McpUrl = "http://${HostName}:${Port}/mcp" + + Write-Host "Adding Daemon MCP server to Claude Code..." -ForegroundColor Cyan + $Cmd = "claude mcp add --transport http $DaemonKey $McpUrl" + Write-Host "[CMD] $Cmd" -ForegroundColor Yellow + + try { + $Output = Invoke-Expression $Cmd 2>&1 + Write-Host $Output + + if ($LASTEXITCODE -eq 0) { + Write-Host "" + Write-Host "[SUCCESS] $DaemonKey installed to Claude Code" -ForegroundColor Green + Write-Host "" + Write-Host "Next steps:" -ForegroundColor Cyan + Write-Host " 1. Start Daemon: .\start_daemon.ps1" + Write-Host " 2. Use Daemon tools in Claude Code to manage ZZZ OD MCP Server" + Write-Host "" + return $true + } else { + Write-Host "[ERROR] Installation failed" -ForegroundColor Red + return $false + } + } catch { + Write-Host "[ERROR] Installation failed: $_" -ForegroundColor Red + return $false + } +} + +function Uninstall-Daemon { + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "ZZZ OD Daemon MCP Server Uninstallation" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + Write-Host "Removing Daemon MCP server from Claude Code..." -ForegroundColor Cyan + $Cmd = "claude mcp remove $DaemonKey" + Write-Host "[CMD] $Cmd" -ForegroundColor Yellow + + try { + $Output = Invoke-Expression $Cmd 2>&1 + Write-Host $Output + + if ($LASTEXITCODE -eq 0) { + Write-Host "[SUCCESS] $DaemonKey uninstalled" -ForegroundColor Green + return $true + } else { + Write-Host "[WARN] Uninstall command completed with errors" -ForegroundColor Yellow + return $true + } + } catch { + Write-Host "[ERROR] Uninstall failed: $_" -ForegroundColor Red + return $false + } +} + +function Check-Installation { + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "ZZZ OD Daemon MCP Server Installation Status" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + $AllGood = $true + + # Check script file + if (Test-Path $DaemonScript) { + Write-Host "[OK] Daemon script exists: $DaemonScript" -ForegroundColor Green + } else { + Write-Host "[ERROR] Daemon script not found: $DaemonScript" -ForegroundColor Red + $AllGood = $false + } + + # Check port + $PortCheck = netstat -ano | Select-String ":$($Port).*LISTENING" + if ($PortCheck) { + Write-Host "[OK] Port $Port is listening" -ForegroundColor Green + Write-Host $PortCheck -ForegroundColor Cyan + } else { + Write-Host "[WARN] Port $Port not listening, Daemon may not be started" -ForegroundColor Yellow + } + + # Check claude mcp list + Write-Host "" + Write-Host "Checking Claude Code MCP servers..." -ForegroundColor Cyan + $Cmd = "claude mcp list" + Write-Host "[CMD] $Cmd" -ForegroundColor Yellow + try { + $Output = Invoke-Expression $Cmd 2>&1 + Write-Host $Output + + if ($Output -match $DaemonKey) { + Write-Host "[OK] $DaemonKey is configured in Claude Code" -ForegroundColor Green + } else { + Write-Host "[WARN] $DaemonKey not found in Claude Code" -ForegroundColor Yellow + $AllGood = $false + } + } catch { + Write-Host "[WARN] Could not check Claude Code config" -ForegroundColor Yellow + } + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + return $AllGood +} + +# Main logic +if ($Check) { + $Success = Check-Installation + if ($Success) { exit 0 } else { exit 1 } +} + +if ($Uninstall) { + $Success = Uninstall-Daemon + if ($Success) { exit 0 } else { exit 1 } +} + +# Default: install +$Success = Install-Daemon -HostName $HostName -Port $Port +if ($Success) { exit 0 } else { exit 1 } diff --git a/tools/mcp/daemon/start_daemon.ps1 b/tools/mcp/daemon/start_daemon.ps1 new file mode 100644 index 0000000000..addd4f3f67 --- /dev/null +++ b/tools/mcp/daemon/start_daemon.ps1 @@ -0,0 +1,44 @@ +# ZZZ OD Daemon 启动脚本 +# +# 使用方式: +# .\start_daemon.ps1 # 使用默认端口(8001) +# .\start_daemon.ps1 -Port 9001 # 指定端口 + +param( + [string]$HostName = "127.0.0.1", + [int]$Port = 23002 +) + +$ErrorActionPreference = "Stop" + +# 获取项目根目录 +$ProjectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent +$DaemonScript = Join-Path $ProjectRoot "tools\mcp\daemon\zzz_od_daemon.py" + +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "ZZZ OD Daemon MCP Server" -ForegroundColor Cyan +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "项目根目录: $ProjectRoot" +Write-Host "Daemon 脚本: $DaemonScript" +Write-Host "Listen URL: http://${HostName}:${Port}/mcp" +Write-Host "" +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "" + +# 检查脚本文件 +if (-not (Test-Path $DaemonScript)) { + Write-Host "[ERROR] Daemon 脚本不存在: $DaemonScript" -ForegroundColor Red + exit 1 +} + +# 切换到项目根目录 +Set-Location $ProjectRoot + +# 执行 daemon 脚本 +try { + uv run python $DaemonScript --host $HostName --port $Port +} catch { + Write-Host "[ERROR] Daemon 启动失败: $_" -ForegroundColor Red + exit 1 +} diff --git a/tools/mcp/daemon/zzz_od_daemon.py b/tools/mcp/daemon/zzz_od_daemon.py new file mode 100644 index 0000000000..37fac1c794 --- /dev/null +++ b/tools/mcp/daemon/zzz_od_daemon.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +""" +ZZZ OD Server Management MCP Server + +轻量级管理服务器,长期运行在 Session 1 中,用于管理 zzz_od MCP 服务器的启停。 +""" +import subprocess +import time +import psutil +import uvicorn +from typing import Optional +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP("ZZZ OD Server Manage") + +# 配置 +PROJECT_ROOT = r"D:\code\workspace\ZenlessZoneZero-OneDragon" +MAIN_SERVER_SCRIPT = "src/zzz_mcp/zzz_mcp_server.py" +MAIN_SERVER_PORT = 23001 + + +def find_main_server_process() -> Optional[psutil.Process]: + """查找 zzz_od MCP 主服务器进程""" + for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']): + try: + cmdline = proc.info['cmdline'] + if cmdline and any('zzz_mcp_server.py' in arg for arg in cmdline): + return proc + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + return None + + +def is_port_in_use(port: int) -> bool: + """检查端口是否被占用""" + for conn in psutil.net_connections(): + if conn.laddr.port == port and conn.status == 'LISTEN': + return True + return False + + +@mcp.tool() +def start_zzz_od_server() -> str: + """ + 启动 ZZZ OD MCP 主服务器 + + 在 Session 1 中启动游戏操作 MCP 服务器,用于游戏窗口检测和操作。 + + Returns: + str: 启动结果信息 + """ + # 检查是否已经在运行 + existing_proc = find_main_server_process() + if existing_proc: + return f"[OK] ZZZ OD MCP Server 已在运行 (PID: {existing_proc.pid})" + + if is_port_in_use(MAIN_SERVER_PORT): + return f"[WARN] 端口 {MAIN_SERVER_PORT} 已被占用,可能有其他程序在使用" + + # 启动服务器 + try: + cmd = f'cd /d "{PROJECT_ROOT}" && uv run --env-file .env python {MAIN_SERVER_SCRIPT} --port {MAIN_SERVER_PORT}' + + # 使用 POPEN 启动,不阻塞 + process = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8' + ) + + # 等待一下确保启动成功 + time.sleep(2) + + # 检查进程是否还在运行 + if process.poll() is None: + return f"[SUCCESS] ZZZ OD MCP Server 启动成功 (PID: {process.pid})\n端口: {MAIN_SERVER_PORT}" + else: + stdout, stderr = process.communicate() + error_msg = stderr if stderr else "未知错误" + return f"[ERROR] 启动失败: {error_msg}" + + except Exception as e: + return f"[ERROR] 启动异常: {str(e)}" + + +@mcp.tool() +def stop_zzz_od_server() -> str: + """ + 停止 ZZZ OD MCP 主服务器 + + 停止正在运行的 zzz_od MCP 服务器进程。 + + Returns: + str: 停止结果信息 + """ + proc = find_main_server_process() + + if not proc: + # 检查端口是否被占用 + if is_port_in_use(MAIN_SERVER_PORT): + return f"[WARN] 未找到 zzz_od_server 进程,但端口 {MAIN_SERVER_PORT} 被占用" + return "[OK] ZZZ OD MCP Server 未运行" + + try: + # 终止进程及其子进程 + children = proc.children(recursive=True) + for child in children: + child.terminate() + proc.terminate() + + # 等待进程结束 + gone, alive = psutil.wait_procs([proc] + children, timeout=5) + + # 如果还有存活进程,强制杀掉 + if alive: + for p in alive: + p.kill() + + return f"[SUCCESS] ZZZ OD MCP Server 已停止 (PID: {proc.pid})" + + except psutil.NoSuchProcess: + return "[OK] ZZZ OD MCP Server 已停止" + except Exception as e: + return f"[ERROR] 停止失败: {str(e)}" + + +@mcp.tool() +def restart_zzz_od_server() -> str: + """ + 重启 ZZZ OD MCP 主服务器 + + 先停止当前运行的服务器,然后重新启动。 + + Returns: + str: 重启结果信息 + """ + stop_result = stop_zzz_od_server() + + if "[ERROR]" in stop_result: + return f"[ERROR] 重启失败 - 停止阶段出错:\n{stop_result}" + + # 等待端口释放 + time.sleep(2) + + start_result = start_zzz_od_server() + + return f"[RESTART]\n{stop_result}\n{start_result}" + + +@mcp.tool() +def get_zzz_od_server_status() -> str: + """ + 查看 ZZZ OD MCP 主服务器状态 + + Returns: + str: 服务器状态信息 + """ + proc = find_main_server_process() + + if not proc: + port_status = "占用" if is_port_in_use(MAIN_SERVER_PORT) else "空闲" + return f"[STATUS] ZZZ OD MCP Server 未运行\n端口 {MAIN_SERVER_PORT}: {port_status}" + + try: + # 获取进程信息 + with proc.oneshot(): + pid = proc.pid + create_time = time.ctime(proc.create_time()) + cpu_percent = proc.cpu_percent(interval=0.1) + memory_info = proc.memory_info() + + # 检查子进程数量 + children = len(proc.children(recursive=True)) + + status = f"""[STATUS] ZZZ OD MCP Server 运行中 +PID: {pid} +启动时间: {create_time} +CPU 使用: {cpu_percent}% +内存使用: {memory_info.rss / 1024 / 1024:.2f} MB +子进程数: {children} +端口: {MAIN_SERVER_PORT}""" + + return status + + except Exception as e: + return f"[STATUS] ZZZ OD MCP Server 运行中 (PID: {proc.pid})\n[ERROR] 无法获取详细信息: {str(e)}" + + +if __name__ == "__main__": + # 运行管理服务器(HTTP stream,端口 8001) + import argparse + + parser = argparse.ArgumentParser(description='ZZZ OD Server Management MCP Server') + parser.add_argument('--host', default='127.0.0.1', help='Host to bind to') + parser.add_argument('--port', type=int, default=23002, help='Port to listen on') + + args = parser.parse_args() + + print("=" * 60) + print("ZZZ OD Server Management MCP Server") + print("=" * 60) + print(f"Host: {args.host}") + print(f"Port: {args.port}") + print(f"\n管理服务器地址: http://{args.host}:{args.port}/mcp") + print("\n可用工具:") + print(" - start_zzz_od_server: 启动主服务器") + print(" - stop_zzz_od_server: 停止主服务器") + print(" - restart_zzz_od_server: 重启主服务器") + print(" - get_zzz_od_server_status: 查看状态") + print("\n" + "=" * 60) + + # 获取 Starlette 应用并使用 uvicorn 运行 + app = mcp.streamable_http_app() + uvicorn.run(app, host=args.host, port=args.port) diff --git a/tools/mcp/install.ps1 b/tools/mcp/install.ps1 new file mode 100644 index 0000000000..c6b0644b27 --- /dev/null +++ b/tools/mcp/install.ps1 @@ -0,0 +1,168 @@ +# ZZZ OD MCP Server Installation Script +# +# Usage: +# .\install.ps1 # Install (default port 23001) +# .\install.ps1 -Port 9001 # Specify port +# .\install.ps1 -Uninstall # Uninstall +# .\install.ps1 -Check # Check status + +param( + [string]$HostName = "127.0.0.1", + [int]$Port = 23001, + [switch]$Uninstall, + [switch]$Check +) + +$ErrorActionPreference = "Stop" + +# Get project root +$ProjectRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$ServerScript = "src\zzz_mcp\zzz_mcp_server.py" + +# Claude Code config path +$ConfigPath = "$env:USERPROFILE\.claude.json" +$ServerKey = "zzz_od" + +function Install-Server { + param([string]$HostName, [int]$Port) + + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "ZZZ OD MCP Server Installation" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Project Root: $ProjectRoot" + Write-Host "Server Script: $ServerScript" + Write-Host "Listen URL: http://${HostName}:${Port}/mcp" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + # Check script file + $ScriptPath = Join-Path $ProjectRoot $ServerScript + if (-not (Test-Path $ScriptPath)) { + Write-Host "[ERROR] Server script not found: $ScriptPath" -ForegroundColor Red + return $false + } + + # Use claude mcp command to add server + $McpUrl = "http://${HostName}:${Port}/mcp" + + Write-Host "Adding MCP server to Claude Code..." -ForegroundColor Cyan + $Cmd = "claude mcp add --transport http $ServerKey $McpUrl" + Write-Host "[CMD] $Cmd" -ForegroundColor Yellow + + try { + $Output = Invoke-Expression $Cmd 2>&1 + Write-Host $Output + + if ($LASTEXITCODE -eq 0) { + Write-Host "" + Write-Host "[SUCCESS] $ServerKey installed to Claude Code" -ForegroundColor Green + Write-Host "" + Write-Host "Next steps:" -ForegroundColor Cyan + Write-Host " 1. Start MCP Server: uv run python $ServerScript --host $HostName --port $Port" + Write-Host " 2. Or use Daemon to manage: .\tools\mcp\daemon\start_daemon.ps1" + Write-Host "" + return $true + } else { + Write-Host "[ERROR] Installation failed" -ForegroundColor Red + return $false + } + } catch { + Write-Host "[ERROR] Installation failed: $_" -ForegroundColor Red + return $false + } +} + +function Uninstall-Server { + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "ZZZ OD MCP Server Uninstallation" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + Write-Host "Removing MCP server from Claude Code..." -ForegroundColor Cyan + $Cmd = "claude mcp remove $ServerKey" + Write-Host "[CMD] $Cmd" -ForegroundColor Yellow + + try { + $Output = Invoke-Expression $Cmd 2>&1 + Write-Host $Output + + if ($LASTEXITCODE -eq 0) { + Write-Host "[SUCCESS] $ServerKey uninstalled" -ForegroundColor Green + return $true + } else { + Write-Host "[WARN] Uninstall command completed with errors" -ForegroundColor Yellow + return $true + } + } catch { + Write-Host "[ERROR] Uninstall failed: $_" -ForegroundColor Red + return $false + } +} + +function Check-Installation { + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "ZZZ OD MCP Server Installation Status" -ForegroundColor Cyan + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + $AllGood = $true + + # Check script file + $ScriptPath = Join-Path $ProjectRoot $ServerScript + if (Test-Path $ScriptPath) { + Write-Host "[OK] Server script exists: $ScriptPath" -ForegroundColor Green + } else { + Write-Host "[ERROR] Server script not found: $ScriptPath" -ForegroundColor Red + $AllGood = $false + } + + # Check port + $PortCheck = netstat -ano | Select-String ":$($Port).*LISTENING" + if ($PortCheck) { + Write-Host "[OK] Port $Port is listening" -ForegroundColor Green + Write-Host $PortCheck -ForegroundColor Cyan + } else { + Write-Host "[WARN] Port $Port not listening, Server may not be started" -ForegroundColor Yellow + } + + # Check claude mcp list + Write-Host "" + Write-Host "Checking Claude Code MCP servers..." -ForegroundColor Cyan + $Cmd = "claude mcp list" + Write-Host "[CMD] $Cmd" -ForegroundColor Yellow + try { + $Output = Invoke-Expression $Cmd 2>&1 + Write-Host $Output + + if ($Output -match $ServerKey) { + Write-Host "[OK] $ServerKey is configured in Claude Code" -ForegroundColor Green + } else { + Write-Host "[WARN] $ServerKey not found in Claude Code" -ForegroundColor Yellow + $AllGood = $false + } + } catch { + Write-Host "[WARN] Could not check Claude Code config" -ForegroundColor Yellow + } + + Write-Host "" + Write-Host "============================================================" -ForegroundColor Cyan + Write-Host "" + + return $AllGood +} + +# Main logic +if ($Check) { + $Success = Check-Installation + if ($Success) { exit 0 } else { exit 1 } +} + +if ($Uninstall) { + $Success = Uninstall-Server + if ($Success) { exit 0 } else { exit 1 } +} + +# Default: install +$Success = Install-Server -HostName $HostName -Port $Port +if ($Success) { exit 0 } else { exit 1 } diff --git a/uv.lock b/uv.lock index b81f4e25d5..67173f584c 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212, upload-time = "2023-09-25T09:04:50.691Z" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + [[package]] name = "audioread" version = "3.0.1" @@ -60,6 +91,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543, upload-time = "2023-11-01T04:04:58.622Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -95,6 +138,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -162,6 +221,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/57/f2e6568dbf464a4b270954e5fa3dee4a4054d163a41c0e7bf0a34eb40f0f/gensim-4.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a54bd53a0e6f991abb837f126663353657270e75be53287e8a568ada0b35b1b0", size = 24010102, upload-time = "2024-07-19T14:40:33.359Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi", marker = "sys_platform == 'win32'" }, + { name = "h11", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'win32'" }, + { name = "certifi", marker = "sys_platform == 'win32'" }, + { name = "httpcore", marker = "sys_platform == 'win32'" }, + { name = "idna", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "humanfriendly" version = "10.0" @@ -201,6 +306,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817, upload-time = "2024-05-02T12:15:00.765Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "sys_platform == 'win32'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'win32'" }, + { name = "referencing", marker = "sys_platform == 'win32'" }, + { name = "rpds-py", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -276,6 +408,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, ] +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'win32'" }, + { name = "httpx", marker = "sys_platform == 'win32'" }, + { name = "httpx-sse", marker = "sys_platform == 'win32'" }, + { name = "jsonschema", marker = "sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'win32'" }, + { name = "pydantic-settings", marker = "sys_platform == 'win32'" }, + { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'win32'" }, + { name = "python-multipart", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette", marker = "sys_platform == 'win32'" }, + { name = "starlette", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'win32'" }, + { name = "uvicorn", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + [[package]] name = "mouseinfo" version = "0.1.3" @@ -313,6 +470,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/22/e61c6c9fa3730f5ac816f12514947ebfbd42340c56807a7a81b4b7cd5786/mss-9.0.1-py3-none-any.whl", hash = "sha256:7ee44db7ab14cbea6a3eb63813c57d677a109ca5979d3b76046e4bddd3ca1a0b", size = 22193, upload-time = "2023-04-20T05:46:41.795Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numba" version = "0.60.0" @@ -444,6 +610,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/e6/2a47ce2eba1aaf287380a44270da897ada03d118a55c19595ec7b4f0831f/protobuf-3.20.2-py2.py3-none-any.whl", hash = "sha256:c9cdf251c582c16fd6a9f5e95836c90828d51b0069ad22f463761d27c6c19019", size = 162128, upload-time = "2022-09-13T21:56:47.5Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pyautogui" version = "0.9.54" @@ -476,6 +652,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "sys_platform == 'win32'" }, + { name = "pydantic-core", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic", marker = "sys_platform == 'win32'" }, + { name = "python-dotenv", marker = "sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + [[package]] name = "pygetwindow" version = "0.0.9" @@ -541,6 +761,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/e1/ed48c7074145898e5c5b0072e87be975c5bd6a1d0f08c27a1daa7064fca0/pyinstaller_hooks_contrib-2025.4-py3-none-any.whl", hash = "sha256:6c2d73269b4c484eb40051fc1acee0beb113c2cfb3b37437b8394faae6f0d072", size = 434451, upload-time = "2025-05-03T20:15:54.579Z" }, ] +[[package]] +name = "pyjwt" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "sys_platform == 'win32'" }, +] + [[package]] name = "pymsgbox" version = "1.0.9" @@ -590,6 +824,19 @@ version = "0.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/04/2ba023d5f771b645f7be0c281cdacdcd939fe13d1deb331fc5ed1a6b3a98/PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78", size = 17219, upload-time = "2022-03-16T04:45:52.36Z" } +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pyscreeze" version = "0.1.30" @@ -701,6 +948,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + [[package]] name = "python-xlib" version = "0.33" @@ -734,12 +999,12 @@ wheels = [ [[package]] name = "pywin32" -version = "306" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689, upload-time = "2023-03-25T23:50:08.499Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547, upload-time = "2023-03-25T23:50:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324, upload-time = "2023-03-25T23:50:30.904Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, ] [[package]] @@ -761,6 +1026,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", size = 144699, upload-time = "2023-07-17T23:58:05.586Z" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "sys_platform == 'win32'" }, + { name = "rpds-py", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "requests" version = "2.32.3" @@ -776,6 +1055,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, +] + [[package]] name = "ruff" version = "0.12.10" @@ -904,6 +1194,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/29/3bbdf1417e5da6da3ed5e35e4b3290cfc605fbe06b8513452a9447516532/soxr-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:2f6f55c520fb90040f604b1203f2100b70c789d973bb0fd79b221187e3841311", size = 184662, upload-time = "2024-07-25T14:18:33.955Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'win32'" }, + { name = "starlette", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + [[package]] name = "sympy" version = "1.13.0" @@ -933,11 +1249,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] @@ -949,6 +1277,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444, upload-time = "2024-06-17T13:40:07.795Z" }, ] +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform == 'win32'" }, + { name = "h11", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + [[package]] name = "vgamepad" version = "0.1.0" @@ -991,8 +1332,11 @@ dependencies = [ dev = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "matplotlib", marker = "sys_platform == 'win32'" }, + { name = "mcp", marker = "sys_platform == 'win32'" }, { name = "polib", marker = "sys_platform == 'win32'" }, + { name = "psutil", marker = "sys_platform == 'win32'" }, { name = "pyinstaller", marker = "sys_platform == 'win32'" }, + { name = "pyright", marker = "sys_platform == 'win32'" }, { name = "pytest", marker = "sys_platform == 'win32'" }, { name = "pytest-asyncio", marker = "sys_platform == 'win32'" }, { name = "pyuac", marker = "sys_platform == 'win32'" }, @@ -1024,8 +1368,11 @@ requires-dist = [ dev = [ { name = "colorama", specifier = "==0.4.6" }, { name = "matplotlib", specifier = "==3.10.3" }, + { name = "mcp", specifier = ">=1.0.0" }, { name = "polib", specifier = "==1.2.0" }, + { name = "psutil", specifier = ">=7.2.2" }, { name = "pyinstaller", specifier = "==6.7.0" }, + { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.1.0" }, { name = "pyuac", specifier = "==0.0.3" }, diff --git a/zzz-od-test b/zzz-od-test new file mode 160000 index 0000000000..13d0619a6b --- /dev/null +++ b/zzz-od-test @@ -0,0 +1 @@ +Subproject commit 13d0619a6bdb01b92c41c6b65b762b56084b43c3