3492 lines
131 KiB
HTML
3492 lines
131 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>BGI 直播间 UI</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--cream: #fff2c9;
|
||
--cream-soft: #f8eecf;
|
||
--cream-deep: #d9bf7c;
|
||
--mint: #b8ead0;
|
||
--mint-2: #92d8b0;
|
||
--mint-3: #68bd91;
|
||
--leaf: #2f7858;
|
||
--leaf-dark: #153c2f;
|
||
--leaf-deep: #0d2b22;
|
||
--glass: rgba(19, 66, 49, 0.52);
|
||
--glass-strong: rgba(15, 55, 42, 0.72);
|
||
--glass-light: rgba(255, 250, 226, 0.24);
|
||
--line: rgba(255, 244, 202, 0.42);
|
||
--line-green: rgba(165, 235, 197, 0.48);
|
||
--text: #f7fff5;
|
||
--text-dim: #cce7d6;
|
||
--shadow: rgba(12, 47, 35, 0.45);
|
||
--custom-bg-image: none;
|
||
--custom-bg-opacity: 0.65;
|
||
--custom-bg-blur: 0px;
|
||
--custom-bg-size: cover;
|
||
}
|
||
|
||
body {
|
||
background: transparent;
|
||
color: var(--text);
|
||
font-family: "汉仪润圆", "HYRunYuan", "Microsoft YaHei", system-ui, sans-serif;
|
||
font-size: 18px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#viewport {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: flex-start; background: transparent;
|
||
overflow: hidden;
|
||
}
|
||
|
||
body.preview #viewport {
|
||
background: radial-gradient(circle at 24% 20%, rgba(150, 224, 179, 0.26), transparent 32%),
|
||
radial-gradient(circle at 80% 78%, rgba(255, 236, 177, 0.18), transparent 34%),
|
||
#10281f;
|
||
}
|
||
|
||
#canvas {
|
||
width: 1920px;
|
||
height: 1080px;
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
margin-left: -960px;
|
||
margin-top: -540px;
|
||
background: transparent;
|
||
transform-origin: center center;
|
||
}
|
||
|
||
#frame {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
z-index: 1;
|
||
isolation: isolate;
|
||
/* clip-path: 在背景上挖出 1098×640 圆角矩形镂空(左 192, 顶 210, rx 44) */
|
||
clip-path: path('M0,0 L1920,0 L1920,1080 L0,1080 Z M236,210 A44,44 0 0 0 192,254 V806 A44,44 0 0 0 236,850 H1246 A44,44 0 0 0 1290,806 V254 A44,44 0 0 0 1246,210 Z');
|
||
-webkit-clip-path: path('M0,0 L1920,0 L1920,1080 L0,1080 Z M236,210 A44,44 0 0 0 192,254 V806 A44,44 0 0 0 236,850 H1246 A44,44 0 0 0 1290,806 V254 A44,44 0 0 0 1246,210 Z');
|
||
}
|
||
|
||
#frame::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(circle at 12% 20%, rgba(255, 248, 211, 0.72) 0 9%, transparent 25%),
|
||
radial-gradient(circle at 86% 10%, rgba(154, 226, 183, 0.64) 0 11%, transparent 28%),
|
||
radial-gradient(circle at 70% 88%, rgba(255, 239, 190, 0.56) 0 13%, transparent 30%),
|
||
linear-gradient(135deg, rgba(223, 244, 231, 0.88) 0%, rgba(249, 240, 207, 0.82) 47%, rgba(191, 232, 207, 0.88) 100%),
|
||
#1a4a38;
|
||
z-index: 1;
|
||
}
|
||
|
||
#frame::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: 0.34;
|
||
background-image: linear-gradient(120deg, rgba(255, 255, 255, 0.22) 0 1px, transparent 1px 38px),
|
||
linear-gradient(30deg, rgba(47, 120, 88, 0.10) 0 1px, transparent 1px 46px),
|
||
radial-gradient(circle, rgba(47, 120, 88, 0.13) 1.2px, transparent 1.6px);
|
||
background-size: 88px 88px, 76px 76px, 34px 34px;
|
||
z-index: 3;
|
||
}
|
||
|
||
#custom-bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image: var(--custom-bg-image);
|
||
background-size: var(--custom-bg-size);
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
opacity: var(--custom-bg-opacity);
|
||
filter: blur(var(--custom-bg-blur));
|
||
transform: scale(1.02);
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#game-cutout {
|
||
position: absolute;
|
||
left: 192px;
|
||
top: 210px;
|
||
width: 1098px;
|
||
height: 640px;
|
||
border-radius: 44px;
|
||
background: transparent;
|
||
pointer-events: none;
|
||
z-index: 5;
|
||
/* 立体边框: 外发光 + 实线边框 + 内阴影 + 投影 */
|
||
border: 3px solid rgba(255, 247, 208, 0.38);
|
||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15),
|
||
0 0 32px rgba(12, 47, 35, 0.28),
|
||
0 24px 60px rgba(7, 28, 20, 0.22),
|
||
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
|
||
inset 0 2px 24px rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
body.preview #game-cutout {
|
||
background: rgba(8, 24, 19, 0.76);
|
||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12) inset,
|
||
0 18px 42px rgba(7, 38, 28, 0.26);
|
||
}
|
||
|
||
/* 游戏画面左侧:可用配置组纵向轮播 */
|
||
.available-groups-panel {
|
||
left: 16px;
|
||
top: 210px;
|
||
width: 160px;
|
||
height: 698px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 14px 6px 15px !important;
|
||
}
|
||
|
||
.available-groups-panel .panel-title {
|
||
font-size: 17px;
|
||
gap: 7px;
|
||
white-space: nowrap;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.available-groups-panel .panel-title::before {
|
||
width: 7px;
|
||
height: 13px;
|
||
}
|
||
|
||
.group-carousel-viewport {
|
||
position: relative;
|
||
width: 100%;
|
||
margin-left: 0;
|
||
z-index: 1;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
border-radius: 18px;
|
||
mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
|
||
-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
|
||
}
|
||
|
||
.group-carousel-track {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
grid-auto-rows: 82px;
|
||
gap: 7px 3px;
|
||
transform: translateY(0);
|
||
will-change: transform;
|
||
}
|
||
|
||
.group-carousel-track.smooth-scroll {
|
||
animation: group-carousel-scroll var(--group-scroll-duration, 90s) linear infinite;
|
||
}
|
||
|
||
@keyframes group-carousel-scroll {
|
||
from { transform: translateY(0); }
|
||
to { transform: translateY(var(--group-scroll-distance, -1000px)); }
|
||
}
|
||
|
||
.group-card {
|
||
position: relative;
|
||
height: 82px;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
padding: 5px 3px 4px;
|
||
overflow: hidden;
|
||
isolation: isolate;
|
||
border-radius: 14px;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.14), rgba(120, 214, 158, 0.07));
|
||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.11);
|
||
}
|
||
|
||
.group-card-image {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 46px;
|
||
height: 46px;
|
||
flex-shrink: 0;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 5px 9px rgba(6, 31, 22, 0.30));
|
||
}
|
||
|
||
.group-card-nahida {
|
||
position: absolute;
|
||
z-index: 1;
|
||
top: 39%;
|
||
left: 69%;
|
||
width: 90px;
|
||
height: 90px;
|
||
object-fit: contain;
|
||
opacity: 0.40;
|
||
pointer-events: none;
|
||
transform: translate(-50%, -50%) rotate(-24deg) scale(1.08);
|
||
filter:
|
||
sepia(0.08)
|
||
saturate(1.18)
|
||
hue-rotate(70deg)
|
||
brightness(1.72)
|
||
contrast(1.12)
|
||
drop-shadow(0 0 3px rgba(218, 255, 228, 0.72))
|
||
drop-shadow(0 0 11px rgba(104, 244, 158, 0.52));
|
||
}
|
||
|
||
.group-card.uses-nahida {
|
||
background:
|
||
radial-gradient(circle at 72% 36%, rgba(116, 241, 161, 0.25), transparent 48%),
|
||
linear-gradient(145deg, rgba(255, 255, 255, 0.16), rgba(92, 225, 144, 0.11));
|
||
border-color: rgba(184, 255, 207, 0.34);
|
||
box-shadow:
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.13),
|
||
inset -12px 8px 24px rgba(87, 224, 139, 0.06);
|
||
}
|
||
|
||
.group-card-name {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 100%;
|
||
color: var(--cream);
|
||
font-size: 11px;
|
||
line-height: 1.1;
|
||
font-weight: 850;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
text-shadow: 0 2px 7px rgba(0, 0, 0, 0.32);
|
||
}
|
||
|
||
.group-carousel-empty {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: rgba(247, 255, 245, 0.54);
|
||
font-size: 13px;
|
||
text-align: center;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.group-carousel-empty[hidden],
|
||
.group-carousel-viewport[hidden] {
|
||
display: none !important;
|
||
}
|
||
|
||
body.frame-off #frame {
|
||
display: none;
|
||
}
|
||
|
||
.deco {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
}
|
||
|
||
.deco-blob {
|
||
border-radius: 44% 56% 64% 36% / 48% 35% 65% 52%;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.48), rgba(255, 255, 255, 0.08)),
|
||
radial-gradient(circle at 34% 30%, rgba(255, 245, 205, 0.85), transparent 28%),
|
||
linear-gradient(135deg, rgba(122, 211, 159, 0.46), rgba(255, 237, 176, 0.36));
|
||
border: 1px solid rgba(255, 255, 255, 0.42);
|
||
box-shadow: inset 0 0 28px rgba(255, 255, 255, 0.22), 0 18px 44px rgba(31, 92, 67, 0.18);
|
||
backdrop-filter: blur(16px) saturate(140%);
|
||
-webkit-backdrop-filter: blur(16px) saturate(140%);
|
||
}
|
||
|
||
.deco-blob.one {
|
||
width: 250px;
|
||
height: 88px;
|
||
left: 1006px;
|
||
top: 22px;
|
||
transform: rotate(-4deg);
|
||
}
|
||
|
||
.deco-blob.two {
|
||
display: none;
|
||
}
|
||
|
||
.deco-blob.three {
|
||
width: 230px;
|
||
height: 230px;
|
||
left: 1648px;
|
||
top: 32px;
|
||
opacity: 0.65;
|
||
}
|
||
|
||
.deco-ribbon {
|
||
left: 1332px;
|
||
top: 18px;
|
||
width: 540px;
|
||
height: 74px;
|
||
border-radius: 32px;
|
||
background: linear-gradient(90deg, rgba(255, 247, 210, 0.36), rgba(137, 218, 171, 0.20));
|
||
border: 1px solid rgba(255, 255, 255, 0.34);
|
||
box-shadow: inset 0 1px 18px rgba(255, 255, 255, 0.24);
|
||
}
|
||
|
||
.deco-leaf {
|
||
width: 82px;
|
||
height: 42px;
|
||
border-radius: 100% 0 100% 0;
|
||
background: linear-gradient(135deg, rgba(96, 183, 132, 0.62), rgba(255, 240, 189, 0.38));
|
||
border: 1px solid rgba(255, 255, 255, 0.30);
|
||
box-shadow: 0 10px 24px rgba(38, 112, 78, 0.16);
|
||
}
|
||
|
||
.deco-leaf::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 13px;
|
||
top: 21px;
|
||
width: 56px;
|
||
height: 1px;
|
||
background: rgba(255, 255, 255, 0.55);
|
||
transform: rotate(-24deg);
|
||
}
|
||
|
||
.leaf-a {
|
||
left: 118px;
|
||
top: 38px;
|
||
transform: rotate(18deg);
|
||
}
|
||
|
||
.leaf-b {
|
||
left: 1212px;
|
||
top: 842px;
|
||
transform: rotate(-38deg);
|
||
}
|
||
|
||
.leaf-c {
|
||
left: 1810px;
|
||
top: 928px;
|
||
transform: rotate(34deg);
|
||
}
|
||
|
||
.deco-star {
|
||
color: rgba(255, 246, 207, 0.92);
|
||
font-size: 28px;
|
||
text-shadow: 0 0 18px rgba(255, 241, 186, 0.66);
|
||
}
|
||
|
||
.star-a {
|
||
left: 510px;
|
||
top: 46px;
|
||
}
|
||
|
||
.star-b {
|
||
left: 1468px;
|
||
top: 954px;
|
||
font-size: 22px;
|
||
}
|
||
|
||
.star-c {
|
||
left: 1850px;
|
||
top: 402px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
#hole-mask,
|
||
#hole-frame {
|
||
display: none;
|
||
}
|
||
|
||
.panel {
|
||
position: absolute;
|
||
z-index: 8;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.035)),
|
||
radial-gradient(circle at 18% 0%, rgba(255, 244, 199, 0.18), transparent 42%),
|
||
radial-gradient(circle at 100% 100%, rgba(129, 218, 166, 0.22), transparent 40%),
|
||
var(--glass);
|
||
border: 1px solid rgba(255, 247, 208, 0.34);
|
||
border-top-color: rgba(255, 255, 255, 0.56);
|
||
border-left-color: rgba(255, 255, 255, 0.42);
|
||
border-radius: 26px;
|
||
box-shadow: 0 22px 58px var(--shadow),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
||
inset 0 -18px 36px rgba(18, 70, 51, 0.18);
|
||
padding: 14px 24px 16px;
|
||
overflow: hidden;
|
||
backdrop-filter: blur(20px) saturate(150%);
|
||
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
||
}
|
||
|
||
.panel::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 18px;
|
||
right: 18px;
|
||
top: 7px;
|
||
height: 22px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.30), rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.22));
|
||
opacity: 0.66;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.panel::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 180px;
|
||
height: 180px;
|
||
right: -80px;
|
||
bottom: -100px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba(255, 239, 187, 0.18), transparent 66%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
body.glass-on .panel {
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.055)),
|
||
radial-gradient(circle at 18% 0%, rgba(255, 244, 199, 0.22), transparent 42%),
|
||
radial-gradient(circle at 100% 100%, rgba(129, 218, 166, 0.25), transparent 40%),
|
||
rgba(19, 66, 49, 0.42);
|
||
backdrop-filter: blur(28px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(28px) saturate(180%);
|
||
}
|
||
|
||
.panel-title {
|
||
position: relative;
|
||
z-index: 1;
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
color: var(--cream);
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
letter-spacing: 0.8px;
|
||
text-shadow: 0 2px 10px rgba(14, 50, 37, 0.46);
|
||
}
|
||
|
||
.panel-title::before {
|
||
content: '';
|
||
width: 9px;
|
||
height: 16px;
|
||
border-radius: 10px 2px 10px 2px;
|
||
background: linear-gradient(180deg, #fff3bf, #87dca8 62%, #4ba977);
|
||
box-shadow: 0 0 16px rgba(170, 235, 193, 0.62);
|
||
transform: rotate(12deg);
|
||
}
|
||
|
||
/* 左上:运行进度 + 排队 / 性能 */
|
||
.top-panel {
|
||
left: 16px;
|
||
top: 16px;
|
||
width: 856px;
|
||
height: 178px;
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.performance-panel {
|
||
left: 888px;
|
||
top: 16px;
|
||
width: 402px;
|
||
height: 178px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.progress-area {
|
||
width: 250px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
min-width: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.queue-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
position: relative;
|
||
z-index: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.performance-area {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.queue-list {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
grid-auto-rows: 34px;
|
||
gap: 7px 9px;
|
||
align-content: start;
|
||
}
|
||
|
||
.queue-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-width: 0;
|
||
font-size: 15px;
|
||
color: var(--text-dim);
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), rgba(170, 236, 198, 0.08));
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
padding: 4px 9px;
|
||
border-radius: 14px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.queue-item.admin {
|
||
background: linear-gradient(90deg, rgba(255, 240, 185, 0.24), rgba(130, 225, 166, 0.18));
|
||
color: var(--cream);
|
||
border-color: rgba(255, 242, 194, 0.36);
|
||
box-shadow: 0 0 18px rgba(157, 232, 184, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.queue-item .idx {
|
||
color: #fff1b7;
|
||
font-weight: 800;
|
||
margin-right: 5px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.queue-item .name {
|
||
min-width: 0;
|
||
max-width: 155px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.queue-item .pts {
|
||
margin-left: 8px;
|
||
flex-shrink: 0;
|
||
font-size: 14px;
|
||
font-family: "汉仪润圆", "HYRunYuan", "Microsoft YaHei", sans-serif;
|
||
color: #fff1b7;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.queue-more {
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 18px;
|
||
font-weight: 900;
|
||
color: var(--cream);
|
||
letter-spacing: 0.5px;
|
||
background: linear-gradient(135deg, rgba(255, 238, 183, 0.24), rgba(125, 221, 165, 0.20));
|
||
}
|
||
|
||
.badge {
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
background: linear-gradient(135deg, rgba(255, 238, 183, 0.30), rgba(125, 221, 165, 0.26));
|
||
color: var(--cream);
|
||
border: 1px solid rgba(255, 255, 255, 0.24);
|
||
padding: 3px 10px;
|
||
border-radius: 999px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.task-name-row {
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.task-nahida-icon {
|
||
width: 25px;
|
||
height: 25px;
|
||
flex: 0 0 auto;
|
||
object-fit: contain;
|
||
filter: brightness(1.55) contrast(1.18) drop-shadow(0 1px 4px rgba(6, 31, 22, 0.9));
|
||
}
|
||
|
||
.task-name {
|
||
min-width: 0;
|
||
font-size: 22px;
|
||
font-weight: 850;
|
||
color: var(--cream);
|
||
line-height: 1.12;
|
||
letter-spacing: 0.4px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.progress-area .task-meta {
|
||
gap: 6px;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.progress-area .task-meta span {
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.progress-bar {
|
||
background: rgba(12, 45, 34, 0.42);
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.20);
|
||
}
|
||
|
||
.progress-fill {
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.35), transparent 30%),
|
||
linear-gradient(90deg, #fff0b5, #a8eabf 45%, #62c58c);
|
||
height: 100%;
|
||
width: 0%;
|
||
border-radius: 999px;
|
||
transition: width 0.5s;
|
||
box-shadow: 0 0 20px rgba(150, 234, 182, 0.46);
|
||
}
|
||
|
||
.task-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 15px;
|
||
line-height: 1.1;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.perf-grid {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
align-items: center;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.perf-card {
|
||
height: 106px;
|
||
border-radius: 22px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(120, 214, 158, 0.06));
|
||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.perf-ring {
|
||
--pct: 0;
|
||
width: 58px;
|
||
height: 58px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: conic-gradient(#fff0b5 0 calc(var(--pct) * 1%), rgba(142, 232, 174, 0.72) calc(var(--pct) * 1%) calc(var(--pct) * 1% + 7%), rgba(255, 255, 255, 0.12) 0 100%);
|
||
box-shadow: 0 0 18px rgba(150, 234, 182, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.perf-ring::before {
|
||
content: '';
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 50%;
|
||
background: rgba(14, 48, 37, 0.78);
|
||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.perf-value {
|
||
position: absolute;
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
color: #fff2c9;
|
||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.perf-label {
|
||
color: var(--text-dim);
|
||
font-weight: 900;
|
||
font-size: 14px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* 左下:独立下播倒计时 */
|
||
.live-end-panel {
|
||
left: 16px;
|
||
top: 924px;
|
||
width: 160px;
|
||
height: 140px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 12px 14px 12px;
|
||
}
|
||
|
||
.live-end-countdown {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 左下:音乐 + 点歌队列 */
|
||
.music-panel {
|
||
left: 192px;
|
||
top: 866px;
|
||
width: 662px;
|
||
height: 198px;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: stretch;
|
||
padding-left: 18px;
|
||
padding-right: 18px;
|
||
}
|
||
|
||
.live-end-panel .panel-title,
|
||
.current-music .panel-title,
|
||
.music-extra .panel-title {
|
||
font-size: 17px;
|
||
gap: 7px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.live-end-panel .panel-title::before,
|
||
.current-music .panel-title::before,
|
||
.music-extra .panel-title::before {
|
||
width: 7px;
|
||
height: 13px;
|
||
}
|
||
|
||
.countdown-stage {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
padding-bottom: 1px;
|
||
}
|
||
|
||
.countdown-value {
|
||
color: #fff2c9;
|
||
font-family: "Microsoft YaHei", sans-serif;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
font-weight: 900;
|
||
letter-spacing: 1px;
|
||
white-space: nowrap;
|
||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.38), 0 0 18px rgba(255, 240, 181, 0.24);
|
||
}
|
||
|
||
.countdown-value.inactive {
|
||
color: rgba(220, 244, 229, 0.62);
|
||
font-size: 21px;
|
||
}
|
||
|
||
.countdown-target {
|
||
max-width: 100%;
|
||
margin-top: 9px;
|
||
color: var(--text-dim);
|
||
font-size: 12px;
|
||
font-weight: 750;
|
||
line-height: 1.3;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.countdown-day {
|
||
margin-top: 3px;
|
||
color: rgba(255, 241, 183, 0.76);
|
||
font-size: 11px;
|
||
line-height: 1.2;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.current-music {
|
||
width: 286px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.cover-row {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 11px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.cover {
|
||
width: 82px;
|
||
height: 82px;
|
||
border-radius: 22px;
|
||
flex-shrink: 0;
|
||
background: radial-gradient(circle at 30% 20%, rgba(255, 247, 205, 0.72), transparent 30%),
|
||
linear-gradient(135deg, rgba(121, 216, 159, 0.52), rgba(255, 236, 177, 0.28));
|
||
border: 1px solid rgba(255, 255, 255, 0.32);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 38px;
|
||
color: rgba(255, 246, 209, 0.76);
|
||
overflow: hidden;
|
||
box-shadow: 0 18px 38px rgba(10, 42, 31, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.30);
|
||
}
|
||
|
||
.cover img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.music-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.music-info .title {
|
||
font-size: 18px;
|
||
font-weight: 850;
|
||
color: var(--cream);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 3px;
|
||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.26);
|
||
}
|
||
|
||
.music-info .artist {
|
||
font-size: 14px;
|
||
color: var(--text-dim);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.music-note {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: fit-content;
|
||
margin-top: 3px;
|
||
max-width: 100%;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
background: rgba(255, 255, 255, 0.10);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
}
|
||
|
||
.music-note::before {
|
||
content: '';
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #8cf1ad;
|
||
box-shadow: 0 0 14px rgba(140, 241, 173, 0.62);
|
||
}
|
||
|
||
.music-extra {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
padding-left: 2px;
|
||
}
|
||
|
||
.playlist {
|
||
min-width: 0;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.playlist-list {
|
||
flex: 1;
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.playlist-list li {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
padding: 6px 7px;
|
||
border-radius: 12px;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), rgba(134, 222, 169, 0.06));
|
||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.playlist-list li .title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
color: var(--cream);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.playlist-list li .by {
|
||
flex-shrink: 0;
|
||
max-width: 38%;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: 12px;
|
||
color: rgba(220, 244, 229, 0.72);
|
||
}
|
||
|
||
.input-panel {
|
||
left: 870px;
|
||
top: 866px;
|
||
width: 420px;
|
||
height: 198px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.input-visualizer {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
}
|
||
|
||
.input-stage {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: 124px 1fr;
|
||
gap: 14px;
|
||
align-items: center;
|
||
padding-top: 1px;
|
||
}
|
||
|
||
.mouse-card, .keys-card {
|
||
height: 136px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(120, 214, 158, 0.06));
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.mouse-card {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mouse-orbit {
|
||
position: absolute;
|
||
left: 10px;
|
||
top: 9px;
|
||
width: 98px;
|
||
height: 98px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at center, rgba(255, 244, 202, 0.10) 0 26%, transparent 27%),
|
||
radial-gradient(circle at center, transparent 0 52%, rgba(255, 255, 255, 0.10) 53%, transparent 55%);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.mouse-orbit::before, .mouse-orbit::after {
|
||
content: '';
|
||
position: absolute;
|
||
background: rgba(255, 244, 202, 0.10);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.mouse-orbit::before {
|
||
left: 48px;
|
||
top: 9px;
|
||
width: 1px;
|
||
height: 80px;
|
||
}
|
||
|
||
.mouse-orbit::after {
|
||
left: 9px;
|
||
top: 48px;
|
||
width: 80px;
|
||
height: 1px;
|
||
}
|
||
|
||
.mouse-body {
|
||
position: relative;
|
||
width: 42px;
|
||
height: 64px;
|
||
border-radius: 23px 23px 20px 20px;
|
||
background: linear-gradient(145deg, rgba(255, 246, 211, 0.22), rgba(99, 190, 133, 0.18));
|
||
border: 2px solid rgba(255, 244, 202, 0.36);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24), 0 10px 22px rgba(8, 36, 26, 0.18);
|
||
overflow: hidden;
|
||
z-index: 2;
|
||
}
|
||
|
||
.mouse-body::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 19px;
|
||
top: 2px;
|
||
bottom: 34px;
|
||
width: 2px;
|
||
background: rgba(255, 244, 202, 0.30);
|
||
}
|
||
|
||
.mouse-body::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 16px;
|
||
top: 18px;
|
||
width: 8px;
|
||
height: 11px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 244, 202, 0.36);
|
||
}
|
||
|
||
.mouse-btn {
|
||
position: absolute;
|
||
top: 0;
|
||
width: 20px;
|
||
height: 30px;
|
||
opacity: 0.34;
|
||
transition: opacity 0.05s, background 0.05s;
|
||
}
|
||
|
||
.mouse-btn.left {
|
||
left: 0;
|
||
border-radius: 23px 0 8px 0;
|
||
}
|
||
|
||
.mouse-btn.right {
|
||
right: 0;
|
||
border-radius: 0 23px 0 8px;
|
||
}
|
||
|
||
.mouse-btn.active {
|
||
opacity: 1;
|
||
background: linear-gradient(180deg, rgba(255, 239, 176, 0.92), rgba(136, 231, 171, 0.66));
|
||
box-shadow: 0 0 18px rgba(158, 238, 186, 0.42);
|
||
}
|
||
|
||
.mouse-trail {
|
||
position: absolute;
|
||
left: 54px;
|
||
top: 54px;
|
||
width: 18px;
|
||
height: 18px;
|
||
margin-left: -9px;
|
||
margin-top: -9px;
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
transform: translate(0, 0) scale(0.55);
|
||
background: radial-gradient(circle, #fff1b7 0 25%, rgba(142, 232, 174, 0.82) 26% 56%, rgba(142, 232, 174, 0.12) 57% 100%);
|
||
box-shadow: 0 0 18px rgba(255, 241, 183, 0.55), 0 0 28px rgba(142, 232, 174, 0.36);
|
||
transition: transform 0.06s linear, opacity 0.08s linear;
|
||
z-index: 3;
|
||
}
|
||
|
||
.mouse-trail.active {
|
||
opacity: 1;
|
||
}
|
||
|
||
.keys-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.key-row {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.keycap {
|
||
width: 34px;
|
||
height: 29px;
|
||
border-radius: 9px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: "汉仪润圆", "HYRunYuan", "Microsoft YaHei", sans-serif;
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
color: rgba(232, 250, 238, 0.72);
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.10), rgba(128, 218, 164, 0.08));
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||
transition: background 0.04s, color 0.04s, transform 0.04s, box-shadow 0.04s;
|
||
}
|
||
|
||
.keycap.space {
|
||
width: 154px;
|
||
}
|
||
|
||
.keycap.active {
|
||
color: #244233;
|
||
transform: translateY(1px);
|
||
background: linear-gradient(180deg, #fff0b5, #8fe5ae 68%, #62c58c);
|
||
box-shadow: 0 0 16px rgba(150, 234, 182, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
||
}
|
||
|
||
/* 右上:BGI 日志 */
|
||
.log-panel {
|
||
left: 1306px;
|
||
top: 16px;
|
||
width: 598px;
|
||
height: 338px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.log-title {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.log-title .status {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-size: 15px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #8fa99a;
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
|
||
}
|
||
|
||
.dot.on {
|
||
background: #8cf1ad;
|
||
box-shadow: 0 0 14px #8cf1ad, 0 0 28px rgba(140, 241, 173, 0.35);
|
||
}
|
||
|
||
.log-box {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
|
||
font-size: 16px;
|
||
line-height: 1.7;
|
||
color: var(--text-dim);
|
||
overflow-y: auto;
|
||
padding: 4px 7px 0 0;
|
||
mask-image: linear-gradient(to bottom, transparent 0, #000 14px, #000 calc(100% - 8px));
|
||
}
|
||
|
||
.log-box::-webkit-scrollbar, .danmu-box::-webkit-scrollbar, .playlist-list::-webkit-scrollbar {
|
||
width: 5px;
|
||
}
|
||
|
||
.log-box::-webkit-scrollbar-thumb, .danmu-box::-webkit-scrollbar-thumb, .playlist-list::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 242, 198, 0.26);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.log-line {
|
||
word-break: break-all;
|
||
padding: 1px 0;
|
||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
|
||
}
|
||
|
||
.log-inf {
|
||
color: #aaf1c3;
|
||
}
|
||
|
||
.log-tp {
|
||
color: #ffe8a8;
|
||
}
|
||
|
||
.log-warn {
|
||
color: #ffd08f;
|
||
}
|
||
|
||
.log-err {
|
||
color: #ffaaa7;
|
||
}
|
||
|
||
.log-blue {
|
||
color: #bfe1ff;
|
||
}
|
||
|
||
.log-fight {
|
||
color: #ffc1d0;
|
||
}
|
||
|
||
.log-pick {
|
||
color: #dfd0ff;
|
||
}
|
||
|
||
.log-dbg {
|
||
color: rgba(216, 232, 220, 0.50);
|
||
}
|
||
|
||
/* 右侧中部:教学轮播 */
|
||
.tutorial-panel {
|
||
left: 1306px;
|
||
top: 370px;
|
||
width: 598px;
|
||
height: 140px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tutorial-title {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.tutorial-title::after {
|
||
content: "轮播";
|
||
margin-left: auto;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: rgba(247, 255, 245, 0.72);
|
||
background: rgba(255, 242, 201, 0.10);
|
||
border: 1px solid rgba(255, 242, 201, 0.13);
|
||
}
|
||
|
||
.command-quick-reference {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 2px 12px;
|
||
margin: 0 12px 2px;
|
||
padding: 3px 8px;
|
||
border-top: 1px solid rgba(255, 242, 201, 0.15);
|
||
border-bottom: 1px solid rgba(255, 242, 201, 0.15);
|
||
color: rgba(244, 255, 247, 0.86);
|
||
font-size: 12px;
|
||
font-weight: 720;
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.command-quick-reference span {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.command-quick-reference strong {
|
||
color: #fff0b5;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.tutorial-stage {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 32px;
|
||
padding: 0 10px 1px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tutorial-text {
|
||
width: 100%;
|
||
color: var(--text);
|
||
font-size: 20px;
|
||
font-weight: 900;
|
||
line-height: 1.25;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
letter-spacing: 0;
|
||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.48), 0 0 14px rgba(255, 240, 181, 0.18);
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
transition: opacity 0.45s ease, transform 0.45s ease;
|
||
}
|
||
|
||
.tutorial-text.long {
|
||
font-size: 18px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.tutorial-text.extra-long {
|
||
font-size: 16px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.tutorial-text.switching {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
|
||
.tutorial-dots {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
.tutorial-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: rgba(247, 255, 245, 0.23);
|
||
transition: width 0.35s ease, background 0.35s ease, box-shadow 0.35s ease;
|
||
}
|
||
|
||
.tutorial-dot.active {
|
||
width: 22px;
|
||
border-radius: 999px;
|
||
background: #fff0b5;
|
||
box-shadow: 0 0 10px rgba(255, 240, 181, 0.48);
|
||
}
|
||
|
||
/* 右下:弹幕 */
|
||
.danmu-panel {
|
||
left: 1306px;
|
||
top: 526px;
|
||
width: 598px;
|
||
height: 538px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.danmu-box {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
overflow-y: auto;
|
||
padding-right: 6px;
|
||
}
|
||
|
||
.danmu-item {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: baseline;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.035)),
|
||
rgba(11, 45, 34, 0.20);
|
||
padding: 8px 11px;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.danmu-name {
|
||
color: #fff1b7;
|
||
font-weight: 800;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.danmu-text {
|
||
color: var(--text);
|
||
font-size: 18px;
|
||
word-break: break-all;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.empty {
|
||
color: rgba(247, 255, 245, 0.48);
|
||
text-align: center;
|
||
padding: 20px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* ===== 礼物特效层 ===== */
|
||
#gift-layer {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 60;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#gift-canvas {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.gift-banner {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 120px;
|
||
transform: translateX(-50%) translateY(-30px) scale(0.8);
|
||
opacity: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 30px;
|
||
border-radius: 999px;
|
||
font-weight: 900;
|
||
white-space: nowrap;
|
||
transition: transform 0.35s cubic-bezier(0.2, 1.4, 0.4, 1), opacity 0.3s ease;
|
||
}
|
||
|
||
.gift-banner.show {
|
||
transform: translateX(-50%) translateY(0) scale(1);
|
||
opacity: 1;
|
||
}
|
||
|
||
.gift-banner.hide {
|
||
transform: translateX(-50%) translateY(-20px) scale(0.92);
|
||
opacity: 0;
|
||
transition: transform 0.4s ease, opacity 0.4s ease;
|
||
}
|
||
|
||
.gift-banner .gb-name { font-weight: 900; }
|
||
.gift-banner .gb-gift { font-weight: 800; }
|
||
|
||
.gift-banner.tier-1 {
|
||
font-size: 22px;
|
||
color: #eafff2;
|
||
background: rgba(20, 60, 45, 0.72);
|
||
border: 1px solid rgba(155, 240, 190, 0.35);
|
||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.gift-banner.tier-2 {
|
||
font-size: 26px;
|
||
color: #fff3c4;
|
||
background: rgba(70, 52, 10, 0.78);
|
||
border: 1px solid rgba(255, 214, 110, 0.55);
|
||
box-shadow: 0 6px 26px rgba(0, 0, 0, 0.4), 0 0 24px rgba(255, 200, 80, 0.25);
|
||
}
|
||
|
||
.gift-banner.tier-3 {
|
||
font-size: 30px;
|
||
color: #ffe9ec;
|
||
background: rgba(88, 22, 40, 0.82);
|
||
border: 2px solid rgba(255, 130, 160, 0.7);
|
||
box-shadow: 0 8px 34px rgba(0, 0, 0, 0.45), 0 0 40px rgba(255, 110, 150, 0.4);
|
||
animation: gift-pulse 0.9s ease-in-out infinite alternate;
|
||
}
|
||
|
||
.gift-banner.tier-4 {
|
||
font-size: 34px;
|
||
color: #fff;
|
||
background: rgba(60, 20, 90, 0.85);
|
||
border: 2px solid rgba(220, 160, 255, 0.85);
|
||
box-shadow: 0 10px 44px rgba(0, 0, 0, 0.5), 0 0 60px rgba(200, 130, 255, 0.55);
|
||
animation: gift-pulse 0.6s ease-in-out infinite alternate;
|
||
}
|
||
|
||
@keyframes gift-pulse {
|
||
from { filter: brightness(1); }
|
||
to { filter: brightness(1.25); }
|
||
}
|
||
|
||
.gift-flash {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(circle at 50% 30%, rgba(255, 235, 170, 0.35), transparent 60%);
|
||
opacity: 0;
|
||
transition: opacity 0.25s ease;
|
||
}
|
||
|
||
.gift-flash.on { opacity: 1; }
|
||
|
||
/* ================================================================
|
||
主题预设:aurora(极光)
|
||
仅做视觉美化,不改变任何面板位置/尺寸/布局(left/top/width/height 不变)。
|
||
通过 body.theme-aurora 覆盖颜色变量与玻璃质感。
|
||
================================================================ */
|
||
body.theme-aurora {
|
||
--cream: #eef3ff;
|
||
--cream-soft: #dce6fb;
|
||
--cream-deep: #a8baff;
|
||
--mint: #6fe0ff;
|
||
--mint-2: #5bc0ff;
|
||
--mint-3: #7d8bff;
|
||
--leaf: #5566ff;
|
||
--leaf-dark: #2c2c66;
|
||
--leaf-deep: #131436;
|
||
--glass: rgba(16, 20, 48, 0.50);
|
||
--glass-strong: rgba(10, 12, 32, 0.72);
|
||
--glass-light: rgba(150, 180, 255, 0.20);
|
||
--line: rgba(150, 180, 255, 0.40);
|
||
--line-green: rgba(120, 200, 255, 0.46);
|
||
--text: #f4f7ff;
|
||
--text-dim: #b7c4e6;
|
||
--shadow: rgba(3, 5, 18, 0.62);
|
||
}
|
||
|
||
body.theme-aurora #frame::before {
|
||
background:
|
||
radial-gradient(circle at 14% 18%, rgba(122, 140, 255, 0.52) 0 10%, transparent 30%),
|
||
radial-gradient(circle at 88% 12%, rgba(94, 231, 255, 0.48) 0 12%, transparent 32%),
|
||
radial-gradient(circle at 72% 90%, rgba(255, 122, 217, 0.44) 0 14%, transparent 34%),
|
||
radial-gradient(circle at 30% 86%, rgba(52, 245, 197, 0.38) 0 10%, transparent 30%),
|
||
linear-gradient(135deg, rgba(20, 22, 56, 0.92) 0%, rgba(28, 24, 68, 0.88) 50%, rgba(13, 18, 50, 0.92) 100%),
|
||
#0b0e26;
|
||
}
|
||
|
||
body.theme-aurora #frame::after {
|
||
opacity: 0.28;
|
||
background-image:
|
||
linear-gradient(120deg, rgba(160, 190, 255, 0.20) 0 1px, transparent 1px 40px),
|
||
linear-gradient(30deg, rgba(120, 200, 255, 0.12) 0 1px, transparent 1px 48px),
|
||
radial-gradient(circle, rgba(150, 180, 255, 0.20) 1.4px, transparent 1.8px);
|
||
background-size: 90px 90px, 78px 78px, 36px 36px;
|
||
}
|
||
|
||
body.theme-aurora #game-cutout {
|
||
border: 3px solid rgba(150, 180, 255, 0.46);
|
||
box-shadow:
|
||
0 0 0 1px rgba(180, 200, 255, 0.18),
|
||
0 0 40px rgba(90, 120, 255, 0.30),
|
||
0 26px 70px rgba(4, 6, 24, 0.55),
|
||
inset 0 0 0 1px rgba(200, 220, 255, 0.10),
|
||
inset 0 2px 28px rgba(0, 0, 0, 0.32);
|
||
animation: aurora-cutout-glow 5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes aurora-cutout-glow {
|
||
0%, 100% {
|
||
box-shadow:
|
||
0 0 0 1px rgba(180, 200, 255, 0.18),
|
||
0 0 40px rgba(90, 120, 255, 0.30),
|
||
0 26px 70px rgba(4, 6, 24, 0.55),
|
||
inset 0 0 0 1px rgba(200, 220, 255, 0.10),
|
||
inset 0 2px 28px rgba(0, 0, 0, 0.32);
|
||
}
|
||
50% {
|
||
box-shadow:
|
||
0 0 0 1px rgba(200, 220, 255, 0.28),
|
||
0 0 58px rgba(110, 150, 255, 0.46),
|
||
0 26px 70px rgba(4, 6, 24, 0.55),
|
||
inset 0 0 0 1px rgba(220, 235, 255, 0.16),
|
||
inset 0 2px 28px rgba(0, 0, 0, 0.32);
|
||
}
|
||
}
|
||
|
||
body.theme-aurora .panel {
|
||
background:
|
||
linear-gradient(145deg, rgba(150, 180, 255, 0.16), rgba(150, 180, 255, 0.04)),
|
||
radial-gradient(circle at 18% 0%, rgba(122, 140, 255, 0.20), transparent 44%),
|
||
radial-gradient(circle at 100% 100%, rgba(94, 231, 255, 0.18), transparent 42%),
|
||
rgba(16, 20, 46, 0.55);
|
||
border: 1px solid rgba(150, 180, 255, 0.36);
|
||
border-top-color: rgba(190, 210, 255, 0.52);
|
||
border-left-color: rgba(170, 195, 255, 0.42);
|
||
box-shadow:
|
||
0 22px 58px var(--shadow),
|
||
inset 0 1px 0 rgba(200, 220, 255, 0.22),
|
||
inset 0 -18px 36px rgba(8, 10, 30, 0.32);
|
||
backdrop-filter: blur(22px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(22px) saturate(160%);
|
||
}
|
||
|
||
body.theme-aurora .panel::before {
|
||
background: linear-gradient(90deg, rgba(170, 195, 255, 0.28), rgba(170, 195, 255, 0.06), rgba(120, 200, 255, 0.22));
|
||
}
|
||
|
||
body.theme-aurora .panel::after {
|
||
background: radial-gradient(circle, rgba(94, 231, 255, 0.16), transparent 66%);
|
||
}
|
||
|
||
body.theme-aurora .panel-title::before {
|
||
background: linear-gradient(180deg, #cfe0ff, #8aa4ff 55%, #5e7cff);
|
||
box-shadow: 0 0 18px rgba(130, 160, 255, 0.65);
|
||
}
|
||
|
||
body.theme-aurora .queue-item {
|
||
background: linear-gradient(90deg, rgba(150, 180, 255, 0.14), rgba(94, 231, 255, 0.10));
|
||
border: 1px solid rgba(150, 180, 255, 0.20);
|
||
}
|
||
|
||
body.theme-aurora .queue-item.admin {
|
||
background: linear-gradient(90deg, rgba(122, 140, 255, 0.30), rgba(94, 231, 255, 0.20));
|
||
border-color: rgba(160, 190, 255, 0.42);
|
||
box-shadow: 0 0 20px rgba(122, 140, 255, 0.24), inset 0 1px 0 rgba(220, 235, 255, 0.20);
|
||
}
|
||
|
||
body.theme-aurora .queue-item .idx { color: #cfe0ff; }
|
||
body.theme-aurora .queue-item .pts { color: #cfe0ff; }
|
||
|
||
body.theme-aurora .group-card {
|
||
background: linear-gradient(145deg, rgba(150, 180, 255, 0.16), rgba(94, 231, 255, 0.08));
|
||
border: 1px solid rgba(150, 180, 255, 0.20);
|
||
box-shadow: inset 0 1px 0 rgba(220, 235, 255, 0.14);
|
||
}
|
||
|
||
body.theme-aurora .group-card.uses-nahida {
|
||
background:
|
||
radial-gradient(circle at 72% 36%, rgba(122, 200, 255, 0.28), transparent 48%),
|
||
linear-gradient(145deg, rgba(150, 180, 255, 0.18), rgba(94, 231, 255, 0.12));
|
||
border-color: rgba(160, 210, 255, 0.38);
|
||
box-shadow:
|
||
inset 0 1px 0 rgba(220, 235, 255, 0.16),
|
||
inset -12px 8px 24px rgba(80, 120, 255, 0.10);
|
||
}
|
||
|
||
body.theme-aurora .group-card-name {
|
||
text-shadow: 0 2px 8px rgba(4, 6, 24, 0.5);
|
||
}
|
||
|
||
body.theme-aurora .progress-bar {
|
||
background: rgba(14, 18, 46, 0.5);
|
||
border: 1px solid rgba(150, 180, 255, 0.22);
|
||
}
|
||
|
||
body.theme-aurora .progress-fill {
|
||
background:
|
||
linear-gradient(90deg, rgba(255, 255, 255, 0.32), transparent 30%),
|
||
linear-gradient(90deg, #cfe0ff, #8aa4ff 45%, #5e7cff);
|
||
box-shadow: 0 0 20px rgba(130, 160, 255, 0.5);
|
||
}
|
||
|
||
body.theme-aurora .perf-card {
|
||
background: linear-gradient(145deg, rgba(150, 180, 255, 0.13), rgba(94, 231, 255, 0.06));
|
||
border: 1px solid rgba(150, 180, 255, 0.18);
|
||
}
|
||
|
||
body.theme-aurora .tutorial-title::after {
|
||
color: rgba(230, 238, 255, 0.74);
|
||
background: rgba(150, 180, 255, 0.12);
|
||
border: 1px solid rgba(150, 180, 255, 0.22);
|
||
}
|
||
|
||
body.theme-aurora .tutorial-dot.active {
|
||
background: #cfe0ff;
|
||
box-shadow: 0 0 10px rgba(150, 180, 255, 0.55);
|
||
}
|
||
|
||
body.theme-aurora .tutorial-text {
|
||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.5), 0 0 16px rgba(150, 180, 255, 0.22);
|
||
}
|
||
/* 桌宠与状态环作为一个整体,只在游戏镂空区外游走。 */
|
||
.kirara-wanderer {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
z-index: 12;
|
||
width: 172px;
|
||
height: 140px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
pointer-events: none;
|
||
will-change: transform;
|
||
}
|
||
.kirara-sprite {
|
||
width: 126px;
|
||
height: 140px;
|
||
flex: none;
|
||
object-fit: contain;
|
||
image-rendering: pixelated;
|
||
filter: drop-shadow(0 5px 1px rgba(5, 22, 18, .8));
|
||
}
|
||
.kirara-status {
|
||
width: 40px;
|
||
height: 40px;
|
||
flex: none;
|
||
overflow: visible;
|
||
filter: drop-shadow(0 2px 1px rgba(7, 22, 21, .8));
|
||
}
|
||
.kirara-ring-track, .kirara-ring-progress { shape-rendering: crispEdges; }
|
||
.kirara-ring-track.stamina { fill: #65562e; }
|
||
.kirara-ring-track.cleanliness { fill: #28566a; }
|
||
.kirara-ring-track.mood { fill: #70413d; }
|
||
.kirara-ring-progress.stamina { fill: #f8d76c; }
|
||
.kirara-ring-progress.cleanliness { fill: #7ccdf5; }
|
||
.kirara-ring-progress.mood { fill: #f58382; }
|
||
.kirara-need-bubble {
|
||
position: absolute;
|
||
z-index: 2;
|
||
box-sizing: border-box;
|
||
padding: 8px 11px;
|
||
border: 3px solid #3d3034;
|
||
background: #fff0c9;
|
||
color: #49372f;
|
||
box-shadow: 4px 4px 0 rgba(18, 33, 29, .65), inset 0 0 0 2px #fff9e5;
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
line-height: 22px;
|
||
letter-spacing: .2px;
|
||
white-space: nowrap;
|
||
text-shadow: none;
|
||
}
|
||
.kirara-need-bubble[hidden] { display: none; }
|
||
.kirara-need-bubble::after {
|
||
content: "";
|
||
position: absolute;
|
||
width: 9px;
|
||
height: 9px;
|
||
background: #fff0c9;
|
||
border: 3px solid #3d3034;
|
||
}
|
||
.kirara-need-bubble[data-place="side"]::after {
|
||
left: -12px;
|
||
top: 17px;
|
||
border-right: 0;
|
||
border-top: 0;
|
||
}
|
||
.kirara-need-bubble[data-place="above"]::after {
|
||
left: 39px;
|
||
bottom: -12px;
|
||
border-left: 0;
|
||
border-top: 0;
|
||
}
|
||
.kirara-need-bubble[data-place="below"]::after {
|
||
left: 39px;
|
||
top: -12px;
|
||
border-right: 0;
|
||
border-bottom: 0;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="viewport">
|
||
<div id="canvas">
|
||
<div id="frame">
|
||
<div id="custom-bg"></div>
|
||
<div class="deco deco-ribbon"></div>
|
||
<div class="deco deco-blob one"></div>
|
||
<div class="deco deco-blob two"></div>
|
||
<div class="deco deco-blob three"></div>
|
||
<div class="deco deco-leaf leaf-a"></div>
|
||
<div class="deco deco-leaf leaf-b"></div>
|
||
<div class="deco deco-leaf leaf-c"></div>
|
||
<div class="deco deco-star star-a">✦</div>
|
||
<div class="deco deco-star star-b">✦</div>
|
||
<div class="deco deco-star star-c">✦</div>
|
||
</div>
|
||
<!-- game-cutout 放在 #frame 外面, 不受 clip-path 裁剪, 保证镂空区域完全透明(OBS 穿透) -->
|
||
<div id="game-cutout"></div>
|
||
|
||
<!-- 镂空区外游走的桌宠;三层状态环跟随角色,不显示文字。 -->
|
||
<div id="kirara-wanderer" class="kirara-wanderer" aria-label="绮良良桌宠状态">
|
||
<canvas id="kirara-sprite" class="kirara-sprite" width="126" height="140" role="img" aria-label="像素绮良良桌宠"></canvas>
|
||
<svg class="kirara-status" viewBox="0 0 80 80" role="img" aria-label="外圈黄色体力、中圈蓝色清洁、内圈红色心情">
|
||
<path id="kirara-stamina-track" class="kirara-ring-track stamina"/>
|
||
<path id="kirara-stamina-ring" class="kirara-ring-progress stamina"/>
|
||
<path id="kirara-cleanliness-track" class="kirara-ring-track cleanliness"/>
|
||
<path id="kirara-cleanliness-ring" class="kirara-ring-progress cleanliness"/>
|
||
<path id="kirara-mood-track" class="kirara-ring-track mood"/>
|
||
<path id="kirara-mood-ring" class="kirara-ring-progress mood"/>
|
||
</svg>
|
||
<div id="kirara-need-bubble" class="kirara-need-bubble" role="status" aria-live="polite" hidden></div>
|
||
</div>
|
||
|
||
<!-- 游戏画面左侧:可用配置组 -->
|
||
<section class="panel available-groups-panel">
|
||
<div class="panel-title">可用配置组</div>
|
||
<div id="group-carousel-viewport" class="group-carousel-viewport">
|
||
<div id="group-carousel-track" class="group-carousel-track"></div>
|
||
</div>
|
||
<div id="group-carousel-empty" class="group-carousel-empty" hidden>暂无可展示的配置组</div>
|
||
</section>
|
||
|
||
<!-- 左上:运行进度 + 排队状态 -->
|
||
<section class="panel top-panel">
|
||
<div class="progress-area">
|
||
<div class="panel-title">运行进度</div>
|
||
<div class="task-name-row">
|
||
<img id="task-nahida-icon" class="task-nahida-icon" src="/uploads/nahida_collect.webp" alt="纳西妲采集" title="该配置组使用纳西妲采集" hidden>
|
||
<div id="task-name" class="task-name">无任务运行</div>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div id="progress-fill" class="progress-fill"></div>
|
||
</div>
|
||
<div class="task-meta">
|
||
<span id="task-operator">操作人:--</span>
|
||
<span id="task-points">剩余积分:--</span>
|
||
</div>
|
||
</div>
|
||
<div class="queue-area">
|
||
<div class="panel-title">排队状态 <span id="queue-count" class="badge">0</span></div>
|
||
<div id="queue-list" class="queue-list">
|
||
<div class="empty">队列为空</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左上右侧:性能监测 -->
|
||
<section class="panel performance-panel">
|
||
<div class="panel-title">性能监测</div>
|
||
<div class="perf-grid">
|
||
<div class="perf-card">
|
||
<div id="perf-cpu-ring" class="perf-ring"><span id="perf-cpu" class="perf-value">--%</span></div>
|
||
<div class="perf-label">CPU</div>
|
||
</div>
|
||
<div class="perf-card">
|
||
<div id="perf-mem-ring" class="perf-ring"><span id="perf-mem" class="perf-value">--%</span></div>
|
||
<div class="perf-label">内存</div>
|
||
</div>
|
||
<div class="perf-card">
|
||
<div id="perf-gpu-ring" class="perf-ring"><span id="perf-gpu" class="perf-value">--%</span></div>
|
||
<div class="perf-label">GPU</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左下:独立下播倒计时 -->
|
||
<section class="panel live-end-panel">
|
||
<div class="panel-title">下播倒计时</div>
|
||
<div class="live-end-countdown">
|
||
<div class="countdown-stage">
|
||
<div id="live-end-countdown" class="countdown-value inactive">--:--:--</div>
|
||
<div id="live-end-target" class="countdown-target">正在读取关播计划</div>
|
||
<div id="live-end-day" class="countdown-day">系统定时</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左下:当前音乐 + 播放列表 -->
|
||
<section class="panel music-panel">
|
||
<div class="current-music">
|
||
<div class="panel-title">当前音乐</div>
|
||
<div class="cover-row">
|
||
<div id="music-cover" class="cover">♪</div>
|
||
<div class="music-info">
|
||
<div id="music-title" class="title">未配置</div>
|
||
<div id="music-artist" class="artist">等待数据源</div>
|
||
<div id="music-note" class="music-note">正在同步播放器</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="music-extra">
|
||
<div class="playlist">
|
||
<div class="panel-title">点歌队列</div>
|
||
<ul id="playlist" class="playlist-list">
|
||
<li class="empty">暂无点歌</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左下右侧:键鼠输入 -->
|
||
<section class="panel input-panel">
|
||
<div class="input-visualizer">
|
||
<div class="panel-title">键鼠输入</div>
|
||
<div class="input-stage">
|
||
<div class="mouse-card">
|
||
<div class="mouse-orbit"></div>
|
||
<div class="mouse-body">
|
||
<div id="mouse-left" class="mouse-btn left"></div>
|
||
<div id="mouse-right" class="mouse-btn right"></div>
|
||
</div>
|
||
<div id="mouse-trail" class="mouse-trail"></div>
|
||
</div>
|
||
<div class="keys-card">
|
||
<div class="key-row">
|
||
<div id="key-q" class="keycap">Q</div>
|
||
<div id="key-w" class="keycap">W</div>
|
||
<div id="key-e" class="keycap">E</div>
|
||
<div id="key-r" class="keycap">R</div>
|
||
</div>
|
||
<div class="key-row">
|
||
<div id="key-a" class="keycap">A</div>
|
||
<div id="key-s" class="keycap">S</div>
|
||
<div id="key-d" class="keycap">D</div>
|
||
<div id="key-f" class="keycap">F</div>
|
||
</div>
|
||
<div class="key-row">
|
||
<div id="key-space" class="keycap space">SPACE</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 右上:BGI 日志 -->
|
||
<section class="panel log-panel">
|
||
<div class="panel-title log-title">
|
||
<span>BGI 日志</span>
|
||
<div class="status"><span id="bgi-dot" class="dot"></span><span id="bgi-status">未运行</span></div>
|
||
</div>
|
||
<div id="log-box" class="log-box"></div>
|
||
</section>
|
||
|
||
<!-- 右侧中部:教学轮播 -->
|
||
<section class="panel tutorial-panel">
|
||
<div class="panel-title tutorial-title">直播间使用说明</div>
|
||
<div id="command-quick-reference" class="command-quick-reference"></div>
|
||
<div class="tutorial-stage">
|
||
<div id="tutorial-text" class="tutorial-text">发送“签到”领取积分,发送“排队”加入队伍</div>
|
||
</div>
|
||
<div id="tutorial-dots" class="tutorial-dots" aria-hidden="true"></div>
|
||
</section>
|
||
|
||
<!-- 右下:直播间弹幕 -->
|
||
<section class="panel danmu-panel">
|
||
<div class="panel-title">直播间弹幕</div>
|
||
<div id="danmu-box" class="danmu-box">
|
||
<div class="empty">等待弹幕...</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 礼物特效层(最顶层,不拦截事件) -->
|
||
<div id="gift-layer">
|
||
<canvas id="gift-canvas"></canvas>
|
||
<div id="gift-flash" class="gift-flash"></div>
|
||
<div id="gift-banner" class="gift-banner tier-1" style="display:none">
|
||
<span class="gb-name"></span><span class="gb-gift"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function esc(s) {
|
||
return String(s).replace(/[&<>"']/g, c => ({
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
"\"": """,
|
||
"'": "'"
|
||
}[c]));
|
||
}
|
||
|
||
function fmt(sec) {
|
||
const m = Math.floor(sec / 60), s = Math.abs(sec) % 60;
|
||
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
||
}
|
||
|
||
function fmtMin(sec) {
|
||
const m = Math.floor(sec / 60);
|
||
return m + ":" + String(sec % 60).padStart(2, "0");
|
||
}
|
||
|
||
const baseTutorialSlides = [
|
||
"发送“签到”领取积分,发送“排队”加入队伍",
|
||
"成为队首后,请在90秒内发送“上号”",
|
||
"发送“上号”后,请在4分钟内完成扫码登录",
|
||
"扫码后,请在1分钟内发送“是”或“不是”确认账号",
|
||
"发送“执行 配置组名称”开始任务",
|
||
"发送“积分”查询积分,发送“队列”查看排队情况",
|
||
"发送“退出”离开队伍,发送“点歌 歌名”点歌"
|
||
];
|
||
const fallbackCommandHints = {
|
||
daily: { enabled: true, aliases: ["自动每日", "每日", "日常"], preferred_alias: "每日" },
|
||
switch_party: { enabled: true, aliases: ["切换队伍", "换队"], preferred_alias: "换队" },
|
||
edit_party: { enabled: true, aliases: ["修改队员", "换角色"], preferred_alias: "换角色" }
|
||
};
|
||
let commandHints = fallbackCommandHints;
|
||
let tutorialSlides = baseTutorialSlides.slice();
|
||
let tutorialIndex = 0;
|
||
|
||
function normalizeCommandHint(value, fallback) {
|
||
const source = value && typeof value === "object" ? value : fallback;
|
||
const aliases = Array.isArray(source.aliases)
|
||
? source.aliases.map((item) => String(item || "").trim()).filter(Boolean)
|
||
: fallback.aliases.slice();
|
||
const preferred = String(source.preferred_alias || "").trim()
|
||
|| aliases.reduce((best, item) => !best || item.length < best.length ? item : best, "")
|
||
|| fallback.preferred_alias;
|
||
return {
|
||
enabled: source.enabled !== false,
|
||
aliases,
|
||
preferred_alias: preferred
|
||
};
|
||
}
|
||
|
||
function renderCommandQuickReference() {
|
||
const el = document.getElementById("command-quick-reference");
|
||
if (!el) return;
|
||
const daily = commandHints.daily;
|
||
if (!daily || !daily.enabled) {
|
||
el.hidden = true;
|
||
el.innerHTML = "";
|
||
return;
|
||
}
|
||
const cmd = esc(daily.preferred_alias || "每日");
|
||
el.hidden = false;
|
||
el.innerHTML = [
|
||
`<span><strong>${cmd}</strong>:基础流程</span>`,
|
||
`<span><strong>${cmd} 秘境</strong> <名称></span>`,
|
||
`<span><strong>${cmd} 地脉</strong> <类型> <国家></span>`,
|
||
`<span><strong>${cmd} 委托</strong>:每日委托</span>`
|
||
].join("");
|
||
}
|
||
|
||
function rebuildTutorialSlides() {
|
||
const slides = baseTutorialSlides.slice();
|
||
const daily = commandHints.daily;
|
||
const switchParty = commandHints.switch_party;
|
||
const editParty = commandHints.edit_party;
|
||
if (daily && daily.enabled) {
|
||
const cmd = daily.preferred_alias || "每日";
|
||
slides.push(`发送“${cmd}”完成邮件、树脂、尘歌壶和每日奖励`);
|
||
slides.push(`发送“${cmd} 秘境 风本”刷到树脂耗尽`);
|
||
slides.push(`发送“${cmd} 地脉 经验 蒙德”刷经验地脉`);
|
||
slides.push(`发送“${cmd} 委托”执行每日委托配置`);
|
||
}
|
||
const partyParts = [];
|
||
if (switchParty && switchParty.enabled) {
|
||
partyParts.push(`“${switchParty.preferred_alias || "换队"} 永冻队”切换队伍`);
|
||
}
|
||
if (editParty && editParty.enabled) {
|
||
partyParts.push(`“${editParty.preferred_alias || "换角色"} 四名角色”修改队员`);
|
||
}
|
||
if (partyParts.length) slides.push(`发送${partyParts.join(",发送")}`);
|
||
tutorialSlides = slides;
|
||
tutorialIndex %= tutorialSlides.length;
|
||
renderCommandQuickReference();
|
||
showTutorialSlide(tutorialIndex, false);
|
||
}
|
||
|
||
function applyCommandHints(value) {
|
||
const source = value && typeof value === "object" ? value : {};
|
||
commandHints = {
|
||
daily: normalizeCommandHint(source.daily, fallbackCommandHints.daily),
|
||
switch_party: normalizeCommandHint(source.switch_party, fallbackCommandHints.switch_party),
|
||
edit_party: normalizeCommandHint(source.edit_party, fallbackCommandHints.edit_party)
|
||
};
|
||
rebuildTutorialSlides();
|
||
}
|
||
|
||
function renderTutorialDots() {
|
||
const dots = document.getElementById("tutorial-dots");
|
||
if (!dots) return;
|
||
dots.innerHTML = tutorialSlides.map((_, index) =>
|
||
'<span class="tutorial-dot' + (index === tutorialIndex ? ' active' : '') + '"></span>'
|
||
).join("");
|
||
}
|
||
|
||
function showTutorialSlide(index, animate = true) {
|
||
const text = document.getElementById("tutorial-text");
|
||
if (!text) return;
|
||
tutorialIndex = (index + tutorialSlides.length) % tutorialSlides.length;
|
||
const update = () => {
|
||
const slide = tutorialSlides[tutorialIndex];
|
||
text.textContent = slide;
|
||
text.classList.toggle("long", slide.length > 20 && slide.length < 24);
|
||
text.classList.toggle("extra-long", slide.length >= 24);
|
||
renderTutorialDots();
|
||
requestAnimationFrame(() => text.classList.remove("switching"));
|
||
};
|
||
if (!animate) {
|
||
update();
|
||
return;
|
||
}
|
||
text.classList.add("switching");
|
||
window.setTimeout(update, 450);
|
||
}
|
||
|
||
function startTutorialCarousel() {
|
||
showTutorialSlide(0, false);
|
||
window.setInterval(() => showTutorialSlide(tutorialIndex + 1), 6500);
|
||
}
|
||
|
||
function simplifyLogLine(line) {
|
||
let text = String(line || "");
|
||
let time = "";
|
||
|
||
// BetterGI 原生日志: [21:46:36.567] [INF] Logger.Name 交互或拾取:"霓裳花"
|
||
let m = text.match(/^\[(\d{2}:\d{2}:\d{2})(?:\.\d+)?\]\s*(?:\[[A-Z]+\]\s*)?(.*)$/);
|
||
if (m) {
|
||
time = m[1];
|
||
text = m[2] || "";
|
||
} else {
|
||
// 本项目日志: 21:46:36 [INFO] [日志监控] ...
|
||
m = text.match(/^(\d{2}:\d{2}:\d{2})(?:\.\d+)?\s*(?:\[[A-Z]+\]\s*)?(.*)$/);
|
||
if (m) {
|
||
time = m[1];
|
||
text = m[2] || "";
|
||
}
|
||
}
|
||
|
||
text = text.trim();
|
||
// 去掉常见 logger 全限定名,只保留真正消息内容。
|
||
text = text.replace(/^(?:[A-Za-z_][\w$]*\.)+[A-Za-z_][\w$]*\s+/, "");
|
||
// 兼容 AutoPickTrigger 后面没有空格或格式略有变化的情况。
|
||
text = text.replace(/^BetterGenshinImpact\.[\w.]+\s*/, "");
|
||
// 前台只保留秒级时间,不显示毫秒、级别、logger 名。
|
||
return time ? ("[" + time + "] " + text) : text;
|
||
}
|
||
|
||
function simplifyLogLines(lines) {
|
||
const result = [];
|
||
const arr = Array.isArray(lines) ? lines : [];
|
||
for (let i = 0; i < arr.length; i++) {
|
||
const current = simplifyLogLine(arr[i]);
|
||
// 有些 BetterGI 日志会把“时间/级别/logger”和正文拆成两行,前台合并成一行显示。
|
||
if (/^\[\d{2}:\d{2}:\d{2}\]\s*$/.test(current) && i + 1 < arr.length) {
|
||
const next = simplifyLogLine(arr[i + 1]).trim();
|
||
if (next && !/^\[\d{2}:\d{2}:\d{2}\]/.test(next)) {
|
||
result.push({raw: String(arr[i] || "") + " " + String(arr[i + 1] || ""), text: current + next});
|
||
i++;
|
||
continue;
|
||
}
|
||
}
|
||
result.push({raw: String(arr[i] || ""), text: current});
|
||
}
|
||
return result.filter(x => x.text.trim());
|
||
}
|
||
|
||
function colorLog(line) {
|
||
const raw = typeof line === "string" ? line : String((line && line.raw) || "");
|
||
const text = typeof line === "string" ? simplifyLogLine(line) : String((line && line.text) || "");
|
||
if (raw.includes("[ERR]") || text.includes("Error") || text.includes("Exception")) return "log-err";
|
||
if (raw.includes("[WRN]") || text.includes("传送完成") || text.includes("返回主界面")) return "log-tp";
|
||
if (text.includes("开始连续执行") || text.includes("执行结束")) return "log-inf";
|
||
if (text.includes("加载完成") || text.includes("初始化")) return "log-blue";
|
||
if (text.includes("动作") || text.includes("战斗") || text.includes("攻击")) return "log-fight";
|
||
if (text.includes("自动拾取") || text.includes("交互或拾取")) return "log-pick";
|
||
if (raw.includes("[DBG]")) return "log-dbg";
|
||
return "";
|
||
}
|
||
|
||
let musicState = {playing: false, current: null, playlist: [], requests: []};
|
||
let availableGroups = [];
|
||
const fallbackSystemScheduleConfig = {
|
||
live_start_time: "09:00",
|
||
live_end_time: "23:00"
|
||
};
|
||
let systemScheduleConfig = {...fallbackSystemScheduleConfig};
|
||
let inputState = {keys: {}, mouse: {left: false, right: false, direction: "idle"}};
|
||
let performanceState = {cpu: null, memory: null, gpu: null, gpu_available: false};
|
||
let lastCoverKey = "";
|
||
|
||
// 在 2px 网格上逐格绘制,轮廓保持像素阶梯,进度仍可随数值变化。
|
||
const kiraraPixelRings = Object.fromEntries(
|
||
[["stamina", 32], ["cleanliness", 23], ["mood", 14]].map(([key, radius]) => {
|
||
const tiles = [];
|
||
for (let y = 4; y < 76; y += 2) {
|
||
for (let x = 4; x < 76; x += 2) {
|
||
const dx = x + 1 - 40;
|
||
const dy = y + 1 - 40;
|
||
if (Math.abs(Math.hypot(dx, dy) - radius) > 3.5) continue;
|
||
const angle = (Math.atan2(dy, dx) + Math.PI / 2 + Math.PI * 2) % (Math.PI * 2);
|
||
tiles.push({angle, path: `M${x} ${y}h2v2h-2z`});
|
||
}
|
||
}
|
||
document.getElementById("kirara-" + key + "-track")
|
||
.setAttribute("d", tiles.map(tile => tile.path).join(""));
|
||
return [key, tiles];
|
||
})
|
||
);
|
||
let kiraraSleeping = false;
|
||
let kiraraSleepStartedAt = 0;
|
||
const kiraraNeedMessages = {
|
||
stamina: ["饿饿,投喂一个饭团叭~", "饿饿,投喂一个薯条叭~"],
|
||
cleanliness: ["脏脏,投喂一个人气票叭~", "脏脏,投喂一个粉丝团灯牌叭~"]
|
||
};
|
||
const kiraraNeedValues = {stamina: 100, cleanliness: 100};
|
||
let kiraraNeedNextAt = null;
|
||
let kiraraNeedCurrent = null;
|
||
let kiraraNeedLastType = null;
|
||
|
||
function kiraraNeedDuration(value) {
|
||
if (value > 60) return 0;
|
||
if (value <= 0) return Infinity;
|
||
if (value >= 40) return (5 + (60 - value) / 4) * 1000;
|
||
if (value >= 20) return (10 + (40 - value)) * 1000;
|
||
return (30 + (20 - value) * 1.5) * 1000;
|
||
}
|
||
|
||
function updateKiraraNeedBubble(now) {
|
||
const bubble = document.getElementById("kirara-need-bubble");
|
||
const eligible = ["stamina", "cleanliness"]
|
||
.filter(type => kiraraNeedDuration(kiraraNeedValues[type]) > 0);
|
||
if (!eligible.length) {
|
||
kiraraNeedCurrent = null;
|
||
kiraraNeedNextAt = null;
|
||
bubble.hidden = true;
|
||
return;
|
||
}
|
||
if (kiraraNeedCurrent && !eligible.includes(kiraraNeedCurrent.type)) {
|
||
kiraraNeedCurrent = null;
|
||
kiraraNeedNextAt = now;
|
||
}
|
||
if (kiraraNeedNextAt === null) kiraraNeedNextAt = now;
|
||
if (now >= kiraraNeedNextAt) {
|
||
const empty = eligible.filter(type => kiraraNeedValues[type] === 0);
|
||
const choices = empty.length ? empty : eligible;
|
||
const type = choices.length === 1 ? choices[0]
|
||
: (kiraraNeedLastType && choices.includes(kiraraNeedLastType)
|
||
? choices.find(candidate => candidate !== kiraraNeedLastType)
|
||
: choices.reduce((a, b) => kiraraNeedValues[a] <= kiraraNeedValues[b] ? a : b));
|
||
const messages = kiraraNeedMessages[type];
|
||
kiraraNeedCurrent = {type, startedAt: now};
|
||
kiraraNeedLastType = type;
|
||
kiraraNeedNextAt = now + 60000;
|
||
bubble.textContent = messages[Math.floor(Math.random() * messages.length)];
|
||
}
|
||
const active = kiraraNeedCurrent &&
|
||
now - kiraraNeedCurrent.startedAt < kiraraNeedDuration(
|
||
kiraraNeedValues[kiraraNeedCurrent.type]);
|
||
bubble.hidden = !active;
|
||
if (!active) return;
|
||
if (kiraraPosition.x < 1306) {
|
||
bubble.dataset.place = "side";
|
||
bubble.style.left = "178px";
|
||
bubble.style.top = "19px";
|
||
} else {
|
||
const below = kiraraPosition.y < 85;
|
||
bubble.dataset.place = below ? "below" : "above";
|
||
bubble.style.left = `${Math.round(Math.min(kiraraPosition.x + 20, 1630) - kiraraPosition.x)}px`;
|
||
bubble.style.top = below ? "144px" : "-61px";
|
||
}
|
||
}
|
||
|
||
function renderKirara(pet) {
|
||
if (!pet) return;
|
||
for (const [key, tiles] of Object.entries(kiraraPixelRings)) {
|
||
const value = Math.max(0, Math.min(100, Number(pet[key]) || 0));
|
||
if (key in kiraraNeedValues) kiraraNeedValues[key] = value;
|
||
const ring = document.getElementById("kirara-" + key + "-ring");
|
||
ring.setAttribute("d", tiles
|
||
.filter(tile => tile.angle < value / 100 * Math.PI * 2)
|
||
.map(tile => tile.path).join(""));
|
||
}
|
||
const mood = Number(pet.mood);
|
||
if (!Number.isFinite(mood) || (mood < 50) === kiraraSleeping) return;
|
||
kiraraSleeping = mood < 50;
|
||
if (kiraraSleeping) {
|
||
kiraraSleepStartedAt = performance.now();
|
||
kiraraWalkEndsAt = 0;
|
||
kiraraWaypoints.length = 0;
|
||
kiraraIdle = null;
|
||
kiraraDirection = "front";
|
||
kiraraDrawnFrameKey = ""; // 强制立即重绘第一帧睡觉姿势
|
||
} else {
|
||
kiraraNextWalkAt = null;
|
||
kiraraNextIdleAt = null;
|
||
}
|
||
}
|
||
|
||
// Crops follow each generated sprite's transparent silhouette.
|
||
const kiraraSpriteBoxes = {
|
||
"kirara_walk_front": [[133,15,407,670], [617,15,392,665], [1126,15,503,672], [1629,15,388,664]],
|
||
"kirara_walk_back": [[68,19,420,691], [563,27,416,685], [1064,19,416,692], [1561,19,420,692]],
|
||
"kirara_walk_left": [[136,28,407,679], [543,28,537,679], [1151,28,446,679], [1673,28,448,679]],
|
||
"kirara_walk_right": [[33,136,397,691], [456,133,399,694], [894,137,419,690], [1330,137,408,690]],
|
||
"kirara_eat_rice": [[14,42,425,681], [510,42,424,681], [1005,42,425,681], [1502,42,424,681]],
|
||
"kirara_eat_dango": [[49,13,443,690], [592,13,443,690], [1135,13,443,690], [1677,13,444,690]],
|
||
"kirara_bath_sponge": [[62,21,437,677], [592,21,429,677], [1143,21,450,677], [1686,21,432,677]],
|
||
"kirara_bath_shower": [[62,24,405,676], [618,24,389,676], [1133,24,437,676], [1639,102,492,599]],
|
||
"kirara_idle_delivery": [[92,23,436,676], [580,21,460,678], [1118,23,444,676], [1647,23,433,676]],
|
||
"kirara_idle_reading": [[59,14,425,692], [601,14,428,692], [1144,44,427,662], [1687,13,424,693]],
|
||
"kirara_idle_phone": [[138,19,405,682], [543,19,484,681], [1118,20,511,682], [1629,18,405,683]],
|
||
"kirara_idle_sleep": [[72,81,471,595], [605,128,467,548], [1122,99,469,577], [1647,69,465,607]],
|
||
"kirara_idle_stretch": [[65,133,365,587], [527,133,430,587], [1022,154,450,566], [1553,134,365,586]],
|
||
"kirara_idle_groom": [[84,38,413,650], [612,38,409,650], [1153,38,421,650], [1696,38,416,650]],
|
||
"kirara_idle_butterfly": [[46,138,418,592], [537,138,430,592], [1061,123,381,607], [1515,111,424,619]],
|
||
"kirara_idle_tea": [[66,13,438,684], [609,13,437,684], [1153,13,436,684], [1695,13,436,684]],
|
||
"kirara_idle_yarn": [[108,26,419,663], [606,26,418,663], [1149,26,418,663], [1646,26,419,663]],
|
||
"kirara_idle_humming": [[76,71,420,671], [561,72,417,670], [1005,72,416,670], [1487,71,420,671]],
|
||
"kirara_rest_breathe": [[23,16,426,679], [571,15,420,680], [1104,20,433,675], [1664,16,424,679]],
|
||
"kirara_emotion_plus3_a": [[69,59,429,627], [622,33,411,594], [1139,10,426,583], [1676,71,428,615]],
|
||
"kirara_emotion_plus3_b": [[56,35,459,654], [573,35,504,654], [1135,32,458,657], [1653,35,466,654]],
|
||
"kirara_emotion_plus1_a": [[65,14,415,678], [611,14,412,678], [1133,14,431,678], [1695,14,413,678]],
|
||
"kirara_emotion_plus1_b": [[63,16,435,683], [608,37,447,664], [1136,28,430,673], [1679,16,432,683]],
|
||
"kirara_emotion_minus1_a": [[73,30,440,662], [606,30,433,662], [1153,30,415,662], [1664,30,438,662]],
|
||
"kirara_emotion_minus1_b": [[146,49,397,638], [543,47,501,640], [1151,44,408,643], [1637,49,418,638]],
|
||
"kirara_emotion_minus3_a": [[52,61,427,644], [534,71,495,634], [1081,61,474,644], [1555,67,476,638]],
|
||
"kirara_emotion_minus3_b": [[52,27,450,665], [598,23,466,669], [1139,15,472,679], [1676,29,444,663]],
|
||
};
|
||
const kiraraSpriteAsset = name => ({
|
||
src: `/uploads/${name}.png`, boxes: kiraraSpriteBoxes[name]
|
||
});
|
||
const kiraraWalkAssets = Object.fromEntries(
|
||
["front", "back", "left", "right"].map(direction =>
|
||
[direction, kiraraSpriteAsset(`kirara_walk_${direction}`)])
|
||
);
|
||
const kiraraActionAssets = {
|
||
feed: [kiraraSpriteAsset("kirara_eat_rice"), kiraraSpriteAsset("kirara_eat_dango")],
|
||
bath: [kiraraSpriteAsset("kirara_bath_sponge"), kiraraSpriteAsset("kirara_bath_shower")]
|
||
};
|
||
const kiraraIdleAssets = Object.fromEntries(
|
||
["delivery", "reading", "phone", "sleep", "stretch",
|
||
"groom", "butterfly", "tea", "yarn", "humming"].map(type =>
|
||
[type, kiraraSpriteAsset(`kirara_idle_${type}`)])
|
||
);
|
||
const kiraraRestAsset = kiraraSpriteAsset("kirara_rest_breathe");
|
||
const kiraraEmotionAssets = {
|
||
"3": [kiraraSpriteAsset("kirara_emotion_plus3_a"), kiraraSpriteAsset("kirara_emotion_plus3_b")],
|
||
"1": [kiraraSpriteAsset("kirara_emotion_plus1_a"), kiraraSpriteAsset("kirara_emotion_plus1_b")],
|
||
"-1": [kiraraSpriteAsset("kirara_emotion_minus1_a"), kiraraSpriteAsset("kirara_emotion_minus1_b")],
|
||
"-3": [kiraraSpriteAsset("kirara_emotion_minus3_a"), kiraraSpriteAsset("kirara_emotion_minus3_b")],
|
||
};
|
||
let kiraraDirection = "front";
|
||
let kiraraDrawnFrameKey = "";
|
||
const kiraraActionQueue = [];
|
||
const kiraraActionVariant = {feed: 0, bath: 0};
|
||
let kiraraAction = null;
|
||
const kiraraIdleTypes = ["delivery", "reading", "phone", "stretch", "groom",
|
||
"butterfly", "tea", "yarn", "humming"];
|
||
let kiraraIdle = null;
|
||
let kiraraNextIdleAt = null;
|
||
let kiraraRestActive = false;
|
||
let kiraraRestStartedAt = 0;
|
||
const kiraraEmotionQueue = [];
|
||
const kiraraEmotionVariant = {"3": 0, "1": 0, "-1": 0, "-3": 0};
|
||
const kiraraEmotionSeen = new Set();
|
||
const kiraraEmotionSeenOrder = [];
|
||
const kiraraEmotionOpenedAtClientTs = Date.now() / 1000;
|
||
let kiraraEmotionBaseline = null;
|
||
let kiraraEmotionAction = null;
|
||
|
||
function drawKiraraSpriteFrame(asset, frame, key, action) {
|
||
if (!asset.image?.complete || !asset.image.naturalWidth || key === kiraraDrawnFrameKey) return;
|
||
const box = asset.boxes[frame];
|
||
if (!box) return;
|
||
// 源矩形越界夹紧:部分浏览器对越界 drawImage 会整帧不绘制,
|
||
// 配合 clearRect 会表现为"桌宠消失"。
|
||
let [sx, sy, sw, sh] = box;
|
||
const maxX = asset.image.naturalWidth, maxY = asset.image.naturalHeight;
|
||
if (sx < 0) { sw += sx; sx = 0; }
|
||
if (sy < 0) { sh += sy; sy = 0; }
|
||
if (sx + sw > maxX) sw = maxX - sx;
|
||
if (sy + sh > maxY) sh = maxY - sy;
|
||
if (sw <= 0 || sh <= 0) return;
|
||
try {
|
||
const canvas = document.getElementById("kirara-sprite");
|
||
const ctx = canvas.getContext("2d");
|
||
ctx.clearRect(0, 0, 126, 140);
|
||
ctx.imageSmoothingEnabled = false;
|
||
const scale = Math.min(
|
||
130 / Math.max(...asset.boxes.map(box => box[3])),
|
||
120 / Math.max(...asset.boxes.map(box => box[2]))
|
||
);
|
||
const width = sw * scale;
|
||
const height = sh * scale;
|
||
ctx.drawImage(asset.image, sx, sy, sw, sh,
|
||
(126 - width) / 2, 136 - height, width, height);
|
||
canvas.dataset.action = action;
|
||
canvas.dataset.direction = action ? "front" : kiraraDirection;
|
||
canvas.dataset.frame = String(frame);
|
||
kiraraDrawnFrameKey = key;
|
||
} catch (e) {
|
||
console.error("kirara draw", e);
|
||
window.__kiraraError = String(e && e.stack || e);
|
||
}
|
||
}
|
||
|
||
function drawKiraraWalkFrame(direction, frame) {
|
||
drawKiraraSpriteFrame(kiraraWalkAssets[direction], frame,
|
||
"walk:" + direction + ":" + frame, "");
|
||
}
|
||
|
||
function drawKiraraActionFrame(action, frame) {
|
||
drawKiraraSpriteFrame(kiraraActionAssets[action.type][action.variant], frame,
|
||
"action:" + action.type + ":" + action.variant + ":" + frame, action.type);
|
||
}
|
||
|
||
function drawKiraraIdleFrame(idle, phase) {
|
||
drawKiraraSpriteFrame(kiraraIdleAssets[idle.type], phase,
|
||
"idle:" + idle.type + ":" + phase, idle.type);
|
||
}
|
||
|
||
function drawKiraraSleepFrame(phase) {
|
||
drawKiraraSpriteFrame(kiraraIdleAssets.sleep, phase,
|
||
"sleep:" + phase, "sleep");
|
||
}
|
||
|
||
function drawKiraraRestFrame(phase) {
|
||
drawKiraraSpriteFrame(kiraraRestAsset, phase,
|
||
"rest:" + phase, "rest");
|
||
}
|
||
|
||
function drawKiraraEmotionFrame(action, frame) {
|
||
drawKiraraSpriteFrame(kiraraEmotionAssets[String(action.delta)][action.variant], frame,
|
||
"emotion:" + action.delta + ":" + action.variant + ":" + frame,
|
||
"emotion");
|
||
}
|
||
|
||
for (const [direction, asset] of Object.entries(kiraraWalkAssets)) {
|
||
asset.image = new Image();
|
||
asset.image.onload = () => {
|
||
if (!kiraraSleeping && !kiraraIdle && !kiraraAction && !kiraraEmotionAction && kiraraDirection === direction) {
|
||
kiraraDrawnFrameKey = "";
|
||
drawKiraraWalkFrame(direction, 1);
|
||
}
|
||
};
|
||
asset.image.src = asset.src;
|
||
}
|
||
for (const [type, variants] of Object.entries(kiraraActionAssets)) {
|
||
for (const [variant, asset] of variants.entries()) {
|
||
asset.image = new Image();
|
||
asset.image.onload = () => {
|
||
if (!kiraraSleeping && kiraraAction?.type === type && kiraraAction.variant === variant) {
|
||
kiraraDrawnFrameKey = "";
|
||
drawKiraraActionFrame(kiraraAction, 0);
|
||
}
|
||
};
|
||
asset.image.src = asset.src;
|
||
}
|
||
}
|
||
for (const [type, asset] of Object.entries(kiraraIdleAssets)) {
|
||
asset.image = new Image();
|
||
asset.image.onload = () => {
|
||
if (!kiraraAction && !kiraraEmotionAction && ((type === "sleep" && kiraraSleeping) ||
|
||
(!kiraraSleeping && kiraraIdle?.type === type))) {
|
||
kiraraDrawnFrameKey = "";
|
||
if (type === "sleep") drawKiraraSleepFrame(0);
|
||
else drawKiraraIdleFrame(kiraraIdle, 0);
|
||
}
|
||
};
|
||
asset.image.src = asset.src;
|
||
}
|
||
kiraraRestAsset.image = new Image();
|
||
kiraraRestAsset.image.onload = () => {
|
||
if (kiraraRestActive && !kiraraSleeping && !kiraraIdle &&
|
||
!kiraraAction && !kiraraEmotionAction) {
|
||
kiraraDrawnFrameKey = "";
|
||
drawKiraraRestFrame(Math.floor((performance.now() - kiraraRestStartedAt) / 900) % 4);
|
||
}
|
||
};
|
||
kiraraRestAsset.image.src = kiraraRestAsset.src;
|
||
for (const [delta, variants] of Object.entries(kiraraEmotionAssets)) {
|
||
for (const [variant, asset] of variants.entries()) {
|
||
asset.image = new Image();
|
||
asset.image.onload = () => {
|
||
if (!kiraraAction && kiraraEmotionAction?.delta === Number(delta) &&
|
||
kiraraEmotionAction.variant === variant) {
|
||
kiraraDrawnFrameKey = "";
|
||
drawKiraraEmotionFrame(kiraraEmotionAction, 0);
|
||
}
|
||
};
|
||
asset.image.src = asset.src;
|
||
}
|
||
}
|
||
|
||
// 以完整桌宠(含状态环)的外接矩形规划路径。上、右、下三条通道
|
||
// 均与游戏镂空矩形保持至少 16px 间距,转弯经过通道交叠处。
|
||
const kiraraPosition = {x: 1450, y: 590};
|
||
const kiraraWaypoints = [];
|
||
let kiraraSpeed = 25;
|
||
let kiraraNextWalkAt = null;
|
||
let kiraraWalkStartedAt = 0;
|
||
let kiraraWalkEndsAt = 0;
|
||
let kiraraLastFrame = 0;
|
||
let kiraraStepDistance = 0;
|
||
|
||
function kiraraRandom(min, max) {
|
||
return min + Math.random() * (max - min);
|
||
}
|
||
|
||
function kiraraPoint(zone) {
|
||
if (zone === "top") return {x: kiraraRandom(16, 1682), y: kiraraRandom(16, 54)};
|
||
if (zone === "bottom") return {x: kiraraRandom(16, 1682), y: kiraraRandom(866, 924)};
|
||
return {x: kiraraRandom(1306, 1682), y: kiraraRandom(16, 924)};
|
||
}
|
||
|
||
function kiraraJunction(edge) {
|
||
return {x: kiraraRandom(1306, 1682),
|
||
y: edge === "top" ? kiraraRandom(16, 54) : kiraraRandom(866, 924)};
|
||
}
|
||
|
||
function appendKiraraOrthogonalPoint(point) {
|
||
const from = kiraraWaypoints[kiraraWaypoints.length - 1] || kiraraPosition;
|
||
if (Math.abs(point.x - from.x) > 0.01 && Math.abs(point.y - from.y) > 0.01) {
|
||
kiraraWaypoints.push(Math.random() < 0.5
|
||
? {x: point.x, y: from.y}
|
||
: {x: from.x, y: point.y});
|
||
}
|
||
kiraraWaypoints.push(point);
|
||
}
|
||
|
||
function chooseKiraraRoute() {
|
||
const kiraraZone = kiraraPosition.x >= 1306 ? "right"
|
||
: kiraraPosition.y <= 54 ? "top" : "bottom";
|
||
const zones = ["top", "right", "bottom"];
|
||
const destination = zones[Math.floor(Math.random() * zones.length)];
|
||
if (destination !== kiraraZone) {
|
||
if (kiraraZone !== "right" && destination !== "right") {
|
||
appendKiraraOrthogonalPoint(kiraraJunction(kiraraZone));
|
||
appendKiraraOrthogonalPoint(kiraraJunction(destination));
|
||
} else {
|
||
appendKiraraOrthogonalPoint(kiraraJunction(
|
||
kiraraZone === "right" ? destination : kiraraZone));
|
||
}
|
||
}
|
||
appendKiraraOrthogonalPoint(kiraraPoint(destination));
|
||
}
|
||
|
||
function advanceKirara(distanceBudget) {
|
||
// 一帧只走一条水平或垂直路段,转角处不出现斜向位移。
|
||
for (let i = 0; distanceBudget > 0.01 && i < 64; i++) {
|
||
if (kiraraWaypoints.length === 0) chooseKiraraRoute();
|
||
const next = kiraraWaypoints[0];
|
||
const dx = next.x - kiraraPosition.x;
|
||
const dy = next.y - kiraraPosition.y;
|
||
const distance = Math.abs(dx) + Math.abs(dy);
|
||
if (distance <= 0.01) {
|
||
kiraraWaypoints.shift();
|
||
continue;
|
||
}
|
||
kiraraDirection = Math.abs(dx) > 0.01
|
||
? (dx > 0 ? "right" : "left")
|
||
: (dy > 0 ? "front" : "back");
|
||
if (distance <= distanceBudget) {
|
||
kiraraPosition.x = next.x;
|
||
kiraraPosition.y = next.y;
|
||
kiraraStepDistance += distance;
|
||
kiraraWaypoints.shift();
|
||
} else {
|
||
if (Math.abs(dx) > 0.01) kiraraPosition.x += Math.sign(dx) * distanceBudget;
|
||
else kiraraPosition.y += Math.sign(dy) * distanceBudget;
|
||
kiraraStepDistance += distanceBudget;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
function queueKiraraGiftAnimations(gift) {
|
||
// 动画按礼物名义恢复量计算;数值满 100 时照样播放。
|
||
for (const [type, field] of [["feed", "pet_animation_stamina"], ["bath", "pet_animation_cleanliness"]]) {
|
||
const raw = Number(gift[field]);
|
||
const points = Number.isFinite(raw) ? Math.max(0, Math.floor(raw)) : 0;
|
||
if (points > 0) kiraraActionQueue.push({type, remaining: points});
|
||
}
|
||
}
|
||
|
||
function syncKiraraEmotionEvents(events, serverTs) {
|
||
if (!Array.isArray(events)) return;
|
||
if (kiraraEmotionBaseline === null) {
|
||
const serverNow = Number(serverTs);
|
||
const clientNow = Date.now() / 1000;
|
||
kiraraEmotionBaseline = Number.isFinite(serverNow) && serverNow > 0
|
||
? serverNow - (clientNow - kiraraEmotionOpenedAtClientTs)
|
||
: kiraraEmotionOpenedAtClientTs;
|
||
}
|
||
for (const event of events) {
|
||
const id = String(event.id || "");
|
||
const delta = Number(event.delta);
|
||
if (!id || !kiraraEmotionAssets[String(delta)] ||
|
||
Number(event.ts) <= kiraraEmotionBaseline || kiraraEmotionSeen.has(id)) continue;
|
||
kiraraEmotionSeen.add(id);
|
||
kiraraEmotionSeenOrder.push(id);
|
||
if (kiraraEmotionSeenOrder.length > 128) {
|
||
kiraraEmotionSeen.delete(kiraraEmotionSeenOrder.shift());
|
||
}
|
||
kiraraEmotionQueue.push({delta});
|
||
}
|
||
}
|
||
|
||
function updateKiraraEmotionAction(now) {
|
||
if (!kiraraEmotionAction && kiraraEmotionQueue.length) {
|
||
const job = kiraraEmotionQueue.shift();
|
||
kiraraWalkEndsAt = 0;
|
||
kiraraWaypoints.length = 0;
|
||
kiraraIdle = null;
|
||
kiraraDirection = "front";
|
||
kiraraEmotionAction = {
|
||
delta: job.delta,
|
||
startedAt: now,
|
||
endsAt: now + 3000
|
||
};
|
||
const key = String(kiraraEmotionAction.delta);
|
||
kiraraEmotionAction.variant = job.variant ?? kiraraEmotionVariant[key]++ % 2;
|
||
}
|
||
if (kiraraEmotionAction && now >= kiraraEmotionAction.endsAt) {
|
||
kiraraEmotionAction = null;
|
||
kiraraNextWalkAt = now + kiraraRandom(60000, 90000);
|
||
kiraraNextIdleAt = now + kiraraRandom(120000, 150000);
|
||
return updateKiraraEmotionAction(now);
|
||
}
|
||
return kiraraEmotionAction;
|
||
}
|
||
|
||
function nextKiraraAction(startedAt) {
|
||
const job = kiraraActionQueue[0];
|
||
if (!job) return null;
|
||
job.remaining--;
|
||
if (job.remaining === 0) kiraraActionQueue.shift();
|
||
const variant = kiraraActionVariant[job.type]++ % 2;
|
||
return {type: job.type, variant, startedAt, endsAt: startedAt + 5000};
|
||
}
|
||
|
||
function updateKiraraAction(now) {
|
||
if (!kiraraAction && kiraraActionQueue.length) {
|
||
if (kiraraEmotionAction) {
|
||
kiraraEmotionQueue.unshift({
|
||
delta: kiraraEmotionAction.delta,
|
||
variant: kiraraEmotionAction.variant
|
||
});
|
||
kiraraEmotionAction = null;
|
||
}
|
||
kiraraWalkEndsAt = 0;
|
||
kiraraWaypoints.length = 0;
|
||
kiraraIdle = null;
|
||
kiraraNextIdleAt = now + kiraraRandom(120000, 150000);
|
||
kiraraDirection = "front";
|
||
kiraraAction = nextKiraraAction(now);
|
||
}
|
||
while (kiraraAction && now >= kiraraAction.endsAt) {
|
||
if (kiraraActionQueue.length) {
|
||
kiraraAction = nextKiraraAction(kiraraAction.endsAt);
|
||
} else {
|
||
kiraraAction = null;
|
||
kiraraNextWalkAt = now + kiraraRandom(60000, 90000);
|
||
}
|
||
}
|
||
return kiraraAction;
|
||
}
|
||
|
||
function animateKirara(now) {
|
||
// 任何一帧异常都不允许杀死渲染循环(否则桌宠永久消失)。
|
||
try {
|
||
updateKiraraNeedBubble(now);
|
||
const action = updateKiraraAction(now);
|
||
if (action) {
|
||
kiraraRestActive = false;
|
||
kiraraLastFrame = now;
|
||
document.getElementById("kirara-wanderer").style.transform =
|
||
`translate3d(${Math.round(kiraraPosition.x)}px, ${Math.round(kiraraPosition.y)}px, 0)`;
|
||
const bob = -Math.abs(Math.sin((now - action.startedAt) / 500 * Math.PI)) * 2;
|
||
document.getElementById("kirara-sprite").style.transform =
|
||
`translateY(${bob.toFixed(1)}px)`;
|
||
drawKiraraActionFrame(action, Math.floor((now - action.startedAt) / 260) % 4);
|
||
requestAnimationFrame(animateKirara);
|
||
return;
|
||
}
|
||
const emotionAction = updateKiraraEmotionAction(now);
|
||
if (emotionAction) {
|
||
kiraraRestActive = false;
|
||
kiraraLastFrame = now;
|
||
document.getElementById("kirara-wanderer").style.transform =
|
||
`translate3d(${Math.round(kiraraPosition.x)}px, ${Math.round(kiraraPosition.y)}px, 0)`;
|
||
document.getElementById("kirara-sprite").style.transform = "none";
|
||
drawKiraraEmotionFrame(emotionAction,
|
||
Math.min(3, Math.floor((now - emotionAction.startedAt) / 650)));
|
||
requestAnimationFrame(animateKirara);
|
||
return;
|
||
}
|
||
if (kiraraSleeping) {
|
||
kiraraRestActive = false;
|
||
kiraraLastFrame = now;
|
||
document.getElementById("kirara-wanderer").style.transform =
|
||
`translate3d(${Math.round(kiraraPosition.x)}px, ${Math.round(kiraraPosition.y)}px, 0)`;
|
||
document.getElementById("kirara-sprite").style.transform = "none";
|
||
drawKiraraSleepFrame(Math.floor((now - kiraraSleepStartedAt) / 650) % 4);
|
||
requestAnimationFrame(animateKirara);
|
||
return;
|
||
}
|
||
if (kiraraNextIdleAt === null) kiraraNextIdleAt = now + kiraraRandom(120000, 150000);
|
||
if (kiraraIdle && now >= kiraraIdle.endsAt) {
|
||
kiraraIdle = null;
|
||
kiraraDirection = "front";
|
||
}
|
||
if (!kiraraIdle && now >= kiraraNextIdleAt) {
|
||
kiraraIdle = {
|
||
type: kiraraIdleTypes[Math.floor(Math.random() * kiraraIdleTypes.length)],
|
||
startedAt: now,
|
||
endsAt: now + kiraraRandom(15000, 20000)
|
||
};
|
||
kiraraNextIdleAt = now + kiraraRandom(120000, 150000);
|
||
kiraraWalkEndsAt = 0;
|
||
kiraraWaypoints.length = 0;
|
||
kiraraDirection = "front";
|
||
}
|
||
if (kiraraIdle) {
|
||
kiraraRestActive = false;
|
||
kiraraLastFrame = now;
|
||
document.getElementById("kirara-wanderer").style.transform =
|
||
`translate3d(${Math.round(kiraraPosition.x)}px, ${Math.round(kiraraPosition.y)}px, 0)`;
|
||
document.getElementById("kirara-sprite").style.transform = "none";
|
||
drawKiraraIdleFrame(kiraraIdle,
|
||
Math.floor((now - kiraraIdle.startedAt) / 360) % 4);
|
||
requestAnimationFrame(animateKirara);
|
||
return;
|
||
}
|
||
if (kiraraNextWalkAt === null) kiraraNextWalkAt = now + kiraraRandom(60000, 90000);
|
||
if (now >= kiraraNextWalkAt) {
|
||
kiraraWalkStartedAt = now;
|
||
kiraraWalkEndsAt = now + kiraraRandom(10000, 15000);
|
||
kiraraNextWalkAt = now + kiraraRandom(60000, 90000);
|
||
kiraraSpeed = kiraraRandom(21, 35); // 原先 42–70 px/s 的一半
|
||
}
|
||
if (kiraraWalkEndsAt) {
|
||
const activeUntil = Math.min(now, kiraraWalkEndsAt);
|
||
const elapsed = Math.max(0, activeUntil - Math.max(kiraraLastFrame, kiraraWalkStartedAt)) / 1000;
|
||
advanceKirara(kiraraSpeed * elapsed);
|
||
}
|
||
const moving = now < kiraraWalkEndsAt;
|
||
if (!moving && kiraraWalkEndsAt) {
|
||
kiraraWalkEndsAt = 0;
|
||
kiraraWaypoints.length = 0;
|
||
}
|
||
if (!moving) kiraraDirection = "front";
|
||
if (moving) kiraraRestActive = false;
|
||
else if (!kiraraRestActive) {
|
||
kiraraRestActive = true;
|
||
kiraraRestStartedAt = now;
|
||
}
|
||
kiraraLastFrame = now;
|
||
document.getElementById("kirara-wanderer").style.transform =
|
||
`translate3d(${Math.round(kiraraPosition.x)}px, ${Math.round(kiraraPosition.y)}px, 0)`;
|
||
const bob = moving ? -Math.abs(Math.sin(kiraraStepDistance / 10 * Math.PI)) * 3 : 0;
|
||
document.getElementById("kirara-sprite").style.transform =
|
||
`translateY(${bob.toFixed(1)}px)`;
|
||
if (moving) {
|
||
drawKiraraWalkFrame(kiraraDirection, Math.floor(kiraraStepDistance / 5) % 4);
|
||
} else if (kiraraRestAsset.image.complete && kiraraRestAsset.image.naturalWidth) {
|
||
drawKiraraRestFrame(Math.floor((now - kiraraRestStartedAt) / 900) % 4);
|
||
} else {
|
||
drawKiraraWalkFrame("front", 1);
|
||
}
|
||
} catch (e) {
|
||
console.error("kirara anim", e);
|
||
window.__kiraraError = String(e && e.stack || e);
|
||
}
|
||
requestAnimationFrame(animateKirara);
|
||
}
|
||
|
||
async function fetchState() {
|
||
try {
|
||
const r = await fetch("/api/state");
|
||
const d = await r.json();
|
||
renderTask(d.current_group, d.group_start_time, d.current_admin, d.users, d.default_running, d.bgi_running, d.current_group_uses_nahida_collect);
|
||
renderQueue(d.queue, d.users, d.current_admin);
|
||
initGiftFxBaseline(d.server_ts);
|
||
giftFx.sync(d.recent_gifts);
|
||
renderKirara(d.pet);
|
||
syncKiraraEmotionEvents(d.pet_emotion_events, d.server_ts);
|
||
} catch (e) {
|
||
console.error("state", e);
|
||
}
|
||
}
|
||
|
||
function shortName(name, maxChars = 8) {
|
||
const arr = Array.from(String(name || ""));
|
||
return arr.length > maxChars ? arr.slice(0, maxChars).join("") + "…" : arr.join("");
|
||
}
|
||
|
||
function renderTask(group, startTime, adminUid, users, isDefault, bgiRunning, usesNahidaCollect) {
|
||
const nameEl = document.getElementById("task-name");
|
||
const nahidaIcon = document.getElementById("task-nahida-icon");
|
||
const pointsEl = document.getElementById("task-points");
|
||
const opEl = document.getElementById("task-operator");
|
||
const fill = document.getElementById("progress-fill");
|
||
const dot = document.getElementById("bgi-dot");
|
||
const status = document.getElementById("bgi-status");
|
||
|
||
if (bgiRunning) {
|
||
dot.classList.add("on");
|
||
status.textContent = "运行中";
|
||
status.style.color = "#9df0bb";
|
||
} else {
|
||
dot.classList.remove("on");
|
||
status.textContent = "未运行";
|
||
status.style.color = "var(--text-dim)";
|
||
}
|
||
|
||
if (!group) {
|
||
nahidaIcon.hidden = true;
|
||
nameEl.textContent = "无任务运行";
|
||
nameEl.style.color = "var(--text-dim)";
|
||
opEl.textContent = "操作人:--";
|
||
pointsEl.textContent = "剩余积分:--";
|
||
fill.style.width = "0%";
|
||
return;
|
||
}
|
||
nahidaIcon.hidden = !usesNahidaCollect;
|
||
nameEl.textContent = group + (isDefault ? " (默认)" : "");
|
||
nameEl.style.color = "var(--cream)";
|
||
if (startTime) {
|
||
const sec = Math.floor((Date.now() - new Date(startTime).getTime()) / 1000);
|
||
fill.style.width = Math.min(sec / 600 * 100, 100) + "%";
|
||
} else fill.style.width = "0%";
|
||
if (adminUid && users && users[adminUid]) {
|
||
const u = users[adminUid];
|
||
opEl.textContent = "操作人:" + shortName(u.uname, 8);
|
||
pointsEl.textContent = "剩余积分:" + (Number.isFinite(Number(u.points)) ? Number(u.points) : 0) + "🌟";
|
||
} else if (isDefault) {
|
||
opEl.textContent = "操作人:系统默认";
|
||
pointsEl.textContent = "剩余积分:--";
|
||
} else {
|
||
opEl.textContent = "操作人:--";
|
||
pointsEl.textContent = "剩余积分:--";
|
||
}
|
||
}
|
||
|
||
function renderQueue(queue, users, adminUid) {
|
||
const el = document.getElementById("queue-list");
|
||
const cnt = document.getElementById("queue-count");
|
||
const total = (queue && queue.length) || 0;
|
||
cnt.textContent = total;
|
||
if (!queue || total === 0) {
|
||
el.innerHTML = '<div class="empty">队列为空</div>';
|
||
return;
|
||
}
|
||
let html = queue.slice(0, 6).map((uid, i) => {
|
||
const u = (users && users[uid]) || {uname: "用户" + uid, points: 0};
|
||
const isAdmin = uid === adminUid;
|
||
return '<div class="queue-item' + (isAdmin ? ' admin' : '') + '">' +
|
||
'<span class="name"><span class="idx">#' + (i + 1) + '</span>' + esc(shortName(u.uname, 8)) + '</span>' +
|
||
'<span class="pts">' + (Number.isFinite(Number(u.points)) ? Number(u.points) : 0) + '🌟</span></div>';
|
||
}).join("");
|
||
if (total > 6) html += '<div class="queue-item queue-more">+' + (total - 6) + '</div>';
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
async function fetchLog() {
|
||
try {
|
||
const r = await fetch("/api/bgi-log?lines=80");
|
||
const d = await r.json();
|
||
const el = document.getElementById("log-box");
|
||
el.innerHTML = simplifyLogLines(d.lines).map(item => {
|
||
const cls = colorLog(item);
|
||
return '<div class="log-line' + (cls ? ' ' + cls : '') + '">' + esc(item.text) + '</div>';
|
||
}).join("");
|
||
el.scrollTop = el.scrollHeight;
|
||
} catch (e) {
|
||
console.error("log", e);
|
||
}
|
||
}
|
||
|
||
async function fetchDanmu() {
|
||
try {
|
||
const r = await fetch("/api/danmu?limit=60");
|
||
const d = await r.json();
|
||
const el = document.getElementById("danmu-box");
|
||
const msgs = d.messages || [];
|
||
if (msgs.length === 0) {
|
||
el.innerHTML = '<div class="empty">等待弹幕...</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = msgs.map(m => {
|
||
return '<div class="danmu-item"><span class="danmu-name">' + esc(m.uname || "?") + ':</span>' +
|
||
'<span class="danmu-text">' + esc(m.text || "") + '</span></div>';
|
||
}).join("");
|
||
el.scrollTop = el.scrollHeight;
|
||
} catch (e) {
|
||
console.error("danmu", e);
|
||
}
|
||
}
|
||
|
||
async function fetchMusic() {
|
||
try {
|
||
const r = await fetch("/api/music");
|
||
const d = await r.json();
|
||
musicState = d || {playing: false, current: null, playlist: [], requests: []};
|
||
renderMusic();
|
||
} catch (e) {
|
||
console.error("music", e);
|
||
}
|
||
}
|
||
|
||
function groupCardHtml(group) {
|
||
const usesNahida = !!group.uses_nahida_collect;
|
||
const nahidaTitle = usesNahida
|
||
? '使用纳西妲采集' + (group.nahida_route_count ? '(' + group.nahida_route_count + '条路线)' : '')
|
||
: '';
|
||
return '<div class="group-card' + (usesNahida ? ' uses-nahida' : '') + '"' +
|
||
(nahidaTitle ? ' title="' + esc(nahidaTitle) + '"' : '') + '>' +
|
||
'<img class="group-card-image" src="' + esc(group.image || "") + '" alt="' + esc(group.name || "") + '">' +
|
||
(usesNahida ? '<img class="group-card-nahida" src="/uploads/nahida_collect.webp" alt="纳西妲采集">' : '') +
|
||
'<div class="group-card-name" title="' + esc(group.name || "") + '">' + esc(group.name || "未命名") + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function startGroupCarousel() {
|
||
const track = document.getElementById("group-carousel-track");
|
||
const viewport = document.getElementById("group-carousel-viewport");
|
||
const empty = document.getElementById("group-carousel-empty");
|
||
if (!track || !viewport || !empty) return;
|
||
track.classList.remove("smooth-scroll");
|
||
track.style.removeProperty("--group-scroll-distance");
|
||
track.style.removeProperty("--group-scroll-duration");
|
||
if (!availableGroups.length) {
|
||
track.innerHTML = "";
|
||
viewport.hidden = true;
|
||
empty.hidden = false;
|
||
return;
|
||
}
|
||
|
||
viewport.hidden = false;
|
||
empty.hidden = true;
|
||
const n = availableGroups.length;
|
||
if (n <= 2) {
|
||
track.innerHTML = availableGroups.map(groupCardHtml).join("");
|
||
return;
|
||
}
|
||
|
||
const rowCount = Math.ceil(n / 2);
|
||
// 复制一份用于无缝循环:第二份首卡与第一份首卡之间的真实距离即一个完整循环周期。
|
||
track.innerHTML = availableGroups.concat(availableGroups).map(groupCardHtml).join("");
|
||
|
||
// 用真实渲染距离计算循环周期,避免硬编码卡片高度/间距导致“播完跳回开头”。
|
||
const cards = track.querySelectorAll(".group-card");
|
||
let distance = 0;
|
||
if (cards.length > n) {
|
||
distance = cards[n].getBoundingClientRect().top - cards[0].getBoundingClientRect().top;
|
||
}
|
||
if (distance <= 0) {
|
||
distance = rowCount * 89;
|
||
}
|
||
const duration = Math.max(45, rowCount * 2.1);
|
||
track.style.setProperty("--group-scroll-distance", "-" + distance.toFixed(2) + "px");
|
||
track.style.setProperty("--group-scroll-duration", duration.toFixed(1) + "s");
|
||
requestAnimationFrame(() => track.classList.add("smooth-scroll"));
|
||
}
|
||
|
||
async function fetchAvailableGroups() {
|
||
try {
|
||
const r = await fetch("/api/groups?t=" + Date.now());
|
||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||
const data = await r.json();
|
||
const groups = Array.isArray(data) ? data : data.groups;
|
||
availableGroups = Array.isArray(groups) ? groups.filter(x => x && x.name && x.image) : [];
|
||
startGroupCarousel();
|
||
} catch (e) {
|
||
console.error("available-groups", e);
|
||
availableGroups = [];
|
||
startGroupCarousel();
|
||
}
|
||
}
|
||
|
||
function getNextStopPushTime(now, hhmm) {
|
||
const match = /^(\d{1,2}):(\d{2})$/.exec(String(hhmm || "").trim());
|
||
if (!match) return null;
|
||
const hour = Number(match[1]);
|
||
const minute = Number(match[2]);
|
||
if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||
return null;
|
||
}
|
||
const target = new Date(now.getTime());
|
||
target.setHours(hour, minute, 0, 0);
|
||
if (target.getTime() <= now.getTime()) target.setDate(target.getDate() + 1);
|
||
return target;
|
||
}
|
||
|
||
function renderLiveEndCountdown(now = new Date()) {
|
||
const valueEl = document.getElementById("live-end-countdown");
|
||
const targetEl = document.getElementById("live-end-target");
|
||
const dayEl = document.getElementById("live-end-day");
|
||
if (!valueEl || !targetEl || !dayEl) return;
|
||
|
||
const configuredTime = String(
|
||
systemScheduleConfig.live_end_time || systemScheduleConfig.bilibili_stop_push_time || ""
|
||
).trim();
|
||
const startTime = String(
|
||
systemScheduleConfig.live_start_time || systemScheduleConfig.bilibili_push_time || ""
|
||
).trim();
|
||
const toMinutes = value => {
|
||
const match = /^(\d{1,2}):(\d{2})$/.exec(value);
|
||
if (!match) return null;
|
||
const hour = Number(match[1]), minute = Number(match[2]);
|
||
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 ? hour * 60 + minute : null;
|
||
};
|
||
const startMinutes = toMinutes(startTime);
|
||
const endMinutes = toMinutes(configuredTime);
|
||
const nowMinutes = now.getHours() * 60 + now.getMinutes();
|
||
const liveNow = startMinutes !== null && endMinutes !== null && startMinutes !== endMinutes && (
|
||
startMinutes < endMinutes
|
||
? nowMinutes >= startMinutes && nowMinutes < endMinutes
|
||
: nowMinutes >= startMinutes || nowMinutes < endMinutes
|
||
);
|
||
if (!liveNow) {
|
||
valueEl.textContent = "--:--:--";
|
||
valueEl.classList.add("inactive");
|
||
targetEl.textContent = "当前未开播";
|
||
dayEl.textContent = startTime && configuredTime ? (startTime + " - " + configuredTime) : "请设置直播时间";
|
||
return;
|
||
}
|
||
|
||
const target = getNextStopPushTime(now, configuredTime);
|
||
if (!target) {
|
||
valueEl.textContent = "--:--:--";
|
||
valueEl.classList.add("inactive");
|
||
targetEl.textContent = "关播时间配置无效";
|
||
dayEl.textContent = configuredTime || "未设置时间";
|
||
return;
|
||
}
|
||
|
||
const remainingSec = Math.max(0, Math.ceil((target.getTime() - now.getTime()) / 1000));
|
||
const hours = Math.floor(remainingSec / 3600);
|
||
const minutes = Math.floor((remainingSec % 3600) / 60);
|
||
const seconds = remainingSec % 60;
|
||
valueEl.textContent = String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
|
||
valueEl.classList.remove("inactive");
|
||
targetEl.textContent = "计划关播 " + String(target.getHours()).padStart(2, "0") + ":" + String(target.getMinutes()).padStart(2, "0");
|
||
dayEl.textContent = target.toDateString() === now.toDateString() ? "今天" : "次日";
|
||
}
|
||
|
||
async function fetchSystemScheduleConfig() {
|
||
try {
|
||
const r = await fetch("/api/system-schedule-config");
|
||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||
systemScheduleConfig = await r.json() || {};
|
||
try {
|
||
localStorage.setItem("liveEndScheduleConfig", JSON.stringify(systemScheduleConfig));
|
||
} catch (_) {
|
||
}
|
||
renderLiveEndCountdown();
|
||
} catch (e) {
|
||
console.error("system-schedule-config", e);
|
||
// 直播姬浏览器源偶发无法请求API时,优先使用最近一次成功配置;首次加载则使用页面构建时的兜底值。
|
||
let cached = null;
|
||
try {
|
||
cached = JSON.parse(localStorage.getItem("liveEndScheduleConfig") || "null");
|
||
} catch (_) {
|
||
}
|
||
systemScheduleConfig = cached && typeof cached === "object"
|
||
? cached
|
||
: {...fallbackSystemScheduleConfig};
|
||
renderLiveEndCountdown();
|
||
const dayEl = document.getElementById("live-end-day");
|
||
if (dayEl) dayEl.textContent += " · 本地配置";
|
||
}
|
||
}
|
||
|
||
async function fetchInputState() {
|
||
try {
|
||
const r = await fetch("/api/input_state");
|
||
inputState = await r.json();
|
||
renderInputState();
|
||
} catch (e) {
|
||
console.error("input", e);
|
||
}
|
||
}
|
||
|
||
async function fetchPerformance() {
|
||
try {
|
||
const r = await fetch("/api/performance");
|
||
performanceState = await r.json();
|
||
renderPerformance();
|
||
} catch (e) {
|
||
console.error("performance", e);
|
||
}
|
||
}
|
||
|
||
async function fetchFrontendConfig() {
|
||
try {
|
||
const r = await fetch("/api/frontend-config");
|
||
const cfg = await r.json();
|
||
const root = document.documentElement;
|
||
if (cfg.background_image) root.style.setProperty("--custom-bg-image", "url('" + cfg.background_image + "')");
|
||
else root.style.setProperty("--custom-bg-image", "none");
|
||
root.style.setProperty("--custom-bg-opacity", String(cfg.background_opacity ?? 0.65));
|
||
root.style.setProperty("--custom-bg-blur", String(cfg.background_blur ?? 0) + "px");
|
||
const fit = cfg.background_fit === "fill" ? "100% 100%" : (cfg.background_fit || "cover");
|
||
root.style.setProperty("--custom-bg-size", fit);
|
||
// 主题预设:classic 为默认(无额外类),其它主题通过 body.theme-<id> 生效
|
||
const themeClasses = Array.from(document.body.classList).filter((c) => c.startsWith("theme-"));
|
||
themeClasses.forEach((c) => document.body.classList.remove(c));
|
||
const theme = cfg.theme || "classic";
|
||
if (theme && theme !== "classic") document.body.classList.add("theme-" + theme);
|
||
applyCommandHints(cfg.command_hints);
|
||
} catch (e) {
|
||
console.error("frontend-config", e);
|
||
applyCommandHints(fallbackCommandHints);
|
||
}
|
||
}
|
||
|
||
function setActive(id, on) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.classList.toggle("active", !!on);
|
||
}
|
||
|
||
function renderPerformance() {
|
||
const setPerf = (name, value) => {
|
||
const textEl = document.getElementById("perf-" + name);
|
||
const ringEl = document.getElementById("perf-" + name + "-ring");
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n)) {
|
||
if (textEl) textEl.textContent = "--%";
|
||
if (ringEl) ringEl.style.setProperty("--pct", 0);
|
||
return;
|
||
}
|
||
const pct = Math.max(0, Math.min(100, n));
|
||
if (textEl) textEl.textContent = pct.toFixed(pct >= 10 ? 0 : 1) + "%";
|
||
if (ringEl) ringEl.style.setProperty("--pct", pct);
|
||
};
|
||
setPerf("cpu", performanceState.cpu);
|
||
setPerf("mem", performanceState.memory);
|
||
setPerf("gpu", performanceState.gpu);
|
||
}
|
||
|
||
function renderInputState() {
|
||
const keys = inputState.keys || {};
|
||
["q", "w", "e", "r", "a", "s", "d", "f", "space"].forEach(k => setActive("key-" + k, keys[k]));
|
||
const mouse = inputState.mouse || {};
|
||
setActive("mouse-left", mouse.left);
|
||
setActive("mouse-right", mouse.right);
|
||
const trail = document.getElementById("mouse-trail");
|
||
if (!trail) return;
|
||
const sx = Number(mouse.smooth_dx || 0);
|
||
const sy = Number(mouse.smooth_dy || 0);
|
||
const mag = Math.max(0, Math.min(1, Number(mouse.magnitude || 0)));
|
||
if (!mag || (mouse.direction || "idle") === "idle") {
|
||
trail.classList.remove("active");
|
||
trail.style.transform = "translate(0, 0) scale(0.55)";
|
||
} else {
|
||
const len = Math.min(34, Math.max(12, mag * 36));
|
||
const norm = Math.max(1, Math.hypot(sx, sy));
|
||
const x = sx / norm * len;
|
||
const y = sy / norm * len;
|
||
trail.classList.add("active");
|
||
trail.style.transform = "translate(" + x.toFixed(1) + "px, " + y.toFixed(1) + "px) scale(" + (0.72 + mag * 0.42).toFixed(2) + ")";
|
||
}
|
||
}
|
||
|
||
function renderMusic() {
|
||
const cur = musicState.current || {};
|
||
const coverEl = document.getElementById("music-cover");
|
||
const titleEl = document.getElementById("music-title");
|
||
const artistEl = document.getElementById("music-artist");
|
||
const noteEl = document.getElementById("music-note");
|
||
const listEl = document.getElementById("playlist");
|
||
|
||
// 以 mpv 播放器真实状态为准(已移除 SMTC 系统媒体监听):
|
||
// idle 或没有曲目时为“未播放”,不再显示“音乐源离线 / SMTC”。
|
||
const isIdle = !!musicState.idle || !cur.title || cur.title === "暂无歌曲";
|
||
|
||
if (isIdle) {
|
||
titleEl.textContent = "未播放";
|
||
artistEl.textContent = "等待点歌或背景歌单";
|
||
coverEl.innerHTML = "♪";
|
||
lastCoverKey = "";
|
||
noteEl.textContent = "点歌或开启背景歌单后将自动播放";
|
||
} else if (!cur.title && !cur.artist) {
|
||
titleEl.textContent = "未配置";
|
||
artistEl.textContent = "等待数据源";
|
||
coverEl.innerHTML = "♪";
|
||
lastCoverKey = "";
|
||
noteEl.textContent = "等待音乐源";
|
||
} else {
|
||
titleEl.textContent = cur.title || "未知歌曲";
|
||
artistEl.textContent = cur.artist || "未知艺人";
|
||
const rawCover = cur.cover ? String(cur.cover) : "";
|
||
const coverKey = rawCover + "|" + (cur.cover_hash || "");
|
||
if (rawCover && coverKey !== lastCoverKey) {
|
||
// 预加载新图, 加载成功才替换 DOM, 避免闪烁
|
||
const sep = rawCover.includes("?") ? "&" : "?";
|
||
const imgSrc = rawCover + sep + "v=" + encodeURIComponent(cur.cover_hash || "stable");
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
coverEl.innerHTML = '<img src="' + esc(imgSrc) + '" alt="">';
|
||
lastCoverKey = coverKey;
|
||
};
|
||
img.onerror = () => { /* 加载失败保留旧图, 不清空 */
|
||
};
|
||
img.src = imgSrc;
|
||
} else if (!rawCover && !lastCoverKey) {
|
||
// 从未有封面才显示音符占位; 已有封面则保留旧图
|
||
coverEl.innerHTML = "♪";
|
||
}
|
||
noteEl.textContent = musicState.playing ? "正在播放" : "已暂停";
|
||
}
|
||
|
||
const playlist = musicState.requests || musicState.playlist || [];
|
||
if (playlist.length === 0) {
|
||
listEl.innerHTML = '<li class="empty">暂无点歌</li>';
|
||
} else {
|
||
listEl.innerHTML = playlist.map((song, i) => {
|
||
const title = song.name || song.title || "未知歌曲";
|
||
const durationSec = Math.max(0, Number(song.duration_sec || 0));
|
||
const duration = durationSec > 0
|
||
? Math.floor(durationSec / 60) + ":" + String(Math.floor(durationSec % 60)).padStart(2, "0")
|
||
: "--:--";
|
||
const by = song.uname ? ("点歌:" + song.uname) : (song.artist || "-");
|
||
return '<li><span class="title">#' + (i + 1) + ' ' + esc(title) + ' [' + esc(duration) + ']</span><span class="by">' + esc(by) + '</span></li>';
|
||
}).join("");
|
||
}
|
||
}
|
||
// ===== 礼物特效引擎 =====
|
||
// 档位: value(折算人民币) >=100→4, >=10→3, >0→2, 免费→1;礼物数量 num 放大粒子规模。
|
||
const giftFx = {
|
||
particles: [],
|
||
rafId: null,
|
||
lastTs: 0,
|
||
baselineInitialized: false,
|
||
openedAtClientTs: Date.now() / 1000,
|
||
bannerTimer: null,
|
||
queue: [],
|
||
playing: false,
|
||
|
||
tierOf(g) {
|
||
const v = Number(g.value || 0);
|
||
if (v >= 100) return 4;
|
||
if (v >= 10) return 3;
|
||
if (v > 0) return 2;
|
||
return 1;
|
||
},
|
||
|
||
canvasSetup() {
|
||
const cv = document.getElementById("gift-canvas");
|
||
const layer = document.getElementById("gift-layer");
|
||
if (!cv || !layer) return null;
|
||
const rect = layer.getBoundingClientRect();
|
||
// 按 1920x1080 逻辑分辨率设置,CSS 已随 #canvas 缩放
|
||
cv.width = 1920;
|
||
cv.height = 1080;
|
||
return cv.getContext("2d");
|
||
},
|
||
|
||
spawnBurst(cx, cy, count, colors, power) {
|
||
for (let i = 0; i < count; i++) {
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const speed = (2 + Math.random() * 6) * power;
|
||
this.particles.push({
|
||
x: cx, y: cy,
|
||
vx: Math.cos(angle) * speed,
|
||
vy: Math.sin(angle) * speed - 2 * power,
|
||
life: 1,
|
||
decay: 0.008 + Math.random() * 0.012,
|
||
size: 3 + Math.random() * 6 * power,
|
||
color: colors[Math.floor(Math.random() * colors.length)],
|
||
shape: Math.random() < 0.3 ? "star" : "circle",
|
||
spin: Math.random() * Math.PI * 2,
|
||
spinV: (Math.random() - 0.5) * 0.2,
|
||
});
|
||
}
|
||
this.startLoop();
|
||
},
|
||
|
||
startLoop() {
|
||
if (this.rafId) return;
|
||
const ctx = this.canvasSetup();
|
||
if (!ctx) return;
|
||
const step = () => {
|
||
ctx.clearRect(0, 0, 1920, 1080);
|
||
this.particles = this.particles.filter(p => p.life > 0);
|
||
for (const p of this.particles) {
|
||
p.x += p.vx;
|
||
p.y += p.vy;
|
||
p.vy += 0.12;
|
||
p.vx *= 0.985;
|
||
p.life -= p.decay;
|
||
p.spin += p.spinV;
|
||
ctx.save();
|
||
ctx.globalAlpha = Math.max(0, p.life);
|
||
ctx.fillStyle = p.color;
|
||
if (p.shape === "star") {
|
||
ctx.translate(p.x, p.y);
|
||
ctx.rotate(p.spin);
|
||
const s = p.size;
|
||
ctx.beginPath();
|
||
for (let k = 0; k < 10; k++) {
|
||
const r = k % 2 === 0 ? s : s * 0.45;
|
||
const a = (Math.PI / 5) * k;
|
||
ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r);
|
||
}
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
} else {
|
||
ctx.beginPath();
|
||
ctx.arc(p.x, p.y, p.size / 2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
ctx.restore();
|
||
}
|
||
if (this.particles.length === 0) {
|
||
this.rafId = null;
|
||
ctx.clearRect(0, 0, 1920, 1080);
|
||
return;
|
||
}
|
||
this.rafId = requestAnimationFrame(step);
|
||
};
|
||
this.rafId = requestAnimationFrame(step);
|
||
},
|
||
|
||
showBanner(g, tier) {
|
||
const banner = document.getElementById("gift-banner");
|
||
if (!banner) return;
|
||
banner.className = "gift-banner tier-" + tier;
|
||
banner.style.display = "flex";
|
||
banner.querySelector(".gb-name").textContent = shortName(g.uname || "热心观众", 10);
|
||
banner.querySelector(".gb-gift").textContent =
|
||
"送出 " + (g.gift_name || "礼物") + (g.num > 1 ? " ×" + g.num : "");
|
||
requestAnimationFrame(() => banner.classList.add("show"));
|
||
clearTimeout(this.bannerTimer);
|
||
const holdMs = 1800 + tier * 600;
|
||
this.bannerTimer = setTimeout(() => {
|
||
banner.classList.remove("show");
|
||
banner.classList.add("hide");
|
||
setTimeout(() => { banner.style.display = "none"; banner.classList.remove("hide"); }, 450);
|
||
}, holdMs);
|
||
},
|
||
|
||
flash(tier) {
|
||
if (tier < 3) return;
|
||
const el = document.getElementById("gift-flash");
|
||
if (!el) return;
|
||
el.classList.add("on");
|
||
setTimeout(() => el.classList.remove("on"), tier >= 4 ? 500 : 300);
|
||
},
|
||
|
||
play(g) {
|
||
const tier = this.tierOf(g);
|
||
const num = Math.min(Number(g.num || 1), 99);
|
||
// 基础粒子数按档位递增,数量 num 再放大(对数压缩避免刷屏)
|
||
const base = [0, 20, 45, 90, 160][tier];
|
||
const count = Math.round(base * (1 + Math.log10(num)));
|
||
const power = 0.7 + tier * 0.25;
|
||
const palette = {
|
||
1: ["#9fe1cb", "#eafff2", "#5dcaa5"],
|
||
2: ["#ffd66e", "#fff3c4", "#ef9f27"],
|
||
3: ["#ff82a0", "#ffe9ec", "#ed93b1", "#ffd66e"],
|
||
4: ["#dcA0ff", "#ffffff", "#afa9ec", "#ffd66e", "#ff82a0"],
|
||
}[tier];
|
||
this.showBanner(g, tier);
|
||
this.flash(tier);
|
||
// 中心主爆点
|
||
this.spawnBurst(960, 420, count, palette, power);
|
||
// 高档位追加侧翼爆点,礼物越大场面越足
|
||
if (tier >= 2) this.spawnBurst(560, 560, Math.round(count * 0.4), palette, power * 0.8);
|
||
if (tier >= 3) this.spawnBurst(1360, 560, Math.round(count * 0.4), palette, power * 0.8);
|
||
if (tier >= 4) {
|
||
setTimeout(() => this.spawnBurst(960, 300, Math.round(count * 0.6), palette, power), 350);
|
||
setTimeout(() => this.spawnBurst(700, 480, Math.round(count * 0.4), palette, power * 0.9), 700);
|
||
setTimeout(() => this.spawnBurst(1220, 480, Math.round(count * 0.4), palette, power * 0.9), 700);
|
||
}
|
||
},
|
||
|
||
enqueue(g) {
|
||
this.queue.push(g);
|
||
this.pump();
|
||
},
|
||
|
||
pump() {
|
||
if (this.playing || this.queue.length === 0) return;
|
||
this.playing = true;
|
||
const g = this.queue.shift();
|
||
this.play(g);
|
||
const tier = this.tierOf(g);
|
||
setTimeout(() => {
|
||
this.playing = false;
|
||
this.pump();
|
||
}, 1200 + tier * 400);
|
||
},
|
||
|
||
// 每轮 state 轮询调用:播放 ts 晚于上次记录的所有礼物
|
||
sync(gifts) {
|
||
if (!Array.isArray(gifts) || gifts.length === 0) return;
|
||
const fresh = gifts.filter(g => Number(g.ts || 0) > this.lastTs);
|
||
if (fresh.length === 0) return;
|
||
this.lastTs = Math.max(...gifts.map(g => Number(g.ts || 0)));
|
||
for (const g of fresh) {
|
||
this.enqueue(g);
|
||
queueKiraraGiftAnimations(g);
|
||
}
|
||
},
|
||
};
|
||
|
||
// 用页面打开时刻作为一次性基线:跳过刷新前的礼物,同时保留打开后的首件礼物。
|
||
function initGiftFxBaseline(serverTs) {
|
||
if (giftFx.baselineInitialized) return;
|
||
const serverNow = Number(serverTs);
|
||
const clientNow = Date.now() / 1000;
|
||
giftFx.lastTs = Number.isFinite(serverNow) && serverNow > 0
|
||
? serverNow - (clientNow - giftFx.openedAtClientTs)
|
||
: giftFx.openedAtClientTs;
|
||
giftFx.baselineInitialized = true;
|
||
}
|
||
|
||
|
||
function scaleCanvas() {
|
||
const canvas = document.getElementById("canvas");
|
||
const scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
||
const offsetX = Math.max(0, (window.innerWidth - 1920 * scale) / 2);
|
||
const offsetY = Math.max(0, (window.innerHeight - 1080 * scale) / 2);
|
||
canvas.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
|
||
}
|
||
|
||
applyCommandHints(fallbackCommandHints);
|
||
startTutorialCarousel();
|
||
renderKirara({stamina: 100, cleanliness: 100, mood: 100});
|
||
requestAnimationFrame(animateKirara);
|
||
fetchFrontendConfig();
|
||
fetchState();
|
||
fetchLog();
|
||
fetchDanmu();
|
||
fetchMusic();
|
||
fetchAvailableGroups();
|
||
fetchSystemScheduleConfig();
|
||
renderLiveEndCountdown();
|
||
fetchInputState();
|
||
fetchPerformance();
|
||
setInterval(fetchState, 2000);
|
||
setInterval(fetchLog, 1500);
|
||
setInterval(fetchDanmu, 1500);
|
||
setInterval(fetchMusic, 5000);
|
||
setInterval(renderLiveEndCountdown, 1000);
|
||
setInterval(fetchSystemScheduleConfig, 60000);
|
||
setInterval(fetchInputState, 33);
|
||
setInterval(fetchPerformance, 2000);
|
||
scaleCanvas();
|
||
const params = new URLSearchParams(location.search);
|
||
if (params.get('preview') === '1') document.body.classList.add('preview');
|
||
if (params.get('frame') === '0') document.body.classList.add('frame-off');
|
||
if (params.get('glass') === '1') document.body.classList.add('glass-on');
|
||
scaleCanvas();
|
||
window.addEventListener("resize", scaleCanvas);
|
||
</script>
|
||
</body>
|
||
</html>
|