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

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

  1. // ==UserScript==
  2. // @name [MWI] Realtime Import Of Battle Simulation
  3. // @name:zh-CN [银河奶牛]战斗模拟实时导入
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.2.6
  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. if (!firstImport) {
  828. let userConfirm = window.confirm(isZH ? "是否要覆盖当前数据" : "Do you want to overwrite the current data?");
  829. if (!userConfirm) {
  830. return;
  831. }
  832. }
  833. firstImport = false;
  834.  
  835. let preTextContent = button.textContent;
  836. let preClassName = button.className;
  837. button.textContent = isZH ? "正在导入数据..." : "Importing...";
  838. button.className = "btn btn-warning";
  839. button.disabled = true;
  840.  
  841. clientData = getInitClientData();
  842. let player = getCurrentPlayerData();
  843.  
  844. 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}}`;
  845.  
  846. const players = {};
  847. let isParty = false;
  848. let zone = "/actions/combat/fly";
  849. let isZoneDungeon = false;
  850.  
  851. if (!player?.partyInfo?.partySlotMap) {
  852. // 个人
  853. players[1] = {
  854. name: player.character.name,
  855. imported: true,
  856. cloudData: false,
  857. battleData: JSON.stringify(player.battleObj),
  858. };
  859. // Zone
  860. for (const action of player.characterActions) {
  861. if (action && action.actionHrid.includes("/actions/combat/")) {
  862. zone = action.actionHrid;
  863. isZoneDungeon = clientData.actionDetailMap[action.actionHrid]?.combatZoneInfo?.isDungeon;
  864. break;
  865. }
  866. }
  867. } else {
  868. // 队伍
  869. isParty = true;
  870. let i = 0;
  871. for (const member of Object.values(player.partyInfo.partySlotMap)) {
  872. i++;
  873. if (member.characterID) {
  874. if (member.characterID === player.character.id) {
  875. players[i] = {
  876. name: player.character.name,
  877. imported: true,
  878. cloudData: false,
  879. battleData: JSON.stringify(player.battleObj),
  880. };
  881. } else {
  882. let memberData = getPlayerData(member.characterID);
  883. let battleObj = memberData?.battleObj;
  884.  
  885. if (readCloudData) {
  886. // 读取共享Trigger数据
  887. let sharedTextDBStr = await getDataFromTextDB(getPlayerUniqueKey(member.characterID));
  888. if (sharedTextDBStr) {
  889. let sharedTextDB = JSON.parse(sharedTextDBStr);
  890. if (battleObj) {
  891. battleObj.triggerMap = {
  892. ...battleObj.triggerMap,
  893. ...sharedTextDB.triggerMap
  894. }
  895. } else {
  896. battleObj = sharedTextDB;
  897. }
  898. } else {
  899. readCloudData = false;
  900. }
  901. }
  902.  
  903. if (battleObj && battleObj.valid) {
  904. players[i] = {
  905. name: battleObj.character.name,
  906. imported: true,
  907. cloudData: readCloudData,
  908. battleData: JSON.stringify(battleObj),
  909. };
  910. } else {
  911. players[i] = {
  912. name: isZH ? "需要点开个人资料" : "Open profile in game",
  913. imported: true,
  914. cloudData: false,
  915. battleData: BLANK_PLAYER_JSON_STR,
  916. };
  917. }
  918. }
  919. }
  920. }
  921. // Zone
  922. zone = player.partyInfo?.party?.actionHrid;
  923. isZoneDungeon = clientData.actionDetailMap[zone]?.combatZoneInfo?.isDungeon;
  924. }
  925.  
  926. // Select zone or dungeon
  927. if (zone) {
  928. document.querySelector(`input#simDungeonToggle`).checked = isZoneDungeon;
  929. document.querySelector(`input#simDungeonToggle`).dispatchEvent(new Event("change"));
  930. let elementZone = isZoneDungeon ? document.querySelector(`select#selectDungeon`) : document.querySelector(`select#selectZone`);
  931. if (elementZone.selectedIndex <= 0) {
  932. for (let i = 0; i < elementZone.options.length; i++) {
  933. if (elementZone.options[i].value === zone) {
  934. elementZone.options[i].selected = true;
  935. break;
  936. }
  937. }
  938. }
  939. }
  940.  
  941. for (let i = 1; i <= 5; i++) {
  942. if (!players[i]) {
  943. players[i] = {
  944. name: `Player ${i}`,
  945. imported: false,
  946. cloudData: false,
  947. battleData: BLANK_PLAYER_JSON_STR,
  948. };
  949. }
  950. let aTab = document.querySelector(`a#player${i}-tab`);
  951. aTab.textContent = players[i].name;
  952. aTab.style.cssText = ''
  953. if (players[i].cloudData) {
  954. aTab.style.backgroundImage = "linear-gradient(-20deg, #00cdac 0%, #8ddad5 100%)";
  955. aTab.style.color = "black";
  956. }
  957. let checkbox = document.querySelector(`input#player${i}.form-check-input.player-checkbox`);
  958. if (checkbox) {
  959. checkbox.checked = players[i].imported;
  960. checkbox.dispatchEvent(new Event("change"));
  961. }
  962. }
  963.  
  964. document.querySelector(`a#group-combat-tab`).click();
  965. const editImport = document.querySelector(`input#inputSetGroupCombatAll`);
  966. editImport.value = JSON.stringify(Object.keys(players).reduce((acc, key) => {
  967. acc[key] = players[key].battleData;
  968. return acc;
  969. }, {}));
  970. document.querySelector(`button#buttonImportSet`).click();
  971.  
  972. // 模拟时长
  973. document.querySelector(`input#inputSimulationTime`).value = 24;
  974.  
  975. button.textContent = isZH ? "成功导入数据" : "Imported Successful";
  976. button.className = "btn btn-success";
  977. button.disabled = false;
  978. setTimeout(() => {
  979. button.textContent = preTextContent;
  980. button.className = preClassName;
  981. }, 1500);
  982.  
  983. if (!isParty) {
  984. setTimeout(() => {
  985. document.querySelector(`button#buttonStartSimulation`).click();
  986. }, 500);
  987. }
  988. }
  989.  
  990. // 监听模拟结果
  991. async function observeResultsForMWICombatSimulate() {
  992. let resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
  993. while (!resultDiv) {
  994. await new Promise((resolve) => setTimeout(resolve, 100));
  995. resultDiv = document.querySelector(`div.row`)?.querySelectorAll(`div.col-md-5`)?.[2]?.querySelector(`div.row > div.col-md-5`);
  996. }
  997.  
  998. const deathDiv = document.querySelector(`div#simulationResultPlayerDeaths`);
  999. const expDiv = document.querySelector(`div#simulationResultExperienceGain`);
  1000. const consumeDiv = document.querySelector(`div#simulationResultConsumablesUsed`);
  1001. deathDiv.style.backgroundColor = "#FFEAE9";
  1002. deathDiv.style.color = "black";
  1003. expDiv.style.backgroundColor = "#CDFFDD";
  1004. expDiv.style.color = "black";
  1005. consumeDiv.style.backgroundColor = "#F0F8FF";
  1006. consumeDiv.style.color = "black";
  1007.  
  1008. let div = document.createElement("div");
  1009. div.id = "tillLevel";
  1010. div.style.backgroundColor = "#FFFFE0";
  1011. div.style.color = "black";
  1012. div.textContent = "";
  1013. resultDiv.append(div);
  1014. }
  1015.  
  1016. // #endregion
  1017.  
  1018. // ==================================================
  1019. // Script Start
  1020. // ==================================================
  1021.  
  1022. if (localStorage.getItem("initClientData")) {
  1023. const obj = JSON.parse(localStorage.getItem("initClientData"));
  1024. GM_setValue("init_client_data", localStorage.getItem("initClientData"));
  1025.  
  1026. clientData.actionDetailMap = obj.actionDetailMap;
  1027. clientData.levelExperienceTable = obj.levelExperienceTable;
  1028. clientData.itemDetailMap = obj.itemDetailMap;
  1029. clientData.actionCategoryDetailMap = obj.actionCategoryDetailMap;
  1030. clientData.abilityDetailMap = obj.abilityDetailMap;
  1031. }
  1032.  
  1033. if (document.URL.includes("/MWICombatSimulatorTest/dist")) {
  1034. addImportButtonForMWICombatSimulate();
  1035. observeResultsForMWICombatSimulate();
  1036. }
  1037.  
  1038. hookWS();
  1039.  
  1040. })();

QingJ © 2025

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