[银河奶牛]战斗模拟实时导入

战斗模拟辅助工具,实时监听角色配置变化,导入当前角色实时配置

  1. // ==UserScript==
  2. // @name [MWI] Realtime Import Of Battle Simulation
  3. // @name:zh-CN [银河奶牛]战斗模拟实时导入
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.2.5
  6. // @description Battle simulation imports the realtime configuration of the current character.
  7. // @description:zh-CN 战斗模拟辅助工具,实时监听角色配置变化,导入当前角色实时配置
  8. // @icon https://www.milkywayidle.com/favicon.svg
  9. // @author Yannis
  10. // @license CC-BY-NC-SA-4.0
  11. // @match https://www.milkywayidle.com/*
  12. // @match https://test.milkywayidle.com/*
  13. // @match https://*/MWICombatSimulatorTest/dist/*
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @connect textdb.online
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js
  19. // ==/UserScript==
  20.  
  21. // 感谢 'MWITool' 为本脚本提供的技术参考,本脚本部分代码来源于 MWITool,请勿删除本版权声明
  22. // 本脚本若有任何问题,欢迎随时与开发者联系与反馈,感谢使用
  23. // Thanks 'MWITool' for the technical reference provided for this script.
  24. // Some of the code in this script is sourced from MWITool.
  25. // Please do not delete this copyright notice.
  26. //
  27. // https://gf.qytechs.cn/en/scripts/494467-mwitools
  28.  
  29. (function () {
  30. 'use strict';
  31.  
  32. const debug = console.log.bind(null, '%c[BatSync]%c', 'color:green', 'color:black');
  33. const info = console.log.bind(null, '%c[BatSync]%c', 'color:cyan', 'color:black');
  34. const error = console.log.bind(null, '%c[BatSync]%c', 'color:red', 'color:black');
  35.  
  36. // 语言设定
  37. const isZHInGameSetting = localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh");
  38. let isZH = isZHInGameSetting;
  39.  
  40. let playerId;
  41. let firstImport = true;
  42. let clientData = {};
  43.  
  44. // #region TextDB
  45.  
  46. // 从TextDB获取数据
  47. async function getDataFromTextDB(key) {
  48. // info(`Get data from TextDB: ${key}`);
  49.  
  50. const response = await new Promise((resolve) => {
  51. GM_xmlhttpRequest({
  52. method: 'GET',
  53. url: `https://textdb.online/${key}`,
  54. timeout: 5000,
  55. onload: resolve,
  56. ontimeout: (e) => resolve({ status: 504, error: "timeout" }),
  57. onerror: (e) => resolve({ status: 500, error: e })
  58. })
  59. });
  60. if (response.status !== 200) {
  61. error(`Error get from TextDB`, {
  62. key: key,
  63. status: response.status,
  64. error: response.error
  65. });
  66. } else {
  67. info(`Get data from TextDB`, {
  68. key: key,
  69. data: response.responseText
  70. });
  71. }
  72.  
  73. return response.responseText;
  74. }
  75.  
  76. // 保存数据到TextDB
  77. async function saveDataToTextDB(key, data) {
  78. // info("保存TextDB数据", {
  79. // key: key,
  80. // data: data
  81. // });
  82.  
  83. const params = new URLSearchParams();
  84. params.append('key', key);
  85. params.append('value', data.toString());
  86.  
  87. const response = await new Promise((resolve) => {
  88. GM_xmlhttpRequest({
  89. method: 'POST',
  90. url: 'https://api.textdb.online/update/',
  91. headers: {
  92. 'Content-Type': 'application/x-www-form-urlencoded',
  93. },
  94. data: params,
  95. onload: resolve,
  96. onerror: function (e) {
  97. error("Error saving to TextDB:", e);
  98. reject(e);
  99. }
  100. });
  101. });
  102.  
  103. if (response.status !== 200) {
  104. error('Failed saving to TextDB:', response);
  105. } else {
  106. info(`Save data to TextDB success, key: ${key}`)
  107. }
  108. }
  109.  
  110. // 生成玩家唯一Key(MD5)
  111. function getPlayerUniqueKey(characterId) {
  112. return `mwi_${characterId}_${md5(md5(characterId))}`;
  113. }
  114.  
  115. // #endregion
  116.  
  117. // #region 角色数据
  118.  
  119. // 获取客户端初始化数据
  120. function getInitClientData() {
  121. return JSON.parse(GM_getValue("init_client_data", ""));
  122. }
  123.  
  124. // 获取当前角色数据
  125. function getCurrentPlayerData() {
  126. let playerId = GM_getValue("current_character_id", null);
  127. if (playerId) {
  128. return getPlayerData(playerId);
  129. } else {
  130. return;
  131. }
  132. }
  133.  
  134. // 获取角色数据
  135. function getPlayerData(id) {
  136. let playersDataStr = GM_getValue("mwi_players_data", null) || JSON.stringify(new Array());
  137. let playersData = JSON.parse(playersDataStr);
  138. const pIndex = playersData.findIndex(obj => obj.character.id === id);
  139. if (pIndex !== -1) {
  140. return playersData[pIndex];
  141. } else {
  142. return;
  143. }
  144. }
  145.  
  146. // 保存角色数据
  147. function saveCharacterData(obj) {
  148. let playersDataStr = GM_getValue("mwi_players_data", null) || JSON.stringify(new Array());
  149. let playersData = JSON.parse(playersDataStr);
  150. playersData = playersData.filter(e => e.character.id !== obj.character.id);
  151. playersData.unshift(obj);
  152. if (playersData.length > 20) {
  153. playersData.pop();
  154. }
  155. GM_setValue("mwi_players_data", JSON.stringify(playersData));
  156. }
  157.  
  158. // #endregion
  159.  
  160. // #region HookMessage
  161.  
  162. // 监听WebSocket
  163. function hookWS() {
  164. const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
  165. const oriGet = dataProperty.get;
  166.  
  167. dataProperty.get = hookedGet;
  168. Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  169.  
  170. function hookedGet() {
  171. const socket = this.currentTarget;
  172. if (!(socket instanceof WebSocket)) {
  173. return oriGet.call(this);
  174. }
  175. if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
  176. return oriGet.call(this);
  177. }
  178.  
  179. const message = oriGet.call(this);
  180. Object.defineProperty(this, "data", { value: message }); // Anti-loop
  181.  
  182. try {
  183. handleMessage(message);
  184. } catch (e) {
  185. error(`处理消息协议时出错: ${e}`);
  186. console.log(e.stack);
  187. }
  188. return message;
  189. }
  190. }
  191.  
  192. // 消息处理
  193. function handleMessage(message) {
  194. let obj = JSON.parse(message);
  195. if (!obj) {
  196. return;
  197. }
  198. switch (obj.type) {
  199. case 'pong': {
  200. // ping-pong
  201. break;
  202. }
  203. case 'active_player_count_updated': {
  204. // 活跃人数更新
  205. break;
  206. }
  207. case 'init_client_data': {
  208. // 客户端数据
  209. GM_setValue("init_client_data", message);
  210. clientData.actionDetailMap = obj.actionDetailMap;
  211. clientData.levelExperienceTable = obj.levelExperienceTable;
  212. clientData.itemDetailMap = obj.itemDetailMap;
  213. clientData.actionCategoryDetailMap = obj.actionCategoryDetailMap;
  214. clientData.abilityDetailMap = obj.abilityDetailMap;
  215. break;
  216. }
  217. case 'init_character_data': {
  218. playerId = obj.character.id;
  219. // 初始化信息
  220. GM_setValue("init_character_data", message);
  221. GM_setValue("current_character_id", playerId);
  222. let player = getPlayerData(playerId);
  223. if (player) {
  224. obj.abilityCombatTriggersMap = { ...player.abilityCombatTriggersMap, ...obj.abilityCombatTriggersMap }
  225. obj.consumableCombatTriggersMap = { ...player.consumableCombatTriggersMap, ...obj.consumableCombatTriggersMap }
  226. }
  227. obj.battleObj = buildBattleObjFromPlayer(obj, true);
  228. saveCharacterData(obj);
  229. saveDataToTextDB(getPlayerUniqueKey(playerId), JSON.stringify(obj.battleObj));
  230. break;
  231. }
  232. case 'profile_shared': {
  233. // 角色详情
  234. let player = getPlayerData(obj.profile.characterSkills[0].characterID)
  235. let battleObj = buildBattleObjFromProfileShared(player, obj);
  236. if (!player) {
  237. // 不是本角色
  238. player = {}
  239. player.character = {}
  240. player.character.id = battleObj.character.id
  241. player.character.name = battleObj.character.name
  242. }
  243. player.battleObj = battleObj;
  244. saveCharacterData(player);
  245. let playerUniqueKey = getPlayerUniqueKey(player.character.id);
  246. info(`Player Uniquekey: `, {
  247. playerId: player.character.id,
  248. playerName: player.character.name,
  249. playerUniqueKey: playerUniqueKey,
  250. textDBUrl: `https://textdb.online/${playerUniqueKey}`
  251. });
  252.  
  253. addExportButton(player.character.id);
  254. break;
  255. }
  256. case 'new_battle': {
  257. // 战斗更新
  258. for (const battlePlayer of obj.players) {
  259. let player = getPlayerData(battlePlayer.character.id);
  260. let battleObj = buildBattleObjFromNewBattle(player, battlePlayer);
  261. if (!player) {
  262. // 不是本角色
  263. player = {}
  264. player.character = {}
  265. player.character.id = battleObj.character.id
  266. player.character.name = battleObj.character.name
  267. }
  268. player.battleObj = battleObj;
  269. saveCharacterData(player);
  270. }
  271. break;
  272. }
  273. case 'items_updated': {
  274. // 物品更新
  275. let player = getPlayerData(playerId);
  276. if (!player) {
  277. break;
  278. }
  279. let update = false;
  280. if (obj.endCharacterItems) {
  281. for (const item of Object.values(obj.endCharacterItems)) {
  282. if (item.itemLocationHrid !== "/item_locations/inventory" && item.count > 0) {
  283. // 装备更新
  284. let equipment = player.battleObj.player.equipment;
  285. equipment = equipment.filter(e => e.itemLocationHrid !== item.itemLocationHrid);
  286. equipment.push({
  287. itemLocationHrid: item.itemLocationHrid,
  288. itemHrid: item.itemHrid,
  289. enhancementLevel: item.enhancementLevel,
  290. })
  291. player.battleObj.player.equipment = equipment;
  292. update = true;
  293. }
  294. }
  295. }
  296. if (update) {
  297. saveCharacterData(player);
  298. }
  299. break;
  300. }
  301. case 'action_type_consumable_slots_updated': {
  302. // 消耗栏更新
  303. let player = getPlayerData(playerId);
  304. if (!player) {
  305. break;
  306. }
  307. player.actionTypeDrinkSlotsMap = obj.actionTypeDrinkSlotsMap;
  308. player.actionTypeFoodSlotsMap = obj.actionTypeFoodSlotsMap;
  309. player.battleObj = buildBattleObjFromPlayer(player, false);
  310. saveCharacterData(player);
  311. break;
  312. }
  313. case 'abilities_updated': {
  314. // 技能更新
  315. let player = getPlayerData(playerId);
  316. let equippedAbilities = JSON.parse(JSON.stringify(player.combatUnit.combatAbilities));
  317. for (let i = equippedAbilities.length; i < 5; i++) {
  318. equippedAbilities.push({})
  319. }
  320. if (obj.endCharacterAbilities) {
  321. for (const ability of obj.endCharacterAbilities) {
  322. // 更新技能详情
  323. const aDetail = player.characterAbilities.find(e => e.abilityHrid === ability.abilityHrid);
  324. if (aDetail) {
  325. aDetail.slotNumber = ability.slotNumber;
  326. }
  327. // 更新已装备技能信息
  328. const aIndex = equippedAbilities.findIndex(e => e.abilityHrid === ability.abilityHrid);
  329. if (aIndex >= 0) {
  330. equippedAbilities[aIndex] = {}
  331. }
  332. if (ability.slotNumber > 0) {
  333. equippedAbilities.splice(ability.slotNumber - 1, 0, {
  334. abilityHrid: ability.abilityHrid,
  335. level: ability.level,
  336. experience: ability.experience,
  337. availableTime: ability.updatedAt
  338. })
  339. }
  340. }
  341. }
  342. player.combatUnit.combatAbilities = equippedAbilities.filter(e => e.abilityHrid && e.abilityHrid.length > 0);
  343. player.battleObj = buildBattleObjFromPlayer(player, false);
  344. saveCharacterData(player);
  345. break;
  346. }
  347. case 'combat_triggers_updated': {
  348. let player = getPlayerData(playerId);
  349. if (!player) {
  350. break;
  351. }
  352. if (obj.combatTriggerTypeHrid === '/combat_trigger_types/ability') {
  353. // 技能栏 Trigger 更新
  354. player.abilityCombatTriggersMap[obj.abilityHrid] = obj.combatTriggers;
  355. } else if (obj.combatTriggerTypeHrid === '/combat_trigger_types/consumable') {
  356. // 消耗栏 Trigger 更新
  357. player.consumableCombatTriggersMap[obj.itemHrid] = obj.combatTriggers;
  358. } else {
  359. break;
  360. }
  361. player.battleObj = buildBattleObjFromPlayer(player, false);
  362. saveCharacterData(player);
  363. saveDataToTextDB(getPlayerUniqueKey(playerId), JSON.stringify(player.battleObj));
  364. break;
  365. }
  366. case 'all_combat_triggers_updated': {
  367. // 所有 Triggers 更新
  368. let player = getPlayerData(playerId);
  369. if (!player) {
  370. break;
  371. }
  372. player.abilityCombatTriggersMap = { ...player.abilityCombatTriggersMap, ...obj.abilityCombatTriggersMap };
  373. player.consumableCombatTriggersMap = { ...player.consumableCombatTriggersMap, ...obj.consumableCombatTriggersMap };
  374. player.battleObj = buildBattleObjFromPlayer(player, false);
  375. saveCharacterData(player);
  376. saveDataToTextDB(getPlayerUniqueKey(playerId), JSON.stringify(player.battleObj));
  377. break;
  378. }
  379. case 'party_updated': {
  380. // 队伍更新
  381. let player = getPlayerData(playerId);
  382. if (!player) {
  383. break;
  384. }
  385. player.partyInfo = obj.partyInfo;
  386. saveCharacterData(player);
  387. break;
  388. }
  389. case 'chat_message_received': {
  390. // 聊天消息
  391. break;
  392. }
  393. case 'action_completed': {
  394. // 行动完成
  395. break;
  396. }
  397. default: {
  398. // info(obj);
  399. }
  400. }
  401. }
  402.  
  403. // #endregion
  404.  
  405. // #region Builders
  406.  
  407. // 构建战斗模拟信息(InitData)
  408. function buildBattleObjFromPlayer(obj, init) {
  409. let battleObj = init ? {} : obj.battleObj;
  410. // Base
  411. battleObj.character = {}
  412. battleObj.character.id = obj.character.id;
  413. battleObj.character.name = obj.character.name;
  414. battleObj.character.gameMode = obj.character.gameMode;
  415. battleObj.timestamp = Date.now();
  416. battleObj.valid = true;
  417. if (init) {
  418. // Levels
  419. battleObj.player = {}
  420. for (const skill of obj.characterSkills) {
  421. if (skill.skillHrid.includes("stamina")) {
  422. battleObj.player.staminaLevel = skill.level;
  423. } else if (skill.skillHrid.includes("intelligence")) {
  424. battleObj.player.intelligenceLevel = skill.level;
  425. } else if (skill.skillHrid.includes("attack")) {
  426. battleObj.player.attackLevel = skill.level;
  427. } else if (skill.skillHrid.includes("power")) {
  428. battleObj.player.powerLevel = skill.level;
  429. } else if (skill.skillHrid.includes("defense")) {
  430. battleObj.player.defenseLevel = skill.level;
  431. } else if (skill.skillHrid.includes("ranged")) {
  432. battleObj.player.rangedLevel = skill.level;
  433. } else if (skill.skillHrid.includes("magic")) {
  434. battleObj.player.magicLevel = skill.level;
  435. }
  436. }
  437. // Equipments
  438. battleObj.player.equipment = [];
  439. if (obj.characterItems) {
  440. for (const item of obj.characterItems) {
  441. if (!item.itemLocationHrid.includes("/item_locations/inventory")) {
  442. battleObj.player.equipment.push({
  443. itemLocationHrid: item.itemLocationHrid,
  444. itemHrid: item.itemHrid,
  445. enhancementLevel: item.enhancementLevel,
  446. });
  447. }
  448. }
  449. }
  450. }
  451. // Food
  452. battleObj.food = {}
  453. battleObj.food["/action_types/combat"] = [];
  454. if (obj.actionTypeFoodSlotsMap["/action_types/combat"]) {
  455. for (const food of obj.actionTypeFoodSlotsMap["/action_types/combat"]) {
  456. if (food) {
  457. battleObj.food["/action_types/combat"].push({
  458. itemHrid: food.itemHrid,
  459. });
  460. } else {
  461. battleObj.food["/action_types/combat"].push({
  462. itemHrid: "",
  463. });
  464. }
  465. }
  466. }
  467. // Drinks
  468. battleObj.drinks = {}
  469. battleObj.drinks["/action_types/combat"] = [];
  470. if (obj.actionTypeDrinkSlotsMap["/action_types/combat"]) {
  471. for (const drink of obj.actionTypeDrinkSlotsMap["/action_types/combat"]) {
  472. if (drink) {
  473. battleObj.drinks["/action_types/combat"].push({
  474. itemHrid: drink.itemHrid,
  475. });
  476. } else {
  477. battleObj.drinks["/action_types/combat"].push({
  478. itemHrid: "",
  479. });
  480. }
  481. }
  482. }
  483. // Abilities
  484. battleObj.abilities = [];
  485. for (let i = 0; i < 5; i++) {
  486. battleObj.abilities.push({
  487. abilityHrid: "",
  488. level: "1",
  489. })
  490. }
  491. if (obj.combatUnit.combatAbilities) {
  492. for (const ability of obj.combatUnit.combatAbilities) {
  493. const aDetail = obj.characterAbilities.find(e => e.abilityHrid === ability.abilityHrid);
  494. if (aDetail) {
  495. battleObj.abilities[aDetail.slotNumber - 1] = {
  496. abilityHrid: ability.abilityHrid,
  497. level: ability.level,
  498. experience: ability.experience,
  499. availableTime: ability.updatedAt
  500. };
  501. }
  502. }
  503. }
  504. // TriggerMap
  505. battleObj.triggerMap = { ...obj.abilityCombatTriggersMap, ...obj.consumableCombatTriggersMap };
  506. // HouseRooms
  507. battleObj.houseRooms = {};
  508. if (obj.characterHouseRoomMap) {
  509. for (const house of Object.values(obj.characterHouseRoomMap)) {
  510. battleObj.houseRooms[house.houseRoomHrid] = house.level;
  511. }
  512. }
  513. return battleObj;
  514. }
  515.  
  516. // 构建战斗模拟信息(ProfileShared)
  517. function buildBattleObjFromProfileShared(player, obj) {
  518. let battleObj = {};
  519. // Base
  520. battleObj.character = {}
  521. battleObj.character.id = player ? player.character.id : obj.profile.characterSkills[0].characterID;
  522. battleObj.character.name = obj.profile.sharableCharacter.name;
  523. battleObj.character.gameMode = obj.profile.sharableCharacter.gameMode;
  524. battleObj.timestamp = Date.now();
  525. battleObj.valid = true;
  526. // Levels
  527. battleObj.player = {}
  528. for (const skill of obj.profile.characterSkills) {
  529. if (skill.skillHrid.includes("stamina")) {
  530. battleObj.player.staminaLevel = skill.level;
  531. } else if (skill.skillHrid.includes("intelligence")) {
  532. battleObj.player.intelligenceLevel = skill.level;
  533. } else if (skill.skillHrid.includes("attack")) {
  534. battleObj.player.attackLevel = skill.level;
  535. } else if (skill.skillHrid.includes("power")) {
  536. battleObj.player.powerLevel = skill.level;
  537. } else if (skill.skillHrid.includes("defense")) {
  538. battleObj.player.defenseLevel = skill.level;
  539. } else if (skill.skillHrid.includes("ranged")) {
  540. battleObj.player.rangedLevel = skill.level;
  541. } else if (skill.skillHrid.includes("magic")) {
  542. battleObj.player.magicLevel = skill.level;
  543. }
  544. }
  545. // Equipments
  546. battleObj.player.equipment = [];
  547. if (obj.profile.wearableItemMap) {
  548. for (const key in obj.profile.wearableItemMap) {
  549. const item = obj.profile.wearableItemMap[key];
  550. battleObj.player.equipment.push({
  551. itemLocationHrid: item.itemLocationHrid,
  552. itemHrid: item.itemHrid,
  553. enhancementLevel: item.enhancementLevel,
  554. });
  555. }
  556. }
  557. // Food and Drinks
  558. battleObj.food = {}
  559. battleObj.food["/action_types/combat"] = [];
  560. battleObj.drinks = {}
  561. battleObj.drinks["/action_types/combat"] = [];
  562. let wearableItemMap = obj.profile.wearableItemMap;
  563. let weapon = null;
  564. if (wearableItemMap) {
  565. weapon = wearableItemMap["/item_locations/main_hand"]?.itemHrid ||
  566. wearableItemMap["/item_locations/two_hand"]?.itemHrid;
  567. }
  568. if (player) {
  569. battleObj.food = player.battleObj.food;
  570. battleObj.drinks = player.battleObj.drinks;
  571. } else if (weapon) {
  572. if (weapon.includes("shooter") || weapon.includes("bow")) {
  573. // 远程
  574. battleObj.food["/action_types/combat"] = [
  575. // 2红1蓝
  576. { itemHrid: "/items/spaceberry_donut" },
  577. { itemHrid: "/items/spaceberry_cake" },
  578. { itemHrid: "/items/star_fruit_yogurt" }
  579. ]
  580. battleObj.drinks["/action_types/combat"] = [
  581. // 经验.超远.暴击
  582. { itemHrid: "/items/wisdom_coffee" },
  583. { itemHrid: "/items/super_ranged_coffee" },
  584. { itemHrid: "/items/critical_coffee" }
  585. ]
  586. } else if (weapon.includes("boomstick") || weapon.includes("staff") || weapon.includes("trident")) {
  587. // 法师
  588. battleObj.food["/action_types/combat"] = [
  589. // 1红2蓝
  590. { itemHrid: "/items/spaceberry_cake" },
  591. { itemHrid: "/items/star_fruit_gummy" },
  592. { itemHrid: "/items/star_fruit_yogurt" }
  593. ]
  594. battleObj.drinks["/action_types/combat"] = [
  595. // 经验.超魔.吟唱
  596. { itemHrid: "/items/wisdom_coffee" },
  597. { itemHrid: "/items/super_magic_coffee" },
  598. { itemHrid: "/items/channeling_coffee" }
  599. ]
  600. } else if (weapon.includes("bulwark")) {
  601. // 双手盾
  602. battleObj.food["/action_types/combat"] = [
  603. // 2红1蓝
  604. { itemHrid: "/items/spaceberry_donut" },
  605. { itemHrid: "/items/spaceberry_cake" },
  606. { itemHrid: "/items/star_fruit_yogurt" }
  607. ]
  608. battleObj.drinks["/action_types/combat"] = [
  609. // 经验.超防.超耐
  610. { itemHrid: "/items/wisdom_coffee" },
  611. { itemHrid: "/items/super_defense_coffee" },
  612. { itemHrid: "/items/super_stamina_coffee" }
  613. ]
  614. } else {
  615. // 近战
  616. battleObj.food["/action_types/combat"] = [
  617. // 2红1蓝
  618. { itemHrid: "/items/spaceberry_donut" },
  619. { itemHrid: "/items/spaceberry_cake" },
  620. { itemHrid: "/items/star_fruit_yogurt" }
  621. ]
  622. battleObj.drinks["/action_types/combat"] = [
  623. // 经验.超力.迅捷
  624. { itemHrid: "/items/wisdom_coffee" },
  625. { itemHrid: "/items/super_power_coffee" },
  626. { itemHrid: "/items/swiftness_coffee" }
  627. ]
  628. }
  629. }
  630. // Abilities
  631. battleObj.abilities = [];
  632. for (let i = 0; i < 5; i++) {
  633. battleObj.abilities.push({
  634. abilityHrid: "",
  635. level: "1",
  636. })
  637. }
  638. if (obj.profile.equippedAbilities) {
  639. let index = 1;
  640. for (const ability of obj.profile.equippedAbilities) {
  641. if (ability && clientData.abilityDetailMap[ability.abilityHrid].isSpecialAbility) {
  642. battleObj.abilities[0] = {
  643. abilityHrid: ability.abilityHrid,
  644. level: ability.level,
  645. experience: ability.experience,
  646. availableTime: ability.updatedAt
  647. };
  648. } else if (ability) {
  649. battleObj.abilities[index++] = {
  650. abilityHrid: ability.abilityHrid,
  651. level: ability.level,
  652. experience: ability.experience,
  653. availableTime: ability.updatedAt
  654. };
  655. }
  656. }
  657. }
  658. // TriggerMap
  659. if (player) {
  660. battleObj.triggerMap = player.battleObj.triggerMap;
  661. }
  662. // HouseRooms
  663. battleObj.houseRooms = {};
  664. for (const house of Object.values(obj.profile.characterHouseRoomMap)) {
  665. battleObj.houseRooms[house.houseRoomHrid] = house.level;
  666. }
  667. return battleObj;
  668. }
  669.  
  670. // 构建战斗模拟信息(NewBattle)
  671. function buildBattleObjFromNewBattle(player, obj) {
  672. let battleObj = {};
  673. if (player) {
  674. battleObj = player.battleObj;
  675. }
  676. // Base
  677. battleObj.character = battleObj.character ?? {};
  678. battleObj.character.id = obj.character.id;
  679. battleObj.character.name = obj.character.name;
  680. battleObj.character.gameMode = obj.character.gameMode;
  681. battleObj.timestamp = Date.now();
  682. battleObj.valid = battleObj.valid;
  683. // Levels
  684. battleObj.player = battleObj.player ?? {};
  685. battleObj.player.staminaLevel = battleObj.player.staminaLevel ?? 1;
  686. battleObj.player.intelligenceLevel = battleObj.player.intelligenceLevel ?? 1;
  687. battleObj.player.attackLevel = battleObj.player.attackLevel ?? 1;
  688. battleObj.player.powerLevel = battleObj.player.powerLevel ?? 1;
  689. battleObj.player.defenseLevel = battleObj.player.defenseLevel ?? 1;
  690. battleObj.player.rangedLevel = battleObj.player.rangedLevel ?? 1;
  691. battleObj.player.magicLevel = battleObj.player.magicLevel ?? 1;
  692. // Equipments
  693. battleObj.player.equipment = battleObj.player.equipment ?? [];
  694. // Food and Drinks
  695. battleObj.food = {};
  696. battleObj.food["/action_types/combat"] = [];
  697. battleObj.drinks = {};
  698. battleObj.drinks["/action_types/combat"] = [];
  699. if (obj.combatConsumables) {
  700. for (const consumable of obj.combatConsumables) {
  701. if (consumable.itemHrid.includes("coffee")) {
  702. battleObj.drinks["/action_types/combat"].push({
  703. itemHrid: consumable.itemHrid
  704. })
  705. } else {
  706. battleObj.food["/action_types/combat"].push({
  707. itemHrid: consumable.itemHrid
  708. })
  709. }
  710. }
  711. }
  712. // Abilities
  713. battleObj.abilities = [];
  714. for (let i = 0; i < 5; i++) {
  715. battleObj.abilities.push({
  716. abilityHrid: "",
  717. level: "1",
  718. })
  719. }
  720. if (obj.combatAbilities) {
  721. let index = 1;
  722. for (const ability of obj.combatAbilities) {
  723. if (ability && clientData.abilityDetailMap[ability.abilityHrid].isSpecialAbility) {
  724. battleObj.abilities[0] = {
  725. abilityHrid: ability.abilityHrid,
  726. level: ability.level,
  727. experience: ability.experience,
  728. availableTime: ability.updatedAt
  729. };
  730. } else if (ability) {
  731. battleObj.abilities[index++] = {
  732. abilityHrid: ability.abilityHrid,
  733. level: ability.level,
  734. experience: ability.experience,
  735. availableTime: ability.updatedAt
  736. };
  737. }
  738. }
  739. }
  740. // TriggerMap
  741. battleObj.triggerMap = { ...battleObj.triggerMap };
  742. // HouseRooms
  743. battleObj.houseRooms = { ...battleObj.houseRooms };
  744. return battleObj;
  745. }
  746.  
  747. // #endregion
  748.  
  749. // #region Battle Simulater
  750.  
  751. // 添加个人资料导出
  752. function addExportButton(characterId) {
  753. const checkElem = () => {
  754. const selectedElement = document.querySelector(`div.SharableProfile_overviewTab__W4dCV`);
  755. if (selectedElement) {
  756. clearInterval(timer);
  757. const button = document.createElement("button");
  758. selectedElement.appendChild(button);
  759. button.textContent = isZH ? "查看云模拟数据" : "View Cloud Data";
  760. button.style.borderRadius = "5px";
  761. button.style.height = "30px";
  762. button.style.backgroundColor = "orange";
  763. button.style.color = "black";
  764. button.style.boxShadow = "none";
  765. button.style.border = "0px";
  766. button.onclick = function () {
  767. window.open(`https://textdb.online/${getPlayerUniqueKey(characterId)}`)
  768. return false;
  769. };
  770. return false;
  771. }
  772. };
  773. let timer = setInterval(checkElem, 200);
  774. }
  775.  
  776. // 添加实时导入按钮
  777. function addImportButtonForMWICombatSimulate() {
  778. const checkElem = () => {
  779. const btnEquipSets = document.querySelector(`button#buttonEquipmentSets`);
  780. if (btnEquipSets) {
  781. clearInterval(timer);
  782.  
  783. let divRow = document.createElement("div");
  784. divRow.className = "row";
  785. btnEquipSets.parentElement.parentElement.prepend(divRow);
  786.  
  787. // 导入按钮
  788. let div1 = document.createElement("div");
  789. div1.className = "mb-3 pt-2";
  790. divRow.append(div1);
  791. let button1 = document.createElement("button");
  792. div1.append(button1);
  793. button1.textContent = isZH ? "实时导入本地数据" : "Real-time Import From Local";
  794. button1.className = "btn btn-warning";
  795. button1.onclick = function () {
  796. const btnGetPrice = document.querySelector(`button#buttonGetPrices`);
  797. if (btnGetPrice) {
  798. btnGetPrice.click();
  799. }
  800. importDataForMWICombatSimulate(button1, false);
  801. return false;
  802. };
  803.  
  804. // 网络导入按钮
  805. let div2 = document.createElement("div");
  806. div2.className = "mb-3 pt-1";
  807. divRow.append(div2);
  808. let button2 = document.createElement("button");
  809. div2.append(button2);
  810. button2.textContent = isZH ? "实时导入网络云数据" : "Real-time Import From Network";
  811. button2.className = "btn btn-warning";
  812. button2.onclick = function () {
  813. const btnGetPrice = document.querySelector(`button#buttonGetPrices`);
  814. if (btnGetPrice) {
  815. btnGetPrice.click();
  816. }
  817. importDataForMWICombatSimulate(button2, true);
  818. return false;
  819. };
  820. }
  821. };
  822. let timer = setInterval(checkElem, 200);
  823. }
  824.  
  825. // 导入数据
  826. async function importDataForMWICombatSimulate(button, readCloudData = false) {
  827. let resetZone = !firstImport;
  828. if (!firstImport) {
  829. let userConfirm = window.confirm(isZH ? "是否要覆盖当前数据" : "Do you want to overwrite the current data?");
  830. if (!userConfirm) {
  831. return;
  832. }
  833. firstImport = false;
  834. }
  835.  
  836. let preTextContent = button.textContent;
  837. let preClassName = button.className;
  838. button.textContent = isZH ? "正在导入数据..." : "Importing...";
  839. button.className = "btn btn-warning";
  840. button.disabled = true;
  841.  
  842. clientData = getInitClientData();
  843. let player = getCurrentPlayerData();
  844.  
  845. const BLANK_PLAYER_JSON_STR = `{\"player\":{\"attackLevel\":1,\"magicLevel\":1,\"powerLevel\":1,\"rangedLevel\":1,\"defenseLevel\":1,\"staminaLevel\":1,\"intelligenceLevel\":1,\"equipment\":[]},\"food\":{\"/action_types/combat\":[{\"itemHrid\":\"\"},{\"itemHrid\":\"\"},{\"itemHrid\":\"\"}]},\"drinks\":{\"/action_types/combat\":[{\"itemHrid\":\"\"},{\"itemHrid\":\"\"},{\"itemHrid\":\"\"}]},\"abilities\":[{\"abilityHrid\":\"\",\"level\":\"1\"},{\"abilityHrid\":\"\",\"level\":\"1\"},{\"abilityHrid\":\"\",\"level\":\"1\"},{\"abilityHrid\":\"\",\"level\":\"1\"},{\"abilityHrid\":\"\",\"level\":\"1\"}],\"triggerMap\":{},\"zone\":\"/actions/combat/fly\",\"simulationTime\":\"100\",\"houseRooms\":{\"/house_rooms/dairy_barn\":0,\"/house_rooms/garden\":0,\"/house_rooms/log_shed\":0,\"/house_rooms/forge\":0,\"/house_rooms/workshop\":0,\"/house_rooms/sewing_parlor\":0,\"/house_rooms/kitchen\":0,\"/house_rooms/brewery\":0,\"/house_rooms/laboratory\":0,\"/house_rooms/observatory\":0,\"/house_rooms/dining_room\":0,\"/house_rooms/library\":0,\"/house_rooms/dojo\":0,\"/house_rooms/gym\":0,\"/house_rooms/armory\":0,\"/house_rooms/archery_range\":0,\"/house_rooms/mystical_study\":0}}`;
  846.  
  847. const players = {};
  848. let isParty = false;
  849. let zone = "/actions/combat/fly";
  850. let isZoneDungeon = false;
  851.  
  852. if (!player?.partyInfo?.partySlotMap) {
  853. // 个人
  854. players[1] = {
  855. name: player.character.name,
  856. imported: true,
  857. cloudData: false,
  858. battleData: JSON.stringify(player.battleObj),
  859. };
  860. // Zone
  861. for (const action of player.characterActions) {
  862. if (action && action.actionHrid.includes("/actions/combat/")) {
  863. zone = action.actionHrid;
  864. isZoneDungeon = clientData.actionDetailMap[action.actionHrid]?.combatZoneInfo?.isDungeon;
  865. break;
  866. }
  867. }
  868. } else {
  869. // 队伍
  870. isParty = true;
  871. let i = 0;
  872. for (const member of Object.values(player.partyInfo.partySlotMap)) {
  873. i++;
  874. if (member.characterID) {
  875. if (member.characterID === player.character.id) {
  876. players[i] = {
  877. name: player.character.name,
  878. imported: true,
  879. cloudData: false,
  880. battleData: JSON.stringify(player.battleObj),
  881. };
  882. } else {
  883. let memberData = getPlayerData(member.characterID);
  884. let battleObj = memberData?.battleObj;
  885.  
  886. if (readCloudData) {
  887. // 读取共享Trigger数据
  888. let sharedTextDBStr = await getDataFromTextDB(getPlayerUniqueKey(member.characterID));
  889. if (sharedTextDBStr) {
  890. let sharedTextDB = JSON.parse(sharedTextDBStr);
  891. if (battleObj) {
  892. battleObj.triggerMap = {
  893. ...battleObj.triggerMap,
  894. ...sharedTextDB.triggerMap
  895. }
  896. } else {
  897. battleObj = sharedTextDB;
  898. }
  899. } else {
  900. readCloudData = false;
  901. }
  902. }
  903.  
  904. if (battleObj && battleObj.valid) {
  905. players[i] = {
  906. name: battleObj.character.name,
  907. imported: true,
  908. cloudData: readCloudData,
  909. battleData: JSON.stringify(battleObj),
  910. };
  911. } else {
  912. players[i] = {
  913. name: isZH ? "需要点开个人资料" : "Open profile in game",
  914. imported: true,
  915. cloudData: false,
  916. battleData: BLANK_PLAYER_JSON_STR,
  917. };
  918. }
  919. }
  920. }
  921. }
  922. // Zone
  923. zone = player.partyInfo?.party?.actionHrid;
  924. isZoneDungeon = clientData.actionDetailMap[zone]?.combatZoneInfo?.isDungeon;
  925. }
  926.  
  927. // Select zone or dungeon
  928. if (zone) {
  929. document.querySelector(`input#simDungeonToggle`).checked = isZoneDungeon;
  930. document.querySelector(`input#simDungeonToggle`).dispatchEvent(new Event("change"));
  931. let elementZone = isZoneDungeon ? document.querySelector(`select#selectDungeon`) : document.querySelector(`select#selectZone`);
  932. if (elementZone.selectedIndex <= 0) {
  933. for (let i = 0; i < elementZone.options.length; i++) {
  934. if (elementZone.options[i].value === zone) {
  935. elementZone.options[i].selected = true;
  936. break;
  937. }
  938. }
  939. }
  940. }
  941.  
  942. for (let i = 1; i <= 5; i++) {
  943. if (!players[i]) {
  944. players[i] = {
  945. name: `Player ${i}`,
  946. imported: false,
  947. cloudData: false,
  948. battleData: BLANK_PLAYER_JSON_STR,
  949. };
  950. }
  951. let aTab = document.querySelector(`a#player${i}-tab`);
  952. aTab.textContent = players[i].name;
  953. aTab.style.cssText = ''
  954. if (players[i].cloudData) {
  955. aTab.style.backgroundImage = "linear-gradient(-20deg, #00cdac 0%, #8ddad5 100%)";
  956. aTab.style.color = "black";
  957. }
  958. let checkbox = document.querySelector(`input#player${i}.form-check-input.player-checkbox`);
  959. if (checkbox) {
  960. checkbox.checked = players[i].imported;
  961. checkbox.dispatchEvent(new Event("change"));
  962. }
  963. }
  964.  
  965. document.querySelector(`a#group-combat-tab`).click();
  966. const editImport = document.querySelector(`input#inputSetGroupCombatAll`);
  967. editImport.value = JSON.stringify(Object.keys(players).reduce((acc, key) => {
  968. acc[key] = players[key].battleData;
  969. return acc;
  970. }, {}));
  971. document.querySelector(`button#buttonImportSet`).click();
  972.  
  973. // 模拟时长
  974. document.querySelector(`input#inputSimulationTime`).value = 24;
  975.  
  976. button.textContent = isZH ? "成功导入数据" : "Imported Successful";
  977. button.className = "btn btn-success";
  978. button.disabled = false;
  979. setTimeout(() => {
  980. button.textContent = preTextContent;
  981. button.className = preClassName;
  982. }, 1500);
  983.  
  984. if (!isParty) {
  985. setTimeout(() => {
  986. document.querySelector(`button#buttonStartSimulation`).click();
  987. }, 500);
  988. }
  989. }
  990.  
  991. // 监听模拟结果
  992. async function observeResultsForMWICombatSimulate() {
  993. let resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
  994. while (!resultDiv) {
  995. await new Promise((resolve) => setTimeout(resolve, 100));
  996. resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
  997. }
  998.  
  999. const deathDiv = document.querySelector(`div#simulationResultPlayerDeaths`);
  1000. const expDiv = document.querySelector(`div#simulationResultExperienceGain`);
  1001. const consumeDiv = document.querySelector(`div#simulationResultConsumablesUsed`);
  1002. deathDiv.style.backgroundColor = "#FFEAE9";
  1003. deathDiv.style.color = "black";
  1004. expDiv.style.backgroundColor = "#CDFFDD";
  1005. expDiv.style.color = "black";
  1006. consumeDiv.style.backgroundColor = "#F0F8FF";
  1007. consumeDiv.style.color = "black";
  1008.  
  1009. let div = document.createElement("div");
  1010. div.id = "tillLevel";
  1011. div.style.backgroundColor = "#FFFFE0";
  1012. div.style.color = "black";
  1013. div.textContent = "";
  1014. resultDiv.append(div);
  1015. }
  1016.  
  1017. // #endregion
  1018.  
  1019. // ==================================================
  1020. // Script Start
  1021. // ==================================================
  1022.  
  1023. if (localStorage.getItem("initClientData")) {
  1024. const obj = JSON.parse(localStorage.getItem("initClientData"));
  1025. GM_setValue("init_client_data", localStorage.getItem("initClientData"));
  1026.  
  1027. clientData.actionDetailMap = obj.actionDetailMap;
  1028. clientData.levelExperienceTable = obj.levelExperienceTable;
  1029. clientData.itemDetailMap = obj.itemDetailMap;
  1030. clientData.actionCategoryDetailMap = obj.actionCategoryDetailMap;
  1031. clientData.abilityDetailMap = obj.abilityDetailMap;
  1032. }
  1033.  
  1034. if (document.URL.includes("/MWICombatSimulatorTest/dist")) {
  1035. addImportButtonForMWICombatSimulate();
  1036. observeResultsForMWICombatSimulate();
  1037. }
  1038.  
  1039. hookWS();
  1040.  
  1041. })();

QingJ © 2025

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