A UserScript to export your characters data from Battle Chronicle to Genshin Optimizer
Last active
October 31, 2025 15:18
-
-
Save atouu/ccf615f6ccd2d228a101118b558cd4d3 to your computer and use it in GitHub Desktop.
Battle Chronicle to GO
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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); | |
| } | |
| `) |
Author
You also broke link matching for https://act.hoyolab.com/app/community-game-records-sea/index.html#/ys with v1.2
Author
@frzyc Didn't know Violentmonkey @match was different from others, fixed it now.
How do I actually run the javascript
Author
@FantasticDanger0 I apologize for very late reply but you need either ViolentMonkey or TamperMonkey
Author
@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.
Author
Fixed, thank you very much. @nguyentvan7
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

@frzyc Fixed! Thank you!