Merge pull request 'Why ga' (#28) from why_ga into main
Reviewed-on: #28
@@ -101,7 +101,7 @@
|
||||
<record id="talent_harvest_blessing" model="yt.codex.talent">
|
||||
<field name="code">talent_harvest_blessing</field>
|
||||
<field name="name">丰收祝福</field>
|
||||
<field name="icon">🌾</field>
|
||||
<field name="icon">assets/icons/talents/harvest_blessing.png</field>
|
||||
<field name="quality">uncommon</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">农作物产量+15%;生长速度+10%</field>
|
||||
@@ -115,7 +115,7 @@
|
||||
<record id="talent_soul_drain" model="yt.codex.talent">
|
||||
<field name="code">talent_soul_drain</field>
|
||||
<field name="name">汲魂者之触</field>
|
||||
<field name="icon">💀</field>
|
||||
<field name="icon">assets/icons/talents/soul_drain.png</field>
|
||||
<field name="quality">epic</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">造成伤害的8%转化为生命回复;对亡灵单位翻倍</field>
|
||||
@@ -129,7 +129,7 @@
|
||||
<record id="talent_elemental_affinity" model="yt.codex.talent">
|
||||
<field name="code">talent_elemental_affinity</field>
|
||||
<field name="name">元素亲和</field>
|
||||
<field name="icon">🔥</field>
|
||||
<field name="icon">assets/icons/talents/elemental_affinity.png</field>
|
||||
<field name="quality">legendary</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">元素伤害+20%;受到元素伤害-15%</field>
|
||||
@@ -143,7 +143,7 @@
|
||||
<record id="talent_swift_stride" model="yt.codex.talent">
|
||||
<field name="code">talent_swift_stride</field>
|
||||
<field name="name">迅捷步伐</field>
|
||||
<field name="icon">💨</field>
|
||||
<field name="icon">assets/icons/talents/swift_stride.png</field>
|
||||
<field name="quality">common</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">基础移速+15%;脱离战斗后额外+10%</field>
|
||||
@@ -157,7 +157,7 @@
|
||||
<record id="talent_precise_strike" model="yt.codex.talent">
|
||||
<field name="code">talent_precise_strike</field>
|
||||
<field name="name">精准打击</field>
|
||||
<field name="icon">🎯</field>
|
||||
<field name="icon">assets/icons/talents/precise_strike.png</field>
|
||||
<field name="quality">uncommon</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">暴击率+8%;暴击伤害+15%</field>
|
||||
@@ -171,7 +171,7 @@
|
||||
<record id="talent_tough_body" model="yt.codex.talent">
|
||||
<field name="code">talent_tough_body</field>
|
||||
<field name="name">坚韧体魄</field>
|
||||
<field name="icon">🛡️</field>
|
||||
<field name="icon">assets/icons/talents/tough_body.png</field>
|
||||
<field name="quality">common</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">最大生命+12%;受到物理伤害-5%</field>
|
||||
@@ -185,7 +185,7 @@
|
||||
<record id="talent_nature_whisper" model="yt.codex.talent">
|
||||
<field name="code">talent_nature_whisper</field>
|
||||
<field name="name">自然低语</field>
|
||||
<field name="icon">🍃</field>
|
||||
<field name="icon">assets/icons/talents/nature_whisper.png</field>
|
||||
<field name="quality">rare</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">采集速度+20%;稀有材料发现率+5%</field>
|
||||
@@ -199,7 +199,7 @@
|
||||
<record id="talent_shadow_stealth" model="yt.codex.talent">
|
||||
<field name="code">talent_shadow_stealth</field>
|
||||
<field name="name">暗影潜行</field>
|
||||
<field name="icon">🌑</field>
|
||||
<field name="icon">assets/icons/talents/shadow_stealth.png</field>
|
||||
<field name="quality">epic</field>
|
||||
<field name="trigger">被动</field>
|
||||
<field name="effect">夜间隐蔽+30%;从隐蔽状态发起的首击伤害+25%</field>
|
||||
|
||||
@@ -17,16 +17,15 @@ const _isDevPort = (port) =>
|
||||
(port === 80 && (location.port === '' || location.port === '80'));
|
||||
|
||||
const API_CONFIG = {
|
||||
// 本地开发一律跨域直连 Odoo 8069(python http.server 无代理能力);
|
||||
// 仅 8080 dev-server(自带反向代理到 Odoo)才走同源空 base。
|
||||
base: _isLocalDev()
|
||||
? (_isDevPort(8080) ? '' : `http://${location.hostname}:8069`)
|
||||
: '',
|
||||
db: _isLocalDev() ? 'game_a' : 'game',
|
||||
? ((_isDevPort(8080) || _isDevPort(8888)) ? '' : `http://${location.hostname}:8080`)
|
||||
: '',
|
||||
db: _isLocalDev() ? 'study' : 'game',
|
||||
};
|
||||
|
||||
// 世界(yt_world)所在库:与 API_CONFIG.db 保持一致(本地均为 game_a),避免跨库导致 auth 失败。
|
||||
const WORLD_DB = API_CONFIG.db;
|
||||
// 世界(yt_world)所在库:本地 Odoo 钉 yt_game;线上游戏库名为 game。按域名自适应,git 同步不冲突。
|
||||
const WORLD_DB = _isLocalDev() ? 'yt_game' : 'game';
|
||||
|
||||
|
||||
// Odoo 后端基础 URL(世界 API 专用,兼容本地跨域 / 生产同域)
|
||||
const WORLD_BASE = API_CONFIG.base || '';
|
||||
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 996 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1005 KiB |
|
After Width: | Height: | Size: 771 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1024 KiB |
|
After Width: | Height: | Size: 796 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 733 KiB |
|
After Width: | Height: | Size: 745 KiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 763 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 684 KiB |
|
After Width: | Height: | Size: 1017 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 882 KiB |
|
After Width: | Height: | Size: 604 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 88 KiB |
@@ -369,83 +369,30 @@ function playIntroSound() {
|
||||
const now = ctx.currentTime;
|
||||
const master = ctx.createGain();
|
||||
master.gain.setValueAtTime(0.0001, now);
|
||||
master.gain.linearRampToValueAtTime(0.55, now + 0.06);
|
||||
master.gain.linearRampToValueAtTime(0.5, now + 0.05);
|
||||
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);
|
||||
});
|
||||
// 干净的「铃」音:正弦主音 + 高八度泛音增亮,平滑指数衰减
|
||||
function bell(freq, start, dur, vol) {
|
||||
const o1 = ctx.createOscillator(); o1.type = 'sine'; o1.frequency.value = freq;
|
||||
const o2 = ctx.createOscillator(); o2.type = 'sine'; o2.frequency.value = freq * 2;
|
||||
const g1 = ctx.createGain();
|
||||
g1.gain.setValueAtTime(0.0001, start);
|
||||
g1.gain.exponentialRampToValueAtTime(vol, start + 0.012);
|
||||
g1.gain.exponentialRampToValueAtTime(0.0001, start + dur);
|
||||
const g2 = ctx.createGain();
|
||||
g2.gain.setValueAtTime(0.0001, start);
|
||||
g2.gain.exponentialRampToValueAtTime(vol * 0.3, start + 0.012);
|
||||
g2.gain.exponentialRampToValueAtTime(0.0001, start + dur * 0.55);
|
||||
o1.connect(g1).connect(master); o1.start(start); o1.stop(start + dur + 0.05);
|
||||
o2.connect(g2).connect(master); o2.start(start); o2.stop(start + dur + 0.05);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
// 开场:极轻星光一闪(光点感)
|
||||
bell(1046.50, now + 0.2, 0.45, 0.06);
|
||||
// 揭示「叮咚」门铃:高音 ding -> 低音 dong
|
||||
bell(1567.98, now + 3.6, 0.55, 0.22); // G6 叮
|
||||
bell(1046.50, now + 4.25, 0.95, 0.22); // C6 咚
|
||||
|
||||
return master;
|
||||
}
|
||||
|
||||
@@ -7436,83 +7436,30 @@ function playIntroSound() {
|
||||
const now = ctx.currentTime;
|
||||
const master = ctx.createGain();
|
||||
master.gain.setValueAtTime(0.0001, now);
|
||||
master.gain.linearRampToValueAtTime(0.55, now + 0.06);
|
||||
master.gain.linearRampToValueAtTime(0.5, now + 0.05);
|
||||
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);
|
||||
});
|
||||
// 干净的「铃」音:正弦主音 + 高八度泛音增亮,平滑指数衰减
|
||||
function bell(freq, start, dur, vol) {
|
||||
const o1 = ctx.createOscillator(); o1.type = 'sine'; o1.frequency.value = freq;
|
||||
const o2 = ctx.createOscillator(); o2.type = 'sine'; o2.frequency.value = freq * 2;
|
||||
const g1 = ctx.createGain();
|
||||
g1.gain.setValueAtTime(0.0001, start);
|
||||
g1.gain.exponentialRampToValueAtTime(vol, start + 0.012);
|
||||
g1.gain.exponentialRampToValueAtTime(0.0001, start + dur);
|
||||
const g2 = ctx.createGain();
|
||||
g2.gain.setValueAtTime(0.0001, start);
|
||||
g2.gain.exponentialRampToValueAtTime(vol * 0.3, start + 0.012);
|
||||
g2.gain.exponentialRampToValueAtTime(0.0001, start + dur * 0.55);
|
||||
o1.connect(g1).connect(master); o1.start(start); o1.stop(start + dur + 0.05);
|
||||
o2.connect(g2).connect(master); o2.start(start); o2.stop(start + dur + 0.05);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
// 开场:极轻星光一闪(光点感)
|
||||
bell(1046.50, now + 0.2, 0.45, 0.06);
|
||||
// 揭示「叮咚」门铃:高音 ding -> 低音 dong
|
||||
bell(1567.98, now + 3.6, 0.55, 0.22); // G6 叮
|
||||
bell(1046.50, now + 4.25, 0.95, 0.22); // C6 咚
|
||||
|
||||
return master;
|
||||
}
|
||||
@@ -8563,6 +8510,12 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
font-size:22px;line-height:1;color:#9cf;transition:transform .22s ease,box-shadow .18s;}
|
||||
#worldChat.collapsed #wcBubble{display:flex;}
|
||||
#wcBubble:hover{transform:scale(1.1);box-shadow:0 0 16px rgba(139,92,246,.35);}
|
||||
/* ===== 最小化态:缩小版完整聊天框(头部+消息区+输入条齐全,总高约为展开态一半) ===== */
|
||||
#worldChat.minified{width:340px;max-height:206px;}
|
||||
#worldChat.minified #wcPanel{display:flex;max-height:200px;}
|
||||
#worldChat.minified #wcBody{height:100px;max-height:100px;}
|
||||
#worldChat.minified #wcEmoji,
|
||||
#worldChat.minified #wcMention{display:none !important;}
|
||||
#wcBadge{position:absolute;top:-2px;right:-4px;min-width:18px;height:18px;border-radius:9px;
|
||||
background:linear-gradient(135deg,#e74c3c,#c0392b);color:#fff;font-size:10px;font-weight:700;
|
||||
display:flex;align-items:center;justify-content:center;padding:0 5px;line-height:1;
|
||||
@@ -8581,6 +8534,9 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
transition:border-color .18s,background .18s;}
|
||||
.wc-emoji-btn:hover{background:rgba(139,92,246,.22);border-color:rgba(139,92,246,.4);}
|
||||
.wc-emoji-btn.active{background:rgba(139,92,246,.32);border-color:rgba(139,92,246,.5);}
|
||||
/* 收起/展开切换按钮:最小化态显示 ▲,展开态显示 — */
|
||||
.wc-toggle{transition:background .18s,color .18s;}
|
||||
.wc-toggle:hover{background:rgba(139,92,246,.28);color:#fff;}
|
||||
.wc-emoji-box{position:absolute;bottom:46px;left:10px;right:10px;overflow:hidden;scrollbar-width:none;
|
||||
display:grid;grid-template-columns:repeat(auto-fill,minmax(30px,1fr));gap:2px;padding:6px;
|
||||
background:rgba(8,14,28,.97);border:1px solid rgba(139,92,246,.4);border-radius:10px;
|
||||
@@ -8659,7 +8615,7 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
.wc-msg.guide .wc-name{color:#c4b5fd;}
|
||||
.wc-msg.guide .wc-bubble{background:linear-gradient(135deg,#2a2150,#3b2f63);border:1px solid #4c3a82;color:#ede9fe;}
|
||||
</style>
|
||||
<div id="worldChat">
|
||||
<div id="worldChat" class="minified">
|
||||
<!-- 折叠态悬浮气泡 -->
|
||||
<div id="wcBubble" onclick="toggleWorldChat()" title="展开聊天">
|
||||
💬
|
||||
@@ -8670,7 +8626,7 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
<div id="wcHeader">
|
||||
<select id="wcChannelSel" class="wc-channel-select"></select>
|
||||
<button id="wcTestBtn" class="wc-test-btn" type="button" onclick="wcSendTest()">测试</button>
|
||||
<span class="wc-toggle" onclick="toggleWorldChat()">—</span>
|
||||
<span id="wcToggleBtn" class="wc-toggle" onclick="toggleWorldChat()" title="收起聊天">—</span>
|
||||
</div>
|
||||
<div id="wcBody">
|
||||
<!-- demo messages injected by JS -->
|
||||
@@ -8700,6 +8656,8 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
let pollTimer = null;
|
||||
const badge = document.getElementById('wcBadge');
|
||||
const mentionBox = document.getElementById('wcMention');
|
||||
const wcSend = document.getElementById('wcSend');
|
||||
const toggleBtn = document.getElementById('wcToggleBtn');
|
||||
|
||||
function totalUnread(){ let s = 0; for (const k in unreadByChannel) s += (unreadByChannel[k] || 0); return s; }
|
||||
function updateBadge(){
|
||||
@@ -8818,11 +8776,33 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
loadMessages(true);
|
||||
updateChannelOptions(); updateBadge();
|
||||
});
|
||||
// 头部按钮随状态切换:最小化态 ▲(展开),展开态 —(收起)
|
||||
function syncChatState(){
|
||||
if(!toggleBtn) return;
|
||||
const minified = chat.classList.contains('minified');
|
||||
toggleBtn.textContent = minified ? '▲' : '—';
|
||||
toggleBtn.title = minified ? '展开聊天' : '收起聊天';
|
||||
}
|
||||
// 展开为完整聊天面板(清当前频道未读 + 滚到底部)
|
||||
function expandChat(){
|
||||
chat.classList.remove('minified');
|
||||
syncChatState();
|
||||
unreadByChannel[currentChannel] = 0; updateChannelOptions(); updateBadge();
|
||||
body.scrollTop = body.scrollHeight;
|
||||
}
|
||||
window.toggleWorldChat = function(){
|
||||
const wasCollapsed = chat.classList.contains('collapsed');
|
||||
chat.classList.toggle('collapsed');
|
||||
if(wasCollapsed){ unreadByChannel[currentChannel] = 0; updateChannelOptions(); updateBadge(); }
|
||||
if(chat.classList.contains('collapsed')){
|
||||
chat.classList.remove('collapsed');
|
||||
chat.classList.add('minified');
|
||||
syncChatState();
|
||||
} else if(chat.classList.contains('minified')){
|
||||
expandChat();
|
||||
} else {
|
||||
chat.classList.add('minified');
|
||||
syncChatState();
|
||||
}
|
||||
};
|
||||
syncChatState();
|
||||
window.wcSendMsg = async function(){
|
||||
const t = input.value.trim();
|
||||
if(!t || !currentChannel) return;
|
||||
@@ -8931,7 +8911,8 @@ if (typeof fitWorld === 'function') fitWorld();
|
||||
// 对外暴露:好友面板「私聊」调用
|
||||
window.ChatAPI = {
|
||||
open(channelId){
|
||||
chat.classList.remove('collapsed');
|
||||
chat.classList.remove('collapsed','minified');
|
||||
syncChatState();
|
||||
if(channelId && channels.some(c => c.id === channelId)){
|
||||
sel.value = channelId; currentChannel = channelId; unreadByChannel[channelId] = 0;
|
||||
loadMessages(true); updateChannelOptions(); updateBadge();
|
||||
|
||||