Skip to content

Instantly share code, notes, and snippets.

@atouu
Last active June 27, 2025 02:36
Show Gist options
  • Save atouu/ccf615f6ccd2d228a101118b558cd4d3 to your computer and use it in GitHub Desktop.
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.4
// @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;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,.5);
position: fixed;
top: 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 Jun 20, 2025

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

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