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);
}
`)
@frzyc
Copy link

frzyc commented Sep 16, 2024

This is super cool!

There are some issues with the way in which you convert the display name for weapons:
image

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

@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

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