From 17150b2c9d85e69943624020128e11a429797520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Thu, 3 Sep 2026 18:06:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E5=85=A5=E6=B8=B8=E6=88=8F=E5=8A=A8?= =?UTF-8?q?=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/common.js | 9 ++ index.html | 263 +++++++++++++++++++++++++++++++++++++++-- world.html | 301 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 538 insertions(+), 35 deletions(-) diff --git a/assets/common.js b/assets/common.js index baf6b7a..2b03851 100644 --- a/assets/common.js +++ b/assets/common.js @@ -180,3 +180,12 @@ function thumbSVG(it){ ``+ ``; } + +// ---------- 全局音效开关(与 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) {} +} diff --git a/index.html b/index.html index f7f00c4..bf2d4c5 100644 --- a/index.html +++ b/index.html @@ -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; } } + +
+
+
+ +
+
宇森游戏
+ +
点击任意位置跳过
+
@@ -250,21 +340,172 @@ document.getElementById('enterGame').addEventListener('click', async ()=>{ diff --git a/world.html b/world.html index dada74f..350fb9b 100644 --- a/world.html +++ b/world.html @@ -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
+ +
+
+
+ +
+
宇森游戏
+ +
点击任意位置跳过
+
+
@@ -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(); +