From 0298548d4a10b668dd74ab299a37b31c7406ed44 Mon Sep 17 00:00:00 2001 From: SilverFox Date: Sun, 26 Jul 2026 23:38:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(island):=20=E4=BF=AE=E5=A4=8D=20?= =?UTF-8?q?=E5=B2=9B=E5=B1=BF-=E6=AF=8F=E6=97=A5=E8=A1=A5=E7=BB=99/?= =?UTF-8?q?=E7=8F=8D=E7=8F=A0=E9=87=87=E8=B4=AD=20=E5=9C=A8=E8=BE=83?= =?UTF-8?q?=E6=85=A2=E7=9A=84=E8=AE=BE=E5=A4=87=E4=B8=8A=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E5=B8=B8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/device/method/minitouch.py | 4 ++- module/island/island.py | 50 ++++++++++++++++++++++++++++++ module/island/island_air_drop.py | 38 +++-------------------- module/island/island_pearl_sell.py | 44 ++------------------------ 4 files changed, 59 insertions(+), 77 deletions(-) diff --git a/module/device/method/minitouch.py b/module/device/method/minitouch.py index f0b8e1475..a71d88580 100644 --- a/module/device/method/minitouch.py +++ b/module/device/method/minitouch.py @@ -718,6 +718,9 @@ def drag_minitouch(self, p1, p2, point_random=(-10, -10, 10, 10)): builder.move(*p2).commit().wait(140) builder.send() + builder.up().commit() + builder.send() + def island_swipe_hold_minitouch(self, p1, p2, hold_time): points = insert_swipe(p0=p1, p3=p2) builder = self.minitouch_builder @@ -725,7 +728,6 @@ def island_swipe_hold_minitouch(self, p1, p2, hold_time): builder.send() for point in points[1:]: builder.move(*point).commit().wait(10) - builder.send() builder.wait(hold_time) builder.send() builder.up().commit() diff --git a/module/island/island.py b/module/island/island.py index 6adc0f935..6db9c1c94 100644 --- a/module/island/island.py +++ b/module/island/island.py @@ -221,6 +221,54 @@ def is_in_friend_island(self): access_map = self.appear(ISLAND_ACCESS_MAP, offset=(20, 20)) return leave and access_map + def check_visit_friend_island(self, visit_button): + """点击好友拜访按钮并等待进入好友岛屿。 + + 检测逻辑分为两个阶段: + 1. 等待 ISLAND_ACCESS_MAP(右上角地图入口)出现,表示已开始加载好友岛 + 2. 等待 AIR_DROP_RUN_AWAY(顶部"离开"按钮)也出现,确认场景完全加载完毕 + 只有两者同时出现(is_in_friend_island() 为 True),才视为成功进入好友岛屿。 + """ + click_timer = Timer(3).start() + self.device.click(visit_button) + self.device.sleep(3) + for _ in self.loop(timeout=30, skip_first=False): + if self.is_in_friend_island(): + logger.info("[岛屿] 二次检测拜访状态......") + # 在等待的过程中, 先后会出现黑底黄鸡loading、UI(一闪而过)、白底沙漏loading + # 因此等待1s后进行二次确认, 避免中间UI一闪而过时出现误判 + self.device.sleep(1) + self.device.screenshot() + if self.is_in_friend_island(): + self._island_expect_friend = True + logger.info("[岛屿] 成功进入好友岛屿") + return True + if self.appear(CANT_ACCESS, offset=(20, 20)): + logger.info("[岛屿] 好友不可访问") + return False + if click_timer.reached(): + self.device.click(visit_button) + click_timer.reset() + if self.ui_additional(): + continue + logger.warning("[岛屿] 拜访好友超时") + return False + + def exit_friend_island(self): + """退出好友岛屿。""" + logger.info("[岛屿] 退出好友岛屿") + self._island_expect_friend = False + for _ in self.loop(timeout=30): + if self.appear_then_click(AIR_DROP_RUN_AWAY, offset=(20, 20), interval=2): + continue + if self.appear(ISLAND_GOTO_MAP): + logger.info("[岛屿] 成功退出好友岛屿") + return True + if self.ui_additional(): + continue + logger.warning("[岛屿] 退出好友岛屿超时") + return False + def _wait_island_map_entry(self, timeout=3): last_status = None for _ in self.loop(timeout=timeout, skip_first=False): @@ -346,6 +394,8 @@ def get_friend_destination_check_button(name): self.appear(ISLAND_CHECK, offset=(20, 20)) or (in_friend and self.is_in_friend_island()) ): + self.device.sleep(1) + logger.info(f"[岛屿] 岛屿地图进入成功: {destination}") return True logger.warning(f"[岛屿] 岛屿地图进入目的地超时: {destination}") diff --git a/module/island/island_air_drop.py b/module/island/island_air_drop.py index e06121856..efb0255f5 100644 --- a/module/island/island_air_drop.py +++ b/module/island/island_air_drop.py @@ -154,17 +154,10 @@ def find_air_drop(self): visit_button = self.calculate_visit_position( air_drop_button_x, air_drop_button_y ) - result = self.check_visit(visit_button) - if result == "skip": - logger.info("[岛屿-每日补给] 无法访问,跳过当前补给") + if not self.check_visit_friend_island(visit_button): continue - elif result == "success": - logger.info("[岛屿-每日补给] 成功进入拜访状态,返回True") - return True - elif result == "timeout": - self.island_error = True - return False has_clickable_air_drop = True + return True # 如果当前页面所有补给都不可用(全部skip或timeout) if not has_clickable_air_drop: logger.info("[岛屿-每日补给] 当前页面没有可用补给目标") @@ -199,21 +192,6 @@ def find_air_drop(self): logger.info("[岛屿-每日补给] 未检测到可用补给") return False - def check_visit(self, visit_button): - number = 20 - self.device.click(visit_button) - self.device.sleep(5) - while number: - self.device.screenshot() - if self.appear(ISLAND_ACCESS_MAP, offset=1): - return "success" - if self.appear(CANT_ACCESS, similarity=0.85): - return "skip" - self.device.sleep(1) - self.device.click(visit_button) - number -= 1 - return "timeout" - def calculate_visit_position(self, air_drop_button_x, air_drop_button_y): visit_button_x1 = air_drop_button_x + 225 # x偏移 @@ -244,15 +222,6 @@ def visit_swipe(self, distance): self.device.click(stop_button) self.device.click_record_clear() - def island_access_map_check(self): - while True: - self.device.screenshot() - if self.appear(ISLAND_ACCESS_MAP): - return False - if self.appear(CANT_ACCESS): - return True - self.device.sleep(0.5) - def island_air_drop(self): self.device.click(ISLAND_AIR_DROP_A) sleep(0.1) @@ -265,6 +234,7 @@ def island_air_drop(self): self.device.click(ISLAND_AIR_DROP_B) sleep(0.1) self.device.click(ISLAND_AIR_DROP_C) + sleep(0.5) def run_and_get(self): self.island_up(3000) @@ -299,7 +269,7 @@ def run_and_get(self): break self.island_down(500) self.island_air_drop() - self.device.click(AIR_DROP_RUN_AWAY) + self.exit_friend_island() def test(self): image = self.device.screenshot() diff --git a/module/island/island_pearl_sell.py b/module/island/island_pearl_sell.py index b308dd0a7..91b111448 100644 --- a/module/island/island_pearl_sell.py +++ b/module/island/island_pearl_sell.py @@ -361,20 +361,6 @@ def back_to_pearl_shop_or_map(self): continue return False - def exit_friend_island(self): - """退出好友岛屿。""" - logger.info("[岛屿-珍珠采购] 退出好友岛屿") - self._island_expect_friend = False - for _ in self.loop(timeout=20): - if self.appear_then_click(AIR_DROP_RUN_AWAY, offset=(20, 20), interval=2): - continue - if self.appear(ISLAND_ACCESS_MAP, offset=(20, 20)): - return True - if self.ui_additional(): - continue - logger.warning("[岛屿-珍珠采购] 退出好友岛屿超时") - return False - # ==================== 好友排名 ==================== def visit_friend_by_rank(self, mode, threshold): @@ -399,7 +385,7 @@ def visit_friend_by_rank(self, mode, threshold): self._rank_visit_target_selected = True logger.info(f"[岛屿-珍珠采购] 选择好友珍珠价格 {target['price']}") - return self.click_rank_visit(target["visit_button"]) + return self.check_visit_friend_island(target["visit_button"]) def switch_to_friend_rank_tab(self): """切换到好友排名页签。""" @@ -530,33 +516,6 @@ def rank_price_button_from_visit(self, visit_button, index): area, f"OCR_ISLAND_PEARL_RANK_PRICE_FROM_VISIT_{index}" ) - def click_rank_visit(self, visit_button): - """点击好友排名拜访按钮并等待进入好友岛屿。 - - 检测逻辑分为两个阶段: - 1. 等待 ISLAND_ACCESS_MAP(右上角地图入口)出现,表示已开始加载好友岛 - 2. 等待 AIR_DROP_RUN_AWAY(顶部"离开"按钮)也出现,确认场景完全加载完毕 - 只有两者同时出现(is_in_friend_island() 为 True),才视为成功进入好友岛屿。 - """ - click_timer = Timer(3).start() - self.device.click(visit_button) - self.device.sleep(3) - for _ in self.loop(timeout=30, skip_first=False): - if self.is_in_friend_island(): - self._island_expect_friend = True - logger.info("[岛屿-珍珠采购] 成功进入好友岛屿") - return True - if self.appear(CANT_ACCESS, offset=(20, 20)): - logger.info("[岛屿-珍珠采购] 好友不可访问") - return False - if click_timer.reached(): - self.device.click(visit_button) - click_timer.reset() - if self.ui_additional(): - continue - logger.warning("[岛屿-珍珠采购] 拜访好友超时") - return False - # ==================== OCR ==================== def ocr_pearl_price(self, kind): @@ -687,6 +646,7 @@ def trade_pearl(self, action, count): logger.warning(f"[岛屿-珍珠采购] 打开珍珠{self._action_name(action)}弹窗超时") return False + self.device.sleep(0.5) if not self.adjust_trade_count(count): logger.warning(f"[岛屿-珍珠采购] 珍珠{self._action_name(action)}数量未调整到目标: {count}") return False From 399f6c520cff4bd52785d2ac0173f31cb3420ceb Mon Sep 17 00:00:00 2001 From: wess09 <> Date: Mon, 27 Jul 2026 13:05:02 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(cdn):=20=E6=B7=BB=E5=8A=A0=E7=AB=99?= =?UTF-8?q?=E7=82=B9=20URL=20=E5=A4=84=E7=90=86=E5=92=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=AB=99=E7=82=B9=E5=9C=B0=E5=9B=BE=E4=B8=8E=20robots.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/build_git_over_cdn_eo_esa.mjs | 164 +++++++++++++++++- 1 file changed, 155 insertions(+), 9 deletions(-) diff --git a/.github/scripts/build_git_over_cdn_eo_esa.mjs b/.github/scripts/build_git_over_cdn_eo_esa.mjs index 584ed8636..757cb4e8c 100644 --- a/.github/scripts/build_git_over_cdn_eo_esa.mjs +++ b/.github/scripts/build_git_over_cdn_eo_esa.mjs @@ -8,6 +8,19 @@ import yazl from "yazl"; const env = process.env; const MAIN_SITE_URL = "https://alas.nanoda.work/"; const MAIN_SITE_HOST = "alas.nanoda.work"; +const CDN_SITE_URLS = [ + "https://ap-update-cdn-cloudflare.3463343.xyz/", + "https://ap-update-cdn-cloudflare-a3.haiteluo.com/", + "https://ap-update-cdn-cloudflare-a1.3463343.xyz/", + "https://ap-update-cdn-cloudflare.nanoda.work/", + "https://ap-update-cdn-cloudflare-a2.3463343.xyz/", + "https://ap-update-cdn-cloudflare-a1.haiteluo.com/", + "https://ap-update-cdn-cloudflare-a2.haiteluo.com/", + "https://ap-update-cdn-cloudflare-a4.haiteluo.com/", + "https://ap-update-cdn-cloudflare-a3.3463343.xyz/", + "https://ap.update.cdn.cloudflare.3463343.xyz/", +]; +const DEFAULT_SITE_URL = CDN_SITE_URLS[0]; const SEO_TITLE = "AzurPilot 更新 CDN - 碧蓝航线自动化更新镜像"; const SEO_DESCRIPTION = "AzurPilot 更新 CDN 提供 Git over CDN 静态更新文件、latest.json、更新包状态与最近提交信息,主站为 https://alas.nanoda.work/。"; @@ -22,6 +35,7 @@ function printHelp() { --ref 构建提交或引用,优先级高于 --branch --history 生成多少个旧提交的更新包,默认 15 --output 输出目录,默认 dist/git-over-cdn + --site-url canonical/sitemap 默认站点 URL,默认 ${DEFAULT_SITE_URL} --remote 拉取历史时使用的 remote,默认 origin --no-fetch 跳过 git fetch --fetch-full 浅克隆时执行 git fetch --unshallow @@ -29,7 +43,7 @@ function printHelp() { 环境变量: GOC_BRANCH, GOC_REF, GOC_HISTORY, GOC_OUTPUT, GOC_REMOTE, - GOC_FETCH=0, GOC_FETCH_FULL=1 + GOC_SITE_URL, GOC_MIRROR_URLS, GOC_FETCH=0, GOC_FETCH_FULL=1 `); } @@ -42,12 +56,50 @@ function envFirst(...names) { return ""; } +function normalizeSiteUrl(value) { + const raw = String(value || DEFAULT_SITE_URL).trim() || DEFAULT_SITE_URL; + const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`; + const url = new URL(withProtocol); + if (!url.pathname.endsWith("/")) { + url.pathname = `${url.pathname}/`; + } + url.search = ""; + url.hash = ""; + return url.toString(); +} + +function parseUrlList(value) { + return String(value || "") + .split(/[\s,]+/) + .map((item) => item.trim()) + .filter(Boolean); +} + +function uniqueUrls(values) { + const urls = []; + const seen = new Set(); + for (const value of values) { + const url = normalizeSiteUrl(value); + if (seen.has(url)) { + continue; + } + seen.add(url); + urls.push(url); + } + return urls; +} + +function resolveAssetUrl(siteUrl, filename) { + return new URL(filename, siteUrl).toString(); +} + function parseArgs(argv) { const options = { branch: envFirst("GOC_BRANCH", "CF_PAGES_BRANCH", "BRANCH", "GITHUB_REF_NAME") || "master", ref: envFirst("GOC_REF", "CF_PAGES_COMMIT_SHA", "COMMIT_SHA", "GITHUB_SHA"), history: env.GOC_HISTORY || "15", output: env.GOC_OUTPUT || "dist/git-over-cdn", + siteUrl: normalizeSiteUrl(env.GOC_SITE_URL || DEFAULT_SITE_URL), remote: env.GOC_REMOTE || "origin", fetch: env.GOC_FETCH !== "0", fetchFull: env.GOC_FETCH_FULL === "1", @@ -68,6 +120,9 @@ function parseArgs(argv) { case "--output": options.output = requireValue(argv, ++i, arg); break; + case "--site-url": + options.siteUrl = normalizeSiteUrl(requireValue(argv, ++i, arg)); + break; case "--remote": options.remote = requireValue(argv, ++i, arg); break; @@ -91,6 +146,11 @@ function parseArgs(argv) { if (!Number.isInteger(options.history) || options.history < 1) { throw new Error(`--history 必须是正整数:${options.history}`); } + options.mirrorUrls = uniqueUrls([ + options.siteUrl, + ...CDN_SITE_URLS, + ...parseUrlList(env.GOC_MIRROR_URLS), + ]); return options; } @@ -305,6 +365,15 @@ function escapeHtml(value) { .replaceAll("'", "'"); } +function escapeXml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + function escapeScriptJson(value) { return String(value).replaceAll("<", "\\u003c"); } @@ -347,13 +416,41 @@ function formatDuration(milliseconds) { return `${sign}${days}天 ${hours}时 ${minutes}分 ${seconds}秒 ${millis}毫秒`; } -function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos) { +function writeSitemapXml(outputDir, mirrorUrls, generatedAt) { + const urlEntries = mirrorUrls.map((siteUrl) => ` + ${escapeXml(siteUrl)} + ${escapeXml(generatedAt)} + hourly + 0.7 + `).join("\n"); + const xml = ` + +${urlEntries} + +`; + + fs.writeFileSync(path.join(outputDir, "sitemap.xml"), xml, "utf8"); +} + +function writeRobotsTxt(outputDir, mirrorUrls) { + const sitemapLines = mirrorUrls.map((siteUrl) => `Sitemap: ${resolveAssetUrl(siteUrl, "sitemap.xml")}`); + const text = [ + "User-agent: *", + "Allow: /", + "", + ...sitemapLines, + "", + ].join("\n"); + + fs.writeFileSync(path.join(outputDir, "robots.txt"), text, "utf8"); +} + +function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos, generatedAtTimestamp) { const latestCommitInfo = commitInfos[0]; if (!latestCommitInfo) { throw new Error("无法读取最新提交信息"); } - const generatedAtTimestamp = Date.now(); const generatedAt = new Date(generatedAtTimestamp).toISOString(); const commitAge = formatDuration(generatedAtTimestamp - latestCommitInfo.committedAtTimestamp); const structuredData = escapeScriptJson(JSON.stringify({ @@ -361,13 +458,15 @@ function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos) { "@type": "WebPage", name: SEO_TITLE, description: SEO_DESCRIPTION, + url: options.siteUrl, isPartOf: { "@type": "WebSite", name: "AzurPilot", url: MAIN_SITE_URL, }, about: "AzurPilot Git over CDN 更新镜像", - relatedLink: MAIN_SITE_URL, + relatedLink: [MAIN_SITE_URL, ...options.mirrorUrls], + sameAs: options.mirrorUrls, dateModified: generatedAt, }, null, 2)); const packRows = oldCommits.map((commit) => { @@ -389,6 +488,25 @@ function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos) { ${escapeHtml(info.authorName)} ${escapeHtml(info.subject)} `).join(""); + const mirrorRows = options.mirrorUrls.map((siteUrl, index) => { + const url = new URL(siteUrl); + const latestJsonUrl = resolveAssetUrl(siteUrl, "latest.json"); + const sitemapUrl = resolveAssetUrl(siteUrl, "sitemap.xml"); + return ` + + ${index === 0 ? "默认" : index + 1} + ${escapeHtml(url.host)} + latest.json + sitemap.xml + `; + }).join(""); + const alternateLinks = options.mirrorUrls + .filter((siteUrl) => siteUrl !== options.siteUrl) + .map((siteUrl) => ``) + .join("\n "); + const seeAlsoMeta = [MAIN_SITE_URL, ...options.mirrorUrls.filter((siteUrl) => siteUrl !== options.siteUrl)] + .map((siteUrl) => ``) + .join("\n "); const html = ` @@ -398,14 +516,16 @@ function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos) { ${escapeHtml(SEO_TITLE)} - + + + ${alternateLinks} - - + + ${seeAlsoMeta} @@ -524,6 +644,8 @@ function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos) {
${escapeHtml(options.branch)}
项目主站
${escapeHtml(MAIN_SITE_URL)}
+
CDN 首页
+
${escapeHtml(options.siteUrl)}
更新包数量
${oldCommits.length}
生成时间
@@ -549,9 +671,27 @@ function writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos) {
${escapeHtml(commitAge)}
版本接口
latest.json
+
站点地图
+
sitemap.xml
+
+

CDN 镜像

+ + + + + + + + + + ${mirrorRows} + +
序号首页版本接口站点地图
+
+

更新包

${oldCommits.length ? ` @@ -702,14 +842,20 @@ async function main() { await buildPack(latest, old, latestDir, repoRoot); } cleanupPackArtifacts(latestDir); - writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos); + const generatedAtTimestamp = Date.now(); + const generatedAt = new Date(generatedAtTimestamp).toISOString(); + writeIndexHtml(outputDir, options, latest, oldCommits, commitInfos, generatedAtTimestamp); + writeSitemapXml(outputDir, options.mirrorUrls, generatedAt); + writeRobotsTxt(outputDir, options.mirrorUrls); console.log("Build git-over-cdn files"); console.log(` branch : ${options.branch}`); console.log(` ref : ${latest}`); console.log(` history: ${options.history}`); console.log(` output : ${path.relative(repoRoot, outputDir).replaceAll(path.sep, "/")}`); - console.log(`Generated index.html, latest.json and ${oldCommits.length} update pack(s)`); + console.log(` site : ${options.siteUrl}`); + console.log(` mirrors: ${options.mirrorUrls.length}`); + console.log(`Generated index.html, robots.txt, sitemap.xml, latest.json and ${oldCommits.length} update pack(s)`); } main().catch((error) => { From 7b8ba1622d910c208f34104efb6acee74e55df4a Mon Sep 17 00:00:00 2001 From: Beatrice-betty Date: Mon, 27 Jul 2026 13:13:28 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(emotion,coalition):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=83=85=E7=BB=AA=E8=AE=A1=E7=AE=97=E5=92=8C=E8=81=94?= =?UTF-8?q?=E7=9B=9F=E6=B2=89=E8=88=B9=E5=BF=83=E6=83=85=E6=89=A3=E5=87=8F?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构情绪恢复计算逻辑,使用浮点值保留恢复余数,跨record调用累积 2. 调整联盟沉船心情扣减规则,仅关卡进入时扣减一次,不再按战斗次数额外扣减 3. 优化record方法,仅在情绪整数变化时更新时间戳并回扣余数秒数 4. 修复情绪显示逻辑,显示实时恢复值而非缓存值 --- module/coalition/coalition_scuttle.py | 76 +++++++++++++++++++++------ module/combat/emotion.py | 59 +++++++++++++++------ 2 files changed, 102 insertions(+), 33 deletions(-) diff --git a/module/coalition/coalition_scuttle.py b/module/coalition/coalition_scuttle.py index 140fa7762..f25495c1e 100644 --- a/module/coalition/coalition_scuttle.py +++ b/module/coalition/coalition_scuttle.py @@ -24,12 +24,14 @@ class CoalitionScuttleCombat(CoalitionCombat): def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expected_end=None): """ - 重写自动搜索战斗执行,D评价沉船时不额外扣减10心情。 + 重写自动搜索战斗执行,联盟沉船不额外扣减心情。 - 进入战斗扣减2心情(正常出击代价),D评价不执行 shipwreck=True 的额外扣减。 + 联盟沉船中一个关卡包含多次战斗(1/2/3/4队), + 但游戏只在整个关卡进入时扣1次2点心情,不按内部战斗次数扣减。 + D评价也不执行 shipwreck=True 的额外扣减。 Args: - emotion_reduce (bool): 是否扣减心情。 + emotion_reduce (bool): 是否扣减心情(仅在第一场战斗时为True)。 fleet_index (int): 舰队编号。 expected_end (callable): 自定义结束条件。 """ @@ -41,7 +43,8 @@ def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expecte self.device.stuck_record_clear() self.device.click_record_clear() - # 进入战斗扣减2心情(正常出击代价) + # 联盟沉船仅在第一场战斗时扣减2心情(关卡进入代价) + # 后续战斗(2/3/4队)不再扣减,与游戏服务端行为一致 if emotion_reduce: self.emotion.reduce(fleet_index) @@ -81,7 +84,7 @@ def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expecte if self.handle_get_ship(): continue - # D评价沉船:不执行 emotion.reduce(shipwreck=True) + # D评价沉船:不额外扣减心情 if self.appear_then_click(OPTS_INFO_D, offset=(30, 30), interval=2): self._withdraw = True self._is_shipwreck = True @@ -97,11 +100,10 @@ def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expecte confirm_timer.reset() break - # A/B评价正常扣减心情 + # A/B/S评价:联盟沉船中不额外扣减心情 + # 游戏服务端只在整个关卡进入时扣1次2点,不按战斗结算类型扣减 if self.appear(BATTLE_STATUS_A) or self.appear(BATTLE_STATUS_B) \ or self.appear(EXP_INFO_A) or self.appear(EXP_INFO_B): - if emotion_reduce: - self.emotion.reduce(fleet_index, shipwreck=True) break # S评价或自动搜索运行中 @@ -117,21 +119,23 @@ def auto_search_combat_execute(self, emotion_reduce=True, fleet_index=1, expecte def coalition_combat(self): """ - 联盟沉船战斗执行,进入战斗扣减2心情,D评价不额外扣减。 + 联盟沉船战斗执行,仅在第一场战斗扣减2心情。 - 原因:沉船任务中舰船被击沉后需要换新船,不应扣减额外心情。 + 联盟沉船一个关卡包含多次战斗(1/2/3/4队), + 但游戏只在整个关卡进入时扣1次2点心情,后续战斗不再扣减。 """ from module.exception import CampaignEnd self.battle_count = 0 - self.combat_preparation(emotion_reduce=False) # 不在此扣减,由 auto_search_combat_execute 统一扣减 + self.combat_preparation(emotion_reduce=False) try: while 1: logger.hr(f'{self.FUNCTION_NAME_BASE}{self.battle_count}', level=2) - self._is_shipwreck = False # 重置沉船标记 + self._is_shipwreck = False + # 仅第一场战斗扣减2心情(关卡进入代价),后续战斗不再扣减 self.auto_search_combat_execute( - emotion_reduce=True, # 进入战斗扣减2心情 + emotion_reduce=self.battle_count == 0, fleet_index=1, expected_end=self.auto_search_combat_end ) @@ -247,7 +251,7 @@ def coalition_combat_re_enter(self, skip_first_screenshot=True): class CoalitionScuttleRun(Coalition, CoalitionScuttleCombat): - """联盟沉船主循环,沉船任务不扣减心情。""" + """联盟沉船主循环,沉船任务进入关卡只扣1次2点心情。""" def handle_combat_low_emotion(self): """ @@ -257,15 +261,53 @@ def handle_combat_low_emotion(self): """ return self.handle_popup_confirm('IGNORE_LOW_EMOTION') + def coalition_execute_once(self, event, stage, fleet): + """执行一次联盟沉船战斗。 + + 覆盖父类方法,将心情预估从多场战斗改为1场(整个关卡只扣1次2点)。 + 联盟沉船虽然内部有多次战斗(1/2/3/4队),但游戏只在整个关卡进入时扣1次心情。 + + Args: + event: 活动名称。 + stage: 关卡名称。 + fleet: 舰队模式。 + """ + self.config.override( + Campaign_Name=f'{event}_{stage}', + Campaign_UseAutoSearch=False, + Fleet_FleetOrder='fleet1_all_fleet2_standby', + ) + if self.config.Coalition_Fleet == 'single' and self.config.Emotion_Fleet1Control == 'prevent_red_face': + logger.warning('AL does not allow single coalition with emotion < 30, ' + 'emotion control is forced to prevent_yellow_face') + self.config.override(Emotion_Fleet1Control='prevent_yellow_face') + if stage == 'sp': + self.config.override(Coalition_Fleet='multi') + + # 联盟沉船:整个关卡只扣1次2点心情,不按内部战斗次数预估 + try: + self.emotion.check_reduce(battle=1) + except ScriptEnd: + self.coalition_map_exit(event) + raise + + if self._coalition_has_oil_icon and self.triggered_stop_condition(oil_check=True, coin_check=True): + self.coalition_map_exit(event) + raise ScriptEnd + + self.enter_map(event=event, stage=stage, mode=fleet) + self.coalition_combat() + def triggered_stop_condition(self, oil_check=False, pt_check=False, coin_check=False): """ - 检查是否触发了停止条件,在父类基础上增加沉船正常结束检测。 + 检查是否触发了停止条件。 + + 联盟沉船不因 triggered_normal_end(舰船被击沉)而停止任务, + 由 RunCount 控制何时停止。D评价和非D评价都算1次有效战斗。 Returns: bool: 是否触发了停止条件。 """ - if self.triggered_normal_end: - return True if super().triggered_stop_condition(oil_check=oil_check, pt_check=pt_check, coin_check=coin_check): return True diff --git a/module/combat/emotion.py b/module/combat/emotion.py index cb1e73959..0685804cb 100644 --- a/module/combat/emotion.py +++ b/module/combat/emotion.py @@ -168,16 +168,20 @@ def max(self): def update(self): """根据实际经过时间计算情绪恢复。 - 使用连续时间恢复计算,而非离散6分钟区间。 - 原方法使用 // 360 计算恢复次数,当 record() 频繁重置时间戳时 - (如联盟沉船每2分钟调用一次),恢复永远无法累积。 - 改为按实际秒数计算恢复量,与游戏服务端行为一致。 - """ - time_diff = int(current_time().timestamp()) - int(self.record.timestamp()) + 使用连续时间恢复计算,保留浮点恢复量以累积分数部分。 + 游戏服务端按实际经过时间精确计算恢复,每6分钟恢复speed点。 + 旧方法用 int() 截断恢复量,每次 record() 重置时间戳后, + 未满1点的恢复余数被丢弃,长时间运行导致严重低估。 + 现改为 floor() 取整保留整数部分,同时 record() 仅在整数变化时 + 重置时间戳并回扣分数秒,确保余数可跨次累积。 + """ + time_diff = current_time().timestamp() - self.record.timestamp() time_diff = max(time_diff, 0) # speed 为每360秒的恢复量,换算为每秒恢复 speed/360 点 recovery = self.speed * time_diff / 360 self.current = min(max(self.value, 0) + int(recovery), self.max) + # 保留未满1点的恢复余数对应的秒数,用于 record() 回扣 + self._fractional_seconds = recovery - int(recovery) def get_recovered(self, expected_reduce=0): """计算情绪恢复到控制阈值的时间。 @@ -268,25 +272,48 @@ def update(self): fleet.update() def record(self): - """将当前情绪值保存到配置中。""" + """将当前情绪值保存到配置中。 + + 仅在心情整数值发生变化时更新 Record 时间戳, + 并将 Record 回扣 fractional_seconds 对应的等效秒数, + 使未满1点的恢复余数可在下次 update() 时继续累积。 + """ if self.using_public: - value = {self.public_fleet.value_name: self.public_fleet.current} - self.config.set_record(**value) + fleet = self.public_fleet + old_value = fleet.value + new_value = fleet.current + # 仅在整数变化时重置时间戳,回扣分数秒 + if new_value != old_value: + record_time = current_time().replace(microsecond=0) + fractional = getattr(fleet, '_fractional_seconds', 0) + if fractional > 0: + # 回扣 fractional_seconds 对应的秒数 + record_time = record_time - timedelta(seconds=fractional * 360 / fleet.speed) + with self.config.multi_set(): + setattr(self.config, fleet.value_name, new_value) + setattr(self.config, fleet.value_name.replace('Value', 'Record'), record_time) return - - value = {} - for fleet in self.fleets: - value[fleet.value_name] = fleet.current - self.config.set_record(**value) + with self.config.multi_set(): + for fleet in self.fleets: + old_value = fleet.value + new_value = fleet.current + if new_value != old_value: + record_time = current_time().replace(microsecond=0) + fractional = getattr(fleet, '_fractional_seconds', 0) + if fractional > 0: + record_time = record_time - timedelta(seconds=fractional * 360 / fleet.speed) + setattr(self.config, fleet.value_name, new_value) + setattr(self.config, fleet.value_name.replace('Value', 'Record'), record_time) def show(self): + """显示当前计算的心情值(含时间恢复),而非上次保存值。""" if self.using_public: - logger.attr(f'情绪公海舰队', self.public_fleet.value) + logger.attr(f'情绪公海舰队', self.public_fleet.current) return for fleet in self.fleets: - logger.attr(f'情绪舰队_{fleet.fleet}', fleet.value) + logger.attr(f'情绪舰队_{fleet.fleet}', fleet.current) @property def reduce_per_battle(self):