nike / Kamihime Project R - Raid log summary live

// ==UserScript==
// @name         Kamihime Project R - Raid log summary live
// @description  Shows summary table of raid participants' Damage/Actions as overlay in battle
// @updateURL    https://openuserjs.org/meta/nike/Kamihime_Project_R_-_Raid_log_summary_live.meta.js
// @license      MIT
// @match        https://gnkh-api-r.prod.nkh.dmmgames.com/front/cocos2d-proj/components-pc/game/app.html
// @run-at       document-start
// ==/UserScript==
(function() {

    var fontSize = 12
	
    var intervalRaidMessages = setInterval(function() {
        if (typeof kh !== 'undefined' && kh.RaidMessageHandler && kh.BattleWorld && kh.RaidScenarioPlayer) {
            clearInterval(intervalRaidMessages);

            if (kh.raidMessages) {
                return;
            }

            kh.raidMessages = {}

            //object model: 
            //{
            //    "job_id": 36,
            //    "job_skin": 0,
            //    "line_1_action": "Musyi's party ",
            //    "line_2_action": " activated Multiple Erosion",
            //    "is_mine": false,
            //    "line_3_damage_result": "312,063 damage dealt",
            //    "line_4_effect_result": "",
            //    "timeStamp": 1672191944260
            //}

            kh.RaidMessageHandler.prototype._postLog = function(message) {
                var battleId = cc?.director?._runningScene?.getBattleId() || 0
                var battleLog = kh.raidMessages[battleId]
                if (!battleLog) {
                    kh.raidMessages = {}
                    kh.raidMessages[battleId] = []
                    battleLog = kh.raidMessages[battleId]
                }
                message.timeStamp = Date.now();
                var existingMessage = battleLog.find((oldMsg) => {
                    if (oldMsg.job_id != message.job_id || oldMsg.job_skin != message.job_skin || oldMsg.line_1_action != message.line_1_action || oldMsg.line_2_action != message.line_2_action || oldMsg.is_mine != message.is_mine || oldMsg.line_3_damage_result != message.line_3_damage_result || oldMsg.line_4_effect_result != message.line_4_effect_result) {
                        return false;
                    }
                    var timeStampDiff = Math.abs(oldMsg.timeStamp - message.timeStamp)
                    if (timeStampDiff < 50) { // detect duplicate messages
                        return true;
                    }
                    return false;

                })
                if (!existingMessage) {
                    battleLog.push(message)
                }
                kh.createInstance("battleUI").raidMessageWindow.postMessage({
                        job_id: message.job_id,
                        job_skin: message.job_skin
                    },
                    message.line_1_action || "",
                    message.line_2_action || "",
                    message.line_3_damage_result || "",
                    message.line_4_effect_result || "",
                    message.play_sound
                );
            };



            var orig_start = kh.BattleWorld.prototype._start;

            kh.BattleWorld.prototype._start = async function new_start(sceneInstanceId) {
                this.UIBackLayer.getChildren()[1]?.getChildren()[0]?.setOpacity(0)
                kh.BattleWorld.prototype.updateRaidLog(cc?.director?._runningScene?.getBattleId())
                return orig_start.apply(this, [sceneInstanceId]);
            }

            kh.BattleWorld.prototype.updateRaidLog = function(battleId) {
                if (!this.UIBackLayer) {
                    return;
                }
                if (!this.raidLogNames) {
                    this.raidLogNames = new ccui.Text()
                    this.raidLogNames.setName("raidLogNames")
                    this.raidLogNames.setFontSize(fontSize);
                    this.raidLogNames.setAnchorPoint(0, 0)
                    this.raidLogNames.setPosition(10, 80)
                    this.raidLogNames.enableOutline(cc.color.BLACK, 2)
                    this.raidLogNames.setFontName("GameFont")
                    this.raidLogTotalDamage = new ccui.Text()
                    this.raidLogTotalDamage.setName("raidLogTotalDamage")
                    this.raidLogTotalDamage.setFontSize(fontSize);
                    this.raidLogTotalDamage.setAnchorPoint(0, 0)
                    this.raidLogTotalDamage.setPosition(10, 80)
                    this.raidLogTotalDamage.enableOutline(cc.color.BLACK, 2)
                    this.raidLogTotalDamage.setFontName("GameFont")
                    this.raidLogNatkDamage = new ccui.Text()
                    this.raidLogNatkDamage.setName("raidLogNatkDamage")
                    this.raidLogNatkDamage.setFontSize(fontSize);
                    this.raidLogNatkDamage.setAnchorPoint(0, 0)
                    this.raidLogNatkDamage.setPosition(10, 80)
                    this.raidLogNatkDamage.enableOutline(cc.color.BLACK, 2)
                    this.raidLogNatkDamage.setFontName("GameFont")

                    this.raidLogBurstDamage = new ccui.Text()
                    this.raidLogBurstDamage.setName("raidLogBurstDamage")
                    this.raidLogBurstDamage.setFontSize(fontSize);
                    this.raidLogBurstDamage.setAnchorPoint(0, 0)
                    this.raidLogBurstDamage.setPosition(10, 80)
                    this.raidLogBurstDamage.enableOutline(cc.color.BLACK, 2)
                    this.raidLogBurstDamage.setFontName("GameFont")

                    this.raidLogAbilityDamage = new ccui.Text()
                    this.raidLogAbilityDamage.setName("raidLogAbilityDamage")
                    this.raidLogAbilityDamage.setFontSize(fontSize);
                    this.raidLogAbilityDamage.setAnchorPoint(0, 0)
                    this.raidLogAbilityDamage.setPosition(10, 80)
                    this.raidLogAbilityDamage.enableOutline(cc.color.BLACK, 2)
                    this.raidLogAbilityDamage.setFontName("GameFont")

                    this.raidLogEidolonDamage = new ccui.Text()
                    this.raidLogEidolonDamage.setName("raidLogEidolonDamage")
                    this.raidLogEidolonDamage.setFontSize(fontSize);
                    this.raidLogEidolonDamage.setAnchorPoint(0, 0)
                    this.raidLogEidolonDamage.setPosition(10, 80)
                    this.raidLogEidolonDamage.enableOutline(cc.color.BLACK, 2)
                    this.raidLogEidolonDamage.setFontName("GameFont")

                    this.raidLogSpeed = new ccui.Text()
                    this.raidLogSpeed.setName("raidLogSpeed")
                    this.raidLogSpeed.setFontSize(fontSize);
                    this.raidLogSpeed.setAnchorPoint(0, 0)
                    this.raidLogSpeed.setPosition(10, 80)
                    this.raidLogSpeed.enableOutline(cc.color.BLACK, 2)
                    this.raidLogSpeed.setFontName("GameFont")

                    this.raidLogLayout = new ccui.Layout()
                    this.raidLogLayout.setName("raidLogLayout")
                    this.raidLogLayout.setAnchorPoint(0, 0)
                    this.raidLogLayout.setPosition(10, 80)
                    this.raidLogLayout.setBackGroundColorType(ccui.Layout.BG_COLOR_SOLID)
                    this.raidLogLayout.setOpacity(128)
                    this.raidLogLayout.setBackGroundColor(cc.color(0, 0, 0))
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogLayout)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogNames)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogTotalDamage)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogNatkDamage)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogBurstDamage)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogAbilityDamage)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogEidolonDamage)
                    this.UIBackLayer.getChildren()[1]?.addChild(this.raidLogSpeed)
                }

                var battleEntries = kh.raidMessages[battleId]
                if (!battleEntries) {
                    return
                }
				
				var repo = kh.createInstance("myselfInfoRepository")
				repo.getMe().then((me) => {
				
                var grouped = _.groupBy(battleEntries, (entry) => entry.line_1_action.trim() + "|" + entry.is_mine)
                var data = Object.entries(grouped).map((group) => {
				    var split = group[0].split("|")
                    var player = split[0]
					var isMine = split[1] == "true"
                    var data = group[1]
                    var totalEntries = data.length
                    var totalDamage = data.filter((entry) => (entry.line_3_damage_result || "").includes("damage dealt"))
                        .map((entry) => parseInt(entry.line_3_damage_result.replace(" damage dealt", "").replaceAll(",", "")) || 0)
                        .reduce((a, b) => a + b, 0)

                    var normalAttackEntries = data.filter((entry) => (entry.line_2_action || "") == "Attacked")
                    var normalAttacksDamage = normalAttackEntries
                        .map((entry) => parseInt(entry.line_3_damage_result.replace(" damage dealt", "").replaceAll(",", "")) || 0)
                        .reduce((a, b) => a + b, 0)
                    var normalAttackCount = normalAttackEntries.length

                    var burstAttackEntries = data.filter((entry) => (entry.line_2_action || "") == "Burst Activated")
                    var burstAttacksDamage = burstAttackEntries
                        .map((entry) => parseInt(entry.line_3_damage_result.replace(" damage dealt", "").replaceAll(",", "")) || 0)
                        .reduce((a, b) => a + b, 0)
                    var burstAttackCount = burstAttackEntries.length

                    var eidolonAttackEntries = data.filter((entry) => (entry.line_2_action || "") != "Burst Activated" && (entry.line_2_action || "").endsWith("Activated"))
                    var eidolonAttacksDamage = eidolonAttackEntries
                        .map((entry) => parseInt(entry.line_3_damage_result.replace(" damage dealt", "").replaceAll(",", "")) || 0)
                        .reduce((a, b) => a + b, 0)
                    var eidolonAttackCount = eidolonAttackEntries.length

                    var abilityAttackEntries = data.filter((entry) => (entry.line_2_action || "").startsWith(" activated"))
                    var abilityAttacksDamage = abilityAttackEntries
                        .map((entry) => parseInt(entry.line_3_damage_result.replace(" damage dealt", "").replaceAll(",", "")) || 0)
                        .reduce((a, b) => a + b, 0)
                    var abilityAttackCount = abilityAttackEntries.length
                    var speed = undefined
                    if (data.length > 3) {
                        var oldestEntry = data.reduce(function(prev, current) {
                            return (prev.timeStamp < current.timeStamp) ? prev : current
                        }).timeStamp
                        var newestEntry = data.reduce(function(prev, current) {
                            return (prev.timeStamp > current.timeStamp) ? prev : current
                        }).timeStamp
                        speed = ((newestEntry - oldestEntry) / (data.length - 1)) / 1000
                    }

                    var defeated = !!data.find((entry) => entry.line_2_action == "has been defeated.")
                    return {
                        player: player.replace("'s party", ""),
                        damage: totalDamage,
                        actions: totalEntries,
                        defeated: defeated,
                        normalAttackCount: normalAttackCount,
                        normalAttackDamage: normalAttacksDamage,
                        burstAttackCount: burstAttackCount,
                        burstAttackDamage: burstAttacksDamage,
                        eidolonAttackCount: eidolonAttackCount,
                        eidolonAttackDamage: eidolonAttacksDamage,
                        abilityAttackCount: abilityAttackCount,
                        abilityAttackDamage: abilityAttacksDamage,
                        speed: speed,
						isMine: isMine

                    }
                })
                var sortedByDamage = data.sort((a, b) => (a.damage < b.damage) ? 1 : -1)
                //"Player: 125.6M  84.4M⚔(3)  3💥(84.4M)  16☄️(54.3M)  7🐦(0.4M)  5.6🏃
                var namesText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    var defeated = entry.defeated
                    if (defeated) {
                        entryText += "☠ "
                    }
					if(entry.isMine) {
						var playerName = me.plainBody.name
					    entryText += "⭐" + playerName
					} else {
					    entryText += entry.player
					}
                    namesText += entryText
                    namesText += "\n"
                })
                var totalDamageText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    var damageText = entry.damage //.toLocaleString(entry.damage)
                    var actions = entry.actions
                    var damageTextMillions = ((damageText / 1000000).toFixed(1)).replace(".0", "")
                    entryText += damageTextMillions + "M"
                    totalDamageText += entryText
                    totalDamageText += "\n"
                })
                var natkDamageText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    if (entry.normalAttackCount > 0) {
                        var millionsStr = ((entry.normalAttackDamage / 1000000).toFixed(1)).replace(".0", "")
                        entryText += "⚔️" + millionsStr + "M" + " (" + entry.normalAttackCount + ")"
                    }
                    natkDamageText += entryText
                    natkDamageText += "\n"
                })
                var burstDamageText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    if (entry.burstAttackCount > 0) {
                        var millionsStr = ((entry.burstAttackDamage / 1000000).toFixed(1)).replace(".0", "")
                        entryText += "💥" + millionsStr + "M" + " (" + entry.burstAttackCount + ")"
                    }
                    burstDamageText += entryText
                    burstDamageText += "\n"
                })
                var abilityDamageText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    if (entry.abilityAttackCount > 0) {
                        var millionsStr = ((entry.abilityAttackDamage / 1000000).toFixed(1)).replace(".0", "")
                        entryText += "☄️" + millionsStr + "M" + " (" + entry.abilityAttackCount + ")"
                    }
                    abilityDamageText += entryText
                    abilityDamageText += "\n"
                })
                var eidolonDamageText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    if (entry.eidolonAttackCount > 0) {
                        entryText += "🐦" + entry.eidolonAttackCount
                    }
                    entryText = entryText.trim()
                    eidolonDamageText += entryText
                    eidolonDamageText += "\n"
                })
                var speedText = ""
                sortedByDamage.forEach((entry) => {
                    var entryText = ""
                    if (entry.speed) {
                        entryText += "🏃" + entry.speed.toFixed(1)
                    }
                    speedText += entryText
                    speedText += "\n"
                })
                namesText = namesText.replace(/\n$/, "")
                totalDamageText = totalDamageText.replace(/\n$/, "")
                natkDamageText = natkDamageText.replace(/\n$/, "")
                burstDamageText = burstDamageText.replace(/\n$/, "")
                abilityDamageText = abilityDamageText.replace(/\n$/, "")
                eidolonDamageText = eidolonDamageText.replace(/\n$/, "")
                speedText = speedText.replace(/\n$/, "")
                var spacing = 5
                this.raidLogNames.setText(namesText);
                this.raidLogTotalDamage.setText(totalDamageText)
                this.raidLogTotalDamage.setPosition(10 + this.raidLogNames.width + spacing, 80)
                this.raidLogNatkDamage.setText(natkDamageText)
                this.raidLogNatkDamage.setPosition(10 + this.raidLogNames.width + spacing + this.raidLogTotalDamage.width + spacing, 80)
                this.raidLogBurstDamage.setText(burstDamageText)
                this.raidLogBurstDamage.setPosition(10 + this.raidLogNames.width + spacing + this.raidLogTotalDamage.width + spacing + this.raidLogNatkDamage.width + spacing, 80)
                this.raidLogAbilityDamage.setText(abilityDamageText)
                this.raidLogAbilityDamage.setPosition(10 + this.raidLogNames.width + spacing + this.raidLogTotalDamage.width + spacing + this.raidLogNatkDamage.width + spacing + this.raidLogBurstDamage.width + spacing, 80)
                this.raidLogEidolonDamage.setText(eidolonDamageText)
                this.raidLogEidolonDamage.setPosition(10 + this.raidLogNames.width + spacing + this.raidLogTotalDamage.width + spacing + this.raidLogNatkDamage.width + spacing + this.raidLogBurstDamage.width + spacing + this.raidLogAbilityDamage.width + spacing, 80)
                this.raidLogSpeed.setText(speedText)
                this.raidLogSpeed.setPosition(10 + this.raidLogNames.width + spacing + this.raidLogTotalDamage.width + spacing + this.raidLogNatkDamage.width + spacing + this.raidLogBurstDamage.width + spacing + this.raidLogAbilityDamage.width + spacing + this.raidLogEidolonDamage.width + spacing, 80)
                var totalWidth = this.raidLogNames.width + spacing + this.raidLogTotalDamage.width + spacing + this.raidLogNatkDamage.width + spacing + this.raidLogBurstDamage.width + spacing + this.raidLogAbilityDamage.width + spacing + this.raidLogEidolonDamage.width + spacing + this.raidLogSpeed.width
                this.raidLogLayout.setContentSize(totalWidth, this.raidLogNames.height);
				
				})
            }

            // simulate current player entries
            kh.RaidScenarioPlayer.prototype.enqueueOwnScenario = function(scenarioData) {
                if (!scenarioData || !scenarioData.length) {
                    return Q.resolve();
                }
                var ability = scenarioData.find((entry) => entry.cmd == "ability" && entry.from == "player") // contains damage
                var damage = scenarioData.filter((entry) => entry.cmd == "damage")
                var attacks = scenarioData.filter((entry) => entry.cmd == "attack" && entry.from == "player") // each attack contains damage
                var summons = scenarioData.filter((entry) => entry.cmd == "summon_damage") // each attack contains damage
                var bursts = scenarioData.filter((entry) => entry.cmd == "burst" && entry.from == "player") // each attack contains damage
				var burstStreak = scenarioData.filter((entry) => entry.cmd == "burst_streak")
                var bonusDamage = damage.map((e) => e.damage).flat(2).filter((e)=>e.to =="enemy").map((e) => e.value).reduce((a, b) => a + b, 0)
                var attackDamage = attacks.map((e) => e.damage).flat(2).map((e) => e.value).reduce((a, b) => a + b, 0)
                var summonDamage = summons.map((e) => e.damage).flat(2).map((e) => e.value).reduce((a, b) => a + b, 0)
				var burstStreakDamage = burstStreak.map((e) => e.damage).flat(2).map((e) => e.value).reduce((a, b) => a + b, 0)
                var totalDamage = bonusDamage + attackDamage + summonDamage + burstStreakDamage
                var raidLogModel = undefined
                var commaNumber = totalDamage.toLocaleString()
                var damageMessage = (totalDamage) ? (commaNumber + " damage dealt") : ""
                var timeStamp = Date.now()
                if (summons.length > 0) {
                    raidLogModel = {
                        "job_id": -1,
                        "job_skin": 0,
                        "line_1_action": "You's party ",
                        "line_2_action": "Eidolon Activated",
                        "is_mine": true,
                        "line_3_damage_result": damageMessage,
                        "line_4_effect_result": "",
                        "timeStamp": timeStamp
                    }
                } else if (bursts.length > 0) {
                    raidLogModel = {
                        "job_id": -1,
                        "job_skin": 0,
                        "line_1_action": "You's party ",
                        "line_2_action": "Burst Activated",
                        "is_mine": true,
                        "line_3_damage_result": damageMessage,
                        "line_4_effect_result": "",
                        "timeStamp": timeStamp
                    }
                } else if (ability) {
                    raidLogModel = {
                        "job_id": -1,
                        "job_skin": 0,
                        "line_1_action": "You's party ",
                        "line_2_action": " activated " + ability.name,
                        "is_mine": true,
                        "line_3_damage_result": damageMessage,
                        "line_4_effect_result": "",
                        "timeStamp": timeStamp
                    }
                } else if (attacks.length > 0) {
                    raidLogModel = {
                        "job_id": -1,
                        "job_skin": 0,
                        "line_1_action": "You's party ",
                        "line_2_action": "Attacked",
                        "is_mine": true,
                        "line_3_damage_result": damageMessage,
                        "line_4_effect_result": "",
                        "timeStamp": timeStamp
                    }
                }
                if (raidLogModel) {
                    var message = raidLogModel
                    var battleId = cc?.director?._runningScene?.getBattleId() || 0
                    var battleLog = kh.raidMessages[battleId]
                    if (!battleLog) {
                        kh.raidMessages = {}
                        kh.raidMessages[battleId] = []
                        battleLog = kh.raidMessages[battleId]
                    }
                    message.timeStamp = Date.now();
                    var existingMessage = battleLog.find((oldMsg) => {
                        if (oldMsg.job_id != message.job_id || oldMsg.job_skin != message.job_skin || oldMsg.line_1_action != message.line_1_action || oldMsg.line_2_action != message.line_2_action || oldMsg.is_mine != message.is_mine || oldMsg.line_3_damage_result != message.line_3_damage_result || oldMsg.line_4_effect_result != message.line_4_effect_result) {
                            return false;
                        }
                        var timeStampDiff = Math.abs(oldMsg.timeStamp - message.timeStamp)
                        if (timeStampDiff < 50) { // detect duplicate messages
                            return true;
                        }
                        return false;

                    })
                    if (!existingMessage) {
                        battleLog.push(message)
                    }
                }
                return this._assignDataToChannels(scenarioData, /* isOwnScenario */ true);
            }

            setInterval(() => {
                if (kh) {
                    var battleId = cc?.director?._runningScene?.getBattleId?.()
                    if (battleId) {
                        kh.createInstance("battleWorld")?.updateRaidLog(battleId)
                    }
                }
            }, 1000)

        }
    }, 10);

})();