// ==UserScript==
// @name Steam Social Center
// @namespace https://github.com/SmallRob/steam-userscripts
// @version 3.2.0
// @description 社交中心界面。对照宿主 steam-activity-modern 4.18.0。3.2.0 游戏比对页重构:改为「一对一 / 多人交集 / 单游戏」三模式切换,选择即自动计算(移除冗余「开始比对」按钮);一对一首屏为韦恩图 + 占比条形图,多人交集为头像胶囊选人 + 每人共同占比条,单游戏为真正的时长条形图。3.1.0 好友排行页新增「排游戏」视图:按好友拥有数排游戏(CS2 有多少人拥有),可展开看具体是谁、点人进抽屉;范围=好友不含本人,只统计已索引好友并给出覆盖率;阈值可调(至少 2/3/5/10 人拥有),索引进行中节流刷新。3.0.0 新增「玩家画像」页:称号+稀有度 Hero、未开封率大数字、总体库存概览 KPI、五维雷达、时长阶梯与阶梯热力格、个人吃灰排行榜、被冷落的神作、最亏/最值、清灰倒计时(三级降级)。数据全部来自 Core 纯缓存聚合,打开页面不发请求;价格与评测是用户动作触发的后台补全任务。样式前缀 .ssc-,不改主脚本 .sf- 样式。
// @author SmallFork & SmallRob
// @license MIT
// ==/UserScript==
/**
* window.SteamSocialCenter.init(host) → boolean
* 四个页:深度大盘(默认)、好友数据、好友排行、游戏比对。
* 打开时只读 steam_social_db,不自动跑等级和游戏库索引。
*/
(function (root) {
'use strict';
const VERSION = '3.2.0';
const API_VERSION = 1;
const ROW = 72;
const REDUCED = typeof root.matchMedia === 'function' && root.matchMedia('(prefers-reduced-motion: reduce)').matches;
const DONUT_COLORS = ['#22d3ee', '#3b82f6', '#a78bfa', '#34d399', '#fbbf24', '#fb7185', '#f472b6', '#60a5fa'];
const OTHER_COLOR = '#475569';
const I = {
chart: '',
users: '',
user: '',
venn: '',
activity: '',
gamepad: '',
clock: '',
shield: '',
trophy: '',
flame: '',
pie: '',
lock: '',
};
const CSS = `
#ssc-root{position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;background:#0b0f16;color:#dbe4ee;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;opacity:0;visibility:hidden;transition:opacity .25s ease}
#ssc-root.ssc-open{opacity:1;visibility:visible}
#ssc-root::before,#ssc-root::after{content:"";position:fixed;border-radius:50%;filter:blur(90px);pointer-events:none;z-index:0}
#ssc-root::before{width:560px;height:560px;left:-180px;top:-200px;background:radial-gradient(circle,rgba(34,211,238,.14),transparent 65%);animation:ssc-drift 26s ease-in-out infinite alternate}
#ssc-root::after{width:680px;height:680px;right:-220px;bottom:-260px;background:radial-gradient(circle,rgba(59,130,246,.12),transparent 65%);animation:ssc-drift 32s ease-in-out infinite alternate-reverse}
@keyframes ssc-drift{from{transform:translate3d(0,0,0) scale(1)}to{transform:translate3d(70px,44px,0) scale(1.18)}}
#ssc-root>*{position:relative;z-index:1}
#ssc-root>.ssc-boot{position:absolute;z-index:10}
/* ---- 顶栏 ---- */
.ssc-top{display:flex;align-items:center;gap:14px;padding:0 22px;height:58px;background:rgba(12,17,26,.85);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-bottom:1px solid rgba(148,163,184,.12);flex-shrink:0}
.ssc-brand{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:800;color:#fff;letter-spacing:.5px;white-space:nowrap}
.ssc-brand-ico{width:30px;height:30px;border-radius:9px;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#22d3ee,#3b82f6);box-shadow:0 0 18px rgba(34,211,238,.35);flex-shrink:0}
.ssc-brand-ico svg{width:16px;height:16px;stroke:#fff;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
.ssc-tabs{display:flex;gap:4px;flex:1;min-width:0;overflow-x:auto;scrollbar-width:none}
.ssc-tabs::-webkit-scrollbar{display:none}
.ssc-tab{border:none;background:transparent;color:#93a4ba;border-radius:9px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer;transition:all .18s ease;white-space:nowrap;flex-shrink:0}
.ssc-tab:hover{color:#e2e8f0;background:rgba(148,163,184,.09)}
.ssc-tab.ssc-on{color:#fff;background:linear-gradient(135deg,rgba(34,211,238,.22),rgba(59,130,246,.22));box-shadow:inset 0 0 0 1px rgba(34,211,238,.35),0 0 16px rgba(34,211,238,.12)}
.ssc-x{width:34px;height:34px;border-radius:9px;border:1px solid rgba(148,163,184,.18);background:rgba(148,163,184,.08);color:#e2e8f0;cursor:pointer;font-size:16px;line-height:1;flex-shrink:0;transition:all .18s ease}
.ssc-x:hover{background:rgba(244,63,94,.22);border-color:rgba(244,63,94,.4);color:#fff}
/* ---- 页面容器 ---- */
.ssc-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:16px 22px 10px}
.ssc-page{flex:1;min-height:0;display:none;flex-direction:column;gap:14px}
.ssc-page.ssc-on{display:flex}
.ssc-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:rgba(34,211,238,.3) transparent;padding:2px 6px 6px 2px}
.ssc-scroll::-webkit-scrollbar{width:6px}
.ssc-scroll::-webkit-scrollbar-thumb{background:rgba(34,211,238,.28);border-radius:3px}
.ssc-scroll::-webkit-scrollbar-track{background:transparent}
/* ---- 大盘:铺满可视高度。右侧自上而下:热门游戏 / 好友数 / 热力图贴底 ---- */
.ssc-page[data-page=dash] .ssc-scroll{display:flex;flex-direction:column}
.ssc-dash-inner{flex:1 0 auto;width:100%;min-height:100%;margin:0;display:flex;flex-direction:column;gap:14px;box-sizing:border-box}
.ssc-dash-stage{flex:1 1 auto;display:grid;grid-template-columns:minmax(0,1.22fr) minmax(380px,.86fr);gap:14px;align-items:stretch;min-height:0}
.ssc-dash-main,.ssc-dash-side{display:flex;flex-direction:column;gap:14px;min-width:0;min-height:100%}
.ssc-dash-main>.ssc-card{flex:1 1 auto;min-height:0}
.ssc-dash-side>.ssc-card[data-card=hot]{flex:0 0 auto}
.ssc-dash-side>.ssc-card[data-card=fcount]{flex:1 1 auto;min-height:0}
.ssc-dash-side>.ssc-card[data-card=heat]{flex:0 0 auto;margin-top:auto}
.ssc-side-card{display:flex;flex-direction:column;min-height:0}
.ssc-side-body{flex:1;min-height:0;display:flex;flex-direction:column}
.ssc-page[data-page=dash] .ssc-tools{background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02));border:1px solid rgba(148,163,184,.12);border-radius:12px;padding:10px 14px}
/* ---- 通用卡片 / 工具 ---- */
.ssc-card{position:relative;background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02));border:1px solid rgba(148,163,184,.12);border-radius:14px;padding:14px 16px;min-width:0;overflow:hidden}
.ssc-card-hd{display:flex;align-items:center;gap:8px;margin-bottom:12px}
.ssc-card-hd svg{width:15px;height:15px;stroke:#22d3ee;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
.ssc-card-hd b{font-size:13.5px;font-weight:700;color:#f1f5f9;white-space:nowrap}
.ssc-card-hd .ssc-sub{margin-left:auto;font-size:11px;color:#7d8aa0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-tools{display:flex;gap:8px;align-items:center;flex-wrap:wrap;flex-shrink:0}
.ssc-tools input,.ssc-tools select,.ssc-field input,.ssc-field select{background:#131b27;color:#e2e8f0;border:1px solid rgba(148,163,184,.22);border-radius:9px;padding:7px 10px;font-size:13px;outline:none;transition:border-color .18s ease}
.ssc-tools input:focus,.ssc-field input:focus{border-color:rgba(34,211,238,.55)}
.ssc-tools input{min-width:220px;flex:1}
.ssc-btn{border:none;border-radius:9px;padding:7px 13px;font-size:12px;font-weight:700;cursor:pointer;color:#e2e8f0;background:rgba(148,163,184,.1);border:1px solid rgba(148,163,184,.16);transition:all .18s ease}
.ssc-btn:hover{background:rgba(148,163,184,.18)}
.ssc-btn.ssc-primary{background:linear-gradient(135deg,#22d3ee,#3b82f6);color:#fff;border-color:transparent;box-shadow:0 4px 14px rgba(34,211,238,.28)}
.ssc-btn.ssc-primary:hover{filter:brightness(1.12)}
.ssc-btn:disabled{opacity:.45;cursor:default}
.ssc-note{font-size:12px;color:#8fa1b8}
.ssc-banner{background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.32);color:#fbbf24;border-radius:10px;padding:9px 12px;font-size:12px;flex-shrink:0}
.ssc-empty{margin:6px 0;text-align:center;color:#7d8aa0;padding:26px 14px;font-size:12.5px;line-height:1.7;border:1px dashed rgba(148,163,184,.16);border-radius:12px}
/* ---- 入场动画 ---- */
.ssc-anim{animation:ssc-up .55s cubic-bezier(.22,1,.36,1) backwards}
@keyframes ssc-up{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
/* ---- 启动加载动画 ---- */
.ssc-boot{position:absolute;inset:0;z-index:10;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:24px;background:radial-gradient(ellipse at center,rgba(14,20,32,.98),rgba(8,11,17,.995));transition:opacity .5s ease,visibility .5s}
.ssc-boot.ssc-boot-hide{opacity:0;visibility:hidden}
.ssc-boot-orbit{position:relative;width:112px;height:112px}
.ssc-boot-orbit i{position:absolute;inset:0;border-radius:50%;border:2px solid transparent}
.ssc-boot-orbit i:nth-child(1){border-top-color:#22d3ee;border-right-color:rgba(34,211,238,.22);animation:ssc-rot 1.1s linear infinite}
.ssc-boot-orbit i:nth-child(2){inset:13px;border-bottom-color:#3b82f6;border-left-color:rgba(59,130,246,.22);animation:ssc-rot 1.6s linear infinite reverse}
.ssc-boot-orbit i:nth-child(3){inset:26px;border-top-color:#a78bfa;border-bottom-color:rgba(167,139,250,.25);animation:ssc-rot .9s linear infinite}
@keyframes ssc-rot{to{transform:rotate(360deg)}}
.ssc-boot-core{position:absolute;inset:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#22d3ee,#3b82f6);box-shadow:0 0 26px rgba(34,211,238,.55);animation:ssc-pulse 1.6s ease-in-out infinite}
.ssc-boot-core svg{width:20px;height:20px;stroke:#fff;stroke-width:2;fill:none;stroke-linecap:round}
@keyframes ssc-pulse{0%,100%{transform:scale(.84);opacity:.7}50%{transform:scale(1);opacity:1}}
.ssc-boot-text{font-size:15px;font-weight:800;letter-spacing:8px;text-indent:8px;color:#e2e8f0;animation:ssc-glow 1.6s ease-in-out infinite}
@keyframes ssc-glow{0%,100%{opacity:.5;text-shadow:none}50%{opacity:1;text-shadow:0 0 20px rgba(34,211,238,.6)}}
.ssc-boot-bar{width:220px;height:3px;border-radius:2px;background:rgba(148,163,184,.12);overflow:hidden}
.ssc-boot-bar i{display:block;height:100%;width:40%;border-radius:2px;background:linear-gradient(90deg,transparent,#22d3ee,#3b82f6,transparent);animation:ssc-scan 1.15s ease-in-out infinite}
@keyframes ssc-scan{0%{transform:translateX(-110%)}100%{transform:translateX(320%)}}
/* ---- Hero 汇总 ---- */
.ssc-hero{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:14px}
.ssc-hero-card{display:flex;align-items:center;gap:13px;padding:16px 17px;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}
.ssc-hero-card:hover{transform:translateY(-2px);border-color:rgba(34,211,238,.3);box-shadow:0 8px 24px rgba(0,0,0,.28)}
.ssc-hero-card::after{content:"";position:absolute;right:-34px;top:-34px;width:120px;height:120px;border-radius:50%;background:radial-gradient(circle,var(--g1),transparent 70%);opacity:.12;pointer-events:none}
.ssc-hero-ico{width:42px;height:42px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.ssc-hero-ico svg{width:20px;height:20px;stroke:#fff;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
.ssc-hero-mid{flex:1;min-width:0}
.ssc-hero-val{font-size:23px;font-weight:800;color:#fff;line-height:1.08;font-variant-numeric:tabular-nums;letter-spacing:.3px;white-space:nowrap}
.ssc-hero-val small{font-size:12px;font-weight:700;color:#8fa1b8;margin-left:2px}
.ssc-hero-lbl{font-size:11.5px;color:#c0cdde;margin-top:3px;font-weight:600}
.ssc-hero-sub{font-size:10px;color:#7d8aa0;margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-hero-bar{height:3px;border-radius:2px;background:rgba(148,163,184,.14);margin-top:7px;overflow:hidden}
.ssc-hero-bar i{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,var(--g1),var(--g2));width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
/* ---- 仪表 ---- */
.ssc-gauges{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}
.ssc-gauge{display:flex;flex-direction:column;align-items:center;padding:14px 12px 16px;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}
.ssc-gauge:hover{transform:translateY(-2px);border-color:rgba(34,211,238,.28);box-shadow:0 8px 24px rgba(0,0,0,.22)}
.ssc-gauge-wrap{position:relative;width:118px;height:100px}
.ssc-gauge-wrap svg{width:118px;height:118px;margin-top:-9px}
.ssc-g-track{fill:none;stroke:rgba(148,163,184,.13);stroke-width:9;stroke-linecap:round;stroke-dasharray:75 25}
.ssc-g-val{fill:none;stroke-width:9;stroke-linecap:round;stroke-dasharray:0 100;transition:stroke-dasharray 1s cubic-bezier(.22,1,.36,1);filter:drop-shadow(0 0 5px currentColor)}
.ssc-gauge-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding-bottom:12px}
.ssc-gauge-center b{font-size:19px;font-weight:800;color:#fff;font-variant-numeric:tabular-nums;line-height:1}
.ssc-gauge-center i{font-style:normal;font-size:10px;color:#7d8aa0;margin-top:2px}
.ssc-gauge-lbl{font-size:12px;font-weight:700;color:#dbe4ee;margin-top:2px}
.ssc-gauge-note{font-size:10px;color:#7d8aa0;margin-top:2px;text-align:center}
/* ---- 大盘网格 ---- */
.ssc-grid2{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:14px;align-items:start}
.ssc-grid2b{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,6fr);gap:14px;align-items:start}
/* ---- 最近好友游戏时长排行 ---- */
.ssc-rr-row{display:flex;gap:10px;padding:10px 11px;border-radius:12px;background:rgba(148,163,184,.045);border:1px solid rgba(148,163,184,.09);margin-bottom:8px;cursor:pointer;transition:all .18s ease}
.ssc-rr-row:last-child{margin-bottom:0}
.ssc-rr-row:hover{border-color:rgba(34,211,238,.4);background:rgba(34,211,238,.06);transform:translateY(-1px)}
.ssc-medal{width:26px;height:26px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;flex-shrink:0;align-self:flex-start;background:rgba(148,163,184,.13);color:#94a3b8}
.ssc-medal.m1{background:linear-gradient(135deg,#fbbf24,#d97706);color:#fff;box-shadow:0 0 12px rgba(251,191,36,.4)}
.ssc-medal.m2{background:linear-gradient(135deg,#cbd5e1,#64748b);color:#fff}
.ssc-medal.m3{background:linear-gradient(135deg,#d97706,#92400e);color:#fff}
.ssc-rr-av{width:38px;height:38px;border-radius:50%;object-fit:cover;border:2px solid rgba(34,211,238,.35);flex-shrink:0;align-self:flex-start;background:#1e293b}
.ssc-rr-main{flex:1;min-width:0}
.ssc-rr-top{display:flex;align-items:baseline;gap:6px;min-width:0}
.ssc-rr-name{font-size:13.5px;font-weight:700;color:#f1f5f9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-rr-hours{margin-left:auto;font-size:14px;font-weight:800;color:#fbbf24;font-variant-numeric:tabular-nums;flex-shrink:0}
.ssc-rr-unit{font-size:10px;color:#a16207;font-weight:700;flex-shrink:0}
.ssc-rr-game{display:flex;align-items:center;gap:8px;font-size:11px;color:#9fb0c5;margin-top:4px;min-width:0}
.ssc-rr-game img{width:16px;height:16px;border-radius:4px;flex-shrink:0;background:rgba(148,163,184,.1)}
.ssc-rr-game .n{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:1;min-width:0}
.ssc-rr-game .gb{flex:1;height:3px;border-radius:2px;background:rgba(148,163,184,.12);overflow:hidden;min-width:26px}
.ssc-rr-game .gb i{display:block;height:100%;background:linear-gradient(90deg,#22d3ee,#3b82f6);border-radius:2px;width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
.ssc-rr-game b{color:#7dd3fc;font-weight:700;flex-shrink:0;font-variant-numeric:tabular-nums}
.ssc-rr-bar{height:3px;border-radius:2px;background:rgba(148,163,184,.1);margin-top:7px;overflow:hidden}
.ssc-rr-bar i{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,#fbbf24,#f59e0b);width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
/* ---- 热门游戏环图 ---- */
.ssc-donut-wrap{display:flex;gap:14px;align-items:center;flex-wrap:wrap}
.ssc-donut{position:relative;width:186px;height:186px;flex-shrink:0;margin:0 auto}
.ssc-donut svg{width:100%;height:100%;display:block}
.ssc-donut .ssc-seg{fill:none;stroke-width:14;cursor:pointer;transition:stroke-dasharray 1s cubic-bezier(.22,1,.36,1),stroke-dashoffset 1s cubic-bezier(.22,1,.36,1),opacity .18s ease,stroke-width .18s ease}
.ssc-donut .ssc-seg.ssc-dim{opacity:.25}
.ssc-donut .ssc-seg.ssc-hot{stroke-width:17}
.ssc-donut-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;pointer-events:none;padding:0 26px}
.ssc-donut-center b{font-size:22px;font-weight:800;color:#fff;font-variant-numeric:tabular-nums;line-height:1.1}
.ssc-donut-center span{font-size:10px;color:#8fa1b8;margin-top:3px;line-height:1.4}
.ssc-legend{flex:1;min-width:210px;display:flex;flex-direction:column;gap:5px}
.ssc-lg-row{display:flex;align-items:center;gap:8px;font-size:12px;padding:5px 8px;border-radius:8px;cursor:pointer;transition:background .15s ease}
.ssc-lg-row:hover,.ssc-lg-row.ssc-hot{background:rgba(148,163,184,.09)}
.ssc-lg-dot{width:9px;height:9px;border-radius:3px;flex-shrink:0}
.ssc-lg-ico{width:18px;height:18px;border-radius:4px;flex-shrink:0;object-fit:cover;background:rgba(148,163,184,.12)}
.ssc-lg-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dbe4ee}
.ssc-lg-meta{color:#7d8aa0;font-size:10.5px;flex-shrink:0;font-variant-numeric:tabular-nums;white-space:nowrap}
.ssc-lg-pct{width:44px;text-align:right;font-weight:700;color:#e2e8f0;flex-shrink:0;font-variant-numeric:tabular-nums}
/* ---- 热力图 ---- */
.ssc-heat{display:flex;flex-direction:column;gap:3px;min-width:0}
.ssc-heat-scroll{overflow-x:auto;scrollbar-width:thin}
.ssc-heat-days{display:grid;grid-template-columns:104px repeat(14,minmax(10px,1fr));gap:3px;font-size:9.5px;color:#64748b;padding-bottom:2px}
.ssc-heat-days span{text-align:center;white-space:nowrap}
.ssc-heat-row{display:grid;grid-template-columns:104px repeat(14,minmax(10px,1fr));gap:3px;align-items:center;border-radius:6px;cursor:pointer}
.ssc-heat-row:hover{background:rgba(148,163,184,.05)}
.ssc-heat-name{display:flex;align-items:center;gap:6px;font-size:11px;color:#c9d5e3;min-width:0;padding-right:4px}
.ssc-heat-name img{width:18px;height:18px;border-radius:50%;flex-shrink:0;background:#1e293b}
.ssc-heat-name span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-cell{aspect-ratio:1;max-height:16px;width:100%;border-radius:3px;background:rgba(148,163,184,.08)}
.ssc-cell.v1{background:#155e75}.ssc-cell.v2{background:#0e7490}.ssc-cell.v3{background:#06b6d4}.ssc-cell.v4{background:#22d3ee;box-shadow:0 0 5px rgba(34,211,238,.35)}.ssc-cell.v5{background:#a5f3fc;box-shadow:0 0 8px rgba(34,211,238,.55)}
.ssc-heat-legend{display:flex;align-items:center;gap:4px;justify-content:flex-end;font-size:10px;color:#64748b;margin-top:10px}
.ssc-heat-legend i{width:10px;height:10px;border-radius:3px;display:inline-block;background:rgba(148,163,184,.08)}
.ssc-heat-legend i.v1{background:#155e75}.ssc-heat-legend i.v2{background:#0e7490}.ssc-heat-legend i.v3{background:#06b6d4}.ssc-heat-legend i.v4{background:#22d3ee}.ssc-heat-legend i.v5{background:#a5f3fc}
/* ---- 好友数排行条形 ---- */
.ssc-fr-row{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:9px;font-size:12px;cursor:pointer;transition:background .15s ease}
.ssc-fr-row:hover{background:rgba(148,163,184,.08)}
.ssc-fr-no{width:22px;color:#7d8aa0;font-weight:700;text-align:center;flex-shrink:0;font-variant-numeric:tabular-nums}
.ssc-fr-av{width:26px;height:26px;border-radius:50%;object-fit:cover;flex-shrink:0;background:#1e293b}
.ssc-fr-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dbe4ee}
.ssc-fr-bar{width:34%;height:7px;border-radius:4px;background:rgba(148,163,184,.1);overflow:hidden;flex-shrink:0}
.ssc-fr-bar i{display:block;height:100%;border-radius:4px;background:linear-gradient(90deg,#22d3ee,#3b82f6);box-shadow:0 0 8px rgba(34,211,238,.35);width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
.ssc-fr-val{width:56px;text-align:right;font-weight:700;color:#67e8f9;flex-shrink:0;font-variant-numeric:tabular-nums}
.ssc-fr-skel{display:flex;align-items:center;gap:9px;padding:7px 8px}
.ssc-fr-skel i{display:block;height:10px;border-radius:6px;background:linear-gradient(90deg,rgba(148,163,184,.08),rgba(148,163,184,.2),rgba(148,163,184,.08));background-size:220% 100%;animation:ssc-shimmer 1.15s ease-in-out infinite}
@keyframes ssc-shimmer{0%{background-position:120% 0}100%{background-position:-120% 0}}
.ssc-fr-wait{padding:8px 4px 2px;font-size:11.5px;color:#8fa1b8;line-height:1.5}
/* ---- 好友数据页 ---- */
.ssc-kpis{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px}
.ssc-kpi{background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02));border:1px solid rgba(148,163,184,.12);border-radius:10px;padding:8px 10px}
.ssc-kpi b{display:block;font-size:17px;color:#fff;font-weight:800;font-variant-numeric:tabular-nums}
.ssc-kpi span{font-size:10.5px;color:#8fa1b8}
.ssc-list{position:relative;flex:1;min-height:0;overflow:auto;border:1px solid rgba(148,163,184,.1);border-radius:12px;background:rgba(0,0,0,.18);scrollbar-width:thin;scrollbar-color:rgba(34,211,238,.3) transparent}
.ssc-list::-webkit-scrollbar{width:6px}
.ssc-list::-webkit-scrollbar-thumb{background:rgba(34,211,238,.28);border-radius:3px}
.ssc-spacer{width:1px}
.ssc-win{position:absolute;left:0;right:0;top:0}
.ssc-row{display:flex;align-items:center;gap:10px;height:64px;margin:0 8px 8px;padding:0 10px;box-sizing:border-box;background:rgba(148,163,184,.045);border:1px solid rgba(148,163,184,.08);border-radius:11px;color:inherit;width:calc(100% - 16px);text-align:left;cursor:pointer;transition:all .18s ease}
.ssc-row:hover{border-color:rgba(34,211,238,.45);background:rgba(34,211,238,.07)}
.ssc-av{width:40px;height:40px;border-radius:50%;object-fit:cover;background:#1e293b;flex-shrink:0}
.ssc-mid{flex:1;min-width:0}
.ssc-name{font-size:14px;font-weight:700;color:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-sub{font-size:12px;color:#8fa1b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-meta{display:flex;gap:6px;flex-shrink:0;font-size:11px;color:#cbd5e1;flex-wrap:wrap;justify-content:flex-end}
.ssc-pill{background:rgba(148,163,184,.1);border-radius:999px;padding:3px 7px;white-space:nowrap}
.ssc-pill.ssc-new{background:rgba(251,113,133,.16);color:#fda4af}
.ssc-pill.ssc-vac{background:rgba(244,63,94,.2);color:#fecaca}
.ssc-pill.ssc-game{background:rgba(139,92,246,.2);color:#ddd6fe}
.ssc-pill.ssc-lv{color:#fbbf24;background:rgba(251,191,36,.1)}
.ssc-empty{margin:auto;text-align:center;color:#8fa1b8;padding:48px 16px;font-size:14px;line-height:1.6}
/* ---- 排行页 ---- */
.ssc-rank-list,.ssc-cmp{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:6px;scrollbar-width:thin;scrollbar-color:rgba(34,211,238,.3) transparent}
.ssc-rank-row{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:10px;background:rgba(148,163,184,.045);border:1px solid rgba(148,163,184,.08)}
.ssc-rank-row .ssc-rk-name{font-size:13px;font-weight:600;color:#e2e8f0;width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0}
.ssc-rank-row .ssc-fr-bar{flex:1;width:auto}
.ssc-rank-row .ssc-rk-val{font-size:12px;font-weight:700;color:#7dd3fc;width:88px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-rank-row button{margin-left:auto;flex-shrink:0}
/* ---- 比对页样式见下方「比对页:模式化」区块(旧三栏布局的样式已清掉)----
/* ---- 玩家画像页 ---- */
.ssc-pf-wrap{display:flex;flex-direction:column;gap:14px}
.ssc-pf-hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:14px;align-items:stretch}
.ssc-pf-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px;align-items:start}
.ssc-pf-col{display:flex;flex-direction:column;gap:14px;min-width:0}
.ssc-pf-card{background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02));border:1px solid rgba(148,163,184,.12);border-radius:14px;padding:14px 16px;min-width:0}
.ssc-pf-hd{display:flex;align-items:center;gap:9px;margin:0 0 12px;min-width:0}
.ssc-pf-hd svg{width:16px;height:16px;stroke:#22d3ee;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}
.ssc-pf-hd b{font-size:14px;font-weight:800;color:#f1f5f9;white-space:nowrap}
.ssc-pf-hd span{font-size:10.5px;color:#7d8aa0;margin-left:auto;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-pf-titlecard{position:relative;overflow:hidden;background:linear-gradient(140deg,rgba(34,211,238,.14),rgba(59,130,246,.07) 45%,rgba(148,163,184,.03));border:1px solid rgba(34,211,238,.28);border-radius:14px;padding:16px 18px}
.ssc-pf-titlecard::after{content:"";position:absolute;right:-60px;top:-70px;width:220px;height:220px;border-radius:50%;background:radial-gradient(circle,rgba(34,211,238,.18),transparent 68%);pointer-events:none}
.ssc-pf-rank{display:flex;align-items:center;gap:10px;position:relative;z-index:1}
.ssc-pf-rank i{font-style:normal;font-size:34px;line-height:1;flex-shrink:0}
.ssc-pf-rank .t{flex:1;min-width:0}
.ssc-pf-rank .t b{display:block;font-size:22px;font-weight:900;color:#fff;letter-spacing:.5px}
.ssc-pf-rank .t span{display:block;font-size:11.5px;color:#a8c4dc;margin-top:3px}
.ssc-pf-rare{flex-shrink:0;padding:5px 11px;border-radius:10px;font-size:12px;font-weight:900;letter-spacing:1px}
.ssc-pf-rare.SSR{background:linear-gradient(135deg,#f43f5e,#fb923c);color:#fff;box-shadow:0 0 18px rgba(244,63,94,.42)}
.ssc-pf-rare.SR{background:linear-gradient(135deg,#a78bfa,#f472b6);color:#fff;box-shadow:0 0 16px rgba(167,139,250,.38)}
.ssc-pf-rare.R{background:linear-gradient(135deg,#22d3ee,#3b82f6);color:#fff}
.ssc-pf-rare.N{background:rgba(148,163,184,.16);color:#cbd5e1}
.ssc-pf-quote{margin-top:11px;font-size:12px;color:#9fb8cf;font-style:italic;position:relative;z-index:1}
.ssc-pf-big{display:flex;align-items:flex-end;gap:12px;margin-bottom:12px}
.ssc-pf-big b{font-size:46px;font-weight:900;color:#fff;line-height:.92;font-variant-numeric:tabular-nums;flex-shrink:0}
.ssc-pf-big b i{font-style:normal;font-size:24px;color:#fb7185}
.ssc-pf-big .d{flex:1;min-width:0;padding-bottom:3px}
.ssc-pf-big .d span{display:block;font-size:12px;color:#cbd5e1;font-weight:700}
.ssc-pf-big .d em{display:block;font-style:normal;font-size:11px;color:#8fa1b8;margin-top:3px}
.ssc-pf-pills{display:flex;flex-wrap:wrap;gap:6px}
.ssc-pf-pill{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:999px;border:1px solid rgba(148,163,184,.16);background:rgba(148,163,184,.06);font-size:11.5px;color:#cbd5e1}
.ssc-pf-pill b{color:#fff;font-weight:800;font-variant-numeric:tabular-nums}
.ssc-pf-pill.na{border-color:rgba(148,163,184,.1);color:#64748b}
.ssc-pf-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
.ssc-pf-kpi{background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02));border:1px solid rgba(148,163,184,.12);border-radius:10px;padding:9px 10px;min-width:0}
.ssc-pf-kpi b{display:block;font-size:16px;color:#fff;font-weight:800;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ssc-pf-kpi span{display:block;font-size:10px;color:#8fa1b8;margin-top:2px}
.ssc-pf-kpi.na b{color:#64748b}
.ssc-pf-radar{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.ssc-pf-radar svg{width:232px;height:232px;flex-shrink:0;margin:0 auto;display:block}
.ssc-pf-rd-ring{fill:none;stroke:rgba(148,163,184,.16);stroke-width:1}
.ssc-pf-rd-axis{stroke:rgba(148,163,184,.14);stroke-width:1}
.ssc-pf-rd-data{fill:rgba(34,211,238,.18);stroke:#22d3ee;stroke-width:2;stroke-linejoin:round}
.ssc-pf-rd-dot{fill:#22d3ee}
.ssc-pf-rd-lbl{font-size:10.5px;fill:#93a4ba}
.ssc-pf-rd-val{font-size:10.5px;fill:#e2e8f0;font-weight:700}
.ssc-pf-rd-na{font-size:10.5px;fill:#64748b}
.ssc-pf-dims{flex:1;min-width:200px;display:flex;flex-direction:column;gap:6px}
.ssc-pf-dim{display:flex;align-items:center;gap:8px;font-size:11.5px}
.ssc-pf-dim .k{width:58px;flex-shrink:0;color:#a8b6c8}
.ssc-pf-dim .b{flex:1;height:6px;border-radius:4px;background:rgba(148,163,184,.12);overflow:hidden;min-width:30px}
.ssc-pf-dim .b i{display:block;height:100%;border-radius:4px;background:linear-gradient(90deg,#22d3ee,#3b82f6);width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
.ssc-pf-dim .v{width:42px;text-align:right;flex-shrink:0;font-weight:700;color:#e2e8f0;font-variant-numeric:tabular-nums}
.ssc-pf-dim .tag{font-size:10px;color:#7dd3fc;width:64px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-pf-dim.na .v{color:#64748b}
.ssc-pf-ladder{display:flex;flex-direction:column;gap:6px}
.ssc-pf-lrow{display:flex;align-items:center;gap:9px;font-size:11.5px}
.ssc-pf-lrow .k{width:74px;flex-shrink:0;color:#cbd5e1;display:flex;align-items:center;gap:4px}
.ssc-pf-lrow .r{width:54px;flex-shrink:0;color:#7d8aa0;font-variant-numeric:tabular-nums;font-size:10.5px}
.ssc-pf-lrow .b{flex:1;height:9px;border-radius:5px;background:rgba(148,163,184,.1);overflow:hidden;min-width:24px}
.ssc-pf-lrow .b i{display:block;height:100%;border-radius:5px;width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
.ssc-pf-lrow .v{width:72px;text-align:right;flex-shrink:0;font-weight:700;color:#e2e8f0;font-variant-numeric:tabular-nums}
.ssc-pf-lrow .c{width:56px;text-align:right;flex-shrink:0;color:#8fa1b8;font-variant-numeric:tabular-nums;font-size:10.5px}
.ssc-pf-heatgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(11px,1fr));gap:3px}
.ssc-pf-cell{aspect-ratio:1;border-radius:3px;background:rgba(148,163,184,.08)}
.ssc-pf-cell.t0{background:#29384c}.ssc-pf-cell.t1{background:#35485f}.ssc-pf-cell.t2{background:#3f5a76}.ssc-pf-cell.t3{background:#4a6d8d}.ssc-pf-cell.t4{background:#fbbf24;box-shadow:0 0 6px rgba(251,191,36,.4)}.ssc-pf-cell.t5{background:#f97316;box-shadow:0 0 7px rgba(249,115,22,.45)}.ssc-pf-cell.t6{background:#ef4444;box-shadow:0 0 9px rgba(239,68,68,.5)}
.ssc-pf-heatlegend{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;font-size:10px;color:#8fa1b8}
.ssc-pf-heatlegend span{display:inline-flex;align-items:center;gap:4px}
.ssc-pf-heatlegend i{width:9px;height:9px;border-radius:2px;display:inline-block}
.ssc-pf-act{display:grid;grid-template-columns:repeat(14,minmax(0,1fr));gap:4px}
.ssc-pf-actc{aspect-ratio:1;border-radius:4px;background:rgba(148,163,184,.08)}
.ssc-pf-actd{display:grid;grid-template-columns:repeat(14,minmax(0,1fr));gap:4px;font-size:9px;color:#64748b;margin-bottom:4px}
.ssc-pf-actd span{text-align:center;white-space:nowrap;overflow:hidden}
.ssc-pf-dust{display:flex;flex-direction:column}
.ssc-pf-drow{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:9px;border-bottom:1px solid rgba(148,163,184,.07);cursor:pointer;transition:background .15s ease}
.ssc-pf-drow:last-child{border-bottom:none}
.ssc-pf-drow:hover{background:rgba(34,211,238,.06)}
.ssc-pf-dno{width:24px;flex-shrink:0;font-weight:800;font-size:12.5px;text-align:center;font-variant-numeric:tabular-nums;color:#94a3b8}
.ssc-pf-drow:nth-child(1) .ssc-pf-dno,.ssc-pf-drow:nth-child(2) .ssc-pf-dno,.ssc-pf-drow:nth-child(3) .ssc-pf-dno{color:#fbbf24}
.ssc-pf-dico{width:64px;height:24px;border-radius:4px;object-fit:cover;flex-shrink:0;background:#16202e}
.ssc-pf-dname{flex:1;min-width:0;font-size:12.5px;color:#e2e8f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-pf-dmeta{flex-shrink:0;font-size:11px;color:#8fa1b8;display:flex;align-items:center;gap:6px;white-space:nowrap}
.ssc-pf-dmeta b{color:#fbbf24;font-weight:800;font-variant-numeric:tabular-nums}
.ssc-pf-dmeta .rv{color:#6ee7b7}
.ssc-pf-neg{display:flex;flex-direction:column;gap:5px}
.ssc-pf-nrow{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:9px;background:rgba(148,163,184,.04);border:1px solid rgba(148,163,184,.07);cursor:pointer;transition:all .15s ease}
.ssc-pf-nrow:hover{border-color:rgba(52,211,153,.35);background:rgba(52,211,153,.05)}
.ssc-pf-nrow .t{flex:1;min-width:0}
.ssc-pf-nrow .t b{display:block;font-size:12.5px;color:#f1f5f9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-pf-nrow .t span{display:block;font-size:10px;color:#8fa1b8;margin-top:2px}
.ssc-pf-rate{padding:3px 7px;border-radius:7px;font-size:11px;font-weight:800;background:rgba(52,211,153,.16);color:#6ee7b7;flex-shrink:0}
.ssc-pf-eta{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.ssc-pf-eta .n{font-size:38px;font-weight:900;color:#fff;line-height:1;font-variant-numeric:tabular-nums}
.ssc-pf-eta .n i{font-style:normal;font-size:15px;color:#8fa1b8;font-weight:700;margin-left:4px}
.ssc-pf-eta .t{flex:1;min-width:190px;font-size:11.5px;color:#8fa1b8;line-height:1.6}
.ssc-pf-eta .t b{color:#e2e8f0}
.ssc-pf-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.ssc-pf-mini{display:flex;flex-direction:column;gap:3px;min-width:0}
.ssc-pf-mini .h{font-size:10.5px;color:#7d8aa0;font-weight:700;margin-bottom:2px}
.ssc-pf-mini .r{display:flex;align-items:center;gap:6px;font-size:11px;padding:3px 0}
.ssc-pf-mini .r .n{flex:1;min-width:0;color:#cbd5e1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-pf-mini .r .v{flex-shrink:0;font-weight:700;color:#fbbf24;font-variant-numeric:tabular-nums}
.ssc-pf-note{font-size:10.5px;color:#64748b;line-height:1.6;margin-top:10px;padding-top:9px;border-top:1px dashed rgba(148,163,184,.12)}
.ssc-pf-warn{background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.26);border-radius:10px;padding:9px 11px;font-size:11px;color:#fcd34d;line-height:1.6}
.ssc-pf-na{background:rgba(148,163,184,.06);border:1px dashed rgba(148,163,184,.18);border-radius:10px;padding:9px 11px;font-size:11px;color:#7d8aa0;line-height:1.6}
/* ---- 排行页:游戏拥有数(按游戏排好友)---- */
.ssc-tools[hidden]{display:none!important}
.ssc-rank-subtools{gap:4px;flex-wrap:wrap}
.ssc-grow{border-radius:11px;border:1px solid rgba(148,163,184,.08);background:rgba(148,163,184,.045);margin-bottom:6px;overflow:hidden}
.ssc-grow-row{display:flex;align-items:center;gap:10px;padding:7px 10px}
.ssc-grow-ico{width:52px;height:20px;border-radius:4px;object-fit:cover;flex-shrink:0;background:#16202e}
.ssc-grow-name{flex:1;min-width:0;font-size:13px;font-weight:600;color:#e2e8f0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-grow-val{font-size:13px;font-weight:800;color:#67e8f9;flex-shrink:0;font-variant-numeric:tabular-nums;white-space:nowrap}
.ssc-grow-pct{font-size:10.5px;color:#8fa1b8;flex-shrink:0;width:48px;text-align:right;font-variant-numeric:tabular-nums}
.ssc-grow-faces{display:flex;gap:2px;flex-shrink:0;align-items:center}
.ssc-grow-faces img{width:18px;height:18px;border-radius:50%;background:#1e293b;border:1px solid rgba(148,163,184,.2)}
.ssc-grow-owners{border-top:1px solid rgba(148,163,184,.1);padding:7px 10px 9px;background:rgba(0,0,0,.18)}
.ssc-grow-owner{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px;cursor:pointer;border-radius:7px;transition:background .15s ease}
.ssc-grow-owner:hover{background:rgba(34,211,238,.07)}
.ssc-grow-owner img{width:22px;height:22px;border-radius:50%;background:#1e293b;flex-shrink:0}
.ssc-grow-owner .n{flex:1;min-width:0;color:#cbd5e1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-grow-owner .h{flex-shrink:0;color:#7dd3fc;font-weight:700;font-variant-numeric:tabular-nums}
.ssc-grow-owner .h.none{color:#fb7185}
.ssc-grow-sum{font-size:10.5px;color:#7d8aa0;padding:2px 0 6px}
/* ---- 比对页:模式化(替代旧的三栏并排)---- */
.ssc-page[data-page=compare]{min-height:0;overflow:hidden;gap:10px}
.ssc-cmp-stage[hidden]{display:none!important}
.ssc-cmp-stage{display:flex;flex-direction:column;gap:12px;min-height:0;flex:1}
.ssc-cmp-bar{display:flex;align-items:center;gap:10px;flex-shrink:0}
.ssc-cmp-bar select,.ssc-cmp-bar input{background:#131b27;color:#e2e8f0;border:1px solid rgba(148,163,184,.22);border-radius:9px;padding:7px 10px;font-size:13px;outline:none;transition:border-color .18s ease}
.ssc-cmp-bar select:focus,.ssc-cmp-bar input:focus{border-color:rgba(34,211,238,.55)}
.ssc-cmp-bar select{flex:0 1 300px;min-width:200px}
.ssc-cmp-bar input{flex:1;min-width:200px;max-width:420px}
.ssc-cmp-out{display:flex;flex-direction:column;gap:12px;min-height:0;flex:1;overflow:auto;scrollbar-width:thin;scrollbar-color:rgba(34,211,238,.3) transparent;padding-right:2px}
.ssc-cmp-out::-webkit-scrollbar{width:6px}
.ssc-cmp-out::-webkit-scrollbar-thumb{background:rgba(148,163,184,.28);border-radius:3px}
/* ---- 对比总览:韦恩图 + 占比 ---- */
.ssc-cmp-overview{display:flex;gap:20px;align-items:center;flex-wrap:wrap;padding:16px 18px;border-radius:14px;border:1px solid rgba(148,163,184,.12);background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02))}
.ssc-venn{flex-shrink:0;width:300px;height:172px;margin:0 auto}
.ssc-venn-l{fill:rgba(59,130,246,.20);stroke:rgba(59,130,246,.55);stroke-width:1.5}
.ssc-venn-r{fill:rgba(244,63,94,.20);stroke:rgba(244,63,94,.55);stroke-width:1.5}
.ssc-venn-shared{fill:rgba(16,185,129,.40);stroke:rgba(16,185,129,.75);stroke-width:1.5}
.ssc-venn-n{font-size:19px;font-weight:800;fill:#e2e8f0;text-anchor:middle;font-variant-numeric:tabular-nums}
.ssc-venn-n.ssc-venn-mid{fill:#6ee7b7}
.ssc-venn-lb{font-size:10.5px;fill:#8fa1b8;text-anchor:middle}
.ssc-cmp-ovside{flex:1;min-width:220px;display:flex;flex-direction:column;gap:10px}
.ssc-cmp-ovrow{display:grid;grid-template-columns:84px minmax(80px,300px) 66px;gap:10px;align-items:center;font-size:12px}
.ssc-cmp-ovrow .k{color:#a8b6c8;white-space:nowrap}
.ssc-cmp-ovrow .t{height:9px;border-radius:5px;background:rgba(148,163,184,.12);overflow:hidden}
.ssc-cmp-ovrow .t i{display:block;height:100%;border-radius:5px;width:0;transition:width .9s cubic-bezier(.22,1,.36,1)}
.ssc-cmp-ovrow .v{text-align:right;font-weight:800;color:#e2e8f0;font-variant-numeric:tabular-nums}
/* ---- 结果分段(共同 / 我独有 / TA 独有)---- */
.ssc-cmp-segs{display:flex;gap:4px;flex-shrink:0;flex-wrap:wrap}
.ssc-cmp-seg{border:1px solid rgba(148,163,184,.14);background:rgba(148,163,184,.05);border-radius:9px;padding:5px 11px;font-size:11.5px;color:#cbd5e1;cursor:pointer;transition:all .18s ease;white-space:nowrap}
.ssc-cmp-seg:hover{border-color:rgba(34,211,238,.4);color:#e2e8f0}
.ssc-cmp-seg.ssc-on{color:#fff;border-color:rgba(34,211,238,.5);background:linear-gradient(135deg,rgba(34,211,238,.18),rgba(59,130,246,.18))}
.ssc-cmp-seg b{font-weight:800;font-variant-numeric:tabular-nums}
.ssc-cmp-seg.g b{color:#6ee7b7}
.ssc-cmp-seg.b b{color:#7dd3fc}
.ssc-cmp-seg.r b{color:#fda4af}
/* ---- bench 条形(对比行)---- */
.ssc-cmp-bench{display:grid;grid-template-columns:86px 1fr 60px;gap:10px;align-items:center;padding:3px 0;font-size:11px}
.ssc-cmp-bench .k{color:#8fa1b8;display:flex;align-items:center;gap:4px;overflow:hidden;white-space:nowrap}
.ssc-cmp-bench .k img{width:16px;height:16px;border-radius:50%;flex-shrink:0;background:#1e293b}
.ssc-cmp-bench .t{height:8px;border-radius:4px;background:rgba(148,163,184,.1);overflow:hidden;min-width:30px}
.ssc-cmp-bench .t i{display:block;height:100%;border-radius:4px;width:0;transition:width .8s cubic-bezier(.22,1,.36,1)}
.ssc-cmp-bench .t i.c1{background:linear-gradient(90deg,#22d3ee,#3b82f6)}
.ssc-cmp-bench .t i.c2{background:linear-gradient(90deg,#a78bfa,#f472b6)}
.ssc-cmp-bench .t i.c3{background:linear-gradient(90deg,#34d399,#22d3ee)}
.ssc-cmp-bench .t i.c4{background:linear-gradient(90deg,#fbbf24,#fb7185)}
.ssc-cmp-bench .v{text-align:right;color:#cbd5e1;font-variant-numeric:tabular-nums;font-weight:700}
.ssc-cmp-bench .v.zero{color:#fb7185;font-weight:600}
/* ---- 游戏行 ---- */
.ssc-cmp-row{display:flex;align-items:flex-start;gap:10px;padding:8px 10px;border-radius:11px;background:rgba(148,163,184,.045);border:1px solid rgba(148,163,184,.08);transition:all .18s ease;text-decoration:none!important;color:inherit!important;cursor:pointer}
.ssc-cmp-row:hover{border-color:rgba(34,211,238,.4);background:rgba(34,211,238,.06);transform:translateY(-1px)}
.ssc-cmp-row .ssc-cmp-gico{margin-top:1px}
.ssc-cmp-gico,.ssc-cmp-gico-big,.ssc-cmp-sug img,.ssc-cmp-sug .ssc-cmp-gico{display:block;object-fit:cover;object-position:center;flex-shrink:0;background:#16202e}
.ssc-cmp-gico{width:72px;height:27px;border-radius:4px}
.ssc-cmp-gico-big{width:150px;height:56px;border-radius:7px}
.ssc-cmp-main{flex:1;min-width:0}
.ssc-cmp-name{font-size:12.5px;font-weight:700;color:#f1f5f9;margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.45}
.ssc-cmp-bars{display:flex;flex-direction:column;gap:2px}
/* ---- 多人交集:人物选择器 ---- */
.ssc-cmp-picker{display:flex;flex-wrap:wrap;gap:6px;flex-shrink:0;max-height:108px;overflow:auto;scrollbar-width:thin;padding:2px}
.ssc-cmp-pick{display:inline-flex;align-items:center;gap:6px;padding:4px 10px 4px 4px;border-radius:999px;border:1px solid rgba(148,163,184,.16);background:rgba(148,163,184,.05);font-size:11.5px;color:#cbd5e1;cursor:pointer;transition:all .18s ease;max-width:190px}
.ssc-cmp-pick:hover{border-color:rgba(34,211,238,.4);color:#e2e8f0}
.ssc-cmp-pick img{width:20px;height:20px;border-radius:50%;flex-shrink:0;background:#1e293b}
.ssc-cmp-pick span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-cmp-pick.ssc-on{border-color:rgba(34,211,238,.55);background:linear-gradient(135deg,rgba(34,211,238,.18),rgba(59,130,246,.16));color:#fff;box-shadow:0 0 12px rgba(34,211,238,.16)}
.ssc-cmp-pick.ssc-on img{box-shadow:0 0 0 2px rgba(34,211,238,.5)}
.ssc-cmp-picker .ssc-note{flex-basis:100%}
/* ---- 单游戏:搜索建议 ---- */
.ssc-cmp-sug{display:flex;flex-wrap:wrap;gap:6px;flex-shrink:0;max-height:76px;overflow:auto;scrollbar-width:thin}
.ssc-cmp-sug .ssc-btn{display:flex;align-items:center;gap:7px;justify-content:flex-start;padding:4px 8px;font-weight:600;text-align:left;min-width:0}
.ssc-cmp-sug .ssc-btn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px}
.ssc-cmp-sug .ssc-btn b{margin-left:auto;color:#7d8aa0;font-size:10px}
.ssc-cmp-sug .ssc-cmp-gico{width:56px;height:21px}
/* ---- 单游戏:时长条形图 ---- */
.ssc-cmp-chart{display:flex;flex-direction:column;gap:7px;padding:14px 16px;border-radius:14px;border:1px solid rgba(148,163,184,.12);background:linear-gradient(160deg,rgba(148,163,184,.07),rgba(148,163,184,.02))}
.ssc-cmp-chart-row{display:grid;grid-template-columns:150px 1fr 68px;gap:10px;align-items:center;font-size:12px;cursor:pointer;border-radius:8px;padding:2px 4px;transition:background .15s ease}
.ssc-cmp-chart-row:hover{background:rgba(34,211,238,.06)}
.ssc-cmp-chart-row .k{display:flex;align-items:center;gap:7px;color:#cbd5e1;min-width:0}
.ssc-cmp-chart-row .k img{width:22px;height:22px;border-radius:50%;flex-shrink:0;background:#1e293b}
.ssc-cmp-chart-row .k span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-cmp-chart-row .t{height:14px;border-radius:7px;background:rgba(148,163,184,.1);overflow:hidden;min-width:40px}
.ssc-cmp-chart-row .t i{display:block;height:100%;border-radius:7px;width:0;transition:width .9s cubic-bezier(.22,1,.36,1);background:linear-gradient(90deg,#22d3ee,#3b82f6)}
.ssc-cmp-chart-row .v{text-align:right;color:#7dd3fc;font-weight:800;font-variant-numeric:tabular-nums}
.ssc-cmp-chart-row.zero .v{color:#fb7185}
.ssc-cmp-ghead{display:flex;align-items:center;gap:12px;padding:10px;border-radius:11px;background:rgba(148,163,184,.05);border:1px solid rgba(148,163,184,.1);flex-shrink:0}
.ssc-cmp-ghead .t{flex:1;min-width:0}
.ssc-cmp-ghead b{font-size:13px;font-weight:800;color:#f1f5f9;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ssc-cmp-ghead span{font-size:10.5px;color:#8fa1b8;margin-top:2px;display:block}
.ssc-cmp-note{font-size:11.5px;color:#8fa1b8;line-height:1.6}
.ssc-cmp-ovside .ssc-cmp-note{font-size:12px;color:#a8b6c8}
/* ---- 响应式 ---- */
@media(max-width:1100px){.ssc-pf-grid,.ssc-pf-hero{grid-template-columns:1fr}.ssc-pf-kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:1100px){.ssc-hero{grid-template-columns:repeat(3,minmax(0,1fr))}.ssc-gauges{grid-template-columns:repeat(2,minmax(0,1fr))}.ssc-dash-stage{grid-template-columns:1fr}.ssc-cmp-overview{gap:12px}.ssc-venn{width:250px;height:144px}}
@media(max-width:900px){.ssc-grid2,.ssc-grid2b,.ssc-dash-stage{grid-template-columns:1fr}.ssc-body{padding:12px 14px 10px}.ssc-top{padding:0 12px}.ssc-kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.ssc-rank-row .ssc-rk-name{width:96px}.ssc-rank-row .ssc-fr-bar{display:none}.ssc-page{gap:12px}.ssc-dash-inner,.ssc-dash-stage,.ssc-dash-main,.ssc-dash-side{gap:12px}.ssc-cmp-overview{flex-direction:column;align-items:stretch}.ssc-cmp-chart-row{grid-template-columns:96px 1fr 56px}.ssc-cmp-bench{grid-template-columns:64px 1fr 52px}}
@media(max-width:640px){.ssc-hero{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (prefers-reduced-motion:reduce){#ssc-root *,#ssc-root *::before,#ssc-root *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}
`;
let host = null;
let core = null;
let inited = false;
let styled = false;
let opened = false;
let page = 'dash';
let query = '';
let sortKey = 'days';
let view = [];
let rootEl = null;
let searchTimer = null;
let gameTimer = null;
let comparePick = [];
let cmpPickInit = false;
let dashTickAt = 0;
let fcountTickAt = 0;
function $(sel, base) { return (base || rootEl).querySelector(sel); }
function text(sel, value) {
const node = $(sel);
if (node) node.textContent = value;
}
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
}[c]));
}
function hours(minutes) {
return (Math.round((minutes || 0) / 6) / 10) + 'h';
}
function animateBars(container) {
requestAnimationFrame(() => {
container.querySelectorAll('[data-w]').forEach((bar) => {
bar.style.width = (Math.max(0, Math.min(100, Number(bar.dataset.w) || 0))) + '%';
});
});
}
function countUp(node, target) {
const fmtPct = node.dataset.fmt === 'pct';
const format = (v) => (fmtPct ? (Math.round(v * 10) / 10).toString() : Math.round(v).toLocaleString('en-US'));
if (REDUCED) { node.textContent = format(target); return; }
const t0 = performance.now();
const dur = 950;
const step = (t) => {
const p = Math.min(1, (t - t0) / dur);
const e = 1 - Math.pow(1 - p, 3);
node.textContent = format(target * e);
if (p < 1 && opened) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
function init(nextHost) {
try {
if (!nextHost || nextHost.apiVersion !== API_VERSION) return false;
const required = ['getApiKey', 'getMySteamId', 'openFriend', 'showToast'];
for (let i = 0; i < required.length; i++) {
if (typeof nextHost[required[i]] !== 'function') return false;
}
core = root.SteamSocialCore;
if (!core || core.apiVersion !== API_VERSION || typeof core.attach !== 'function') return false;
if (!core.attach(nextHost)) return false;
host = nextHost;
injectStyle();
inited = true;
return true;
} catch (e) {
return false;
}
}
function injectStyle() {
if (styled) return;
styled = true;
if (typeof GM_addStyle === 'function') GM_addStyle(CSS);
else if (root.document) {
const style = root.document.createElement('style');
style.textContent = CSS;
root.document.head.appendChild(style);
}
}
function isOpen() { return opened; }
function toast(message, type) {
try { host.showToast(message, type || 'info'); } catch (e) { /* 宿主提示失败时忽略 */ }
}
function ensureDom() {
if (rootEl) return;
const doc = root.document;
rootEl = doc.createElement('div');
rootEl.id = 'ssc-root';
rootEl.innerHTML = `
${I.chart}社交中心
填写 Steam Web API Key 后才能读取好友名单。Key 在好友列表的设置里保存,这里不会发请求。
已索引 0 / 0
游戏数、总时长、近两周只统计已经索引的好友。索引不会在打开时自动开始。
`;
doc.body.appendChild(rootEl);
const dashBox = $('#ssc-dash');
dashBox.addEventListener('click', onDashClick);
dashBox.addEventListener('mouseover', onDashHover);
dashBox.addEventListener('mouseout', onDashHoverOut);
rootEl.querySelector('.ssc-x').addEventListener('click', close);
rootEl.querySelectorAll('.ssc-top .ssc-tab').forEach((btn) => {
btn.addEventListener('click', () => showPage(btn.dataset.page));
});
$('#ssc-q').addEventListener('input', (e) => {
query = e.target.value || '';
clearTimeout(searchTimer);
searchTimer = setTimeout(() => paintList(false), 120);
});
$('#ssc-sort').addEventListener('change', (e) => {
sortKey = e.target.value;
paintList(false);
});
$('#ssc-refresh').addEventListener('click', () => refreshRoster(true));
$('#ssc-level').addEventListener('click', onLevelClick);
$('#ssc-list').addEventListener('scroll', onListScroll);
$('#ssc-list').addEventListener('click', onRowClick);
rootEl.querySelectorAll('[data-rank]').forEach((btn) => {
btn.addEventListener('click', () => {
rootEl.querySelectorAll('[data-rank]').forEach((node) => node.classList.toggle('ssc-on', node === btn));
renderRank(btn.dataset.rank);
});
});
rootEl.querySelectorAll('[data-rankview]').forEach((btn) => {
btn.addEventListener('click', () => {
rankView = btn.dataset.rankview || 'friend';
rootEl.querySelectorAll('[data-rankview]').forEach((node) => node.classList.toggle('ssc-on', node === btn));
const friendTools = $('#ssc-rank-friend-tools');
if (friendTools) friendTools.hidden = rankView !== 'friend';
const current = rootEl.querySelector('[data-rank].ssc-on');
renderRank(current ? current.dataset.rank : 'days');
});
});
$('#ssc-own-min').addEventListener('change', (e) => {
ownMin = Number(e.target.value) || 2;
renderOwnershipRank();
});
$('#ssc-index').addEventListener('click', onIndexClick);
$('#ssc-dash-index').addEventListener('click', onIndexClick);
$('#ssc-game-q').addEventListener('input', () => {
clearTimeout(gameTimer);
gameTimer = setTimeout(renderGameSuggest, 200);
});
rootEl.querySelectorAll('[data-cmpmode]').forEach((btn) => {
btn.addEventListener('click', () => showCmpMode(btn.dataset.cmpmode));
});
$('#ssc-pair').addEventListener('change', () => { cmpPairSeg = 'shared'; renderPair(); });
$('#ssc-pf-subject').addEventListener('change', (e) => {
pfSubject = e.target.value || '';
renderProfile(false);
});
$('#ssc-pf-price').addEventListener('click', onPfPrice);
$('#ssc-pf-review').addEventListener('click', onPfReview);
$('#ssc-pf').addEventListener('click', onPfClick);
buildKpiShell();
}
function buildKpiShell() {
const keys = [
['total', '好友'],
['ingame', '游戏中'],
['online', '在线'],
['offline', '离线'],
['fresh', '30 天内新好友'],
['countries', '国家'],
['vac', 'VAC 已知'],
];
const hostNode = $('#ssc-kpis');
hostNode.innerHTML = '';
keys.forEach((item) => {
const card = document.createElement('div');
card.className = 'ssc-kpi';
const value = document.createElement('b');
value.id = 'ssc-kpi-' + item[0];
value.textContent = '–';
const label = document.createElement('span');
label.textContent = item[1];
card.appendChild(value);
card.appendChild(label);
hostNode.appendChild(card);
});
}
function showPage(next) {
page = next || 'dash';
rootEl.querySelectorAll('.ssc-top .ssc-tab').forEach((btn) => {
btn.classList.toggle('ssc-on', btn.dataset.page === page);
});
rootEl.querySelectorAll('.ssc-page').forEach((node) => {
node.classList.toggle('ssc-on', node.dataset.page === page);
});
if (page === 'dash') {
ensureFriendCounts();
renderDash(false);
}
if (page === 'data') paintList(true);
if (page === 'profile') renderProfile(false);
if (page === 'rank') {
syncIndexButton();
const current = rootEl.querySelector('[data-rank].ssc-on');
renderRank(current ? current.dataset.rank : 'days');
}
if (page === 'compare') { fillCompareSources(); showCmpMode(cmpMode); }
}
async function open() {
if (!inited) return false;
ensureDom();
if (opened) return true;
opened = true;
rootEl.classList.add('ssc-open');
document.body.style.overflow = 'hidden';
document.addEventListener('keydown', onKey);
const boot = $('#ssc-boot');
if (boot) boot.classList.remove('ssc-boot-hide');
const minDelay = new Promise((resolve) => setTimeout(resolve, REDUCED ? 60 : 850));
try { await core.ready; } catch (e) { /* 内存模式仍可打开 */ }
paintFromCache();
if (safeKey() && core.loadRoster()) ensureFriendCounts();
showPage(page);
await minDelay;
if (boot) boot.classList.add('ssc-boot-hide');
const key = safeKey();
const banner = $('#ssc-key-banner');
if (banner) banner.hidden = !!key;
if (!key) return true;
if (!core.rosterFresh()) refreshRoster(false);
else core.startVacFill(onVac);
ensureFriendCounts();
return true;
}
function close() {
if (!opened && !rootEl) return;
opened = false;
if (core) {
try { core.pauseLevelJob(); } catch (e) { /* 无任务 */ }
try { core.pauseLibraryIndexJob(); } catch (e) { /* 无任务 */ }
try { core.pauseFriendCountJob(); } catch (e) { /* 无任务 */ }
try { core.pauseVacFill(); } catch (e) { /* 无任务 */ }
}
document.removeEventListener('keydown', onKey);
if (rootEl) rootEl.classList.remove('ssc-open');
document.body.style.overflow = '';
}
function onKey(e) {
if (e.key === 'Escape' && opened) close();
}
function safeKey() {
try { return host.getApiKey(); } catch (e) { return ''; }
}
function paintFromCache() {
paintKpi();
paintList(true);
syncLevelButton();
syncIndexButton();
}
function paintKpi() {
const data = core.kpi();
text('#ssc-kpi-total', String(data.total));
text('#ssc-kpi-ingame', String(data.ingame));
text('#ssc-kpi-online', String(data.online));
text('#ssc-kpi-offline', String(data.offline));
text('#ssc-kpi-fresh', String(data.fresh));
text('#ssc-kpi-countries', String(data.countries));
text('#ssc-kpi-vac', data.vacKnown + (data.vacBanned ? ' · 封禁 ' + data.vacBanned : ''));
}
function paintList(keepScroll) {
if (!rootEl) return;
view = core.queryFriends({ q: query, sort: sortKey });
const list = $('#ssc-list');
const top = keepScroll ? list.scrollTop : 0;
const spacer = list.querySelector('.ssc-spacer');
spacer.style.height = (view.length * ROW) + 'px';
if (!view.length) {
list.querySelector('.ssc-win').textContent = '';
const empty = document.createElement('div');
empty.className = 'ssc-empty';
empty.textContent = core.loadRoster()
? '没有匹配的好友'
: (safeKey() ? '还没有好友缓存,正在准备名单…' : '没有缓存。填写 API Key 后才能拉取好友名单。');
list.querySelector('.ssc-win').appendChild(empty);
return;
}
if (!keepScroll) list.scrollTop = 0;
else list.scrollTop = top;
drawWindow();
requestAnimationFrame(() => { if (opened) drawWindow(); });
}
let scrollRaf = 0;
function onListScroll() {
if (scrollRaf) return;
scrollRaf = requestAnimationFrame(() => {
scrollRaf = 0;
drawWindow();
});
}
function drawWindow() {
const list = $('#ssc-list');
if (!list || !view.length) return;
const top = list.scrollTop;
const height = list.clientHeight || 480;
const start = Math.max(0, Math.floor(top / ROW) - 4);
const end = Math.min(view.length, Math.ceil((top + height) / ROW) + 4);
const win = list.querySelector('.ssc-win');
const vac = core.getVac();
const levels = core.getLevels();
const frag = document.createDocumentFragment();
for (let i = start; i < end; i++) frag.appendChild(renderRow(view[i], vac, levels));
win.textContent = '';
win.style.transform = 'translateY(' + (start * ROW) + 'px)';
win.appendChild(frag);
}
function renderRow(friend, vac, levels) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ssc-row';
btn.dataset.id = friend.steamid;
const img = document.createElement('img');
img.className = 'ssc-av';
img.alt = '';
if (/^https:\/\//.test(friend.avatar || '')) img.src = friend.avatar;
const mid = document.createElement('div');
mid.className = 'ssc-mid';
const name = document.createElement('div');
name.className = 'ssc-name';
name.textContent = friend.personaname || friend.steamid;
const sub = document.createElement('div');
sub.className = 'ssc-sub';
sub.textContent = statusText(friend);
mid.appendChild(name);
mid.appendChild(sub);
const meta = document.createElement('div');
meta.className = 'ssc-meta';
meta.appendChild(pill(daysText(friend), isFresh(friend) ? 'ssc-pill ssc-new' : 'ssc-pill'));
if (friend.countryCode) meta.appendChild(pill(friend.countryCode, 'ssc-pill'));
const level = levels[friend.steamid];
const levelPill = pill(level == null ? '等级 —' : ('Lv.' + level), 'ssc-pill ssc-lv');
meta.appendChild(levelPill);
const ban = vac[friend.steamid];
if (ban && (ban.vac || ban.gameBan || ban.communityBanned)) meta.appendChild(pill('VAC', 'ssc-pill ssc-vac'));
const mutual = core.getMutual(friend.steamid);
if (mutual && !mutual.private) meta.appendChild(pill('共同 ' + mutual.count, 'ssc-pill'));
btn.appendChild(img);
btn.appendChild(mid);
btn.appendChild(meta);
return btn;
}
function pill(label, cls) {
const node = document.createElement('span');
node.className = cls;
node.textContent = label;
return node;
}
function statusText(friend) {
if (friend.personastate > 0 && friend.gameextrainfo) return '游戏中 · ' + friend.gameextrainfo;
if (friend.personastate > 0) return '在线';
return '离线';
}
function daysText(friend) {
const days = core.pure.friendDays(friend.friend_since, Date.now());
return days == null ? '好友天数未知' : (days + ' 天');
}
function isFresh(friend) {
return core.pure.isNewFriend(friend, Date.now());
}
function onRowClick(e) {
const row = e.target.closest('.ssc-row');
if (!row) return;
const friend = view.find((item) => item.steamid === row.dataset.id);
if (!friend) return;
host.openFriend(friend.steamid, friend.personaname || '', friend.avatar || '');
}
async function refreshRoster(manual) {
if (!safeKey()) {
toast('需要 API Key 才能刷新名单', 'warn');
return;
}
const btn = $('#ssc-refresh');
if (btn) btn.disabled = true;
try {
await core.refreshRoster();
const list = $('#ssc-list');
const top = list ? list.scrollTop : 0;
paintKpi();
paintList(true);
if (list) list.scrollTop = top;
drawWindow();
if (page === 'dash') renderDash(true);
core.startVacFill(onVac);
ensureFriendCounts();
if (manual) toast('好友名单已更新', 'info');
} catch (e) {
const code = e && e.code;
if (code === 'NO_KEY') toast('需要 API Key', 'warn');
else if (code === 'NO_STEAMID') toast('没有识别到你的 SteamID', 'error');
else toast('好友名单更新失败', 'error');
} finally {
if (btn) btn.disabled = false;
}
}
function onVac() {
if (!opened || !rootEl) return;
paintKpi();
const vac = core.getVac();
rootEl.querySelectorAll('.ssc-row').forEach((row) => {
const ban = vac[row.dataset.id];
const old = row.querySelector('.ssc-vac');
const show = ban && (ban.vac || ban.gameBan || ban.communityBanned);
if (show && !old) row.querySelector('.ssc-meta').appendChild(pill('VAC', 'ssc-pill ssc-vac'));
if (!show && old) old.remove();
});
}
function onLevelClick() {
if (!safeKey()) { toast('补齐等级需要 API Key', 'warn'); return; }
if (!core.loadRoster()) { toast('先有好友名单才能补等级', 'warn'); return; }
const state = core.levelJobState();
if (state.running && !state.paused) core.pauseLevelJob();
else if (state.paused) core.resumeLevelJob();
else core.startLevelJob(onLevelItem);
syncLevelButton();
}
function onLevelItem(id, level) {
if (!id || !rootEl) return;
const row = rootEl.querySelector('.ssc-row[data-id="' + cssEscape(id) + '"] .ssc-lv');
if (row) row.textContent = 'Lv.' + level;
syncLevelButton();
}
function cssEscape(value) {
return window.CSS && CSS.escape ? CSS.escape(value) : String(value).replace(/"/g, '');
}
function syncLevelButton() {
const state = core.levelJobState();
const btn = $('#ssc-level');
if (!btn) return;
if (state.running && !state.paused) btn.textContent = '暂停等级';
else if (state.paused) btn.textContent = '继续等级';
else btn.textContent = '补齐等级';
text('#ssc-level-note', state.total ? ('等级 ' + state.done + ' / ' + state.total) : '');
}
function onIndexClick() {
if (!safeKey()) { toast('补齐索引需要 API Key', 'warn'); return; }
if (!core.loadRoster()) { toast('先有好友名单才能索引游戏库', 'warn'); return; }
const state = core.libraryJobState();
if (state.running && !state.paused) core.pauseLibraryIndexJob();
else if (state.paused) core.resumeLibraryIndexJob();
else {
const started = core.startLibraryIndexJob(onIndex);
if (!started.running && !started.total) toast('游戏库索引已经齐了', 'info');
}
syncIndexButton();
}
function onIndex(state) {
if (!rootEl) return;
syncIndexButton();
const stats = core.indexStats();
const note = '已索引 ' + stats.indexed + ' / ' + stats.total + (state.failed ? ' · 跳过 ' + state.failed : '');
text('#ssc-index-count', note);
text('#ssc-dash-cover', note);
if (page === 'dash' && (state.phase === 'tick' || state.phase === 'done')) {
const now = Date.now();
if (state.phase === 'done' || now - dashTickAt >= 1600) {
dashTickAt = now;
renderDash(true);
}
}
if (page === 'rank') {
const current = rootEl.querySelector('[data-rank].ssc-on');
if (rankView === 'game') {
// 索引进行中也节流刷新,让「谁拥有」排名随数据长出来
const now = Date.now();
if (state.phase === 'done' || now - rankGameTickAt >= 1600) {
rankGameTickAt = now;
renderOwnershipRank();
}
} else if (state.phase === 'done') {
renderRank(current ? current.dataset.rank : 'days');
}
}
if (state.phase === 'done' && page === 'compare') fillCompareSources();
}
function syncIndexButton() {
const state = core.libraryJobState();
const stats = core.indexStats();
const label = state.running && !state.paused ? '暂停索引' : (state.paused ? '继续索引' : '补齐深度数据');
['#ssc-index', '#ssc-dash-index'].forEach((sel) => {
const btn = $(sel);
if (btn) btn.textContent = sel === '#ssc-index' ? label.replace('深度数据', '索引') : label;
});
const note = '已索引 ' + stats.indexed + ' / ' + stats.total;
const indexNote = $('#ssc-index-count');
if (indexNote && !state.running) indexNote.textContent = note;
const dashNote = $('#ssc-dash-cover');
if (dashNote && !state.running) dashNote.textContent = note;
}
// ==================== 深度大盘 ====================
function gaugeSvg(pct, color) {
const p = Math.max(0, Math.min(1, pct || 0));
return '';
}
function heroCard(o) {
const pct = o.pct == null ? null : Math.max(0, Math.min(1, o.pct));
return '' +
'
' + o.icon + '
' +
'
' +
'
0' +
(o.suffix ? '' + esc(o.suffix) + '' : '') +
(o.unit ? '' + esc(o.unit) + '' : '') +
'
' +
'
' + esc(o.lbl) + '
' +
(o.sub ? '
' + esc(o.sub) + '
' : '') +
(pct != null ? '
' : '') +
'
';
}
function gaugeCard(o) {
const pct = Math.max(0, Math.min(1, o.pct || 0));
return '' +
'
' + gaugeSvg(pct, o.color) +
'
0% · ' + esc(o.note) + '
' +
'
' +
'
' + esc(o.lbl) + '
' +
'
';
}
function heatLevel(value, max) {
if (!value) return '';
const p = value / (max || 1);
if (p <= 0.2) return 'v1';
if (p <= 0.4) return 'v2';
if (p <= 0.6) return 'v3';
if (p <= 0.85) return 'v4';
return 'v5';
}
function renderDash(live) {
if (!rootEl || !opened) return;
const box = $('#ssc-dash');
if (!box) return;
const d = core.dashAggregate();
const prevScroll = live ? box.scrollTop : 0;
box.innerHTML = dashHtml(d, live);
animateDash(box, live);
text('#ssc-dash-gen', d.coverage.total ? ('数据截至 ' + new Date(d.generatedAt).toLocaleTimeString('zh-CN', { hour12: false })) : '');
if (live && prevScroll) box.scrollTop = prevScroll;
}
function dashHtml(d, live) {
const k = d.kpis;
const cov = d.coverage;
const delay = (i) => (live ? 0 : Math.min(i * 60, 420));
const onlineTotal = k.online + k.ingame;
const pubTotal = d.publicCount + d.privateCount;
if (!cov.total && !k.total) {
return '' + (safeKey()
? '还没有好友缓存,点上方「刷新名单」或在好友数据页刷新后回来。'
: '填写 Steam Web API Key 后才能读取好友名单与深度数据。') + '
';
}
const hero =
heroCard({ icon: I.users, val: k.total, suffix: d.limit ? ' / ' + d.limit : '', lbl: '好友总数', sub: d.limit ? '上限随等级估算(Lv.' + d.ownLevel + ')' : '补齐等级后显示上限', g1: '#3b82f6', g2: '#60a5fa', pct: d.limit ? k.total / d.limit : null, delay: delay(0) }) +
heroCard({ icon: I.activity, val: k.online, lbl: '当前在线', sub: '不含游戏中', g1: '#10b981', g2: '#34d399', delay: delay(1) }) +
heroCard({ icon: I.gamepad, val: k.ingame, lbl: '游戏中', sub: k.ingame ? '正在开黑' : '此刻无人游戏中', g1: '#8b5cf6', g2: '#a78bfa', delay: delay(2) }) +
heroCard({ icon: I.clock, val: Math.round(d.recent2wMinutes / 60), unit: '小时', lbl: '好友近2周总时长', sub: '已索引 ' + cov.recent + ' / ' + cov.total + ' 人', g1: '#f59e0b', g2: '#fbbf24', delay: delay(3) }) +
heroCard({ icon: I.shield, val: d.publicCount, lbl: '数据公开好友', sub: '私密 ' + d.privateCount + ' 人', g1: '#ec4899', g2: '#f472b6', pct: pubTotal ? d.publicCount / pubTotal : null, delay: delay(4) });
const gauges =
gaugeCard({ lbl: '在线率', note: onlineTotal + '/' + k.total, pct: k.total ? onlineTotal / k.total : 0, color: '#22d3ee', delay: delay(5) }) +
gaugeCard({ lbl: '游戏中占比', note: k.ingame + '/' + k.total, pct: k.total ? k.ingame / k.total : 0, color: '#a78bfa', delay: delay(6) }) +
gaugeCard({ lbl: '好友位使用', note: d.limit ? k.total + '/' + d.limit : '需本人等级', pct: d.limit ? k.total / d.limit : 0, color: '#60a5fa', delay: delay(7) }) +
gaugeCard({ lbl: '资料公开率', note: pubTotal ? d.publicCount + '/' + pubTotal : '待索引', pct: pubTotal ? d.publicCount / pubTotal : 0, color: '#34d399', delay: delay(8) });
return '' + hero + '
' +
'
' + gauges + '
' +
'
' +
'
' + recentRankHtml(d, delay) + '
' +
'
' + hotGamesHtml(d, delay) + friendsRankHtml(d, delay) + heatHtml(d, delay) + '
' +
'
';
}
function recentRankHtml(d, delay) {
const rows = d.recentRank;
let body;
if (!rows.length) {
body = '近2周暂无好友游玩记录。点上方「补齐深度数据」索引好友游戏库后出现(不会自动开始)。
';
} else {
const max = rows[0].minutes || 1;
body = rows.map((row, i) => {
const games = (row.topGames || []).map((game) => {
const icon = game.icon
? '
'
: '';
return '' + icon +
'' + esc(game.name) + '' +
'' +
'' + hours(game.minutes) + '
';
}).join('');
const avatar = /^https:\/\//.test(row.avatar || '')
? '
'
: '';
return '' +
'
' + (i + 1) + '' + avatar +
'
' +
'
' + esc(row.name) + '' +
'0小时
' +
games +
'
' +
'
';
}).join('');
}
return '' +
'
' + I.trophy + '最近好友游戏时长排行近2周 · Top 10 · 点击进抽屉
' + body + '
';
}
function donutSegments(hotGames, totalMinutes) {
const total = totalMinutes || 1;
const segs = hotGames.map((game, i) => ({
name: game.name, minutes: game.minutes, players: game.players, icon: game.icon,
color: game.appid === 0 ? OTHER_COLOR : DONUT_COLORS[i % DONUT_COLORS.length],
pct: (game.minutes / total) * 100,
}));
const used = segs.reduce((sum, seg) => sum + seg.pct, 0);
if (segs.length && used < 99.5) segs.push({ name: '未计入', minutes: 0, players: 0, icon: '', color: OTHER_COLOR, pct: Math.max(0, 100 - used) });
return segs;
}
function hotGamesHtml(d, delay) {
let body;
if (!d.hotGames.length) {
body = '还没有热门游戏数据。索引好友游戏库后,这里会汇总大家近2周在玩什么。
';
} else {
const segs = donutSegments(d.hotGames, d.hotTotalMinutes);
let acc = 0;
const circles = segs.map((seg, i) => {
const len = Math.max(0.4, seg.pct - 0.6);
const off = 25 - acc;
acc += seg.pct;
return '';
}).join('');
const legend = segs.map((seg, i) => {
const icon = seg.icon ? '
' : '';
return '' +
'' + icon +
'' + esc(seg.name) + '' +
'' + (seg.minutes ? hours(seg.minutes) + ' · ' + seg.players + '人' : '—') + '' +
'' + seg.pct.toFixed(1) + '%
';
}).join('');
body = '' +
'
' +
'
0小时 · 近2周
共 ' + d.hotKinds + ' 款
' +
'
' +
'
' + legend + '
' +
'
';
}
return '' +
'
' + I.pie + '热门游戏分布近2周 · 按好友合计时长
' +
'
' + body + '
';
}
function heatHtml(d, delay) {
let body;
if (!d.heat.rows.length) {
body = '热力图按天累积:每天索引/刷新后记录一次游玩快照,相邻两天差值即当日游戏时长。今天先跑一轮「补齐深度数据」。
';
} else {
const days = d.heat.days;
const header = '' + days.map((day, i) =>
'' + (i % 2 ? '' : day.label) + '').join('') + '
';
const rows = d.heat.rows.map((row) => {
const avatar = /^https:\/\//.test(row.avatar || '')
? '
'
: '';
const cells = row.cells.map((value) => {
if (value == null) return '';
const level = heatLevel(value, d.heat.max);
const tip = esc(row.name) + (value ? ' · ' + hours(value) : ' · 无游玩');
return '';
}).join('');
return '' +
'
' + avatar + '' + esc(row.name) + '
' + cells + '
';
}).join('');
const legend = '少 多
';
body = '' + legend;
}
return '' +
'
' + I.flame + '好友游戏时长热力图近 14 天 · 每日增量
' +
'
' + body + '
';
}
function friendRankSkeleton() {
let html = '';
for (let i = 0; i < 8; i++) {
const w = 42 + ((i * 13) % 38);
html += '
';
}
return html;
}
function friendsRankHtml(d, delay) {
const job = core.friendCountJobState ? core.friendCountJobState() : { running: false, known: 0, rosterTotal: 0 };
const rows = d.friendsRank;
const incomplete = job.rosterTotal > 0 && job.known < job.rosterTotal;
const settled = !job.running && job.total > 0 && job.done >= job.total;
const loading = !rows.length && !!safeKey() && incomplete && !settled;
let sub = '好友总数';
if (job.running) sub = '后台读取 ' + job.known + ' / ' + job.rosterTotal;
else if (rows.length) sub = 'Top ' + rows.length + (job.rosterTotal ? ' · 已缓存 ' + job.known + '/' + job.rosterTotal : '');
else if (job.rosterTotal) sub = '已缓存 ' + job.known + ' / ' + job.rosterTotal;
let body;
if (rows.length) {
const max = rows[0].count || 1;
body = rows.map((row, i) =>
'' +
'
' + (i + 1) + '' +
(/^https:\/\//.test(row.avatar || '') ? '
 + ')
' : '') +
'
' + esc(row.name) + '' +
'
' +
'
' + row.count + '人' +
'
').join('');
} else if (loading) {
body = friendRankSkeleton() + '正在后台读取好友数,已有结果会立刻出现在这里。
';
} else if (!safeKey()) {
body = '填写 Steam Web API Key 后,打开大盘会在后台补齐好友数。
';
} else {
body = '这批好友的好友列表都未公开,排行会在有人公开后自动补上。
';
}
return '' +
'
' + I.users + '好友数排行' + esc(sub) + '
' +
'
' + body + '
';
}
function ensureFriendCounts() {
if (!safeKey() || !core.loadRoster() || typeof core.startFriendCountJob !== 'function') return;
try { core.startFriendCountJob(onFriendCount); } catch (e) { /* 无 Key 或名单时跳过 */ }
}
function onFriendCount(state) {
if (!rootEl || !opened || page !== 'dash') return;
const now = Date.now();
if (state.phase !== 'done' && now - fcountTickAt < 900) return;
fcountTickAt = now;
const box = $('#ssc-dash');
const card = box && box.querySelector('[data-card="fcount"]');
if (!card) { renderDash(true); return; }
const html = friendsRankHtml(core.dashAggregate(), () => 0);
const holder = document.createElement('div');
holder.innerHTML = html;
const next = holder.firstElementChild;
if (!next) return;
card.replaceWith(next);
animateDash(next, true);
}
function animateDash(box, live) {
box.querySelectorAll('[data-cu]').forEach((node) => {
const target = Number(node.dataset.cu) || 0;
if (live || REDUCED) {
node.textContent = node.dataset.fmt === 'pct' ? String(target) : Math.round(target).toLocaleString('en-US');
} else {
countUp(node, target);
}
});
box.querySelectorAll('.ssc-g-val').forEach((circle) => {
const pct = Math.max(0, Math.min(1, Number(circle.dataset.pct) || 0));
circle.setAttribute('stroke-dasharray', (75 * pct).toFixed(2) + ' ' + (100 - 75 * pct).toFixed(2));
});
requestAnimationFrame(() => {
box.querySelectorAll('.ssc-seg').forEach((circle) => {
const len = Number(circle.dataset.len) || 0;
circle.setAttribute('stroke-dasharray', len.toFixed(2) + ' ' + (100 - len).toFixed(2));
});
box.querySelectorAll('[data-w]').forEach((bar) => {
bar.style.width = Math.max(0, Math.min(100, Number(bar.dataset.w) || 0)) + '%';
});
});
}
function openFromDashRow(row) {
if (!row) return;
host.openFriend(row.dataset.sid || '', row.dataset.name || '', row.dataset.av || '');
}
function onDashClick(e) {
if (e.target.closest('.ssc-lg-row')) return;
openFromDashRow(e.target.closest('.ssc-rr-row, .ssc-heat-row, .ssc-fr-row'));
}
function onDashHover(e) {
const row = e.target.closest('.ssc-lg-row');
const seg = e.target.closest('.ssc-seg');
const index = row ? row.dataset.seg : (seg ? seg.dataset.seg : null);
if (index == null) return;
const box = $('#ssc-dash');
box.querySelectorAll('.ssc-seg').forEach((circle) => {
circle.classList.toggle('ssc-hot', circle.dataset.seg === index);
circle.classList.toggle('ssc-dim', circle.dataset.seg !== index);
});
box.querySelectorAll('.ssc-lg-row').forEach((node) => {
node.classList.toggle('ssc-hot', node.dataset.seg === index);
});
}
function onDashHoverOut(e) {
if (e.target.closest('.ssc-lg-row') || e.target.closest('.ssc-seg')) {
const box = $('#ssc-dash');
box.querySelectorAll('.ssc-seg.ssc-dim,.ssc-seg.ssc-hot').forEach((circle) => circle.classList.remove('ssc-dim', 'ssc-hot'));
box.querySelectorAll('.ssc-lg-row.ssc-hot').forEach((node) => node.classList.remove('ssc-hot'));
}
}
// ==================== 好友排行页 ====================
let rankView = 'friend';
let ownMin = 2;
let rankGameTickAt = 0;
function renderRank(kind) {
if (rankView === 'game') { renderOwnershipRank(); return; }
const data = core.rank(kind || 'days');
const box = $('#ssc-rank');
box.textContent = '';
const stats = core.indexStats();
const indexedKinds = kind === 'recent2w' || kind === 'gameCount' || kind === 'playtime';
if (indexedKinds) {
text('#ssc-index-count', '已索引 ' + data.indexed + ' / ' + data.total);
} else if (!core.libraryJobState().running) {
text('#ssc-index-count', '已索引 ' + stats.indexed + ' / ' + stats.total);
}
if (!data.rows.length) {
const empty = document.createElement('div');
empty.className = 'ssc-empty';
empty.textContent = indexedKinds ? '还没有可排的索引数据。点「补齐索引」才会请求游戏库。' : '还没有好友名单。';
box.appendChild(empty);
return;
}
const max = data.rows.length && data.rows[0].value != null ? data.rows[0].value : 0;
data.rows.slice(0, 50).forEach((row, index) => {
const line = document.createElement('div');
line.className = 'ssc-rank-row ssc-anim';
line.style.animationDelay = Math.min(index * 40, 320) + 'ms';
const pct = max > 0 && row.value != null ? Math.max(2, (row.value / max) * 100) : 0;
line.innerHTML =
'' + (index + 1) + '' +
(/^https:\/\//.test(row.avatar || '') ? '
' : '') +
'' + esc(row.name) + '' +
'' +
'' + esc(row.label) + '';
const openBtn = document.createElement('button');
openBtn.type = 'button';
openBtn.className = 'ssc-btn';
openBtn.textContent = '查看';
openBtn.addEventListener('click', () => host.openFriend(row.steamid, row.name, row.avatar || ''));
line.appendChild(openBtn);
box.appendChild(line);
});
animateBars(box);
}
// ============ 游戏拥有数排行:按好友拥有数排游戏 ============
// ⚠️ 名字不要叫 renderGameRank —— 比对页已有一个同名函数(单游戏时长),
// 函数声明提升后同名者后者覆盖前者,会把本视图渲染到 #ssc-game-out 去。
function renderOwnershipRank() {
const box = $('#ssc-rank');
if (!box) return;
box.textContent = '';
// 能力降级:Core 2.0.0 没有 gameOwnershipRank
if (typeof core.gameOwnershipRank !== 'function') {
text('#ssc-rank-note', '');
const empty = document.createElement('div');
empty.className = 'ssc-empty';
empty.textContent = '当前 SteamSocialCore 没有「游戏拥有数排行」能力(需要 2.1.0 及以上)。请更新社交库后重试。';
box.appendChild(empty);
return;
}
const data = core.gameOwnershipRank({ minOwners: ownMin });
text('#ssc-index-count', '已索引 ' + data.indexed + ' / ' + data.friendTotal);
if (!data.indexed) {
text('#ssc-rank-note', '游戏拥有数只统计已经索引游戏库的好友。索引不会在打开时自动开始。');
const empty = document.createElement('div');
empty.className = 'ssc-empty';
empty.textContent = '还没有已索引的好友游戏库。点「补齐索引」才会请求游戏库。';
box.appendChild(empty);
return;
}
text('#ssc-rank-note', '范围 = 好友(不含本人)。已索引 ' + data.indexed + ' / ' + data.friendTotal
+ ' 位好友,共出现 ' + data.distinctGames + ' 款游戏;下面按「多少好友拥有」降序,'
+ '右侧百分比是占已索引好友的比例。索引不会在打开时自动开始。');
if (!data.rows.length) {
const empty = document.createElement('div');
empty.className = 'ssc-empty';
empty.textContent = '没有满足「至少 ' + ownMin + ' 人拥有」的游戏。降低门槛试试。';
box.appendChild(empty);
return;
}
const max = data.rows[0].owners || 1;
data.rows.forEach((row, index) => {
const wrap = document.createElement('div');
wrap.className = 'ssc-grow ssc-anim';
wrap.style.animationDelay = Math.min(index * 30, 300) + 'ms';
const pct = (row.owners / max) * 100;
const sharePct = data.indexed ? Math.round((row.owners / data.indexed) * 1000) / 10 : 0;
const faces = row.top
.filter((owner) => /^https:\/\//.test(owner.avatar || ''))
.slice(0, 5)
.map((owner) => '
')
.join('');
const line = document.createElement('div');
line.className = 'ssc-grow-row';
line.innerHTML =
'' + (index + 1) + '' +
gameIconHtml(row.appid, 'ssc-grow-ico') +
'' + esc(row.name) + '' +
(faces ? '' + faces + '' : '') +
'' +
'' + row.owners + ' 人' +
'' + sharePct + '%';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ssc-btn';
btn.textContent = '谁有';
btn.addEventListener('click', () => toggleGameOwners(wrap, row, btn));
line.appendChild(btn);
wrap.appendChild(line);
box.appendChild(wrap);
});
animateBars(box);
}
/** 展开「谁拥有这款游戏」:按游玩时长降序,点人进抽屉 */
function toggleGameOwners(wrap, row, btn) {
const existing = wrap.querySelector('.ssc-grow-owners');
if (existing) {
existing.remove();
btn.textContent = '谁有';
return;
}
const detail = core.rankGame(row.appid);
const panel = document.createElement('div');
panel.className = 'ssc-grow-owners';
const sum = document.createElement('div');
sum.className = 'ssc-grow-sum';
sum.textContent = detail.rows.length
? ('共 ' + detail.rows.length + ' 位已索引好友持有 · 按游玩时长降序,点人进抽屉')
: '';
panel.appendChild(sum);
if (!detail.rows.length) {
const none = document.createElement('div');
none.className = 'ssc-cmp-note';
none.textContent = '没有已索引好友持有这款游戏。';
panel.appendChild(none);
} else {
detail.rows.forEach((owner) => {
const item = document.createElement('div');
item.className = 'ssc-grow-owner';
item.title = '打开 ' + owner.name + ' 的抽屉';
const avatar = /^https:\/\//.test(owner.avatar || '')
? '
'
: '';
item.innerHTML = avatar +
'' + esc(owner.name) + '' +
'' + (owner.minutes ? hours(owner.minutes) : '买了没玩') + '';
item.addEventListener('click', () => host.openFriend(owner.steamid, owner.name, owner.avatar || ''));
panel.appendChild(item);
});
}
wrap.appendChild(panel);
btn.textContent = '收起';
}
// ==================== 游戏比对页 ====================
const GAME_CAP = 'https://cdn.cloudflare.steamstatic.com/steam/apps/';
const GAME_ICON_FALLBACK = 'data:image/svg+xml,' + encodeURIComponent('').replace(/'/g, '%27');
function gameIconHtml(appid, cls) {
const id = Number(appid);
if (!id) return '';
const url = GAME_CAP + id + '/capsule_231x87.jpg';
return '
';
}
function avatarOf(steamid) {
const roster = core.loadRoster();
if (roster && roster.own && roster.own.avatar && steamid === host.getMySteamId()) return roster.own.avatar || '';
const hit = ((roster && roster.friends) || []).find((friend) => friend.steamid === steamid);
return hit ? hit.avatar || '' : '';
}
function noteDiv(value) {
const node = document.createElement('div');
node.className = 'ssc-cmp-note';
node.textContent = value;
return node;
}
function indexedPeople() {
const roster = core.loadRoster();
const people = [];
const self = host.getMySteamId();
if (self && core.peekGames(self)) {
people.push({ steamid: self, personaname: '我', avatar: (roster && roster.own && roster.own.avatar) || '' });
}
((roster && roster.friends) || []).forEach((friend) => {
if (core.peekGames(friend.steamid)) people.push(friend);
});
return people;
}
function fillCompareSources() {
const people = indexedPeople();
const self = host.getMySteamId();
const pair = $('#ssc-pair');
const many = $('#ssc-many');
if (!pair || !many) return;
pair.textContent = '';
many.textContent = '';
const friends = people.filter((person) => person.steamid !== self);
if (!friends.length) {
const opt = document.createElement('option');
opt.textContent = '还没有已索引的好友';
opt.value = '';
pair.appendChild(opt);
}
friends.forEach((friend) => {
const opt = document.createElement('option');
opt.value = friend.steamid;
opt.textContent = friend.personaname || friend.steamid;
pair.appendChild(opt);
});
// 人物选择器:头像胶囊(点选即算),取代旧版的复选框墙
people.forEach((person) => {
const pick = document.createElement('button');
pick.type = 'button';
pick.className = 'ssc-cmp-pick';
pick.dataset.steamid = person.steamid;
const avatar = avatarOf(person.steamid);
pick.innerHTML = (/^https:\/\//.test(avatar) ? '
' : '') +
'' + esc(person.personaname || person.steamid) + '';
pick.addEventListener('click', () => {
const at = comparePick.indexOf(person.steamid);
if (at === -1) {
if (comparePick.length >= 4) { toast('最多选择 4 人', 'warn'); return; }
comparePick = comparePick.concat(person.steamid);
} else {
comparePick = comparePick.filter((id) => id !== person.steamid);
}
syncComparePicker();
renderMany();
});
many.appendChild(pick);
});
if (!people.length) {
const tip = document.createElement('div');
tip.className = 'ssc-note';
tip.textContent = '索引完成后,这里才会出现可以比对的人。';
many.appendChild(tip);
}
// 首次进入时预选「我 + 第一位好友」,避免多人交集首屏是空的
if (!cmpPickInit && people.length) {
cmpPickInit = true;
const selfId = host.getMySteamId();
const picked = [];
if (people.some((person) => person.steamid === selfId)) picked.push(selfId);
people.forEach((person) => { if (picked.length < 2 && picked.indexOf(person.steamid) === -1) picked.push(person.steamid); });
comparePick = picked;
}
syncComparePicker();
}
/** 人物胶囊选中态同步 */
function syncComparePicker() {
rootEl.querySelectorAll('.ssc-cmp-pick').forEach((node) => {
node.classList.toggle('ssc-on', comparePick.indexOf(node.dataset.steamid) !== -1);
});
}
// ============ 比对模式切换 ============
let cmpMode = 'pair';
let cmpPairSeg = 'shared';
function showCmpMode(mode) {
cmpMode = mode || 'pair';
rootEl.querySelectorAll('[data-cmpmode]').forEach((btn) => {
btn.classList.toggle('ssc-on', btn.dataset.cmpmode === cmpMode);
});
rootEl.querySelectorAll('[data-cmpstage]').forEach((stage) => {
stage.hidden = stage.dataset.cmpstage !== cmpMode;
});
renderCmpMode();
}
function renderCmpMode() {
if (cmpMode === 'pair') renderPair();
else if (cmpMode === 'many') renderMany();
else renderGameSuggest();
}
/** 数字越大字号越小,避免韦恩图中间的标签溢出 */
function vennFontSize(value) {
const n = Math.abs(Number(value) || 0);
if (n >= 1000) return 13;
if (n >= 100) return 16;
return 19;
}
/** 对比总览:韦恩图 + 占比 bench */
function cmpOverviewHtml(cmp) {
const myTotal = cmp.myTotalGames || 0;
const taTotal = cmp.friendTotalGames || 0;
const maxTotal = Math.max(myTotal, taTotal) || 1;
const myShare = myTotal ? (cmp.sharedCount / myTotal * 100) : 0;
const taShare = taTotal ? (cmp.sharedCount / taTotal * 100) : 0;
const venn =
'';
const bench = (key, pct, color, value) =>
'' + esc(key) + '' +
'' +
'' + value + '
';
const side =
'' +
bench('共同占我库', myShare, 'linear-gradient(90deg,#3b82f6,#60a5fa)', myShare.toFixed(1) + '%') +
bench('共同占 TA 库', taShare, 'linear-gradient(90deg,#10b981,#34d399)', taShare.toFixed(1) + '%') +
bench('我的库规模', (myTotal / maxTotal) * 100, 'linear-gradient(90deg,#3b82f6,#60a5fa)', myTotal + ' 款') +
bench('TA 库规模', (taTotal / maxTotal) * 100, 'linear-gradient(90deg,#f43f5e,#fb7185)', taTotal + ' 款') +
'
时长合计:我 ' + hours(cmp.myTotalMinutes) + ' · TA ' + hours(cmp.friendTotalMinutes) + '
' +
'
';
return '' + venn + side + '
';
}
/** bench 行(对比用) */
function benchHtml(key, color, minutes, max, avatar) {
const pct = max ? Math.max(0, Math.min(100, minutes / max * 100)) : 0;
const avatarHtml = avatar ? '
' : '';
return '' + avatarHtml + '' + esc(key) + '' +
'' +
'' + (minutes ? hours(minutes) : '没玩过') + '
';
}
/** 游戏行(可点进商店) */
function cmpGameRow(appid, name, inner) {
const row = document.createElement('a');
row.className = 'ssc-cmp-row';
row.href = 'https://store.steampowered.com/app/' + appid;
row.target = '_blank';
row.rel = 'noopener';
row.innerHTML = gameIconHtml(appid) +
'' + esc(name || ('App ' + appid)) + '
' +
'
' + inner + '
';
return row;
}
function renderPair() {
const out = $('#ssc-pair-out');
if (!out) return;
out.textContent = '';
const self = host.getMySteamId();
if (!self || !core.peekGames(self)) {
out.appendChild(noteDiv('你的游戏库还没有索引。到排行页点「补齐索引」,或先打开一次我的游戏库。'));
return;
}
const select = $('#ssc-pair');
const friendId = select ? select.value : '';
if (!friendId) {
out.appendChild(noteDiv('选择一个已索引的好友。'));
return;
}
const cmp = core.comparePair(self, friendId);
if (!cmp) {
out.appendChild(noteDiv('这个好友还没有游戏库索引。'));
return;
}
// ① 对比总览(韦恩图 + 占比)
const overview = document.createElement('div');
overview.innerHTML = cmpOverviewHtml(cmp);
out.appendChild(overview.firstElementChild);
// ② 结果分段
const segs = document.createElement('div');
segs.className = 'ssc-cmp-segs';
[['shared', '共同', cmp.sharedCount, 'g'], ['mine', '我独有', cmp.myOnlyCount, 'b'], ['theirs', 'TA 独有', cmp.friendOnlyCount, 'r']]
.forEach((seg) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ssc-cmp-seg ' + seg[3] + (cmpPairSeg === seg[0] ? ' ssc-on' : '');
btn.innerHTML = seg[1] + ' ' + seg[2] + '';
btn.addEventListener('click', () => { cmpPairSeg = seg[0]; renderPair(); });
segs.appendChild(btn);
});
out.appendChild(segs);
// ③ 列表
if (cmpPairSeg === 'shared') {
const shared = cmp.shared.slice().sort((a, b) => (b.myPlaytime + b.friendPlaytime) - (a.myPlaytime + a.myPlaytime)).slice(0, 40);
if (!shared.length) {
out.appendChild(noteDiv('没有共同游戏 —— 去「TA 独有」里看看 TA 的私藏吧。'));
} else {
const max = Math.max(1, ...shared.map((game) => Math.max(game.myPlaytime, game.friendPlaytime)));
shared.forEach((game) => {
out.appendChild(cmpGameRow(game.appid, game.name,
benchHtml('我', 'c1', game.myPlaytime, max) +
benchHtml('TA', 'c2', game.friendPlaytime, max)));
});
}
} else if (cmpPairSeg === 'mine') {
const mine = cmp.myOnly.slice().sort((a, b) => b.myPlaytime - a.myPlaytime).slice(0, 40);
if (!mine.length) {
out.appendChild(noteDiv('你没有任何 TA 没有的游戏 —— 库存完全被覆盖。'));
} else {
const max = Math.max(1, ...mine.map((game) => game.myPlaytime));
mine.forEach((game) => {
out.appendChild(cmpGameRow(game.appid, game.name,
benchHtml('我', 'c1', game.myPlaytime, max) +
benchHtml('TA', 'c2', 0, max)));
});
}
} else {
const theirs = cmp.friendOnly.slice().sort((a, b) => b.friendPlaytime - a.friendPlaytime).slice(0, 40);
if (!theirs.length) {
out.appendChild(noteDiv('TA 没有任何你没有的游戏。'));
} else {
const max = Math.max(1, ...theirs.map((game) => game.friendPlaytime));
theirs.forEach((game) => {
out.appendChild(cmpGameRow(game.appid, game.name,
benchHtml('我', 'c1', 0, max) +
benchHtml('TA', 'c2', game.friendPlaytime, max)));
});
}
}
animateBars(out);
}
function renderMany() {
const out = $('#ssc-many-out');
if (!out) return;
out.textContent = '';
const ids = comparePick.slice(0, 4);
if (ids.length < 2) {
out.appendChild(noteDiv('至少选择 2 人 —— 点上面的人物胶囊,最多 4 人。'));
return;
}
const cmp = core.compareMany(ids);
const people = cmp.people || [];
if (cmp.missing && cmp.missing.length) {
out.appendChild(noteDiv('有 ' + cmp.missing.length + ' 人没有索引,未计入。'));
}
// 共同度:占每个人库的比例
const overview = document.createElement('div');
overview.className = 'ssc-cmp-overview';
const bars = people.map((person) => {
const total = person.total || 0;
const pct = total ? (cmp.count / total * 100) : 0;
const avatar = avatarOf(person.steamid);
return '' +
(avatar ? '
' : '') +
esc(person.name || person.steamid) + '' +
'
' +
'
' + pct.toFixed(1) + '% ';
}).join('');
overview.innerHTML = '共同 '
+ cmp.count + ' 款 · 下面是它占每个人库的比例
' + bars + '
';
out.appendChild(overview);
if (!cmp.count) {
out.appendChild(noteDiv('这几位好友没有共有的游戏。'));
return;
}
let max = 1;
cmp.games.forEach((game) => {
Object.keys(game.playtimes).forEach((key) => { max = Math.max(max, game.playtimes[key] || 0); });
});
cmp.games.slice(0, 30).forEach((game) => {
const inner = people.map((person, index) =>
benchHtml(person.name || person.steamid, 'c' + ((index % 4) + 1), game.playtimes[person.steamid] || 0, max)).join('');
out.appendChild(cmpGameRow(game.appid, game.name, inner));
});
animateBars(out);
}
function renderGameSuggest() {
const q = $('#ssc-game-q').value;
const box = $('#ssc-game-suggest');
if (!box) return;
box.textContent = '';
if (!q || q.trim().length < 2) {
text('#ssc-cmp-hint', '输入游戏名或 AppID,看看好友们在这款游戏上花了多少时间');
return;
}
text('#ssc-cmp-hint', '');
core.searchGames(q, 10).forEach((game) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'ssc-btn';
btn.innerHTML = gameIconHtml(game.appid) + '' + esc(game.name) + '' + game.appid + '';
btn.addEventListener('click', () => renderGameRank(game.appid));
box.appendChild(btn);
});
if (!box.children.length) {
box.appendChild(noteDiv('没找到匹配的游戏 —— 试试 AppID。'));
}
}
function renderGameRank(appid) {
const data = core.rankGame(appid);
const out = $('#ssc-game-out');
if (!out) return;
out.textContent = '';
// 游戏头部(大图标 + 索引统计)
const head = document.createElement('div');
head.className = 'ssc-cmp-ghead';
head.innerHTML = gameIconHtml(appid, 'ssc-cmp-gico-big') +
'' + esc(data.name || ('App ' + appid)) + '' + data.rows.length + ' 位已索引好友持有 · 已索引 '
+ data.indexed + ' / ' + data.total + ' 位好友
';
out.appendChild(head);
if (!data.rows.length) {
out.appendChild(noteDiv('已索引的好友里没有人拥有这款游戏。'));
return;
}
// 条形图:谁的时长最长
const max = data.rows[0].minutes || 1;
const chart = document.createElement('div');
chart.className = 'ssc-cmp-chart';
data.rows.slice(0, 40).forEach((row) => {
const item = document.createElement('div');
item.className = 'ssc-cmp-chart-row' + (row.minutes ? '' : ' zero');
item.title = '打开 ' + row.name + ' 抽屉';
const avatar = avatarOf(row.steamid);
item.innerHTML =
'' + (/^https:\/\//.test(avatar) ? '
' : '') +
'' + esc(row.name) + '' +
'' +
'' + (row.minutes ? hours(row.minutes) : '没玩过') + '';
item.addEventListener('click', () => host.openFriend(row.steamid, row.name, avatar));
chart.appendChild(item);
});
out.appendChild(chart);
animateBars(out);
}
// ==================== 玩家画像页 ====================
// 数据全部来自 Core 的纯缓存聚合(profileAggregate / profileHeat / profileDustRank),
// 打开页面不发任何请求;价格与评测是**用户动作**触发的后台补全任务。
const PF_TIER_COLORS = ['#29384c', '#35485f', '#3f5a76', '#4a6d8d', '#fbbf24', '#f97316', '#ef4444'];
const PF_DIM_COLORS = {
hoarding: '#fb7185', depth: '#22d3ee', activity: '#34d399', breadth: '#a78bfa', spend: '#fbbf24',
};
const PF_TIER_RANGE = ['0', '<1h', '1~5h', '5~20h', '20~100h', '100~500h', '500h+'];
let pfSubject = '';
let pfTimer = null;
function pfMoney(value) {
if (value == null) return '—';
const num = Number(value) || 0;
return '¥' + num.toLocaleString('zh-CN', { minimumFractionDigits: num % 1 ? 2 : 0, maximumFractionDigits: 2 });
}
function pfNum(value, digits) {
if (value == null) return '—';
const num = Number(value) || 0;
return num.toLocaleString('zh-CN', { minimumFractionDigits: digits || 0, maximumFractionDigits: digits == null ? 0 : digits });
}
function pfIcon(icon, appid) {
if (!icon) return '';
return '
';
}
/** 主体下拉:本人 + 已索引的好友 */
function fillPfSubjects() {
const select = $('#ssc-pf-subject');
if (!select) return;
const subjects = core.profileSubjects();
const current = select.value;
select.innerHTML = '';
subjects.forEach((subject) => {
const option = document.createElement('option');
option.value = subject.steamid;
option.textContent = (subject.self ? '【我】' : '') + subject.name + (subject.indexed ? '' : '(未索引)');
select.appendChild(option);
});
const wanted = pfSubject || current || '';
if (wanted && subjects.some((s) => s.steamid === wanted)) select.value = wanted;
if (!select.value && subjects.length) select.value = subjects[0].steamid;
pfSubject = select.value;
}
function pfBusyNote() {
const price = core.priceJobState();
const review = core.reviewJobState();
if (price.running) return '补价格中 ' + price.done + '/' + price.total + ' …';
if (review.running) return '补评测中 ' + review.done + '/' + review.total + ' …';
return '';
}
/** 页面渲染入口 */
function renderProfile(live) {
if (!rootEl || !opened) return;
const box = $('#ssc-pf');
if (!box) return;
// 能力降级:宿主与库的版本发布有时间差,Core 没画像能力时给出说明页而不是崩掉
if (typeof core.profileAggregate !== 'function' || typeof core.profileSubjects !== 'function') {
box.innerHTML = '当前 SteamSocialCore 没有画像能力(需要 2.0.0 及以上)。'
+ '请更新社交库后重试 —— 其余社交中心功能不受影响。
';
text('#ssc-pf-note', '');
return;
}
fillPfSubjects();
const subjects = core.profileSubjects();
if (!pfSubject || !subjects.some((s) => s.steamid === pfSubject)) {
const self = subjects.find((s) => s.self);
pfSubject = self ? self.steamid : (subjects[0] ? subjects[0].steamid : '');
}
const select = $('#ssc-pf-subject');
if (select && select.value !== pfSubject) select.value = pfSubject;
if (!pfSubject) {
box.innerHTML = '还没有可画像的对象。先在好友排行页「补齐索引」,或在设置里填 SteamID。
';
text('#ssc-pf-note', '');
return;
}
const data = core.profileAggregate(pfSubject);
if (!data.ok) {
box.innerHTML = '' + esc(data.reason || '没有这个人的游戏库缓存') + '
';
text('#ssc-pf-note', '');
return;
}
const heat = core.profileHeat(pfSubject);
const dust = core.profileDustRank(pfSubject);
const prevScroll = live ? box.scrollTop : 0;
box.innerHTML = pfHtml(data, heat, dust);
animateBars(box);
const note = pfBusyNote();
text('#ssc-pf-note', note || ('价格覆盖 ' + data.coverage.price.pct + '% · 评测覆盖 ' + data.coverage.review.pct + '%'));
if (live && prevScroll) box.scrollTop = prevScroll;
}
function pfHtml(data, heat, dust) {
return '' +
'
' + pfTitleHtml(data) + pfBigHtml(data) + '
' +
'
' + pfKpiHtml(data) + '
' +
'
' +
'
' + pfRadarCard(data) + pfLadderCard(data) + pfHeatCard(heat) + pfCountdownCard(data) + '
' +
'
' + pfDustCard(dust, data) + pfNeglectedCard(data) + pfCphCard(data) + '
' +
'
';
}
/** 称号卡:稀有度徽章 + 称号 + 语录 */
function pfTitleHtml(data) {
const persona = data.persona;
const rare = persona.rarity
? '' + esc(persona.rarity) + ''
: '—';
const tags = [];
if (persona.scaleTag) tags.push(persona.scaleTag);
if (persona.walletTag) tags.push(persona.walletTag);
if (persona.traits && persona.traits.length) {
persona.traits.forEach((trait) => { if (tags.indexOf(trait) === -1) tags.push(trait); });
}
const tagHtml = tags.length
? '' + tags.map((tag) => '' + esc(tag) + '').join('') + '
'
: '';
return '' +
'
' + esc(persona.emoji || '❔') + '' +
'
' + esc(persona.title) + '' + esc(persona.subtitle) + '
' + rare +
'
' + tagHtml +
(persona.quote ? '
' + esc(persona.quote) + '
' : '') +
'
' + esc(persona.scoreBasis) + '
' + esc(persona.rarityBasis) + '
' +
'
';
}
/** 大数字:未开封率 + 吃灰价值 + 概览胶囊 */
function pfBigHtml(data) {
const ladder = data.ladder;
const stock = data.stock;
const baseline = data.baseline;
const recentKinds = baseline.has2weeks;
const pills = [];
pills.push('' + pfNum(baseline.games) + ' / ' + pfNum(baseline.ownedEntries) + ' 款');
pills.push('' + pfNum(Math.round(baseline.hours)) + ' 小时');
pills.push('近两周在玩 ' + pfNum(recentKinds) + ' 款');
// 「全服吃灰」是全平台量级指标,本地算不出来 —— 标 external,不编数
pills.push('全服吃灰 不可算 · 需外部全平台未游玩比例');
const dustValue = data.cph.silentStockValueCny;
return '' +
'
' + pfNum(ladder.neverPlayedPct) + '%' +
'
的游戏买来就没打开过未开封 ' + pfNum(ladder.neverPlayed) + ' 款 · 吃灰价值 ' + pfMoney(dustValue) + '
' +
'
' + pills.join('') + '
' +
'
' + esc(ladder.basis) + '
' +
'
';
}
/** 库存概览 KPI 组 */
function pfKpiHtml(data) {
const b = data.baseline;
const stock = data.stock;
const ladder = data.ladder;
const recentHours = b.minutes2weeks / 60;
const cards = [
['游戏数', pfNum(b.games) + ' 款', false],
['玩过', pfNum(b.played) + ' 款', false],
['总时长', pfNum(b.hours, 1) + ' h', false],
['近两周', recentHours > 0 ? pfNum(recentHours, 1) + ' h' : '0 h', false],
['未开封率', pfNum(ladder.neverPlayedPct) + ' %', false],
['库存原价', pfMoney(stock.stockValueCny), false],
['每小时价值', stock.valuePerHourCny == null ? '—' : pfMoney(stock.valuePerHourCny) + ' /h', stock.valuePerHourCny == null],
['价格覆盖', pfNum(data.coverage.price.pct) + ' %', data.coverage.price.pct < 50],
];
return '' + I.chart + '总体库存概览价格 proxy · 非历史实付价
' +
'' + cards.map((card) =>
'
' + card[1] + '' + card[0] + '
').join('') + '
' +
'' + esc(stock.valuePerHourBasis) + ' · ' + esc(stock.stockValueBasis) + '
';
}
/** 五维雷达 */
function pfRadarCard(data) {
const dims = data.persona.dimensions;
const cx = 120;
const cy = 120;
const radius = 76;
const n = dims.length;
const angleOf = (i) => (-90 + i * (360 / n)) * Math.PI / 180;
const pointAt = (i, ratio) => [
cx + Math.cos(angleOf(i)) * radius * ratio,
cy + Math.sin(angleOf(i)) * radius * ratio,
];
const rings = [0.25, 0.5, 0.75, 1].map((ratio) => {
const points = dims.map((_, i) => pointAt(i, ratio).map((v) => v.toFixed(1)).join(',')).join(' ');
return '';
}).join('');
const axes = dims.map((_, i) => {
const p = pointAt(i, 1);
return '';
}).join('');
const scored = dims.map((dim, i) => ({ dim, i })).filter((row) => row.dim.score != null);
const points = scored.map((row) => pointAt(row.i, Math.max(0, row.dim.score) / 100));
let shape = '';
if (points.length === n) {
shape = '';
} else if (points.length > 1) {
shape = '';
}
const dots = points.map((p) => '').join('');
const labels = dims.map((dim, i) => {
const p = pointAt(i, 1.24);
const anchor = Math.abs(Math.cos(angleOf(i))) < 0.2 ? 'middle' : (Math.cos(angleOf(i)) > 0 ? 'start' : 'end');
const value = dim.score == null ? '—' : dim.score;
const cls = dim.score == null ? 'ssc-pf-rd-na' : 'ssc-pf-rd-val';
return '' + esc(dim.dimension) + ''
+ '' + value + '';
}).join('');
const rows = dims.map((dim) => {
const pct = dim.score == null ? 0 : dim.score;
return '' +
'' + esc(dim.dimension) + '' +
'' +
'' + (dim.score == null ? '—' : dim.score) + '' +
'' + esc(dim.personaTag || '') + '
';
}).join('');
const missing = data.persona.missingDimensions;
return '' +
'
' + I.chart + '五维人格' + (missing.length ? '缺 ' + esc(missing.join('、')) + '(已剔除并重新归一化)' : '五维齐全') + '
' +
'
' +
'
' + rows + '
' +
'
' + esc(data.persona.rarityBasis) + '
';
}
/** 时长阶梯 */
function pfLadderCard(data) {
const ladder = data.ladder;
const max = ladder.tiers.reduce((peak, tier) => Math.max(peak, tier.count), 0) || 1;
const rows = ladder.tiers.map((tier, i) => {
const pct = (tier.count / max) * 100;
return '' +
'' + esc(tier.emoji) + ' ' + esc(tier.label) + '' +
'' + esc(PF_TIER_RANGE[i]) + '' +
'' +
'' + pfNum(tier.pct, 1) + '%' +
'' + pfNum(tier.count) + ' 款
';
}).join('');
return '' +
'
' + I.clock + '时长阶梯7 档 · 单位分钟 · 左闭右开
' +
'
' + rows + '
' +
'
打开过 ' + pfNum(ladder.openedEver) + ' 款(' + pfNum(ladder.openedEverPct, 1) + '%),未开封 ' + pfNum(ladder.neverPlayed) + ' 款。
' +
'
';
}
/** 热力分析:阶梯热力格 + 近 14 天活跃 */
function pfHeatCard(heat) {
if (!heat || !heat.cells.length) {
return '' + I.activity + '热力分析
' +
'
还没有游戏库缓存,补齐索引后这里会出现每款游戏的热力格。
';
}
const cells = heat.cells.map((cell) =>
'').join('');
const legend = heat.tierMeta.map((tier, i) =>
'' + esc(tier.label) + '').join('');
const max = heat.max || 0;
const act = heat.series.map((value, i) => {
if (value == null) return '';
const ratio = max > 0 ? value / max : 0;
const alpha = 0.2 + ratio * 0.8;
return '';
}).join('');
const actDays = heat.days.map((day, i) => '' + (i % 2 ? '' : day.label) + '').join('');
return '' +
'
' + I.activity + '热力分析每格一款游戏 · 共 ' + pfNum(heat.total) + ' 格
' +
'
' + cells + '
' +
'
' + legend + '
' +
'
' + I.clock + '近 14 天活跃按天累积快照,相邻差值即当日时长
' +
'
' + actDays + '
' +
'
' + act + '
' +
'
热力图按天累积:每天索引/刷新后记一次快照,没有快照的那天留空。
' +
'
';
}
/** 吃灰排行榜 */
function pfDustCard(dust, data) {
if (!dust || !dust.items.length) {
return '' + I.flame + '个人吃灰排行榜
' +
'
还没有价格数据。点上方「补齐价格」后,这里按原价排出最心疼的 N 款。
';
}
const rows = dust.items.map((item, i) => {
const review = item.review;
const meta = [];
meta.push('买了没玩');
meta.push('' + pfMoney(item.originalPriceCny) + '');
if (review) meta.push('好评 ' + review.r + '%');
return '' +
'' + (i + 1) + '' +
pfIcon(item.icon, item.appid) +
'' + esc(item.name) + '' +
'' + meta.join(' · ') + '
';
}).join('');
return '' +
'
' + I.flame + '个人吃灰排行榜' + pfNum(dust.count) + ' 款未开封 · 合计 ' + pfMoney(dust.totalValueCny) + '
' +
'
' + rows + '
' +
'
按「当前挂牌原价」降序 —— 价格是 proxy(实时挂牌价),不是历史实付价。点击任一行打开商店页。
' +
'
';
}
/** 被冷落的神作 */
function pfNeglectedCard(data) {
const neglected = data.neglected;
if (!neglected.count) {
return '' + I.trophy + '被冷落的神作好评率 ≥ 90% 且评测 ≥ 1000
' +
'
当前评测覆盖 ' + pfNum(neglected.ratingCoveragePct, 1) + '%,还没有命中的未开封神作。点上方「补齐评测」扩大覆盖。
';
}
const rows = neglected.items.slice(0, 10).map((item) => {
return '' +
'
' + esc(item.name) + '' + pfNum(item.reviewCount) + ' 条评测 · 原价 ' + pfMoney(item.originalPriceCny) + '
' +
'
' + item.rating + '% ';
}).join('');
const more = neglected.count > 10 ? '还有 ' + pfNum(neglected.count - 10) + ' 款未展示(按好评率降序取前 10)
' : '';
return '' +
'
' + I.trophy + '被冷落的神作命中 ' + pfNum(neglected.count) + ' 款
' +
'
' + rows + '
' + more +
'
' + esc(neglected.ratingCoverageNote) + '
' +
'
';
}
/** 最亏 / 最值 */
function pfCphCard(data) {
const cph = data.cph;
const mini = (title, rows, empty) => '' + title + '
' +
(rows.length ? rows.map((row) => '
' + esc(row.name) + '' + pfMoney(row.costPerHourCny) + '/h
').join('')
: '
' + empty + '
') + '
';
return '' +
'
' + I.pie + '最亏 / 最值每小时成本 · 当前挂牌价 proxy
' +
'
' +
mini('最亏(¥/小时最高)', cph.worst.slice(0, 5), '没有可算的已玩 priced 游戏') +
mini('最值(¥/小时最低)', cph.best.slice(0, 5), '没有可算的已玩 priced 游戏') +
'
' +
'
' + esc(cph.basis) + ' · 已玩且有价格 ' + pfNum(cph.pricedPlayedGames) + ' 款;沉默库存 ' + pfNum(cph.silentStockCount) + ' 款 / ' + pfMoney(cph.silentStockValueCny) + '
' +
'
';
}
/** 清灰倒计时 */
function pfCountdownCard(data) {
const cd = data.countdown;
let head;
let body;
if (cd.status === 'done') {
head = '';
body = '';
} else if (cd.status === 'unavailable') {
head = '—
' + esc(cd.reason || '不可算') + '
';
body = '倒计时不可算 —— 缺近期活跃数据时宁可留空,也不编一个速度。
';
} else {
head = '' + pfNum(cd.etaDays) + '天
' +
'把 ' + pfNum(cd.remainingGames) + ' 款未开封游戏全部打通,需要约 ' + pfNum(cd.remainingHours, 1) + ' 小时。
' +
'当前速度 ' + pfNum(cd.dailyMinutes, 1) + ' 分钟/天(来源 ' + esc(cd.method) + ',置信 ' + esc(cd.confidence) + '),预计 ' + esc(cd.etaDate || '—') + ' 清完。
';
body = cd.reason ? '' + esc(cd.reason) + '
' : '';
}
return '' +
'
' + I.shield + '清灰倒计时三级降级 exact → estimated → unavailable
' +
head + body +
'
' + esc(cd.demandBasis || '') + ' · 时间基准是游戏库快照时刻(' + esc(new Date(cd.asOf).toLocaleString('zh-CN')) + '),不是墙上时钟。
' +
'
';
}
/** 交互:点击吃灰榜 / 神作行 → 打开商店页 */
function onPfClick(event) {
const row = event.target.closest('[data-appid]');
if (!row) return;
const appid = row.getAttribute('data-appid');
if (!appid) return;
window.open('https://store.steampowered.com/app/' + appid, '_blank', 'noopener');
}
/** 补价格(用户动作) */
function onPfPrice() {
if (!pfSubject) return;
if (!core.peekGames(pfSubject)) {
toast('这个人还没有游戏库缓存 —— 先在好友排行页补齐索引', 'info');
return;
}
const state = core.startPriceJob(pfSubject, (state, phase) => {
text('#ssc-pf-note', pfBusyNote());
if (phase === 'done') {
toast(state.failed ? ('价格补齐完成,失败 ' + state.failed + ' 条') : '价格补齐完成', 'success');
renderProfile(false);
return;
}
if (pfTimer) clearTimeout(pfTimer);
pfTimer = setTimeout(() => renderProfile(true), 800);
});
text('#ssc-pf-note', pfBusyNote());
if (!state.total) toast('价格缓存都是新鲜的,无需补齐', 'info');
}
/** 补评测(只补吃灰榜头部,控制请求量) */
function onPfReview() {
if (!pfSubject) return;
if (!core.peekGames(pfSubject)) {
toast('这个人还没有游戏库缓存 —— 先在好友排行页补齐索引', 'info');
return;
}
const dust = core.profileDustRank(pfSubject, 60);
const appids = dust.items.map((item) => item.appid);
if (!appids.length) {
toast('没有未开封游戏可评 —— 先补齐价格', 'info');
return;
}
core.startReviewJob(appids, (state, phase) => {
text('#ssc-pf-note', pfBusyNote());
if (phase === 'done') {
toast(state.failed ? ('评测补齐完成,失败 ' + state.failed + ' 条') : '评测补齐完成', 'success');
renderProfile(false);
return;
}
if (pfTimer) clearTimeout(pfTimer);
pfTimer = setTimeout(() => renderProfile(true), 800);
});
text('#ssc-pf-note', pfBusyNote());
}
root.SteamSocialCenter = {
version: VERSION,
apiVersion: API_VERSION,
init,
open,
close,
isOpen,
};
})(typeof window !== 'undefined' ? window : globalThis);