Why ga #9

Merged
Ghost merged 3 commits from why_ga into main 2026-08-28 16:36:59 +08:00
2 changed files with 72 additions and 43 deletions
+5 -7
View File
@@ -17,16 +17,14 @@ 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 || '';
+67 -36
View File
@@ -812,15 +812,10 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
.td-front.got{box-shadow:0 0 0 2px rgba(70,224,122,.7),0 6px 18px rgba(70,224,122,.35);}
.td-front .td-got{position:absolute;top:6px;right:8px;font-size:10px;color:#46e07a;font-weight:700;}
.td-card.done{cursor:default;}
/* 抽卡揭晓特效:光晕扩散 + 斜向扫光 + 正面弹跳 + 星星迸发(翻面后接续播放) */
.td-front .td-glow{position:absolute;inset:0;border-radius:14px;pointer-events:none;opacity:0;
background:radial-gradient(circle at 50% 42%,rgba(167,139,250,.9),rgba(74,175,255,.3) 45%,transparent 70%);}
.td-card.revealing .td-glow{animation:td-glow 1s ease-out .5s forwards;}
@keyframes td-glow{0%{opacity:0;transform:scale(.4);}35%{opacity:1;}100%{opacity:0;transform:scale(1.3);}}
.td-front .td-shine{position:absolute;top:0;left:-60%;width:60%;height:100%;pointer-events:none;opacity:0;
background:linear-gradient(105deg,transparent,rgba(255,255,255,.6),transparent);transform:skewX(-18deg);}
.td-card.revealing .td-shine{animation:td-shine 1s ease-in-out .55s forwards;}
@keyframes td-shine{0%{left:-60%;opacity:0;}45%{opacity:.9;}100%{left:120%;opacity:0;}}
/* 抽卡揭晓特效:命运涟漪环 + 正面弹跳 + 星星迸发(翻面后接续播放,无亮光扫过) */
.td-front .td-ring{position:absolute;left:50%;top:50%;width:64%;height:64%;margin:-32% 0 0 -32%;border-radius:50%;pointer-events:none;opacity:0;border:2px solid rgba(167,139,250,.85);box-shadow:0 0 14px rgba(167,139,250,.45);}
.td-card.revealing .td-ring{animation:td-ring .9s ease-out .5s forwards;}
@keyframes td-ring{0%{opacity:.9;transform:scale(.25);}70%{opacity:.5;}100%{opacity:0;transform:scale(2.5);}}
.td-card.revealing .td-front{animation:td-pop 1s cubic-bezier(.18,1.5,.4,1) .5s both;}
@keyframes td-pop{0%{transform:scale(.82) rotateY(180deg);filter:brightness(1.7);}60%{transform:scale(1.06) rotateY(180deg);}100%{transform:scale(1) rotateY(180deg);filter:brightness(1);}}
.td-spark{position:absolute;left:50%;top:46%;width:6px;height:6px;border-radius:50%;pointer-events:none;z-index:5;opacity:0;
@@ -833,14 +828,16 @@ body { background: #0a0e17; overflow: hidden; font-family: 'Microsoft YaHei', sa
.td-btn.ghost{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.16);box-shadow:none;color:#cdd8e6;}
.td-count{color:#9fb3cc;font-size:13px;}
.td-q-common{color:#9ca3af;} .td-q-uncommon{color:#5fd17a;} .td-q-rare{color:#4aafff;} .td-q-epic{color:#c084fc;} .td-q-legendary{color:#f59e0b;} .td-q-mythic{color:#fb7185;}
/* 抽卡完成动画:未翻牌碎裂 / 已翻牌飞向天赋按钮 */
.td-card.shatter{pointer-events:none;}
.td-card.shatter .td-inner{opacity:0;transform:scale(.92);transition:opacity .22s,transform .22s;}
.shard{position:absolute;width:16px;height:16px;background:linear-gradient(135deg,#a78bfa,#4c1d95);border:1px solid rgba(196,181,253,.35);border-radius:3px;pointer-events:none;opacity:0;left:50%;top:50%;margin:-8px 0 0 -8px;box-shadow:0 0 8px rgba(139,92,246,.5);}
.td-card.shatter .shard{animation:shard-burst .7s cubic-bezier(.2,.7,.2,1) forwards;}
@keyframes shard-burst{0%{opacity:1;transform:translate3d(0,0,0) rotate(0deg) scale(1);}100%{opacity:0;transform:translate3d(var(--tx),var(--ty),0) rotate(var(--rot)) scale(.15);}}
.td-fly-clone{position:fixed;z-index:9999;pointer-events:none;border-radius:14px;overflow:hidden;box-shadow:0 0 28px rgba(139,92,246,.75);transition:transform .75s cubic-bezier(.2,.8,.2,1),opacity .75s;}
/* 抽卡完成动画:未翻牌光尘消散 / 已翻牌飞向天赋按钮 */
.td-card.dissolve{pointer-events:none;}
.td-card.dissolve .td-inner{opacity:0;transform:scale(.82) translateY(-14px);filter:blur(3px);transition:opacity .5s ease,transform .5s ease,filter .5s ease;}
.dust{position:absolute;width:7px;height:7px;border-radius:50%;pointer-events:none;opacity:0;left:50%;top:50%;margin:-3.5px 0 0 -3.5px;background:radial-gradient(circle,#c4b5fd,#7c3aed);box-shadow:0 0 6px rgba(167,139,250,.7);}
.td-card.dissolve .dust{animation:tdust .8s ease-out forwards;}
@keyframes tdust{0%{opacity:1;transform:translate(0,0) scale(1);}100%{opacity:0;transform:translate(var(--dx),var(--dy)) scale(.2);}}
.td-fly-clone{position:fixed;z-index:9999;pointer-events:none;border-radius:14px;overflow:hidden;box-shadow:0 0 28px rgba(139,92,246,.75);transition:transform 1.05s cubic-bezier(.2,.8,.2,1),opacity 1.05s;}
.td-fly-clone .td-inner{transition:none !important;}
#talentBtn.talent-pulse{animation:tbtn-pulse .55s ease-out;}
@keyframes tbtn-pulse{0%{box-shadow:0 0 0 0 rgba(139,92,246,.7);transform:scale(1);}50%{transform:scale(1.18);}100%{box-shadow:0 0 0 18px rgba(139,92,246,0);transform:scale(1);}}
</style>
<link rel="stylesheet" href="assets/story-player.css" />
</head>
@@ -7089,7 +7086,7 @@ if (typeof fitWorld === 'function') fitWorld();
'<div class="td-inner">' +
'<div class="td-face td-back">🌟</div>' +
'<div class="td-face td-front">' +
'<div class="td-glow"></div><div class="td-shine"></div>' +
'<div class="td-ring"></div>' +
'<div class="td-got" hidden>已获得</div>' +
'<div class="td-ico">' + (t.icon || '🌟') + '</div>' +
'<div class="td-q ' + tdQualityClass(t.quality) + '">' + (QLABEL[t.quality] || '普通') + '</div>' +
@@ -7151,6 +7148,24 @@ if (typeof fitWorld === 'function') fitWorld();
g.gain.setValueAtTime(0.16, now);
g.gain.exponentialRampToValueAtTime(0.0001, now + 0.10);
n.connect(hp).connect(g).connect(ctx.destination); n.start(now); n.stop(now + 0.12);
} else if(type === 'vanish'){
// 未选中牌消散:低沉下滑“嗖”声
const o = ctx.createOscillator(), g = ctx.createGain();
o.type = 'sine'; o.frequency.setValueAtTime(420, now);
o.frequency.exponentialRampToValueAtTime(90, now + 0.35);
g.gain.setValueAtTime(0.0001, now);
g.gain.exponentialRampToValueAtTime(0.18, now + 0.04);
g.gain.exponentialRampToValueAtTime(0.0001, now + 0.38);
o.connect(g).connect(ctx.destination); o.start(now); o.stop(now + 0.4);
} else if(type === 'collect'){
// 已翻牌收进天赋:上行短“叮”声
const o = ctx.createOscillator(), g = ctx.createGain();
o.type = 'triangle'; o.frequency.setValueAtTime(880, now);
o.frequency.exponentialRampToValueAtTime(1318.5, now + 0.12);
g.gain.setValueAtTime(0.0001, now);
g.gain.exponentialRampToValueAtTime(0.2, now + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, now + 0.3);
o.connect(g).connect(ctx.destination); o.start(now); o.stop(now + 0.32);
}
}
function tdSpawnSparks(card){
@@ -7173,14 +7188,14 @@ if (typeof fitWorld === 'function') fitWorld();
return;
}
card.classList.add('flipped');
playTalentSfx('whoosh'); // 翻牌起手音
playTalentSfx('deal'); // 翻牌起手音(蜘蛛纸牌纸张啪声)
tdFlipped++;
tdNames.push(t.name);
tdUpdateCount();
const res = await API.talentDrawActivate(t.code);
const front = card.querySelector('.td-front');
if(res && res.ok){
card.classList.add('revealing'); // 触发光晕/扫光/星星特效
card.classList.add('revealing'); // 触发命运涟漪环/星星特效
tdSpawnSparks(card);
setTimeout(() => playTalentSfx('chime'), 480); // 翻到正面时响起获得音
card.classList.add('done');
@@ -7210,7 +7225,12 @@ if (typeof fitWorld === 'function') fitWorld();
const flipped = cards.filter(c => c.classList.contains('flipped'));
const unflipped = cards.filter(c => !c.classList.contains('flipped'));
const target = document.getElementById('talentBtn');
const targetRect = target ? target.getBoundingClientRect() : null;
let targetRect = target ? target.getBoundingClientRect() : null;
if(!targetRect || targetRect.width === 0){ // 按钮隐藏时回退到顶栏右侧
const tb = document.getElementById('topBar');
const r = tb ? tb.getBoundingClientRect() : { right: window.innerWidth, top: 8 };
targetRect = { left: r.right - 56, top: (tb ? r.top : 8) + 6, width: 50, height: 34 };
}
let completed = 0;
const total = flipped.length + (unflipped.length ? 1 : 0);
@@ -7233,45 +7253,56 @@ if (typeof fitWorld === 'function') fitWorld();
const rect = card.getBoundingClientRect();
const clone = card.cloneNode(true);
clone.classList.add('td-fly-clone');
clone.classList.remove('dealt','revealing'); // 去掉发牌/揭晓动画类,否则其 animation 会覆盖飞行 transform
clone.style.left = rect.left + 'px';
clone.style.top = rect.top + 'px';
clone.style.width = rect.width + 'px';
clone.style.height = rect.height + 'px';
clone.style.opacity = '1';
clone.style.transform = 'translate3d(0,0,0) rotate(0deg)';
document.body.appendChild(clone);
void clone.offsetWidth; // 强制 reflow,保证起始 transform 已渲染
card.style.opacity = '0';
setTimeout(()=>{
playTalentSfx('deal');
if(targetRect){
const tx = targetRect.left + targetRect.width/2 - (rect.left + rect.width/2);
const ty = targetRect.top + targetRect.height/2 - (rect.top + rect.height/2);
const scale = Math.max(0.18, Math.min(0.32, targetRect.width / rect.width));
clone.style.transform = 'translate3d(' + tx + 'px,' + ty + 'px,0) scale(' + scale + ')';
const scale = Math.max(0.16, Math.min(0.28, targetRect.width / rect.width));
const rot = -360 - (i % 2 === 0 ? 180 : 0); // 每张牌朝按钮方向旋转飞入
clone.style.transform = 'translate3d(' + tx + 'px,' + ty + 'px,0) scale(' + scale + ') rotate(' + rot + 'deg)';
// 到达时给天赋按钮一个脉冲反馈
setTimeout(()=>{
const btn = document.getElementById('talentBtn');
if(btn){ btn.classList.remove('talent-pulse'); void btn.offsetWidth; btn.classList.add('talent-pulse'); }
}, 980);
}
clone.style.opacity = '0';
}, 60 + i*90);
setTimeout(()=>{ clone.remove(); checkDone(); }, 820);
}, 80 + i*120);
setTimeout(()=>{ clone.remove(); checkDone(); }, 1150);
});
// 未翻开的牌:延迟碎裂
// 未翻开的牌:延迟光尘消散 + 消散音效
if(unflipped.length){
setTimeout(()=>{
unflipped.forEach(card => { createShatter(card); card.style.opacity = '0'; });
setTimeout(checkDone, 720);
playTalentSfx('vanish');
unflipped.forEach(card => { createDissolve(card); card.style.opacity = '0'; });
setTimeout(checkDone, 820);
}, flipped.length ? 520 : 0);
}
}
function createShatter(card){
const N = 10;
function createDissolve(card){
const N = 14;
for(let i=0;i<N;i++){
const s = document.createElement('div'); s.className = 'shard';
const angle = (Math.PI*2*i/N) + (Math.random()*0.7-0.35);
const dist = 55 + Math.random()*90;
s.style.setProperty('--tx', (Math.cos(angle)*dist).toFixed(1) + 'px');
s.style.setProperty('--ty', (Math.sin(angle)*dist).toFixed(1) + 'px');
s.style.setProperty('--rot', (Math.random()*360-180).toFixed(0) + 'deg');
const s = document.createElement('div'); s.className = 'dust';
const ang = Math.random() * Math.PI * 2;
const dist = 30 + Math.random() * 70;
const dy = -(20 + Math.random() * 60); // 偏向上飘散
s.style.setProperty('--dx', (Math.cos(ang) * dist).toFixed(1) + 'px');
s.style.setProperty('--dy', (Math.sin(ang) * dist + dy).toFixed(1) + 'px');
card.appendChild(s);
}
card.classList.add('shatter');
card.classList.add('dissolve');
}
(function bindTalentDraw(){
const ov = document.getElementById('talentDraw');