发送模板
当设置为使用自定义模板时,此脚本文件将指导 League Akari 所发送的内容。
类型定义可参照:/src/main/shards/in-game-send/env-type.ts
。
悬崖勒马
在真正发送之前,可以先使用 试运行
查看效果。
下述为预定义的模板,您可以根据自己的需求进行修改。复制到设置项位置,并点击 更新
即可使用。
牛马发送模板
部分实现自:hh-lol-prophet
<%
/* 计算方式来自:[hh-lol-prophet](https://github.com/real-web-world/hh-lol-prophet)
*
* 目前该评分模板文件仅适用于普通模式, 斗魂竞技场无法使用
*/
%>
<%
// true 或 false, 开启后敌方牛马会变成没有马
const 将敌方牛马转为没有马 = true
// true 或 false, 开启后将展示双方的开黑情况
const 发送开黑信息 = true
const RANKING_SCORE = { 0: 10, 1: 5, 2: 0, 3: -5, 4: -10 }
const NIUMA_NAME = {
S: '通天代',
A: '小代',
B: '上等马',
C: '中等马',
D: '下等马',
E: '牛马',
F: '没有马'
}
function noZero(n) {
return n === 0 ? 1 : n
}
function extractGame(puuid, game) {
const pId = game.participantIdentities.find((p) => p.player.puuid === puuid)?.participantId
if (!pId) {
return null
}
const participant = game.participants.find((p) => p.participantId === pId)
if (!participant) {
return null
}
const isFirstBlood = participant.stats.firstBloodKill
const isFirstBloodAssist = participant.stats.firstBloodAssist
const triple = participant.stats.tripleKills
const quadra = participant.stats.quadraKills
const penta = participant.stats.pentaKills
const isRecent = Date.now() - game.gameCreation < 5 * 60 * 60 * 1000
// SGP 有 teamPosition 字段,LCU 没有. 目前的 LCU 字段似乎并不准确
const isSupport = participant.stats.teamPosition
? participant.stats.teamPosition === 'UTILITY'
: null
// 参团率计算
const team = participant.teamId
const teamParticipants = game.participants.filter((p) => p.teamId === team)
// 累加求总, 用于后面占比计算
const totalKills = teamParticipants.reduce((acc, p) => acc + p.stats.kills, 0)
const totalDmg = teamParticipants.reduce((acc, p) => acc + p.stats.totalDamageDealtToChampions, 0)
const totalAssists = teamParticipants.reduce((acc, p) => acc + p.stats.assists, 0)
const sortedByKpr = teamParticipants.toSorted((a, b) => {
const aKa = a.stats.kills + a.stats.assists
const bKa = b.stats.kills + b.stats.assists
if (aKa === bKa) {
return a.stats.deaths - b.stats.deaths
}
return bKa - aKa
})
const kprRank = sortedByKpr.findIndex((p) => p.participantId === pId)
// 经济计算
const sortedByGr = teamParticipants.toSorted((a, b) => {
return b.stats.goldEarned - a.stats.goldEarned
})
const grRank = sortedByGr.findIndex((p) => p.participantId === pId)
const sortedByDr = teamParticipants.toSorted((a, b) => {
return b.stats.totalDamageDealtToChampions - a.stats.totalDamageDealtToChampions
})
const drRank = sortedByDr.findIndex((p) => p.participantId === pId)
const sortedByVr = teamParticipants.toSorted((a, b) => {
return b.stats.visionScore - a.stats.visionScore
})
const vrRank = sortedByVr.findIndex((p) => p.participantId === pId)
const isKrGt35 = participant.stats.kills / noZero(totalKills) > 0.35
const isKrGt50 = participant.stats.kills / noZero(totalKills) > 0.5
const isDrGt35 = participant.stats.totalDamageDealtToChampions / noZero(totalDmg) > 0.35
const isDrGt50 = participant.stats.totalDamageDealtToChampions / noZero(totalDmg) > 0.5
const isArGt35 = participant.stats.assists / noZero(totalAssists) > 0.35
const isAssGt50 = participant.stats.assists / noZero(totalAssists) > 0.5
const csPerMin =
(participant.stats.totalMinionsKilled + participant.stats.neutralMinionsKilled) /
(game.gameDuration / 60)
return {
isFirstBlood,
isFirstBloodAssist,
isSupport,
kprRank,
grRank,
drRank,
vrRank,
isRecent,
triple,
quadra,
penta,
isKrGt35,
isKrGt50,
isDrGt35,
isDrGt50,
isArGt35,
isAssGt50,
csPerMin,
kills: participant.stats.kills,
assists: participant.stats.assists,
deaths: participant.stats.deaths,
memberCount: teamParticipants.length,
kpr: (participant.stats.kills + participant.stats.assists) / noZero(totalKills),
kda: (participant.stats.kills + participant.stats.assists) / noZero(participant.stats.deaths)
}
}
function getTitles() {
const mh = it.targetMembers
.map((puuid) => [puuid, it.matchHistory[puuid]])
.filter(([_, data]) => Boolean(data))
const players = mh.map(([puuid, m]) => {
// 仅限正常对局
const filtered = m.data.filter((g) => !it.utils.isPveQueue(g.queueId))
const scores = filtered
.map((p) => {
const stats = extractGame(puuid, p)
// 如果无法提取到有效信息, 返回 null, 标记之以过滤
if (!stats) {
return null
}
let base = 100
if (stats.isFirstBlood) {
base += 10
}
if (stats.isFirstBloodAssist) {
base += 5
}
if (stats.triple) {
base += 5
}
if (stats.quadra) {
base += 10
}
if (stats.penta) {
base += 20
}
base += RANKING_SCORE[stats.kprRank] || 0
// 在分路信息的情况下, 这个分数就不计算了
if (stats.isSupport && stats.grRank <= 1) {
base += RANKING_SCORE[stats.grRank] || 0
} else if (stats.isSupport === false /* it may be null */) {
base += RANKING_SCORE[stats.grRank] || 0
}
if (stats.drRank <= 1) {
base += RANKING_SCORE[stats.drRank] || 0
}
if (stats.vrRank <= 1) {
base += RANKING_SCORE[stats.vrRank] || 0
}
if (stats.isKrGt50) {
if (stats.kills > 15) {
base += 40
} else if (stats.kills > 10) {
base += 20
} else if (stats.kills > 5) {
base += 10
}
} else if (stats.isKrGt35) {
if (stats.kills > 15) {
base += 20
} else if (stats.kills > 10) {
base += 10
} else if (stats.kills > 5) {
base += 5
}
}
if (stats.isAssGt50) {
if (stats.assists > 15) {
base += 40
} else if (stats.assists > 10) {
base += 20
} else if (stats.assists > 5) {
base += 10
}
} else if (stats.isArGt35) {
if (stats.assists > 15) {
base += 20
} else if (stats.assists > 10) {
base += 10
} else if (stats.assists > 5) {
base += 5
}
}
if (stats.isDrGt50) {
if (stats.kills > 15) {
base += 40
} else if (stats.kills > 10) {
base += 20
} else if (stats.kills > 5) {
base += 10
}
} else if (stats.isDrGt35) {
if (stats.kills > 15) {
base += 20
} else if (stats.kills > 10) {
base += 10
} else if (stats.kills > 5) {
base += 5
}
}
if (stats.csPerMin >= 10) {
base += 20
} else if (stats.csPerMin >= 9) {
base += 10
} else if (stats.csPerMin >= 8) {
base += 5
}
base += stats.kda + ((stats.kills - stats.deaths) / noZero(stats.memberCount)) * stats.kpr
return { base, isRecent: stats.isRecent }
})
.filter(Boolean)
if (scores.length === 0) {
return { puuid, base: null }
}
const recent = scores.filter((s) => s.isRecent)
const old = scores.filter((s) => !s.isRecent)
const recentBase = recent.reduce((acc, s) => acc + s.base, 0)
const oldBase = old.reduce((acc, s) => acc + s.base, 0)
let finalScore
// 加权平均时考虑某一方数据为空的情况
if (recent.length === 0) {
finalScore = oldBase / old.length
} else if (old.length === 0) {
finalScore = recentBase / recent.length
} else {
finalScore = (recentBase / recent.length) * 0.8 + (oldBase / old.length) * 0.2
}
return {
puuid,
base: finalScore
}
})
const one = players.map(({ puuid, base }) => {
let name
if (it.queryStage.phase === 'champ-select') {
name = it.summoner[puuid]?.data.gameName || '未知召唤师'
} else {
let selection = it.championSelections[puuid] || -1
name = it.gameData.champions[selection]?.name || '未知英雄'
}
if (base) {
let 牛马名
if (base >= 180) {
牛马名 = NIUMA_NAME.S
} else if (base >= 150) {
牛马名 = NIUMA_NAME.A
} else if (base >= 125) {
牛马名 = NIUMA_NAME.B
} else if (base >= 105) {
牛马名 = NIUMA_NAME.C
} else if (base >= 95) {
牛马名 = NIUMA_NAME.D
} else {
if (将敌方牛马转为没有马) {
牛马名 = it.allyMembers.includes(puuid) ? NIUMA_NAME.E : NIUMA_NAME.F
} else {
牛马名 = NIUMA_NAME.E
}
}
const {
averageKda = 0,
count = 0,
winRate = 0
} = it.playerStats.players[puuid]?.summary || {}
// 事实上,别想在选人阶段发出任何评分,私募腾讯全屏蔽了
if (it.queryStage.phase === 'champ-select') {
return `${牛马名} ${name} 评分: ${base.toFixed(0)},近${count}场KDA ${averageKda.toFixed(1)} 胜率 ${(winRate * 100).toFixed(0)}%`
} else {
return `${牛马名} ${name} 评分: ${base.toFixed(1)},近${count}场KDA ${averageKda.toFixed(2)} 胜率 ${(winRate * 100).toFixed(0)}%`
}
} else {
return `${name} 近期无有效对局`
}
})
if (发送开黑信息) {
// 在 teams 的哪个里面, teamId: string[]
const selfTeamId = Object.entries(it.teams).find(([teamId, players]) => {
return players.includes(it.selfPuuid)
})?.[0]
const messages = Object.entries(it.premadeTeams).map(([teamId, groups]) => {
if (selfTeamId) {
if (it.target === 'ally') {
if (teamId !== selfTeamId) {
return null
}
} else if (it.target === 'enemy') {
if (teamId === selfTeamId) {
return null
}
}
}
// 玩家选择转换成英雄名
const names = groups.map((list) => {
const names = list.map((puuid) => {
let selection = it.championSelections[puuid] || -1
name = it.gameData.champions[selection]?.name || '未知英雄'
return name
})
return names
})
if (!names.length) {
return null
}
// 英雄名转换成空格分隔
const texts = names.map((n) => {
return n.join(' ')
})
let premadeTitle
if (!selfTeamId) {
premadeTitle = '开黑'
} else {
premadeTitle = teamId === selfTeamId ? '我方开黑' : '敌方开黑'
}
return `${premadeTitle} ${texts.map((s) => `[${s}]`).join(' ')}`
}).filter(Boolean)
return [...one, ...messages]
}
return one
}
%>
<%~ getTitles().join('\n') %>
猫咪发送模板
一个评分机制基于 hh-lol-prophet 的变种文案。
<%
/**
* 评分部分实现自:[hh-lol-prophet](https://github.com/real-web-world/hh-lol-prophet)
*/
const RANKING_SCORE = { 0: 10, 1: 5, 2: 0, 3: -5, 4: -10 }
const startText = [
'喵星传输完成!正在展示峡谷猫咪数据...',
'喵星通信完成!正在展示最新战绩...',
'喵星同步完毕!正在展示战斗日志...',
'已请求到所有喵星数据!即将展示...'
]
const noDataText = '喵星数据库空空如也... 没有找到任何猫咪的战斗记录,看来大家最近都在猫窝里休息!'
const emptyPrefixText = '喵星信号中断...'
const emptyText = [
'战绩本子空空如也,是不是在偷偷睡懒觉?',
'没有近期战绩呢,就像一只没捕到老鼠的猫。',
'数据不够喵,是在猫窝里偷懒吗?'
]
const nekos = {
S: {
title: '布偶猫',
texts: [
'峡谷王者,堪称完美。',
'每一爪都精准,队伍的灵魂核心。',
'稳健与气场并存,胜利离不开你。'
]
},
A: {
title: '英国短毛猫',
texts: [
'沉稳老练,总能稳住局势。',
'扎实操作,队伍的可靠后盾。',
'表现稳健,关键时刻绝不掉链子。'
]
},
B: {
title: '橘猫',
texts: [
'佛系选手,偶尔也能秀一爪。',
'表现合格,下次多亮眼些就更好了。',
'稳扎稳打,虽无惊喜但不拖后腿。'
]
},
C: {
title: '土猫',
texts: [
'偶有亮点,但失误较多,还需努力。',
'节奏迷离,潜力尚在。',
'表现起伏不定,保持稳定更重要。'
]
},
D: {
title: '折耳猫',
texts: [
'节奏散乱,练练爪子再来!',
'表现欠佳,但心态可爱值拉满。',
'操作混乱,但别灰心,下次再战!'
]
},
E: {
title: '无毛猫',
texts: ['风一吹就没了,快稳住!', '表现惨淡,还需苦练。', '节奏全无,但下次还有机会!']
}
}
// 将 0 视为 1 的小工具函数
function noZero(n) {
return n === 0 ? 1 : n
}
// 提取某局对当前玩家的统计信息
function extractGame(puuid, game) {
if (game.endOfGameResult === 'Abort_AntiCheatExit') return null
const pId = game.participantIdentities.find((p) => p.player.puuid === puuid)?.participantId
if (!pId) return null
const participant = game.participants.find((p) => p.participantId === pId)
if (!participant) return null
if (participant.stats.gameEndedInEarlySurrender) return null
const isFirstBlood = participant.stats.firstBloodKill
const isFirstBloodAssist = participant.stats.firstBloodAssist
const triple = participant.stats.tripleKills
const quadra = participant.stats.quadraKills
const penta = participant.stats.pentaKills
const isRecent = Date.now() - game.gameCreation < 5 * 60 * 60 * 1000
const isSupport = participant.stats.teamPosition
? participant.stats.teamPosition === 'UTILITY'
: null
const team = participant.teamId
const teamParticipants = game.participants.filter((p) => p.teamId === team)
const totalKills = teamParticipants.reduce((acc, p) => acc + p.stats.kills, 0)
const totalDmg = teamParticipants.reduce((acc, p) => acc + p.stats.totalDamageDealtToChampions, 0)
const totalAssists = teamParticipants.reduce((acc, p) => acc + p.stats.assists, 0)
const sortedByKpr = teamParticipants.toSorted((a, b) => {
const aKa = a.stats.kills + a.stats.assists
const bKa = b.stats.kills + b.stats.assists
return aKa === bKa ? a.stats.deaths - b.stats.deaths : bKa - aKa
})
const kprRank = sortedByKpr.findIndex((p) => p.participantId === pId)
const sortedByGr = teamParticipants.toSorted((a, b) => b.stats.goldEarned - a.stats.goldEarned)
const grRank = sortedByGr.findIndex((p) => p.participantId === pId)
const sortedByDr = teamParticipants.toSorted(
(a, b) => b.stats.totalDamageDealtToChampions - a.stats.totalDamageDealtToChampions
)
const drRank = sortedByDr.findIndex((p) => p.participantId === pId)
const sortedByVr = teamParticipants.toSorted((a, b) => b.stats.visionScore - a.stats.visionScore)
const vrRank = sortedByVr.findIndex((p) => p.participantId === pId)
const isKrGt35 = participant.stats.kills / noZero(totalKills) > 0.35
const isKrGt50 = participant.stats.kills / noZero(totalKills) > 0.5
const isDrGt35 = participant.stats.totalDamageDealtToChampions / noZero(totalDmg) > 0.35
const isDrGt50 = participant.stats.totalDamageDealtToChampions / noZero(totalDmg) > 0.5
const isArGt35 = participant.stats.assists / noZero(totalAssists) > 0.35
const isAssGt50 = participant.stats.assists / noZero(totalAssists) > 0.5
const csPerMin =
(participant.stats.totalMinionsKilled + participant.stats.neutralMinionsKilled) /
(game.gameDuration / 60)
return {
isWin: participant.stats.win,
isLose: !participant.stats.win,
isFirstBlood,
isFirstBloodAssist,
isSupport,
kprRank,
grRank,
drRank,
vrRank,
isRecent,
triple,
quadra,
penta,
isKrGt35,
isKrGt50,
isDrGt35,
isDrGt50,
isArGt35,
isAssGt50,
csPerMin,
kills: participant.stats.kills,
assists: participant.stats.assists,
deaths: participant.stats.deaths,
memberCount: teamParticipants.length,
kpr: (participant.stats.kills + participant.stats.assists) / noZero(totalKills),
kda: (participant.stats.kills + participant.stats.assists) / noZero(participant.stats.deaths)
}
}
function getPlayerStats(it) {
const mh = it.targetMembers
.map((puuid) => [puuid, it.matchHistory[puuid]])
.filter(([_, data]) => Boolean(data)) // 有可能玩家无历史数据
const result = []
// 逐个玩家计算
for (const [puuid, m] of mh) {
const filtered = m.data.filter((g) => !it.utils.isPveQueue(g.queueId))
const singleGames = []
for (const game of filtered) {
const stats = extractGame(puuid, game)
if (!stats) {
continue
}
let base = 100
if (stats.isFirstBlood) base += 10
if (stats.isFirstBloodAssist) base += 5
if (stats.triple) base += 5
if (stats.quadra) base += 10
if (stats.penta) base += 20
base += RANKING_SCORE[stats.kprRank] || 0
// 辅助位如果金钱排在队伍前列,加分
if (stats.isSupport && stats.grRank <= 1) {
base += RANKING_SCORE[stats.grRank] || 0
} else if (stats.isSupport === false) {
base += RANKING_SCORE[stats.grRank] || 0
}
if (stats.drRank <= 1) {
base += RANKING_SCORE[stats.drRank] || 0
}
if (stats.vrRank <= 1) {
base += RANKING_SCORE[stats.vrRank] || 0
}
if (stats.isKrGt50) {
if (stats.kills > 15) base += 40
else if (stats.kills > 10) base += 20
else if (stats.kills > 5) base += 10
} else if (stats.isKrGt35) {
if (stats.kills > 15) base += 20
else if (stats.kills > 10) base += 10
else if (stats.kills > 5) base += 5
}
if (stats.isAssGt50) {
if (stats.assists > 15) base += 40
else if (stats.assists > 10) base += 20
else if (stats.assists > 5) base += 10
} else if (stats.isArGt35) {
if (stats.assists > 15) base += 20
else if (stats.assists > 10) base += 10
else if (stats.assists > 5) base += 5
}
if (stats.isDrGt50) {
if (stats.kills > 15) base += 40
else if (stats.kills > 10) base += 20
else if (stats.kills > 5) base += 10
} else if (stats.isDrGt35) {
if (stats.kills > 15) base += 20
else if (stats.kills > 10) base += 10
else if (stats.kills > 5) base += 5
}
if (stats.csPerMin >= 10) {
base += 20
} else if (stats.csPerMin >= 9) {
base += 10
} else if (stats.csPerMin >= 8) {
base += 5
}
base += stats.kda + ((stats.kills - stats.deaths) / noZero(stats.memberCount)) * stats.kpr
// 将该对局的统计信息、单局评分等暂存
singleGames.push({
base,
isRecent: stats.isRecent,
extractedStats: stats
})
}
if (singleGames.length === 0) {
result.push({
puuid,
stats: {
rating: null,
gameStats: []
}
})
continue
}
// 划分近期 / 非近期对局
const recent = singleGames.filter((g) => g.isRecent)
const old = singleGames.filter((g) => !g.isRecent)
const recentBase = recent.reduce((acc, g) => acc + g.base, 0)
const oldBase = old.reduce((acc, g) => acc + g.base, 0)
let finalScore
if (recent.length === 0) {
// 没有近期对局,就用老对局的平均
finalScore = oldBase / old.length
} else if (old.length === 0) {
// 没有老对局,就用近期对局的平均
finalScore = recentBase / recent.length
} else {
// 如果近期、老对局都有,近期占比 80%,老对局占比 20%
finalScore = (recentBase / recent.length) * 0.8 + (oldBase / old.length) * 0.2
}
const extractedAll = singleGames.map((g) => g.extractedStats)
result.push({
puuid,
stats: {
rating: finalScore,
gameStats: extractedAll
}
})
}
return result
}
function getRank(score) {
if (score >= 180) {
return 'S'
} else if (score >= 150) {
return 'A'
} else if (score >= 125) {
return 'B'
} else if (score >= 105) {
return 'C'
} else if (score >= 95) {
return 'D'
} else {
return 'E'
}
}
function generateTextLines() {
// it 是全局环境的一个对象,包含了所有需要的数据
const players = getPlayerStats(it)
const lines = []
for (const player of players) {
const summary = it.playerStats?.players?.[player.puuid]?.summary
const championId = it.championSelections[player.puuid]
const championName = it.gameData.champions[championId]?.name
if (player.stats.rating === null || summary === null) {
lines.push({
puuid: player.puuid,
text: `${championName} ${emptyPrefixText} ${emptyText[Math.floor(Math.random() * emptyText.length)]}`
})
continue
}
const rank = getRank(player.stats.rating)
const neko = nekos[rank || 'B']
const part1 = `${neko.title} ${championName} 评分${player.stats.rating.toFixed()},${neko.texts[Math.floor(Math.random() * neko.texts.length)]}`
const part2 = []
if (summary.winningStreak >= 3) {
part2.push(`${summary.winningStreak} 连胜,`)
} else if (summary.losingStreak >= 3) {
part2.push(`${summary.losingStreak} 连败,`)
}
part2.push(`胜率 ${(summary.winRate * 100).toFixed()}%,`)
part2.push(`KDA ${summary.averageKda.toFixed(2)}。`)
if (player.stats.gameStats.length) {
if (player.stats.gameStats[0].penta) {
part2.push('顺便一提,上局拿了五杀!')
} else if (player.stats.gameStats[0].quadra >= 2) {
part2.push(`顺便一提,上局拿了${player.stats.gameStats[0].quadra}个四杀!`)
}
}
lines.push({
puuid: player.puuid,
text: part1 + part2.join('')
})
}
if (lines.length) {
lines.unshift({
puuid: null,
text: startText[Math.floor(Math.random() * startText.length)]
})
} else {
lines.push({
puuid: null,
text: noDataText
})
}
return lines.map((l) => l.text)
}
%>
<%~ generateTextLines().join('\n') %>