@sjtdev/koishi-plugin-dota2tracker 1.5.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +2429 -1625
- package/package.json +7 -3
- package/queries/GetWeeklyMetaByPosition.graphql +29 -0
- package/queries/PlayerExtraInfo.graphql +2 -2
- package/queries/PlayerPerformanceForHeroRecommendation.graphql +23 -0
- package/template/hero/hero_1.ejs +29 -29
- package/template/item/item/recipe.ejs +3 -3
- package/template/item/item.ejs +3 -3
- package/template/item/itemlist.ejs +1 -1
- package/template/match/match_1/item.ejs +4 -4
- package/template/match/match_1/main.ejs +8 -8
- package/template/match/match_1/player.ejs +10 -10
- package/template/match/match_1/style.css +12 -0
- package/template/match/match_1.ejs +6 -6
- package/template/match/match_2/original.ejs +122 -82
- package/template/match/match_2+/charts.ejs +3 -3
- package/template/match/match_2+/lane_outcome.ejs +2 -2
- package/template/player/player_1.ejs +49 -23
- package/template/rank/rank_fun.ejs +6 -6
- package/queries/HeroMatchupWinrate.graphql +0 -25
- package/template/guild_member/guild_member.ejs +0 -176
package/lib/index.js
CHANGED
|
@@ -5,10 +5,10 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
|
-
var __glob = (map) => (
|
|
9
|
-
var fn = map[
|
|
8
|
+
var __glob = (map) => (path5) => {
|
|
9
|
+
var fn = map[path5];
|
|
10
10
|
if (fn) return fn();
|
|
11
|
-
throw new Error("Module not found in bundle: " +
|
|
11
|
+
throw new Error("Module not found in bundle: " + path5);
|
|
12
12
|
};
|
|
13
13
|
var __commonJS = (cb, mod) => function __require() {
|
|
14
14
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
@@ -35,20 +35,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
35
35
|
));
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
|
|
38
|
-
// src/locales/en-US.schema.yml
|
|
39
|
-
var require_en_US_schema = __commonJS({
|
|
40
|
-
"src/locales/en-US.schema.yml"(exports2, module2) {
|
|
41
|
-
module2.exports = { _config: { base: { $desc: "Basic Settings", STRATZ_API_TOKEN: "Required. API TOKEN from stratz.com, available at https://stratz.com/api.", dataParsingTimeoutMinutes: "Time to wait for match data parsing (in minutes). If the data parsing time exceeds the waiting time, the report will be generated directly without waiting for the parsing to complete.", proxyAddress: "Proxy address. Leave blank to disable the proxy." }, message: { $desc: "Message Settings", urlInMessageType: { $desc: "Include links in messages, <br/>please select the message type:", $inner: ["Include stratz match page link in match query and report messages", "Include stratz player page link in player information query messages", "Include Dota Encyclopedia hero page link in hero data query messages"] }, rankBroadSwitch: "Rank change broadcast", rankBroadStar: "Star change broadcast", rankBroadLeader: "Leaderboard rank change broadcast", rankBroadFun: "Fun broadcast template", maxSendItemCount: "Maximum number of item images to send<br/>When exceeded, the following option determines whether to send the item list", showItemListAtTooMuchItems: "Send item list when exceeding max count<br/>Controls whether to send the item list image when search results exceed maxSendItemCount", customItemAlias: { $desc: "Custom item aliases<br/>\nAdd additional aliases when built-in list is insufficient. \nFor widely-used missing aliases, please submit issues/pull requests to the source repository.<br/>\n(Example **Keyword**: Blink Dagger,**Alias**: Blink)", keyword: "Keyword", alias: "Alias" } }, report: { $desc: "Summary Settings", dailyReportSwitch: "Daily Report Function", dailyReportHours: "Daily report time in hours", dailyReportShowCombi: "Show combinations in daily report", weeklyReportSwitch: "Weekly Report Function", weeklyReportDayHours: "Weekly report published on (day) at (hour)", weeklyReportShowCombi: "Show combinations in weekly report" }, template: { $desc: "Template Settings", template_match: "Template used to generate match information images, see https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html for template display.", template_player: "Template used to generate player information images. (Currently only one template available)", template_hero: "Template used to generate hero information images. (Currently only one template available)", playerRankEstimate: "Estimate the rank of players without a rank in the player template <br>Estimated rank will be displayed as a gray image", templateFonts: 'Font names used in the template. Requires font files installed on the koishi host machine. \nMultiple fonts can be added; the system will use the first available font from top to bottom. \nIf all fonts are unavailable, falls back to system defaults. \nImportant formatting rules: \n- Enclose font names in quotes (" ") if they contain spaces or special characters (recommended for all font names)\n- Do NOT enclose generic font family names (e.g. sans-serif, monospace) in quotes\nExamples:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor details on font-family syntax, see:\n[📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)' } } };
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// src/locales/zh-CN.schema.yml
|
|
46
|
-
var require_zh_CN_schema = __commonJS({
|
|
47
|
-
"src/locales/zh-CN.schema.yml"(exports2, module2) {
|
|
48
|
-
module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,留空时不使用代理" }, message: { $desc: "消息设置", urlInMessageType: { $desc: "在消息中附带链接,<br/>请选择消息类型:", $inner: ["在查询比赛与战报消息中附带stratz比赛页面链接", "在查询玩家信息消息中附带stratz玩家页面链接", "在查询英雄数据消息中附带刀塔百科对应英雄页面链接"] }, rankBroadSwitch: "段位变动播报", rankBroadStar: "星级变动播报", rankBroadLeader: "冠绝名次变动播报", rankBroadFun: "整活播报模板", maxSendItemCount: "最大发送物品图片数量,<br/> 当超过指定数量时将由下方选项决定是否发送查询结果的物品列表图片", showItemListAtTooMuchItems: "在查询结果的物品数量超过指定数量时,是否发送查询结果的物品列表图片", customItemAlias: { $desc: "额外物品别名设置<br/>当插件内置的[物品别名列表](https://github.com/sjtdev/koishi-plugin-dota2tracker/blob/master/src/locales/zh-CN.constants.json#L304-L407)中没有想要的物品别名可在此处追加,如果是插件疏漏的广为人知的物品别名推荐到源码仓库提交issue或pull request完善列表。<br/>(例如 **关键词**: 闪烁匕首,**别名**: 跳刀)", keyword: "关键词", alias: "别名" } }, report: { $desc: "总结设置", dailyReportSwitch: "日报功能", dailyReportHours: "日报时间小时", dailyReportShowCombi: "日报是否显示组合", weeklyReportSwitch: "周报功能", weeklyReportDayHours: "周报发布于周(几)的(几)点", weeklyReportShowCombi: "周报是否显示组合" }, template: { $desc: "模板设置", template_match: "生成比赛信息图片使用的模板,显示效果见 [📖 文档-模板展示页](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html)。", template_player: "生成玩家信息图片使用的模板。(目前仅有一张模板)", template_hero: "生成英雄信息图片使用的模板。(目前仅有一张模板)", playerRankEstimate: "在player模板中对没有段位的玩家进行段位估算 <br>估算的段位将以灰色图片显示", templateFonts: '模板所使用的字体名。需要 koishi 所在设备安装字体文件。 \n可添加多个字体名,将从上到下回退到第一个可用字体;若所有字体都不可用,则使用系统默认字体。 \n其中字体名若包含空格或特殊字符需要在名称首尾添加引号(此处建议尽量强制使用引号); \n若使用字体族名则必须**不使用引号**,如:\n```\n"Microsoft YaHei"\nsans-serif\n```\n有关font-family的更多信息,请查阅 [📖 MDN: font-family](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family) ' } } };
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
38
|
// src/locales/en-US.constants.json
|
|
53
39
|
var require_en_US_constants = __commonJS({
|
|
54
40
|
"src/locales/en-US.constants.json"(exports2, module2) {
|
|
@@ -780,7 +766,14 @@ var require_zh_CN_constants = __commonJS({
|
|
|
780
766
|
// src/locales/en-US.command.yml
|
|
781
767
|
var require_en_US_command = __commonJS({
|
|
782
768
|
"src/locales/en-US.command.yml"(exports2, module2) {
|
|
783
|
-
module2.exports = { commands: { dota2tracker: { subscribe: { description: "After subscribing, players need to bind their Steam ID to this group.", usage: "After subscribing, players need to bind their Steam ID to this group. BOT will subscribe to the new game data of bound players in this group. After the STRATZ game analysis is completed, the game data will be generated into a picture battle report and published to this group.", messages: { subscribe_success: "Subscription successful.", subscribed: "This Channel has been subscribed, no need to subscribe again." } }, unsubscribe: { description: "Unsubscribe from this group.", messages: { unsubscribe_success: "Unsubscription successful.", not_subscribed: "This Channel has not been subscribed yet, so there is no need to unsubscribe." } }, bind: { description: "Bind your SteamID and optionally set a nickname.", usage: "Bind your SteamID to your account. If the group is subscribed, your new match data will be posted in the group in real-time.", examples: 'bind 123456789\nbind 123456789 John\nbind 123456789 "John Doe"', messages: { steam_id_invalid: "Invalid SteamID.", bind_success: "Binding successful,\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", bind_failed: "Binding failed, {0}", reason_without_match: "Invalid SteamID or no matches found.", reason_fetch_failed: "Poor network conditions or other reasons prevented the verification of the SteamID. Please try again later.", already_binded: "You are already bound, no need to bind again.\nHere is your personal information:\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", nick_name_too_long: "Nickname is too long, please limit it to 20 characters or less. (It can also be left blank)", is_anonymous: 'Please note: Your Steam player data is not public, and you will not be able to use the main functions of the BOT, such as "battle report tracking," "query-recent-match commands," etc.\nIf you need to make data public, please set it to public in the DOTA2 game settings.' } }, unbind: { description: "Unbind your personal information.", messages: { unbind_success: "Unbinding successful.", not_binded: "Not bound, no need to unbind." } }, rename: { description: "Change the nickname set during binding.", examples: 'rename John\nrename "John Doe"', messages: { rename_success: "Rename successful, now you are called {nick_name}.", empty_input: "Please enter your nickname.", not_binded: "Please bind first, you can set a nickname during binding.", nick_name_too_long: "Nickname is too long, please limit it to 20 characters." } }, "query-members": { description: "Query the players bound in this group.", messages: { no_members: "No players bound in this group.", query_failed: "Failed to query group members." } }, "query-match": { description: "Query the match data of the specified match ID and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, examples: "query-match 1234567890\nquery-match 1234567890 -p\nquery-match 1234567890 --parse", messages: { empty_input: "Please enter the match ID.", match_id_invalid: "Invalid match ID.", querying_match: "Searching for match details, please wait...", query_failed: "Failed to get match data.", waiting_for_parse: "Match data has not been parsed yet, a parse request has been sent to the server. The battle report will be sent once parsing is complete or times out." } }, "query-recent-match": { description: "Query the most recent match data and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, usage: "Query the most recent match data of the specified player and generate a picture.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-recent-match\nquery-recent-match 123456789\nquery-recent-match John\nquery-recent-match 123456789 -p\nquery-recent-match John --parse", messages: { not_binded: "By default, it tries to find your information from the bound SteamID players, but it seems you are not bound.\nPlease bind your SteamID in this group. (You can enter [-bind -h] for help)\nOr follow the command with the SteamID or nickname of the player you want to query.", steam_id_invalid: "Invalid SteamID and the player was not found in this group.", querying_match: "Searching for match details, please wait...", query_failed: "Failed to get the player's recent match.", not_in_group: "Command failed.\nCurrently not in a group chat, you must provide the specified player's SteamID.", is_anonymous: "Your player data is not public, and recent match data cannot be obtained.\nIf you need to make data public, please set it to public in the DOTA2 game settings." } }, "query-player": { description: "Query the player's personal information, optionally specify a hero.", options: { hero: "Query the player's usage of the specified hero (same as querying a hero, can use nickname or ID)" }, usage: "Query the personal information of the specified player and generate a picture, optionally specify a hero.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-player\nquery-player 123456789\nquery-player John\nquery-player John --hero Anti-Mage\nquery-player John -o Anti-Mage", messages: { not_binded: "By default, it tries to find your information from the bound SteamID players, but it seems you are not bound.\nPlease bind your SteamID in this group. (You can enter [bind -h] for help)\nOr follow the command with the SteamID or nickname of the player you want to query.", steam_id_invalid: "Invalid SteamID and the player was not found in this group.", querying_player: "Retrieving player data, please wait...", query_failed: "Failed to get player information.", not_in_group: "Command failed.\nCurrently not in a group chat, you must provide the specified player's SteamID." } }, "query-hero": { description: "Query hero skills/stats information.", options: { random: "Randomly select a hero." }, usage: "Query the hero's skill descriptions and various stats, generate a picture.\nThe parameter can be the hero's ID, name, or common nickname.", examples: "query-hero 15\nquery-hero Razor\nquery-hero -r", messages: { not_found: "Hero not found, please confirm and re-enter.", querying_hero: "Retrieving hero data, please wait...", query_failed: "Failed to get hero data.", empty_input: "Please enter a parameter." } }, "query-item": { description: "Query item information", usage: "Query item descriptions and attributes, then generate and publish an image report.\nParameters can be item name (supports fuzzy search), item alias, or item ID.\nYou can set the maximum number of items to send per query on the configuration page, as well as whether to send the item list when the limit is exceeded or parameters are not entered.", examples: "query-item Vanguard", messages: { query_list_failed: "Failed to retrieve item list data", query_item_failed: "Failed to retrieve data for item '{0}'", querying_item: "Querying item data, please wait...", cache_building: "Initializing or rebuilding item cache for the current version, please wait...", empty_input: "No keywords provided. \n{#if show}Displaying full item list per current configuration\n{:else}No content available\n{/if}", not_found: "No items found matching the keywords, please verify and retry", too_many_items: "Found {count} items, exceeding maximum display limit ({max} items)\n{#if show}(Displaying item list){/if}", finded_items: "Matching items: \n{#each items as item}\n{item.name_loc}{#if item !== items[items.length - 1]}, {/if}\n{/each}" } } } } };
|
|
769
|
+
module2.exports = { commands: { dota2tracker: { subscribe: { description: "After subscribing, players need to bind their Steam ID to this group.", usage: "After subscribing, players need to bind their Steam ID to this group. BOT will subscribe to the new game data of bound players in this group. After the STRATZ game analysis is completed, the game data will be generated into a picture battle report and published to this group.", messages: { subscribe_success: "Subscription successful.", subscribed: "This Channel has been subscribed, no need to subscribe again." } }, unsubscribe: { description: "Unsubscribe from this group.", messages: { unsubscribe_success: "Unsubscription successful.", not_subscribed: "This Channel has not been subscribed yet, so there is no need to unsubscribe." } }, bind: { description: "Bind your SteamID and optionally set a nickname.", usage: "Bind your SteamID to your account. If the group is subscribed, your new match data will be posted in the group in real-time.", examples: 'bind 123456789\nbind 123456789 John\nbind 123456789 "John Doe"', messages: { steam_id_invalid: "Invalid SteamID.", bind_success: "Binding successful,\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", bind_failed: "Binding failed, {0}", reason_without_match: "Invalid SteamID or no matches found.", reason_fetch_failed: "Poor network conditions or other reasons prevented the verification of the SteamID. Please try again later.", already_binded: "You are already bound, no need to bind again.\nHere is your personal information:\nID: {userId}\nNickname: {nickName}\nSteamID: {steamId}", nick_name_too_long: "Nickname is too long, please limit it to 20 characters or less. (It can also be left blank)", is_anonymous: 'Please note: Your Steam player data is not public, and you will not be able to use the main functions of the BOT, such as "battle report tracking," "query-recent-match commands," etc.\nIf you need to make data public, please set it to public in the DOTA2 game settings.' } }, unbind: { description: "Unbind your personal information.", messages: { unbind_success: "Unbinding successful.", not_binded: "Not bound, no need to unbind." } }, rename: { description: "Change the nickname set during binding.", examples: 'rename John\nrename "John Doe"', messages: { rename_success: "Rename successful, now you are called {nick_name}.", empty_input: "Please enter your nickname.", not_binded: "Please bind first, you can set a nickname during binding.", nick_name_too_long: "Nickname is too long, please limit it to 20 characters.", nick_name_same: "The input content is the same as the original nickname and does not need to be renamed." } }, "query-members": { description: "Query the players bound in this group.", messages: { title: "Guild DOTA 2 Roster (Total: {count})", table_headers: { nickname: "Nickname", winrate: "Win Rate (L10)", last_match: "Last Match" }, no_members: "No players bound in this group.", query_failed: "Failed to query group members." } }, "query-match": { description: "Query the match data of the specified match ID and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, examples: "query-match 1234567890\nquery-match 1234567890 -p\nquery-match 1234567890 --parse", messages: { empty_input: "Please enter the match ID.", match_id_invalid: "Invalid match ID.", querying_match: "Searching for match details, please wait...", query_failed: "Failed to get match data.", waiting_for_parse: "Match data has not been parsed yet, a parse request has been sent to the server. The battle report will be sent once parsing is complete or times out." } }, "query-recent-match": { description: "Query the most recent match data and generate a picture.", options: { parse: "Whether to wait for match data parsing" }, usage: "Query the most recent match data of the specified player and generate a picture.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-recent-match\nquery-recent-match 123456789\nquery-recent-match John\nquery-recent-match 123456789 -p\nquery-recent-match John --parse", messages: { querying_match: "Searching for match details, please wait...", query_failed: "Failed to get the player's recent match.", is_anonymous: "Your player data is not public, and recent match data cannot be obtained.\nIf you need to make data public, please set it to public in the DOTA2 game settings." } }, "query-player": { description: "Query the player's personal information, optionally specify a hero.", options: { hero: "Query the player's usage of the specified hero (same as querying a hero, can use nickname or ID)" }, usage: "Query the personal information of the specified player and generate a picture, optionally specify a hero.\nThe parameter can be the player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to query the SteamID of the command caller.", examples: "query-player\nquery-player 123456789\nquery-player John\nquery-player John --hero Anti-Mage\nquery-player John -o Anti-Mage", messages: { querying_player: "Retrieving player data, please wait...", query_failed: "Failed to get player information." } }, "query-hero": { description: "Query hero skills/stats information.", options: { random: "Randomly select a hero." }, usage: "Query the hero's skill descriptions and various stats, generate a picture.\nThe parameter can be the hero's ID, name, or common nickname.", examples: "query-hero 15\nquery-hero Razor\nquery-hero -r", messages: { not_found: "Hero not found, please confirm and re-enter.", querying_hero: "Retrieving hero data, please wait...", query_failed: "Failed to get hero data.", empty_input: "Please enter a parameter." } }, "query-item": { description: "Query item information", usage: "Query item descriptions and attributes, then generate and publish an image report.\nParameters can be item name (supports fuzzy search), item alias, or item ID.\nYou can set the maximum number of items to send per query on the configuration page, as well as whether to send the item list when the limit is exceeded or parameters are not entered.", examples: "query-item Vanguard", messages: { query_list_failed: "Failed to retrieve item list data", query_item_failed: "Failed to retrieve data for item '{0}'", querying_item: "Querying item data, please wait...", cache_building: "Initializing or rebuilding item cache for the current version, please wait...", empty_input: "No keywords provided. \n{#if show}Displaying full item list per current configuration\n{:else}No content available\n{/if}", not_found: "No items found matching the keywords, please verify and retry", too_many_items: "Found {count} items, exceeding maximum display limit ({max} items)\n{#if show}(Displaying item list){/if}", finded_items: "Matching items: \n{#each items as item}\n{item.name_loc}{#if item !== items[items.length - 1]}, {/if}\n{/each}" } }, "hero-of-the-day": { description: "Get hero recommendations for the day.", usage: "Fetches recent and lifetime match history to recommend heroes based on parameters like wins, performance score, and hot streaks.\nThe parameter can be a player's SteamID or the nickname of a player bound in this group. If no parameter is provided, it will try to look up the command caller's SteamID.", options: { days: "-d <number> The range of recent days to consider, default is 30." }, examples: 'hero-of-the-day\nhero-of-the-day -d 60\nhero-of-the-day 1234567890\nhero-of-the-day "John Doe"', messages: { title_recommendation: "Today's Recommendation:", recommendation_intro: "The recommended heroes for you today are:", recommendation_heroes: "{#each heroes as hero}{hero}{#if hero !== heroes[heroes.length - 1]}, {/if}{/each}", recommendation_type_lifetime_only: "Your recent match history is empty. This recommendation is based on your lifetime statistics.", recommendation_type_no_record: "Cannot provide recommendations due to insufficient recent and lifetime data.", details: { pool_description: "The recommendation is generated by scoring your recent and lifetime hero performance, sorting by total score, and then randomly selecting from the top 10 heroes weighted by their scores.", table_intro: "Below is the detailed score breakdown for the top 10 heroes.", table_headers: { hero: "Hero", recent_wins: "Recent Wins Score", lifetime_wins: "Lifetime Wins<br>(Logarithmic)", imp_bonus: "IMP Bonus", is_hot_streak: "Hot Streak", total_score: "Total Score" }, scoring_formula: "Current Scoring Formula: [Recent Wins x 1] + [log(Lifetime Wins + 1) x 5] + [Recent IMP x 0.1]", hot_streak_desc: "If a hero was played in the last 3 days, it's considered a 'Hot Streak' hero, receiving a 20% bonus to its total score." }, title_meta: "Meta Trends:", meta_intro: "Top 3 advantage heroes for each position with a <b>pick rate ≥2%</b> within ±1 of your rank bracket ({tiers}) over the last week, sorted by win rate:", meta_table_header: "Hero (Pick% Win%)", meta_position: "Pos {pos}:" } }, common: { messages: { user_not_binded_in_channel: "By default, it tries to find your information from the bound SteamID players, but it seems you are not bound.\nPlease bind your SteamID in this group. (You can enter [bind -h] for help)\nOr follow the command with the SteamID or nickname of the player you want to query.", user_not_in_group: "Command failed.\nCurrently not in a group chat, you must provide the specified player's SteamID.", invalid_input_include_steam_id: "Invalid SteamID and the player was not found in this group by the given input." } }, help: { description: "Get the link to the detailed online documentation for the plugin.", messages: { content: "For a detailed guide on DOTA2Tracker features, command usage, and algorithm explanations, please visit the online documentation:\nhttps://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/" } } } } };
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// src/locales/en-US.schema.yml
|
|
774
|
+
var require_en_US_schema = __commonJS({
|
|
775
|
+
"src/locales/en-US.schema.yml"(exports2, module2) {
|
|
776
|
+
module2.exports = { _config: { base: { $desc: "Basic Settings", STRATZ_API_TOKEN: "Required. API TOKEN from stratz.com, available at https://stratz.com/api.", dataParsingTimeoutMinutes: "Time to wait for match data parsing (in minutes). If the data parsing time exceeds the waiting time, the report will be generated directly without waiting for the parsing to complete.", proxyAddress: "Proxy address. Leave blank to disable the proxy.", suppressStratzNetworkErrors: "When enabled, downgrades Stratz network errors (e.g. timeouts) to lower-priority output. Debug-level logs are hidden by default. To enable debug logging, see [📖 Documentation: Configuration](http://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/configs.html#suppressstratznetworkerrors-boolean)" }, message: { $desc: "Message Settings", useHeroNicknames: "When disabled, only the official hero names will be used.", urlInMessageType: { $desc: "Include links in messages, <br/>please select the message type:", $inner: ["Include stratz match page link in match query and report messages", "Include stratz player page link in player information query messages", "Include Dota Encyclopedia hero page link in hero data query messages"] }, rankBroadSwitch: "Rank change broadcast", rankBroadStar: "Star change broadcast", rankBroadLeader: "Leaderboard rank change broadcast", rankBroadFun: "Fun broadcast template", maxSendItemCount: "Maximum number of item images to send<br/>When exceeded, the following option determines whether to send the item list", showItemListAtTooMuchItems: "Send item list when exceeding max count<br/>Controls whether to send the item list image when search results exceed maxSendItemCount", customItemAlias: { $desc: "Custom item aliases<br/>\nAdd additional aliases when built-in list is insufficient. \nFor widely-used missing aliases, please submit issues/pull requests to the source repository.<br/>\n(Example **Keyword**: Blink Dagger,**Alias**: Blink)", keyword: "Keyword", alias: "Alias" } }, report: { $desc: "Summary Settings", dailyReportSwitch: "Daily Report Function", dailyReportHours: "Daily report time in hours", dailyReportShowCombi: "Show combinations in daily report", weeklyReportSwitch: "Weekly Report Function", weeklyReportDayHours: "Weekly report published on (day) at (hour)", weeklyReportShowCombi: "Show combinations in weekly report" }, template: { $desc: "Template Settings", template_match: "Template used to generate match information images, see [📖 Template Display](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html) for template display.", template_player: "Template used to generate player information images. (Currently only one template available)", template_hero: "Template used to generate hero information images. (Currently only one template available)", playerRankEstimate: "Estimate the rank of players without a rank in the player template <br>Estimated rank will be displayed as a gray image", templateFonts: 'Font names used in the template. Requires font files installed on the koishi host machine. \nMultiple fonts can be added; the system will use the first available font from top to bottom. \nIf all fonts are unavailable, falls back to system defaults. \nImportant formatting rules: \n- Enclose font names in quotes (" ") if they contain spaces or special characters (recommended for all font names)\n- Do NOT enclose generic font family names (e.g. sans-serif, monospace) in quotes\nExamples:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor details on font-family syntax, see:\n[📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)' } } };
|
|
784
777
|
}
|
|
785
778
|
});
|
|
786
779
|
|
|
@@ -794,14 +787,21 @@ var require_en_US_template = __commonJS({
|
|
|
794
787
|
// src/locales/en-US.yml
|
|
795
788
|
var require_en_US = __commonJS({
|
|
796
789
|
"src/locales/en-US.yml"(exports2, module2) {
|
|
797
|
-
module2.exports = { dota2tracker: { heroes_nicknames: { "0": 'Please strictly follow the format of "", "" when filling out, if the format is incorrect, only the default name of the hero will be used. The default name of the hero may be omitted.', "1": '"Anti-Mage"', "2": '"Axe"', "3": '"Bane"', "4": '"Bloodseeker"', "5": '"Crystal Maiden"', "6": '"Drow Ranger"', "7": '"Earthshaker"', "8": '"Juggernaut"', "9": '"Mirana"', "10": '"Morphling"', "11": '"Shadow Fiend"', "12": '"Phantom Lancer"', "13": '"Puck"', "14": '"Pudge"', "15": '"Razor"', "16": '"Sand King"', "17": '"Storm Spirit"', "18": '"Sven"', "19": '"Tiny"', "20": '"Vengeful Spirit"', "21": '"Windranger"', "22": '"Zeus"', "23": '"Kunkka"', "25": '"Lina"', "26": '"Lion"', "27": '"Shadow Shaman"', "28": '"Slardar"', "29": '"Tidehunter"', "30": '"Witch Doctor"', "31": '"Lich"', "32": '"Riki"', "33": '"Enigma"', "34": '"Tinker"', "35": '"Sniper"', "36": '"Necrophos"', "37": '"Warlock"', "38": '"Beastmaster"', "39": '"Queen of Pain"', "40": '"Venomancer"', "41": '"Faceless Void"', "42": '"Wraith King"', "43": '"Death Prophet"', "44": '"Phantom Assassin"', "45": '"Pugna"', "46": '"Templar Assassin"', "47": '"Viper"', "48": '"Luna"', "49": '"Dragon Knight"', "50": '"Dazzle"', "51": '"Clockwerk"', "52": '"Leshrac"', "53": `"Nature's Prophet"`, "54": '"Lifestealer"', "55": '"Dark Seer"', "56": '"Clinkz"', "57": '"Omniknight"', "58": '"Enchantress"', "59": '"Huskar"', "60": '"Night Stalker"', "61": '"Broodmother"', "62": '"Bounty Hunter"', "63": '"Weaver"', "64": '"Jakiro"', "65": '"Batrider"', "66": '"Chen"', "67": '"Spectre"', "68": '"Ancient Apparition"', "69": '"Doom"', "70": '"Ursa"', "71": '"Spirit Breaker"', "72": '"Gyrocopter"', "73": '"Alchemist"', "74": '"Invoker"', "75": '"Silencer"', "76": '"Outworld Devourer"', "77": '"Lycan"', "78": '"Brewmaster"', "79": '"Shadow Demon"', "80": '"Lone Druid"', "81": '"Chaos Knight"', "82": '"Meepo"', "83": '"Treant Protector"', "84": '"Ogre Magi"', "85": '"Undying"', "86": '"Rubick"', "87": '"Disruptor"', "88": '"Nyx Assassin"', "89": '"Naga Siren"', "90": '"Keeper of the Light"', "91": '"Io"', "92": '"Visage"', "93": '"Slark"', "94": '"Medusa"', "95": '"Troll Warlord"', "96": '"Centaur Warrunner"', "97": '"Magnus"', "98": '"Timbersaw"', "99": '"Bristleback"', "100": '"Tusk"', "101": '"Skywrath Mage"', "102": '"Abaddon"', "103": '"Elder Titan"', "104": '"Legion Commander"', "105": '"Techies"', "106": '"Ember Spirit"', "107": '"Earth Spirit"', "108": '"Underlord"', "109": '"Terrorblade"', "110": '"Phoenix"', "111": '"Oracle"', "112": '"Winter Wyvern"', "113": '"Arc Warden"', "114": '"Monkey King"', "119": '"Dark Willow"', "120": '"Pangolier"', "121": '"Grimstroke"', "123": '"Hoodwink"', "126": '"Void Spirit"', "128": '"Snapfire"', "129": '"Mars"', "131": '"Ring Master"', "135": '"Dawnbreaker"', "136": '"Marci"', "137": '"Primal Beast"', "138": '"Muerta"', "145": '"Kez"' }, broadcast: { WIN_NEGATIVE: `"Won the match by sheer luck", "Won the match by a stroke of bad luck", "Coasted to victory", "Didn't even show up for the team fight, but my teammates won 4v5"`, WIN_POSITIVE: '"Led the team to victory", "Dominated the opponents and secured the win", "Carried the game to victory", "Treated the opponents like pigs and won", "Won again; this game is just so monotonous and dull", "Simply achieved a win in the match"', LOSE_NEGATIVE: '"Got crushed and lost the match", "Lost the match miserably", "Got my head knocked sideways and lost the match with a blown mindset", "Went fishing but got eaten by the fish, lost the match", "Played terribly", "Simply suffered a loss in the match"', LOSE_POSITIVE: `"Lost the match with no way to turn it around", "Gave it my all, but still lost the match", "Though we lost, we still have honor", "Couldn't carry my teammates, lost the match", "Lost again, it hurts; I'd rather it be me losing"`, message: "{name}'s {hero_name} {comment}.\nKDA: {kda}, GPM/XPM: {gpm_xpm}, Last Hits/Denies: {lh_dn}, Damage/Tower Damage: {damage}, Kill/Death Contribution Rate: {kc_dc}", rank_changed: "Player {name} rank changed: {prev.medal} {prev.star} → {curr.medal} {curr.star}" }, logger: { fetch_guilds_failed: "Failed to fetch guild information.", match_tracked: "Tracked
|
|
790
|
+
module2.exports = { dota2tracker: { heroes_nicknames: { "0": 'Please strictly follow the format of "", "" when filling out, if the format is incorrect, only the default name of the hero will be used. The default name of the hero may be omitted.', "1": '"Anti-Mage"', "2": '"Axe"', "3": '"Bane"', "4": '"Bloodseeker"', "5": '"Crystal Maiden"', "6": '"Drow Ranger"', "7": '"Earthshaker"', "8": '"Juggernaut"', "9": '"Mirana"', "10": '"Morphling"', "11": '"Shadow Fiend"', "12": '"Phantom Lancer"', "13": '"Puck"', "14": '"Pudge"', "15": '"Razor"', "16": '"Sand King"', "17": '"Storm Spirit"', "18": '"Sven"', "19": '"Tiny"', "20": '"Vengeful Spirit"', "21": '"Windranger"', "22": '"Zeus"', "23": '"Kunkka"', "25": '"Lina"', "26": '"Lion"', "27": '"Shadow Shaman"', "28": '"Slardar"', "29": '"Tidehunter"', "30": '"Witch Doctor"', "31": '"Lich"', "32": '"Riki"', "33": '"Enigma"', "34": '"Tinker"', "35": '"Sniper"', "36": '"Necrophos"', "37": '"Warlock"', "38": '"Beastmaster"', "39": '"Queen of Pain"', "40": '"Venomancer"', "41": '"Faceless Void"', "42": '"Wraith King"', "43": '"Death Prophet"', "44": '"Phantom Assassin"', "45": '"Pugna"', "46": '"Templar Assassin"', "47": '"Viper"', "48": '"Luna"', "49": '"Dragon Knight"', "50": '"Dazzle"', "51": '"Clockwerk"', "52": '"Leshrac"', "53": `"Nature's Prophet"`, "54": '"Lifestealer"', "55": '"Dark Seer"', "56": '"Clinkz"', "57": '"Omniknight"', "58": '"Enchantress"', "59": '"Huskar"', "60": '"Night Stalker"', "61": '"Broodmother"', "62": '"Bounty Hunter"', "63": '"Weaver"', "64": '"Jakiro"', "65": '"Batrider"', "66": '"Chen"', "67": '"Spectre"', "68": '"Ancient Apparition"', "69": '"Doom"', "70": '"Ursa"', "71": '"Spirit Breaker"', "72": '"Gyrocopter"', "73": '"Alchemist"', "74": '"Invoker"', "75": '"Silencer"', "76": '"Outworld Devourer"', "77": '"Lycan"', "78": '"Brewmaster"', "79": '"Shadow Demon"', "80": '"Lone Druid"', "81": '"Chaos Knight"', "82": '"Meepo"', "83": '"Treant Protector"', "84": '"Ogre Magi"', "85": '"Undying"', "86": '"Rubick"', "87": '"Disruptor"', "88": '"Nyx Assassin"', "89": '"Naga Siren"', "90": '"Keeper of the Light"', "91": '"Io"', "92": '"Visage"', "93": '"Slark"', "94": '"Medusa"', "95": '"Troll Warlord"', "96": '"Centaur Warrunner"', "97": '"Magnus"', "98": '"Timbersaw"', "99": '"Bristleback"', "100": '"Tusk"', "101": '"Skywrath Mage"', "102": '"Abaddon"', "103": '"Elder Titan"', "104": '"Legion Commander"', "105": '"Techies"', "106": '"Ember Spirit"', "107": '"Earth Spirit"', "108": '"Underlord"', "109": '"Terrorblade"', "110": '"Phoenix"', "111": '"Oracle"', "112": '"Winter Wyvern"', "113": '"Arc Warden"', "114": '"Monkey King"', "119": '"Dark Willow"', "120": '"Pangolier"', "121": '"Grimstroke"', "123": '"Hoodwink"', "126": '"Void Spirit"', "128": '"Snapfire"', "129": '"Mars"', "131": '"Ring Master"', "135": '"Dawnbreaker"', "136": '"Marci"', "137": '"Primal Beast"', "138": '"Muerta"', "145": '"Kez"' }, broadcast: { WIN_NEGATIVE: `"Won the match by sheer luck", "Won the match by a stroke of bad luck", "Coasted to victory", "Didn't even show up for the team fight, but my teammates won 4v5"`, WIN_POSITIVE: '"Led the team to victory", "Dominated the opponents and secured the win", "Carried the game to victory", "Treated the opponents like pigs and won", "Won again; this game is just so monotonous and dull", "Simply achieved a win in the match"', LOSE_NEGATIVE: '"Got crushed and lost the match", "Lost the match miserably", "Got my head knocked sideways and lost the match with a blown mindset", "Went fishing but got eaten by the fish, lost the match", "Played terribly", "Simply suffered a loss in the match"', LOSE_POSITIVE: `"Lost the match with no way to turn it around", "Gave it my all, but still lost the match", "Though we lost, we still have honor", "Couldn't carry my teammates, lost the match", "Lost again, it hurts; I'd rather it be me losing"`, message: "{name}'s {hero_name} {comment}.\nKDA: {kda}, GPM/XPM: {gpm_xpm}, Last Hits/Denies: {lh_dn}, Damage/Tower Damage: {damage}, Kill/Death Contribution Rate: {kc_dc}", rank_changed: "Player {name} rank changed: {prev.medal} {prev.star} → {curr.medal} {curr.star}" }, logger: { fetch_guilds_failed: "Failed to fetch guild information.", match_tracked: "Tracked a new, unbroadcasted match {match.id} from {#each messageToLogger as item}users in guild {item.platform}:{item.guildId} [{#each item.players as player}{player.nickname}({player.steamId}){#if player !== item.players[item.players.length - 1]}, {/if}{/each}]{#if item !== messageToLogger[messageToLogger.length - 1]}, {/if}{/each}.", parse_request_sent: "The parsing request for match {matchId} has been successfully sent to the STRATZ server.", parse_request_failed: "The parsing request for match {matchId} failed to send.", match_parsed: "Match {matchId} has been parsed, an image was generated and published to {#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}, {/if}{/each}.", match_unparsed: "Match {matchId} exceeded the waiting time [{timeout}] and remains unparsed, an image was generated and published to {#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}, {/if}{/each}.", waiting_for_parse: "Match {matchId} is still being parsed, continue waiting.", report_sent: "Posted {title} on {platform}:{guildId}.", rank_sent: "Posted rank change information of {player.nickName} ({player.steamId}) on {platform}:{guildId}.", ejs_error: "Error rendering EJS template: {error}", cron_not_enabled: "Cron service is not enabled; match report tracking cannot run.", stratz_token_banned: "Stratz API request denied (403). This usually means your token or IP has been temporarily restricted due to unusual activity. For details, see the documentation: http://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/api-403.html", stratz_api_query_error: "Stratz API returned partial data with errors: {cause}" }, time: { years_months_ago: "{years} years and {months} months ago", years_ago: "{years} years ago" } } };
|
|
798
791
|
}
|
|
799
792
|
});
|
|
800
793
|
|
|
801
794
|
// src/locales/zh-CN.command.yml
|
|
802
795
|
var require_zh_CN_command = __commonJS({
|
|
803
796
|
"src/locales/zh-CN.command.yml"(exports2, module2) {
|
|
804
|
-
module2.exports = { commands: { dota2tracker: { subscribe: { description: "
|
|
797
|
+
module2.exports = { commands: { dota2tracker: { description: "dota2tracker的一系列指令,可以使用dota2tracker -h查看所有可用指令。", subscribe: { description: "[订阅本群]", usage: "订阅后还需玩家在本群绑定SteamID,BOT将订阅本群中已绑定玩家的新比赛数据,在STRATZ比赛解析完成后将比赛数据生成为图片战报发布至本群中。", messages: { subscribed: "本群已订阅,无需重复订阅。", subscribe_success: "订阅成功。" } }, unsubscribe: { description: "[取消订阅] 取消订阅本群。", messages: { unsubscribe_success: "取消订阅成功。", not_subscribed: "本群尚未订阅,无需取消订阅。" } }, bind: { description: "[绑定] 绑定SteamID,并起一个别名(也可以不起)。", usage: "将你的SteamID与你的账号绑定,若本群已订阅将会实时获取你的新比赛数据发布至群中。", examples: '绑定 123456789\n绑定 123456789 张三\n绑定 123456789 "张 三"', messages: { steam_id_invalid: "SteamID无效。", bind_success: "绑定成功,\nID:{userId}\n别名:{nickName}\nSteamID:{steamId}", bind_failed: "绑定失败,{0}", reason_without_match: "SteamID无效或无任何场次。", reason_fetch_failed: "网络状况不佳或其他原因无法验证SteamID,请稍后重试。", already_binded: "你已绑定,无需重复绑定。\n以下是你的个人信息:\nID:{userId}\n别名:{nickName}\nSteamID:{steamId}", nick_name_too_long: "别名过长,请限制在20个字符以内。(也可以留空)", is_anonymous: "请注意:你的Steam玩家数据并未公开,将无法使用BOT的主要功能,如“战报追踪”、“查询最近指令”等。\n如需公开数据,请在DOTA2游戏内设置中公开。" } }, unbind: { description: "[取消绑定] 取消绑定你的个人信息。", messages: { unbind_success: "取消绑定成功。", not_binded: "尚未绑定,无需取消绑定。" } }, rename: { description: "[改名] 修改绑定时设定的别名。", examples: '改名 李四\n改名 "李 四"', messages: { rename_success: "改名成功,现在你叫{nick_name}了。", empty_input: "请输入你的别名。", not_binded: "请先绑定,绑定时即可设定别名。", nick_name_too_long: "别名过长,请限制在20个字符以内。", nick_name_same: "目标别名与原始别名相同,无需改名。" } }, "query-members": { description: "[查询群友] 查询本群已绑定的玩家。", messages: { title: "本群 DOTA2 玩家名册 (共 {count} 人)", table_headers: { nickname: "昵称/别名", winrate: "胜率 (近10场)", last_match: "最近比赛" }, no_members: "本群尚无绑定玩家。", query_failed: "查询群友失败。" } }, "query-match": { description: "[查询比赛] 查询指定比赛ID的比赛数据,生成图片发布。", options: { parse: "-p 是否等待解析比赛数据" }, examples: "查询比赛 1234567890\n查询比赛 1234567890 -p\n查询比赛 1234567890 --parse", messages: { empty_input: "请输入比赛ID。", match_id_invalid: "比赛ID无效。", querying_match: "正在搜索对局详情,请稍后……", query_failed: "获取比赛数据失败。", waiting_for_parse: "比赛数据尚未解析,已发送解析请求到服务器,战报将在解析完成或超时后发送。" } }, "query-recent-match": { description: "[查询最近比赛] 查询最近的比赛数据,生成图片发布。", options: { parse: "-p 是否等待解析比赛数据" }, usage: "查询指定玩家的最近一场比赛的比赛数据,生成图片发布。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", examples: "查询最近比赛\n查询最近比赛 123456789\n查询最近比赛 张三\n查询最近比赛 123456789 -p\n查询最近比赛 张三 --parse", messages: { querying_match: "正在搜索对局详情,请稍后……", query_failed: "获取玩家最近比赛失败。", not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。", is_anonymous: "你的比赛数据未公开,无法获取最近比赛数据。\n如需公开数据,请在DOTA2游戏内设置中公开。" } }, "query-player": { description: "[查询玩家] 查询玩家的个人信息,可指定英雄。", options: { hero: "-o 查询玩家指定英雄使用情况(同查询英雄,可用别名或ID)" }, usage: "查询指定玩家的个人信息,生成图片发布,可指定英雄。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", examples: "查询玩家\n查询玩家 123456789\n查询玩家 张三\n查询玩家 张三 --hero 敌法师\n查询玩家 张三 -o 15", messages: { querying_player: "正在获取玩家数据,请稍后……", query_failed: "获取玩家信息失败。", not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。" } }, "query-hero": { description: "[查询英雄] 查询英雄技能/面板信息。", options: { random: "-r 随机选择英雄" }, usage: "查询英雄的技能说明与各项数据,生成图片发布。\n参数可输入英雄ID、英雄名、英雄常用别名。", examples: "查询英雄 15\n查询英雄 雷泽\n查询英雄 电魂", messages: { not_found: "未找到输入的英雄,请确认后重新输入。", querying_hero: "正在获取英雄数据,请稍后……", query_failed: "获取英雄数据失败。", empty_input: "请输入参数。" } }, "query-item": { description: "[查询物品] 查询物品信息。", usage: "查询物品的描述与各项数据,生成图片发布。\n参数可输入物品名(可模糊查找)、物品别名、物品ID。\n可在配置页中设置每次查询的最大发送数量、以及是否在超过限制或未输入参数时发送物品列表。", examples: "查询物品 先锋盾", messages: { query_list_failed: "获取物品列表数据失败。", query_item_failed: "获取物品「{0}」数据失败", querying_item: "正在查询物品数据,请稍候…", cache_building: "初次使用或缓存已过期,正在生成当前版本的物品缓存,请稍后……", empty_input: "未输入关键字参数。根据当前配置{#if show},将返回全部物品列表{:else}无内容可发送{/if}。", not_found: "未找到与关键字匹配的物品,请确认后重试。", too_many_items: "找到{count}个物品,超过最大发送限制({max}个){#if show},将发送物品列表{/if}。", finded_items: "找到以下物品:{#each items as item}{item.name_loc}{#if item !== items[items.length - 1]}、{/if}{/each}" } }, "hero-of-the-day": { description: "[今日英雄] 获取今日英雄推荐。", usage: "获取近期比赛记录、生涯比赛记录,根据胜场、表现分、是否手热等参数计算推荐英雄。\n参数可输入该玩家的SteamID或已在本群绑定玩家的别名,无参数时尝试查询调用指令玩家的SteamID。", options: { days: "-d <number> 近期天数范围,默认值为30" }, examples: "今日英雄\n今日英雄 -d 60\n今日英雄 1234567890\n今日英雄 张三", messages: { title_recommendation: "今日推荐:", recommendation_intro: "今日为您推荐的英雄是:", recommendation_heroes: "{#each heroes as hero}{hero}{#if hero !== heroes[heroes.length - 1]}、{/if}{/each}", recommendation_type_lifetime_only: "您的近期英雄记录为空,本次推荐结果基于您的生涯数据。", recommendation_type_no_record: "无法根据您的近期与生涯数据进行推荐。", details: { pool_description: "推荐结果根据对您的近期与生涯英雄使用记录计分后,按总分排序后对前10位英雄以分数为权重随机取得。", table_intro: "以下是前10位英雄具体得分表。", table_headers: { hero: "英雄名称", recent_wins: "近期胜场分", lifetime_wins: "生涯胜场分<br>(对数)", imp_bonus: "imp奖励分", is_hot_streak: "是否手热", total_score: "总分" }, scoring_formula: "当前计分规则:[近期胜场数 x 1] + [log(生涯胜场数+1) x 5] + [近期imp x 0.1]", hot_streak_desc: "若英雄在3天内使用过,则记为手热英雄,总分提升20%。" }, title_meta: "环境趋势:", meta_intro: "一周内,基于您段位±1 ({tiers}) 范围内各位置<b>选择率≥2%</b>按胜率从高到低前三名优势英雄:", meta_table_header: "英雄名称(选择率% 胜率%)", meta_position: "{pos}号位:" } }, common: { messages: { user_not_binded_in_channel: "无参数时默认从已绑定SteamID玩家中寻找你的信息,但你似乎并没有绑定。\n请在本群绑定SteamID。(可输入【绑定 -h】获取帮助)\n或在指令后跟上希望查询的SteamID或已绑定玩家的别名。", user_not_in_group: "指令调用失败。\n当前不属于群聊状态,必须提供指定玩家的SteamID。", invalid_input_include_steam_id: "SteamID无效并且未在本群根据输入信息找到玩家。" } }, help: { description: "[DOTA2指南] 获取插件的详细在线文档链接。", messages: { content: "DOTA2Tracker 详细功能指南、指令用法、算法说明,请访问在线文档:\nhttps://sjtdev.github.io/koishi-plugin-dota2tracker/" } } } } };
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// src/locales/zh-CN.schema.yml
|
|
802
|
+
var require_zh_CN_schema = __commonJS({
|
|
803
|
+
"src/locales/zh-CN.schema.yml"(exports2, module2) {
|
|
804
|
+
module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,留空时不使用代理", suppressStratzNetworkErrors: "开启后将stratz网络错误日志(如超时、网络不通等,但403 Forbidden除外)使用debug级别输出,默认不显示debug级日志。 \n若需要开启debug日志显示,请见 [📖 配置项#suppressstratznetworkerrors](http://sjtdev.github.io/koishi-plugin-dota2tracker/configs.html#suppressstratznetworkerrors-boolean)" }, message: { $desc: "消息设置", useHeroNicknames: "是否使用英雄别名。关闭后仅使用英雄正式名称。", urlInMessageType: { $desc: "在消息中附带链接,<br/>请选择消息类型:", $inner: ["在查询比赛与战报消息中附带stratz比赛页面链接", "在查询玩家信息消息中附带stratz玩家页面链接", "在查询英雄数据消息中附带刀塔百科对应英雄页面链接"] }, rankBroadSwitch: "段位变动播报", rankBroadStar: "星级变动播报", rankBroadLeader: "冠绝名次变动播报", rankBroadFun: "整活播报模板", maxSendItemCount: "最大发送物品图片数量,<br/> 当超过指定数量时将由下方选项决定是否发送查询结果的物品列表图片", showItemListAtTooMuchItems: "在查询结果的物品数量超过指定数量时,是否发送查询结果的物品列表图片", customItemAlias: { $desc: "额外物品别名设置<br/>当插件内置的[物品别名列表](https://github.com/sjtdev/koishi-plugin-dota2tracker/blob/master/src/locales/zh-CN.constants.json#L304-L407)中没有想要的物品别名可在此处追加,如果是插件疏漏的广为人知的物品别名推荐到源码仓库提交issue或pull request完善列表。<br/>(例如 **关键词**: 闪烁匕首,**别名**: 跳刀)", keyword: "关键词", alias: "别名" } }, report: { $desc: "总结设置", dailyReportSwitch: "日报功能", dailyReportHours: "日报时间小时", dailyReportShowCombi: "日报是否显示组合", weeklyReportSwitch: "周报功能", weeklyReportDayHours: "周报发布于周(几)的(几)点", weeklyReportShowCombi: "周报是否显示组合" }, template: { $desc: "模板设置", template_match: "生成比赛信息图片使用的模板,显示效果见 [📖 模板展示页](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-match.html)。", template_player: "生成玩家信息图片使用的模板。(目前仅有一张模板)", template_hero: "生成英雄信息图片使用的模板。(目前仅有一张模板)", playerRankEstimate: "在player模板中对没有段位的玩家进行段位估算 <br>估算的段位将以灰色图片显示", templateFonts: '模板所使用的字体名。需要 koishi 所在设备安装字体文件。 \n可添加多个字体名,将从上到下回退到第一个可用字体;若所有字体都不可用,则使用系统默认字体。 \n其中字体名若包含空格或特殊字符需要在名称首尾添加引号(此处建议尽量强制使用引号); \n若使用字体族名则必须**不使用引号**,如:\n```\n"Microsoft YaHei"\nsans-serif\n```\n有关font-family的更多信息,请查阅 [📖 MDN: font-family](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family) ' } } };
|
|
805
805
|
}
|
|
806
806
|
});
|
|
807
807
|
|
|
@@ -815,7 +815,7 @@ var require_zh_CN_template = __commonJS({
|
|
|
815
815
|
// src/locales/zh-CN.yml
|
|
816
816
|
var require_zh_CN = __commonJS({
|
|
817
817
|
"src/locales/zh-CN.yml"(exports2, module2) {
|
|
818
|
-
module2.exports = { dota2tracker: { heroes_nicknames: { "0": '请严格遵循 "", "" 格式填写(如下方默认数据,注意是英文半角符号),如果格式有误将仅使用英雄默认名称。可以不包含英雄默认名称。', "1": '"敌法师", "敌法", "AM"', "2": '"斧王"', "3": '"祸乱之源", "祸乱", "水桶腰"', "4": '"血魔"', "5": '"水晶室女", "冰女", "CM"', "6": '"卓尔游侠", "小黑"', "7": '"撼地者", "小牛", "牛头"', "8": '"主宰", "剑圣", "jugg", "奶棒人"', "9": '"米拉娜", "白虎", "pom"', "10": '"变体精灵", "水人"', "11": '"影魔", "影魔王", "SF", "影儿魔儿"', "12": '"幻影长矛手", "PL"', "13": '"帕克"', "14": '"帕吉", "屠夫", "扒鸡", "啪唧"', "15": '"雷泽", "电魂", "电棍"', "16": '"沙王", "SK"', "17": '"风暴之灵", "蓝猫"', "18": '"斯温", "流浪剑客", "流浪"', "19": '"小小"', "20": '"复仇之魂", "复仇", "VS"', "21": '"风行者", "风行", "WR"', "22": '"宙斯"', "23": '"昆卡", "船长"', "25": '"莉娜", "火女"', "26": '"莱恩", "恶魔巫师", "Lion"', "27": '"暗影萨满", "小Y", "小歪"', "28": '"斯拉达", "大鱼", "大鱼人"', "29": '"潮汐猎人", "潮汐", "西瓜皮"', "30": '"巫医"', "31": '"巫妖"', "32": '"力丸", "隐形刺客", "隐刺"', "33": '"谜团"', "34": '"修补匠", "TK", "Tinker"', "35": '"狙击手", "矮人火枪手", "火枪", "传说哥"', "36": '"瘟疫法师", "死灵法", "NEC"', "37": '"术士", "Warlock"', "38": '"兽王"', "39": '"痛苦女王", "女王", "QOP"', "40": '"剧毒术士", "剧毒"', "41": '"虚空假面", "虚空", "JB脸"', "42": '"冥魂大帝", "骷髅王"', "43": '"死亡先知", "DP"', "44": '"幻影刺客", "幻刺", "PA"', "45": '"帕格纳", "骨法", "湮灭法师"', "46": '"圣堂刺客", "圣堂", "TA"', "47": '"冥界亚龙", "毒龙", "Viper"', "48": '"露娜", "月骑", "Luna"', "49": '"龙骑士", "龙骑"', "50": '"戴泽", "暗影牧师", "暗牧"', "51": '"发条技师", "发条"', "52": '"拉席克", "老鹿"', "53": '"先知"', "54": '"噬魂鬼", "小狗"', "55": '"黑暗贤者", "黑贤"', "56": '"克林克兹", "小骷髅"', "57": '"全能骑士", "全能"', "58": '"魅惑魔女", "小鹿"', "59": '"哈斯卡", "神灵", "神灵武士"', "60": '"暗夜魔王", "夜魔"', "61": '"育母蜘蛛", "蜘蛛"', "62": '"赏金猎人", "赏金"', "63": '"编织者", "蚂蚁"', "64": '"杰奇洛", "双头龙"', "65": '"蝙蝠骑士", "蝙蝠"', "66": '"陈", "老陈"', "67": '"幽鬼", "SPE", "UG"', "68": '"远古冰魄", "冰魂"', "69": '"末日使者", "末日", "Doom"', "70": '"熊战士", "拍拍", "拍拍熊"', "71": '"裂魂人", "白牛", "sb"', "72": '"矮人直升机", "飞机"', "73": '"炼金术士", "炼金"', "74": '"祈求者", "卡尔"', "75": '"沉默术士", "沉默"', "76": '"殁境神蚀者", "黑鸟"', "77": '"狼人"', "78": '"酒仙", "熊猫", "熊猫酒仙"', "79": '"暗影恶魔", "毒狗"', "80": '"德鲁伊", "熊德"', "81": '"混沌骑士", "混沌", "CK"', "82": '"米波"', "83": '"树精卫士", "大树", "树精"', "84": '"食人魔魔法师", "蓝胖"', "85": '"不朽尸王", "尸王"', "86": '"拉比克"', "87": '"干扰者", "萨尔"', "88": '"司夜刺客", "小强"', "89": '"娜迦海妖", "小娜迦"', "90": '"光之守卫", "光法"', "91": '"艾欧", "小精灵", "精灵", "IO"', "92": '"维萨吉", "死灵龙", "死灵飞龙"', "93": '"斯拉克", "小鱼", "小鱼人"', "94": '"美杜莎", "一姐", "美杜莎"', "95": '"巨魔战将", "巨魔", "巨馍蘸酱"', "96": '"半人马战行者", "人马", "半人马"', "97": '"马格纳斯", "猛犸"', "98": '"伐木机", "花母鸡"', "99": '"钢背兽", "钢背"', "100": '"巨牙海民", "海民"', "101": '"天怒法师", "天怒"', "102": '"亚巴顿"', "103": '"上古巨神", "大牛"', "104": '"军团指挥官", "军团"', "105": '"工程师", "炸弹", "炸弹人"', "106": '"灰烬之灵", "火猫"', "107": '"大地之灵", "土猫"', "108": '"孽主", "大屁股"', "109": '"恐怖利刃", "TB"', "110": '"凤凰", "烧鸡"', "111": '"神谕者", "神谕"', "112": '"寒冬飞龙", "冰龙"', "113": '"天穹守望者", "电狗"', "114": '"齐天大圣", "大圣"', "119": '"邪影芳灵", "小仙女", "花仙子"', "120": '"石鳞剑士", "滚滚"', "121": '"天涯墨客", "墨客"', "123": '"森海飞霞", "松鼠", "小松鼠", "小松许"', "126": '"虚无之灵", "紫猫"', "128": '"电炎绝手", "老奶奶"', "129": '"玛尔斯"', "131": '"百戏大王"', "135": '"破晓辰星", "大锤"', "136": '"玛西"', "137": '"獸", "畜"', "138": '"琼英碧灵", "奶绿", "绿奶奶"', "145": '"凯", "鸟人"' }, broadcast: { WIN_NEGATIVE: '"侥幸赢得了比赛", "走狗屎运赢得了比赛", "躺赢了比赛", "打团都没来, 队友4V5赢得了比赛"', WIN_POSITIVE: '"带领团队走向了胜利", "暴打对面后赢得了胜利", " CARRY全场赢得了胜利", "把对面当猪宰了, 赢得了胜利", "又赢了, 这游戏就是这么枯燥, 且乏味", "直接进行一个比赛的赢"', LOSE_NEGATIVE: '"被人按在地上摩擦, 输掉了这场比赛", "悲惨地输掉了比赛", "头都被打歪了, 心态爆炸地输掉了比赛", "捕鱼被鱼吃了, 输掉了比赛", "打的是个几把", "直接进行一个比赛的输"', LOSE_POSITIVE: '"无力回天输掉了比赛", "尽力了, 但还是输了比赛", "背靠世界树, 虽败犹荣", "带不动队友, 输了比赛", "又输了, 很难受, 宁愿输的是我"', message: "{name}的{hero_name}{comment}。\nKDA:{kda},GPM/XPM:{gpm_xpm},补刀/反补:{lh_dn},伤害/塔伤:{damage},参战/参葬率:{kc_dc}", rank_changed: "群友 {name} 段位变动:{prev.medal}{prev.star} → {curr.medal}{curr.star}" }, logger: { fetch_guilds_failed: "获取群组信息失败,将继续后续步骤。", match_tracked: "追踪到来自{#each messageToLogger as item}群组 {item.platform}:{item.guildId}
|
|
818
|
+
module2.exports = { dota2tracker: { heroes_nicknames: { "0": '请严格遵循 "", "" 格式填写(如下方默认数据,注意是英文半角符号),如果格式有误将仅使用英雄默认名称。可以不包含英雄默认名称。', "1": '"敌法师", "敌法", "AM"', "2": '"斧王"', "3": '"祸乱之源", "祸乱", "水桶腰"', "4": '"血魔"', "5": '"水晶室女", "冰女", "CM"', "6": '"卓尔游侠", "小黑"', "7": '"撼地者", "小牛", "牛头"', "8": '"主宰", "剑圣", "jugg", "奶棒人"', "9": '"米拉娜", "白虎", "pom"', "10": '"变体精灵", "水人"', "11": '"影魔", "影魔王", "SF", "影儿魔儿"', "12": '"幻影长矛手", "PL"', "13": '"帕克"', "14": '"帕吉", "屠夫", "扒鸡", "啪唧"', "15": '"雷泽", "电魂", "电棍"', "16": '"沙王", "SK"', "17": '"风暴之灵", "蓝猫"', "18": '"斯温", "流浪剑客", "流浪"', "19": '"小小"', "20": '"复仇之魂", "复仇", "VS"', "21": '"风行者", "风行", "WR"', "22": '"宙斯"', "23": '"昆卡", "船长"', "25": '"莉娜", "火女"', "26": '"莱恩", "恶魔巫师", "Lion"', "27": '"暗影萨满", "小Y", "小歪"', "28": '"斯拉达", "大鱼", "大鱼人"', "29": '"潮汐猎人", "潮汐", "西瓜皮"', "30": '"巫医"', "31": '"巫妖"', "32": '"力丸", "隐形刺客", "隐刺"', "33": '"谜团"', "34": '"修补匠", "TK", "Tinker"', "35": '"狙击手", "矮人火枪手", "火枪", "传说哥"', "36": '"瘟疫法师", "死灵法", "NEC"', "37": '"术士", "Warlock"', "38": '"兽王"', "39": '"痛苦女王", "女王", "QOP"', "40": '"剧毒术士", "剧毒"', "41": '"虚空假面", "虚空", "JB脸"', "42": '"冥魂大帝", "骷髅王"', "43": '"死亡先知", "DP"', "44": '"幻影刺客", "幻刺", "PA"', "45": '"帕格纳", "骨法", "湮灭法师"', "46": '"圣堂刺客", "圣堂", "TA"', "47": '"冥界亚龙", "毒龙", "Viper"', "48": '"露娜", "月骑", "Luna"', "49": '"龙骑士", "龙骑"', "50": '"戴泽", "暗影牧师", "暗牧"', "51": '"发条技师", "发条"', "52": '"拉席克", "老鹿"', "53": '"先知"', "54": '"噬魂鬼", "小狗"', "55": '"黑暗贤者", "黑贤"', "56": '"克林克兹", "小骷髅"', "57": '"全能骑士", "全能"', "58": '"魅惑魔女", "小鹿"', "59": '"哈斯卡", "神灵", "神灵武士"', "60": '"暗夜魔王", "夜魔"', "61": '"育母蜘蛛", "蜘蛛"', "62": '"赏金猎人", "赏金"', "63": '"编织者", "蚂蚁"', "64": '"杰奇洛", "双头龙"', "65": '"蝙蝠骑士", "蝙蝠"', "66": '"陈", "老陈"', "67": '"幽鬼", "SPE", "UG"', "68": '"远古冰魄", "冰魂"', "69": '"末日使者", "末日", "Doom"', "70": '"熊战士", "拍拍", "拍拍熊"', "71": '"裂魂人", "白牛", "sb"', "72": '"矮人直升机", "飞机"', "73": '"炼金术士", "炼金"', "74": '"祈求者", "卡尔"', "75": '"沉默术士", "沉默"', "76": '"殁境神蚀者", "黑鸟"', "77": '"狼人"', "78": '"酒仙", "熊猫", "熊猫酒仙"', "79": '"暗影恶魔", "毒狗"', "80": '"德鲁伊", "熊德"', "81": '"混沌骑士", "混沌", "CK"', "82": '"米波"', "83": '"树精卫士", "大树", "树精"', "84": '"食人魔魔法师", "蓝胖"', "85": '"不朽尸王", "尸王"', "86": '"拉比克"', "87": '"干扰者", "萨尔"', "88": '"司夜刺客", "小强"', "89": '"娜迦海妖", "小娜迦"', "90": '"光之守卫", "光法"', "91": '"艾欧", "小精灵", "精灵", "IO"', "92": '"维萨吉", "死灵龙", "死灵飞龙"', "93": '"斯拉克", "小鱼", "小鱼人"', "94": '"美杜莎", "一姐", "美杜莎"', "95": '"巨魔战将", "巨魔", "巨馍蘸酱"', "96": '"半人马战行者", "人马", "半人马"', "97": '"马格纳斯", "猛犸"', "98": '"伐木机", "花母鸡"', "99": '"钢背兽", "钢背"', "100": '"巨牙海民", "海民"', "101": '"天怒法师", "天怒"', "102": '"亚巴顿"', "103": '"上古巨神", "大牛"', "104": '"军团指挥官", "军团"', "105": '"工程师", "炸弹", "炸弹人"', "106": '"灰烬之灵", "火猫"', "107": '"大地之灵", "土猫"', "108": '"孽主", "大屁股"', "109": '"恐怖利刃", "TB"', "110": '"凤凰", "烧鸡"', "111": '"神谕者", "神谕"', "112": '"寒冬飞龙", "冰龙"', "113": '"天穹守望者", "电狗"', "114": '"齐天大圣", "大圣"', "119": '"邪影芳灵", "小仙女", "花仙子"', "120": '"石鳞剑士", "滚滚"', "121": '"天涯墨客", "墨客"', "123": '"森海飞霞", "松鼠", "小松鼠", "小松许"', "126": '"虚无之灵", "紫猫"', "128": '"电炎绝手", "老奶奶"', "129": '"玛尔斯"', "131": '"百戏大王"', "135": '"破晓辰星", "大锤"', "136": '"玛西"', "137": '"獸", "畜"', "138": '"琼英碧灵", "奶绿", "绿奶奶"', "145": '"凯", "鸟人"' }, broadcast: { WIN_NEGATIVE: '"侥幸赢得了比赛", "走狗屎运赢得了比赛", "躺赢了比赛", "打团都没来, 队友4V5赢得了比赛"', WIN_POSITIVE: '"带领团队走向了胜利", "暴打对面后赢得了胜利", " CARRY全场赢得了胜利", "把对面当猪宰了, 赢得了胜利", "又赢了, 这游戏就是这么枯燥, 且乏味", "直接进行一个比赛的赢"', LOSE_NEGATIVE: '"被人按在地上摩擦, 输掉了这场比赛", "悲惨地输掉了比赛", "头都被打歪了, 心态爆炸地输掉了比赛", "捕鱼被鱼吃了, 输掉了比赛", "打的是个几把", "直接进行一个比赛的输"', LOSE_POSITIVE: '"无力回天输掉了比赛", "尽力了, 但还是输了比赛", "背靠世界树, 虽败犹荣", "带不动队友, 输了比赛", "又输了, 很难受, 宁愿输的是我"', message: "{name}的{hero_name}{comment}。\nKDA:{kda},GPM/XPM:{gpm_xpm},补刀/反补:{lh_dn},伤害/塔伤:{damage},参战/参葬率:{kc_dc}", rank_changed: "群友 {name} 段位变动:{prev.medal}{prev.star} → {curr.medal}{curr.star}" }, logger: { fetch_guilds_failed: "获取群组信息失败,将继续后续步骤。", match_tracked: "追踪到来自{#each messageToLogger as item}群组 {item.platform}:{item.guildId} 的玩家 [{#each item.players as player}{player.nickname}({player.steamId}){#if player !== item.players[item.players.length - 1]}、{/if}{/each}]{#if item !== messageToLogger[messageToLogger.length - 1]}、{/if}{/each}的尚未播报过的最新比赛 {match.id}。", parse_request_sent: "比赛 {matchId} 解析请求已成功发送至STRATZ服务器。", parse_request_failed: "比赛 {matchId} 解析请求发送失败。", match_parsed: "比赛 {matchId} 已解析,生成图片并发布于{#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}、{/if}{/each}。", match_unparsed: "比赛 {matchId} 超过等待时间[{timeout}分钟]仍未解析,生成图片并发布于{#each guilds as guild}{guild.platform}:{guild.guildId}{#if guild !== guilds[guilds.length - 1]}、{/if}{/each}。", waiting_for_parse: "比赛 {matchId} 尚未解析完成,继续等待。", report_sent: "发布{title}于{platform}:{guildId}。", rank_sent: "向{platform}:{guildId}发布{player.nickName}({player.steamId})的段位变动信息。", ejs_error: "EJS模板渲染错误:{error}", cron_not_enabled: "未启用cron服务,无法运行战报追踪等定时任务。", stratz_token_banned: "Stratz API 请求被拒绝(403),若频繁发生很有可能意味着您的Token或IP因异常使用被临时限制访问。有关此报错请见文档:http://sjtdev.github.io/koishi-plugin-dota2tracker/api-403.html", stratz_api_query_error: "Stratz API 返回了有效数据,但报错: {cause}" }, time: { years_months_ago: "{years}年{months}个月前", years_ago: "{years}年前" } } };
|
|
819
819
|
}
|
|
820
820
|
});
|
|
821
821
|
|
|
@@ -823,1150 +823,1730 @@ var require_zh_CN = __commonJS({
|
|
|
823
823
|
var src_exports = {};
|
|
824
824
|
__export(src_exports, {
|
|
825
825
|
Config: () => Config,
|
|
826
|
-
GraphqlLanguageEnum: () => GraphqlLanguageEnum,
|
|
827
826
|
apply: () => apply,
|
|
828
827
|
inject: () => inject,
|
|
829
828
|
name: () => name,
|
|
830
829
|
usage: () => usage
|
|
831
830
|
});
|
|
832
831
|
module.exports = __toCommonJS(src_exports);
|
|
833
|
-
var
|
|
832
|
+
var import_path4 = __toESM(require("path"));
|
|
834
833
|
|
|
835
|
-
// src/
|
|
836
|
-
var utils_exports = {};
|
|
837
|
-
__export(utils_exports, {
|
|
838
|
-
CONFIGS: () => CONFIGS,
|
|
839
|
-
HeroDescType: () => HeroDescType,
|
|
840
|
-
ImageFormat: () => ImageFormat,
|
|
841
|
-
ImageType: () => ImageType,
|
|
842
|
-
enhancedSimpleHashToSeed: () => enhancedSimpleHashToSeed,
|
|
843
|
-
formatHeroDesc: () => formatHeroDesc,
|
|
844
|
-
formatNumber: () => formatNumber,
|
|
845
|
-
getFormattedHeroData: () => getFormattedHeroData,
|
|
846
|
-
getFormattedItemListData: () => getFormattedItemListData,
|
|
847
|
-
getFormattedMatchData: () => getFormattedMatchData,
|
|
848
|
-
getFormattedPlayerData: () => getFormattedPlayerData,
|
|
849
|
-
getImageUrl: () => getImageUrl,
|
|
850
|
-
init: () => init,
|
|
851
|
-
playerisValid: () => playerisValid,
|
|
852
|
-
query: () => query,
|
|
853
|
-
queryHeroDetailsFromValve: () => queryHeroDetailsFromValve,
|
|
854
|
-
queryItemDetailsFromValve: () => queryItemDetailsFromValve,
|
|
855
|
-
queryItemListFromValve: () => queryItemListFromValve,
|
|
856
|
-
queryLastPatchNumber: () => queryLastPatchNumber,
|
|
857
|
-
readDirectoryFilesSync: () => readDirectoryFilesSync,
|
|
858
|
-
roundToDecimalPlaces: () => roundToDecimalPlaces,
|
|
859
|
-
sec2time: () => sec2time,
|
|
860
|
-
winRateColor: () => winRateColor
|
|
861
|
-
});
|
|
862
|
-
var import_fs = __toESM(require("fs"));
|
|
863
|
-
var dotaconstants = __toESM(require("dotaconstants"));
|
|
864
|
-
var import_path = __toESM(require("path"));
|
|
834
|
+
// src/app/common/i18n.ts
|
|
865
835
|
var import_koishi = require("koishi");
|
|
866
|
-
var
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
var
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
({ http, setTimeout, APIKEY: CONFIGS.STRATZ_API.TOKEN, proxyAddress } = params);
|
|
873
|
-
}
|
|
874
|
-
__name(init, "init");
|
|
875
|
-
async function fetchData(query2) {
|
|
876
|
-
return await http.post(CONFIGS.STRATZ_API.URL, JSON.stringify(query2), {
|
|
877
|
-
responseType: "json",
|
|
878
|
-
headers: {
|
|
879
|
-
"User-Agent": "STRATZ_API",
|
|
880
|
-
"Content-Type": "application/json",
|
|
881
|
-
Authorization: `Bearer ${CONFIGS.STRATZ_API.TOKEN}`
|
|
882
|
-
},
|
|
883
|
-
proxyAgent: proxyAddress || void 0
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
__name(fetchData, "fetchData");
|
|
887
|
-
async function query(queryName, variables) {
|
|
888
|
-
if (queryName.startsWith("Players") && variables?.steamAccountIds.length > 5) {
|
|
889
|
-
const playerIds = variables?.steamAccountIds ?? [];
|
|
890
|
-
const chunkSize = 5;
|
|
891
|
-
let allPlayers = [];
|
|
892
|
-
for (let i = 0; i < playerIds.length; i += chunkSize) {
|
|
893
|
-
const chunk = playerIds.slice(i, i + chunkSize);
|
|
894
|
-
variables.steamAccountIds = chunk;
|
|
895
|
-
const query_str = loadGraphqlFile(queryName);
|
|
896
|
-
const result = await new Promise((resolve) => setTimeout(async () => resolve(await fetchData({ query: query_str, variables })), 200));
|
|
897
|
-
if (result?.errors) throw { errors: result.errors };
|
|
898
|
-
if (result.data && result.data.players) {
|
|
899
|
-
allPlayers = allPlayers.concat(result.data.players);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
return { players: allPlayers };
|
|
903
|
-
} else {
|
|
904
|
-
const query_str = loadGraphqlFile(queryName);
|
|
905
|
-
const result = await fetchData({ query: query_str, variables });
|
|
906
|
-
if (result.errors) throw { errors: result.errors };
|
|
907
|
-
return result.data;
|
|
908
|
-
}
|
|
836
|
+
var dotaconstants = __toESM(require("dotaconstants"));
|
|
837
|
+
|
|
838
|
+
// src/app/common/utils.ts
|
|
839
|
+
var import_luxon = require("luxon");
|
|
840
|
+
function sec2time(sec) {
|
|
841
|
+
return sec ? (sec < 0 ? "-" : "") + Math.floor(Math.abs(sec) / 60) + ":" + ("00" + Math.abs(sec) % 60).slice(-2) : "--:--";
|
|
909
842
|
}
|
|
910
|
-
__name(
|
|
911
|
-
function
|
|
912
|
-
return
|
|
843
|
+
__name(sec2time, "sec2time");
|
|
844
|
+
function formatNumber(num) {
|
|
845
|
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
913
846
|
}
|
|
914
|
-
__name(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
return valveLanguageTag2;
|
|
919
|
-
})(valveLanguageTag || {});
|
|
920
|
-
async function queryHeroDetailsFromValve(heroId, languageTag = "zh-CN") {
|
|
921
|
-
return (await http.get(`https://www.dota2.com/datafeed/herodata?language=${valveLanguageTag[languageTag]}&hero_id=${heroId}`)).result.data.heroes[0];
|
|
847
|
+
__name(formatNumber, "formatNumber");
|
|
848
|
+
function roundToDecimalPlaces(number, decimalPlaces = 2) {
|
|
849
|
+
const factor = Math.pow(10, decimalPlaces);
|
|
850
|
+
return Math.round(number * factor) / factor;
|
|
922
851
|
}
|
|
923
|
-
__name(
|
|
924
|
-
|
|
925
|
-
|
|
852
|
+
__name(roundToDecimalPlaces, "roundToDecimalPlaces");
|
|
853
|
+
function enhancedSimpleHashToSeed(inputString) {
|
|
854
|
+
const encoded = btoa(inputString);
|
|
855
|
+
let total = 0;
|
|
856
|
+
let complexFactor = 1;
|
|
857
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
858
|
+
total += encoded.charCodeAt(i) * complexFactor;
|
|
859
|
+
complexFactor++;
|
|
860
|
+
total %= 9973;
|
|
861
|
+
}
|
|
862
|
+
total = total % 9973 * (total % 9973) % 9973;
|
|
863
|
+
return total % 1e3 / 1e3;
|
|
926
864
|
}
|
|
927
|
-
__name(
|
|
928
|
-
|
|
929
|
-
|
|
865
|
+
__name(enhancedSimpleHashToSeed, "enhancedSimpleHashToSeed");
|
|
866
|
+
function formatCustomRelativeTime(targetDate, i18n, languageTag) {
|
|
867
|
+
const now = import_luxon.DateTime.now();
|
|
868
|
+
const diffInMonths = now.diff(targetDate, "months").get("months");
|
|
869
|
+
if (diffInMonths < 12) {
|
|
870
|
+
return targetDate.setLocale(languageTag.toLowerCase()).toRelative({ base: now });
|
|
871
|
+
} else {
|
|
872
|
+
const years = Math.floor(diffInMonths / 12);
|
|
873
|
+
const months = Math.floor(diffInMonths % 12);
|
|
874
|
+
if (months === 0) {
|
|
875
|
+
return i18n.$t(languageTag, "dota2tracker.time.years_ago", { years });
|
|
876
|
+
} else {
|
|
877
|
+
return i18n.$t(languageTag, "dota2tracker.time.years_months_ago", { years, months });
|
|
878
|
+
}
|
|
879
|
+
}
|
|
930
880
|
}
|
|
931
|
-
__name(
|
|
932
|
-
|
|
933
|
-
|
|
881
|
+
__name(formatCustomRelativeTime, "formatCustomRelativeTime");
|
|
882
|
+
function clamp(num, min, max, defaultValue = 0) {
|
|
883
|
+
const valueToClamp = Number.isFinite(num) ? num : defaultValue;
|
|
884
|
+
if (min > max) {
|
|
885
|
+
[min, max] = [max, min];
|
|
886
|
+
console.warn(`'clamp' function's min value (${min}) was greater than its max value (${max}). Their values have been swapped.`);
|
|
887
|
+
}
|
|
888
|
+
return Math.min(Math.max(valueToClamp, min), max);
|
|
934
889
|
}
|
|
935
|
-
__name(
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
})
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
890
|
+
__name(clamp, "clamp");
|
|
891
|
+
|
|
892
|
+
// require("../../locales/**/*.constants.json") in src/app/common/i18n.ts
|
|
893
|
+
var globRequire_locales_constants_json = __glob({
|
|
894
|
+
"../../locales/en-US.constants.json": () => require_en_US_constants(),
|
|
895
|
+
"../../locales/zh-CN.constants.json": () => require_zh_CN_constants()
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// require("../../locales/**/*.yml") in src/app/common/i18n.ts
|
|
899
|
+
var globRequire_locales_yml = __glob({
|
|
900
|
+
"../../locales/en-US.command.yml": () => require_en_US_command(),
|
|
901
|
+
"../../locales/en-US.schema.yml": () => require_en_US_schema(),
|
|
902
|
+
"../../locales/en-US.template.yml": () => require_en_US_template(),
|
|
903
|
+
"../../locales/en-US.yml": () => require_en_US(),
|
|
904
|
+
"../../locales/zh-CN.command.yml": () => require_zh_CN_command(),
|
|
905
|
+
"../../locales/zh-CN.schema.yml": () => require_zh_CN_schema(),
|
|
906
|
+
"../../locales/zh-CN.template.yml": () => require_zh_CN_template(),
|
|
907
|
+
"../../locales/zh-CN.yml": () => require_zh_CN()
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// require("../../locales/**/*.command.yml") in src/app/common/i18n.ts
|
|
911
|
+
var globRequire_locales_command_yml = __glob({
|
|
912
|
+
"../../locales/en-US.command.yml": () => require_en_US_command(),
|
|
913
|
+
"../../locales/zh-CN.command.yml": () => require_zh_CN_command()
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// require("../../locales/**/*.template.yml") in src/app/common/i18n.ts
|
|
917
|
+
var globRequire_locales_template_yml = __glob({
|
|
918
|
+
"../../locales/en-US.template.yml": () => require_en_US_template(),
|
|
919
|
+
"../../locales/zh-CN.template.yml": () => require_zh_CN_template()
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// src/app/common/i18n.ts
|
|
923
|
+
var LanguageTags = {
|
|
924
|
+
"en-US": { graphqlTag: "ENGLISH", valveTag: "english" },
|
|
925
|
+
"zh-CN": { graphqlTag: "S_CHINESE", valveTag: "schinese" }
|
|
926
|
+
};
|
|
927
|
+
var I18NService = class extends import_koishi.Service {
|
|
928
|
+
static {
|
|
929
|
+
__name(this, "I18NService");
|
|
930
|
+
}
|
|
931
|
+
constantLocales = {};
|
|
932
|
+
i18n;
|
|
933
|
+
globalLanguageTag;
|
|
934
|
+
constructor(ctx) {
|
|
935
|
+
super(ctx, "dota2tracker.i18n", true);
|
|
936
|
+
this.config = ctx.config;
|
|
937
|
+
this.i18n = this.ctx.i18n;
|
|
938
|
+
for (const supportLanguageTag of Object.keys(LanguageTags)) {
|
|
939
|
+
this.constantLocales[supportLanguageTag] = globRequire_locales_constants_json(`../../locales/${supportLanguageTag}.constants.json`);
|
|
940
|
+
this.i18n.define(supportLanguageTag, globRequire_locales_yml(`../../locales/${supportLanguageTag}.yml`));
|
|
941
|
+
this.i18n.define(supportLanguageTag, globRequire_locales_command_yml(`../../locales/${supportLanguageTag}.command.yml`));
|
|
942
|
+
this.i18n.define(supportLanguageTag, globRequire_locales_template_yml(`../../locales/${supportLanguageTag}.template.yml`));
|
|
969
943
|
}
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
944
|
+
this.globalLanguageTag = this.i18n.fallback(Object.values(this.i18n.locales).map((locale) => Object.keys(locale).at(0))).find((locale) => Object.keys(LanguageTags).includes(locale));
|
|
945
|
+
}
|
|
946
|
+
getGraphqlLanguageTag(languageTag) {
|
|
947
|
+
return LanguageTags[languageTag].graphqlTag;
|
|
948
|
+
}
|
|
949
|
+
getValveLanguageTag(languageTag) {
|
|
950
|
+
return LanguageTags[languageTag].valveTag;
|
|
951
|
+
}
|
|
952
|
+
async getLanguageTag(options) {
|
|
953
|
+
const { session, channel, channelId } = options || {};
|
|
954
|
+
const resolvedChannel = channel ?? (await this.ctx.database?.get("channel", { id: session?.event.channel.id ?? channelId }))?.at(0);
|
|
955
|
+
return this.i18n.fallback((resolvedChannel?.locales ?? []).concat(Object.values(this.i18n.locales).map((locale) => Object.keys(locale).at(0)))).find((locale) => Object.keys(LanguageTags).includes(locale));
|
|
956
|
+
}
|
|
957
|
+
getConstantLocale(tag) {
|
|
958
|
+
return this.constantLocales[tag];
|
|
959
|
+
}
|
|
960
|
+
render(...args) {
|
|
961
|
+
return this.i18n.render.apply(this.i18n, args);
|
|
962
|
+
}
|
|
963
|
+
$t(languageTag, key, param) {
|
|
964
|
+
const keys = Array.isArray(key) ? key : key.split(".");
|
|
965
|
+
const params = Array.isArray(param) ? param : [param];
|
|
966
|
+
const constantTranslation = keys.reduce((result2, k) => {
|
|
967
|
+
return result2?.[k] ?? null;
|
|
968
|
+
}, this.constantLocales[languageTag] || {});
|
|
969
|
+
if (constantTranslation) {
|
|
970
|
+
if (Array.isArray(params)) {
|
|
971
|
+
return constantTranslation.replace(/\{(\d+)\}/g, (_, index) => params[+index] || "");
|
|
985
972
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
}
|
|
989
|
-
match.party = {};
|
|
990
|
-
let party_index = 0;
|
|
991
|
-
const party_mark = ["I", "II", "III", "IV"];
|
|
992
|
-
let heroOrderList = {};
|
|
993
|
-
for (let hero of match.pickBans ?? []) {
|
|
994
|
-
if (hero.isPick) heroOrderList[hero.heroId] = hero.order;
|
|
995
|
-
}
|
|
996
|
-
let processLaneOutcome = /* @__PURE__ */ __name(function(outcome) {
|
|
997
|
-
switch (outcome) {
|
|
998
|
-
case "RADIANT_VICTORY":
|
|
999
|
-
return { radiant: "advantage", dire: "disadvantage" };
|
|
1000
|
-
case "RADIANT_STOMP":
|
|
1001
|
-
return { radiant: "stomp", dire: "stomped" };
|
|
1002
|
-
case "DIRE_VICTORY":
|
|
1003
|
-
return { radiant: "disadvantage", dire: "advantage" };
|
|
1004
|
-
case "DIRE_STOMP":
|
|
1005
|
-
return { radiant: "stomped", dire: "stomp" };
|
|
1006
|
-
default:
|
|
1007
|
-
return { radiant: "tie", dire: "tie" };
|
|
1008
|
-
}
|
|
1009
|
-
}, "processLaneOutcome");
|
|
1010
|
-
let laneResult = { top: {}, mid: {}, bottom: {} };
|
|
1011
|
-
laneResult.top = processLaneOutcome(match.topLaneOutcome);
|
|
1012
|
-
laneResult.mid = processLaneOutcome(match.midLaneOutcome);
|
|
1013
|
-
laneResult.bottom = processLaneOutcome(match.bottomLaneOutcome);
|
|
1014
|
-
match.players.forEach((player) => {
|
|
1015
|
-
player.team = player.isRadiant ? "radiant" : "dire";
|
|
1016
|
-
player.rank = {
|
|
1017
|
-
medal: parseInt(player.steamAccount.seasonRank?.toString().split("")[0] ?? 0),
|
|
1018
|
-
star: parseInt(player.steamAccount.seasonRank?.toString().split("")[1] ?? 0),
|
|
1019
|
-
leaderboard: player.steamAccount.seasonLeaderboardRank,
|
|
1020
|
-
inTop100: player.steamAccount.seasonLeaderboardRank ? player.steamAccount.seasonLeaderboardRank <= 10 ? "8c" : player.steamAccount.seasonLeaderboardRank <= 100 ? "8b" : void 0 : void 0
|
|
1021
|
-
};
|
|
1022
|
-
player.killContribution = (player.kills + player.assists) / match[player.team].killsCount;
|
|
1023
|
-
player.deathContribution = player.deaths / match[player.team === "radiant" ? "dire" : "radiant"].killsCount;
|
|
1024
|
-
player.damageReceived = (player.stats?.heroDamageReport?.receivedTotal?.physicalDamage ?? 0) + (player.stats?.heroDamageReport?.receivedTotal?.magicalDamage ?? 0) + (player.stats?.heroDamageReport?.receivedTotal?.pureDamage ?? 0);
|
|
1025
|
-
match[player.team].heroDamage = (match[player.team].heroDamage ?? 0) + player.heroDamage;
|
|
1026
|
-
match[player.team].damageReceived = (match[player.team].damageReceived ?? 0) + player.damageReceived;
|
|
1027
|
-
match[player.team].networth += player.networth;
|
|
1028
|
-
match[player.team].experience += Math.floor(player.experiencePerMinute / 60 * match.durationSeconds);
|
|
1029
|
-
player.titles = [];
|
|
1030
|
-
player.mvpScore = // 计算MVP分数
|
|
1031
|
-
player.kills * 5 + player.assists * 3 + (player.stats.heroDamageReport?.dealtTotal.stunDuration ?? 0) / 100 * 0.1 + (player.stats.heroDamageReport?.dealtTotal.disableDuration ?? 0) / 100 * 0.05 + (player.stats.heroDamageReport?.dealtTotal.slowDuration ?? 0) / 100 * 0.025 + player.heroDamage * 1e-3 + player.towerDamage * 0.01 + player.heroHealing * 2e-3 + player.imp * 0.25;
|
|
1032
|
-
player.order = heroOrderList[player.hero.id];
|
|
1033
|
-
if (player.partyId != null) {
|
|
1034
|
-
if (!match.party[player.partyId]) match.party[player.partyId] = party_mark[party_index++];
|
|
1035
|
-
}
|
|
1036
|
-
if (player.stats.matchPlayerBuffEvent) {
|
|
1037
|
-
const buffMap = /* @__PURE__ */ new Map();
|
|
1038
|
-
for (const event of player.stats.matchPlayerBuffEvent) {
|
|
1039
|
-
const isAbility = event.abilityId != null;
|
|
1040
|
-
const id = isAbility ? event.abilityId : event.itemId;
|
|
1041
|
-
const type = isAbility ? "ability" : "item";
|
|
1042
|
-
const compositeKey = `${type}|${id}`;
|
|
1043
|
-
const current = buffMap.get(compositeKey);
|
|
1044
|
-
if (!current || event.stackCount > current.stackCount) {
|
|
1045
|
-
buffMap.set(compositeKey, {
|
|
1046
|
-
id,
|
|
1047
|
-
type,
|
|
1048
|
-
stackCount: event.stackCount ?? 0
|
|
1049
|
-
});
|
|
1050
|
-
}
|
|
973
|
+
if (typeof params === "object" && params !== null) {
|
|
974
|
+
return constantTranslation.replace(/\{(\w+)\}/g, (_, key2) => params[key2] || "");
|
|
1051
975
|
}
|
|
1052
|
-
|
|
1053
|
-
} else {
|
|
1054
|
-
player.buffs = [];
|
|
1055
|
-
}
|
|
1056
|
-
switch (player.lane) {
|
|
1057
|
-
case "SAFE_LANE":
|
|
1058
|
-
player.laneResult = laneResult[player.isRadiant ? "bottom" : "top"][player.team];
|
|
1059
|
-
break;
|
|
1060
|
-
case "OFF_LANE":
|
|
1061
|
-
player.laneResult = laneResult[!player.isRadiant ? "bottom" : "top"][player.team];
|
|
1062
|
-
break;
|
|
1063
|
-
case "JUNGLE":
|
|
1064
|
-
player.laneResult = "jungle";
|
|
1065
|
-
break;
|
|
1066
|
-
default:
|
|
1067
|
-
player.laneResult = laneResult.mid[player.team];
|
|
1068
|
-
break;
|
|
976
|
+
return constantTranslation;
|
|
1069
977
|
}
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
switch (item.itemId) {
|
|
1097
|
-
case 30:
|
|
1098
|
-
case 40:
|
|
1099
|
-
case 42:
|
|
1100
|
-
case 43:
|
|
1101
|
-
case 188:
|
|
1102
|
-
supportItemsCount[item.itemId]++;
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
978
|
+
const originalKey = Array.isArray(key) ? key : [key];
|
|
979
|
+
const elements = this.render([languageTag], originalKey, param ?? {});
|
|
980
|
+
const result = elements.map((el) => el.toString()).join("");
|
|
981
|
+
if (result == key) return;
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
gt(key, param) {
|
|
985
|
+
return this.$t(this.globalLanguageTag, key, param);
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* [私有] 获取单个英雄的官方译名。
|
|
989
|
+
* 这是获取英雄名的最快方式。
|
|
990
|
+
*/
|
|
991
|
+
_getOfficialHeroName(heroId, languageTag) {
|
|
992
|
+
return this.$t(languageTag, `dota2tracker.template.hero_names.${heroId}`);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* [私有] 获取单个英雄的所有名称(官方名 + 社区别名)。
|
|
996
|
+
*/
|
|
997
|
+
_getAllHeroNames(heroId, languageTag) {
|
|
998
|
+
const officialName = this._getOfficialHeroName(heroId, languageTag);
|
|
999
|
+
let nicknames = [];
|
|
1000
|
+
try {
|
|
1001
|
+
const rawContent = this.i18n.render([languageTag], [`dota2tracker.heroes_nicknames.${heroId}`], {}).at(0)?.attrs?.content ?? "";
|
|
1002
|
+
if (rawContent) {
|
|
1003
|
+
nicknames = JSON.parse(`[${rawContent}]`);
|
|
1105
1004
|
}
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
this.logger.error(`Failed to parse nicknames for heroId ${heroId}: ${error.message}`);
|
|
1007
|
+
nicknames = [];
|
|
1106
1008
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1009
|
+
return Array.from(/* @__PURE__ */ new Set([officialName, ...nicknames]));
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* [公共] 根据配置,获取英雄的最终显示名称(官方名称或随机别名)。
|
|
1013
|
+
* 这是插件其他部分应该调用的唯一方法。
|
|
1014
|
+
*/
|
|
1015
|
+
getDisplayNameForHero(heroId, languageTag, options) {
|
|
1016
|
+
if (options.forceOfficialName || !this.config.useHeroNicknames) {
|
|
1017
|
+
return this._getOfficialHeroName(heroId, languageTag);
|
|
1114
1018
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
for (let i = 0; i <= 5; i++) {
|
|
1119
|
-
const key = `item${i}Id`;
|
|
1120
|
-
const itemId = player[key];
|
|
1121
|
-
if (itemId === void 0 || itemId === null) {
|
|
1122
|
-
player.items.push(null);
|
|
1123
|
-
} else if (dotaconstants.item_ids[itemId]) {
|
|
1124
|
-
const name2 = dotaconstants.item_ids[itemId];
|
|
1125
|
-
const isRecipe = name2.startsWith(prefix);
|
|
1126
|
-
const cleanName = isRecipe ? name2.substring(prefix.length) : name2;
|
|
1127
|
-
player.items.push({
|
|
1128
|
-
id: itemId,
|
|
1129
|
-
name: cleanName,
|
|
1130
|
-
time: items_timelist[itemId]?.getNextElement ? items_timelist[itemId].getNextElement() : void 0,
|
|
1131
|
-
isRecipe
|
|
1132
|
-
});
|
|
1133
|
-
} else {
|
|
1134
|
-
player.items.push(null);
|
|
1135
|
-
}
|
|
1019
|
+
const allNames = this._getAllHeroNames(heroId, languageTag);
|
|
1020
|
+
if (allNames.length === 0) {
|
|
1021
|
+
return this._getOfficialHeroName(heroId, languageTag);
|
|
1136
1022
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const cleanName = isRecipe ? name2.substring(prefix.length) : name2;
|
|
1146
|
-
player.backpacks.push({
|
|
1147
|
-
id: itemId,
|
|
1148
|
-
name: cleanName,
|
|
1149
|
-
time: items_timelist[itemId],
|
|
1150
|
-
isRecipe
|
|
1151
|
-
});
|
|
1152
|
-
} else {
|
|
1153
|
-
player.backpacks.push(null);
|
|
1154
|
-
}
|
|
1023
|
+
let randomizer;
|
|
1024
|
+
if (options.random instanceof import_koishi.Random) {
|
|
1025
|
+
randomizer = options.random;
|
|
1026
|
+
} else if (options.seed) {
|
|
1027
|
+
const seed = enhancedSimpleHashToSeed(options.seed);
|
|
1028
|
+
randomizer = new import_koishi.Random(() => seed);
|
|
1029
|
+
} else {
|
|
1030
|
+
randomizer = new import_koishi.Random();
|
|
1155
1031
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
time: items_timelist[itemId],
|
|
1173
|
-
isRecipe
|
|
1174
|
-
});
|
|
1175
|
-
} else {
|
|
1176
|
-
player.unitItems.push(null);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
for (let i = 0; i <= 2; i++) {
|
|
1180
|
-
const key = `backpack${i}Id`;
|
|
1181
|
-
const itemId = player.additionalUnit[key];
|
|
1182
|
-
if (itemId === void 0 || itemId === null) {
|
|
1183
|
-
player.unitBackpacks.push(null);
|
|
1184
|
-
} else if (dotaconstants.item_ids[itemId]) {
|
|
1185
|
-
const name2 = dotaconstants.item_ids[itemId];
|
|
1186
|
-
const isRecipe = name2.startsWith(prefix2);
|
|
1187
|
-
const cleanName = isRecipe ? name2.substring(prefix2.length) : name2;
|
|
1188
|
-
player.unitBackpacks.push({
|
|
1189
|
-
id: itemId,
|
|
1190
|
-
name: cleanName,
|
|
1191
|
-
time: items_timelist[itemId],
|
|
1192
|
-
isRecipe
|
|
1193
|
-
});
|
|
1194
|
-
} else {
|
|
1195
|
-
player.unitBackpacks.push(null);
|
|
1196
|
-
}
|
|
1032
|
+
return randomizer.pick(allNames);
|
|
1033
|
+
}
|
|
1034
|
+
// ✅ 用于缓存每个语言的别名映射表
|
|
1035
|
+
_nicknameCache = /* @__PURE__ */ new Map();
|
|
1036
|
+
/**
|
|
1037
|
+
* [私有] 为指定语言构建一个从“别名/官方名”到 heroId 的映射表。
|
|
1038
|
+
* 这是一个昂贵的操作,其结果应该被缓存。
|
|
1039
|
+
*/
|
|
1040
|
+
_buildNicknameMap(languageTag) {
|
|
1041
|
+
this.logger.debug(`Building nickname map for ${languageTag}...`);
|
|
1042
|
+
const heroIds = Object.keys(dotaconstants.heroes).map(Number);
|
|
1043
|
+
const nicknameMap = /* @__PURE__ */ new Map();
|
|
1044
|
+
for (const heroId of heroIds) {
|
|
1045
|
+
const allNames = this._getAllHeroNames(heroId, languageTag);
|
|
1046
|
+
for (const name2 of allNames) {
|
|
1047
|
+
nicknameMap.set(name2.toLowerCase(), heroId);
|
|
1197
1048
|
}
|
|
1198
1049
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1050
|
+
return nicknameMap;
|
|
1051
|
+
}
|
|
1052
|
+
findHeroIdInLocale(input) {
|
|
1053
|
+
if (input === null || input === void 0 || input === "") return;
|
|
1054
|
+
const inputStr = String(input).toLowerCase();
|
|
1055
|
+
if (/^\d+$/.test(inputStr)) {
|
|
1056
|
+
const heroId = Number(inputStr);
|
|
1057
|
+
if (dotaconstants.heroes[heroId]) {
|
|
1058
|
+
return heroId;
|
|
1059
|
+
}
|
|
1202
1060
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
ComparisonMode2["Max"] = "max";
|
|
1207
|
-
ComparisonMode2["Min"] = "min";
|
|
1208
|
-
})(ComparisonMode || (ComparisonMode = {}));
|
|
1209
|
-
function findMaxByProperty(primaryProperty, secondaryProperty = null, players = match.players, primaryMode = "max" /* Max */, secondaryMode = "max" /* Max */) {
|
|
1210
|
-
let maxPlayer = players.reduce((result, player) => {
|
|
1211
|
-
const primaryComparison = primaryMode === "max" /* Max */ ? player[primaryProperty] > result[primaryProperty] : player[primaryProperty] < result[primaryProperty];
|
|
1212
|
-
const secondaryComparison = secondaryMode === "max" /* Max */ ? player[secondaryProperty] > result[secondaryProperty] : player[secondaryProperty] < result[secondaryProperty];
|
|
1213
|
-
if (primaryComparison) {
|
|
1214
|
-
return player;
|
|
1215
|
-
} else if (player[primaryProperty] === result[primaryProperty] && secondaryProperty && secondaryComparison) {
|
|
1216
|
-
return player;
|
|
1061
|
+
for (const languageTag of Object.keys(LanguageTags)) {
|
|
1062
|
+
if (!this._nicknameCache.has(languageTag)) {
|
|
1063
|
+
this._nicknameCache.set(languageTag, this._buildNicknameMap(languageTag));
|
|
1217
1064
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
}
|
|
1222
|
-
__name(findMaxByProperty, "findMaxByProperty");
|
|
1223
|
-
findMaxByProperty(
|
|
1224
|
-
"mvpScore",
|
|
1225
|
-
void 0,
|
|
1226
|
-
match.players.filter((player) => match.didRadiantWin == player.isRadiant)
|
|
1227
|
-
)?.titles.push("dota2tracker.template.titles.MVP");
|
|
1228
|
-
findMaxByProperty(
|
|
1229
|
-
"mvpScore",
|
|
1230
|
-
void 0,
|
|
1231
|
-
match.players.filter((player) => match.didRadiantWin != player.isRadiant)
|
|
1232
|
-
)?.titles.push("dota2tracker.template.titles.Soul");
|
|
1233
|
-
findMaxByProperty("networth")?.titles.push("dota2tracker.template.titles.Rich");
|
|
1234
|
-
findMaxByProperty("experiencePerMinute")?.titles.push("dota2tracker.template.titles.Wise");
|
|
1235
|
-
if (match.parsedDateTime && match.players.every((player) => player?.stats?.heroDamageReport?.dealtTotal)) {
|
|
1236
|
-
match.players.reduce(
|
|
1237
|
-
(max, player) => player.stats.heroDamageReport.dealtTotal.stunDuration + player.stats.heroDamageReport.dealtTotal.disableDuration / 2 + player.stats.heroDamageReport.dealtTotal.slowDuration / 4 > max.stats.heroDamageReport.dealtTotal.stunDuration + max.stats.heroDamageReport.dealtTotal.disableDuration / 2 + max.stats.heroDamageReport.dealtTotal.slowDuration / 4 ? player : max
|
|
1238
|
-
).titles.push("dota2tracker.template.titles.Controller");
|
|
1239
|
-
match.players.reduce(
|
|
1240
|
-
(max, player) => player.stats.heroDamageReport.receivedTotal.physicalDamage + player.stats.heroDamageReport.receivedTotal.magicalDamage + player.stats.heroDamageReport.receivedTotal.pureDamage > max.stats.heroDamageReport.receivedTotal.physicalDamage + max.stats.heroDamageReport.receivedTotal.magicalDamage + max.stats.heroDamageReport.receivedTotal.pureDamage ? player : max
|
|
1241
|
-
).titles.push("dota2tracker.template.titles.Tank");
|
|
1242
|
-
}
|
|
1243
|
-
findMaxByProperty("heroDamage")?.titles.push("dota2tracker.template.titles.Nuker");
|
|
1244
|
-
findMaxByProperty("kills", "heroDamage")?.titles.push("dota2tracker.template.titles.Breaker");
|
|
1245
|
-
findMaxByProperty("deaths", "networth", void 0, void 0, "min" /* Min */)?.titles.push("dota2tracker.template.titles.Ghost");
|
|
1246
|
-
findMaxByProperty("assists", "heroDamage")?.titles.push("dota2tracker.template.titles.Assister");
|
|
1247
|
-
findMaxByProperty("towerDamage", "heroDamage")?.titles.push("dota2tracker.template.titles.Demolisher");
|
|
1248
|
-
findMaxByProperty("heroHealing")?.titles.push("dota2tracker.template.titles.Healer");
|
|
1249
|
-
match.players.reduce((lowest, player) => {
|
|
1250
|
-
const currentContribution = (player.kills + player.assists) / match[player.team].killsCount;
|
|
1251
|
-
const lowestContribution = (lowest.kills + lowest.assists) / match[lowest.team].killsCount;
|
|
1252
|
-
if (currentContribution < lowestContribution) {
|
|
1253
|
-
return player;
|
|
1254
|
-
} else if (currentContribution === lowestContribution) {
|
|
1255
|
-
const currentPlayerScore = player.kills + player.assists;
|
|
1256
|
-
const lowestPlayerScore = lowest.kills + lowest.assists;
|
|
1257
|
-
if (currentPlayerScore < lowestPlayerScore) {
|
|
1258
|
-
return player;
|
|
1259
|
-
} else if (currentPlayerScore === lowestPlayerScore) {
|
|
1260
|
-
return player.heroDamage < lowest.heroDamage ? player : lowest;
|
|
1065
|
+
const nicknameMap = this._nicknameCache.get(languageTag);
|
|
1066
|
+
if (nicknameMap.has(inputStr)) {
|
|
1067
|
+
return nicknameMap.get(inputStr);
|
|
1261
1068
|
}
|
|
1262
1069
|
}
|
|
1263
|
-
return
|
|
1264
|
-
}).titles.push("dota2tracker.template.titles.Idle");
|
|
1265
|
-
return match;
|
|
1266
|
-
}
|
|
1267
|
-
__name(getFormattedMatchData, "getFormattedMatchData");
|
|
1268
|
-
function getFormattedPlayerData(param) {
|
|
1269
|
-
const { playerQuery, playerExtraQuery, genHero, estimateRank } = param;
|
|
1270
|
-
const player = playerQuery.player;
|
|
1271
|
-
const playerExtra = playerExtraQuery?.player;
|
|
1272
|
-
if (player.steamAccount.isAnonymous) {
|
|
1273
|
-
for (let index = 0; index < 25; index++) {
|
|
1274
|
-
const random2 = new import_koishi.Random(() => enhancedSimpleHashToSeed(`${player.steamAccount.id}-${index}`));
|
|
1275
|
-
player.matches.push({
|
|
1276
|
-
id: 1e9 + index,
|
|
1277
|
-
gameMode: "UNKNOWN",
|
|
1278
|
-
lobbyType: "UNRANKED",
|
|
1279
|
-
didRadiantWin: random2.bool(0.5),
|
|
1280
|
-
rank: random2.int(0, 8) * 10,
|
|
1281
|
-
radiantKills: [random2.int(0, 30)],
|
|
1282
|
-
direKills: [random2.int(0, 30)],
|
|
1283
|
-
parsedDateTime: 1,
|
|
1284
|
-
players: [
|
|
1285
|
-
{
|
|
1286
|
-
steamAccount: { id: player.steamAccount.id },
|
|
1287
|
-
isRadiant: true,
|
|
1288
|
-
kills: random2.int(0, 20),
|
|
1289
|
-
deaths: random2.int(0, 20),
|
|
1290
|
-
assists: random2.int(0, 20),
|
|
1291
|
-
hero: { id: random2.pick(Object.keys(dotaconstants.heroes)), shortName: dotaconstants.heroes[random2.pick(Object.keys(dotaconstants.heroes))].name.match(/^npc_dota_hero_(.+)$/)[1] }
|
|
1292
|
-
}
|
|
1293
|
-
]
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1070
|
+
return void 0;
|
|
1296
1071
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1072
|
+
async generateUsage() {
|
|
1073
|
+
let usage2;
|
|
1074
|
+
const GlobalLanguageTag = await this.getLanguageTag();
|
|
1075
|
+
usage2 = this.$t(GlobalLanguageTag, "dota2tracker.usage");
|
|
1076
|
+
!this.ctx.cron && (usage2 += "\n" + this.$t(GlobalLanguageTag, "dota2tracker.usage_cron"));
|
|
1077
|
+
return usage2;
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// src/app/core/hero.service.ts
|
|
1082
|
+
var import_koishi2 = require("koishi");
|
|
1083
|
+
var dotaconstants2 = __toESM(require("dotaconstants"));
|
|
1084
|
+
var import_luxon2 = require("luxon");
|
|
1085
|
+
|
|
1086
|
+
// src/app/common/constants.ts
|
|
1087
|
+
var DAYS_30 = 2592e6;
|
|
1088
|
+
var RANK_BRACKETS = ["UNCALIBRATED", "HERALD", "GUARDIAN", "CRUSADER", "ARCHON", "LEGEND", "ANCIENT", "DIVINE", "IMMORTAL"];
|
|
1089
|
+
|
|
1090
|
+
// src/app/core/hero.service.ts
|
|
1091
|
+
var HeroService = class _HeroService extends import_koishi2.Service {
|
|
1092
|
+
static {
|
|
1093
|
+
__name(this, "HeroService");
|
|
1094
|
+
}
|
|
1095
|
+
constructor(ctx) {
|
|
1096
|
+
super(ctx, "dota2tracker.hero", true);
|
|
1097
|
+
}
|
|
1098
|
+
async getWeeklyHeroMeta(rank) {
|
|
1099
|
+
const MINIMUM_PICK_RATE = 0.02;
|
|
1100
|
+
const RECOMMENDATION_COUNT = 3;
|
|
1101
|
+
const tier = clamp(Math.floor(rank / 10), 2, 7);
|
|
1102
|
+
const targetTiers = [tier - 1, tier, tier + 1];
|
|
1103
|
+
const rankBrackets = [];
|
|
1104
|
+
for (let i = tier - 1; i <= tier + 3; i++) rankBrackets.push(RANK_BRACKETS[i]);
|
|
1105
|
+
const weeklyHeroMeta = await this._getMetaData(rankBrackets);
|
|
1106
|
+
const recommendation = {};
|
|
1107
|
+
for (let postr in weeklyHeroMeta) {
|
|
1108
|
+
const pos = weeklyHeroMeta[postr];
|
|
1109
|
+
recommendation[postr] = pos.filter((hero) => hero.pickRate > MINIMUM_PICK_RATE).sort((a, b) => b.winRate - a.winRate).slice(0, RECOMMENDATION_COUNT);
|
|
1304
1110
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1111
|
+
return { recommendation, targetTiers };
|
|
1112
|
+
}
|
|
1113
|
+
async _getMetaData(rankBrackets) {
|
|
1114
|
+
const dateStr = import_luxon2.DateTime.utc().toFormat("yyyy-MM-dd");
|
|
1115
|
+
const cacheKey = `${dateStr}_${rankBrackets.join("-")}`;
|
|
1116
|
+
const metaCache = await this.ctx.dota2tracker.cache.getWeeklyMetaCache(cacheKey);
|
|
1117
|
+
if (metaCache) return metaCache;
|
|
1118
|
+
const result = await this.ctx.dota2tracker.stratzAPI.queryGetWeeklyMetaByPosition({ bracketIds: rankBrackets });
|
|
1119
|
+
const weeklyHeroMeta = result.heroStats;
|
|
1120
|
+
for (const postr in weeklyHeroMeta) {
|
|
1121
|
+
const pos = weeklyHeroMeta[postr];
|
|
1122
|
+
const totalCount = pos.reduce((acc, cur) => acc + cur.matchCount, 0) / 2;
|
|
1123
|
+
pos.forEach((hero) => {
|
|
1124
|
+
hero.pickRate = totalCount > 0 ? hero.matchCount / totalCount : 0;
|
|
1125
|
+
hero.winRate = hero.matchCount > 0 ? hero.winCount / hero.matchCount : 0;
|
|
1126
|
+
});
|
|
1311
1127
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const tier2 = Math.floor(rank / 10);
|
|
1342
|
-
const stars2 = rank % 10;
|
|
1343
|
-
return tier2 >= 1 && tier2 <= 8 && stars2 >= 0 && stars2 <= 5;
|
|
1128
|
+
const now = import_luxon2.DateTime.utc();
|
|
1129
|
+
const endOfDay = now.endOf("day");
|
|
1130
|
+
const ttl = endOfDay.diff(now).toMillis();
|
|
1131
|
+
this.ctx.dota2tracker.cache.setWweeklyMetaCache(cacheKey, weeklyHeroMeta, ttl);
|
|
1132
|
+
return weeklyHeroMeta;
|
|
1133
|
+
}
|
|
1134
|
+
async getHeroDetails(input, languageTag, isRandom = false) {
|
|
1135
|
+
const heroId = this.ctx.dota2tracker.i18n.findHeroIdInLocale(isRandom ? import_koishi2.Random.pick(Object.keys(dotaconstants2.heroes)) : input);
|
|
1136
|
+
if (!heroId) return;
|
|
1137
|
+
return _HeroService.formatHeroDetails(await this.ctx.dota2tracker.valveAPI.queryHeroDetailsFromValve(heroId, languageTag));
|
|
1138
|
+
}
|
|
1139
|
+
static formatHeroDetails(rawHero) {
|
|
1140
|
+
let hero = Object.assign({}, rawHero);
|
|
1141
|
+
hero.facet_abilities.forEach((fa, i) => {
|
|
1142
|
+
if (fa.abilities.length) {
|
|
1143
|
+
fa.abilities.forEach((ab) => {
|
|
1144
|
+
if (!hero.facets[i].abilities) hero.facets[i].abilities = [];
|
|
1145
|
+
if (hero.facets[i].description_loc !== ab.desc_loc)
|
|
1146
|
+
hero.facets[i].abilities.push({
|
|
1147
|
+
id: ab.id,
|
|
1148
|
+
name: ab.name,
|
|
1149
|
+
name_loc: ab.name_loc,
|
|
1150
|
+
description_ability_loc: this.formatHeroDesc(ab.desc_loc, ab.special_values, "facet" /* Facet */)
|
|
1151
|
+
});
|
|
1152
|
+
else hero.facets[i].description_loc = this.formatHeroDesc(hero.facets[i].description_loc, ab.special_values, "facet" /* Facet */);
|
|
1153
|
+
ab.ability_is_facet = true;
|
|
1154
|
+
ab.facet = hero.facets[i];
|
|
1155
|
+
hero.abilities.push(ab);
|
|
1156
|
+
});
|
|
1344
1157
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
star: parseInt(player.steamAccount.seasonRank?.toString().split("")[1] ?? 0),
|
|
1354
|
-
leaderboard: player.steamAccount.seasonLeaderboardRank,
|
|
1355
|
-
inTop100: player.steamAccount.seasonLeaderboardRank ? player.steamAccount.seasonLeaderboardRank <= 10 ? "8c" : player.steamAccount.seasonLeaderboardRank <= 100 ? "8b" : void 0 : void 0
|
|
1356
|
-
};
|
|
1357
|
-
player.dotaPlus = Object.values(filteredDotaPlus);
|
|
1358
|
-
player.dotaPlus?.sort((a, b) => {
|
|
1359
|
-
if (b.level !== a.level) {
|
|
1360
|
-
return b.level - a.level;
|
|
1361
|
-
}
|
|
1362
|
-
return a.heroId - b.heroId;
|
|
1363
|
-
});
|
|
1364
|
-
player.heroesPerformanceTop10 = playerExtra?.heroesPerformance.slice(0, 10) ?? [];
|
|
1365
|
-
if (genHero) {
|
|
1366
|
-
const { matchCount, winCount, imp } = player.heroesPerformanceTop10[0];
|
|
1367
|
-
player.matchCount = matchCount;
|
|
1368
|
-
player.winCount = winCount;
|
|
1369
|
-
player.performance.imp = imp;
|
|
1370
|
-
player.dotaPlus = player.dotaPlus.filter((dpHero) => dpHero.heroId == genHero.heroId);
|
|
1371
|
-
player.genHero = genHero;
|
|
1372
|
-
}
|
|
1373
|
-
return player;
|
|
1374
|
-
}
|
|
1375
|
-
__name(getFormattedPlayerData, "getFormattedPlayerData");
|
|
1376
|
-
function getFormattedHeroData(rawHero) {
|
|
1377
|
-
let hero = Object.assign({}, rawHero);
|
|
1378
|
-
hero.facet_abilities.forEach((fa, i) => {
|
|
1379
|
-
if (fa.abilities.length) {
|
|
1380
|
-
fa.abilities.forEach((ab) => {
|
|
1381
|
-
if (!hero.facets[i].abilities) hero.facets[i].abilities = [];
|
|
1382
|
-
if (hero.facets[i].description_loc !== ab.desc_loc)
|
|
1158
|
+
});
|
|
1159
|
+
const all_special_values = [...hero.abilities.flatMap((ab) => ab.special_values), ...hero.facet_abilities.flatMap((fas) => fas.abilities.flatMap((fa) => fa.special_values))];
|
|
1160
|
+
hero.abilities.forEach((ab) => {
|
|
1161
|
+
ab.facets_loc.forEach((facet, i) => {
|
|
1162
|
+
i = i + (hero.facets.length - ab.facets_loc.length);
|
|
1163
|
+
if (i < 0) return;
|
|
1164
|
+
if (facet) {
|
|
1165
|
+
if (!hero.facets[i].abilities) hero.facets[i].abilities = [];
|
|
1383
1166
|
hero.facets[i].abilities.push({
|
|
1384
1167
|
id: ab.id,
|
|
1385
1168
|
name: ab.name,
|
|
1386
1169
|
name_loc: ab.name_loc,
|
|
1387
|
-
description_ability_loc: formatHeroDesc(
|
|
1170
|
+
description_ability_loc: this.formatHeroDesc(facet, ab.special_values, "facet" /* Facet */),
|
|
1171
|
+
attributes: []
|
|
1388
1172
|
});
|
|
1389
|
-
|
|
1390
|
-
ab.ability_is_facet = true;
|
|
1391
|
-
ab.facet = hero.facets[i];
|
|
1392
|
-
hero.abilities.push(ab);
|
|
1173
|
+
}
|
|
1393
1174
|
});
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
name: ab.name,
|
|
1406
|
-
name_loc: ab.name_loc,
|
|
1407
|
-
description_ability_loc: formatHeroDesc(facet, ab.special_values, "facet" /* Facet */),
|
|
1408
|
-
attributes: []
|
|
1175
|
+
hero.facets.forEach((facet) => {
|
|
1176
|
+
const svs = ab.special_values.filter((sv) => sv.facet_bonus.name === facet.name);
|
|
1177
|
+
svs.forEach((sv) => {
|
|
1178
|
+
if (sv.heading_loc) {
|
|
1179
|
+
if (!facet.abilities) facet.abilities = [];
|
|
1180
|
+
facet.abilities.find((ability) => ab.id == ability.id)?.attributes.push({
|
|
1181
|
+
heading_loc: sv.heading_loc,
|
|
1182
|
+
values: [...sv.facet_bonus.values],
|
|
1183
|
+
is_percentage: sv.is_percentage
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1409
1186
|
});
|
|
1410
|
-
|
|
1187
|
+
facet.description_loc = this.formatHeroDesc(facet.description_loc, svs, "facet" /* Facet */);
|
|
1188
|
+
});
|
|
1189
|
+
ab.desc_loc = this.formatHeroDesc(ab.desc_loc, ab.special_values, ab.ability_is_facet ? "facet" /* Facet */ : void 0);
|
|
1190
|
+
ab.notes_loc = ab.notes_loc.map((note) => this.formatHeroDesc(note, ab.special_values));
|
|
1191
|
+
if (ab.ability_has_scepter) ab.scepter_loc = this.formatHeroDesc(ab.scepter_loc, ab.special_values, "scepter" /* Scepter */);
|
|
1192
|
+
if (ab.ability_has_shard) ab.shard_loc = this.formatHeroDesc(ab.shard_loc, ab.special_values, "shard" /* Shard */);
|
|
1411
1193
|
});
|
|
1412
|
-
hero.
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1194
|
+
hero.talents.forEach((talent) => {
|
|
1195
|
+
const regex = /\{s:(.*?)\}/g;
|
|
1196
|
+
let match;
|
|
1197
|
+
while ((match = regex.exec(talent.name_loc)) !== null) {
|
|
1198
|
+
const specialValueName = match[1];
|
|
1199
|
+
const target = talent.special_values?.find((sv) => sv.name === specialValueName);
|
|
1200
|
+
if (target) {
|
|
1201
|
+
talent.name_loc = talent.name_loc.replace(match[0], target.values_float.join("/"));
|
|
1202
|
+
} else {
|
|
1203
|
+
const abilities = hero.abilities.filter((ability) => ability.special_values.some((specialValue) => specialValue.bonuses.some((bonus) => bonus.name === talent.name)));
|
|
1204
|
+
for (const ability of abilities) {
|
|
1205
|
+
const specialValues = ability.special_values.filter((specialValue) => specialValue.bonuses.some((bonus) => bonus.name === talent.name));
|
|
1206
|
+
const regex2 = /{s:bonus_(.*?)}/g;
|
|
1207
|
+
let match2;
|
|
1208
|
+
const replacements = [];
|
|
1209
|
+
while ((match2 = regex2.exec(talent.name_loc)) !== null) {
|
|
1210
|
+
const specialValue = specialValues.find((sv) => sv.name === String(match2[1]));
|
|
1211
|
+
const replacement = specialValue?.bonuses.find((bonus) => bonus.name === talent.name)?.value;
|
|
1212
|
+
if (replacement !== void 0) {
|
|
1213
|
+
replacements.push({
|
|
1214
|
+
original: match2[0],
|
|
1215
|
+
replacement
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
replacements.forEach(({ original, replacement }) => {
|
|
1220
|
+
talent.name_loc = talent.name_loc.replace(original, replacement);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1422
1223
|
}
|
|
1423
|
-
}
|
|
1424
|
-
facet.description_loc = formatHeroDesc(facet.description_loc, svs, "facet" /* Facet */);
|
|
1224
|
+
}
|
|
1425
1225
|
});
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
let match;
|
|
1434
|
-
while ((match = regex.exec(talent.name_loc)) !== null) {
|
|
1435
|
-
const specialValueName = match[1];
|
|
1436
|
-
const target = talent.special_values?.find((sv) => sv.name === specialValueName);
|
|
1437
|
-
if (target) {
|
|
1438
|
-
talent.name_loc = talent.name_loc.replace(match[0], target.values_float.join("/"));
|
|
1226
|
+
return hero;
|
|
1227
|
+
}
|
|
1228
|
+
static formatHeroDesc(template, special_values, type = "normal" /* Normal */) {
|
|
1229
|
+
return template.replace(/%%|%([^%]+)%|\{([^}]+)\}/g, (match, p1, p2) => {
|
|
1230
|
+
const field = p1 || p2;
|
|
1231
|
+
if (match === "%%") {
|
|
1232
|
+
return "%";
|
|
1439
1233
|
} else {
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1234
|
+
const fieldName = field.replace(/^s:/, "").replace(/^shard_/, "").toLowerCase();
|
|
1235
|
+
const specialValue = special_values.find((sv) => {
|
|
1236
|
+
const nameLower = sv.name.toLowerCase();
|
|
1237
|
+
return nameLower === fieldName || nameLower === `bonus_${fieldName}` || nameLower === `shard_${fieldName}` || `bonus_${nameLower}` === fieldName || `shard_${nameLower}` === fieldName;
|
|
1238
|
+
});
|
|
1239
|
+
if (specialValue) {
|
|
1240
|
+
let valuesToUse = "";
|
|
1241
|
+
switch (type) {
|
|
1242
|
+
case "facet" /* Facet */:
|
|
1243
|
+
valuesToUse = specialValue.facet_bonus.name ? specialValue.facet_bonus.values.join(" / ") : specialValue.values_float.join(" / ");
|
|
1244
|
+
break;
|
|
1245
|
+
case "scepter" /* Scepter */:
|
|
1246
|
+
valuesToUse = specialValue.values_scepter.length ? specialValue.values_scepter.join(" / ") : specialValue.values_float.join(" / ");
|
|
1247
|
+
break;
|
|
1248
|
+
case "shard" /* Shard */:
|
|
1249
|
+
valuesToUse = specialValue.values_shard.length ? specialValue.values_shard.join(" / ") : specialValue.values_float.join(" / ");
|
|
1250
|
+
break;
|
|
1251
|
+
default:
|
|
1252
|
+
valuesToUse = specialValue.values_float.join(" / ");
|
|
1455
1253
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1254
|
+
return `<span class="value">${valuesToUse}</span>`;
|
|
1255
|
+
} else {
|
|
1256
|
+
return match;
|
|
1459
1257
|
}
|
|
1460
1258
|
}
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
// src/app/core/item.service.ts
|
|
1264
|
+
var import_koishi3 = require("koishi");
|
|
1265
|
+
var ItemService = class _ItemService extends import_koishi3.Service {
|
|
1266
|
+
static {
|
|
1267
|
+
__name(this, "ItemService");
|
|
1268
|
+
}
|
|
1269
|
+
constructor(ctx) {
|
|
1270
|
+
super(ctx, "dota2tracker.item", true);
|
|
1271
|
+
this.config = ctx.config;
|
|
1272
|
+
}
|
|
1273
|
+
async getItemDetails(itemId, languageTag) {
|
|
1274
|
+
return await this.ctx.dota2tracker.valveAPI.queryItemDetailsFromValve(itemId, languageTag);
|
|
1275
|
+
}
|
|
1276
|
+
async getItemList({ languageTag, onCacheMissTip }) {
|
|
1277
|
+
const currentGameVersion = await this.ctx.dota2tracker.valveAPI.queryLastPatchNumber();
|
|
1278
|
+
let itemList;
|
|
1279
|
+
const cache = await this.ctx.dota2tracker.cache.getItemListConstants(languageTag);
|
|
1280
|
+
try {
|
|
1281
|
+
if (!cache || cache.gameVersion != currentGameVersion) {
|
|
1282
|
+
await onCacheMissTip?.();
|
|
1283
|
+
itemList = _ItemService.getFormattedItemListData(await this.ctx.dota2tracker.valveAPI.queryItemListFromValve(languageTag));
|
|
1284
|
+
this.ctx.dota2tracker.cache.cacheItemListConstants(languageTag, itemList, currentGameVersion);
|
|
1285
|
+
} else {
|
|
1286
|
+
itemList = cache.itemList;
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
throw error;
|
|
1461
1290
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
__name(
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1291
|
+
return itemList;
|
|
1292
|
+
}
|
|
1293
|
+
static getFormattedItemListData(rawItems) {
|
|
1294
|
+
const processItemName = /* @__PURE__ */ __name((name2) => name2.replace(/^item_/i, "").replace(/^recipe_/i, "recipe_"), "processItemName");
|
|
1295
|
+
const [recipes, items] = rawItems.reduce(
|
|
1296
|
+
(acc, item) => {
|
|
1297
|
+
const processed = {
|
|
1298
|
+
...item,
|
|
1299
|
+
name: processItemName(item.name),
|
|
1300
|
+
name_loc: item.name_loc,
|
|
1301
|
+
name_english_loc: item.name_english_loc
|
|
1302
|
+
};
|
|
1303
|
+
item.name.includes("_recipe_") ? acc[0].push(processed) : acc[1].push(processed);
|
|
1304
|
+
return acc;
|
|
1305
|
+
},
|
|
1306
|
+
[[], []]
|
|
1307
|
+
);
|
|
1308
|
+
const itemMap = /* @__PURE__ */ new Map();
|
|
1309
|
+
items.concat(recipes).forEach(
|
|
1310
|
+
(item) => itemMap.set(item.id, {
|
|
1311
|
+
id: item.id,
|
|
1312
|
+
name: item.name,
|
|
1313
|
+
name_loc: item.name_loc
|
|
1314
|
+
})
|
|
1315
|
+
);
|
|
1316
|
+
const processedItems = items.map((baseItem) => {
|
|
1317
|
+
const recipe = recipes.find((r) => r.name === `recipe_${baseItem.name.replace("item_", "")}`);
|
|
1318
|
+
return {
|
|
1319
|
+
...baseItem,
|
|
1320
|
+
recipes: (recipe?.recipes || baseItem.recipes).map((recipe2) => ({
|
|
1321
|
+
...recipe2,
|
|
1322
|
+
// 转换ID数组为对象数组
|
|
1323
|
+
items: recipe2.items.map((id) => itemMap.get(id)).filter(Boolean)
|
|
1324
|
+
})),
|
|
1325
|
+
required_recipe: recipe ? !!recipe.name_loc.trim() : false,
|
|
1326
|
+
builds_into: []
|
|
1475
1327
|
};
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
name_loc: item.name_loc
|
|
1487
|
-
})
|
|
1488
|
-
);
|
|
1489
|
-
const processedItems = items.map((baseItem) => {
|
|
1490
|
-
const recipe = recipes.find((r) => r.name === `recipe_${baseItem.name.replace("item_", "")}`);
|
|
1491
|
-
return {
|
|
1492
|
-
...baseItem,
|
|
1493
|
-
recipes: (recipe?.recipes || baseItem.recipes).map((recipe2) => ({
|
|
1494
|
-
...recipe2,
|
|
1495
|
-
// 转换ID数组为对象数组
|
|
1496
|
-
items: recipe2.items.map((id) => itemMap.get(id)).filter(Boolean)
|
|
1497
|
-
})),
|
|
1498
|
-
required_recipe: recipe ? !!recipe.name_loc.trim() : false,
|
|
1499
|
-
builds_into: []
|
|
1500
|
-
};
|
|
1501
|
-
});
|
|
1502
|
-
const buildsIntoMap = /* @__PURE__ */ new Map();
|
|
1503
|
-
processedItems.forEach((item) => {
|
|
1504
|
-
item.recipes.forEach((recipe) => {
|
|
1505
|
-
recipe.items.forEach((material) => {
|
|
1506
|
-
if (!buildsIntoMap.has(material.id)) {
|
|
1507
|
-
buildsIntoMap.set(material.id, []);
|
|
1508
|
-
}
|
|
1509
|
-
buildsIntoMap.get(material.id).push(itemMap.get(item.id));
|
|
1328
|
+
});
|
|
1329
|
+
const buildsIntoMap = /* @__PURE__ */ new Map();
|
|
1330
|
+
processedItems.forEach((item) => {
|
|
1331
|
+
item.recipes.forEach((recipe) => {
|
|
1332
|
+
recipe.items.forEach((material) => {
|
|
1333
|
+
if (!buildsIntoMap.has(material.id)) {
|
|
1334
|
+
buildsIntoMap.set(material.id, []);
|
|
1335
|
+
}
|
|
1336
|
+
buildsIntoMap.get(material.id).push(itemMap.get(item.id));
|
|
1337
|
+
});
|
|
1510
1338
|
});
|
|
1511
1339
|
});
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
}))
|
|
1520
|
-
}));
|
|
1521
|
-
}
|
|
1522
|
-
__name(getFormattedItemListData, "getFormattedItemListData");
|
|
1523
|
-
function sec2time(sec) {
|
|
1524
|
-
return sec ? (sec < 0 ? "-" : "") + Math.floor(Math.abs(sec) / 60) + ":" + ("00" + Math.abs(sec) % 60).slice(-2) : "--:--";
|
|
1525
|
-
}
|
|
1526
|
-
__name(sec2time, "sec2time");
|
|
1527
|
-
function formatNumber(num) {
|
|
1528
|
-
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1529
|
-
}
|
|
1530
|
-
__name(formatNumber, "formatNumber");
|
|
1531
|
-
function readDirectoryFilesSync(directoryPath) {
|
|
1532
|
-
try {
|
|
1533
|
-
const files = import_fs.default.readdirSync(directoryPath);
|
|
1534
|
-
const fileNames = files.filter((file) => import_path.default.extname(file).toLowerCase() === ".ejs").map((file) => import_path.default.basename(file, ".ejs"));
|
|
1535
|
-
return fileNames;
|
|
1536
|
-
} catch (error) {
|
|
1537
|
-
console.error("Error reading directory:", error);
|
|
1538
|
-
return [];
|
|
1340
|
+
return processedItems.map((item) => ({
|
|
1341
|
+
...item,
|
|
1342
|
+
builds_into: (buildsIntoMap.get(item.id) || []).map((target) => ({
|
|
1343
|
+
id: target.id,
|
|
1344
|
+
name: target.name,
|
|
1345
|
+
name_loc: target.name_loc
|
|
1346
|
+
}))
|
|
1347
|
+
}));
|
|
1539
1348
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
red = 255;
|
|
1549
|
-
green = scale;
|
|
1550
|
-
blue = scale;
|
|
1551
|
-
} else {
|
|
1552
|
-
let scale = Math.round(255 * ((value - 50) / 50));
|
|
1553
|
-
red = 255 - scale;
|
|
1554
|
-
green = 255;
|
|
1555
|
-
blue = 255 - scale;
|
|
1349
|
+
searchItems(items, keyword, languageTag, config) {
|
|
1350
|
+
if (!keyword) return [];
|
|
1351
|
+
const alias = this.ctx.dota2tracker.i18n.getConstantLocale(languageTag).dota2tracker.items_alias?.[keyword] ?? config.customItemAlias.filter((cia) => cia.alias == keyword).map((cia) => cia.keyword);
|
|
1352
|
+
const exactMatch = items.filter(
|
|
1353
|
+
(item) => alias?.some((a) => item.name_loc.trim().toLowerCase() == a.toLowerCase()) || item.name_loc.trim().toLowerCase() === keyword.trim().toLowerCase() || Number.isInteger(Number(keyword)) && item.id === Number(keyword)
|
|
1354
|
+
);
|
|
1355
|
+
if (exactMatch.length) return exactMatch;
|
|
1356
|
+
return this.fuzzySearchItems(alias.length ? alias : [keyword], items);
|
|
1556
1357
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1358
|
+
fuzzySearchItems(keywords, items) {
|
|
1359
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1360
|
+
if (!keywords.length) return [];
|
|
1361
|
+
for (const item of items) {
|
|
1362
|
+
const cleanName = item.name_loc.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "").trim();
|
|
1363
|
+
let matchAllKeywords = true;
|
|
1364
|
+
for (const keyword of keywords) {
|
|
1365
|
+
const cleanKeyword = keyword.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "").trim();
|
|
1366
|
+
if (cleanKeyword.length === 0) continue;
|
|
1367
|
+
const keywordChars = Array.from(cleanKeyword);
|
|
1368
|
+
const isMatched = (
|
|
1369
|
+
// 完全连续匹配(如"水剑")
|
|
1370
|
+
cleanName.includes(cleanKeyword) || // 包含所有字符(如同时有"水"和"剑")
|
|
1371
|
+
keywordChars.every((c) => cleanName.includes(c))
|
|
1372
|
+
);
|
|
1373
|
+
if (!isMatched) {
|
|
1374
|
+
matchAllKeywords = false;
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (matchAllKeywords) {
|
|
1379
|
+
resultMap.set(item.id, item);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return Array.from(resultMap.values());
|
|
1570
1383
|
}
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// src/app/core/match.service.ts
|
|
1387
|
+
var import_koishi4 = require("koishi");
|
|
1388
|
+
var dotaconstants3 = __toESM(require("dotaconstants"));
|
|
1389
|
+
var MatchService = class _MatchService extends import_koishi4.Service {
|
|
1390
|
+
constructor(ctx, pluginVersion2) {
|
|
1391
|
+
super(ctx, "dota2tracker.match", true);
|
|
1392
|
+
this.pluginVersion = pluginVersion2;
|
|
1393
|
+
}
|
|
1394
|
+
static {
|
|
1395
|
+
__name(this, "MatchService");
|
|
1396
|
+
}
|
|
1397
|
+
async getMatchResult({ matchId, requestParse }) {
|
|
1398
|
+
const matchQuery = await this.getMatchData(matchId);
|
|
1399
|
+
if (matchQuery) {
|
|
1400
|
+
if (!this.isMatchParsed(matchQuery) && requestParse && this.ctx.cron) {
|
|
1401
|
+
return {
|
|
1402
|
+
status: "PENDING",
|
|
1403
|
+
matchId
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
status: "READY",
|
|
1408
|
+
matchData: matchQuery
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
status: "NOT_FOUND"
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
async generateMatchData(rawMatchData, languageTag) {
|
|
1416
|
+
return await this.formatMatchData(rawMatchData, languageTag);
|
|
1417
|
+
}
|
|
1418
|
+
async getMatchData(matchId) {
|
|
1419
|
+
try {
|
|
1420
|
+
let queryLocal = await this.ctx.dota2tracker.cache.getMatchCache(matchId);
|
|
1421
|
+
let matchQuery;
|
|
1422
|
+
if (queryLocal?.data && queryLocal.pluginVersion == this.pluginVersion) {
|
|
1423
|
+
matchQuery = queryLocal.data;
|
|
1424
|
+
this.ctx.dota2tracker.cache.setMatchCache(matchId, matchQuery, this.pluginVersion);
|
|
1425
|
+
} else {
|
|
1426
|
+
matchQuery = await this.ctx.dota2tracker.stratzAPI.queryMatchInfo(matchId);
|
|
1427
|
+
if (this.isMatchParsed(matchQuery)) {
|
|
1428
|
+
this.ctx.dota2tracker.cache.setMatchCache(matchId, matchQuery, this.pluginVersion);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return matchQuery;
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
this.ctx.dota2tracker.cache.deleteMatchCache(matchId);
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
async formatMatchData(matchQuery, languageTag) {
|
|
1438
|
+
try {
|
|
1439
|
+
let constantsQuery = await this.ctx.dota2tracker.cache.getFacetConstantsCache(languageTag);
|
|
1440
|
+
if (!constantsQuery || // 缓存中没有 constants
|
|
1441
|
+
!matchQuery.constants.gameVersions?.[0]?.id || // 当前版本信息无效
|
|
1442
|
+
!constantsQuery.constants.gameVersions?.[0]?.id || // 缓存版本信息无效
|
|
1443
|
+
matchQuery.constants.gameVersions[0].id !== constantsQuery.constants.gameVersions[0].id)
|
|
1444
|
+
constantsQuery = await this.ctx.dota2tracker.stratzAPI.queryConstants(languageTag);
|
|
1445
|
+
const facetData = await _MatchService.constantsInjectFacetData(constantsQuery, matchQuery, languageTag, this.ctx.dota2tracker.hero);
|
|
1446
|
+
this.ctx.dota2tracker.cache.setFacetConstantsCache(languageTag, constantsQuery);
|
|
1447
|
+
const match = _MatchService.extendMatchData(matchQuery, facetData);
|
|
1448
|
+
return match;
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
this.ctx.dota2tracker.cache.deleteFacetConstantsCache(languageTag);
|
|
1451
|
+
throw error;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
static async constantsInjectFacetData(constantsQuery, matchQuery, languageTag, heroService) {
|
|
1455
|
+
const facetData = {};
|
|
1456
|
+
for (let player of matchQuery.match.players) {
|
|
1457
|
+
if (player.variant != null) {
|
|
1458
|
+
const constantsFacet = constantsQuery.constants.facets.find((facet) => facet.id == player.hero.facets[player.variant - 1]?.facetId);
|
|
1459
|
+
let displayName = constantsFacet.language?.displayName;
|
|
1460
|
+
if (!displayName && heroService) {
|
|
1461
|
+
const valveFacet = (await heroService.getHeroDetails(player.hero.id, languageTag)).facets.find((facet) => facet.index === player.variant - 1);
|
|
1462
|
+
constantsFacet.language.displayName = valveFacet.title_loc;
|
|
1463
|
+
constantsFacet.name = valveFacet.name;
|
|
1464
|
+
}
|
|
1465
|
+
facetData[player.steamAccountId] = { id: constantsFacet.id, name: constantsFacet.name, icon: constantsFacet.icon, color: constantsFacet.color, displayName: constantsFacet.language?.displayName };
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return facetData;
|
|
1469
|
+
}
|
|
1470
|
+
// 对比赛数据进行补充以供生成模板函数使用
|
|
1471
|
+
static extendMatchData(matchQuery, facetData) {
|
|
1472
|
+
const match = matchQuery.match;
|
|
1473
|
+
["radiant", "dire"].forEach((team) => {
|
|
1474
|
+
match[team] = { killsCount: match?.[team + "Kills"]?.reduce((acc, cva) => acc + cva, 0) ?? 0, damageReceived: 0, heroDamage: 0, networth: 0, experience: 0 };
|
|
1475
|
+
});
|
|
1476
|
+
if (!match.parsedDateTime) {
|
|
1477
|
+
match.players.reduce((acc, player) => {
|
|
1478
|
+
if (player.isRadiant) {
|
|
1479
|
+
acc.radiant.killsCount += player.kills;
|
|
1480
|
+
} else {
|
|
1481
|
+
acc.dire.killsCount += player.kills;
|
|
1482
|
+
}
|
|
1483
|
+
return acc;
|
|
1484
|
+
}, match);
|
|
1485
|
+
}
|
|
1486
|
+
match.party = {};
|
|
1487
|
+
let party_index = 0;
|
|
1488
|
+
const party_mark = ["I", "II", "III", "IV"];
|
|
1489
|
+
let heroOrderList = {};
|
|
1490
|
+
for (let hero of match.pickBans ?? []) {
|
|
1491
|
+
if (hero.isPick) heroOrderList[hero.heroId] = hero.order;
|
|
1492
|
+
}
|
|
1493
|
+
let processLaneOutcome = /* @__PURE__ */ __name(function(outcome) {
|
|
1494
|
+
switch (outcome) {
|
|
1495
|
+
case "RADIANT_VICTORY":
|
|
1496
|
+
return { radiant: "advantage", dire: "disadvantage" };
|
|
1497
|
+
case "RADIANT_STOMP":
|
|
1498
|
+
return { radiant: "stomp", dire: "stomped" };
|
|
1499
|
+
case "DIRE_VICTORY":
|
|
1500
|
+
return { radiant: "disadvantage", dire: "advantage" };
|
|
1501
|
+
case "DIRE_STOMP":
|
|
1502
|
+
return { radiant: "stomped", dire: "stomp" };
|
|
1503
|
+
default:
|
|
1504
|
+
return { radiant: "tie", dire: "tie" };
|
|
1505
|
+
}
|
|
1506
|
+
}, "processLaneOutcome");
|
|
1507
|
+
const laneResult = { top: {}, mid: {}, bottom: {} };
|
|
1508
|
+
laneResult.top = processLaneOutcome(match.topLaneOutcome);
|
|
1509
|
+
laneResult.mid = processLaneOutcome(match.midLaneOutcome);
|
|
1510
|
+
laneResult.bottom = processLaneOutcome(match.bottomLaneOutcome);
|
|
1511
|
+
match.players.forEach((player) => {
|
|
1512
|
+
player.team = player.isRadiant ? "radiant" : "dire";
|
|
1513
|
+
player.rank = {
|
|
1514
|
+
medal: parseInt(player.steamAccount.seasonRank?.toString().split("")[0] ?? 0),
|
|
1515
|
+
star: parseInt(player.steamAccount.seasonRank?.toString().split("")[1] ?? 0),
|
|
1516
|
+
leaderboard: player.steamAccount.seasonLeaderboardRank,
|
|
1517
|
+
inTop100: player.steamAccount.seasonLeaderboardRank ? player.steamAccount.seasonLeaderboardRank <= 10 ? "8c" : player.steamAccount.seasonLeaderboardRank <= 100 ? "8b" : void 0 : void 0
|
|
1518
|
+
};
|
|
1519
|
+
player.killContribution = (player.kills + player.assists) / match[player.team].killsCount;
|
|
1520
|
+
player.deathContribution = player.deaths / match[player.team === "radiant" ? "dire" : "radiant"].killsCount;
|
|
1521
|
+
player.damageReceived = (player.stats?.heroDamageReport?.receivedTotal?.physicalDamage ?? 0) + (player.stats?.heroDamageReport?.receivedTotal?.magicalDamage ?? 0) + (player.stats?.heroDamageReport?.receivedTotal?.pureDamage ?? 0);
|
|
1522
|
+
match[player.team].heroDamage = (match[player.team].heroDamage ?? 0) + player.heroDamage;
|
|
1523
|
+
match[player.team].damageReceived = (match[player.team].damageReceived ?? 0) + player.damageReceived;
|
|
1524
|
+
match[player.team].networth += player.networth;
|
|
1525
|
+
match[player.team].experience += Math.floor(player.experiencePerMinute / 60 * match.durationSeconds);
|
|
1526
|
+
player.titles = [];
|
|
1527
|
+
player.mvpScore = // 计算MVP分数
|
|
1528
|
+
player.kills * 5 + player.assists * 3 + (player.stats.heroDamageReport?.dealtTotal.stunDuration ?? 0) / 100 * 0.1 + (player.stats.heroDamageReport?.dealtTotal.disableDuration ?? 0) / 100 * 0.05 + (player.stats.heroDamageReport?.dealtTotal.slowDuration ?? 0) / 100 * 0.025 + player.heroDamage * 1e-3 + player.towerDamage * 0.01 + player.heroHealing * 2e-3 + player.imp * 0.25;
|
|
1529
|
+
player.order = heroOrderList[player.hero.id];
|
|
1530
|
+
if (player.partyId != null) {
|
|
1531
|
+
if (!match.party[player.partyId]) match.party[player.partyId] = party_mark[party_index++];
|
|
1532
|
+
}
|
|
1533
|
+
if (player.stats.matchPlayerBuffEvent) {
|
|
1534
|
+
const buffMap = /* @__PURE__ */ new Map();
|
|
1535
|
+
for (const event of player.stats.matchPlayerBuffEvent) {
|
|
1536
|
+
const isAbility = event.abilityId != null;
|
|
1537
|
+
const id = isAbility ? event.abilityId : event.itemId;
|
|
1538
|
+
const type = isAbility ? "ability" : "item";
|
|
1539
|
+
const compositeKey = `${type}|${id}`;
|
|
1540
|
+
const current = buffMap.get(compositeKey);
|
|
1541
|
+
if (!current || event.stackCount > current.stackCount) {
|
|
1542
|
+
buffMap.set(compositeKey, {
|
|
1543
|
+
id,
|
|
1544
|
+
type,
|
|
1545
|
+
stackCount: event.stackCount ?? 0
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1603
1548
|
}
|
|
1604
|
-
|
|
1549
|
+
player.buffs = Array.from(buffMap.values());
|
|
1605
1550
|
} else {
|
|
1606
|
-
|
|
1551
|
+
player.buffs = [];
|
|
1607
1552
|
}
|
|
1553
|
+
switch (player.lane) {
|
|
1554
|
+
case "SAFE_LANE":
|
|
1555
|
+
player.laneResult = laneResult[player.isRadiant ? "bottom" : "top"][player.team];
|
|
1556
|
+
break;
|
|
1557
|
+
case "OFF_LANE":
|
|
1558
|
+
player.laneResult = laneResult[!player.isRadiant ? "bottom" : "top"][player.team];
|
|
1559
|
+
break;
|
|
1560
|
+
case "JUNGLE":
|
|
1561
|
+
player.laneResult = "jungle";
|
|
1562
|
+
break;
|
|
1563
|
+
default:
|
|
1564
|
+
player.laneResult = laneResult.mid[player.team];
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
const supportItemIds = [30, 40, 42, 43, 188];
|
|
1568
|
+
const supportItemsCount = supportItemIds.reduce((obj, key) => {
|
|
1569
|
+
obj[key] = 0;
|
|
1570
|
+
return obj;
|
|
1571
|
+
}, {});
|
|
1572
|
+
const purchaseTimesMap = {};
|
|
1573
|
+
if (player.stats?.itemPurchases) {
|
|
1574
|
+
for (const item of player.stats.itemPurchases) {
|
|
1575
|
+
if (!supportItemIds.includes(item.itemId)) {
|
|
1576
|
+
if (!purchaseTimesMap[item.itemId]) {
|
|
1577
|
+
purchaseTimesMap[item.itemId] = [];
|
|
1578
|
+
}
|
|
1579
|
+
purchaseTimesMap[item.itemId].push(item.time);
|
|
1580
|
+
}
|
|
1581
|
+
switch (item.itemId) {
|
|
1582
|
+
case 30:
|
|
1583
|
+
case 40:
|
|
1584
|
+
case 42:
|
|
1585
|
+
case 43:
|
|
1586
|
+
case 188:
|
|
1587
|
+
supportItemsCount[item.itemId]++;
|
|
1588
|
+
break;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
player.supportItemsCount = [];
|
|
1593
|
+
for (let itemId in supportItemsCount) {
|
|
1594
|
+
if (supportItemsCount[itemId] === 0) continue;
|
|
1595
|
+
player.supportItemsCount.push({
|
|
1596
|
+
name: dotaconstants3.item_ids[itemId],
|
|
1597
|
+
count: supportItemsCount[itemId]
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
const purchaseTimeIndices = /* @__PURE__ */ new Map();
|
|
1601
|
+
player.items = [];
|
|
1602
|
+
for (let i = 0; i <= 5; i++) {
|
|
1603
|
+
const itemId = player[`item${i}Id`];
|
|
1604
|
+
const itemObject = createItemObject(itemId, purchaseTimesMap, purchaseTimeIndices);
|
|
1605
|
+
player.items.push(itemObject);
|
|
1606
|
+
}
|
|
1607
|
+
player.backpacks = [];
|
|
1608
|
+
for (let i = 0; i <= 2; i++) {
|
|
1609
|
+
const itemId = player[`backpack${i}Id`];
|
|
1610
|
+
const itemObject = createItemObject(itemId, purchaseTimesMap, purchaseTimeIndices);
|
|
1611
|
+
player.backpacks.push(itemObject);
|
|
1612
|
+
}
|
|
1613
|
+
if (player.additionalUnit) {
|
|
1614
|
+
player.unitItems = [];
|
|
1615
|
+
player.unitBackpacks = [];
|
|
1616
|
+
for (let i = 0; i <= 5; i++) {
|
|
1617
|
+
const itemId = player.additionalUnit[`item${i}Id`];
|
|
1618
|
+
const itemObject = createItemObject(itemId, purchaseTimesMap, purchaseTimeIndices);
|
|
1619
|
+
player.unitItems.push(itemObject);
|
|
1620
|
+
}
|
|
1621
|
+
for (let i = 0; i <= 2; i++) {
|
|
1622
|
+
const itemId = player.additionalUnit[`backpack${i}Id`];
|
|
1623
|
+
const itemObject = createItemObject(itemId, purchaseTimesMap, purchaseTimeIndices);
|
|
1624
|
+
player.unitBackpacks.push(itemObject);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
player.formattedNetworth = formatNumber(player.networth);
|
|
1628
|
+
player.facet = facetData[player.steamAccountId];
|
|
1629
|
+
});
|
|
1630
|
+
let ComparisonMode;
|
|
1631
|
+
((ComparisonMode2) => {
|
|
1632
|
+
ComparisonMode2["Max"] = "max";
|
|
1633
|
+
ComparisonMode2["Min"] = "min";
|
|
1634
|
+
})(ComparisonMode || (ComparisonMode = {}));
|
|
1635
|
+
function findMaxByProperty(primaryProperty, secondaryProperty = null, players = match.players, primaryMode = "max" /* Max */, secondaryMode = "max" /* Max */) {
|
|
1636
|
+
let maxPlayer = players.reduce((result, player) => {
|
|
1637
|
+
const primaryComparison = primaryMode === "max" /* Max */ ? player[primaryProperty] > result[primaryProperty] : player[primaryProperty] < result[primaryProperty];
|
|
1638
|
+
const secondaryComparison = secondaryMode === "max" /* Max */ ? player[secondaryProperty] > result[secondaryProperty] : player[secondaryProperty] < result[secondaryProperty];
|
|
1639
|
+
if (primaryComparison) {
|
|
1640
|
+
return player;
|
|
1641
|
+
} else if (player[primaryProperty] === result[primaryProperty] && secondaryProperty && secondaryComparison) {
|
|
1642
|
+
return player;
|
|
1643
|
+
}
|
|
1644
|
+
return result;
|
|
1645
|
+
});
|
|
1646
|
+
return maxPlayer[primaryProperty] > 0 ? maxPlayer : null;
|
|
1608
1647
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1648
|
+
__name(findMaxByProperty, "findMaxByProperty");
|
|
1649
|
+
findMaxByProperty(
|
|
1650
|
+
"mvpScore",
|
|
1651
|
+
void 0,
|
|
1652
|
+
match.players.filter((player) => match.didRadiantWin == player.isRadiant)
|
|
1653
|
+
)?.titles.push("dota2tracker.template.titles.MVP");
|
|
1654
|
+
findMaxByProperty(
|
|
1655
|
+
"mvpScore",
|
|
1656
|
+
void 0,
|
|
1657
|
+
match.players.filter((player) => match.didRadiantWin != player.isRadiant)
|
|
1658
|
+
)?.titles.push("dota2tracker.template.titles.Soul");
|
|
1659
|
+
findMaxByProperty("networth")?.titles.push("dota2tracker.template.titles.Rich");
|
|
1660
|
+
findMaxByProperty("experiencePerMinute")?.titles.push("dota2tracker.template.titles.Wise");
|
|
1661
|
+
if (match.parsedDateTime && match.players.every((player) => player?.stats?.heroDamageReport?.dealtTotal)) {
|
|
1662
|
+
match.players.reduce(
|
|
1663
|
+
(max, player) => player.stats.heroDamageReport.dealtTotal.stunDuration + player.stats.heroDamageReport.dealtTotal.disableDuration / 2 + player.stats.heroDamageReport.dealtTotal.slowDuration / 4 > max.stats.heroDamageReport.dealtTotal.stunDuration + max.stats.heroDamageReport.dealtTotal.disableDuration / 2 + max.stats.heroDamageReport.dealtTotal.slowDuration / 4 ? player : max
|
|
1664
|
+
).titles.push("dota2tracker.template.titles.Controller");
|
|
1665
|
+
match.players.reduce(
|
|
1666
|
+
(max, player) => player.stats.heroDamageReport.receivedTotal.physicalDamage + player.stats.heroDamageReport.receivedTotal.magicalDamage + player.stats.heroDamageReport.receivedTotal.pureDamage > max.stats.heroDamageReport.receivedTotal.physicalDamage + max.stats.heroDamageReport.receivedTotal.magicalDamage + max.stats.heroDamageReport.receivedTotal.pureDamage ? player : max
|
|
1667
|
+
).titles.push("dota2tracker.template.titles.Tank");
|
|
1668
|
+
}
|
|
1669
|
+
findMaxByProperty("heroDamage")?.titles.push("dota2tracker.template.titles.Nuker");
|
|
1670
|
+
findMaxByProperty("kills", "heroDamage")?.titles.push("dota2tracker.template.titles.Breaker");
|
|
1671
|
+
findMaxByProperty("deaths", "networth", void 0, void 0, "min" /* Min */)?.titles.push("dota2tracker.template.titles.Ghost");
|
|
1672
|
+
findMaxByProperty("assists", "heroDamage")?.titles.push("dota2tracker.template.titles.Assister");
|
|
1673
|
+
findMaxByProperty("towerDamage", "heroDamage")?.titles.push("dota2tracker.template.titles.Demolisher");
|
|
1674
|
+
findMaxByProperty("heroHealing")?.titles.push("dota2tracker.template.titles.Healer");
|
|
1675
|
+
match.players.reduce((lowest, player) => {
|
|
1676
|
+
const currentContribution = (player.kills + player.assists) / match[player.team].killsCount;
|
|
1677
|
+
const lowestContribution = (lowest.kills + lowest.assists) / match[lowest.team].killsCount;
|
|
1678
|
+
if (currentContribution < lowestContribution) {
|
|
1679
|
+
return player;
|
|
1680
|
+
} else if (currentContribution === lowestContribution) {
|
|
1681
|
+
const currentPlayerScore = player.kills + player.assists;
|
|
1682
|
+
const lowestPlayerScore = lowest.kills + lowest.assists;
|
|
1683
|
+
if (currentPlayerScore < lowestPlayerScore) {
|
|
1684
|
+
return player;
|
|
1685
|
+
} else if (currentPlayerScore === lowestPlayerScore) {
|
|
1686
|
+
return player.heroDamage < lowest.heroDamage ? player : lowest;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return lowest;
|
|
1690
|
+
}).titles.push("dota2tracker.template.titles.Idle");
|
|
1691
|
+
match.durationTime = sec2time(match.durationSeconds);
|
|
1692
|
+
return match;
|
|
1620
1693
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1694
|
+
isMatchParsed(match) {
|
|
1695
|
+
return match.match?.parsedDateTime && match.match.players.filter((player) => player?.stats?.heroDamageReport?.dealtTotal).length > 0;
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
function createItemObject(itemId, purchaseTimesMap, purchaseTimeIndices) {
|
|
1699
|
+
if (itemId === void 0 || itemId === null) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
if (dotaconstants3.item_ids[itemId]) {
|
|
1703
|
+
const currentIndex = purchaseTimeIndices.get(itemId) || 0;
|
|
1704
|
+
const seconds = purchaseTimesMap[itemId]?.[currentIndex];
|
|
1705
|
+
purchaseTimeIndices.set(itemId, currentIndex + 1);
|
|
1706
|
+
const name2 = dotaconstants3.item_ids[itemId];
|
|
1707
|
+
const prefix = "recipe_";
|
|
1708
|
+
const isRecipe = name2.startsWith(prefix);
|
|
1709
|
+
const cleanName = isRecipe ? name2.substring(prefix.length) : name2;
|
|
1710
|
+
return {
|
|
1711
|
+
id: itemId,
|
|
1712
|
+
name: cleanName,
|
|
1713
|
+
seconds,
|
|
1714
|
+
time: sec2time(seconds),
|
|
1715
|
+
isRecipe
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
return null;
|
|
1623
1719
|
}
|
|
1624
|
-
__name(
|
|
1625
|
-
|
|
1626
|
-
// src/index.ts
|
|
1627
|
-
var import_fs2 = __toESM(require("fs"));
|
|
1628
|
-
var import_path2 = __toESM(require("path"));
|
|
1629
|
-
var import_moment = __toESM(require("moment"));
|
|
1630
|
-
var dotaconstants2 = __toESM(require("dotaconstants"));
|
|
1631
|
-
var import_koishi3 = require("koishi");
|
|
1632
|
-
var ejs = __toESM(require("ejs"));
|
|
1633
|
-
|
|
1634
|
-
// require("./locales/**/*.schema.yml") in src/index.ts
|
|
1635
|
-
var globRequire_locales_schema_yml = __glob({
|
|
1636
|
-
"./locales/en-US.schema.yml": () => require_en_US_schema(),
|
|
1637
|
-
"./locales/zh-CN.schema.yml": () => require_zh_CN_schema()
|
|
1638
|
-
});
|
|
1639
|
-
|
|
1640
|
-
// require("./locales/**/*.constants.json") in src/index.ts
|
|
1641
|
-
var globRequire_locales_constants_json = __glob({
|
|
1642
|
-
"./locales/en-US.constants.json": () => require_en_US_constants(),
|
|
1643
|
-
"./locales/zh-CN.constants.json": () => require_zh_CN_constants()
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
// require("./locales/**/*.yml") in src/index.ts
|
|
1647
|
-
var globRequire_locales_yml = __glob({
|
|
1648
|
-
"./locales/en-US.command.yml": () => require_en_US_command(),
|
|
1649
|
-
"./locales/en-US.schema.yml": () => require_en_US_schema(),
|
|
1650
|
-
"./locales/en-US.template.yml": () => require_en_US_template(),
|
|
1651
|
-
"./locales/en-US.yml": () => require_en_US(),
|
|
1652
|
-
"./locales/zh-CN.command.yml": () => require_zh_CN_command(),
|
|
1653
|
-
"./locales/zh-CN.schema.yml": () => require_zh_CN_schema(),
|
|
1654
|
-
"./locales/zh-CN.template.yml": () => require_zh_CN_template(),
|
|
1655
|
-
"./locales/zh-CN.yml": () => require_zh_CN()
|
|
1656
|
-
});
|
|
1720
|
+
__name(createItemObject, "createItemObject");
|
|
1657
1721
|
|
|
1658
|
-
//
|
|
1659
|
-
var
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
rankBroadFun: import_koishi2.Schema.boolean().default(false)
|
|
1709
|
-
}),
|
|
1710
|
-
import_koishi2.Schema.object({})
|
|
1711
|
-
]).i18n(Object.keys(GraphqlLanguageEnum).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.message, acc), {}))
|
|
1712
|
-
]),
|
|
1713
|
-
import_koishi2.Schema.intersect([
|
|
1714
|
-
import_koishi2.Schema.object({
|
|
1715
|
-
dailyReportSwitch: import_koishi2.Schema.boolean().default(false)
|
|
1716
|
-
}).i18n(Object.keys(GraphqlLanguageEnum).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})),
|
|
1717
|
-
import_koishi2.Schema.union([
|
|
1718
|
-
import_koishi2.Schema.object({
|
|
1719
|
-
dailyReportSwitch: import_koishi2.Schema.const(true).required(),
|
|
1720
|
-
dailyReportHours: import_koishi2.Schema.number().min(0).max(23).default(6),
|
|
1721
|
-
dailyReportShowCombi: import_koishi2.Schema.boolean().default(true)
|
|
1722
|
-
}),
|
|
1723
|
-
import_koishi2.Schema.object({})
|
|
1724
|
-
]).i18n(Object.keys(GraphqlLanguageEnum).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})),
|
|
1725
|
-
import_koishi2.Schema.object({
|
|
1726
|
-
weeklyReportSwitch: import_koishi2.Schema.boolean().default(false)
|
|
1727
|
-
}).i18n(Object.keys(GraphqlLanguageEnum).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})).description(void 0),
|
|
1728
|
-
import_koishi2.Schema.union([
|
|
1729
|
-
import_koishi2.Schema.object({
|
|
1730
|
-
weeklyReportSwitch: import_koishi2.Schema.const(true).required(),
|
|
1731
|
-
weeklyReportDayHours: import_koishi2.Schema.tuple([import_koishi2.Schema.number().min(1).max(7), import_koishi2.Schema.number().min(0).max(23)]).default([1, 10]),
|
|
1732
|
-
weeklyReportShowCombi: import_koishi2.Schema.boolean().default(true)
|
|
1733
|
-
}),
|
|
1734
|
-
import_koishi2.Schema.object({})
|
|
1735
|
-
]).i18n(Object.keys(GraphqlLanguageEnum).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {}))
|
|
1736
|
-
]),
|
|
1737
|
-
import_koishi2.Schema.object({
|
|
1738
|
-
template_match: import_koishi2.Schema.union([...readDirectoryFilesSync(import_path2.default.join(pluginDir2, "template", "match"))]).default("match_1"),
|
|
1739
|
-
template_player: import_koishi2.Schema.union([...readDirectoryFilesSync(import_path2.default.join(pluginDir2, "template", "player"))]).default("player_1"),
|
|
1740
|
-
template_hero: import_koishi2.Schema.union([...readDirectoryFilesSync(import_path2.default.join(pluginDir2, "template", "hero"))]).default("hero_1"),
|
|
1741
|
-
playerRankEstimate: import_koishi2.Schema.boolean().default(true),
|
|
1742
|
-
templateFonts: import_koishi2.Schema.array(String).default([]).role("table")
|
|
1743
|
-
}).i18n(Object.keys(GraphqlLanguageEnum).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.template, acc), {}))
|
|
1744
|
-
]);
|
|
1745
|
-
var pendingMatches = [];
|
|
1746
|
-
var random = new import_koishi3.Random(() => Math.random());
|
|
1747
|
-
var days_30 = 2592e6;
|
|
1748
|
-
var constantLocales = {};
|
|
1749
|
-
async function apply(ctx, config) {
|
|
1750
|
-
init({ http: ctx.http, setTimeout: ctx.setTimeout, APIKEY: config.STRATZ_API_TOKEN, proxyAddress: config.proxyAddress });
|
|
1751
|
-
for (const supportLanguageTag of Object.keys(GraphqlLanguageEnum)) {
|
|
1752
|
-
constantLocales[supportLanguageTag] = globRequire_locales_constants_json(`./locales/${supportLanguageTag}.constants.json`);
|
|
1753
|
-
ctx.i18n.define(supportLanguageTag, globRequire_locales_yml(`./locales/${supportLanguageTag}.yml`));
|
|
1754
|
-
ctx.i18n.define(supportLanguageTag, globRequire_locales_command_yml(`./locales/${supportLanguageTag}.command.yml`));
|
|
1755
|
-
ctx.i18n.define(supportLanguageTag, globRequire_locales_template_yml(`./locales/${supportLanguageTag}.template.yml`));
|
|
1756
|
-
}
|
|
1757
|
-
const getLanguageTag = /* @__PURE__ */ __name(async function(options) {
|
|
1758
|
-
const { session, channel, channelId } = options || {};
|
|
1759
|
-
const resolvedChannel = channel ?? (await ctx.database.get("channel", { id: session?.event.channel.id ?? channelId }))?.at(0);
|
|
1760
|
-
return ctx.i18n.fallback((resolvedChannel?.locales ?? []).concat(Object.values(ctx.i18n.locales).map((locale) => Object.keys(locale).at(0)))).find((locale) => Object.keys(GraphqlLanguageEnum).some((language) => locale == language));
|
|
1761
|
-
}, "getLanguageTag");
|
|
1762
|
-
const GlobalLanguageTag = await getLanguageTag();
|
|
1763
|
-
usage = $t(GlobalLanguageTag, "dota2tracker.usage");
|
|
1764
|
-
if (!ctx.cron)
|
|
1765
|
-
usage += "\n" + $t(GlobalLanguageTag, "dota2tracker.usage_cron");
|
|
1766
|
-
ctx.command("dota2tracker.subscribe").alias("订阅本群").action(async ({ session }) => {
|
|
1767
|
-
if (session.guild) {
|
|
1768
|
-
let currentGuild = (await ctx.database.get("dt_subscribed_guilds", {
|
|
1769
|
-
guildId: session.event.channel.id,
|
|
1770
|
-
platform: session.event.platform
|
|
1771
|
-
}))[0];
|
|
1772
|
-
if (currentGuild) session.send(session.text(".subscribed"));
|
|
1773
|
-
else {
|
|
1774
|
-
ctx.database.create("dt_subscribed_guilds", {
|
|
1775
|
-
guildId: session.event.channel.id,
|
|
1776
|
-
platform: session.event.platform
|
|
1777
|
-
});
|
|
1778
|
-
session.send(session.text(".subscribe_success"));
|
|
1722
|
+
// src/app/core/player.service.ts
|
|
1723
|
+
var import_koishi5 = require("koishi");
|
|
1724
|
+
var dotaconstants4 = __toESM(require("dotaconstants"));
|
|
1725
|
+
var import_luxon3 = require("luxon");
|
|
1726
|
+
var PlayerService = class _PlayerService extends import_koishi5.Service {
|
|
1727
|
+
static {
|
|
1728
|
+
__name(this, "PlayerService");
|
|
1729
|
+
}
|
|
1730
|
+
constructor(ctx) {
|
|
1731
|
+
super(ctx, "dota2tracker.player", true);
|
|
1732
|
+
this.config = ctx.config;
|
|
1733
|
+
}
|
|
1734
|
+
async getHeroRecommendation(steamId, player) {
|
|
1735
|
+
const RECENT_IMP_WEIGHT = 0.1;
|
|
1736
|
+
const LIFETIME_WIN_LOG_WEIGHT = 5;
|
|
1737
|
+
const HOT_STREAK_MULTIPLIER = 1.2;
|
|
1738
|
+
const HOT_STREAK_DAYS = 3;
|
|
1739
|
+
const RECOMMENDATION_COUNT = 3;
|
|
1740
|
+
const RECOMMENDATION_POOL_SIZE = 10;
|
|
1741
|
+
const now = import_luxon3.DateTime.now();
|
|
1742
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
1743
|
+
const threeDaysAgo = now.minus({ days: HOT_STREAK_DAYS });
|
|
1744
|
+
for (const hero of player.lifetimePerformance) {
|
|
1745
|
+
const breakdown = {
|
|
1746
|
+
heroId: hero.heroId,
|
|
1747
|
+
recentWinScore: 0,
|
|
1748
|
+
lifetimeWinScore: Math.log(hero.winCount + 1) * LIFETIME_WIN_LOG_WEIGHT,
|
|
1749
|
+
impBonus: 0,
|
|
1750
|
+
isHotStreak: false,
|
|
1751
|
+
hotStreakBonus: 0,
|
|
1752
|
+
baseScore: 0,
|
|
1753
|
+
totalScore: 0
|
|
1754
|
+
};
|
|
1755
|
+
scoreMap.set(hero.heroId, breakdown);
|
|
1756
|
+
}
|
|
1757
|
+
for (const hero of player.recentPerformance) {
|
|
1758
|
+
const lastPlayedDateTime = import_luxon3.DateTime.fromSeconds(hero.lastPlayedDateTime);
|
|
1759
|
+
const entry = scoreMap.get(hero.heroId);
|
|
1760
|
+
entry.recentWinScore = hero.winCount;
|
|
1761
|
+
entry.impBonus = (hero.imp || 0) * RECENT_IMP_WEIGHT;
|
|
1762
|
+
entry.isHotStreak = lastPlayedDateTime > threeDaysAgo;
|
|
1763
|
+
scoreMap.set(hero.heroId, entry);
|
|
1764
|
+
}
|
|
1765
|
+
for (const breakdown of scoreMap.values()) {
|
|
1766
|
+
breakdown.baseScore = breakdown.recentWinScore + breakdown.lifetimeWinScore + breakdown.impBonus;
|
|
1767
|
+
if (breakdown.isHotStreak) {
|
|
1768
|
+
breakdown.totalScore = breakdown.baseScore * HOT_STREAK_MULTIPLIER;
|
|
1769
|
+
breakdown.hotStreakBonus = breakdown.totalScore - breakdown.baseScore;
|
|
1770
|
+
} else {
|
|
1771
|
+
breakdown.totalScore = breakdown.baseScore;
|
|
1779
1772
|
}
|
|
1780
1773
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1774
|
+
const recommendationType = player.recentPerformance.length > 0 ? "PERSONALIZED" : player.lifetimePerformance.length > 0 ? "LIFETIME_ONLY" : "LIFETIME_NO_RECORD";
|
|
1775
|
+
const recommendationPool = Array.from(scoreMap.values()).sort((a, b) => b.totalScore - a.totalScore).slice(0, RECOMMENDATION_POOL_SIZE);
|
|
1776
|
+
const weights = {};
|
|
1777
|
+
recommendationPool.forEach((hero) => {
|
|
1778
|
+
weights[hero.heroId] = hero.totalScore;
|
|
1779
|
+
});
|
|
1780
|
+
let seedIndex = 0;
|
|
1781
|
+
const recommendedHeroes = [];
|
|
1782
|
+
for (let i = 0; i < RECOMMENDATION_COUNT && Object.keys(weights).length > 0; i++) {
|
|
1783
|
+
const seed = enhancedSimpleHashToSeed(`${now.toFormat("yyyyMMdd")}-${steamId}-${seedIndex++}`);
|
|
1784
|
+
const random = new import_koishi5.Random(() => seed);
|
|
1785
|
+
const heroId = random.weightedPick(weights);
|
|
1786
|
+
recommendedHeroes.push(Number(heroId));
|
|
1787
|
+
delete weights[heroId];
|
|
1788
|
+
}
|
|
1789
|
+
return { recommendedHeroes, recommendationPool, recommendationType };
|
|
1790
|
+
}
|
|
1791
|
+
async getMembersInChannel(session) {
|
|
1792
|
+
const subscribedPlayers = await this.ctx.dota2tracker.database.getSubscribedMembersInChannel(session);
|
|
1793
|
+
if (!subscribedPlayers || !subscribedPlayers.length) return [];
|
|
1794
|
+
const players = (await this.ctx.dota2tracker.stratzAPI.queryPlayersInfoWith10MatchesForGuild({ steamAccountIds: subscribedPlayers.map((player) => player.steamId) })).players;
|
|
1795
|
+
const result = [];
|
|
1796
|
+
players.forEach((player) => {
|
|
1797
|
+
const winCount = player.matches.filter((match) => match.didRadiantWin === match.players.find((innerPlayer) => innerPlayer.steamAccount.id === player.steamAccount.id).isRadiant).length;
|
|
1798
|
+
const winRate = winCount / player.matches.length;
|
|
1799
|
+
const lastMatchTime = player.matches[0]?.startDateTime;
|
|
1800
|
+
result.push({
|
|
1801
|
+
name: subscribedPlayers.find((subPlayer) => subPlayer.steamId == player.steamAccount.id).nickName || player.steamAccount.name,
|
|
1802
|
+
steamId: player.steamAccount.id,
|
|
1803
|
+
winRate,
|
|
1804
|
+
lastMatchTime
|
|
1805
|
+
});
|
|
1806
|
+
});
|
|
1807
|
+
return result;
|
|
1808
|
+
}
|
|
1809
|
+
async resolveSteamId(session, input) {
|
|
1810
|
+
if (input) {
|
|
1811
|
+
if (/^\d{1,11}$/.test(input)) {
|
|
1812
|
+
return { success: true, steamId: input };
|
|
1795
1813
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
if (session.guild) {
|
|
1800
|
-
if (!steam_id || !/^\d{1,11}$/.test(steam_id)) {
|
|
1801
|
-
session.send(session.text(".steam_id_invalid"));
|
|
1802
|
-
return;
|
|
1814
|
+
const targetPlayer = await this.ctx.dota2tracker.database.getSubscribedPlayerByNickNameOrSession(session, input);
|
|
1815
|
+
if (targetPlayer) {
|
|
1816
|
+
return { success: true, steamId: String(targetPlayer.steamId) };
|
|
1803
1817
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1818
|
+
return { success: false, reason: "INVALID_INPUT" };
|
|
1819
|
+
}
|
|
1820
|
+
const sessionPlayer = await this.ctx.dota2tracker.database.getSubscribedPlayerByNickNameOrSession(session);
|
|
1821
|
+
if (sessionPlayer) {
|
|
1822
|
+
return { success: true, steamId: String(sessionPlayer.steamId) };
|
|
1823
|
+
}
|
|
1824
|
+
return { success: false, reason: "NOT_BINDED" };
|
|
1825
|
+
}
|
|
1826
|
+
async getLastMatchId(steamId) {
|
|
1827
|
+
let lastMatchId = 0;
|
|
1828
|
+
try {
|
|
1829
|
+
const lastMatchQuery = await this.ctx.dota2tracker.stratzAPI.queryPlayersLastMatchRankInfo({
|
|
1830
|
+
steamAccountIds: [steamId]
|
|
1831
|
+
});
|
|
1832
|
+
if (lastMatchQuery.players[0].steamAccount.isAnonymous) return { matchId: 0, isAnonymous: true };
|
|
1833
|
+
lastMatchId = lastMatchQuery.players[0].matches[0]?.id;
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
this.logger.error(error);
|
|
1836
|
+
}
|
|
1837
|
+
return { matchId: lastMatchId };
|
|
1838
|
+
}
|
|
1839
|
+
async getFormattedPlayerData(steamId, heroId, languageTag) {
|
|
1840
|
+
const playerQuery = await this.ctx.dota2tracker.stratzAPI.queryPlayerInfoWith25Matches({
|
|
1841
|
+
steamAccountId: steamId,
|
|
1842
|
+
heroIds: heroId
|
|
1843
|
+
});
|
|
1844
|
+
const playerExtraQuery = !playerQuery.player.steamAccount.isAnonymous ? await this.ctx.dota2tracker.stratzAPI.queryPlayerExtraInfo({
|
|
1845
|
+
steamAccountId: steamId,
|
|
1846
|
+
matchCount: playerQuery.player.matchCount,
|
|
1847
|
+
heroIds: heroId
|
|
1848
|
+
}) : {
|
|
1849
|
+
player: {
|
|
1850
|
+
heroesPerformance: [],
|
|
1851
|
+
dotaPlus: null
|
|
1812
1852
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1853
|
+
};
|
|
1854
|
+
const player = _PlayerService.extendPlayerData({
|
|
1855
|
+
playerQuery,
|
|
1856
|
+
playerExtraQuery,
|
|
1857
|
+
genHero: heroId ? { heroId, name: this.ctx.dota2tracker.i18n.getConstantLocale(languageTag).dota2tracker.template.hero_names[heroId] } : null,
|
|
1858
|
+
estimateRank: this.config.playerRankEstimate
|
|
1859
|
+
});
|
|
1860
|
+
return player;
|
|
1861
|
+
}
|
|
1862
|
+
async validateSteamId(steamId) {
|
|
1863
|
+
try {
|
|
1864
|
+
const queryRes = await this.ctx.dota2tracker.stratzAPI.queryVerifyingPlayer(Number(steamId));
|
|
1865
|
+
if (queryRes.player?.matchCount) {
|
|
1866
|
+
return {
|
|
1867
|
+
status: "VALID",
|
|
1868
|
+
steamId,
|
|
1869
|
+
isAnonymous: queryRes.player.steamAccount.isAnonymous
|
|
1870
|
+
};
|
|
1871
|
+
} else {
|
|
1872
|
+
return { status: "NO_DOTA_PROFILE", reason: ".reason_without_match" };
|
|
1817
1873
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
return {
|
|
1876
|
+
status: "API_ERROR",
|
|
1877
|
+
reason: ".reason_fetch_failed"
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
static extendPlayerData(param) {
|
|
1882
|
+
const { playerQuery, playerExtraQuery, genHero, estimateRank } = param;
|
|
1883
|
+
const player = playerQuery.player;
|
|
1884
|
+
const playerExtra = playerExtraQuery?.player;
|
|
1885
|
+
if (player.steamAccount.isAnonymous) {
|
|
1886
|
+
for (let index = 0; index < 25; index++) {
|
|
1887
|
+
const random = new import_koishi5.Random(() => enhancedSimpleHashToSeed(`${player.steamAccount.id}-${index}`));
|
|
1888
|
+
const heroId = random.pick(Object.keys(dotaconstants4.heroes));
|
|
1889
|
+
player.matches.push({
|
|
1890
|
+
id: 1e9 + index,
|
|
1891
|
+
gameMode: "UNKNOWN",
|
|
1892
|
+
lobbyType: "UNRANKED",
|
|
1893
|
+
didRadiantWin: random.bool(0.5),
|
|
1894
|
+
rank: random.int(0, 8) * 10,
|
|
1895
|
+
radiantKills: [random.int(0, 30)],
|
|
1896
|
+
direKills: [random.int(0, 30)],
|
|
1897
|
+
parsedDateTime: 1,
|
|
1898
|
+
players: [
|
|
1899
|
+
{
|
|
1900
|
+
steamAccount: { id: player.steamAccount.id },
|
|
1901
|
+
isRadiant: true,
|
|
1902
|
+
kills: random.int(0, 20),
|
|
1903
|
+
deaths: random.int(0, 20),
|
|
1904
|
+
assists: random.int(0, 20),
|
|
1905
|
+
hero: { id: heroId, shortName: dotaconstants4.heroes[heroId].name.match(/^npc_dota_hero_(.+)$/)[1] }
|
|
1906
|
+
}
|
|
1907
|
+
]
|
|
1908
|
+
});
|
|
1821
1909
|
}
|
|
1822
|
-
session.send(session.text(".bind_success", { userId: session.event.user.id, nickName: nick_name || "", steamId: steam_id }) + (verifyRes.isAnonymous ? "\n" + session.text(".is_anonymous") : ""));
|
|
1823
|
-
ctx.database.create("dt_subscribed_players", {
|
|
1824
|
-
userId: session.event.user.id,
|
|
1825
|
-
guildId: session.event.channel.id,
|
|
1826
|
-
platform: session.event.platform,
|
|
1827
|
-
steamId: parseInt(steam_id),
|
|
1828
|
-
nickName: nick_name || ""
|
|
1829
|
-
});
|
|
1830
1910
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1911
|
+
let filteredDotaPlus = {};
|
|
1912
|
+
playerExtra?.dotaPlus?.forEach((item) => {
|
|
1913
|
+
if (!filteredDotaPlus[item.heroId] || filteredDotaPlus[item.heroId].level < item.level) {
|
|
1914
|
+
filteredDotaPlus[item.heroId] = {
|
|
1915
|
+
heroId: item.heroId,
|
|
1916
|
+
level: item.level
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
playerExtra?.heroesPerformance?.forEach((hero) => {
|
|
1921
|
+
if (filteredDotaPlus[hero.hero.id]) {
|
|
1922
|
+
filteredDotaPlus[hero.hero.id].shortName = hero.hero.shortName;
|
|
1923
|
+
filteredDotaPlus[hero.hero.id].winCount = hero.winCount;
|
|
1924
|
+
filteredDotaPlus[hero.hero.id].matchCount = hero.matchCount;
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
if (!player.steamAccount.seasonRank && estimateRank && !player.steamAccount.isAnonymous) {
|
|
1928
|
+
player.isEstimatedRank = true;
|
|
1929
|
+
player.steamAccount.seasonRank = this.estimateWeightedRank(player);
|
|
1930
|
+
}
|
|
1931
|
+
player.rank = {
|
|
1932
|
+
medal: parseInt(player.steamAccount.seasonRank?.toString().split("")[0] ?? 0),
|
|
1933
|
+
star: parseInt(player.steamAccount.seasonRank?.toString().split("")[1] ?? 0),
|
|
1934
|
+
leaderboard: player.steamAccount.seasonLeaderboardRank,
|
|
1935
|
+
inTop100: player.steamAccount.seasonLeaderboardRank ? player.steamAccount.seasonLeaderboardRank <= 10 ? "8c" : player.steamAccount.seasonLeaderboardRank <= 100 ? "8b" : void 0 : void 0
|
|
1936
|
+
};
|
|
1937
|
+
player.dotaPlus = Object.values(filteredDotaPlus);
|
|
1938
|
+
player.dotaPlus?.sort((a, b) => {
|
|
1939
|
+
if (b.level !== a.level) {
|
|
1940
|
+
return b.level - a.level;
|
|
1941
|
+
}
|
|
1942
|
+
return a.heroId - b.heroId;
|
|
1943
|
+
});
|
|
1944
|
+
player.heroesPerformanceTop10 = playerExtra?.heroesPerformance.slice(0, 10) ?? [];
|
|
1945
|
+
if (genHero) {
|
|
1946
|
+
const { matchCount, winCount, imp } = player.heroesPerformanceTop10[0];
|
|
1947
|
+
player.matchCount = matchCount;
|
|
1948
|
+
player.winCount = winCount;
|
|
1949
|
+
player.performance.imp = imp;
|
|
1950
|
+
player.dotaPlus = player.dotaPlus.filter((dpHero) => dpHero.heroId == genHero.heroId);
|
|
1951
|
+
player.genHero = genHero;
|
|
1952
|
+
}
|
|
1953
|
+
player.matches.forEach((match) => {
|
|
1954
|
+
match.durationTime = sec2time(match.durationSeconds);
|
|
1955
|
+
});
|
|
1956
|
+
return player;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* 根据最近的比赛记录,估算玩家的隐藏段位。
|
|
1960
|
+
* @param matches 玩家的比赛记录数组
|
|
1961
|
+
* @returns 估算出的段位数值,或在无法估算时返回 "Unknown"
|
|
1962
|
+
*/
|
|
1963
|
+
static estimateWeightedRank(player) {
|
|
1964
|
+
if (player.steamAccount?.seasonRank) {
|
|
1965
|
+
return player.steamAccount.seasonRank;
|
|
1966
|
+
}
|
|
1967
|
+
const matches = player.matches;
|
|
1968
|
+
if (!matches || matches.length === 0) {
|
|
1969
|
+
return 0;
|
|
1970
|
+
}
|
|
1971
|
+
const validRanks = matches.map((match) => match.rank).filter(validateRank);
|
|
1972
|
+
if (validRanks.length === 0) {
|
|
1973
|
+
return 0;
|
|
1974
|
+
}
|
|
1975
|
+
let weightedSum = 0;
|
|
1976
|
+
let totalWeight = 0;
|
|
1977
|
+
for (let i = 0; i < validRanks.length; i++) {
|
|
1978
|
+
const rank = validRanks[i];
|
|
1979
|
+
const value = rankToValue(rank);
|
|
1980
|
+
const weight = calculateWeight(i, validRanks.length);
|
|
1981
|
+
weightedSum += value * weight;
|
|
1982
|
+
totalWeight += weight;
|
|
1983
|
+
}
|
|
1984
|
+
if (totalWeight === 0) {
|
|
1985
|
+
return 0;
|
|
1986
|
+
}
|
|
1987
|
+
const weightedAverage = weightedSum / totalWeight;
|
|
1988
|
+
const tier = Math.floor(weightedAverage / 6);
|
|
1989
|
+
const stars = Math.round(weightedAverage % 6);
|
|
1990
|
+
const finalTier = Math.max(1, Math.min(tier, 8));
|
|
1991
|
+
const finalStars = Math.max(0, Math.min(stars, 5));
|
|
1992
|
+
return finalTier * 10 + finalStars;
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
function rankToValue(rank) {
|
|
1996
|
+
const tier = Math.floor(rank / 10);
|
|
1997
|
+
const stars = rank % 10;
|
|
1998
|
+
return tier * 6 + stars;
|
|
1999
|
+
}
|
|
2000
|
+
__name(rankToValue, "rankToValue");
|
|
2001
|
+
function calculateWeight(index, total) {
|
|
2002
|
+
if (total <= 1) {
|
|
2003
|
+
return 1;
|
|
2004
|
+
}
|
|
2005
|
+
return 1 - 0.5 / (total - 1) * index;
|
|
2006
|
+
}
|
|
2007
|
+
__name(calculateWeight, "calculateWeight");
|
|
2008
|
+
function validateRank(rank) {
|
|
2009
|
+
if (!rank && rank !== 0) return false;
|
|
2010
|
+
if (rank === 80) return true;
|
|
2011
|
+
const tier = Math.floor(rank / 10);
|
|
2012
|
+
const stars = rank % 10;
|
|
2013
|
+
return tier >= 1 && tier <= 8 && stars >= 0 && stars <= 5;
|
|
2014
|
+
}
|
|
2015
|
+
__name(validateRank, "validateRank");
|
|
2016
|
+
|
|
2017
|
+
// src/app/data/cache.ts
|
|
2018
|
+
var import_koishi6 = require("koishi");
|
|
2019
|
+
var CacheService = class extends import_koishi6.Service {
|
|
2020
|
+
static {
|
|
2021
|
+
__name(this, "CacheService");
|
|
2022
|
+
}
|
|
2023
|
+
constructor(ctx) {
|
|
2024
|
+
super(ctx, "dota2tracker.cache", true);
|
|
2025
|
+
}
|
|
2026
|
+
setWweeklyMetaCache(key, value, time) {
|
|
2027
|
+
this.ctx.cache.set("dt_weekly_metadata", key, value, time);
|
|
2028
|
+
}
|
|
2029
|
+
async getWeeklyMetaCache(key) {
|
|
2030
|
+
return this.ctx.cache.get("dt_weekly_metadata", key);
|
|
2031
|
+
}
|
|
2032
|
+
cacheItemListConstants(languageTag, itemList, gameVersion) {
|
|
2033
|
+
this.ctx.cache.set("dt_itemlist_constants", languageTag, {
|
|
2034
|
+
gameVersion,
|
|
2035
|
+
itemList
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
async getItemListConstants(languageTag) {
|
|
2039
|
+
return this.ctx.cache.get("dt_itemlist_constants", languageTag);
|
|
2040
|
+
}
|
|
2041
|
+
async getMatchCache(matchId) {
|
|
2042
|
+
return this.ctx.cache.get("dt_previous_query_results", String(matchId));
|
|
2043
|
+
}
|
|
2044
|
+
setMatchCache(matchId, matchQuery, pluginVersion2) {
|
|
2045
|
+
this.ctx.cache.set("dt_previous_query_results", String(matchQuery.match.id), { data: matchQuery, pluginVersion: pluginVersion2 }, DAYS_30);
|
|
2046
|
+
}
|
|
2047
|
+
markMatchAsSended(matchId) {
|
|
2048
|
+
this.ctx.cache.set("dt_sended_match_id", String(matchId), void 0, DAYS_30);
|
|
2049
|
+
}
|
|
2050
|
+
deleteMatchCache(matchId) {
|
|
2051
|
+
this.ctx.cache.delete("dt_previous_query_results", String(matchId));
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* 获取所有近期已发送过的比赛ID。
|
|
2055
|
+
* @returns 返回一个包含所有已发送ID的 Set,便于高效查询。
|
|
2056
|
+
*/
|
|
2057
|
+
async getSendedMatchIds() {
|
|
2058
|
+
const sendedIds = /* @__PURE__ */ new Set();
|
|
2059
|
+
for await (const key of this.ctx.cache.keys("dt_sended_match_id")) {
|
|
2060
|
+
sendedIds.add(Number(key));
|
|
2061
|
+
}
|
|
2062
|
+
return sendedIds;
|
|
2063
|
+
}
|
|
2064
|
+
async getFacetConstantsCache(languageTag) {
|
|
2065
|
+
return this.ctx.cache.get("dt_facets_constants", languageTag);
|
|
2066
|
+
}
|
|
2067
|
+
setFacetConstantsCache(languageTag, constants) {
|
|
2068
|
+
this.ctx.cache.set("dt_facets_constants", languageTag, constants, DAYS_30);
|
|
2069
|
+
}
|
|
2070
|
+
deleteFacetConstantsCache(languageTag) {
|
|
2071
|
+
this.ctx.cache.delete("dt_facets_constants", languageTag);
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
|
|
2075
|
+
// src/app/data/database.ts
|
|
2076
|
+
var import_koishi7 = require("koishi");
|
|
2077
|
+
var DatabaseService = class extends import_koishi7.Service {
|
|
2078
|
+
static {
|
|
2079
|
+
__name(this, "DatabaseService");
|
|
2080
|
+
}
|
|
2081
|
+
constructor(ctx) {
|
|
2082
|
+
super(ctx, "dota2tracker.database", true);
|
|
2083
|
+
ctx.model.extend("dt_subscribed_guilds", { id: "unsigned", guildId: "string", platform: "string" }, { autoInc: true });
|
|
2084
|
+
ctx.model.extend(
|
|
2085
|
+
"dt_subscribed_players",
|
|
2086
|
+
{
|
|
2087
|
+
id: "unsigned",
|
|
2088
|
+
userId: "string",
|
|
2089
|
+
guildId: "string",
|
|
2090
|
+
platform: "string",
|
|
2091
|
+
steamId: "integer",
|
|
2092
|
+
nickName: "string",
|
|
2093
|
+
rank: "json"
|
|
2094
|
+
},
|
|
2095
|
+
{ autoInc: true }
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
async setPlayerRank(playerId, rank) {
|
|
2099
|
+
return this.ctx.database.set("dt_subscribed_players", playerId, { rank });
|
|
2100
|
+
}
|
|
2101
|
+
async getActiveSubscribedPlayers() {
|
|
2102
|
+
const subscribedGuilds = await this.ctx.database.get("dt_subscribed_guilds", void 0);
|
|
2103
|
+
const subscribedPlayersInGuild = (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedGuilds.some((guild) => guild.guildId == player.guildId));
|
|
2104
|
+
return subscribedPlayersInGuild;
|
|
2105
|
+
}
|
|
2106
|
+
async isUserBinded(session) {
|
|
2107
|
+
const subscribedPlayer = await this.ctx.database.get("dt_subscribed_players", this.getUserQuery(session));
|
|
2108
|
+
return subscribedPlayer.length > 0;
|
|
2109
|
+
}
|
|
2110
|
+
async getBindedUser(session) {
|
|
2111
|
+
const subscribedPlayer = await this.ctx.database.get("dt_subscribed_players", this.getUserQuery(session));
|
|
2112
|
+
return subscribedPlayer.length > 0 ? subscribedPlayer[0] : null;
|
|
2113
|
+
}
|
|
2114
|
+
async bindUser(session, steamId, nickName) {
|
|
2115
|
+
return this.ctx.database.create("dt_subscribed_players", {
|
|
2116
|
+
...this.getUserQuery(session),
|
|
2117
|
+
steamId: Number(steamId),
|
|
2118
|
+
nickName: nickName || ""
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
async unbindUser(session) {
|
|
2122
|
+
return this.ctx.database.remove("dt_subscribed_players", this.getUserQuery(session));
|
|
2123
|
+
}
|
|
2124
|
+
async isChannelSubscribed(session) {
|
|
2125
|
+
const subscribedChannels = await this.ctx.database.get("dt_subscribed_guilds", this.getChannelQuery(session));
|
|
2126
|
+
return subscribedChannels.length > 0;
|
|
2127
|
+
}
|
|
2128
|
+
async subscribeChannel(session) {
|
|
2129
|
+
return this.ctx.database.create("dt_subscribed_guilds", this.getChannelQuery(session));
|
|
2130
|
+
}
|
|
2131
|
+
async unSubscribeChannel(session) {
|
|
2132
|
+
return this.ctx.database.remove("dt_subscribed_guilds", this.getChannelQuery(session));
|
|
2133
|
+
}
|
|
2134
|
+
async getSubscribedMembersInChannel(session) {
|
|
2135
|
+
return this.ctx.database.get("dt_subscribed_players", this.getChannelQuery(session));
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* [辅助方法] 根据 session 生成频道查询条件
|
|
2139
|
+
*/
|
|
2140
|
+
getChannelQuery(session) {
|
|
2141
|
+
return {
|
|
2142
|
+
guildId: session.event.channel.id,
|
|
2143
|
+
platform: session.event.platform
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
getUserQuery(session) {
|
|
2147
|
+
return {
|
|
2148
|
+
guildId: session.event.channel.id,
|
|
2149
|
+
platform: session.event.platform,
|
|
2150
|
+
userId: session.event.user.id
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
/** 从已订阅玩家中查找玩家返回SteamId,不需要以昵称匹配时仅需传入Session */
|
|
2154
|
+
async getSubscribedPlayerByNickNameOrSession(session, nickName) {
|
|
2155
|
+
const player = (await this.ctx.database.get("dt_subscribed_players", {
|
|
2156
|
+
guildId: session.event.channel.id,
|
|
2157
|
+
platform: session.event.platform,
|
|
2158
|
+
...nickName ? { nickName } : { userId: session.event.user.id }
|
|
2159
|
+
}))?.[0];
|
|
2160
|
+
return player;
|
|
2161
|
+
}
|
|
2162
|
+
getChannelInfo({ platform, guildId }) {
|
|
2163
|
+
return `${platform}:${guildId}`;
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
// src/app/data/stratz.api.ts
|
|
2168
|
+
var import_fs = __toESM(require("fs"));
|
|
2169
|
+
var import_koishi8 = require("koishi");
|
|
2170
|
+
var import_path = __toESM(require("path"));
|
|
2171
|
+
var import_p_queue = __toESM(require("p-queue"));
|
|
2172
|
+
|
|
2173
|
+
// src/app/common/error.ts
|
|
2174
|
+
var import_util = require("util");
|
|
2175
|
+
var NetworkError = class extends Error {
|
|
2176
|
+
static {
|
|
2177
|
+
__name(this, "NetworkError");
|
|
2178
|
+
}
|
|
2179
|
+
constructor(message, options) {
|
|
2180
|
+
super(message, options);
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
var ForbiddenError = class extends Error {
|
|
2184
|
+
static {
|
|
2185
|
+
__name(this, "ForbiddenError");
|
|
2186
|
+
}
|
|
2187
|
+
constructor(message, options) {
|
|
2188
|
+
super(message, options);
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
function handleError(error, logger, i18n, config) {
|
|
2192
|
+
if (error instanceof ForbiddenError) {
|
|
2193
|
+
let output = i18n.gt("dota2tracker.logger.stratz_token_banned") + "\n";
|
|
2194
|
+
output += error.stack || error.message;
|
|
2195
|
+
logger.error(output);
|
|
2196
|
+
} else if (error instanceof NetworkError) {
|
|
2197
|
+
if (config.suppressStratzNetworkErrors) {
|
|
2198
|
+
logger.debug(error);
|
|
2199
|
+
logger.info(1);
|
|
2200
|
+
} else {
|
|
2201
|
+
logger.error(error);
|
|
2202
|
+
}
|
|
2203
|
+
} else {
|
|
2204
|
+
let output = "An unexpected error was thrown:\n";
|
|
2205
|
+
if (error instanceof Error && error.stack) {
|
|
2206
|
+
output += error.stack;
|
|
2207
|
+
} else {
|
|
2208
|
+
output += (0, import_util.inspect)(error, { depth: null });
|
|
1843
2209
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
2210
|
+
logger.error(output);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
__name(handleError, "handleError");
|
|
2214
|
+
|
|
2215
|
+
// src/app/data/stratz.api.ts
|
|
2216
|
+
var StratzAPI = class extends import_koishi8.Service {
|
|
2217
|
+
constructor(ctx, pluginDir3) {
|
|
2218
|
+
super(ctx, "dota2tracker.stratz-api", true);
|
|
2219
|
+
this.pluginDir = pluginDir3;
|
|
2220
|
+
this.config = ctx.config;
|
|
2221
|
+
this.queue = new import_p_queue.default({ concurrency: 1, interval: 200, intervalCap: 1 });
|
|
2222
|
+
}
|
|
2223
|
+
static {
|
|
2224
|
+
__name(this, "StratzAPI");
|
|
2225
|
+
}
|
|
2226
|
+
BASE_URL = "https://api.stratz.com/graphql";
|
|
2227
|
+
queue;
|
|
2228
|
+
async queryGetWeeklyMetaByPosition({ bracketIds }) {
|
|
2229
|
+
return this.query("GetWeeklyMetaByPosition", { bracketIds }, (data) => !!data.heroStats);
|
|
2230
|
+
}
|
|
2231
|
+
async queryPlayerPerformanceForHeroRecommendation({ steamAccountId, recentDateTime }) {
|
|
2232
|
+
return this.query(
|
|
2233
|
+
"PlayerPerformanceForHeroRecommendation",
|
|
2234
|
+
{
|
|
2235
|
+
steamAccountId,
|
|
2236
|
+
recentDateTime
|
|
2237
|
+
},
|
|
2238
|
+
(data) => !!data.player
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
async queryPlayersMatchesForDaily(steamAccountIds, seconds) {
|
|
2242
|
+
return this.query(
|
|
2243
|
+
"PlayersMatchesForDaily",
|
|
2244
|
+
{
|
|
2245
|
+
steamAccountIds,
|
|
2246
|
+
seconds
|
|
2247
|
+
},
|
|
2248
|
+
(data) => !!data.players
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
async queryVerifyingPlayer(steamAccountId) {
|
|
2252
|
+
return this.query("VerifyingPlayer", { steamAccountId }, (data) => !!data.player);
|
|
2253
|
+
}
|
|
2254
|
+
async queryPlayerExtraInfo({ steamAccountId, matchCount, heroIds }) {
|
|
2255
|
+
return this.query(
|
|
2256
|
+
"PlayerExtraInfo",
|
|
2257
|
+
{
|
|
2258
|
+
steamAccountId,
|
|
2259
|
+
matchCount,
|
|
2260
|
+
heroIds
|
|
2261
|
+
},
|
|
2262
|
+
(data) => !!data.player
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
async queryPlayersInfoWith10MatchesForGuild({ steamAccountIds }) {
|
|
2266
|
+
return this.query("PlayersInfoWith10MatchesForGuild", { steamAccountIds }, (data) => !!data.players);
|
|
2267
|
+
}
|
|
2268
|
+
async queryPlayerInfoWith25Matches({ steamAccountId, heroIds }) {
|
|
2269
|
+
return this.query(
|
|
2270
|
+
"PlayerInfoWith25Matches",
|
|
2271
|
+
{
|
|
2272
|
+
steamAccountId,
|
|
2273
|
+
heroIds
|
|
2274
|
+
},
|
|
2275
|
+
(data) => !!data.player
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
async queryPlayersLastMatchRankInfo({ steamAccountIds }) {
|
|
2279
|
+
return this.query("PlayersLastmatchRankinfo", { steamAccountIds }, (data) => !!data.players);
|
|
2280
|
+
}
|
|
2281
|
+
async queryConstants(languageTag) {
|
|
2282
|
+
return this.query("Constants", { language: this.ctx.dota2tracker.i18n.getGraphqlLanguageTag(languageTag) }, (data) => !!data.constants);
|
|
2283
|
+
}
|
|
2284
|
+
async queryMatchInfo(matchId) {
|
|
2285
|
+
return this.query("MatchInfo", { matchId }, (data) => !!data.match);
|
|
2286
|
+
}
|
|
2287
|
+
async requestParseMatch(matchId) {
|
|
2288
|
+
const response = await this.query("RequestMatchDataAnalysis", {
|
|
2289
|
+
matchId
|
|
2290
|
+
});
|
|
2291
|
+
return response?.stratz?.matchRetry;
|
|
2292
|
+
}
|
|
2293
|
+
async query(queryName, variables, isValid = () => true) {
|
|
2294
|
+
if (queryName.startsWith("Players") && variables?.steamAccountIds.length > 5) {
|
|
2295
|
+
const playerIds = variables?.steamAccountIds ?? [];
|
|
2296
|
+
const chunkSize = 5;
|
|
2297
|
+
let allPlayers = [];
|
|
2298
|
+
for (let i = 0; i < playerIds.length; i += chunkSize) {
|
|
2299
|
+
const chunk = playerIds.slice(i, i + chunkSize);
|
|
2300
|
+
variables.steamAccountIds = chunk;
|
|
2301
|
+
const query_str = this.loadGraphqlFile(queryName);
|
|
2302
|
+
const result = await this.fetchData({ query: query_str, variables }, isValid);
|
|
2303
|
+
if (result && result.players) {
|
|
2304
|
+
allPlayers = allPlayers.concat(result.players);
|
|
1860
2305
|
}
|
|
1861
|
-
sessionPlayer.nickName = nick_name;
|
|
1862
|
-
await ctx.database.set("dt_subscribed_players", sessionPlayer.id, { nickName: sessionPlayer.nickName });
|
|
1863
|
-
session.send(session.text(".rename_success", { nick_name }));
|
|
1864
|
-
} else {
|
|
1865
|
-
session.send(session.text(".not_binded"));
|
|
1866
2306
|
}
|
|
2307
|
+
return { players: allPlayers };
|
|
2308
|
+
} else {
|
|
2309
|
+
const query_str = this.loadGraphqlFile(queryName);
|
|
2310
|
+
const result = await this.fetchData({ query: query_str, variables }, isValid);
|
|
2311
|
+
return result;
|
|
1867
2312
|
}
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2313
|
+
}
|
|
2314
|
+
fetchData(query, isValid) {
|
|
2315
|
+
return this.queue.add(async () => {
|
|
2316
|
+
try {
|
|
2317
|
+
const result = await this.ctx.http.post(this.BASE_URL, JSON.stringify(query), {
|
|
2318
|
+
responseType: "json",
|
|
2319
|
+
headers: {
|
|
2320
|
+
"User-Agent": "STRATZ_API",
|
|
2321
|
+
"Content-Type": "application/json",
|
|
2322
|
+
Authorization: `Bearer ${this.config.STRATZ_API_TOKEN}`
|
|
2323
|
+
},
|
|
2324
|
+
proxyAgent: this.config.proxyAddress || void 0
|
|
2325
|
+
});
|
|
2326
|
+
const isDataValid = isValid(result.data);
|
|
2327
|
+
if (result.errors) {
|
|
2328
|
+
const error = result.errors.map((e) => e.message).join("\n");
|
|
2329
|
+
if (isDataValid) {
|
|
2330
|
+
this.logger.warn(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.stratz_api_query_error", { cause: error }));
|
|
2331
|
+
return result.data;
|
|
2332
|
+
} else {
|
|
2333
|
+
throw new Error("Stratz API Error", { cause: error });
|
|
1886
2334
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const users2 = [];
|
|
1894
|
-
for (const subscribedPlayer of subscribedPlayers2) {
|
|
1895
|
-
const queryPlayer = playersInfo.find((player) => player.steamAccount.id == subscribedPlayer.steamId);
|
|
1896
|
-
const queryMember = memberList2?.data.find((member) => member.user?.id == subscribedPlayer.userId);
|
|
1897
|
-
users2.push({
|
|
1898
|
-
...subscribedPlayer,
|
|
1899
|
-
...queryPlayer,
|
|
1900
|
-
...queryMember
|
|
1901
|
-
});
|
|
1902
|
-
}
|
|
1903
|
-
return users2;
|
|
2335
|
+
}
|
|
2336
|
+
return result.data;
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
if (error.response) {
|
|
2339
|
+
if (error.response.status === 403) {
|
|
2340
|
+
throw new ForbiddenError("Stratz API Forbidden", { cause: error });
|
|
1904
2341
|
}
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
session.send(session.text(".query_failed"));
|
|
2342
|
+
throw new NetworkError("Stratz API HTTP Error", { cause: error });
|
|
2343
|
+
} else if (error.code) {
|
|
2344
|
+
throw new NetworkError("Stratz API Connection Error", { cause: error });
|
|
2345
|
+
} else {
|
|
2346
|
+
throw error;
|
|
1911
2347
|
}
|
|
1912
2348
|
}
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
loadGraphqlFile(queryName) {
|
|
2352
|
+
return import_fs.default.readFileSync(import_path.default.join(this.pluginDir, "queries", `${queryName}.graphql`), { encoding: "utf-8" }).replace(/[\r\n]+/g, " ");
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
|
|
2356
|
+
// src/app/data/valve.api.ts
|
|
2357
|
+
var import_koishi9 = require("koishi");
|
|
2358
|
+
var ValveAPI = class extends import_koishi9.Service {
|
|
2359
|
+
static {
|
|
2360
|
+
__name(this, "ValveAPI");
|
|
2361
|
+
}
|
|
2362
|
+
constructor(ctx) {
|
|
2363
|
+
super(ctx, "dota2tracker.valve-api", true);
|
|
2364
|
+
}
|
|
2365
|
+
async queryHeroDetailsFromValve(heroId, languageTag = "zh-CN") {
|
|
2366
|
+
return (await this.ctx.http.get(`https://www.dota2.com/datafeed/herodata?language=${this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag)}&hero_id=${heroId}`)).result.data.heroes[0];
|
|
2367
|
+
}
|
|
2368
|
+
async queryItemListFromValve(languageTag = "zh-CN") {
|
|
2369
|
+
return (await this.ctx.http.get(`https://www.dota2.com/datafeed/itemlist?language=${this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag)}`)).result.data.itemabilities;
|
|
2370
|
+
}
|
|
2371
|
+
async queryItemDetailsFromValve(itemId, languageTag = "zh-CN") {
|
|
2372
|
+
return (await this.ctx.http.get(`https://www.dota2.com/datafeed/itemdata?language=${this.ctx.dota2tracker.i18n.getValveLanguageTag(languageTag)}&item_id=${itemId}`)).result.data.items[0];
|
|
2373
|
+
}
|
|
2374
|
+
async queryLastPatchNumber() {
|
|
2375
|
+
return (await this.ctx.http.get("https://www.dota2.com/datafeed/patchnoteslist")).patches.at(-1).patch_number;
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
|
|
2379
|
+
// src/app/presentation/image.renderer.ts
|
|
2380
|
+
var import_koishi10 = require("koishi");
|
|
2381
|
+
var import_ejs = __toESM(require("ejs"));
|
|
2382
|
+
var import_fs2 = __toESM(require("fs"));
|
|
2383
|
+
var import_path2 = __toESM(require("path"));
|
|
2384
|
+
var dotaconstants5 = __toESM(require("dotaconstants"));
|
|
2385
|
+
|
|
2386
|
+
// src/app/common/types.ts
|
|
2387
|
+
var ImageType = /* @__PURE__ */ ((ImageType2) => {
|
|
2388
|
+
ImageType2["Icons"] = "icons";
|
|
2389
|
+
ImageType2["IconsFacets"] = "icons/facets";
|
|
2390
|
+
ImageType2["Heroes"] = "heroes";
|
|
2391
|
+
ImageType2["HeroIcons"] = "heroes/icons";
|
|
2392
|
+
ImageType2["HeroStats"] = "heroes/stats";
|
|
2393
|
+
ImageType2["Items"] = "items";
|
|
2394
|
+
ImageType2["Abilities"] = "abilities";
|
|
2395
|
+
ImageType2["Local"] = "local";
|
|
2396
|
+
return ImageType2;
|
|
2397
|
+
})(ImageType || {});
|
|
2398
|
+
var ImageFormat = /* @__PURE__ */ ((ImageFormat2) => {
|
|
2399
|
+
ImageFormat2["png"] = "png";
|
|
2400
|
+
ImageFormat2["svg"] = "svg";
|
|
2401
|
+
return ImageFormat2;
|
|
2402
|
+
})(ImageFormat || {});
|
|
2403
|
+
|
|
2404
|
+
// src/app/presentation/image.renderer.ts
|
|
2405
|
+
var import_luxon4 = require("luxon");
|
|
2406
|
+
var ImageRenderer = class extends import_koishi10.Service {
|
|
2407
|
+
constructor(ctx, pluginDir3) {
|
|
2408
|
+
super(ctx, "dota2tracker.image", true);
|
|
2409
|
+
this.pluginDir = pluginDir3;
|
|
2410
|
+
this.config = ctx.config;
|
|
2411
|
+
}
|
|
2412
|
+
static {
|
|
2413
|
+
__name(this, "ImageRenderer");
|
|
2414
|
+
}
|
|
2415
|
+
async renderToImageByFile(data, templateName, type, languageTag) {
|
|
2416
|
+
const html = await this.generateHTML(data, { source: "FILE", templateName, type }, languageTag);
|
|
2417
|
+
return this.ctx.puppeteer.render(html);
|
|
2418
|
+
}
|
|
2419
|
+
async renderToImageByEJSCode(data, ejsCode, languageTag) {
|
|
2420
|
+
const html = await this.generateHTML(data, { source: "CODE", code: ejsCode }, languageTag);
|
|
2421
|
+
return this.ctx.puppeteer.render(html);
|
|
2422
|
+
}
|
|
2423
|
+
async generateHTML(data, template, languageTag) {
|
|
2424
|
+
const templateData = {
|
|
2425
|
+
data,
|
|
2426
|
+
ImageType,
|
|
2427
|
+
ImageFormat,
|
|
2428
|
+
dotaconstants: dotaconstants5,
|
|
2429
|
+
DateTime: import_luxon4.DateTime,
|
|
2430
|
+
$t: /* @__PURE__ */ __name((key, params) => this.ctx.dota2tracker.i18n.$t(languageTag, key, params), "$t"),
|
|
2431
|
+
languageTag,
|
|
2432
|
+
Random: import_koishi10.Random,
|
|
2433
|
+
fontFamily: this.config.templateFonts,
|
|
2434
|
+
getImageUrl: this.getImageUrl.bind(this)
|
|
2435
|
+
};
|
|
1916
2436
|
try {
|
|
1917
|
-
let
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2437
|
+
let html;
|
|
2438
|
+
if (template.source === "FILE") {
|
|
2439
|
+
const templatePath = import_path2.default.join(this.pluginDir, "template", template.type, `${template.templateName}.ejs`);
|
|
2440
|
+
html = await import_ejs.default.renderFile(templatePath, templateData, {
|
|
2441
|
+
strict: false
|
|
2442
|
+
});
|
|
1922
2443
|
} else {
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2444
|
+
html = await import_ejs.default.render(template.code, templateData, {
|
|
2445
|
+
strict: false,
|
|
2446
|
+
async: true
|
|
2447
|
+
});
|
|
1926
2448
|
}
|
|
1927
|
-
|
|
2449
|
+
if (process.env.NODE_ENV === "development") import_fs2.default.writeFileSync(import_path2.default.join(this.pluginDir, "temp.html"), html);
|
|
2450
|
+
return html;
|
|
1928
2451
|
} catch (error) {
|
|
1929
|
-
|
|
1930
|
-
throw
|
|
2452
|
+
this.logger.error(error);
|
|
2453
|
+
throw error;
|
|
1931
2454
|
}
|
|
1932
2455
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2456
|
+
getImageUrl(image, type = "local" /* Local */, format = "png" /* png */) {
|
|
2457
|
+
if (type === "local" /* Local */) {
|
|
2458
|
+
try {
|
|
2459
|
+
if (format === "svg" /* svg */) return import_fs2.default.readFileSync(import_path2.default.join(this.pluginDir, "template", "images", `${image}.svg`));
|
|
2460
|
+
const imageData = import_fs2.default.readFileSync(import_path2.default.join(this.pluginDir, "template", "images", `${image}.${format}`));
|
|
2461
|
+
const base64Data = imageData.toString("base64");
|
|
2462
|
+
return `data:image/png;base64,${base64Data}`;
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
console.error(error);
|
|
2465
|
+
return "";
|
|
2466
|
+
}
|
|
2467
|
+
} else return `https://cdn.akamai.steamstatic.com/apps/dota2/images/dota_react/${type}/${image}.${format}`;
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
// src/app/presentation/message.builder.ts
|
|
2472
|
+
var import_koishi11 = require("koishi");
|
|
2473
|
+
var import_luxon5 = require("luxon");
|
|
2474
|
+
var MessageBuilder = class extends import_koishi11.Service {
|
|
2475
|
+
static {
|
|
2476
|
+
__name(this, "MessageBuilder");
|
|
2477
|
+
}
|
|
2478
|
+
constructor(ctx) {
|
|
2479
|
+
super(ctx, "dota2tracker.message-builder", true);
|
|
2480
|
+
this.config = ctx.config;
|
|
2481
|
+
}
|
|
2482
|
+
async buildHeroOfTheDayMessage(languageTag, heroRcmd, metaRcmd) {
|
|
2483
|
+
const $t = /* @__PURE__ */ __name((key, params) => this.ctx.dota2tracker.i18n.$t(languageTag, `commands.dota2tracker.hero-of-the-day.messages.${key}`, params), "$t");
|
|
2484
|
+
let ejs2 = "<html><head><style>body{width:fit-content;height:fit-content;margin:0;padding:12px;}</style></head><body>";
|
|
2485
|
+
if (heroRcmd.recommendationType !== "LIFETIME_NO_RECORD") {
|
|
2486
|
+
ejs2 += `<h3>${$t("title_recommendation")}</h3>`;
|
|
2487
|
+
ejs2 += `<p>${$t("recommendation_intro")}</p>`;
|
|
2488
|
+
ejs2 += `<p><b>${$t("recommendation_heroes", { heroes: heroRcmd.recommendedHeroes.map((heroId) => this.ctx.dota2tracker.i18n.getDisplayNameForHero(heroId, languageTag, { forceOfficialName: true })) })}</b></p>`;
|
|
2489
|
+
if (heroRcmd.recommendationType === "LIFETIME_ONLY") {
|
|
2490
|
+
ejs2 += `<p style='color:#666'>${$t("recommendation_type_lifetime_only")}</p>`;
|
|
2491
|
+
}
|
|
2492
|
+
ejs2 += `<p>${$t("details.pool_description")}</p>`;
|
|
2493
|
+
ejs2 += `<p>${$t("details.table_intro")}</p>`;
|
|
2494
|
+
ejs2 += `<div style="display: grid;grid-template-columns: repeat(6, auto);gap: 10px;text-align: center;align-items: center;">`;
|
|
2495
|
+
ejs2 += `<div style="display: contents">
|
|
2496
|
+
<div style="text-align:left">${$t("details.table_headers.hero")}</div>
|
|
2497
|
+
<div>${$t("details.table_headers.recent_wins")}</div>
|
|
2498
|
+
<div>${$t("details.table_headers.lifetime_wins")}</div>
|
|
2499
|
+
<div>${$t("details.table_headers.imp_bonus")}</div>
|
|
2500
|
+
<div>${$t("details.table_headers.is_hot_streak")}</div>
|
|
2501
|
+
<div>${$t("details.table_headers.total_score")}</div>
|
|
2502
|
+
</div>`;
|
|
2503
|
+
ejs2 += heroRcmd.recommendationPool.map(
|
|
2504
|
+
(hero) => `<div style="display: contents">
|
|
2505
|
+
<div style="text-align:left">${this.ctx.dota2tracker.i18n.getDisplayNameForHero(hero.heroId, languageTag, { forceOfficialName: true }) + (heroRcmd.recommendedHeroes.includes(hero.heroId) ? "*" : "")}</div>
|
|
2506
|
+
<div>${hero.recentWinScore}</div>
|
|
2507
|
+
<div>${roundToDecimalPlaces(hero.lifetimeWinScore)}</div>
|
|
2508
|
+
<div>${roundToDecimalPlaces(hero.impBonus)}</div>
|
|
2509
|
+
<div>${hero.isHotStreak ? "√" : "/"}</div>
|
|
2510
|
+
<div>${hero.totalScore.toFixed(2)}</div>
|
|
2511
|
+
</div>`
|
|
2512
|
+
).join("");
|
|
2513
|
+
ejs2 += `</div>`;
|
|
2514
|
+
ejs2 += `<p>${$t("details.scoring_formula")}</p>`;
|
|
2515
|
+
ejs2 += `<p>${$t("details.hot_streak_desc")}</p>`;
|
|
2516
|
+
} else {
|
|
2517
|
+
ejs2 += `<p>${$t("recommendation_type_no_record")}</p>`;
|
|
2518
|
+
}
|
|
2519
|
+
ejs2 += "<br>";
|
|
2520
|
+
ejs2 += `<h3>${$t("title_meta")}</h3>`;
|
|
2521
|
+
const tiersText = metaRcmd.targetTiers.map((tier) => this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.template.ranks." + tier)).join(", ");
|
|
2522
|
+
ejs2 += `<p>${$t("meta_intro", { tiers: tiersText })}</p>`;
|
|
2523
|
+
ejs2 += `<i style="font-size:12px;color:#333">${$t("meta_table_header")}</i>`;
|
|
2524
|
+
for (const pos in metaRcmd.recommendation) {
|
|
2525
|
+
const heroesText = metaRcmd.recommendation[pos].map((hero) => `${this.ctx.dota2tracker.i18n.getDisplayNameForHero(hero.heroId, languageTag, { forceOfficialName: true })}(${roundToDecimalPlaces(hero.pickRate * 100)}% ${roundToDecimalPlaces(hero.winRate * 100)}%)`).join("、");
|
|
2526
|
+
ejs2 += `<p>${$t("meta_position", { pos: pos.at(-1) })}${heroesText}</p>`;
|
|
1948
2527
|
}
|
|
2528
|
+
ejs2 += "</body></html>";
|
|
2529
|
+
return await this.ctx.dota2tracker.image.renderToImageByEJSCode(void 0, ejs2, languageTag);
|
|
1949
2530
|
}
|
|
1950
|
-
|
|
1951
|
-
async function generateMatchMessage(match, languageTag, guild) {
|
|
2531
|
+
buildMatchMessage(languageTag, match, players) {
|
|
1952
2532
|
let broadMatchMessage = "";
|
|
1953
|
-
|
|
1954
|
-
let broadPlayers = match.players.filter((item) =>
|
|
2533
|
+
const playerIds = players.map((player) => player.steamId);
|
|
2534
|
+
let broadPlayers = match.players.filter((item) => playerIds.includes(item.steamAccountId));
|
|
1955
2535
|
for (let player of broadPlayers) {
|
|
1956
|
-
const
|
|
2536
|
+
const random = new import_koishi11.Random(() => enhancedSimpleHashToSeed(`${match.id}-${player.steamAccountId}-${player.playerSlot}`));
|
|
1957
2537
|
let comment;
|
|
1958
2538
|
if (player.isRadiant == match.didRadiantWin) {
|
|
1959
2539
|
if (player.deathContribution < 0.2 || player.killContribution > 0.75 || player.heroDamage / player.networth > 1.5 || player.towerDamage > 1e4 || player.imp > 0)
|
|
1960
|
-
comment =
|
|
1961
|
-
else comment =
|
|
2540
|
+
comment = random.pick(customConvertArrayOfString(this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.broadcast.WIN_POSITIVE")));
|
|
2541
|
+
else comment = random.pick(customConvertArrayOfString(this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.broadcast.WIN_NEGATIVE")));
|
|
1962
2542
|
} else {
|
|
1963
2543
|
if (player.deathContribution < 0.25 || player.killContribution > 0.75 || player.heroDamage / player.networth > 1 || player.towerDamage > 5e3 || player.imp > 0)
|
|
1964
|
-
comment =
|
|
1965
|
-
else comment =
|
|
2544
|
+
comment = random.pick(customConvertArrayOfString(this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.broadcast.LOSE_NEGATIVE")));
|
|
2545
|
+
else comment = random.pick(customConvertArrayOfString(this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.broadcast.LOSE_NEGATIVE")));
|
|
1966
2546
|
}
|
|
1967
|
-
let broadPlayerMessage =
|
|
2547
|
+
let broadPlayerMessage = this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.broadcast.message", {
|
|
1968
2548
|
name: player.steamAccount.name,
|
|
1969
|
-
hero_name:
|
|
2549
|
+
hero_name: this.ctx.dota2tracker.i18n.getDisplayNameForHero(player.hero.id, languageTag, { random }),
|
|
1970
2550
|
comment,
|
|
1971
2551
|
kda: `${((player.kills + player.assists) / (player.deaths || 1)).toFixed(2)} [${player.kills}/${player.deaths}/${player.assists}]`,
|
|
1972
2552
|
gpm_xpm: `${player.goldPerMinute}/${player.experiencePerMinute}`,
|
|
@@ -1976,507 +2556,404 @@ async function apply(ctx, config) {
|
|
|
1976
2556
|
});
|
|
1977
2557
|
broadMatchMessage += broadPlayerMessage + "\n";
|
|
1978
2558
|
}
|
|
2559
|
+
if (this.config.urlInMessageType.includes("match")) {
|
|
2560
|
+
broadMatchMessage += "https://stratz.com/matches/" + match.id;
|
|
2561
|
+
}
|
|
1979
2562
|
return broadMatchMessage;
|
|
1980
2563
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
return await ctx.puppeteer.render(imageHTML);
|
|
2564
|
+
buildPlayerMessage(steamId) {
|
|
2565
|
+
if (!this.config.urlInMessageType.includes("player")) return "";
|
|
2566
|
+
return `https://stratz.com/players/${steamId}`;
|
|
1985
2567
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
if (!session.argv?.source.startsWith("dota2tracker.query-recent-match")) await session.send(session.text("commands.dota2tracker.query-match.messages.querying_match"));
|
|
1997
|
-
try {
|
|
1998
|
-
const languageTag = await getLanguageTag({ session });
|
|
1999
|
-
const matchQuery = await queryMatchData(Number(match_id));
|
|
2000
|
-
if (matchQuery.match.parsedDateTime && matchQuery.match.players.filter((player) => player?.stats?.heroDamageReport?.dealtTotal).length > 0 || !options.parse) {
|
|
2001
|
-
const match = await formatMatchData(matchQuery, languageTag);
|
|
2002
|
-
const image = await generateMatchImage(match, languageTag);
|
|
2003
|
-
session.send((ctx.config.urlInMessageType.some((type) => type == "match") ? "https://stratz.com/matches/" + match.id : "") + image);
|
|
2004
|
-
} else {
|
|
2005
|
-
session.send(session.text("commands.dota2tracker.query-match.messages.waiting_for_parse"));
|
|
2006
|
-
pendingMatches.push({
|
|
2007
|
-
matchId: matchQuery.match.id,
|
|
2008
|
-
guilds: { [languageTag]: [{ guildId: session.event.channel.id, platform: session.event.platform, players: [] }] },
|
|
2009
|
-
queryTime: /* @__PURE__ */ new Date(),
|
|
2010
|
-
hasMessage: true
|
|
2011
|
-
});
|
|
2012
|
-
query("RequestMatchDataAnalysis", {
|
|
2013
|
-
matchId: matchQuery.match.id
|
|
2014
|
-
}).then((response) => ctx.logger.info($t(GlobalLanguageTag, `dota2tracker.logger.parse_request_${response.stratz.matchRetry ? "sent" : "failed"}`, { matchId: matchQuery.match.id })));
|
|
2015
|
-
}
|
|
2016
|
-
} catch (error) {
|
|
2017
|
-
session.send(session.text("commands.dota2tracker.query-match.messages.query_failed"));
|
|
2018
|
-
ctx.logger.error(error);
|
|
2019
|
-
}
|
|
2020
|
-
});
|
|
2021
|
-
ctx.command("dota2tracker.query-recent-match [input_data]").alias("查询最近比赛").option("parse", "-p").action(async ({ session, options }, input_data) => {
|
|
2022
|
-
if (session.guild || !session.guild && input_data) {
|
|
2023
|
-
let sessionPlayer;
|
|
2024
|
-
if (!input_data) {
|
|
2025
|
-
sessionPlayer = (await ctx.database.get("dt_subscribed_players", {
|
|
2026
|
-
guildId: session.event.channel.id,
|
|
2027
|
-
platform: session.event.platform,
|
|
2028
|
-
userId: session.event.user.id
|
|
2029
|
-
}))[0];
|
|
2030
|
-
if (!sessionPlayer) {
|
|
2031
|
-
session.send(session.text(".not_binded"));
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
let flagBindedPlayer = sessionPlayer || (await ctx.database.get("dt_subscribed_players", {
|
|
2036
|
-
guildId: session.event.channel.id,
|
|
2037
|
-
platform: session.event.platform,
|
|
2038
|
-
nickName: input_data
|
|
2039
|
-
}))[0];
|
|
2040
|
-
if (!(flagBindedPlayer || /^\d{1,11}$/.test(input_data))) {
|
|
2041
|
-
session.send(session.text(".steam_id_invalid"));
|
|
2042
|
-
return;
|
|
2043
|
-
}
|
|
2044
|
-
let lastMatchId = 0;
|
|
2045
|
-
try {
|
|
2046
|
-
await session.send(session.text(".querying_match"));
|
|
2047
|
-
const lastMatchQuery = await query("PlayersLastmatchRankinfo", {
|
|
2048
|
-
steamAccountIds: [parseInt(flagBindedPlayer?.steamId ?? input_data)]
|
|
2049
|
-
});
|
|
2050
|
-
if (lastMatchQuery.players[0].steamAccount.isAnonymous) {
|
|
2051
|
-
await session.send(session.text(".is_anonymous"));
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
lastMatchId = lastMatchQuery.players[0].matches[0]?.id;
|
|
2055
|
-
} catch (error) {
|
|
2056
|
-
session.send(session.text(".query_failed"));
|
|
2057
|
-
ctx.logger.error(error);
|
|
2058
|
-
return;
|
|
2059
|
-
}
|
|
2060
|
-
session.execute(`dota2tracker.query-match ${lastMatchId}${options.parse ? " -p" : ""}`);
|
|
2061
|
-
} else {
|
|
2062
|
-
session.send(session.text(".not_in_group"));
|
|
2568
|
+
buildHeroMessage(hero) {
|
|
2569
|
+
if (!this.config.urlInMessageType.includes("hero")) return "";
|
|
2570
|
+
const heroNameForUrl = hero?.name?.match(/^npc_dota_hero_(.+)$/)?.[1];
|
|
2571
|
+
return `https://wiki.dota2.com.cn/hero/${heroNameForUrl}.html`;
|
|
2572
|
+
}
|
|
2573
|
+
async buildMembersMessage(members, languageTag) {
|
|
2574
|
+
const $t = /* @__PURE__ */ __name((key, params) => this.ctx.dota2tracker.i18n.$t(languageTag, `commands.dota2tracker.query-members.messages.${key}`, params), "$t");
|
|
2575
|
+
if (members.length === 0) {
|
|
2576
|
+
return $t("no_members");
|
|
2063
2577
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
platform: session.event.platform,
|
|
2073
|
-
userId: session.event.user.id
|
|
2074
|
-
}))[0];
|
|
2075
|
-
if (!sessionPlayer) {
|
|
2076
|
-
session.send(session.text(".not_binded"));
|
|
2077
|
-
return;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
let flagBindedPlayer = sessionPlayer || (await ctx.database.get("dt_subscribed_players", {
|
|
2081
|
-
guildId: session.event.channel.id,
|
|
2082
|
-
platform: session.event.platform,
|
|
2083
|
-
nickName: input_data
|
|
2084
|
-
}))[0];
|
|
2085
|
-
if (!(flagBindedPlayer || /^\d{1,11}$/.test(input_data))) {
|
|
2086
|
-
session.send(session.text(".steam_id_invalid"));
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
session.send(session.text(".querying_player"));
|
|
2090
|
-
let heroId = findingHero(options.hero);
|
|
2091
|
-
try {
|
|
2092
|
-
let steamId = Number(flagBindedPlayer?.steamId ?? input_data);
|
|
2093
|
-
const playerQuery = await query("PlayerInfoWith25Matches", {
|
|
2094
|
-
steamAccountId: steamId,
|
|
2095
|
-
heroIds: heroId
|
|
2096
|
-
});
|
|
2097
|
-
const playerExtraQuery = !playerQuery.player.steamAccount.isAnonymous ? await query("PlayerExtraInfo", {
|
|
2098
|
-
steamAccountId: steamId,
|
|
2099
|
-
matchCount: playerQuery.player.matchCount,
|
|
2100
|
-
totalHeroCount: Object.keys(dotaconstants2.heroes).length,
|
|
2101
|
-
heroIds: heroId
|
|
2102
|
-
}) : {
|
|
2103
|
-
player: {
|
|
2104
|
-
heroesPerformance: [],
|
|
2105
|
-
dotaPlus: null
|
|
2578
|
+
let ejs2 = `
|
|
2579
|
+
<html>
|
|
2580
|
+
<head>
|
|
2581
|
+
<style>
|
|
2582
|
+
body {
|
|
2583
|
+
padding: 16px;
|
|
2584
|
+
width: fit-content;
|
|
2585
|
+
background-color: #f7f7f7;
|
|
2106
2586
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
session.send(
|
|
2115
|
-
(ctx.config.urlInMessageType.some((type) => type == "player") ? "https://stratz.com/players/" + player.steamAccount.id : "") + await ctx.puppeteer.render(await genImageHTML(player, config.template_player, "player" /* Player */, ctx, languageTag))
|
|
2116
|
-
);
|
|
2117
|
-
} catch (error) {
|
|
2118
|
-
ctx.logger.error(error);
|
|
2119
|
-
session.send(session.text(".query_failed"));
|
|
2120
|
-
}
|
|
2121
|
-
} else {
|
|
2122
|
-
session.send(session.text(".not_in_group"));
|
|
2123
|
-
}
|
|
2124
|
-
});
|
|
2125
|
-
ctx.command("dota2tracker.query-hero <input_data>").option("random", "-r").alias("查询英雄").action(async ({ session, options }, input_data) => {
|
|
2126
|
-
const languageTag = await getLanguageTag({ session });
|
|
2127
|
-
if (options.random) input_data = random.pick(Object.keys(dotaconstants2.heroes));
|
|
2128
|
-
if (input_data) {
|
|
2129
|
-
let heroId = findingHero(input_data);
|
|
2130
|
-
if (!heroId) {
|
|
2131
|
-
session.send(session.text(".not_found"));
|
|
2132
|
-
return;
|
|
2133
|
-
}
|
|
2134
|
-
await session.send(session.text(".querying_hero"));
|
|
2135
|
-
try {
|
|
2136
|
-
let hero = await queryHeroDetailsFromValve(heroId, languageTag);
|
|
2137
|
-
hero = getFormattedHeroData(hero);
|
|
2138
|
-
await session.send(
|
|
2139
|
-
(ctx.config.urlInMessageType.some((type) => type == "hero") ? `https://wiki.dota2.com.cn/hero/${hero["name"].match(/^npc_dota_hero_(.+)$/)[1]}.html` : "") + await ctx.puppeteer.render(await genImageHTML(hero, config.template_hero, "hero" /* Hero */, ctx, languageTag))
|
|
2140
|
-
);
|
|
2141
|
-
} catch (error) {
|
|
2142
|
-
ctx.logger.error(error);
|
|
2143
|
-
session.send(session.text(".query_failed"));
|
|
2144
|
-
}
|
|
2145
|
-
} else {
|
|
2146
|
-
session.send(session.text(".empty_input"));
|
|
2147
|
-
}
|
|
2148
|
-
});
|
|
2149
|
-
function findingHero(input) {
|
|
2150
|
-
const heroIds = Object.keys(dotaconstants2.heroes).map((id) => parseInt(id));
|
|
2151
|
-
let tid;
|
|
2152
|
-
for (const loc of Object.keys(GraphqlLanguageEnum)) {
|
|
2153
|
-
for (const id_nicknames of getHeroNicknames(heroIds, loc)) {
|
|
2154
|
-
for (const [id, nicknames] of Object.entries(id_nicknames)) {
|
|
2155
|
-
for (const nickname of nicknames) {
|
|
2156
|
-
if (input == nickname) return Number(id);
|
|
2587
|
+
table {
|
|
2588
|
+
border-collapse: collapse;
|
|
2589
|
+
width: 100%;
|
|
2590
|
+
background-color: white;
|
|
2591
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
2592
|
+
border-radius: 8px;
|
|
2593
|
+
overflow: hidden;
|
|
2157
2594
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2595
|
+
th, td {
|
|
2596
|
+
text-align: left;
|
|
2597
|
+
padding: 12px 16px;
|
|
2598
|
+
border-bottom: 1px solid #eeeeee;
|
|
2599
|
+
}
|
|
2600
|
+
tr:last-child td {
|
|
2601
|
+
border-bottom: none;
|
|
2602
|
+
}
|
|
2603
|
+
th {
|
|
2604
|
+
font-weight: bold;
|
|
2605
|
+
background-color: #f2f2f2;
|
|
2606
|
+
}
|
|
2607
|
+
h3 {
|
|
2608
|
+
text-align: center;
|
|
2609
|
+
margin-bottom: 20px;
|
|
2610
|
+
color: #333;
|
|
2611
|
+
}
|
|
2612
|
+
</style>
|
|
2613
|
+
</head>
|
|
2614
|
+
<body>
|
|
2615
|
+
<h3>${$t("title", { count: members.length })}</h3>
|
|
2616
|
+
<table>
|
|
2617
|
+
<thead>
|
|
2618
|
+
<tr>
|
|
2619
|
+
<th>${$t("table_headers.nickname")}</th>
|
|
2620
|
+
<th>${$t("table_headers.winrate")}</th>
|
|
2621
|
+
<th>${$t("table_headers.last_match")}</th>
|
|
2622
|
+
</tr>
|
|
2623
|
+
</thead>
|
|
2624
|
+
<tbody>
|
|
2625
|
+
`;
|
|
2626
|
+
for (const member of members) {
|
|
2627
|
+
const winRate = typeof member.winRate === "number" && !isNaN(member.winRate) ? `${roundToDecimalPlaces(member.winRate * 100).toFixed(1)}%` : "-----";
|
|
2628
|
+
const lastMatch = member.lastMatchTime ? formatCustomRelativeTime(import_luxon5.DateTime.fromSeconds(member.lastMatchTime), this.ctx.dota2tracker.i18n, languageTag) : "----------";
|
|
2629
|
+
ejs2 += `
|
|
2630
|
+
<tr>
|
|
2631
|
+
<td>${member.name}</td>
|
|
2632
|
+
<td>${winRate}</td>
|
|
2633
|
+
<td>${lastMatch}</td>
|
|
2634
|
+
</tr>
|
|
2635
|
+
`;
|
|
2164
2636
|
}
|
|
2165
|
-
|
|
2637
|
+
ejs2 += `
|
|
2638
|
+
</tbody>
|
|
2639
|
+
</table>
|
|
2640
|
+
</body>
|
|
2641
|
+
</html>
|
|
2642
|
+
`;
|
|
2643
|
+
return await this.ctx.dota2tracker.image.renderToImageByEJSCode({}, ejs2, languageTag);
|
|
2166
2644
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
let content = [];
|
|
2174
|
-
try {
|
|
2175
|
-
const rawContent = ctx.i18n.render([languageTag], [`dota2tracker.heroes_nicknames.${heroId}`], {}).at(0)?.attrs?.content ?? "";
|
|
2176
|
-
content = JSON.parse(`[${rawContent}]`);
|
|
2177
|
-
} catch (error) {
|
|
2178
|
-
ctx.logger.error(`Failed to parse heroId ${heroId} content: ${error.message}`);
|
|
2179
|
-
content = [];
|
|
2180
|
-
}
|
|
2181
|
-
result.push({
|
|
2182
|
-
[heroId]: Array.from(/* @__PURE__ */ new Set([$t(languageTag, "dota2tracker.template.hero_names." + heroId), ...content]))
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
return Array.isArray(heroIds) ? result : result[0][heroIds];
|
|
2645
|
+
buildRankChangedMessage(languageTag, name2, prevRank, currRank) {
|
|
2646
|
+
return this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.broadcast.rank_changed", {
|
|
2647
|
+
name: name2,
|
|
2648
|
+
prev: { medal: this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.template.ranks." + prevRank.medal), star: prevRank.star },
|
|
2649
|
+
curr: { medal: this.ctx.dota2tracker.i18n.$t(languageTag, "dota2tracker.template.ranks." + currRank.medal), star: currRank.star }
|
|
2650
|
+
});
|
|
2186
2651
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
await session.send(session.text(".querying_item"));
|
|
2194
|
-
const languageTag = await getLanguageTag({ session });
|
|
2195
|
-
const currentGameVersion = await queryLastPatchNumber();
|
|
2196
|
-
let itemList;
|
|
2197
|
-
const cache = await ctx.cache.get("dt_itemlist_constants", languageTag);
|
|
2198
|
-
try {
|
|
2199
|
-
if (!cache || cache.gameVersion != currentGameVersion) {
|
|
2200
|
-
await session.send(session.text(".cache_building"));
|
|
2201
|
-
itemList = await getFormattedItemListData(await queryItemListFromValve(languageTag));
|
|
2202
|
-
await ctx.cache.set("dt_itemlist_constants", languageTag, {
|
|
2203
|
-
gameVersion: currentGameVersion,
|
|
2204
|
-
itemList
|
|
2205
|
-
});
|
|
2206
|
-
} else {
|
|
2207
|
-
itemList = cache.itemList;
|
|
2208
|
-
}
|
|
2209
|
-
} catch (error) {
|
|
2210
|
-
ctx.logger.error(error);
|
|
2211
|
-
await session.send(session.text(".query_list_failed"));
|
|
2212
|
-
return;
|
|
2213
|
-
}
|
|
2214
|
-
const matchedItemList = searchItems(itemList, input_data, languageTag);
|
|
2215
|
-
if (!input_data || matchedItemList.length > config.maxSendItemCount || !matchedItemList.length) {
|
|
2216
|
-
if (!input_data) await session.send(session.text(".empty_input", { show: config.showItemListAtTooMuchItems }));
|
|
2217
|
-
if (matchedItemList.length > config.maxSendItemCount) await session.send(session.text(".too_many_items", { count: matchedItemList.length, max: config.maxSendItemCount, show: config.showItemListAtTooMuchItems }));
|
|
2218
|
-
if (input_data && matchedItemList.length === 0) await session.send(session.text(".not_found"));
|
|
2219
|
-
if (config.showItemListAtTooMuchItems && (matchedItemList.length || !input_data))
|
|
2220
|
-
await session.send(await ctx.puppeteer.render(await genImageHTML(matchedItemList.length ? matchedItemList : itemList, "itemlist", "item" /* Item */, ctx, languageTag)));
|
|
2221
|
-
} else {
|
|
2222
|
-
await session.send(session.text(".finded_items", { items: matchedItemList }));
|
|
2223
|
-
for (const litem of matchedItemList) {
|
|
2224
|
-
try {
|
|
2225
|
-
const item = Object.assign(await queryItemDetailsFromValve(litem.id, languageTag), litem);
|
|
2226
|
-
await session.send(await ctx.puppeteer.render(await genImageHTML(item, "item", "item" /* Item */, ctx, languageTag)));
|
|
2227
|
-
} catch (error) {
|
|
2228
|
-
ctx.logger.error(error);
|
|
2229
|
-
await session.send(session.text(".query_item_failed", [litem.name_loc]));
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
}
|
|
2233
|
-
});
|
|
2234
|
-
function searchItems(items, keyword, languageTag) {
|
|
2235
|
-
if (!keyword) return [];
|
|
2236
|
-
const alias = constantLocales[languageTag].dota2tracker.items_alias?.[keyword] ?? config.customItemAlias.filter((cia) => cia.alias == keyword).map((cia) => cia.keyword);
|
|
2237
|
-
const exactMatch = items.filter(
|
|
2238
|
-
(item) => alias?.some((a) => item.name_loc.trim().toLowerCase() == a.toLowerCase()) || item.name_loc.trim().toLowerCase() === keyword.trim().toLowerCase() || Number.isInteger(Number(keyword)) && item.id === Number(keyword)
|
|
2239
|
-
);
|
|
2240
|
-
if (exactMatch.length) return exactMatch;
|
|
2241
|
-
return fuzzySearchItems(alias.length ? alias : [keyword], items);
|
|
2652
|
+
};
|
|
2653
|
+
function customConvertArrayOfString(str) {
|
|
2654
|
+
try {
|
|
2655
|
+
return JSON.parse(`[${str}]`);
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
throw error;
|
|
2242
2658
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2659
|
+
}
|
|
2660
|
+
__name(customConvertArrayOfString, "customConvertArrayOfString");
|
|
2661
|
+
|
|
2662
|
+
// src/app/tasks/match-watcher.task.ts
|
|
2663
|
+
var import_koishi12 = require("koishi");
|
|
2664
|
+
var import_luxon6 = require("luxon");
|
|
2665
|
+
var MatchWatcherTask = class extends import_koishi12.Service {
|
|
2666
|
+
static {
|
|
2667
|
+
__name(this, "MatchWatcherTask");
|
|
2668
|
+
}
|
|
2669
|
+
constructor(ctx) {
|
|
2670
|
+
super(ctx, "dota2tracker.match-watcher", true);
|
|
2671
|
+
this.config = ctx.config;
|
|
2672
|
+
}
|
|
2673
|
+
async discovery() {
|
|
2674
|
+
try {
|
|
2675
|
+
const activePlayers = await this.ctx.dota2tracker.database.getActiveSubscribedPlayers();
|
|
2676
|
+
const uniqueSteamIds = activePlayers.map((player) => player.steamId).filter((steamId, index, self) => self.indexOf(steamId) === index);
|
|
2677
|
+
const playersData = (await this.ctx.dota2tracker.stratzAPI.queryPlayersLastMatchRankInfo({ steamAccountIds: uniqueSteamIds })).players;
|
|
2678
|
+
await this.discoverNewMatches(playersData, activePlayers);
|
|
2679
|
+
if (this.config.rankBroadSwitch) await this.detectRankChanges(playersData, activePlayers);
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.ctx.config);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
async discoverNewMatches(playersData, activePlayers) {
|
|
2685
|
+
const sendedMatchIds = await this.ctx.dota2tracker.cache.getSendedMatchIds();
|
|
2686
|
+
const lastMatches = playersData.map((player) => player.matches[0]).filter((match) => match && match.id).filter((item, index, self) => index === self.findIndex((t) => t.id === item.id)).filter((match) => import_luxon6.DateTime.fromSeconds(match.startDateTime) > import_luxon6.DateTime.now().minus({ days: 1 })).filter((match) => !this.ctx.dota2tracker.parsePolling.isPending(match.id)).filter((match) => !sendedMatchIds.has(match.id));
|
|
2687
|
+
for (const match of lastMatches) {
|
|
2688
|
+
const steamIdsInMatch = new Set(match.players.map((p) => p.steamAccount.id));
|
|
2689
|
+
const relevantActivePlayers = activePlayers.filter((p) => steamIdsInMatch.has(p.steamId));
|
|
2690
|
+
if (!relevantActivePlayers.length) {
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
const playersByGuild = /* @__PURE__ */ new Map();
|
|
2694
|
+
for (const player of relevantActivePlayers) {
|
|
2695
|
+
const guildKey = `${player.platform}:${player.guildId}`;
|
|
2696
|
+
if (!playersByGuild.has(guildKey)) {
|
|
2697
|
+
playersByGuild.set(guildKey, {
|
|
2698
|
+
guildInfo: { platform: player.platform, channelId: player.guildId, guildId: player.guildId },
|
|
2699
|
+
players: []
|
|
2700
|
+
});
|
|
2262
2701
|
}
|
|
2702
|
+
playersByGuild.get(guildKey).players.push({
|
|
2703
|
+
steamId: player.steamId,
|
|
2704
|
+
nickname: player.nickName
|
|
2705
|
+
});
|
|
2263
2706
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2707
|
+
const messageToLogger = [];
|
|
2708
|
+
const subscribers = [];
|
|
2709
|
+
for (const [key, { guildInfo, players }] of playersByGuild.entries()) {
|
|
2710
|
+
const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: guildInfo.channelId });
|
|
2711
|
+
const subscriber = this.ctx.dota2tracker.parsePolling.createSubscriberByAutomatic({
|
|
2712
|
+
type: "GUILD",
|
|
2713
|
+
...guildInfo,
|
|
2714
|
+
languageTag,
|
|
2715
|
+
relevantPlayers: players
|
|
2716
|
+
});
|
|
2717
|
+
messageToLogger.push({
|
|
2718
|
+
platform: guildInfo.platform,
|
|
2719
|
+
guildId: guildInfo.guildId,
|
|
2720
|
+
players
|
|
2721
|
+
});
|
|
2722
|
+
subscribers.push(subscriber);
|
|
2723
|
+
}
|
|
2724
|
+
if (subscribers.length > 0) {
|
|
2725
|
+
this.ctx.dota2tracker.parsePolling.add(match.id, subscribers);
|
|
2726
|
+
this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.match_tracked", { messageToLogger, match }));
|
|
2266
2727
|
}
|
|
2267
2728
|
}
|
|
2268
|
-
return Array.from(resultMap.values());
|
|
2269
2729
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
{
|
|
2277
|
-
id: "unsigned",
|
|
2278
|
-
userId: "string",
|
|
2279
|
-
guildId: "string",
|
|
2280
|
-
platform: "string",
|
|
2281
|
-
steamId: "integer",
|
|
2282
|
-
nickName: "string",
|
|
2283
|
-
rank: "json"
|
|
2284
|
-
},
|
|
2285
|
-
{ autoInc: true }
|
|
2286
|
-
);
|
|
2287
|
-
if (config.dailyReportSwitch && ctx.cron) {
|
|
2288
|
-
ctx.cron(`0 ${config.dailyReportHours} * * *`, async function() {
|
|
2289
|
-
const oneDayAgo = (0, import_moment.default)().subtract(1, "days").unix();
|
|
2290
|
-
await report(oneDayAgo, "dota2tracker.template.yesterdays_summary", config.dailyReportShowCombi);
|
|
2291
|
-
});
|
|
2292
|
-
}
|
|
2293
|
-
if (config.weeklyReportSwitch && ctx.cron) {
|
|
2294
|
-
ctx.cron(`0 ${config.weeklyReportDayHours[1]} * * ${config.weeklyReportDayHours[0]}`, async function() {
|
|
2295
|
-
const oneWeekAgo = (0, import_moment.default)().subtract(1, "weeks").unix();
|
|
2296
|
-
await report(oneWeekAgo, "dota2tracker.template.last_weeks_summary", config.weeklyReportShowCombi);
|
|
2730
|
+
async detectRankChanges(playersData, activePlayers) {
|
|
2731
|
+
const rankMap = /* @__PURE__ */ new Map();
|
|
2732
|
+
for (const player of playersData) {
|
|
2733
|
+
rankMap.set(player.steamAccount.id, {
|
|
2734
|
+
rank: player.steamAccount.seasonRank,
|
|
2735
|
+
leader: player.steamAccount.seasonLeaderboardRank
|
|
2297
2736
|
});
|
|
2298
2737
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
})).players;
|
|
2310
|
-
const lastMatches = players.map((player) => player.matches[0]).filter((match) => match && match.id).filter((item, index, self) => index === self.findIndex((t) => t.id === item.id)).filter((match) => import_moment.default.unix(match.startDateTime).isAfter((0, import_moment.default)().subtract(1, "days"))).filter((match) => !pendingMatches.some((pendingMatch) => pendingMatch.matchId == match.id));
|
|
2311
|
-
const sendedMatchesIds = [];
|
|
2312
|
-
for await (const sendedMatchesId of ctx.cache.keys("dt_sended_match_id")) {
|
|
2313
|
-
sendedMatchesIds.push(Number(sendedMatchesId));
|
|
2314
|
-
}
|
|
2315
|
-
for (const match of lastMatches.filter((match2) => !sendedMatchesIds.includes(match2.id))) {
|
|
2316
|
-
const tempGuildsByLanguage = {};
|
|
2317
|
-
for (const player of match.players) {
|
|
2318
|
-
const subscribedPlayers = subscribedPlayersInGuild.filter((subscribedPlayer) => subscribedPlayer.steamId === player.steamAccount.id);
|
|
2319
|
-
for (const subscribedPlayer of subscribedPlayers) {
|
|
2320
|
-
if (subscribedPlayer) {
|
|
2321
|
-
const languageTag = await getLanguageTag({ channelId: subscribedPlayer.guildId });
|
|
2322
|
-
if (!tempGuildsByLanguage[languageTag]) {
|
|
2323
|
-
tempGuildsByLanguage[languageTag] = [];
|
|
2324
|
-
}
|
|
2325
|
-
const tempGuild = tempGuildsByLanguage[languageTag].find((guild) => guild.guildId === subscribedPlayer.guildId && guild.platform === subscribedPlayer.platform);
|
|
2326
|
-
if (tempGuild) {
|
|
2327
|
-
tempGuild.players.push(subscribedPlayer);
|
|
2328
|
-
} else {
|
|
2329
|
-
tempGuildsByLanguage[languageTag].push({
|
|
2330
|
-
guildId: subscribedPlayer.guildId,
|
|
2331
|
-
platform: subscribedPlayer.platform,
|
|
2332
|
-
players: [subscribedPlayer]
|
|
2333
|
-
});
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
pendingMatches.push({
|
|
2339
|
-
matchId: match.id,
|
|
2340
|
-
guilds: tempGuildsByLanguage,
|
|
2341
|
-
queryTime: /* @__PURE__ */ new Date(),
|
|
2342
|
-
hasMessage: true
|
|
2343
|
-
});
|
|
2344
|
-
const messageToLogger = [];
|
|
2345
|
-
Object.entries(tempGuildsByLanguage).forEach(([languageTag, guilds]) => {
|
|
2346
|
-
guilds.forEach((guild) => {
|
|
2347
|
-
messageToLogger.push({
|
|
2348
|
-
languageTag,
|
|
2349
|
-
platform: guild.platform,
|
|
2350
|
-
guildId: guild.guildId,
|
|
2351
|
-
players: guild.players.map((player) => ({ nickName: player.nickName, steamId: player.steamId }))
|
|
2352
|
-
});
|
|
2353
|
-
});
|
|
2354
|
-
});
|
|
2355
|
-
ctx.logger.info($t(GlobalLanguageTag, "dota2tracker.logger.match_tracked", { messageToLogger, match }));
|
|
2356
|
-
if (!match.parsedDateTime) {
|
|
2357
|
-
const response = await query("RequestMatchDataAnalysis", {
|
|
2358
|
-
matchId: match.id
|
|
2359
|
-
});
|
|
2360
|
-
ctx.logger.info($t(GlobalLanguageTag, `dota2tracker.logger.parse_request_${response.stratz.matchRetry ? "sent" : "failed"}`, { matchId: match.id }));
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
const rankMap = players.reduce((map, player) => {
|
|
2364
|
-
map[player.steamAccount.id] = {
|
|
2365
|
-
rank: player.steamAccount.seasonRank,
|
|
2366
|
-
leader: player.steamAccount.seasonLeaderboardRank
|
|
2738
|
+
for (let subPlayer of activePlayers) {
|
|
2739
|
+
if (subPlayer.rank.rank !== rankMap.get(subPlayer.steamId)?.rank || subPlayer.rank.leader !== rankMap.get(subPlayer.steamId)?.leader) {
|
|
2740
|
+
if (Object.keys(subPlayer.rank).length != 0) {
|
|
2741
|
+
const ranks = ["prevRank", "currRank"].reduce((acc, key) => {
|
|
2742
|
+
const source = key === "prevRank" ? subPlayer.rank : rankMap.get(subPlayer.steamId);
|
|
2743
|
+
acc[key] = {
|
|
2744
|
+
medal: parseInt(source.rank?.toString().split("")[0] ?? "0"),
|
|
2745
|
+
star: parseInt(source.rank?.toString().split("")[1] ?? "0"),
|
|
2746
|
+
leader: source.leader,
|
|
2747
|
+
inTop100: source.leader ? source.leader <= 10 ? "8c" : source.leader <= 100 ? "8b" : void 0 : void 0
|
|
2367
2748
|
};
|
|
2368
|
-
return
|
|
2749
|
+
return acc;
|
|
2369
2750
|
}, {});
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
name: name2,
|
|
2395
|
-
avatar: guildMember?.avatar ?? players.find((player) => subPlayer.steamId == player.steamAccount.id).steamAccount.avatar,
|
|
2396
|
-
isRising: rankMap[subPlayer.steamId].rank > subPlayer.rank.rank || rankMap[subPlayer.steamId].rank == subPlayer.rank.rank && rankMap[subPlayer.steamId].leader < subPlayer.rank.leader || rankMap[subPlayer.steamId].leader > 0 && subPlayer.rank.leader == null,
|
|
2397
|
-
prevRank,
|
|
2398
|
-
currRank,
|
|
2399
|
-
date: (0, import_moment.default)()
|
|
2400
|
-
},
|
|
2401
|
-
"rank" + (config.rankBroadFun ? "_fun" : ""),
|
|
2402
|
-
"rank" /* Rank */,
|
|
2403
|
-
ctx,
|
|
2404
|
-
languageTag
|
|
2405
|
-
)
|
|
2406
|
-
);
|
|
2407
|
-
await ctx.broadcast([`${subPlayer.platform}:${subPlayer.guildId}`], img);
|
|
2408
|
-
} else {
|
|
2409
|
-
const message = $t(languageTag, "dota2tracker.broadcast.rank_changed", {
|
|
2410
|
-
name: name2,
|
|
2411
|
-
prev: { medal: $t(languageTag, "dota2tracker.template.ranks." + prevRank.medal), star: prevRank.star },
|
|
2412
|
-
curr: { medal: $t(languageTag, "dota2tracker.template.ranks." + currRank.medal), star: currRank.star }
|
|
2413
|
-
});
|
|
2414
|
-
const img = await ctx.puppeteer.render(await genImageHTML(currRank, "rank" + (config.rankBroadFun ? "2" : ""), "rank" /* Rank */, ctx, languageTag));
|
|
2415
|
-
await ctx.broadcast([`${subPlayer.platform}:${subPlayer.guildId}`], message + img);
|
|
2416
|
-
}
|
|
2417
|
-
ctx.logger.info($t(GlobalLanguageTag, "dota2tracker.logger.rank_sent", { platform: subPlayer.platform, guildId: subPlayer.guildId, player: { nickName: subPlayer.nickName, steamId: subPlayer.steamId } }));
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
ctx.database.set("dt_subscribed_players", subPlayer.id, { rank: rankMap[subPlayer.steamId] });
|
|
2751
|
+
const prevRank = ranks["prevRank"];
|
|
2752
|
+
const currRank = ranks["currRank"];
|
|
2753
|
+
if (prevRank.medal !== currRank.medal || prevRank.star !== currRank.star && this.config.rankBroadStar || prevRank.leader !== currRank.leader && this.config.rankBroadLeader) {
|
|
2754
|
+
const guildMember = await this.ctx.bots.find((bot) => bot.platform == subPlayer.platform)?.getGuildMember?.(subPlayer.guildId, subPlayer.userId);
|
|
2755
|
+
const name2 = subPlayer.nickName ?? guildMember?.nick ?? playersData.find((player) => player.steamAccount.id == subPlayer.steamId)?.steamAccount.name ?? String(subPlayer.steamId);
|
|
2756
|
+
const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: subPlayer.guildId });
|
|
2757
|
+
if (this.config.rankBroadFun === true) {
|
|
2758
|
+
const img = await this.ctx.dota2tracker.image.renderToImageByFile(
|
|
2759
|
+
{
|
|
2760
|
+
name: name2,
|
|
2761
|
+
avatar: guildMember?.avatar ?? playersData.find((player) => subPlayer.steamId == player.steamAccount.id).steamAccount.avatar,
|
|
2762
|
+
isRising: rankMap.get(subPlayer.steamId).rank > subPlayer.rank.rank || rankMap.get(subPlayer.steamId).rank == subPlayer.rank.rank && rankMap.get(subPlayer.steamId).leader < subPlayer.rank.leader || rankMap.get(subPlayer.steamId).leader > 0 && subPlayer.rank.leader == null,
|
|
2763
|
+
prevRank,
|
|
2764
|
+
currRank,
|
|
2765
|
+
date: import_luxon6.DateTime.now().toFormat(languageTag === "zh-CN" ? "yyyy/MM/dd HH时mm分" : "yyyy/MM/dd HH:mm")
|
|
2766
|
+
},
|
|
2767
|
+
"rank_fun",
|
|
2768
|
+
"rank" /* Rank */,
|
|
2769
|
+
languageTag
|
|
2770
|
+
);
|
|
2771
|
+
await this.ctx.broadcast([`${subPlayer.platform}:${subPlayer.guildId}`], img);
|
|
2772
|
+
} else {
|
|
2773
|
+
const message = this.ctx.dota2tracker.messageBuilder.buildRankChangedMessage(languageTag, name2, prevRank, currRank);
|
|
2774
|
+
await this.ctx.broadcast([`${subPlayer.platform}:${subPlayer.guildId}`], message);
|
|
2422
2775
|
}
|
|
2776
|
+
this.ctx.dota2tracker.database.setPlayerRank(subPlayer.id, rankMap.get(subPlayer.steamId));
|
|
2777
|
+
this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.rank_sent", { platform: subPlayer.platform, guildId: subPlayer.guildId, player: { nickName: subPlayer.nickName, steamId: subPlayer.steamId } }));
|
|
2423
2778
|
}
|
|
2779
|
+
} else {
|
|
2780
|
+
this.ctx.dota2tracker.database.setPlayerRank(subPlayer.id, rankMap.get(subPlayer.steamId));
|
|
2424
2781
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2787
|
+
// src/app/tasks/parse-polling.task.ts
|
|
2788
|
+
var import_koishi13 = require("koishi");
|
|
2789
|
+
var import_luxon7 = require("luxon");
|
|
2790
|
+
var ParsePollingTask = class extends import_koishi13.Service {
|
|
2791
|
+
static {
|
|
2792
|
+
__name(this, "ParsePollingTask");
|
|
2793
|
+
}
|
|
2794
|
+
pendingMatches = /* @__PURE__ */ new Map();
|
|
2795
|
+
pollingIndex = 0;
|
|
2796
|
+
constructor(ctx) {
|
|
2797
|
+
super(ctx, "dota2tracker.parse-polling", true);
|
|
2798
|
+
this.config = ctx.config;
|
|
2799
|
+
}
|
|
2800
|
+
createSubscriberByCommand(session, languageTag, options) {
|
|
2801
|
+
return {
|
|
2802
|
+
languageTag,
|
|
2803
|
+
relevantPlayers: [],
|
|
2804
|
+
source: "COMMAND",
|
|
2805
|
+
type: session.isDirect ? "PRIVATE" : "CHANNEL",
|
|
2806
|
+
platform: session.platform,
|
|
2807
|
+
selfId: session.selfId,
|
|
2808
|
+
userId: session.userId,
|
|
2809
|
+
channelId: session.channelId,
|
|
2810
|
+
guildId: session.guildId,
|
|
2811
|
+
templateName: options?.templateName
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
createSubscriberByAutomatic(target) {
|
|
2815
|
+
return {
|
|
2816
|
+
languageTag: target.languageTag,
|
|
2817
|
+
relevantPlayers: target.relevantPlayers,
|
|
2818
|
+
source: "AUTOMATIC",
|
|
2819
|
+
type: "CHANNEL",
|
|
2820
|
+
platform: target.platform,
|
|
2821
|
+
channelId: target.channelId,
|
|
2822
|
+
guildId: target.guildId
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
add(matchId, subscribers) {
|
|
2826
|
+
const isNewEntry = !this.pendingMatches.has(matchId);
|
|
2827
|
+
const entry = this.pendingMatches.get(matchId) || { matchId, requestTime: /* @__PURE__ */ new Date(), subscribers: [] };
|
|
2828
|
+
entry.subscribers.push(...subscribers);
|
|
2829
|
+
this.pendingMatches.set(matchId, entry);
|
|
2830
|
+
if (isNewEntry) {
|
|
2831
|
+
this.ctx.dota2tracker.stratzAPI.requestParseMatch(matchId).then((value) => this.logger.info(this.ctx.dota2tracker.i18n.gt(`dota2tracker.logger.parse_request_${value ? "sent" : "failed"}`, { matchId })));
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
async polling() {
|
|
2835
|
+
try {
|
|
2836
|
+
if (this.pendingMatches.size === 0) return;
|
|
2837
|
+
const matches = Array.from(this.pendingMatches.values());
|
|
2838
|
+
if (this.pollingIndex >= matches.length) this.pollingIndex = 0;
|
|
2839
|
+
const pendingMatch = matches[this.pollingIndex];
|
|
2840
|
+
this.pollingIndex++;
|
|
2841
|
+
const timeout = import_luxon7.DateTime.fromJSDate(pendingMatch.requestTime).plus({ minutes: this.config.dataParsingTimeoutMinutes });
|
|
2842
|
+
const needToWait = import_luxon7.DateTime.now() < timeout;
|
|
2843
|
+
const result = await this.ctx.dota2tracker.match.getMatchResult({ matchId: pendingMatch.matchId, requestParse: needToWait });
|
|
2844
|
+
if (result.status === "PENDING") {
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (result.status === "READY") {
|
|
2848
|
+
const subscribersByLang = /* @__PURE__ */ new Map();
|
|
2849
|
+
for (const subscriber of pendingMatch.subscribers) {
|
|
2850
|
+
if (!subscribersByLang.has(subscriber.languageTag)) subscribersByLang.set(subscriber.languageTag, []);
|
|
2851
|
+
subscribersByLang.get(subscriber.languageTag).push(subscriber);
|
|
2852
|
+
}
|
|
2853
|
+
const guildsToLogger = [];
|
|
2854
|
+
for (const [languageTag, subscribers] of subscribersByLang) {
|
|
2855
|
+
const formattedMatchData = await this.ctx.dota2tracker.match.generateMatchData(result.matchData, languageTag);
|
|
2856
|
+
const defaultTemplateSubscribers = subscribers.filter((s) => s.source === "AUTOMATIC" || s.source === "COMMAND" && !s.templateName);
|
|
2857
|
+
const customTemplateSubscribers = subscribers.filter((s) => s.source === "COMMAND" && s.templateName);
|
|
2858
|
+
if (defaultTemplateSubscribers.length > 0) {
|
|
2859
|
+
const defaultImage = await this.ctx.dota2tracker.image.renderToImageByFile(formattedMatchData, this.config.template_match, "match" /* Match */, languageTag);
|
|
2860
|
+
for (const sub of defaultTemplateSubscribers) {
|
|
2861
|
+
const message = this.ctx.dota2tracker.messageBuilder.buildMatchMessage(languageTag, formattedMatchData, sub.relevantPlayers);
|
|
2862
|
+
await this.broadcastMessage(sub, message + defaultImage);
|
|
2863
|
+
guildsToLogger.push({ platform: sub.platform, guildId: sub.channelId, languageTag });
|
|
2864
|
+
}
|
|
2457
2865
|
}
|
|
2866
|
+
for (const sub of customTemplateSubscribers) {
|
|
2867
|
+
const image = await this.ctx.dota2tracker.image.renderToImageByFile(formattedMatchData, sub.source === "COMMAND" ? sub.templateName : this.config.template_match, "match" /* Match */, languageTag);
|
|
2868
|
+
const message = this.ctx.dota2tracker.messageBuilder.buildMatchMessage(languageTag, formattedMatchData, sub.relevantPlayers);
|
|
2869
|
+
await this.broadcastMessage(sub, message + image);
|
|
2870
|
+
guildsToLogger.push({ platform: sub.platform, guildId: sub.channelId, languageTag });
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
this.logger.info(
|
|
2874
|
+
this.ctx.dota2tracker.i18n.gt(`dota2tracker.logger.match_${result.matchData.match.parsedDateTime ? "parsed" : "unparsed"}`, {
|
|
2875
|
+
matchId: result.matchData.match.id,
|
|
2876
|
+
timeout: this.config.dataParsingTimeoutMinutes,
|
|
2877
|
+
guilds: guildsToLogger
|
|
2878
|
+
})
|
|
2879
|
+
);
|
|
2880
|
+
this.ctx.dota2tracker.cache.markMatchAsSended(pendingMatch.matchId);
|
|
2881
|
+
this.pendingMatches.delete(pendingMatch.matchId);
|
|
2882
|
+
}
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.ctx.config);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
async broadcastMessage(subscriber, message) {
|
|
2888
|
+
if (subscriber.type === "PRIVATE") {
|
|
2889
|
+
try {
|
|
2890
|
+
const bot = this.ctx.bots[`${subscriber.platform}:${subscriber.selfId}`];
|
|
2891
|
+
if (!bot) return false;
|
|
2892
|
+
await bot.sendPrivateMessage(subscriber.userId, message);
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
this.logger.error(error);
|
|
2895
|
+
return false;
|
|
2896
|
+
}
|
|
2897
|
+
return true;
|
|
2898
|
+
}
|
|
2899
|
+
const messageIds = await this.ctx.broadcast([`${subscriber.platform}:${subscriber.channelId}`], message);
|
|
2900
|
+
return messageIds.length > 0;
|
|
2901
|
+
}
|
|
2902
|
+
isPending(matchId) {
|
|
2903
|
+
return this.pendingMatches.has(matchId);
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
// src/app/tasks/report.task.ts
|
|
2908
|
+
var import_koishi14 = require("koishi");
|
|
2909
|
+
var import_luxon8 = require("luxon");
|
|
2910
|
+
var ReportTask = class extends import_koishi14.Service {
|
|
2911
|
+
static {
|
|
2912
|
+
__name(this, "ReportTask");
|
|
2913
|
+
}
|
|
2914
|
+
/*
|
|
2915
|
+
还没计划好怎么动这一坨,先原样移植吧。
|
|
2916
|
+
*/
|
|
2917
|
+
constructor(ctx) {
|
|
2918
|
+
super(ctx, "dota2tracker.report", true);
|
|
2919
|
+
this.config = ctx.config;
|
|
2920
|
+
if (this.config.dailyReportSwitch) {
|
|
2921
|
+
ctx.cron(`0 ${this.config.dailyReportHours} * * *`, async () => {
|
|
2922
|
+
try {
|
|
2923
|
+
const oneDayAgo = import_luxon8.DateTime.now().minus({ days: 1 }).toSeconds();
|
|
2924
|
+
await this.report(oneDayAgo, "dota2tracker.template.yesterdays_summary", this.config.dailyReportShowCombi);
|
|
2925
|
+
} catch (error) {
|
|
2926
|
+
handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.config);
|
|
2458
2927
|
}
|
|
2459
2928
|
});
|
|
2460
2929
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2930
|
+
if (this.config.weeklyReportSwitch) {
|
|
2931
|
+
ctx.cron(`0 ${this.config.weeklyReportDayHours[1]} * * ${this.config.weeklyReportDayHours[0]}`, async () => {
|
|
2932
|
+
try {
|
|
2933
|
+
const oneWeekAgo = import_luxon8.DateTime.now().minus({ weeks: 1 }).toSeconds();
|
|
2934
|
+
await this.report(oneWeekAgo, "dota2tracker.template.last_weeks_summary", this.config.weeklyReportShowCombi);
|
|
2935
|
+
} catch (error) {
|
|
2936
|
+
handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.config);
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async report(timeAgo, titleKey, showCombi) {
|
|
2942
|
+
const subscribedGuilds = await this.ctx.database.get("dt_subscribed_guilds", void 0);
|
|
2943
|
+
const subscribedPlayersInGuild = (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedGuilds.some((guild) => guild.guildId == player.guildId));
|
|
2944
|
+
const steamIds = subscribedPlayersInGuild.map((player) => player.steamId).filter((value, index, self) => self.indexOf(value) === index);
|
|
2945
|
+
const players = (await this.ctx.dota2tracker.stratzAPI.queryPlayersMatchesForDaily(steamIds, timeAgo)).players.filter((player) => player.matches?.length > 0);
|
|
2469
2946
|
const matches = players.map((player) => player.matches.map((match) => match)).flat().filter((item, index, self) => index === self.findIndex((t) => t.id === item.id));
|
|
2470
2947
|
for (let subPlayer of subscribedPlayersInGuild) {
|
|
2471
2948
|
let player = players.find((player2) => subPlayer.steamId == player2.steamAccount.id);
|
|
2472
2949
|
if (!player) continue;
|
|
2473
2950
|
let guildMember;
|
|
2474
2951
|
try {
|
|
2475
|
-
guildMember = await ctx.bots.find((bot) => bot.platform == subPlayer.platform)?.getGuildMember(subPlayer.guildId, subPlayer.userId);
|
|
2952
|
+
guildMember = await this.ctx.bots.find((bot) => bot.platform == subPlayer.platform)?.getGuildMember(subPlayer.guildId, subPlayer.userId);
|
|
2476
2953
|
} catch (error) {
|
|
2477
|
-
|
|
2954
|
+
this.logger.warn(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.fetch_guilds_failed") + error);
|
|
2478
2955
|
}
|
|
2479
|
-
|
|
2956
|
+
player.name = subPlayer.nickName || (guildMember?.nick ?? players.find((player2) => player2.steamAccount.id == subPlayer.steamId)?.steamAccount.name);
|
|
2480
2957
|
player.winCount = player.matches.filter((match) => match.didRadiantWin == match.players.find((innerPlayer) => innerPlayer.steamAccount.id == player.steamAccount.id).isRadiant).length;
|
|
2481
2958
|
player.loseCount = player.matches.length - player.winCount;
|
|
2482
2959
|
player.avgKills = roundToDecimalPlaces(player.matches.reduce((acc, match) => acc + match.players.find((innerPlayer) => innerPlayer.steamAccount.id == player.steamAccount.id).kills, 0) / player.matches.length, 2);
|
|
@@ -2513,118 +2990,445 @@ async function apply(ctx, config) {
|
|
|
2513
2990
|
});
|
|
2514
2991
|
const combinations = Array.from(combinationsMap.values());
|
|
2515
2992
|
try {
|
|
2516
|
-
const languageTag = await getLanguageTag({ channelId: guild.guildId });
|
|
2517
|
-
await ctx.broadcast(
|
|
2993
|
+
const languageTag = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: guild.guildId });
|
|
2994
|
+
await this.ctx.broadcast(
|
|
2518
2995
|
[`${guild.platform}:${guild.guildId}`],
|
|
2519
|
-
await ctx.
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
ctx,
|
|
2534
|
-
await getLanguageTag({ channelId: guild.guildId })
|
|
2535
|
-
)
|
|
2996
|
+
await this.ctx.dota2tracker.image.renderToImageByFile(
|
|
2997
|
+
{
|
|
2998
|
+
title: this.ctx.dota2tracker.i18n.$t(languageTag, titleKey),
|
|
2999
|
+
players: currentsubscribedPlayers.sort((a, b) => {
|
|
3000
|
+
if (a.matches.length > b.matches.length) return -1;
|
|
3001
|
+
else if (a.matches.length < b.matches.length) return 1;
|
|
3002
|
+
else return a.steamAccount.id - b.steamAccount.id;
|
|
3003
|
+
}),
|
|
3004
|
+
combinations,
|
|
3005
|
+
showCombi
|
|
3006
|
+
},
|
|
3007
|
+
"daily",
|
|
3008
|
+
"report" /* Report */,
|
|
3009
|
+
languageTag
|
|
2536
3010
|
)
|
|
2537
3011
|
);
|
|
2538
|
-
|
|
3012
|
+
this.logger.info(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.report_sent", { title: this.ctx.dota2tracker.i18n.$t(languageTag, titleKey), guildId: guild.guildId, platform: guild.platform }));
|
|
2539
3013
|
} catch (error) {
|
|
2540
|
-
|
|
3014
|
+
this.logger.error(error);
|
|
2541
3015
|
}
|
|
2542
3016
|
}
|
|
2543
3017
|
}
|
|
2544
3018
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
3019
|
+
};
|
|
3020
|
+
|
|
3021
|
+
// src/app/commands/channel.command.ts
|
|
3022
|
+
function registerSubscibeCommand(ctx) {
|
|
3023
|
+
ctx.command("dota2tracker.subscribe").alias("订阅本群").action(async ({ session }) => {
|
|
3024
|
+
if (!session.isDirect) {
|
|
3025
|
+
if (await ctx.dota2tracker.database.isChannelSubscribed(session)) return session.text(".subscribed");
|
|
3026
|
+
else {
|
|
3027
|
+
ctx.dota2tracker.database.subscribeChannel(session);
|
|
3028
|
+
return session.text(".subscribe_success");
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
ctx.command("dota2tracker.unsubscribe").alias("取消订阅").action(async ({ session }) => {
|
|
3033
|
+
if (!session.isDirect) {
|
|
3034
|
+
if (await ctx.dota2tracker.database.isChannelSubscribed(session)) {
|
|
3035
|
+
ctx.dota2tracker.database.unSubscribeChannel(session);
|
|
3036
|
+
return session.text(".unsubscribe_success");
|
|
3037
|
+
} else return session.text(".not_subscribed");
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
__name(registerSubscibeCommand, "registerSubscibeCommand");
|
|
3042
|
+
|
|
3043
|
+
// src/app/commands/help.command.ts
|
|
3044
|
+
function registerHelpCommand(ctx) {
|
|
3045
|
+
ctx.command("dota2tracker.help").alias("DOTA2指南").alias("DOTA2帮助").alias("DOTA2说明").action(async ({ session }) => {
|
|
3046
|
+
return session.text(".content");
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
__name(registerHelpCommand, "registerHelpCommand");
|
|
3050
|
+
|
|
3051
|
+
// src/app/commands/_helper.ts
|
|
3052
|
+
async function resolvePlayerAndHandleErrors(ctx, session, input) {
|
|
3053
|
+
if (session.isDirect && !input) {
|
|
3054
|
+
session.send(session.text("commands.dota2tracker.common.messages.user_not_in_group"));
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
3057
|
+
const result = await ctx.dota2tracker.player.resolveSteamId(session, input);
|
|
3058
|
+
if (result.success === false) {
|
|
3059
|
+
switch (result.reason) {
|
|
3060
|
+
case "NOT_BINDED":
|
|
3061
|
+
session.send(session.text("commands.dota2tracker.common.messages.user_not_binded_in_channel"));
|
|
3062
|
+
break;
|
|
3063
|
+
case "NICKNAME_NOT_FOUND":
|
|
3064
|
+
case "INVALID_INPUT":
|
|
3065
|
+
session.send(session.text("commands.dota2tracker.common.messages.invalid_input_include_steam_id"));
|
|
3066
|
+
break;
|
|
3067
|
+
}
|
|
3068
|
+
return null;
|
|
3069
|
+
}
|
|
3070
|
+
return Number(result.steamId);
|
|
3071
|
+
}
|
|
3072
|
+
__name(resolvePlayerAndHandleErrors, "resolvePlayerAndHandleErrors");
|
|
3073
|
+
|
|
3074
|
+
// src/app/commands/hero-of-the-day.command.ts
|
|
3075
|
+
var import_luxon9 = require("luxon");
|
|
3076
|
+
function registerHeroOfTheDayCommand(ctx) {
|
|
3077
|
+
ctx.command("dota2tracker.hero-of-the-day <input_data>").alias("今日英雄").option("days", "-d <value:number>").action(async ({ session, options }, input_data) => {
|
|
3078
|
+
const steamId = await resolvePlayerAndHandleErrors(ctx, session, input_data);
|
|
3079
|
+
if (steamId === null) return;
|
|
3080
|
+
const days = clamp(options.days, 1, 180, 30);
|
|
3081
|
+
const result = await ctx.dota2tracker.stratzAPI.queryPlayerPerformanceForHeroRecommendation({
|
|
3082
|
+
steamAccountId: steamId,
|
|
3083
|
+
recentDateTime: import_luxon9.DateTime.now().minus({ days }).toUnixInteger()
|
|
3084
|
+
});
|
|
3085
|
+
const recommendationPromise = ctx.dota2tracker.player.getHeroRecommendation(steamId, result.player);
|
|
3086
|
+
const metaPromise = ctx.dota2tracker.hero.getWeeklyHeroMeta(PlayerService.estimateWeightedRank(result.player));
|
|
3087
|
+
const [recommendation, weeklyHeroMeta] = await Promise.all([recommendationPromise, metaPromise]);
|
|
3088
|
+
const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3089
|
+
const message = ctx.dota2tracker.messageBuilder.buildHeroOfTheDayMessage(languageTag, recommendation, weeklyHeroMeta);
|
|
3090
|
+
return message;
|
|
2547
3091
|
});
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
if (
|
|
2555
|
-
|
|
2556
|
-
|
|
3092
|
+
}
|
|
3093
|
+
__name(registerHeroOfTheDayCommand, "registerHeroOfTheDayCommand");
|
|
3094
|
+
|
|
3095
|
+
// src/app/commands/query-hero.command.ts
|
|
3096
|
+
function registerQueryHeroCommand(ctx) {
|
|
3097
|
+
ctx.command("dota2tracker.query-hero <input_data>").option("random", "-r").alias("查询英雄").action(async ({ session, options }, input_data) => {
|
|
3098
|
+
if (input_data) {
|
|
3099
|
+
await session.send(session.text(".querying_hero"));
|
|
3100
|
+
const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3101
|
+
const heroData = await ctx.dota2tracker.hero.getHeroDetails(input_data, languageTag, options.random);
|
|
3102
|
+
if (!heroData) return session.text(".not_found");
|
|
3103
|
+
const image = await ctx.dota2tracker.image.renderToImageByFile(heroData, ctx.config.template_hero, "hero" /* Hero */, languageTag);
|
|
3104
|
+
const message = ctx.dota2tracker.messageBuilder.buildHeroMessage(heroData);
|
|
3105
|
+
await session.send(message + image);
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
__name(registerQueryHeroCommand, "registerQueryHeroCommand");
|
|
3110
|
+
|
|
3111
|
+
// src/app/commands/query-item.command.ts
|
|
3112
|
+
function registerQueryItemCommand(ctx) {
|
|
3113
|
+
ctx.command("dota2tracker.query-item").alias("查询物品").action(async ({ session }, input_data) => {
|
|
3114
|
+
if (!input_data) {
|
|
3115
|
+
if (ctx.config.showItemListAtTooMuchItems) {
|
|
3116
|
+
await session.send(session.text(".querying_item"));
|
|
3117
|
+
const languageTag2 = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3118
|
+
const allItems = await ctx.dota2tracker.item.getItemList({ languageTag: languageTag2, onCacheMissTip: /* @__PURE__ */ __name(() => session.send(session.text(".cache_building")), "onCacheMissTip") });
|
|
3119
|
+
const image = await ctx.dota2tracker.image.renderToImageByFile(allItems, "itemlist", "item" /* Item */, languageTag2);
|
|
3120
|
+
await session.send(image);
|
|
3121
|
+
} else {
|
|
3122
|
+
await session.send(session.text(".empty_input"));
|
|
2557
3123
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
await session.send(session.text(".querying_item"));
|
|
3127
|
+
const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3128
|
+
const itemList = await ctx.dota2tracker.item.getItemList({ languageTag, onCacheMissTip: /* @__PURE__ */ __name(() => session.send(session.text(".cache_building")), "onCacheMissTip") });
|
|
3129
|
+
const matchedItemList = ctx.dota2tracker.item.searchItems(itemList, input_data, languageTag, ctx.config);
|
|
3130
|
+
if (matchedItemList.length === 0) {
|
|
3131
|
+
await session.send(session.text(".not_found"));
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if (matchedItemList.length > ctx.config.maxSendItemCount) {
|
|
3135
|
+
await session.send(session.text(".too_many_items", { count: matchedItemList.length, max: ctx.config.maxSendItemCount }));
|
|
3136
|
+
if (ctx.config.showItemListAtTooMuchItems) {
|
|
3137
|
+
const image = await ctx.dota2tracker.image.renderToImageByFile(matchedItemList, "itemlist", "item" /* Item */, languageTag);
|
|
3138
|
+
await session.send(image);
|
|
2560
3139
|
}
|
|
2561
|
-
return
|
|
3140
|
+
return;
|
|
2562
3141
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
ImageType,
|
|
2575
|
-
ImageFormat,
|
|
2576
|
-
dotaconstants: dotaconstants2,
|
|
2577
|
-
moment: import_moment.default,
|
|
2578
|
-
eh: escapeHTML,
|
|
2579
|
-
$t: templateI18nHelper,
|
|
2580
|
-
languageTag,
|
|
2581
|
-
Random: import_koishi3.Random,
|
|
2582
|
-
fontFamily: ctx2.config.templateFonts
|
|
2583
|
-
};
|
|
2584
|
-
function templateI18nHelper(key, param) {
|
|
2585
|
-
return $t(languageTag, key, param);
|
|
3142
|
+
await session.send(session.text(".finded_items", { items: matchedItemList }));
|
|
3143
|
+
for (const basicItemInfo of matchedItemList) {
|
|
3144
|
+
try {
|
|
3145
|
+
const itemDetails = await ctx.dota2tracker.item.getItemDetails(basicItemInfo.id, languageTag);
|
|
3146
|
+
const finalItemData = { ...itemDetails, ...basicItemInfo };
|
|
3147
|
+
const image = await ctx.dota2tracker.image.renderToImageByFile(finalItemData, "item", "item" /* Item */, languageTag);
|
|
3148
|
+
await session.send(image);
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
ctx.logger.error(`查询物品[${basicItemInfo.name_loc}]详情失败:`, error);
|
|
3151
|
+
await session.send(session.text(".query_item_failed", { name: basicItemInfo.name_loc }));
|
|
3152
|
+
}
|
|
2586
3153
|
}
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
__name(registerQueryItemCommand, "registerQueryItemCommand");
|
|
3157
|
+
|
|
3158
|
+
// src/app/commands/query-match.command.ts
|
|
3159
|
+
function registerQueryMatchCommand(ctx) {
|
|
3160
|
+
ctx.command("dota2tracker.query-match <match_id>").alias("查询比赛").option("parse", "-p").option("template", "-t <value:string>").action(async ({ session, options }, match_id) => {
|
|
3161
|
+
if (!match_id) return session.text(".empty_input");
|
|
3162
|
+
if (!/^\d{1,11}$/.test(match_id)) return session.text(".match_id_invalid");
|
|
3163
|
+
await session.send(session.text(".querying_match"));
|
|
3164
|
+
return await handleQueryMatchCommand(ctx, ctx.config, session, options, match_id);
|
|
3165
|
+
});
|
|
3166
|
+
ctx.command("dota2tracker.query-recent-match [input_data]").alias("查询最近比赛").option("parse", "-p").option("template", "-t <value:string>").action(async ({ session, options }, input_data) => {
|
|
3167
|
+
if (session.guild || !session.guild && input_data) {
|
|
3168
|
+
const steamId = await resolvePlayerAndHandleErrors(ctx, session, input_data);
|
|
3169
|
+
if (steamId === null) return;
|
|
3170
|
+
session.send(session.text(".querying_match"));
|
|
3171
|
+
const lastMatchId = await ctx.dota2tracker.player.getLastMatchId(Number(steamId));
|
|
3172
|
+
if (!lastMatchId?.matchId) return session.text(".query_failed");
|
|
3173
|
+
if (lastMatchId.isAnonymous) return session.text(".is_anonymous");
|
|
3174
|
+
return await handleQueryMatchCommand(ctx, ctx.config, session, options, lastMatchId.matchId);
|
|
3175
|
+
} else {
|
|
3176
|
+
session.send(session.text(".user_not_in_group"));
|
|
2597
3177
|
}
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
__name(registerQueryMatchCommand, "registerQueryMatchCommand");
|
|
3181
|
+
async function handleQueryMatchCommand(ctx, config, session, options, matchId) {
|
|
3182
|
+
const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3183
|
+
const result = await ctx.dota2tracker.match.getMatchResult({ matchId: Number(matchId), requestParse: options.parse });
|
|
3184
|
+
if (result.status === "PENDING") {
|
|
3185
|
+
const subscriber = ctx.dota2tracker.parsePolling.createSubscriberByCommand(session, languageTag, { templateName: options?.template });
|
|
3186
|
+
ctx.dota2tracker.parsePolling.add(result.matchId, [subscriber]);
|
|
3187
|
+
return session.text(".waiting_for_parse");
|
|
3188
|
+
} else if (result.status === "NOT_FOUND") {
|
|
3189
|
+
return session.text(".query_failed");
|
|
3190
|
+
} else {
|
|
3191
|
+
const formattedMatchData = await ctx.dota2tracker.match.generateMatchData(result.matchData, languageTag);
|
|
3192
|
+
const message = ctx.dota2tracker.messageBuilder.buildMatchMessage(languageTag, formattedMatchData, []);
|
|
3193
|
+
const image = await ctx.dota2tracker.image.renderToImageByFile(formattedMatchData, options.template || config.template_match, "match" /* Match */, languageTag);
|
|
3194
|
+
await ctx.dota2tracker.image.renderToImageByFile(formattedMatchData, options.template || config.template_match, "match" /* Match */, languageTag);
|
|
3195
|
+
return message + image;
|
|
2598
3196
|
}
|
|
2599
|
-
__name(genImageHTML, "genImageHTML");
|
|
2600
3197
|
}
|
|
2601
|
-
__name(
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
};
|
|
2612
|
-
return escape[match];
|
|
3198
|
+
__name(handleQueryMatchCommand, "handleQueryMatchCommand");
|
|
3199
|
+
|
|
3200
|
+
// src/app/commands/query-members.command.ts
|
|
3201
|
+
function registerQueryMembersCommand(ctx) {
|
|
3202
|
+
ctx.command("dota2tracker.query-members").alias("查询群友").action(async ({ session }) => {
|
|
3203
|
+
if (!session.isDirect) {
|
|
3204
|
+
const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3205
|
+
const members = await ctx.dota2tracker.player.getMembersInChannel(session);
|
|
3206
|
+
return await ctx.dota2tracker.messageBuilder.buildMembersMessage(members, languageTag);
|
|
3207
|
+
}
|
|
2613
3208
|
});
|
|
2614
3209
|
}
|
|
2615
|
-
__name(
|
|
2616
|
-
|
|
3210
|
+
__name(registerQueryMembersCommand, "registerQueryMembersCommand");
|
|
3211
|
+
|
|
3212
|
+
// src/app/commands/query-player.command.ts
|
|
3213
|
+
function registerQueryPlayerCommand(ctx) {
|
|
3214
|
+
ctx.command("dota2tracker.query-player <input_data>").option("hero", "-o <value:string>").alias("查询玩家").action(async ({ session, options }, input_data) => {
|
|
3215
|
+
if (session.guild || !session.guild && input_data) {
|
|
3216
|
+
const steamId = await resolvePlayerAndHandleErrors(ctx, session, input_data);
|
|
3217
|
+
if (steamId === null) return;
|
|
3218
|
+
session.send(session.text(".querying_player"));
|
|
3219
|
+
const heroId = ctx.dota2tracker.i18n.findHeroIdInLocale(options.hero);
|
|
3220
|
+
const languageTag = await ctx.dota2tracker.i18n.getLanguageTag({ session });
|
|
3221
|
+
const formattedPlayerData = await ctx.dota2tracker.player.getFormattedPlayerData(steamId, heroId, languageTag);
|
|
3222
|
+
const image = await ctx.dota2tracker.image.renderToImageByFile(formattedPlayerData, ctx.config.template_player, "player" /* Player */, languageTag);
|
|
3223
|
+
const message = ctx.dota2tracker.messageBuilder.buildPlayerMessage(steamId);
|
|
3224
|
+
return message + image;
|
|
3225
|
+
} else {
|
|
3226
|
+
return session.text("commands.dota2tracker.common.messages.user_not_in_group");
|
|
3227
|
+
}
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
__name(registerQueryPlayerCommand, "registerQueryPlayerCommand");
|
|
3231
|
+
|
|
3232
|
+
// src/app/commands/user.command.ts
|
|
3233
|
+
function registerUserCommand(ctx) {
|
|
3234
|
+
ctx.command("dota2tracker.bind <steam_id> [nick_name]").alias("绑定").action(async ({ session }, steam_id, nick_name) => {
|
|
3235
|
+
if (!session.isDirect) {
|
|
3236
|
+
if (!steam_id || !/^\d{1,11}$/.test(steam_id)) {
|
|
3237
|
+
return session.text(".steam_id_invalid");
|
|
3238
|
+
}
|
|
3239
|
+
if (!/^(?:.{1,20})?$/.test(nick_name ?? "")) {
|
|
3240
|
+
return session.text(".nick_name_too_long");
|
|
3241
|
+
}
|
|
3242
|
+
const sessionPlayer = await ctx.dota2tracker.database.getBindedUser(session);
|
|
3243
|
+
if (sessionPlayer) {
|
|
3244
|
+
return session.text(".already_binded", sessionPlayer);
|
|
3245
|
+
}
|
|
3246
|
+
try {
|
|
3247
|
+
let verifyRessult = await ctx.dota2tracker.player.validateSteamId(steam_id);
|
|
3248
|
+
switch (verifyRessult.status) {
|
|
3249
|
+
case "VALID":
|
|
3250
|
+
session.send(session.text(".bind_success", { userId: session.event.user.id, nickName: nick_name || "", steamId: steam_id }) + (verifyRessult.isAnonymous ? "\n" + session.text(".is_anonymous") : ""));
|
|
3251
|
+
ctx.dota2tracker.database.bindUser(session, steam_id, nick_name);
|
|
3252
|
+
break;
|
|
3253
|
+
default:
|
|
3254
|
+
session.send(session.text(`.bind_failed`, [session.text(verifyRessult.reason)]));
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
} catch (error) {
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
ctx.command("dota2tracker.unbind").alias("取消绑定").action(async ({ session }) => {
|
|
3262
|
+
if (!session.isDirect) {
|
|
3263
|
+
const sessionPlayer = await ctx.dota2tracker.database.getBindedUser(session);
|
|
3264
|
+
if (sessionPlayer) {
|
|
3265
|
+
await ctx.database.remove("dt_subscribed_players", sessionPlayer.id);
|
|
3266
|
+
return session.text(".unbind_success");
|
|
3267
|
+
} else {
|
|
3268
|
+
return session.text(".not_binded");
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
ctx.command("dota2tracker.rename <nick_name>").alias("改名").action(async ({ session }, nick_name) => {
|
|
3273
|
+
if (!session.isDirect) {
|
|
3274
|
+
const sessionPlayer = await ctx.dota2tracker.database.getBindedUser(session);
|
|
3275
|
+
if (sessionPlayer) {
|
|
3276
|
+
if (!nick_name) {
|
|
3277
|
+
return session.text(".emtpy_input");
|
|
3278
|
+
}
|
|
3279
|
+
if (!/^.{1,20}$/.test(nick_name ?? "")) {
|
|
3280
|
+
return session.text(".nick_name_too_long");
|
|
3281
|
+
}
|
|
3282
|
+
if (nick_name === sessionPlayer.nickName) {
|
|
3283
|
+
return session.text(".nick_name_same");
|
|
3284
|
+
}
|
|
3285
|
+
await ctx.database.set("dt_subscribed_players", sessionPlayer.id, { nickName: nick_name });
|
|
3286
|
+
return session.text(".rename_success", { nick_name });
|
|
3287
|
+
} else {
|
|
3288
|
+
return session.text(".not_binded");
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
__name(registerUserCommand, "registerUserCommand");
|
|
3294
|
+
|
|
3295
|
+
// src/config.ts
|
|
3296
|
+
var import_koishi15 = require("koishi");
|
|
3297
|
+
var import_fs3 = __toESM(require("fs"));
|
|
3298
|
+
var import_path3 = __toESM(require("path"));
|
|
3299
|
+
|
|
3300
|
+
// require("./locales/**/*.schema.yml") in src/config.ts
|
|
3301
|
+
var globRequire_locales_schema_yml = __glob({
|
|
3302
|
+
"./locales/en-US.schema.yml": () => require_en_US_schema(),
|
|
3303
|
+
"./locales/zh-CN.schema.yml": () => require_zh_CN_schema()
|
|
3304
|
+
});
|
|
3305
|
+
|
|
3306
|
+
// src/config.ts
|
|
3307
|
+
var pluginDir = import_path3.default.resolve(__dirname, "..");
|
|
3308
|
+
var Config = import_koishi15.Schema.intersect([
|
|
3309
|
+
import_koishi15.Schema.object({
|
|
3310
|
+
STRATZ_API_TOKEN: import_koishi15.Schema.string().required().role("secret"),
|
|
3311
|
+
dataParsingTimeoutMinutes: import_koishi15.Schema.number().default(60).min(0).max(1440),
|
|
3312
|
+
proxyAddress: import_koishi15.Schema.string(),
|
|
3313
|
+
suppressStratzNetworkErrors: import_koishi15.Schema.boolean().default(false)
|
|
3314
|
+
}).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.base, acc), {})),
|
|
3315
|
+
import_koishi15.Schema.intersect([
|
|
3316
|
+
import_koishi15.Schema.object({
|
|
3317
|
+
useHeroNicknames: import_koishi15.Schema.boolean().default(true),
|
|
3318
|
+
urlInMessageType: import_koishi15.Schema.array(import_koishi15.Schema.union([import_koishi15.Schema.const("match"), import_koishi15.Schema.const("player"), import_koishi15.Schema.const("hero")])).role("checkbox"),
|
|
3319
|
+
maxSendItemCount: import_koishi15.Schema.number().default(5).min(1).max(10),
|
|
3320
|
+
showItemListAtTooMuchItems: import_koishi15.Schema.boolean().default(true),
|
|
3321
|
+
customItemAlias: import_koishi15.Schema.array(
|
|
3322
|
+
import_koishi15.Schema.object({
|
|
3323
|
+
keyword: import_koishi15.Schema.string().required(),
|
|
3324
|
+
alias: import_koishi15.Schema.string().required()
|
|
3325
|
+
})
|
|
3326
|
+
).default([]).role("table"),
|
|
3327
|
+
rankBroadSwitch: import_koishi15.Schema.boolean().default(false)
|
|
3328
|
+
}).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.message, acc), {})),
|
|
3329
|
+
import_koishi15.Schema.union([
|
|
3330
|
+
import_koishi15.Schema.object({
|
|
3331
|
+
rankBroadSwitch: import_koishi15.Schema.const(true).required(),
|
|
3332
|
+
rankBroadStar: import_koishi15.Schema.boolean().default(true),
|
|
3333
|
+
rankBroadLeader: import_koishi15.Schema.boolean().default(true),
|
|
3334
|
+
rankBroadFun: import_koishi15.Schema.boolean().default(false)
|
|
3335
|
+
}),
|
|
3336
|
+
import_koishi15.Schema.object({})
|
|
3337
|
+
]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.message, acc), {}))
|
|
3338
|
+
]),
|
|
3339
|
+
import_koishi15.Schema.intersect([
|
|
3340
|
+
import_koishi15.Schema.object({
|
|
3341
|
+
dailyReportSwitch: import_koishi15.Schema.boolean().default(false)
|
|
3342
|
+
}).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})),
|
|
3343
|
+
import_koishi15.Schema.union([
|
|
3344
|
+
import_koishi15.Schema.object({
|
|
3345
|
+
dailyReportSwitch: import_koishi15.Schema.const(true).required(),
|
|
3346
|
+
dailyReportHours: import_koishi15.Schema.number().min(0).max(23).default(6),
|
|
3347
|
+
dailyReportShowCombi: import_koishi15.Schema.boolean().default(true)
|
|
3348
|
+
}),
|
|
3349
|
+
import_koishi15.Schema.object({})
|
|
3350
|
+
]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})),
|
|
3351
|
+
import_koishi15.Schema.object({
|
|
3352
|
+
weeklyReportSwitch: import_koishi15.Schema.boolean().default(false)
|
|
3353
|
+
}).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {})).description(void 0),
|
|
3354
|
+
import_koishi15.Schema.union([
|
|
3355
|
+
import_koishi15.Schema.object({
|
|
3356
|
+
weeklyReportSwitch: import_koishi15.Schema.const(true).required(),
|
|
3357
|
+
weeklyReportDayHours: import_koishi15.Schema.tuple([import_koishi15.Schema.number().min(1).max(7), import_koishi15.Schema.number().min(0).max(23)]).default([1, 10]),
|
|
3358
|
+
weeklyReportShowCombi: import_koishi15.Schema.boolean().default(true)
|
|
3359
|
+
}),
|
|
3360
|
+
import_koishi15.Schema.object({})
|
|
3361
|
+
]).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.report, acc), {}))
|
|
3362
|
+
]),
|
|
3363
|
+
import_koishi15.Schema.object({
|
|
3364
|
+
template_match: import_koishi15.Schema.union([...readDirectoryFilesSync(import_path3.default.join(pluginDir, "template", "match"))]).default("match_1"),
|
|
3365
|
+
template_player: import_koishi15.Schema.union([...readDirectoryFilesSync(import_path3.default.join(pluginDir, "template", "player"))]).default("player_1"),
|
|
3366
|
+
template_hero: import_koishi15.Schema.union([...readDirectoryFilesSync(import_path3.default.join(pluginDir, "template", "hero"))]).default("hero_1"),
|
|
3367
|
+
playerRankEstimate: import_koishi15.Schema.boolean().default(true),
|
|
3368
|
+
templateFonts: import_koishi15.Schema.array(String).default([]).role("table")
|
|
3369
|
+
}).i18n(Object.keys(LanguageTags).reduce((acc, cur) => (acc[cur] = globRequire_locales_schema_yml(`./locales/${cur}.schema.yml`)._config.template, acc), {}))
|
|
3370
|
+
]);
|
|
3371
|
+
function readDirectoryFilesSync(directoryPath) {
|
|
2617
3372
|
try {
|
|
2618
|
-
|
|
3373
|
+
const files = import_fs3.default.readdirSync(directoryPath);
|
|
3374
|
+
const fileNames = files.filter((file) => import_path3.default.extname(file).toLowerCase() === ".ejs").map((file) => import_path3.default.basename(file, ".ejs"));
|
|
3375
|
+
return fileNames;
|
|
2619
3376
|
} catch (error) {
|
|
2620
|
-
|
|
3377
|
+
console.error("Error reading directory:", error);
|
|
3378
|
+
return [];
|
|
2621
3379
|
}
|
|
2622
3380
|
}
|
|
2623
|
-
__name(
|
|
3381
|
+
__name(readDirectoryFilesSync, "readDirectoryFilesSync");
|
|
3382
|
+
|
|
3383
|
+
// src/index.ts
|
|
3384
|
+
var name = "dota2tracker";
|
|
3385
|
+
var usage = "";
|
|
3386
|
+
var inject = {
|
|
3387
|
+
required: ["http", "database", "puppeteer", "cache"],
|
|
3388
|
+
optional: ["cron"]
|
|
3389
|
+
};
|
|
3390
|
+
var pluginDir2 = import_path4.default.resolve(__dirname, "..");
|
|
3391
|
+
var pluginVersion = require(import_path4.default.join(pluginDir2, "package.json")).version;
|
|
3392
|
+
async function apply(ctx, config) {
|
|
3393
|
+
ctx.dota2tracker = {};
|
|
3394
|
+
ctx.dota2tracker.i18n = new I18NService(ctx);
|
|
3395
|
+
ctx.dota2tracker.image = new ImageRenderer(ctx, pluginDir2);
|
|
3396
|
+
ctx.dota2tracker.messageBuilder = new MessageBuilder(ctx);
|
|
3397
|
+
ctx.dota2tracker.match = new MatchService(ctx, pluginVersion);
|
|
3398
|
+
ctx.dota2tracker.player = new PlayerService(ctx);
|
|
3399
|
+
if (ctx.cron) {
|
|
3400
|
+
ctx.dota2tracker.matchWatcher = new MatchWatcherTask(ctx);
|
|
3401
|
+
ctx.dota2tracker.parsePolling = new ParsePollingTask(ctx);
|
|
3402
|
+
ctx.dota2tracker.report = new ReportTask(ctx);
|
|
3403
|
+
ctx.cron("* * * * *", async () => {
|
|
3404
|
+
await ctx.dota2tracker.matchWatcher.discovery();
|
|
3405
|
+
await ctx.dota2tracker.parsePolling.polling();
|
|
3406
|
+
});
|
|
3407
|
+
} else {
|
|
3408
|
+
ctx.logger.info(ctx.dota2tracker.i18n.gt("dota2tracker.logger.cron_not_enabled"));
|
|
3409
|
+
}
|
|
3410
|
+
ctx.dota2tracker.hero = new HeroService(ctx);
|
|
3411
|
+
ctx.dota2tracker.item = new ItemService(ctx);
|
|
3412
|
+
ctx.dota2tracker.cache = new CacheService(ctx);
|
|
3413
|
+
ctx.dota2tracker.database = new DatabaseService(ctx);
|
|
3414
|
+
ctx.dota2tracker.valveAPI = new ValveAPI(ctx);
|
|
3415
|
+
ctx.dota2tracker.stratzAPI = new StratzAPI(ctx, pluginDir2);
|
|
3416
|
+
ctx.dota2tracker = ctx.dota2tracker;
|
|
3417
|
+
usage = await ctx.dota2tracker.i18n.generateUsage();
|
|
3418
|
+
registerHelpCommand(ctx);
|
|
3419
|
+
registerSubscibeCommand(ctx);
|
|
3420
|
+
registerUserCommand(ctx);
|
|
3421
|
+
registerQueryMembersCommand(ctx);
|
|
3422
|
+
registerQueryMatchCommand(ctx);
|
|
3423
|
+
registerQueryPlayerCommand(ctx);
|
|
3424
|
+
registerQueryHeroCommand(ctx);
|
|
3425
|
+
registerQueryItemCommand(ctx);
|
|
3426
|
+
registerHeroOfTheDayCommand(ctx);
|
|
3427
|
+
}
|
|
3428
|
+
__name(apply, "apply");
|
|
2624
3429
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2625
3430
|
0 && (module.exports = {
|
|
2626
3431
|
Config,
|
|
2627
|
-
GraphqlLanguageEnum,
|
|
2628
3432
|
apply,
|
|
2629
3433
|
inject,
|
|
2630
3434
|
name,
|