/* ============================================================================= * SGLV PTW · Tab: 成就分析 (Achievements) * ----------------------------------------------------------------------------- * Phase 3 / T3.2新增能力。借鉴 steam-game-hub-2.0 `achievement-analytics.ts:106-390`。 * * 三个分析维度: * ① 稀有度分档 —— 按全球达成率分4 档(>=50% 常见 / >=20% 少见 / >=5% 稀有 / <5% 传说) * ② 成就时间线 —— 按解锁年份/月份聚合,看清成就节奏 * ③ 获取速度 —— 已解锁成就的解锁耗时分布(相对首个/最末解锁点) * * ⚠️ 口径纪律(重要): * `percent` 是**全球达成率**(越高越常见),不是稀有度。 * 分档必须**反向**解读:percent 越低越稀有。 * 分档阈值与主脚本 SGIS 的成就列表保持一致(50/20/5), * 避免同一游戏在两个入口显示不同稀有度。 * * 数据源(均经 PTW_HOST 桥接,Tab 不直连 SGLV_API): * getPlayerAchievements(appId) → { error?, achievements[], total, unlocked, gameName } * achievements[i] = { name, apiname, achieved, unlocktime } * getGlobalAchievements(appId)→ [{ name, percent }] 已按 percent 降序 * * 限流纪律:两个取数函数均**无内部限流**,必须经 sglvSmartPool 约束并发。 * ========================================================================== */ (function (root) { 'use strict'; // ============================================================================= // 样式(内联,随库走) // ----------------------------------------------------------------------------- // 命名空间 sglv-ptw-ach-*;配色沿用主脚本成就列表口径(50/20/5 分档), // 保证同一游戏在 SGIS 侧栏与 PTW 浮窗中稀有度读数一致。 // ========================================================================== const CSS = ` .sglv-ptw-ach{display:flex;flex-direction:column;gap:14px;} .sglv-ptw-ach-picker{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--sglv-border,rgba(102,192,244,.2));border-radius:10px;background:var(--sglv-panel-2,rgba(255,255,255,.02));} .sglv-ptw-ach-picker-label{font-size:12px;color:var(--sglv-text-dim,#94a3b8);flex-shrink:0;} .sglv-ptw-ach-select{flex:1;min-width:0;padding:7px 10px;border-radius:8px;border:1px solid var(--sglv-border,rgba(102,192,244,.25));background:var(--sglv-panel,#1a2733);color:var(--sglv-text,#f8fafc);font-size:13px;outline:none;} .sglv-ptw-ach-select:focus{border-color:#66c0f4;} .sglv-ptw-ach-body{display:flex;flex-direction:column;gap:14px;} /* 状态占位(加载中 / 无 Key / 失败) */ .sglv-ptw-ach-state{padding:36px 20px;text-align:center;color:var(--sglv-text-dim,#94a3b8);} .sglv-ptw-ach-state-icon{font-size:32px;margin-bottom:10px;} .sglv-ptw-ach-state-msg{font-size:14px;font-weight:600;color:#e8a33d;} .sglv-ptw-ach-state-sub{margin-top:8px;font-size:12px;opacity:.8;line-height:1.7;} /* HERO 摘要 */ .sglv-ptw-ach-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;} .sglv-ptw-ach-hero-item{padding:14px 16px;border:1px solid var(--sglv-border,rgba(102,192,244,.16));border-radius:12px;background:var(--sglv-panel-2,rgba(255,255,255,.025));text-align:center;} .sglv-ptw-ach-hero-val{font-size:26px;font-weight:700;color:var(--sglv-text,#f8fafc);line-height:1.2;} .sglv-ptw-ach-hero-total{font-size:14px;font-weight:400;color:var(--sglv-text-dim,#94a3b8);} .sglv-ptw-ach-hero-lbl{margin-top:4px;font-size:12px;color:var(--sglv-text-dim,#94a3b8);} .sglv-ptw-ach-hero-bar{margin:10px 0 4px;height:5px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;} .sglv-ptw-ach-hero-bar i{display:block;height:100%;border-radius:3px;} .sglv-ptw-ach-hero-sub{font-size:11px;color:var(--sglv-text-dim,#94a3b8);} /* 面板 */ .sglv-ptw-ach-panel{padding:14px 16px;border:1px solid var(--sglv-border,rgba(102,192,244,.16));border-radius:12px;background:var(--sglv-panel-2,rgba(255,255,255,.02));} .sglv-ptw-ach-panel-title{display:flex;align-items:center;gap:7px;margin-bottom:12px;font-size:13px;font-weight:600;color:var(--sglv-text,#e8eef4);} .sglv-ptw-ach-count{margin-left:auto;padding:1px 8px;border-radius:10px;background:rgba(102,192,244,.14);color:#66c0f4;font-size:11px;} .sglv-ptw-ach-note{padding:12px;border:1px dashed rgba(232,163,61,.4);border-radius:8px;color:#e8a33d;font-size:12px;text-align:center;} /* 稀有度分布条 */ .sglv-ptw-ach-tier-body{display:flex;flex-direction:column;gap:9px;} .sglv-ptw-ach-tier{display:grid;grid-template-columns:52px 1fr 58px;align-items:center;gap:10px;} .sglv-ptw-ach-tier-label{font-size:12px;color:var(--sglv-text-dim,#94a3b8);} .sglv-ptw-ach-tier-track{height:7px;border-radius:4px;background:rgba(255,255,255,.07);overflow:hidden;} .sglv-ptw-ach-tier-track i{display:block;height:100%;border-radius:4px;transition:width .35s ease;} .sglv-ptw-ach-tier-val{font-size:12px;text-align:right;color:var(--sglv-text-dim,#94a3b8);font-variant-numeric:tabular-nums;} /* 时间线 */ .sglv-ptw-ach-chart{margin:2px 0 10px;overflow-x:auto;} .sglv-ptw-ach-tl-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:6px;} .sglv-ptw-ach-tl-row{display:flex;align-items:center;justify-content:space-between;padding:5px 9px;border-radius:7px;background:rgba(255,255,255,.03);font-size:12px;color:var(--sglv-text-dim,#94a3b8);} .sglv-ptw-ach-tl-row b{color:#66c0f4;} /* 明细列表 */ .sglv-ptw-ach-list{display:flex;flex-direction:column;gap:2px;max-height:420px;overflow-y:auto;} .sglv-ptw-ach-item{display:grid;grid-template-columns:8px 1fr auto;align-items:center;gap:10px;padding:8px 8px;border-radius:8px;transition:background .15s;} .sglv-ptw-ach-item:hover{background:rgba(102,192,244,.07);} .sglv-ptw-ach-item.locked{opacity:.62;} .sglv-ptw-ach-dot{width:8px;height:8px;border-radius:50%;} .sglv-ptw-ach-info{min-width:0;} .sglv-ptw-ach-name{font-size:13px;color:var(--sglv-text,#e8eef4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .sglv-ptw-ach-meta{margin-top:2px;font-size:11px;color:var(--sglv-text-dim,#94a3b8);} .sglv-ptw-ach-tier-tag{padding:1px 7px;border:1px solid;border-radius:9px;font-size:10px;white-space:nowrap;} @media (max-width:820px){.sglv-ptw-ach-tier{grid-template-columns:48px 1fr 52px;}.sglv-ptw-ach-hero-val{font-size:22px;}} `; let cssInjected = false; function injectCSS() { if (cssInjected) return; cssInjected = true; try { const st = document.createElement('style'); st.id = 'sglv-ptw-ach-style'; st.textContent = CSS; document.head.appendChild(st); } catch (e) { /* 样式失败不阻断功能 */ } } // ---- 稀有度分档(percent = 全球达成率,越低越稀有)---- const RARITY_TIERS = [ { key: 'common', min: 50, color: '#22c55e', label: { zh: '常见', en: 'Common' } }, { key: 'uncommon', min: 20, color: '#84cc16', label: { zh: '少见', en: 'Uncommon' } }, { key: 'rare', min: 5, color: '#fbbf24', label: { zh: '稀有', en: 'Rare' } }, { key: 'legendary', min: 0, color: '#ef4444', label: { zh: '传说', en: 'Legendary' } }, ]; function rarityOf(percent) { const p = Number(percent) || 0; return RARITY_TIERS.find(t => p >= t.min) || RARITY_TIERS[RARITY_TIERS.length - 1]; } /** 稀有度积分:越稀有得分越高(0-100),用于「成就分」汇总 */ function rarityScore(percent) { const p = Math.max(0, Math.min(100, Number(percent) || 0)); // percent=0 → 100 分;percent=100 → 0 分 return Math.round((100 - p) * 10) / 10; } function fmtDate(ts) { if (!ts) return ''; try { const d = new Date(ts * 1000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } catch (e) { return ''; } } function fmtDay(ts) { if (!ts) return ''; try { const d = new Date(ts * 1000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } catch (e) { return ''; } } // ========================================================================== function mount(container, ctx) { const H = ctx.host; const T = H.T; const ICONS = H.ICONS; const isZh = H.isZh; const escHtml = H.escHtml; const Charts = H.SGLVCharts; injectCSS(); const root_ = document.createElement('div'); root_.className = 'sglv-ptw-ach'; container.appendChild(root_); // ---- 游戏选择器 ---- const games = (ctx.getGames() || []).slice() .sort((a, b) => (b.playtime || 0) - (a.playtime || 0)); if (!games.length) { root_.innerHTML = H.emptyHtml(T.ptNoData); return; } const picker = document.createElement('div'); picker.className = 'sglv-ptw-ach-picker'; picker.innerHTML = `