A UserScript to export your characters data from Battle Chronicle to Genshin Optimizer
-
-
Save atouu/ccf615f6ccd2d228a101118b558cd4d3 to your computer and use it in GitHub Desktop.
| // ==UserScript== | |
| // @name Battle Chronicle to GO | |
| // @namespace https://github.com/atouu | |
| // @match https://act.hoyolab.com/app/community-game-records-sea/* | |
| // @exclude https://act.hoyolab.com/app/community-game-records-sea/rpg/* | |
| // @grant GM.xmlHttpRequest | |
| // @grant GM_addStyle | |
| // @version 1.5 | |
| // @author atouu | |
| // @description Export Battle Chronicle characters data to Genshin Optimizer | |
| // @downloadURL https://gist.github.com/atouu/ccf615f6ccd2d228a101118b558cd4d3/raw/bctogo.user.js | |
| // @updateURL https://gist.github.com/atouu/ccf615f6ccd2d228a101118b558cd4d3/raw/bctogo.user.js | |
| // ==/UserScript== | |
| const PROP_TO_GO = { | |
| 1: "hp", 2: "hp", 3: "hp_", 5: "atk", | |
| 6: "atk_", 7: "def", 8: "def", 9: "def_", | |
| 20: "critRate_", 22: "critDMG_", 23: "enerRech_", 26: "heal_", | |
| 28: "eleMas", 30: "physical_dmg_", 40: "pyro_dmg_", 41: "electro_dmg_", | |
| 42: "hydro_dmg_", 43: "dendro_dmg_", 44: "anemo_dmg_", 45: "geo_dmg_", | |
| 46: "cryo_dmg_" | |
| } | |
| const SLOT_TO_GO = { | |
| 1: "flower", | |
| 2: "plume", | |
| 3: "sands", | |
| 4: "goblet", | |
| 5: "circlet" | |
| } | |
| const exportBtn = document.createElement("button") | |
| exportBtn.classList.add("go-export-btn") | |
| exportBtn.innerText = "Export" | |
| exportBtn.addEventListener("click", async () => { | |
| exportBtn.innerText = "Exporting..." | |
| exportBtn.disabled = true | |
| const charDetails = await getCharacterDetails() | |
| const characters = [] | |
| const artifacts = [] | |
| const weapons = [] | |
| charDetails.data.list.forEach(t => { | |
| const charName = t.base.name.replace(' ', '') | |
| characters.push({ | |
| key: charName == "Traveler" ? "Traveler" + t.base.element : charName, | |
| level: t.base.level, | |
| constellation: t.base.actived_constellation_num, | |
| talent: calcTalents(t.skills, t.constellations) | |
| }) | |
| t.relics.forEach(u => { | |
| artifacts.push({ | |
| setKey: toPascalCase(u.set.name), | |
| slotKey: SLOT_TO_GO[u.pos], | |
| rarity: u.rarity, | |
| level: u.level, | |
| mainStatKey: PROP_TO_GO[u.main_property.property_type], | |
| location: charName, | |
| substats: u.sub_property_list.map(x => ({ | |
| key: PROP_TO_GO[x.property_type], | |
| value: parseFloat(x.value.replace('%', '')) | |
| })) | |
| }) | |
| }) | |
| weapons.push({ | |
| key: toPascalCase(t.weapon.name), | |
| level: t.weapon.level, | |
| ascension: t.weapon.promote_level, | |
| refinement: t.weapon.affix_level, | |
| location: charName, | |
| lock: false | |
| }) | |
| }) | |
| showExportDialog(JSON.stringify({ | |
| format: "GOOD", | |
| version: 2, | |
| source: "Battle Chronicle to GO", | |
| characters: characters, | |
| artifacts: artifacts, | |
| weapons: weapons | |
| }, null, 2)) | |
| exportBtn.innerText = "Export" | |
| exportBtn.disabled = false | |
| }) | |
| const observer = new MutationObserver(() => { | |
| const header = document.querySelector(".mhy-hoyolab-header__right, .nav-bar div.right") | |
| if (!header) return | |
| observer.disconnect(); | |
| header.prepend(exportBtn) | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| async function getCharacterDetails() { | |
| const getChars = await gmXHRAsync({ | |
| method: "POST", | |
| url: "https://bbs-api-os.hoyolab.com/game_record/genshin/api/character/list", | |
| headers: { | |
| "Accept": "application/json, text/plain, */*", | |
| "Content-Type": "application/json;charset=utf-8", | |
| "x-rpc-language": "en-us", | |
| "x-rpc-lang": "en-us", | |
| }, | |
| data: JSON.stringify({ | |
| role_id: unsafeWindow._gs_.state.crtRole.game_uid, | |
| server: unsafeWindow._gs_.state.crtRole.region | |
| }) | |
| }) | |
| const charIds = JSON.parse(getChars.responseText).data.list.map(v => v.id) | |
| const getDetails = await gmXHRAsync({ | |
| method: "POST", | |
| url: "https://bbs-api-os.hoyolab.com/game_record/genshin/api/character/detail", | |
| headers: { | |
| "Accept": "application/json, text/plain, */*", | |
| "Content-Type": "application/json;charset=utf-8", | |
| "x-rpc-language": "en-us", | |
| "x-rpc-lang": "en-us", | |
| }, | |
| data: JSON.stringify({ | |
| character_ids: charIds, | |
| role_id: unsafeWindow._gs_.state.crtRole.game_uid, | |
| server: unsafeWindow._gs_.state.crtRole.region | |
| }) | |
| }) | |
| return (JSON.parse(getDetails.responseText)) | |
| } | |
| function showExportDialog(content) { | |
| const dialogHtml = ` | |
| <div class="go-export-backdrop"> | |
| <div class="go-export-dialog"> | |
| <h1>Battle Chronicle to GO</h1> | |
| <p>Copy and paste the JSON below into Genshin Optimizer. Note that if your character was ascended and stays at level 20, 40, 50, 60, | |
| 70 or 80, you need to manually set their ascension level in Genshin Optimizer as battle chronicle doesn't have a data about it.</p> | |
| <textarea onfocus="this.select()" readonly></textarea> | |
| <span>Click anywhere outside to close.</span> | |
| </div> | |
| </div> | |
| ` | |
| document.body.insertAdjacentHTML("beforeend", dialogHtml) | |
| const backdrop = document.querySelector(".go-export-backdrop") | |
| backdrop.addEventListener("click", (e) => { | |
| if (e.target == backdrop) { | |
| backdrop.remove() | |
| } | |
| }) | |
| const textarea = document.querySelector(".go-export-dialog textarea") | |
| textarea.value = content | |
| } | |
| function calcTalents(talents, cons) { | |
| const consEffects = [2,4].map(e => cons[e]?.is_actived ? cons[e].effect : null ).toString() | |
| const levels = talents.map(e => consEffects.includes(e.name) ? e.level - 3 : e.level) | |
| return { | |
| auto: levels[0], | |
| skill: levels[1], | |
| burst: levels[2] | |
| } | |
| } | |
| function toPascalCase(s) { | |
| return s.replace(/"|'/g, '').split(/ |-/).map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('') | |
| } | |
| function gmXHRAsync(options) { | |
| return new Promise((resolve, reject) => { | |
| GM.xmlHttpRequest({ | |
| ...options, | |
| onload: (response) => resolve(response), | |
| onerror: (error) => reject(error), | |
| ontimeout: (timeout) => reject(timeout) | |
| }); | |
| }); | |
| } | |
| /** Styles **/ | |
| GM_addStyle(` | |
| body:has(.go-export-backdrop) { | |
| overflow: hidden; | |
| } | |
| .go-export-btn { | |
| cursor: pointer; | |
| border: none; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 12px; | |
| height: 32px; | |
| border-radius: 16px; | |
| margin-right: 16px; | |
| background-color: #343746; | |
| color: #8592a3; | |
| font-size: 14px; | |
| font-family: SFProText-Semibold,SFProText,sans-serif; | |
| font-weight: 600; | |
| } | |
| .go-export-backdrop { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background-color: rgba(0,0,0,.5); | |
| position: fixed; | |
| inset: 0; | |
| z-index: 9999; | |
| } | |
| .go-export-dialog { | |
| display: flex; | |
| flex-flow: column; | |
| gap: 0.5em; | |
| border-radius: 1em; | |
| margin: 1em; | |
| padding: 1em; | |
| background: rgba(0,0,0,.48); | |
| max-width: 45em; | |
| backdrop-filter: blur(2em); | |
| z-index: 999; | |
| } | |
| .go-export-dialog h1 { | |
| font-size: 1.5em; | |
| color: hsla(0,0%,100%,.85); | |
| } | |
| .go-export-dialog p { | |
| color: hsla(0,0%,100%,.75); | |
| } | |
| .go-export-dialog textarea { | |
| display: block; | |
| width: 100%; | |
| height: 20em; | |
| resize: none; | |
| font-family: monospace, monospace; | |
| } | |
| .go-export-dialog span { | |
| color: hsla(0,0%,100%,.45); | |
| } | |
| `) |
@frzyc Fixed! Thank you!
You also broke link matching for https://act.hoyolab.com/app/community-game-records-sea/index.html#/ys with v1.2
@frzyc Didn't know Violentmonkey @match was different from others, fixed it now.
How do I actually run the javascript
@FantasticDanger0 I apologize for very late reply but you need either ViolentMonkey or TamperMonkey
@mowkibowki I accidentally removed your comment oops I apologize. Iirc you need to enable userscript permission in extension's setting to make userscripts work: https://www.tampermonkey.net/faq.php?locale=en#Q209
On line 174, Manekin has no constellations, so you will get an undefined reference on cons[e].is_actived. Changing to cons[e]?.is_actived should fix it.
Fixed, thank you very much. @nguyentvan7

This is super cool!
There are some issues with the way in which you convert the display name for weapons:

Here is a list of GO keys for weapons. https://github.com/frzyc/genshin-optimizer/blob/master/libs/gi/consts/src/weapon.ts