Boosterpack_Enhance

补充包制作工具

  1. // ==UserScript==
  2. // @name:zh-CN 补充包合成器增强
  3. // @name Boosterpack_Enhance
  4. // @namespace https://blog.chrxw.com
  5. // @version 1.4
  6. // @description 补充包制作工具
  7. // @description:zh-CN 补充包制作工具
  8. // @author Chr_
  9. // @match https://steamcommunity.com/tradingcards/boostercreator*
  10. // @match https://steamcommunity.com//tradingcards/boostercreator/*
  11. // @license AGPL-3.0
  12. // @icon https://blog.chrxw.com/favicon.ico
  13. // @grant GM_addStyle
  14. // @grant GM_getResourceText
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/tabulator/6.3.0/js/tabulator.min.js
  16. // @resource css https://cdnjs.cloudflare.com/ajax/libs/tabulator/6.3.0/css/tabulator_midnight.min.css
  17. // ==/UserScript==
  18.  
  19. (() => {
  20. 'use strict';
  21.  
  22. const g_boosterData = {};
  23. const g_faveriteBooster = new Set();
  24. let g_craftMode = "2";
  25.  
  26. // 初始化
  27. setTimeout(async () => {
  28. loadFavorite();
  29. await initBoosterData();
  30. initPanel();
  31. }, 200);
  32.  
  33. function genDiv(cls) {
  34. const d = document.createElement("div");
  35. d.className = cls;
  36. return d;
  37. }
  38. function genInput(cls) {
  39. const i = document.createElement("input");
  40. i.className = cls;
  41. return i;
  42. }
  43. function genSpan(name) {
  44. const s = document.createElement("span");
  45. s.textContent = name;
  46. return s;
  47. }
  48. function genCheckbox(name, cls, key = null, checked = false) {
  49. const l = document.createElement("label");
  50. const i = document.createElement("input");
  51. const s = genSpan(name);
  52. i.textContent = name;
  53. i.title = name;
  54. i.type = "checkbox";
  55. i.className = "fac_checkbox";
  56. i.checked = localStorage.getItem(key) === "true";
  57. l.title = name;
  58. l.appendChild(i);
  59. l.appendChild(s);
  60. return [l, i];
  61. }
  62. function genImage(url, cls = "bh-image") {
  63. const i = document.createElement("img");
  64. i.src = url;
  65. i.className = cls;
  66. return i;
  67. }
  68. function genButton(name, foo, cls = "bh-button") {
  69. const b = document.createElement("button");
  70. b.textContent = name;
  71. b.title = name;
  72. b.className = cls;
  73. b.addEventListener("click", foo);
  74. return b;
  75. }
  76. function genOption(name, value) {
  77. const o = document.createElement("option");
  78. o.textContent = name;
  79. o.value = value;
  80. return o;
  81. }
  82. function genSelector() {
  83. const s = document.createElement("select");
  84. s.appendChild(genOption("可交易", "2"));
  85. s.appendChild(genOption("不可交易", "3"));
  86. return s;
  87. }
  88.  
  89. function initPanel() {
  90. const area = document.querySelector("div.booster_creator_area");
  91.  
  92. const filterContainer = genDiv("bh-filter");
  93. area.appendChild(filterContainer);
  94.  
  95. const iptSearch = genInput("bh-search");
  96. iptSearch.placeholder = "搜索名称 / AppId";
  97. let t = 0;
  98. iptSearch.addEventListener("keydown", () => {
  99. clearTimeout(t);
  100. t = setTimeout(updateFilter, 500);
  101. });
  102. filterContainer.appendChild(iptSearch);
  103.  
  104. const [lblOnlyFavorite, chkOnlyFavorite] = genCheckbox("仅显示已收藏", "bh-checkbox", "bh-onlyfavorite", false);
  105. chkOnlyFavorite.addEventListener("change", updateFilter);
  106. filterContainer.appendChild(lblOnlyFavorite);
  107. const [lblOnlyCraftable, chkOnlyCraftable] = genCheckbox("仅显示可合成", "bh-checkbox", "bh-onlycraftable", false);
  108. chkOnlyCraftable.addEventListener("change", updateFilter);
  109. filterContainer.appendChild(lblOnlyCraftable);
  110.  
  111. const btnSearch = genButton("清除过滤条件", () => {
  112. iptSearch.value = "";
  113. chkOnlyFavorite.checked = false;
  114. chkOnlyCraftable.checked = false;
  115. updateFilter();
  116. }, "bh-button");
  117. filterContainer.appendChild(btnSearch);
  118. filterContainer.appendChild(genSpan(""));
  119.  
  120. const tabledata = Object.values(g_boosterData);
  121.  
  122. const divRight = genDiv("bh-right");
  123. filterContainer.appendChild(divRight);
  124.  
  125. const selPackPrefer = genSelector();
  126. selPackPrefer.addEventListener("change", (_) => {
  127. g_craftMode = selPackPrefer.value;
  128. console.log(g_craftMode);
  129. })
  130. divRight.appendChild(selPackPrefer);
  131.  
  132. const btnBatchCraft = genButton("批量合成收藏的包", async () => {
  133. const favoriteItems = tabledata.filter(x => x.favorite && x.available);
  134. if (favoriteItems.length === 0) {
  135. alert("无可合成项目");
  136. } else {
  137. for (let fav of favoriteItems) {
  138. await doCraftBooster2(fav.appid, fav.contailer);
  139. await asleep(200);
  140. }
  141. }
  142. }, "bh-button-right");
  143. divRight.appendChild(btnBatchCraft);
  144.  
  145. const tableContainer = genDiv("bh-table");
  146. area.appendChild(tableContainer);
  147.  
  148. const rowMenu = [
  149. {
  150. label: "收藏 / 取消收藏",
  151. action: doEditFavorite,
  152. }, {
  153. label: "合成补充包",
  154. action: doCraftBooster,
  155. },
  156. ];
  157.  
  158. const table = new Tabulator(tableContainer, {
  159. height: 600,
  160. data: tabledata,
  161. layout: "fitDataStretch",
  162. rowHeight: 40,
  163. rowContextMenu: rowMenu,
  164. initialSort: [
  165. { column: "favorite", dir: "desc" },
  166. ],
  167. columns: [
  168. { title: "AppId", field: "appid" },
  169. { title: "图片", field: "appid", formatter: appImageFormatter, headerSort: false, width: 100, resizable: false },
  170. { title: "名称", field: "fullName", width: 300 },
  171. { title: "张数", field: "cardSet" },
  172. { title: "宝珠", field: "gemPrice" },
  173. {
  174. title: "收藏",
  175. field: "favorite",
  176. formatter: "tickCross",
  177. sorter: "boolean",
  178. cellClick: (e, cell) => doEditFavorite(e, cell.getRow()),
  179. },
  180. { title: "合成", field: "available", formatter: "tickCross", sorter: "boolean" },
  181. { title: "操作", field: "available", frozen: true, formatter: operatorFormatter, headerSort: false },
  182. ],
  183. });
  184.  
  185. window.addEventListener("hashchange", () => {
  186. const appId = location.hash.replace("#", "");
  187. iptSearch.value = appId;
  188. updateFilter();
  189. });
  190.  
  191. window.addEventListener("beforeunload", () => {
  192. localStorage.setItem("bh-onlyfavorite", chkOnlyFavorite.checked);
  193. localStorage.setItem("bh-onlycraftable", chkOnlyCraftable.checked);
  194. });
  195.  
  196. updateFilter();
  197.  
  198. function doEditFavorite(e, row) {
  199. const cell = row.getCell("favorite");
  200. const newValue = !cell.getValue();
  201. cell.setValue(newValue);
  202. const appId = row.getCell("appid").getValue();
  203. const strAppId = `${appId}`;
  204. if (newValue) {
  205. g_faveriteBooster.add(strAppId);
  206. } else {
  207. g_faveriteBooster.delete(strAppId);
  208. }
  209. saveFavorite();
  210. }
  211.  
  212. function doCraftBooster(e, row) {
  213. const appid = row.getCell("appid").getValue();
  214. const available = row.getCell("available").getValue();
  215. const container = row.getCell("container").getValue();
  216. if (available) {
  217. doCraftBooster2(appid, container);
  218. }
  219. }
  220.  
  221. function updateFilter() {
  222. const filters = [{ field: "keywords", type: "like", value: iptSearch.value.trim(), matchAll: true }];
  223. if (chkOnlyFavorite.checked) {
  224. filters.push({ field: "favorite", type: "=", value: true });
  225. }
  226. if (chkOnlyCraftable.checked) {
  227. filters.push({ field: "available", type: "=", value: true });
  228. }
  229. table.setFilter(filters);
  230. }
  231.  
  232. function appImageFormatter(cell, formatterParams, onRendered) {
  233. const appid = cell.getValue();
  234. const src = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${appid}/capsule_231x87.jpg`;
  235. const image = genImage(src, "be-row-image");
  236. return image;
  237. };
  238.  
  239. function operatorFormatter(cell, formatterParams, onRendered) {
  240. const data = cell.getRow().getData();
  241. return data.contailer;
  242. };
  243. };
  244.  
  245. function doCraftBooster2(appid, contailer) {
  246. let btn = contailer.querySelector("button");
  247. if (btn) {
  248. btn.disabled = true;
  249. }
  250. craftBoosterpack(appid)
  251. .then((success) => {
  252. if (success) {
  253. g_boosterData[appid].available = false;
  254. contailer.innerHTML = "";
  255. contailer.appendChild(genSpan("合成成功"));
  256. btn = null;
  257. } else {
  258. btn.textContent = "合成失败";
  259. }
  260. })
  261. .catch((err) => {
  262. console.error(err);
  263. btn.textContent = "合成失败";
  264. }).finally(() => {
  265. if (btn) {
  266. btn.disabled = false;
  267. }
  268. });
  269. }
  270.  
  271. // 读取补充包列表
  272. async function initBoosterData() {
  273. const gemPrice2SetCount = {
  274. 1200: 5,
  275. 1000: 6,
  276. 857: 7,
  277. 750: 8,
  278. 667: 9,
  279. 600: 10,
  280. 545: 11,
  281. 500: 12,
  282. 462: 13,
  283. 429: 14,
  284. 400: 15
  285. };
  286.  
  287. const currentData = parseBoosterData(document.body.innerHTML);
  288.  
  289. const nameEnDict = {};
  290. if (g_strLanguage !== "english") {
  291. const html = await loadSecondLanguage("english");
  292. const secondData = parseBoosterData(html);
  293.  
  294. for (const { appid, name } of secondData) {
  295. if (appid && name) {
  296. nameEnDict[appid] = name;
  297. }
  298. }
  299. }
  300.  
  301. for (const item of currentData) {
  302. const { appid, name, unavailable, price, series, available_at_time } = item;
  303. const intPrice = parseInt(price);
  304. if (appid && name && intPrice === intPrice) {
  305. const nameEn = nameEnDict[appid] ?? "";
  306. let fullName;
  307. let keywords;
  308. if (name === nameEn) {
  309. fullName = name;
  310. keywords = `${appid} ${name}`.toLowerCase();
  311. } else {
  312. fullName = `${name} (${nameEn})`;
  313. keywords = `${appid} ${name} ${nameEn}`.toLowerCase();
  314. }
  315.  
  316. const cardSet = gemPrice2SetCount[intPrice] ?? 0;
  317. const favorite = g_faveriteBooster.has(`${appid}`);
  318.  
  319. //生成按钮
  320. const contailer = genDiv();
  321. if (!available_at_time) {
  322. const benCraft = genButton("合成补充包", (e) => doCraftBooster2(appid, contailer), "bh-button");
  323. contailer.appendChild(benCraft);
  324. } else {
  325. const time = genSpan(available_at_time);
  326. time.className = "bh-tips";
  327. contailer.appendChild(time);
  328. }
  329.  
  330. g_boosterData[appid] = {
  331. appid,
  332. fullName,
  333. gemPrice: intPrice,
  334. keywords,
  335. series,
  336. cardSet,
  337. available_at_time,
  338. available: !unavailable,
  339. favorite,
  340. contailer,
  341. };
  342. }
  343. }
  344. }
  345.  
  346. function parseBoosterData(html) {
  347. const matchJson = new RegExp(/CBoosterCreatorPage\.Init\(([\s\S]+}]),\s*parseFloat/);
  348. const result = html.match(matchJson);
  349. if (result) {
  350. const json = result[1];
  351. return JSON.parse(json);
  352. } else {
  353. return [];
  354. }
  355. }
  356.  
  357. function loadFavorite() {
  358. const value = localStorage.getItem("be_faviorite") ?? "";
  359. const arr = value.split('|').filter(x => x);
  360.  
  361. g_faveriteBooster.clear();
  362. for (const item of arr) {
  363. g_faveriteBooster.add(item);
  364. }
  365. }
  366.  
  367. function saveFavorite() {
  368. const value = Array.from(g_faveriteBooster).join('|');
  369. console.log(g_faveriteBooster);
  370. localStorage.setItem("be_faviorite", value);
  371. }
  372.  
  373. // 加载第二语言
  374. function loadSecondLanguage(lang) {
  375. return new Promise((resolve, reject) => {
  376. fetch(
  377. `https://steamcommunity.com/tradingcards/boostercreator/?l=${lang}`,
  378. {
  379. method: "GET",
  380. credentials: "include",
  381. })
  382. .then((response) => {
  383. return response.text();
  384. })
  385. .then((text) => {
  386. resolve(text);
  387. })
  388. .catch((err) => {
  389. reject(err);
  390. });
  391. });
  392. }
  393.  
  394. // 合成补充包
  395. function craftBoosterpack(appId) {
  396. return new Promise((resolve, reject) => {
  397. const formData = new FormData();
  398. formData.append("sessionid", g_sessionID);
  399. formData.append("appid", appId);
  400. formData.append("series", "1");
  401. formData.append("tradability_preference", g_craftMode);
  402.  
  403. fetch(
  404. "https://steamcommunity.com/tradingcards/ajaxcreatebooster/",
  405. {
  406. method: "POST",
  407. body: formData,
  408. credentials: "include",
  409. })
  410. .then(response => {
  411. return response.json();
  412. })
  413. .then((json) => {
  414. if (json.purchase_result) {
  415. const { success } = json.purchase_result;
  416. resolve(success === 1);
  417. }
  418. resolve(false);
  419. })
  420. .catch((err) => {
  421. reject(err);
  422. });
  423. });
  424. }
  425.  
  426. //异步延时
  427. function asleep(ms) {
  428. return new Promise(resolve => setTimeout(resolve, ms));
  429. }
  430. })();
  431.  
  432. GM_addStyle(GM_getResourceText("css"));
  433. GM_addStyle(`
  434. img.be-row-image {
  435. width: 90px;
  436. height: auto;
  437. }
  438. div.bh-filter {
  439. padding: 10px 0;
  440. }
  441. div.bh-filter > * {
  442. margin-right: 10px;
  443. }
  444. div.bh-right {
  445. display: inline;
  446. position: absolute;
  447. right: 10px;
  448. }
  449. div.bh-right > * {
  450. margin-left: 10px;
  451. }
  452. span.bh-tips {
  453. font-size: 10px;
  454. }
  455. `);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址