Skip to content

Instantly share code, notes, and snippets.

@atouu
Last active October 31, 2025 15:18
Show Gist options
  • Select an option

  • Save atouu/ccf615f6ccd2d228a101118b558cd4d3 to your computer and use it in GitHub Desktop.

Select an option

Save atouu/ccf615f6ccd2d228a101118b558cd4d3 to your computer and use it in GitHub Desktop.
Battle Chronicle to GO
// ==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);
}
`)
@atouu
Copy link
Author

atouu commented Sep 16, 2024

@frzyc Fixed! Thank you!

@frzyc
Copy link

frzyc commented Sep 16, 2024

You also broke link matching for https://act.hoyolab.com/app/community-game-records-sea/index.html#/ys with v1.2

@atouu
Copy link
Author

atouu commented Sep 16, 2024

@frzyc Didn't know Violentmonkey @match was different from others, fixed it now.

@FantasticDanger0
Copy link

How do I actually run the javascript

@atouu
Copy link
Author

atouu commented Jun 20, 2025

@FantasticDanger0 I apologize for very late reply but you need either ViolentMonkey or TamperMonkey

@atouu
Copy link
Author

atouu commented Aug 22, 2025

@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

@nguyentvan7
Copy link

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.

@atouu
Copy link
Author

atouu commented Oct 31, 2025

Fixed, thank you very much. @nguyentvan7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment