feat: 千星脚本添加html遮罩 (#3081)

* feat: 版本检测工具

* feat: 千星脚本添加html遮罩

* 添加存档

* feat: 版本检测
This commit is contained in:
躁动的氨气
2026-05-10 11:39:14 +08:00
committed by GitHub
parent c5b12e2e21
commit c868dc6ee8
16 changed files with 10083 additions and 12 deletions

View File

@@ -24,7 +24,7 @@
自己发布的千星奇域的地图也可以用来增长经验,但是并不需要发布成功,仅需要点击一下发布再预览详情,即可在弹窗中进行收藏。
这就意味着可以自行制作秒刷图,然后“发布”进行收藏,在收藏中使用自己实际上并未真正发布的地图进行秒刷。
最新版脚本进行了重构优化了点击速度秒刷图一周刷完仅需8分45秒。
地图制作请自行学习
焚诀存档地图已存放至BetterGI/User/JsScript/WeeklyThousandStarRealm/map目录下可自行在游戏中导入
## 🌟 功能特点

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
{
"name": "disclaimer-mask",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node ../../build/rename.js dist/index.html dist/disclaimer-mask.html",
"preview": "vite preview"
},
"dependencies": {
"gsap": "^3.12.7",
"vue": "^3.5.30"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.4"
}
}

View File

