Merge pull request '进入游戏动画' (#24) from why_ga into main

Reviewed-on: #24
This commit is contained in:
李鹏宇
2026-09-03 18:07:02 +08:00
3 changed files with 538 additions and 35 deletions
+9
View File
@@ -180,3 +180,12 @@ function thumbSVG(it){
`<circle cx="50" cy="50" r="46" fill="none" stroke="${tcol}" stroke-width="2.5" opacity=".9"/>`+
`</svg>`;
}
// ---------- 全局音效开关(与 world.html 设置面板共用) ----------
function loadWorldSFXSetting() {
try { return localStorage.getItem('world_sfx_enabled') !== 'false'; }
catch (e) { return true; }
}
function saveWorldSFXSetting(enabled) {
try { localStorage.setItem('world_sfx_enabled', enabled ? 'true' : 'false'); } catch (e) {}
}
+252 -11
View File
@@ -87,10 +87,100 @@
.sl-dots{right:16px;gap:10px;}
.hero .enter-btn{font-size:15px;padding:11px 30px;}
}
/* 进游戏开场动画(yusen 科技感)—— 总时长约 5.2s */
#introSplash {
position: fixed; inset: 0; z-index: 600; overflow: hidden;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background:
radial-gradient(ellipse at 50% 48%, rgba(20,44,78,.55), rgba(3,5,10,.97) 70%),
#03050a;
opacity: 0; visibility: hidden; transition: opacity .5s ease;
}
#introSplash.show { opacity: 1; visibility: visible; }
#introSplash.hide { opacity: 0; visibility: hidden; transition: opacity .45s ease; }
#introSplash .intro-grid {
position: absolute; inset: -50%;
background-image:
linear-gradient(rgba(74,175,255,.09) 1px, transparent 1px),
linear-gradient(90deg, rgba(74,175,255,.09) 1px, transparent 1px);
background-size: 44px 44px;
transform: perspective(440px) rotateX(62deg);
animation: introGridMove 3.2s linear infinite;
-webkit-mask-image: radial-gradient(ellipse at 50% 38%, #000 8%, transparent 72%);
mask-image: radial-gradient(ellipse at 50% 38%, #000 8%, transparent 72%);
}
@keyframes introGridMove { from { background-position: 0 0; } to { background-position: 0 44px; } }
#introSplash .intro-sweep {
position: absolute; top: 0; bottom: 0; width: 42%; left: -42%;
transform: skewX(-18deg); z-index: 3; pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(140,210,255,.20), transparent);
}
#introSplash.show .intro-sweep { animation: introSweep 1.8s ease-in-out .6s 1; }
@keyframes introSweep { 0% { left: -42%; } 100% { left: 110%; } }
#introSplash .intro-logo {
position: relative; z-index: 2; margin: 0;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
font-weight: 800; font-size: clamp(48px, 12vw, 132px);
letter-spacing: .22em; text-indent: .22em; text-transform: lowercase;
color: #eaf6ff; opacity: 0; transform: scale(.92); filter: blur(8px);
text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35);
}
#introSplash.show .intro-logo { animation: introLogoIn 2s cubic-bezier(.16,1,.3,1) .2s forwards, introHoldPulse 1.6s ease-in-out 3.6s 1 forwards; }
@keyframes introLogoIn {
0% { opacity: 0; transform: scale(.92); filter: blur(8px); }
30% { opacity: 1; transform: scale(1.04); filter: blur(0); text-shadow: 3px 0 rgba(255,46,86,.55), -3px 0 rgba(40,200,255,.55), 0 0 18px rgba(74,175,255,.7); }
48% { text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35); }
100% { opacity: 1; transform: scale(1); filter: blur(0); }
}
@keyframes introHoldPulse {
0% { text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35); }
50% { text-shadow: 0 0 28px rgba(74,175,255,.9), 0 0 72px rgba(74,175,255,.5), 0 0 120px rgba(138,92,246,.25); }
100% { text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35); }
}
#introSplash .intro-line {
position: relative; z-index: 2; margin-top: 18px; height: 2px; width: 0;
background: linear-gradient(90deg, transparent, #4af, #a8e6cf, transparent);
box-shadow: 0 0 12px rgba(74,175,255,.7);
}
#introSplash.show .intro-line { animation: introLine 1.4s ease-out 1.4s forwards; }
@keyframes introLine { to { width: min(320px, 60vw); } }
#introSplash .intro-sub {
position: relative; z-index: 2; margin-top: 14px; font-size: 13px;
letter-spacing: .4em; text-indent: .4em; color: rgba(150,200,255,.55); opacity: 0;
}
#introSplash.show .intro-sub { animation: introFade 1.2s ease 2.6s forwards; }
#introSplash .intro-skip {
position: absolute; bottom: 26px; z-index: 4; font-size: 12px; letter-spacing: .15em;
color: rgba(180,210,255,.4); opacity: 0;
}
#introSplash.show .intro-skip { animation: introFade 1s ease 3.6s forwards; }
#introSplash .intro-audio-unlock {
position: absolute; bottom: 58px; z-index: 5; cursor: pointer;
padding: 9px 20px; border-radius: 999px; font-size: 13px; letter-spacing: .12em;
color: #eaf6ff; background: rgba(74,175,255,.12);
border: 1px solid rgba(74,175,255,.5); box-shadow: 0 0 18px rgba(74,175,255,.35);
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
opacity: 0; transition: opacity .4s ease, transform .2s ease;
}
#introSplash.show .intro-audio-unlock { opacity: 1; }
#introSplash .intro-audio-unlock:hover { transform: scale(1.05); }
#introSplash.hide .intro-audio-unlock { opacity: 0; }
@keyframes introFade { to { opacity: 1; } }
</style>
<link rel="stylesheet" href="assets/story-player.css" />
</head>
<body>
<!-- 进游戏开场动画(yusen 科技感) -->
<div id="introSplash">
<div class="intro-grid"></div>
<div class="intro-sweep"></div>
<div class="intro-logo">yusen</div>
<div class="intro-line"></div>
<div class="intro-sub">宇森游戏</div>
<button id="introAudioUnlock" class="intro-audio-unlock" style="display:none;">🔊 点击开启音效</button>
<div class="intro-skip">点击任意位置跳过</div>
</div>
<div class="app">
<header class="topbar">
<a href="index.html" class="logo"><span class="cn">宇森</span></a>
@@ -250,21 +340,172 @@ document.getElementById('enterGame').addEventListener('click', async ()=>{
</script>
<script src="assets/story-player.js"></script>
<script>
// 首页首次访问自动播放前两幕剧情漫画
// 进游戏开场动画(yusen 科技感):首次访问播放,约 5.2s,可点击跳过,本会话只播一次
let _introAudioCtx = null;
function getIntroAudioCtx() {
try {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return null;
if (!_introAudioCtx) _introAudioCtx = new AC();
if (_introAudioCtx.state === 'suspended') _introAudioCtx.resume();
return _introAudioCtx;
} catch (e) { return null; }
}
function stopIntroSound(master) {
if (!master) return;
try {
const ctx = master.context;
const t = ctx.currentTime;
master.gain.cancelScheduledValues(t);
master.gain.setValueAtTime(Math.max(0.0001, master.gain.value), t);
master.gain.exponentialRampToValueAtTime(0.0001, t + 0.12);
} catch (e) {}
}
function playIntroSound() {
// 参考背景音乐设置:读取「音效」开关,关闭时不发声
if (typeof loadWorldSFXSetting === 'function' && !loadWorldSFXSetting()) return null;
const ctx = getIntroAudioCtx();
if (!ctx) return null;
const now = ctx.currentTime;
const master = ctx.createGain();
master.gain.setValueAtTime(0.0001, now);
master.gain.linearRampToValueAtTime(0.55, now + 0.06);
master.connect(ctx.destination);
// 1. 深空底噪:50Hz + 100Hz 正弦,贯穿全程
[50, 100].forEach(f => {
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, now);
g.gain.linearRampToValueAtTime(0.05, now + 0.8);
g.gain.setValueAtTime(0.05, now + 4.2);
g.gain.linearRampToValueAtTime(0.0001, now + 5.2);
o.connect(g).connect(master); o.start(now); o.stop(now + 5.3);
});
// 2. 低频滤波噪声扫过(0.2s - 1.5s)
const len = Math.floor(ctx.sampleRate * 1.4);
const buf = ctx.createBuffer(1, len, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 1.4);
const noise = ctx.createBufferSource(); noise.buffer = buf;
const nf = ctx.createBiquadFilter(); nf.type = 'lowpass';
nf.frequency.setValueAtTime(800, now + 0.2); nf.frequency.linearRampToValueAtTime(2200, now + 0.9); nf.frequency.linearRampToValueAtTime(600, now + 1.5);
const ng = ctx.createGain();
ng.gain.setValueAtTime(0.0001, now + 0.2); ng.gain.linearRampToValueAtTime(0.16, now + 0.7); ng.gain.linearRampToValueAtTime(0.0001, now + 1.5);
noise.connect(nf).connect(ng).connect(master); noise.start(now + 0.2); noise.stop(now + 1.6);
// 3. 数字 glitch 短鸣(0.5 / 0.9 / 1.3s)
[0.5, 0.9, 1.3].forEach((t, i) => {
const o = ctx.createOscillator(); o.type = 'square';
o.frequency.setValueAtTime(1200 + i * 600, now + t); o.frequency.exponentialRampToValueAtTime(1800 + i * 400, now + t + 0.04);
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, now + t); g.gain.exponentialRampToValueAtTime(0.05, now + t + 0.01); g.gain.exponentialRampToValueAtTime(0.0001, now + t + 0.08);
o.connect(g).connect(master); o.start(now + t); o.stop(now + t + 0.1);
});
// 4. 上扬合成器扫频(1.2s - 2.7s)
const sweep = ctx.createOscillator(); sweep.type = 'sawtooth';
sweep.frequency.setValueAtTime(180, now + 1.2); sweep.frequency.exponentialRampToValueAtTime(1100, now + 2.5);
const sg = ctx.createGain();
sg.gain.setValueAtTime(0.0001, now + 1.2); sg.gain.linearRampToValueAtTime(0.11, now + 2.0); sg.gain.linearRampToValueAtTime(0.0001, now + 2.7);
const sf = ctx.createBiquadFilter(); sf.type = 'lowpass';
sf.frequency.setValueAtTime(600, now + 1.2); sf.frequency.linearRampToValueAtTime(3200, now + 2.5);
sweep.connect(sf).connect(sg).connect(master); sweep.start(now + 1.2); sweep.stop(now + 2.8);
// 5. 冲击点(2.4s):噪声爆发 + 低频重击
const impactLen = Math.floor(ctx.sampleRate * 0.35);
const impactBuf = ctx.createBuffer(1, impactLen, ctx.sampleRate);
const id = impactBuf.getChannelData(0);
for (let i = 0; i < impactLen; i++) id[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / impactLen, 1.2);
const impactNoise = ctx.createBufferSource(); impactNoise.buffer = impactBuf;
const impactF = ctx.createBiquadFilter(); impactF.type = 'bandpass'; impactF.frequency.value = 450; impactF.Q.value = 1.2;
const impactG = ctx.createGain();
impactG.gain.setValueAtTime(0.0001, now + 2.4); impactG.gain.exponentialRampToValueAtTime(0.24, now + 2.42); impactG.gain.exponentialRampToValueAtTime(0.0001, now + 2.85);
impactNoise.connect(impactF).connect(impactG).connect(master); impactNoise.start(now + 2.4); impactNoise.stop(now + 2.9);
const thump = ctx.createOscillator(); thump.type = 'sine';
thump.frequency.setValueAtTime(90, now + 2.4); thump.frequency.exponentialRampToValueAtTime(35, now + 2.8);
const thumpG = ctx.createGain();
thumpG.gain.setValueAtTime(0.0001, now + 2.4); thumpG.gain.exponentialRampToValueAtTime(0.28, now + 2.43); thumpG.gain.exponentialRampToValueAtTime(0.0001, now + 2.9);
thump.connect(thumpG).connect(master); thump.start(now + 2.4); thump.stop(now + 2.95);
// 6. 科技琶音(2.9s - 4.3s)
[523.25, 659.25, 783.99, 1046.50, 783.99, 659.25].forEach((f, i) => {
const t = now + 2.9 + i * 0.24;
const o = ctx.createOscillator(); o.type = 'triangle'; o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.08, t + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55);
o.connect(g).connect(master); o.start(t); o.stop(t + 0.6);
});
// 7. 最终揭示和弦(4.4s - 5.2s)
[392.00, 523.25, 659.25, 783.99].forEach((f, i) => {
const t = now + 4.4 + i * 0.04;
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.12, t + 0.05); g.gain.exponentialRampToValueAtTime(0.0001, t + 1.0);
o.connect(g).connect(master); o.start(t); o.stop(t + 1.1);
});
return master;
}
function playIntro(done) {
const el = document.getElementById('introSplash');
if (!el) { done && done(); return; }
try {
if (sessionStorage.getItem('introPlayed')) { el.remove(); done && done(); return; }
} catch (e) {}
el.classList.add('show');
let soundMaster = null;
try { soundMaster = playIntroSound(); } catch (e) { console.warn('intro音效异常', e); }
// 浏览器自动播放策略:无用户手势时 AudioContext 被挂起,音效静音 → 显示解锁按钮
const unlockBtn = document.getElementById('introAudioUnlock');
let needsUnlock = false;
try { const ctx = getIntroAudioCtx(); needsUnlock = !!ctx && ctx.state !== 'running'; } catch (e) {}
if (needsUnlock && unlockBtn) unlockBtn.style.display = '';
const unlockAudio = async () => {
try {
const ctx = getIntroAudioCtx();
if (ctx && ctx.state === 'suspended') await ctx.resume();
if (ctx && ctx.state === 'running') {
try { stopIntroSound(soundMaster); } catch (e) {}
try { soundMaster = playIntroSound(); } catch (e) {}
}
} catch (e) {}
if (unlockBtn) unlockBtn.style.display = 'none';
};
if (unlockBtn) unlockBtn.addEventListener('click', (e) => { e.stopPropagation(); unlockAudio(); });
let finished = false;
const finish = () => {
if (finished) return; finished = true;
try { stopIntroSound(soundMaster); } catch (e) {}
try { sessionStorage.setItem('introPlayed', '1'); } catch (e) {}
el.classList.remove('show'); el.classList.add('hide');
setTimeout(() => { el.remove(); done && done(); }, 480);
};
const timer = setTimeout(finish, 5200);
const onSkip = () => { clearTimeout(timer); finish(); };
el.addEventListener('click', onSkip);
window.addEventListener('keydown', onSkip, { once: true });
}
// 首页首次访问:先播 yusen 开场动画,再播剧情漫画
(function() {
if (localStorage.getItem('seen_intro_part1')) return;
const btn = document.getElementById('enterGame');
if (btn) btn.style.display = 'none';
playIntroStory([
'assets/story/story_01_transmigration.jpg',
'assets/story/story_02_awakening.jpg'
]).then(() => {
localStorage.setItem('seen_intro_part1', '1');
if (btn) btn.style.display = '';
}).catch(e => {
console.warn('首页剧情播放异常', e);
localStorage.setItem('seen_intro_part1', '1');
if (btn) btn.style.display = '';
playIntro(() => {
playIntroStory([
'assets/story/story_01_transmigration.jpg',
'assets/story/story_02_awakening.jpg'
]).then(() => {
localStorage.setItem('seen_intro_part1', '1');
if (btn) btn.style.display = '';
}).catch(e => {
console.warn('首页剧情播放异常', e);
localStorage.setItem('seen_intro_part1', '1');
if (btn) btn.style.display = '';
});
});
})();
</script>
+277 -24
View File
@@ -606,6 +606,86 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
#loadingScreen .loadFill { height: 100%; width: 0%; background: linear-gradient(90deg, #4af, #a8e6cf); border-radius: 6px; transition: width 0.3s; }
#loadingScreen .loadPct { font-size: 20px; color: #4af; font-variant-numeric: tabular-nums; letter-spacing: 1px; }
/* 进游戏开场动画(yusen 科技感)—— 总时长约 5.2s */
#introSplash {
position: fixed; inset: 0; z-index: 600; overflow: hidden;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background:
radial-gradient(ellipse at 50% 48%, rgba(20,44,78,.55), rgba(3,5,10,.97) 70%),
#03050a;
opacity: 0; visibility: hidden; transition: opacity .5s ease;
}
#introSplash.show { opacity: 1; visibility: visible; }
#introSplash.hide { opacity: 0; visibility: hidden; transition: opacity .45s ease; }
#introSplash .intro-grid {
position: absolute; inset: -50%;
background-image:
linear-gradient(rgba(74,175,255,.09) 1px, transparent 1px),
linear-gradient(90deg, rgba(74,175,255,.09) 1px, transparent 1px);
background-size: 44px 44px;
transform: perspective(440px) rotateX(62deg);
animation: introGridMove 3.2s linear infinite;
-webkit-mask-image: radial-gradient(ellipse at 50% 38%, #000 8%, transparent 72%);
mask-image: radial-gradient(ellipse at 50% 38%, #000 8%, transparent 72%);
}
@keyframes introGridMove { from { background-position: 0 0; } to { background-position: 0 44px; } }
#introSplash .intro-sweep {
position: absolute; top: 0; bottom: 0; width: 42%; left: -42%;
transform: skewX(-18deg); z-index: 3; pointer-events: none;
background: linear-gradient(90deg, transparent, rgba(140,210,255,.20), transparent);
}
#introSplash.show .intro-sweep { animation: introSweep 1.8s ease-in-out .6s 1; }
@keyframes introSweep { 0% { left: -42%; } 100% { left: 110%; } }
#introSplash .intro-logo {
position: relative; z-index: 2; margin: 0;
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
font-weight: 800; font-size: clamp(48px, 12vw, 132px);
letter-spacing: .22em; text-indent: .22em; text-transform: lowercase;
color: #eaf6ff; opacity: 0; transform: scale(.92); filter: blur(8px);
text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35);
}
#introSplash.show .intro-logo { animation: introLogoIn 2s cubic-bezier(.16,1,.3,1) .2s forwards, introHoldPulse 1.6s ease-in-out 3.6s 1 forwards; }
@keyframes introLogoIn {
0% { opacity: 0; transform: scale(.92); filter: blur(8px); }
30% { opacity: 1; transform: scale(1.04); filter: blur(0); text-shadow: 3px 0 rgba(255,46,86,.55), -3px 0 rgba(40,200,255,.55), 0 0 18px rgba(74,175,255,.7); }
48% { text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35); }
100% { opacity: 1; transform: scale(1); filter: blur(0); }
}
@keyframes introHoldPulse {
0% { text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35); }
50% { text-shadow: 0 0 28px rgba(74,175,255,.9), 0 0 72px rgba(74,175,255,.5), 0 0 120px rgba(138,92,246,.25); }
100% { text-shadow: 0 0 18px rgba(74,175,255,.7), 0 0 42px rgba(74,175,255,.35); }
}
#introSplash .intro-line {
position: relative; z-index: 2; margin-top: 18px; height: 2px; width: 0;
background: linear-gradient(90deg, transparent, #4af, #a8e6cf, transparent);
box-shadow: 0 0 12px rgba(74,175,255,.7);
}
#introSplash.show .intro-line { animation: introLine 1.4s ease-out 1.4s forwards; }
@keyframes introLine { to { width: min(320px, 60vw); } }
#introSplash .intro-sub {
position: relative; z-index: 2; margin-top: 14px; font-size: 13px;
letter-spacing: .4em; text-indent: .4em; color: rgba(150,200,255,.55); opacity: 0;
}
#introSplash.show .intro-sub { animation: introFade 1.2s ease 2.6s forwards; }
#introSplash .intro-skip {
position: absolute; bottom: 26px; z-index: 4; font-size: 12px; letter-spacing: .15em;
color: rgba(180,210,255,.4); opacity: 0;
}
#introSplash.show .intro-skip { animation: introFade 1s ease 3.6s forwards; }
#introSplash .intro-audio-unlock {
position: absolute; bottom: 58px; z-index: 5; cursor: pointer;
padding: 9px 20px; border-radius: 999px; font-size: 13px; letter-spacing: .12em;
color: #eaf6ff; background: rgba(74,175,255,.12);
border: 1px solid rgba(74,175,255,.5); box-shadow: 0 0 18px rgba(74,175,255,.35);
-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
opacity: 0; transition: opacity .4s ease, transform .2s ease;
}
#introSplash.show .intro-audio-unlock { opacity: 1; }
#introSplash .intro-audio-unlock:hover { transform: scale(1.05); }
#introSplash.hide .intro-audio-unlock { opacity: 0; }
@keyframes introFade { to { opacity: 1; } }
/* 单位详情弹窗 */
#unitModal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
@@ -969,6 +1049,17 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
</div>
</div>
<!-- 进游戏开场动画(yusen 科技感) -->
<div id="introSplash">
<div class="intro-grid"></div>
<div class="intro-sweep"></div>
<div class="intro-logo">yusen</div>
<div class="intro-line"></div>
<div class="intro-sub">宇森游戏</div>
<button id="introAudioUnlock" class="intro-audio-unlock" style="display:none;">🔊 点击开启音效</button>
<div class="intro-skip">点击任意位置跳过</div>
</div>
<!-- 主视图 -->
<canvas id="gameCanvas"></canvas>
<div id="worldFrame"><span class="wf-tl"></span><span class="wf-tr"></span><span class="wf-bl"></span><span class="wf-br"></span></div>
@@ -7213,12 +7304,184 @@ function init() {
const MIN_LOAD_MS = 1500; // [PLACEHOLDER] 体感:1.5s 是否够?可下调至 1000
const hold = Math.max(0, MIN_LOAD_MS - (Date.now() - loadStart));
setTimeout(() => {
document.getElementById('loadingScreen').style.display = 'none';
requestAnimationFrame(render);
const ls = document.getElementById('loadingScreen');
if (ls) ls.style.display = 'none';
playIntro(() => {
maybePlayIntroStory().then(() => {
startGameGate();
requestAnimationFrame(render);
});
});
}, hold);
});
}
// 进游戏开场动画(yusen 科技感):加载完成后覆盖播放,约 5.2s,可点击跳过,本会话只播一次
let _introAudioCtx = null;
function getIntroAudioCtx() {
try {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return null;
if (!_introAudioCtx) _introAudioCtx = new AC();
if (_introAudioCtx.state === 'suspended') _introAudioCtx.resume();
return _introAudioCtx;
} catch (e) { return null; }
}
function stopIntroSound(master) {
if (!master) return;
try {
const ctx = master.context;
const t = ctx.currentTime;
master.gain.cancelScheduledValues(t);
master.gain.setValueAtTime(Math.max(0.0001, master.gain.value), t);
master.gain.exponentialRampToValueAtTime(0.0001, t + 0.12);
} catch (e) {}
}
function playIntroSound() {
// 参考背景音乐设置:读取「音效」开关,关闭时不发声
if (typeof loadWorldSFXSetting === 'function' && !loadWorldSFXSetting()) return null;
const ctx = getIntroAudioCtx();
if (!ctx) return null;
const now = ctx.currentTime;
const master = ctx.createGain();
master.gain.setValueAtTime(0.0001, now);
master.gain.linearRampToValueAtTime(0.55, now + 0.06);
master.connect(ctx.destination);
// 1. 深空底噪:50Hz + 100Hz 正弦,贯穿全程
[50, 100].forEach(f => {
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, now);
g.gain.linearRampToValueAtTime(0.05, now + 0.8);
g.gain.setValueAtTime(0.05, now + 4.2);
g.gain.linearRampToValueAtTime(0.0001, now + 5.2);
o.connect(g).connect(master); o.start(now); o.stop(now + 5.3);
});
// 2. 低频滤波噪声扫过(0.2s - 1.5s)
const len = Math.floor(ctx.sampleRate * 1.4);
const buf = ctx.createBuffer(1, len, ctx.sampleRate);
const data = buf.getChannelData(0);
for (let i = 0; i < len; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 1.4);
const noise = ctx.createBufferSource(); noise.buffer = buf;
const nf = ctx.createBiquadFilter(); nf.type = 'lowpass';
nf.frequency.setValueAtTime(800, now + 0.2); nf.frequency.linearRampToValueAtTime(2200, now + 0.9); nf.frequency.linearRampToValueAtTime(600, now + 1.5);
const ng = ctx.createGain();
ng.gain.setValueAtTime(0.0001, now + 0.2); ng.gain.linearRampToValueAtTime(0.16, now + 0.7); ng.gain.linearRampToValueAtTime(0.0001, now + 1.5);
noise.connect(nf).connect(ng).connect(master); noise.start(now + 0.2); noise.stop(now + 1.6);
// 3. 数字 glitch 短鸣(0.5 / 0.9 / 1.3s)
[0.5, 0.9, 1.3].forEach((t, i) => {
const o = ctx.createOscillator(); o.type = 'square';
o.frequency.setValueAtTime(1200 + i * 600, now + t); o.frequency.exponentialRampToValueAtTime(1800 + i * 400, now + t + 0.04);
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, now + t); g.gain.exponentialRampToValueAtTime(0.05, now + t + 0.01); g.gain.exponentialRampToValueAtTime(0.0001, now + t + 0.08);
o.connect(g).connect(master); o.start(now + t); o.stop(now + t + 0.1);
});
// 4. 上扬合成器扫频(1.2s - 2.7s)
const sweep = ctx.createOscillator(); sweep.type = 'sawtooth';
sweep.frequency.setValueAtTime(180, now + 1.2); sweep.frequency.exponentialRampToValueAtTime(1100, now + 2.5);
const sg = ctx.createGain();
sg.gain.setValueAtTime(0.0001, now + 1.2); sg.gain.linearRampToValueAtTime(0.11, now + 2.0); sg.gain.linearRampToValueAtTime(0.0001, now + 2.7);
const sf = ctx.createBiquadFilter(); sf.type = 'lowpass';
sf.frequency.setValueAtTime(600, now + 1.2); sf.frequency.linearRampToValueAtTime(3200, now + 2.5);
sweep.connect(sf).connect(sg).connect(master); sweep.start(now + 1.2); sweep.stop(now + 2.8);
// 5. 冲击点(2.4s):噪声爆发 + 低频重击
const impactLen = Math.floor(ctx.sampleRate * 0.35);
const impactBuf = ctx.createBuffer(1, impactLen, ctx.sampleRate);
const id = impactBuf.getChannelData(0);
for (let i = 0; i < impactLen; i++) id[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / impactLen, 1.2);
const impactNoise = ctx.createBufferSource(); impactNoise.buffer = impactBuf;
const impactF = ctx.createBiquadFilter(); impactF.type = 'bandpass'; impactF.frequency.value = 450; impactF.Q.value = 1.2;
const impactG = ctx.createGain();
impactG.gain.setValueAtTime(0.0001, now + 2.4); impactG.gain.exponentialRampToValueAtTime(0.24, now + 2.42); impactG.gain.exponentialRampToValueAtTime(0.0001, now + 2.85);
impactNoise.connect(impactF).connect(impactG).connect(master); impactNoise.start(now + 2.4); impactNoise.stop(now + 2.9);
const thump = ctx.createOscillator(); thump.type = 'sine';
thump.frequency.setValueAtTime(90, now + 2.4); thump.frequency.exponentialRampToValueAtTime(35, now + 2.8);
const thumpG = ctx.createGain();
thumpG.gain.setValueAtTime(0.0001, now + 2.4); thumpG.gain.exponentialRampToValueAtTime(0.28, now + 2.43); thumpG.gain.exponentialRampToValueAtTime(0.0001, now + 2.9);
thump.connect(thumpG).connect(master); thump.start(now + 2.4); thump.stop(now + 2.95);
// 6. 科技琶音(2.9s - 4.3s)
[523.25, 659.25, 783.99, 1046.50, 783.99, 659.25].forEach((f, i) => {
const t = now + 2.9 + i * 0.24;
const o = ctx.createOscillator(); o.type = 'triangle'; o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.08, t + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55);
o.connect(g).connect(master); o.start(t); o.stop(t + 0.6);
});
// 7. 最终揭示和弦(4.4s - 5.2s)
[392.00, 523.25, 659.25, 783.99].forEach((f, i) => {
const t = now + 4.4 + i * 0.04;
const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
const g = ctx.createGain();
g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.12, t + 0.05); g.gain.exponentialRampToValueAtTime(0.0001, t + 1.0);
o.connect(g).connect(master); o.start(t); o.stop(t + 1.1);
});
return master;
}
function playIntro(done) {
const el = document.getElementById('introSplash');
if (!el) { done && done(); return; }
try {
if (sessionStorage.getItem('introPlayed')) { el.remove(); done && done(); return; }
} catch (e) {}
el.classList.add('show');
let soundMaster = null;
try { soundMaster = playIntroSound(); } catch (e) { console.warn('intro音效异常', e); }
// 浏览器自动播放策略:无用户手势时 AudioContext 被挂起,音效静音 → 显示解锁按钮
const unlockBtn = document.getElementById('introAudioUnlock');
let needsUnlock = false;
try { const ctx = getIntroAudioCtx(); needsUnlock = !!ctx && ctx.state !== 'running'; } catch (e) {}
if (needsUnlock && unlockBtn) unlockBtn.style.display = '';
const unlockAudio = async () => {
try {
const ctx = getIntroAudioCtx();
if (ctx && ctx.state === 'suspended') await ctx.resume();
if (ctx && ctx.state === 'running') {
try { stopIntroSound(soundMaster); } catch (e) {}
try { soundMaster = playIntroSound(); } catch (e) {}
}
} catch (e) {}
if (unlockBtn) unlockBtn.style.display = 'none';
};
if (unlockBtn) unlockBtn.addEventListener('click', (e) => { e.stopPropagation(); unlockAudio(); });
let finished = false;
const finish = () => {
if (finished) return; finished = true;
try { stopIntroSound(soundMaster); } catch (e) {}
try { sessionStorage.setItem('introPlayed', '1'); } catch (e) {}
el.classList.remove('show'); el.classList.add('hide');
setTimeout(() => { el.remove(); done && done(); }, 480);
};
const timer = setTimeout(finish, 5200);
const onSkip = () => { clearTimeout(timer); finish(); };
el.addEventListener('click', onSkip);
window.addEventListener('keydown', onSkip, { once: true });
}
// 首次进入剧情:在 yusen 开场动画之后播放,再进入登录/游戏
async function maybePlayIntroStory() {
if (localStorage.getItem('seen_intro_part1')) return;
try {
await playIntroStory([
'assets/story/story_01_transmigration.jpg',
'assets/story/story_02_awakening.jpg'
], {
onStart: () => {
const authSplash = document.getElementById('authSplash');
if (authSplash) authSplash.style.display = 'none';
}
});
} catch (e) { console.warn('剧情播放异常', e); }
localStorage.setItem('seen_intro_part1', '1');
}
window.addEventListener('resize', () => {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight - BOTTOM_BAR_H;
@@ -7363,6 +7626,7 @@ window.addEventListener('keydown', e => {
if (typeof fitWorld === 'function') fitWorld();
</script>
<script src="assets/game-api.js?v=20260825"></script>
<script src="assets/common.js?v=20260825"></script>
<!-- ===== 我的天赋面板(顶栏 🌟 按钮触发,展示玩家已拥有的天赋) ===== -->
<div id="talentPanel" hidden>
@@ -7572,8 +7836,8 @@ if (typeof fitWorld === 'function') fitWorld();
}catch(e){ return null; }
}
function playTalentSfx(type){
const tg = document.getElementById('sfxToggle');
if(tg && !tg.classList.contains('active')) return; // 音效关闭则不发声
// 音效开关统一走 localStorage(设置面板可能未打开)
if (typeof loadWorldSFXSetting === 'function' && !loadWorldSFXSetting()) return;
const ctx = _getSfxCtx(); if(!ctx) return;
const now = ctx.currentTime;
if(type === 'whoosh'){
@@ -7792,29 +8056,12 @@ if (typeof fitWorld === 'function') fitWorld();
<script src="assets/characters.js?v=20260825"></script>
<script>
// ===== 登录闸门:未登录跳登录页;已登录显示昵称+退出 =====
(async () => {
async function startGameGate() {
const splash = document.getElementById('authSplash');
const area = document.getElementById('userArea');
const trigger = document.getElementById('uaTrigger');
const menu = document.getElementById('uaMenu');
try {
// 剧情漫画:首次进入(未看过前两幕)先全屏播放,播完标记,再走登录判定
if (!localStorage.getItem('seen_intro_part1')) {
try {
await playIntroStory([
'assets/story/story_01_transmigration.jpg',
'assets/story/story_02_awakening.jpg'
], {
onStart: () => {
const loading = document.getElementById('loadingScreen');
if (loading) loading.style.display = 'none';
const authSplash = document.getElementById('authSplash');
if (authSplash) authSplash.style.display = 'none';
}
});
} catch (e) { console.warn('剧情播放异常', e); }
localStorage.setItem('seen_intro_part1', '1');
}
const me = await API.me();
if (!me || !me.uid) { location.replace('login.html?next=world.html'); return; }
document.getElementById('uaName').textContent = me.name || me.login;
@@ -8036,9 +8283,10 @@ if (typeof fitWorld === 'function') fitWorld();
const btnSettings = document.getElementById('btnSettings');
if (btnSettings) btnSettings.addEventListener('click', () => {
const bgmEnabled = loadWorldBGMSetting();
const sfxEnabled = loadWorldSFXSetting();
openAppModal('设置', `
<div class="settingRow"><span>背景音乐</span><span class="toggleSwitch${bgmEnabled ? ' active' : ''}" id="bgmToggle"></span></div>
<div class="settingRow"><span>音效</span><span class="toggleSwitch active" id="sfxToggle"></span></div>
<div class="settingRow"><span>音效</span><span class="toggleSwitch${sfxEnabled ? ' active' : ''}" id="sfxToggle"></span></div>
<div class="settingRow"><span>通知推送</span><span class="toggleSwitch active" id="notifyToggle"></span></div>
<div class="settingRow"><span>画质</span><span style="color:#e0e0e0;">高</span></div>
<div class="settingRow"><span>语言</span><span style="color:#e0e0e0;">简体中文</span></div>
@@ -8073,6 +8321,11 @@ if (typeof fitWorld === 'function') fitWorld();
audio.pause();
}
};
const sfxToggle = document.getElementById('sfxToggle');
if (sfxToggle) sfxToggle.onclick = () => {
const now = sfxToggle.classList.toggle('active');
saveWorldSFXSetting(now);
};
const redeemBtn = document.getElementById('redeemBtn');
const redeemInput = document.getElementById('redeemInput');
const redeemMsg = document.getElementById('redeemMsg');
@@ -8193,7 +8446,7 @@ if (typeof fitWorld === 'function') fitWorld();
} finally {
if (splash) splash.remove();
}
})();
}
</script>
<script src="assets/world-app.js?v=20260825"></script>