@@ -0,0 +1,229 @@
<template>
<div class="overlay">
<div class="scan-lines"></div>
<div class="panel" ref="panel">
<div class="panel-inner">
<div class="icon-wrap" ref="icon">
<div class="icon-ring"></div>
<span class="icon-text">!</span>
</div>
<div class="title" ref="title">WARNING</div>
<div class="divider" ref="divider"></div>
<div class="message" ref="message">请先同意免责声明后再运行此脚本</div>
<div class="reason-block" ref="reasonBlock">
<div class="reason-line" v-for="(line, i) in reasonTexts" :key="i" ref="reasonLineEls">{{ line }}</div>
</div>
<div class="instruction" ref="instruction">
请在脚本的 <span class="highlight">自定义 JS 配置</span> 中手动勾选我已阅读说明中的免责声明
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import gsap from 'gsap'
const panel = ref(null)
const icon = ref(null)
const title = ref(null)
const divider = ref(null)
const message = ref(null)
const reasonBlock = ref(null)
const reasonLineEls = ref([])
const instruction = ref(null)
const reasonTexts = [
'本脚本依赖「删除奇域地图存档」来实现重复获取成就经验。',
'当脚本执行删除存档时,可能会误删你正常游玩的关卡数据。',
'请务必确认你能承担潜在损失后再继续使用。',
]
onMounted(async () => {
const tl = gsap.timeline({defaults: {ease: 'power3.out'}})
tl.from(panel.value, {
scale: 0, opacity: 0, rotation: -8,
duration: 0.6, ease: 'back.out(1.4)'
})
.from(icon.value, {
scale: 0, rotation: -360, opacity: 0,
duration: 0.6, ease: 'elastic.out(1, 0.5)'
}, '-=0.2')
.from(title.value, {
y: -20, opacity: 0, duration: 0.3
}, '-=0.1')
.from(divider.value, {
scaleX: 0, duration: 0.4
}, '-=0.1')
.from(message.value, {
y: 20, opacity: 0, duration: 0.4
}, '-=0.1')
.from(reasonBlock.value, {
opacity: 0, duration: 0.3
})
.from(reasonLineEls.value, {
x: -30, opacity: 0,
duration: 0.4, stagger: 0.5
}, '-=0.1')
.from(instruction.value, {
y: 15, opacity: 0, duration: 0.5
})
// 持续脉冲
gsap.to(panel.value, {
boxShadow: '0 0 60px rgba(255,50,50,0.5), 0 0 120px rgba(255,50,50,0.2)',
duration: 1.5, repeat: -1, yoyo: true, ease: 'sine.inOut'
})
gsap.to('.icon-ring', {
scale: 1.2, opacity: 0.3,
duration: 1.5, repeat: -1, yoyo: true, ease: 'sine.inOut'
})
})
</script>
<style>
@property --border-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; }
.overlay {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.scan-lines {
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent, transparent 2px,
rgba(255, 50, 50, 0.03) 2px,
rgba(255, 50, 50, 0.03) 4px
);
pointer-events: none;
animation: scan 8s linear infinite;
}
@keyframes scan {
from { transform: translateY(0); }
to { transform: translateY(4px); }
}
.panel {
--border-angle: 0deg;
position: relative;
padding: 2px;
border-radius: 18px;
background: conic-gradient(from var(--border-angle), transparent 60%, #ff4444, #ff2222, #ff4444, transparent 40%) border-box;
animation: rotate-border 3s linear infinite;
}
@keyframes rotate-border {
to { --border-angle: 360deg; }
}
.panel-inner {
background: rgba(8, 0, 0, 0.9);
border-radius: 16px;
padding: 44px 60px;
display: flex;
flex-direction: column;
align-items: center;
gap: 22px;
max-width: 700px;
box-shadow:
0 0 30px rgba(255, 50, 50, 0.2),
0 0 60px rgba(255, 50, 50, 0.1),
inset 0 0 40px rgba(255, 50, 50, 0.03);
backdrop-filter: blur(12px);
}
.icon-wrap {
position: relative;
width: 70px;
height: 70px;
display: flex;
justify-content: center;
align-items: center;
}
.icon-ring {
position: absolute;
inset: 0;
border: 3px solid rgba(255, 80, 80, 0.7);
border-radius: 50%;
}
.icon-text {
font-size: 46px;
font-weight: 900;
color: #ff4444;
text-shadow: 0 0 20px rgba(255, 68, 68, 0.8), 0 0 40px rgba(255, 68, 68, 0.4);
font-family: 'Arial Black', sans-serif;
}
.title {
font-size: 36px;
font-weight: 900;
letter-spacing: 14px;
color: #ff4444;
text-shadow: 0 0 20px rgba(255, 68, 68, 0.6), 0 0 40px rgba(255, 68, 68, 0.3);
font-family: 'Arial Black', monospace;
}
.divider {
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #ff4444, transparent);
}
.message {
font-size: 20px;
color: #ffcccc;
text-align: center;
text-shadow: 0 0 10px rgba(255, 68, 68, 0.4);
letter-spacing: 2px;
}
.reason-block {
width: 100%;
padding: 16px 20px;
border-left: 3px solid #ff4444;
background: rgba(255, 50, 50, 0.06);
border-radius: 0 8px 8px 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.reason-line {
font-size: 16px;
color: #ddaaaa;
line-height: 1.6;
letter-spacing: 1px;
}
.instruction {
font-size: 15px;
color: #cc8888;
text-align: center;
line-height: 1.6;
}
.instruction .highlight {
color: #ff9999;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@@ -0,0 +1,4 @@
import { createConfig } from '../../vite.base.config.js'
import vue from '@vitejs/plugin-vue'
export default createConfig([vue()])

View File

@@ -4,7 +4,8 @@ import {
findImgAndClick,
waitUntilTextAppear,
waitUntilImgDisappear,
waitUntilImgAppear, findImg
waitUntilImgAppear,
findImg
} from "../../../packages/utils/tool";
import fold_triangle from "assets/fold_triangle.png";
@@ -12,6 +13,7 @@ import check_box from "assets/check_box.png";
import exit_room from "assets/exit_room.png";
import room_ready from "assets/room_ready.png";
import paimon from "../../../packages/assets/imgs/paimon_menu.png";
import { checkVersion } from "../../../packages/utils/tool";
const duration = 1000; // 默认点击等待延时
@@ -26,6 +28,19 @@ const useFixedAttempts = userAttempts > 0;
const weekMaxExp = Number(settings.weekMaxExp || "4000");
const singleExp = Number(settings.singleExp || "270");
let weekTotal = initWeekTotal();
let progressWinId = null;
let useMask = false;
function sendProgress(pct, text, cur, tot) {
if (!progressWinId) return;
htmlMask.send(progressWinId, "/progress", JSON.stringify({
title: "运行进度",
progress: pct,
current: cur || 0,
total: tot || 0,
status: text
}));
}
// 读取存档
function loadWeekData() {
@@ -85,8 +100,8 @@ function initWeekTotal() {
// 配置变化 -> 重算
if (
stored.weekMaxExp !== weekMaxExp ||
stored.singleExp !== singleExp
stored.weekMaxExp !== weekMaxExp ||
stored.singleExp !== singleExp
) {
stored.weekMaxExp = weekMaxExp;
stored.singleExp = singleExp;
@@ -315,6 +330,9 @@ async function playMap() {
await findTextAndClick("开始游戏", 960, 540, 960, 540, 5, 50, 50);
log.info("开始执行第{i}/{total}次奇域挑战", 1, total);
if (useMask) {
sendProgress(0, `正在执行第1/${total}次挑战`, 1, total);
}
let firstOutputCount = 0;
await waitUntilTextAppear(
"返回大厅",
@@ -337,6 +355,9 @@ async function playMap() {
decreaseWeekTotal();
}
log.info("本次关卡结束");
if (useMask) {
sendProgress(Math.round(1 / total * 100), `第1/${total}次挑战完成`, 1, total);
}
await deleteSource();
@@ -348,6 +369,9 @@ async function playMap() {
await sleep(duration);
await findTextAndClick("开始游戏", 960, 540, 960, 540, 20, 50, 50);
log.info("开始执行第{i}/{total}次奇域挑战", i + 1, total);
if (useMask) {
sendProgress(Math.round(i / total * 100), `正在执行第${i + 1}/${total}次挑战`, i + 1, total);
}
let outputCount = 0;
await waitUntilTextAppear(
"返回大厅",
@@ -369,6 +393,9 @@ async function playMap() {
if (!useFixedAttempts) {
decreaseWeekTotal();
}
if (useMask) {
sendProgress(Math.round((i + 1) / total * 100), `${i + 1}/${total}次挑战完成`, i + 1, total);
}
await deleteSource();
}
}
@@ -386,21 +413,43 @@ async function exitToTeyvat() {
await waitUntilImgAppear(paimon);
// 有镜头拉近动画
await sleep(duration);
log.info("运行结束");
}
(async function () {
const version = getVersion();
const minVersion = '0.60.2-alpha.3';
useMask = checkVersion(version, minVersion);
if (!runJS) {
log.error("您未同意此脚本的免责声明,请先同意后重新运行此脚本!");
if (useMask) {
htmlMask.show("assets/disclaimer-mask.html");
await sleep(10000);
}
return;
}
if (useMask) {
const showSkill = roomID === "37135473336";
progressWinId = htmlMask.show("assets/progress-mask.html");
await sleep(duration);
sendProgress(0, "准备中...");
if (showSkill && progressWinId) {
htmlMask.request(progressWinId, "/showskill", JSON.stringify({
show: showSkill
}));
}
}
await genshin.returnMainUi();
if (useFixedAttempts) {
// 手动模式:忽略本周计数
log.info(
"已进入指定次数模式,本次将执行{count}次挑战(不计入周进度)",
userAttempts
"已进入指定次数模式,本次将执行{count}次挑战(不计入周进度)",
userAttempts
);
} else {
// 每周模式
@@ -409,11 +458,16 @@ async function exitToTeyvat() {
const done = Math.ceil(weekMaxExp / singleExp) - leave;
log.info(
"本周共需刷取 {total} 次,已刷 {done} 次,剩余 {leave} 次",
Math.ceil(weekMaxExp / singleExp),
done,
leave
"本周共需刷取 {total} 次,已刷 {done} 次,剩余 {leave} 次",
Math.ceil(weekMaxExp / singleExp),
done,
leave
);
if (leave < 1) {
log.info("本周任务已完成,结束运行");
return;
}
}
if (starMode) {
@@ -424,4 +478,8 @@ async function exitToTeyvat() {
}
await playMap();
await exitToTeyvat();
if (useMask) {
sendProgress(100, "全部完成!");
}
await sleep(3000);
})();

View File

@@ -1,8 +1,8 @@
{
"manifest_version": 1,
"name": "千星奇域每周成就经验刷取",
"version": "3.4",
"bgi_version": "0.57.0",
"version": "3.5",
"bgi_version": "0.60.0",
"description": "无需自己找图可用于利用成就高经验值刷取经验默认配置每周刷满需要22分钟秒刷图仅需9分钟",
"authors": [
{

Binary file not shown.

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
{
"name": "progress-mask",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node ../../build/rename.js dist/index.html dist/progress-mask.html",
"preview": "vite preview"
},
"dependencies": {
"gsap": "^3.12.7",
"vue": "^3.5.30"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.4"
}
}

View File

@@ -0,0 +1,318 @@
<template>
<div class="overlay">
<canvas ref="canvas" class="particle-canvas"></canvas>
<div class="content" ref="content">
<div class="title" ref="titleEl">{{ title }}</div>
<div class="fraction" ref="fractionWrap">
<span class="frac-current">{{ current }}</span>
<span class="frac-sep">/</span>
<span class="frac-total">{{ total }}</span>
</div>
<div class="bar-area" ref="barArea">
<div class="bar-glow"></div>
<div class="bar-track">
<div class="bar-fill" ref="barFill">
<div class="bar-shine"></div>
</div>
</div>
</div>
<div class="percent-wrap" ref="percentWrap">
<span class="percent-value">{{ displayProgress }}</span>
<span class="percent-unit">%</span>
</div>
<div class="status" ref="statusEl">{{ status }}</div>
</div>
<div v-if="showSkill" class="special-skill">
<span>如需使用秒刷图请查看脚本说明中的焚诀</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import gsap from 'gsap'
const canvas = ref(null)
const content = ref(null)
const titleEl = ref(null)
const fractionWrap = ref(null)
const barArea = ref(null)
const barFill = ref(null)
const percentWrap = ref(null)
const statusEl = ref(null)
const showSkill = ref(false)
const title = ref('')
const status = ref('等待中...')
const progress = ref(0)
const displayProgress = ref(0)
const current = ref(0)
const total = ref(0)
let animId = null
let particles = []
onMounted(() => {
setTimeout(() => {
showSkill.value = false
}, 60000)
window.htmlMask.onMessage = (msg) => {
if (msg.url === '/progress') {
if (msg.data.title !== undefined) title.value = msg.data.title
if (msg.data.status) status.value = msg.data.status
if (msg.data.current !== undefined) current.value = msg.data.current
if (msg.data.total !== undefined) total.value = msg.data.total
if (msg.data.progress !== undefined) {
const pct = Math.min(100, Math.max(0, msg.data.progress))
progress.value = pct
gsap.to(displayProgress, { value: pct, duration: 0.5, snap: { value: 1 } })
gsap.to(barFill.value, { height: pct + '%', duration: 2, ease: 'power2.out' })
}
} else if (msg.url === '/showskill') {
showSkill.value = msg.data.show;
}
}
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } })
tl.from(content.value, { opacity: 0, x: 40, duration: 0.5 })
.from(titleEl.value, { y: -15, opacity: 0, duration: 0.3 }, '-=0.2')
.from(fractionWrap.value, { scale: 0.5, opacity: 0, duration: 0.4, ease: 'back.out(1.7)' }, '-=0.1')
.from(barArea.value, { scaleY: 0, opacity: 0, duration: 0.4 }, '-=0.1')
.from(percentWrap.value, { scale: 0.5, opacity: 0, duration: 0.4, ease: 'back.out(1.7)' }, '-=0.1')
.from(statusEl.value, { y: 10, opacity: 0, duration: 0.3 }, '-=0.1')
initParticles()
})
onUnmounted(() => {
window.htmlMask.onMessage = null
if (animId) cancelAnimationFrame(animId)
})
function initParticles() {
const cvs = canvas.value
cvs.width = window.innerWidth
cvs.height = window.innerHeight
const ctx = cvs.getContext('2d')
const COUNT = 35
particles = Array.from({ length: COUNT }, () => spawn(cvs))
;(function loop() {
ctx.clearRect(0, 0, cvs.width, cvs.height)
for (const p of particles) {
p.x += p.vx
p.y += p.vy
p.life -= p.decay
if (p.life <= 0) Object.assign(p, spawn(cvs))
ctx.save()
ctx.globalAlpha = p.life * p.alpha
ctx.shadowBlur = p.size * 6
ctx.shadowColor = p.color
ctx.fillStyle = p.color
ctx.beginPath()
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
animId = requestAnimationFrame(loop)
})()
}
function spawn(cvs) {
// 粒子从右侧进度条区域生成
const rx = cvs.width - 70
const cy = cvs.height / 2
return {
x: rx + (Math.random() - 0.5) * 60,
y: cy + (Math.random() - 0.5) * 280,
vx: (Math.random() - 0.5) * 0.5 + 0.3,
vy: -(Math.random() * 0.8 + 0.2),
size: Math.random() * 2.5 + 0.5,
life: 1,
decay: Math.random() * 0.008 + 0.004,
alpha: Math.random() * 0.5 + 0.2,
color: Math.random() > 0.3 ? '#00ff88' : '#88ffcc',
}
}
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; font-family: 'Segoe UI', system-ui, sans-serif; }
.overlay {
width: 100vw;
height: 100vh;
display: flex;
justify-content: flex-end;
align-items: center;
padding-right: 50px;
position: relative;
}
.particle-canvas {
position: absolute;
inset: 0;
pointer-events: none;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
z-index: 1;
}
/* 标题 */
.title {
font-size: 15px;
letter-spacing: 6px;
color: rgba(0, 255, 136, 0.85);
text-transform: uppercase;
}
/* 分数 3/15 */
.fraction {
display: flex;
align-items: baseline;
gap: 4px;
line-height: 1;
}
.frac-current {
font-size: 30px;
font-weight: 600;
color: #fff;
text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
}
.frac-sep {
font-size: 22px;
font-weight: 300;
color: rgba(0, 255, 136, 0.7);
}
.frac-total {
font-size: 22px;
font-weight: 300;
color: rgba(255, 255, 255, 0.75);
}
/* 竖向进度条 */
.bar-area {
position: relative;
}
.bar-glow {
position: absolute;
top: 50%;
left: 50%;
width: 400%;
height: 130%;
transform: translate(-50%, -50%);
background: radial-gradient(ellipse, rgba(0, 255, 136, 0.08), transparent 60%);
pointer-events: none;
filter: blur(20px);
animation: glow-pulse 2.5s ease-in-out infinite;
}
@keyframes glow-pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.bar-track {
width: 14px;
height: 260px;
background: rgba(0, 0, 0, 0.55);
border-radius: 7px;
position: relative;
overflow: hidden;
box-shadow:
inset 2px 3px 6px rgba(0, 0, 0, 0.6),
inset -1px 0 2px rgba(255, 255, 255, 0.04),
1px 0 0 rgba(255, 255, 255, 0.03);
}
.bar-fill {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0%;
border-radius: 7px;
background: linear-gradient(90deg,
rgba(255,255,255,0.2) 0%,
#00ff88 15%,
#00dd70 45%,
#00aa55 80%,
#008844 100%
);
box-shadow:
0 0 12px rgba(0, 255, 136, 0.4),
0 0 28px rgba(0, 255, 136, 0.15);
}
/* 3D 高光 */
.bar-shine {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 40%;
background: linear-gradient(90deg, rgba(255,255,255,0.35), transparent);
border-radius: 7px 0 0 7px;
pointer-events: none;
}
/* 百分比 */
.percent-wrap {
display: flex;
align-items: baseline;
line-height: 1;
}
.percent-value {
font-size: 36px;
font-weight: 200;
color: #fff;
text-shadow:
0 0 20px rgba(0, 255, 136, 0.3),
0 0 40px rgba(0, 255, 136, 0.1);
font-variant-numeric: tabular-nums;
}
.percent-unit {
font-size: 18px;
font-weight: 300;
color: rgba(0, 255, 136, 0.7);
margin-left: 2px;
}
/* 状态文字 */
.status {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 1px;
max-width: 100px;
text-align: center;
line-height: 1.4;
}
.special-skill {
display: flex;
position: absolute;
bottom: 10px;
left: 10px;
color: #0aff96;
font-size: 20px;
font-weight: bold;
text-shadow: 5px 0 5px rgba(0, 255, 136, 0.4),
0 5px 5px rgba(0, 255, 136, 0.4),
5px 5px 5px rgba(0, 255, 136, 0.4);
}
</style>

View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View File

@@ -0,0 +1,4 @@
import { createConfig } from '../../vite.base.config.js'
import vue from '@vitejs/plugin-vue'
export default createConfig([vue()])