@sjtdev/koishi-plugin-dota2tracker 2.4.0 → 2.5.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/changelog.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # 更新日志
2
2
 
3
+ ## [2.5.0](https://github.com/sjtdev/koishi-plugin-dota2tracker/compare/v2.4.0...v2.5.0) (2026-02-18)
4
+
5
+ ### ✨ 新增功能
6
+
7
+ * **report/daily:** 重写了全新的日报模板 ([8e8687a](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/8e8687adc4ad92df423c8b2be4a23f54ec07c105))
8
+
9
+ ### 🐛 Bug 修复
10
+
11
+ * **font-service:** 调整监控重载字体目录打印日志逻辑防止刷屏 ([49a16fd](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/49a16fd288184e6619d416fb304d541d7e6d0181))
12
+ * **logger/font:** 修复重载字体目录时打印日志失败 ([9226d76](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/9226d7683b020d87b72edb85318c7422565a20b1))
13
+
14
+ ### 📝 文档
15
+
16
+ * 补上关于重构`font`内容的文档 ([ac25965](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/ac25965dab839f8040e19d6f9f6237c2d1404a7d))
17
+ * **template-fonts:** 更新一些文档说明 ([a4530fc](https://github.com/sjtdev/koishi-plugin-dota2tracker/commit/a4530fcd86d2cdfc048e718fa37242af5e41f14d))
18
+
3
19
  ## [2.4.0](https://github.com/sjtdev/koishi-plugin-dota2tracker/compare/v2.3.4...v2.4.0) (2026-02-15)
4
20
 
5
21
  ### ✨ 新增功能
package/lib/index.js CHANGED
@@ -775,14 +775,14 @@ var require_en_US_command = __commonJS({
775
775
  // src/locales/en-US.schema.yml
776
776
  var require_en_US_schema = __commonJS({
777
777
  "src/locales/en-US.schema.yml"(exports2, module2) {
778
- 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 if not using a proxy. \n※Cannot use the global proxy address configured by the `proxy-agent` plugin. This option must be set if you want to use a proxy.", suppressStratzNetworkErrors: "**Please use the `suppressApiNetworkErrors` option below, which applies to both Stratz and OpenDota. \nThis option is still effective. If either this or `suppressApiNetworkErrors` is enabled, Stratz/OpenDota logs will be downgraded to debug output. \nThis option will be removed in a future version.**", suppressApiNetworkErrors: "When enabled, Stratz/OpenDota network error logs will be output at the debug level. \n(e.g., timeouts, network connection issues, but excludes 403 Forbidden) \nKoishi does not display debug-level logs by default. To enable debug log display, please see [📖 Configs#suppressapinetworkerrors](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "Enable OpenDota as a fallback data source for match tracking and query-match features.", OPENDOTA_API_KEY: "Your paid subscription API key for OpenDota. \nSee https://www.opendota.com/api-keys for details. \nFree users should leave this blank.", OpenDotaIPStack: "If you experience frequent failures when accessing the OpenDota API, it might be caused by a faulty IPv6 environment. \nSetting this option will force the use of IPv4 when accessing the OpenDota API to try and resolve the issue." }, 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" }, autoRecallTips: 'Automatically recall tip messages after the command finishes, e.g., "Searching for match details, please wait..."' }, 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 for generating match information images. See [📖 Template Info#Match Info Template](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-match.html) for display effects.", template_player: "Template used for generating player information images. (Currently only one template available)", template_hero: "Template used for generating hero information images. (Currently only one template available)", playerRankEstimate: "Estimate rank for unranked players in the player template <br>Estimated ranks will be displayed as gray images", templateFonts: '**>Deprecated!<** \n**If you need to configure fonts, please use the `fonts.*` configuration option below instead!** \nFont names used in the template. Requires font files to be installed on the device running Koishi. \nMultiple font names can be added, and it will fallback from top to bottom to the first available font; if no fonts are available, the system default font will be used. \nIf a font name contains spaces or special characters, quotes must be added around the name (it is recommended to always use quotes here); \nIf using a generic font family name, you must **NOT use quotes**, e.g.:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor more information on font-family, please refer to [📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)', fontPath: "Font directory path", fonts: { description: "**Interim solution. A more comprehensive and flexible font configuration scheme will be implemented in the console in the future.** \nFont configuration used in the template. \nIt will automatically scan font files in the `fontPath` directory to generate the selectable font list below. \nYou can separately configure fallback lists for sans-serif (sans), serif (serif), and monospace (mono) font families. \nThe main font type used in the templates is **sans-serif**, while some text in certain templates uses **serif** and **monospace** fonts. \nFor more information on font configuration, please refer to [📖 Template Info#Template Fonts](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-fonts.html)", sans: { $desc: "Sans-serif fonts (sans-serif)" }, serif: { $desc: "Serif fonts (serif)" }, mono: { $desc: "Monospace fonts (monospace)" } } } } };
778
+ 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 if not using a proxy. \n※Cannot use the global proxy address configured by the `proxy-agent` plugin. This option must be set if you want to use a proxy.", suppressStratzNetworkErrors: "**Please use the `suppressApiNetworkErrors` option below, which applies to both Stratz and OpenDota. \nThis option is still effective. If either this or `suppressApiNetworkErrors` is enabled, Stratz/OpenDota logs will be downgraded to debug output. \nThis option will be removed in a future version.**", suppressApiNetworkErrors: "When enabled, Stratz/OpenDota network error logs will be output at the debug level. \n(e.g., timeouts, network connection issues, but excludes 403 Forbidden) \nKoishi does not display debug-level logs by default. To enable debug log display, please see [📖 Configs#suppressapinetworkerrors](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "Enable OpenDota as a fallback data source for match tracking and query-match features.", OPENDOTA_API_KEY: "Your paid subscription API key for OpenDota. \nSee https://www.opendota.com/api-keys for details. \nFree users should leave this blank.", OpenDotaIPStack: "If you experience frequent failures when accessing the OpenDota API, it might be caused by a faulty IPv6 environment. \nSetting this option will force the use of IPv4 when accessing the OpenDota API to try and resolve the issue." }, 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" }, autoRecallTips: 'Automatically recall tip messages after the command finishes, e.g., "Searching for match details, please wait..."' }, 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 for generating match information images. See [📖 Template Info#Match Info Template](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-match.html) for display effects.", template_player: "Template used for generating player information images. (Currently only one template available)", template_hero: "Template used for generating hero information images. (Currently only one template available)", playerRankEstimate: "Estimate rank for unranked players in the player template <br>Estimated ranks will be displayed as gray images", templateFonts: '**>Deprecated!<** \n**If you need to configure fonts, please use the `fonts.*` configuration option below instead!** \nFont names used in the template. Requires font files to be installed on the device running Koishi. \nMultiple font names can be added, and it will fallback from top to bottom to the first available font; if no fonts are available, the system default font will be used. \nIf a font name contains spaces or special characters, quotes must be added around the name (it is recommended to always use quotes here); \nIf using a generic font family name, you must **NOT use quotes**, e.g.:\n```\n"Microsoft YaHei"\nsans-serif\n```\nFor more information on font-family, please refer to [📖 MDN: font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family)', fontPath: "Font directory path", fonts: { description: "**Interim solution. A more comprehensive and flexible font configuration scheme will be implemented in the console in the future.** \nFont configuration used in the template. \nIt will automatically scan font files in the `fontPath` directory to generate the selectable font list below. \nYou can separately configure fallback lists for sans-serif (sans), serif (serif), and monospace (mono) font families. \nThe main font type used in the templates is **sans-serif**, while some text in certain templates uses **serif** and **monospace** fonts. \nFor more information on font configuration, please refer to [📖 Template Info#Template Fonts](https://sjtdev.github.io/koishi-plugin-dota2tracker/en-US/template-fonts.html)", sans: { $desc: "Sans-serif fonts (sans-serif)" }, serif: { $desc: "Serif fonts (serif)" }, mono: { $desc: "Monospace fonts (monospace)" } } } } };
779
779
  }
780
780
  });
781
781
 
782
782
  // src/locales/en-US.template.yml
783
783
  var require_en_US_template = __commonJS({
784
784
  "src/locales/en-US.template.yml"(exports2, module2) {
785
- module2.exports = { dota2tracker: { template: { radiant: "Radiant", dire: "Dire", won: "Won", lost: "Lost", match_id_: "Match ID: {0}", game_mode_: "Mode: {0}", start_time_: "Start Time: {0}", end_time_: "End Time: {0}", pick_order: "#{0}", random: "R", hero_damage_: "Damage: {0}", building_damage_: "Building: {0}", damage_received_: "Received: {0}", lasthit_: "LastHit: {0}", deny_: "Deny: {0}", "lh/dn_": "LH/DN: {0}", GPM: "GPM", XPM: "XPM", heal_: "Heal: {0}", crowd_control_duration_: "CCD: {0}", "GPM/XPM_": "GPM/XPM: {0}", lane: "Lane", lane_: "Lane: ", lane_advantage: "Lane +", lane_disadvantage: "Lane -", lane_jungle: "Jungle", lane_stomp: "Lane+++", lane_stomped: "Lane---", lane_tie: "Lane ==", analysis_successful: "Analysis successful", analysis_incomplete: "Analysis incomplete", analysis_by_opendota: "Analysis by OpenDota", kill: "Kill", kill_contribution_: "KC: {0}", position: "Position", position_: "Position: ", position_1: "Carry", position_2: "Mid", position_3: "OffLane", position_4: "Softsup", position_5: "Hardsup", dire_won: "Dire Won", radiant_won: "Radiant Won", total_damage: "Damage", total_experience: "Exp.", total_gold: "Gold", region_: "Region: {0}", duration_: "Duration: {0}", position_undefined: "?", top10_: "Top 10 Heroes by Matches: ", match_count_: "Matches: ", last25matches_: "Last 25 Matches: ", winrate_: "Winrate: ", imp_: "IMP: ", lane_advantage_rate_: "Lane Advantage Rate: ", hero: "Hero", all_matches_: "All Matches: ", match_count: "Matches", winrate: "Winrate", imp: "IMP", win_count: "Wins", lose_count: "Losses", recently_heroes: "Heroes used more than once recently: ", recently_positions: "Performance in the last 25 matches across all positions: ", winning_streak: "Winning Streak", losing_streak: "Losing Streak", id: "ID", mode: "Mode", kda_kc: "KDA(KC)", time: "Time", duration: "Duration", rank: "Rank", un_parsed: "(Unparsed)", combined_win_loss_summary: "Combined Win/Loss Summary: ", yesterdays_summary: "Yesterday's Summary", last_weeks_summary: "Last Week's Summary", report_won: "W", report_lost: "L", report_winrate: "WR", anonymous_player_1: "This profile is private.", anonymous_player_2: "Background is for display purposes only. It is not {player}’s data.", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>Sad", rank_fun_up_message: "Hip hip hooray! Our awesome member AVATAR_PLACEHOLDER{name} has leveled up from PREV_PLACEHOLDER to CURR_PLACEHOLDER!", titles: { MVP: "MVP-#FFA500", Soul: "Soul-#66CCFF", Rich: "Rich-#FFD700", Wise: "Wise-#8888FF", Controller: "Controller-#FF00FF", Nuker: "Nuker-#CC0088", Breaker: "Breaker-#DD0000", Ghost: "Ghost-#CCCCCC", Utility: "Utility-#20B2AA", Assister: "Assister-#006400", Demolisher: "Demolisher-#FEDCBA", Healer: "Healer-#00FF00", Tank: "Tank-#84A1C7", Idle: "Idle-#DDDDDD" }, situation: "Situation", networth: "Net Worth", experience: "Experience", OUTCOME_MAP: { RADIANT_VICTORY: "RADIANT VICTORY", RADIANT_STOMP: "RADIANT STOMP", DIRE_VICTORY: "DIRE VICTORY", DIRE_STOMP: "DIRE STOMP", TIE: "TIE" }, lane_top: "Top", lane_mid: "Mid", lane_bottom: "Bottom", empty_extra_info: "No extra info", opendota: { networth_unavailable: "Net Worth Chart Unavailable", networth_unavailable_reason: "Data source OpenDota does not provide per-minute net worth data.", lane_outcome_tip: "(Based on gold earned, not net worth; ref only.)", gold_t: "Gold Earned" } } } };
785
+ module2.exports = { dota2tracker: { template: { radiant: "Radiant", dire: "Dire", won: "Won", lost: "Lost", match_id_: "Match ID: {0}", game_mode_: "Mode: {0}", start_time_: "Start Time: {0}", end_time_: "End Time: {0}", pick_order: "#{0}", random: "R", hero_damage_: "Damage: {0}", building_damage_: "Building: {0}", damage_received_: "Received: {0}", lasthit_: "LastHit: {0}", deny_: "Deny: {0}", "lh/dn_": "LH/DN: {0}", GPM: "GPM", XPM: "XPM", heal_: "Heal: {0}", crowd_control_duration_: "CCD: {0}", "GPM/XPM_": "GPM/XPM: {0}", lane: "Lane", lane_: "Lane: ", lane_advantage: "Lane +", lane_disadvantage: "Lane -", lane_jungle: "Jungle", lane_stomp: "Lane+++", lane_stomped: "Lane---", lane_tie: "Lane ==", analysis_successful: "Analysis successful", analysis_incomplete: "Analysis incomplete", analysis_by_opendota: "Analysis by OpenDota", kill: "Kill", kill_contribution_: "KC: {0}", position: "Position", position_: "Position: ", position_1: "Carry", position_2: "Mid", position_3: "OffLane", position_4: "Softsup", position_5: "Hardsup", dire_won: "Dire Won", radiant_won: "Radiant Won", total_damage: "Damage", total_experience: "Exp.", total_gold: "Gold", region_: "Region: {0}", duration_: "Duration: {0}", position_undefined: "?", top10_: "Top 10 Heroes by Matches: ", match_count_: "Matches: ", last25matches_: "Last 25 Matches: ", winrate_: "Winrate: ", imp_: "IMP: ", lane_advantage_rate_: "Lane Advantage Rate: ", hero: "Hero", all_matches_: "All Matches: ", match_count: "Matches", winrate: "Winrate", imp: "IMP", win_count: "Wins", lose_count: "Losses", recently_heroes: "Heroes used more than once recently: ", recently_positions: "Performance in the last 25 matches across all positions: ", winning_streak: "Winning Streak", losing_streak: "Losing Streak", id: "ID", mode: "Mode", kda_kc: "KDA(KC)", time: "Time", duration: "Duration", rank: "Rank", un_parsed: "(Unparsed)", combined_win_loss_summary: "Combined Win/Loss Summary: ", yesterdays_summary: "Yesterday's Summary", last_weeks_summary: "Last Week's Summary", report_won: "W", report_lost: "L", report_winrate: "WR", anonymous_player_1: "This profile is private.", anonymous_player_2: "Background is for display purposes only. It is not {player}’s data.", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>Sad", rank_fun_up_message: "Hip hip hooray! Our awesome member AVATAR_PLACEHOLDER{name} has leveled up from PREV_PLACEHOLDER to CURR_PLACEHOLDER!", titles: { MVP: "MVP-#FFA500", Soul: "Soul-#66CCFF", Rich: "Rich-#FFD700", Wise: "Wise-#8888FF", Controller: "Controller-#FF00FF", Nuker: "Nuker-#CC0088", Breaker: "Breaker-#DD0000", Ghost: "Ghost-#CCCCCC", Utility: "Utility-#20B2AA", Assister: "Assister-#006400", Demolisher: "Demolisher-#FEDCBA", Healer: "Healer-#00FF00", Tank: "Tank-#84A1C7", Idle: "Idle-#DDDDDD" }, situation: "Situation", networth: "Net Worth", experience: "Experience", OUTCOME_MAP: { RADIANT_VICTORY: "RADIANT VICTORY", RADIANT_STOMP: "RADIANT STOMP", DIRE_VICTORY: "DIRE VICTORY", DIRE_STOMP: "DIRE STOMP", TIE: "TIE" }, lane_top: "Top", lane_mid: "Mid", lane_bottom: "Bottom", empty_extra_info: "No extra info", opendota: { networth_unavailable: "Net Worth Chart Unavailable", networth_unavailable_reason: "Data source OpenDota does not provide per-minute net worth data.", lane_outcome_tip: "(Based on gold earned, not net worth; ref only.)", gold_t: "Gold Earned" }, report: { daily: { plugin_name: "Koishi Dota 2 Plugin", title: "Daily Group Recap", meta: { date_format: "cccc, LLLL d, yyyy", summary: "Performance summary for {0}. High-density battle report generated automatically.", footer_format: "ID: #{0} • Server: {1}" }, stats: { matches: "Matches", win_rate: "Win Rate", kills: "Total Kills", duration: "Duration", avg_time: "Avg", matches_subtext: "%s W - %s L", vs_yesterday: "vs Yesterday", kills_avg: "Avg {0}" }, spotlight: { mvp_title: "The King", lvp_title: "The Suspect", score_label: "MVP Score" }, squad: { title: "Performance", subtitle: "Ranked by KDA (desc)", header: { rank: "Rank", player_info: "Player Info", hero_pool: "Hero Pool (Sorted by Wins)", kda: "KDA Ratio", impact: "Impact" }, impact: { dmg: "Dmg", gold: "Gold" } }, footer: { generated_by: "Generated by Koishi Bot" } } } } } };
786
786
  }
787
787
  });
788
788
 
@@ -803,14 +803,14 @@ var require_zh_CN_command = __commonJS({
803
803
  // src/locales/zh-CN.schema.yml
804
804
  var require_zh_CN_schema = __commonJS({
805
805
  "src/locales/zh-CN.schema.yml"(exports2, module2) {
806
- module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,不使用代理请留空。 \n※无法使用`proxy-agent`插件配置的全局代理地址,欲使用代理必须设置此项。", suppressStratzNetworkErrors: "**请使用下方通用于 stratz 与 opendota 的配置项`suppressApiNetworkErrors`。 \n此配置项仍然生效,当与`suppressApiNetworkErrors`任一启用时将会使 stratz/opendota 日志降级到debug输出。 \n此配置项将于下版本被移除。**", suppressApiNetworkErrors: "开启后将 stratz/opendota 网络错误日志使用debug级别输出。 \n(如超时、网络不通等,但403 Forbidden除外) \nkoishi默认不显示debug级日志,若需要开启debug日志显示,请见 [📖 配置项#suppressapinetworkerrors](http://sjtdev.github.io/koishi-plugin-dota2tracker/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "启用 OpenDota 作为战报追踪与查询比赛功能的备用数据源。", OPENDOTA_API_KEY: "OpenDota 的订阅付费APIKEY, \n可在 https://www.opendota.com/api-keys 查看详情。 \nOpenDota 的免费用户此处请留空。", OpenDotaIPStack: "若访问 OpenDota API 时频繁失败,可能是由于错误的 IPv6 环境导致的。 \n设置此选项可在访问 OpenDota API 时强制使用 IPv4 尝试解决问题。" }, 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: "别名" }, autoRecallTips: "在指令调用结束后自动撤回提示消息,如:“正在搜索对局详情,请稍后……”" }, 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: '**>已弃用!<** \n**如果需要配置字体请使用下方`fonts.*`配置项代替!** \n模板所使用的字体名。需要 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)', fontPath: "字体文件文件夹路径", fonts: { description: "**过渡方案,之后会重启控制台页面并在其中实现更完善更灵活的字体配置方案。** \n模板所使用的字体配置。\n会自动读取配置项 `fontPath` 目录下的字体文件为下方配置项生成可选字体列表。 \n可分别配置无衬线(sans)、衬线(serif)、等宽(mono)字体族的备选表。 \n模板的主要使用字体类型为**无衬线字体**,部分模板的一些文本会使用到**衬线字体**及**等宽字体**。 \n关于字体文件配置的更多信息,请查阅 [📖 模板相关#模板字体](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-fonts.html)", sans: { $desc: "无衬线字体(sans-serif)" }, serif: { $desc: "衬线字体(serif)" }, mono: { $desc: "等宽字体(monospace)" } } } } };
806
+ module2.exports = { _config: { base: { $desc: "基础设置", STRATZ_API_TOKEN: "※必须。stratz.com的API TOKEN,可在 https://stratz.com/api 获取。", dataParsingTimeoutMinutes: "等待比赛数据解析的时间(单位:分钟)。如果数据解析时间超过等待时间,将直接生成战报而不再等待解析完成。", proxyAddress: "代理地址,不使用代理请留空。 \n※无法使用`proxy-agent`插件配置的全局代理地址,欲使用代理必须设置此项。", suppressStratzNetworkErrors: "**请使用下方通用于 stratz 与 opendota 的配置项`suppressApiNetworkErrors`。 \n此配置项仍然生效,当与`suppressApiNetworkErrors`任一启用时将会使 stratz/opendota 日志降级到debug输出。 \n此配置项将于下版本被移除。**", suppressApiNetworkErrors: "开启后将 stratz/opendota 网络错误日志使用debug级别输出。 \n(如超时、网络不通等,但403 Forbidden除外) \nkoishi默认不显示debug级日志,若需要开启debug日志显示,请见 [📖 配置项#suppressapinetworkerrors](http://sjtdev.github.io/koishi-plugin-dota2tracker/configs.html#suppressapinetworkerrors-boolean)", enableOpenDotaFallback: "启用 OpenDota 作为战报追踪与查询比赛功能的备用数据源。", OPENDOTA_API_KEY: "OpenDota 的订阅付费APIKEY, \n可在 https://www.opendota.com/api-keys 查看详情。 \nOpenDota 的免费用户此处请留空。", OpenDotaIPStack: "若访问 OpenDota API 时频繁失败,可能是由于错误的 IPv6 环境导致的。 \n设置此选项可在访问 OpenDota API 时强制使用 IPv4 尝试解决问题。" }, 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: "别名" }, autoRecallTips: "在指令调用结束后自动撤回提示消息,如:“正在搜索对局详情,请稍后……”" }, 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: '**>已弃用!<** \n**如果需要配置字体请使用下方`fonts.*`配置项代替!** \n模板所使用的字体名。需要 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)', fontPath: "字体文件文件夹路径", fonts: { description: "**过渡方案,之后会重启控制台页面并在其中实现更完善更灵活的字体配置方案。** \n模板所使用的字体配置。\n会自动读取配置项 `fontPath` 目录下的字体文件为下方配置项生成可选字体列表。 \n可分别配置无衬线(sans)、衬线(serif)、等宽(mono)字体族的备选表。 \n模板的主要使用字体类型为**无衬线字体**,部分模板的一些文本会使用到**衬线字体**及**等宽字体**。 \n关于字体文件配置的更多信息,请查阅 [📖 模板相关#模板字体](https://sjtdev.github.io/koishi-plugin-dota2tracker/template-fonts.html)", sans: { $desc: "无衬线字体(sans-serif)" }, serif: { $desc: "衬线字体(serif)" }, mono: { $desc: "等宽字体(monospace)" } } } } };
807
807
  }
808
808
  });
809
809
 
810
810
  // src/locales/zh-CN.template.yml
811
811
  var require_zh_CN_template = __commonJS({
812
812
  "src/locales/zh-CN.template.yml"(exports2, module2) {
813
- module2.exports = { dota2tracker: { template: { radiant: "天辉", dire: "夜魇", won: "获胜", lost: "失败", match_id_: "比赛编号:{0}", game_mode_: "模式:{0}", start_time_: "起始时间:{0}", end_time_: "结束时间:{0}", pick_order: "第{0}手", random: "随机", hero_damage_: "英雄伤害:{0}", building_damage_: "建筑伤害:{0}", damage_received_: "受到伤害:{0}", lasthit_: "补刀:{0}", deny_: "反补:{0}", "lh/dn_": "补刀:{0}/{1}", GPM: "GPM", XPM: "XPM", heal_: "治疗量:{0}", crowd_control_duration_: "控制时间:{0}", "GPM/XPM_": "GPM/XPM:{0}", lane: "对线", lane_: "对线:", lane_advantage: "对线优势", lane_disadvantage: "对线劣势", lane_stomp: "对线碾压", lane_stomped: "对线被碾", lane_tie: "对线平手", lane_jungle: "野区霸主", analysis_successful: "录像分析成功", analysis_incomplete: "分析结果不完整", analysis_by_opendota: "数据解析自 OpenDota", kill: "击杀", kill_contribution_: "参战率:{0}", position: "位置", position_: "位置:", position_1: "优势路", position_2: "中路", position_3: "烈士路", position_4: "采灵芝", position_5: "工具人", position_undefined: "?", total_damage: "总伤害", total_gold: "总经济", total_experience: "总经验", radiant_won: "天辉获胜", dire_won: "夜魇获胜", duration_: "持续时间:{0}", region_: "地区:{0}", match_count_: "场次:", last25matches_: "最近25场:", winrate_: "胜率:", imp_: "表现:", lane_advantage_rate_: "线优率:", top10_: "全期场次前十的英雄:", hero: "英雄", all_matches_: "全期场次:", match_count: "场次", winrate: "胜率", imp: "表现", win_count: "胜场", lose_count: "败场", recently_heroes: "近期使用场次大于1的英雄:", recently_positions: "近25场各个位置的表现:", winning_streak: "连胜", losing_streak: "连败", id: "ID", mode: "模式", kda_kc: "KDA(参战率)", time: "时间", duration: "时长", rank: "段位", un_parsed: "(未解析)", combined_win_loss_summary: "组合胜负情况:", yesterdays_summary: "昨日总结", last_weeks_summary: "上周总结", report_won: "胜", report_lost: "负", report_winrate: "胜率", anonymous_player_1: "数据未公开", anonymous_player_2: "背景仅供展示目的,不属于{player}的数据。", rank_fun_up_message: "热烈祝贺群友 AVATAR_PLACEHOLDER{name} 在天梯中再获进步,<br/>由 PREV_PLACEHOLDER 升为 CURR_PLACEHOLDER,再接再厉,再创辉煌!", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>寄", titles: { MVP: "MVP-#FFA500", Soul: "魂-#66CCFF", Rich: "富-#FFD700", Wise: "睿-#8888FF", Controller: "控-#FF00FF", Nuker: "爆-#CC0088", Breaker: "破-#DD0000", Ghost: "鬼-#CCCCCC", Utility: "辅-#20B2AA", Assister: "助-#006400", Demolisher: "拆-#FEDCBA", Healer: "奶-#00FF00", Tank: "耐-#84A1C7", Idle: "摸-#DDDDDD" }, situation: "局势", networth: "经济", experience: "经验", OUTCOME_MAP: { RADIANT_VICTORY: "天辉优势", RADIANT_STOMP: "天辉碾压", DIRE_VICTORY: "夜魇优势", DIRE_STOMP: "夜魇碾压", TIE: "势均力敌" }, lane_top: "上路", lane_mid: "中路", lane_bottom: "下路", empty_extra_info: "比赛未解析或信息缺失,无法展示更多数据。", opendota: { networth_unavailable: "经济走势图不可用", networth_unavailable_reason: "数据源 OpenDota 未提供每分钟经济数据。", lane_outcome_tip: "(数据基于累计获得金币而非经济,仅供参考)", gold_t: "累计获得金币" } } } };
813
+ module2.exports = { dota2tracker: { template: { radiant: "天辉", dire: "夜魇", won: "获胜", lost: "失败", match_id_: "比赛编号:{0}", game_mode_: "模式:{0}", start_time_: "起始时间:{0}", end_time_: "结束时间:{0}", pick_order: "第{0}手", random: "随机", hero_damage_: "英雄伤害:{0}", building_damage_: "建筑伤害:{0}", damage_received_: "受到伤害:{0}", lasthit_: "补刀:{0}", deny_: "反补:{0}", "lh/dn_": "补刀:{0}/{1}", GPM: "GPM", XPM: "XPM", heal_: "治疗量:{0}", crowd_control_duration_: "控制时间:{0}", "GPM/XPM_": "GPM/XPM:{0}", lane: "对线", lane_: "对线:", lane_advantage: "对线优势", lane_disadvantage: "对线劣势", lane_stomp: "对线碾压", lane_stomped: "对线被碾", lane_tie: "对线平手", lane_jungle: "野区霸主", analysis_successful: "录像分析成功", analysis_incomplete: "分析结果不完整", analysis_by_opendota: "数据解析自 OpenDota", kill: "击杀", kill_contribution_: "参战率:{0}", position: "位置", position_: "位置:", position_1: "优势路", position_2: "中路", position_3: "烈士路", position_4: "采灵芝", position_5: "工具人", position_undefined: "?", total_damage: "总伤害", total_gold: "总经济", total_experience: "总经验", radiant_won: "天辉获胜", dire_won: "夜魇获胜", duration_: "持续时间:{0}", region_: "地区:{0}", match_count_: "场次:", last25matches_: "最近25场:", winrate_: "胜率:", imp_: "表现:", lane_advantage_rate_: "线优率:", top10_: "全期场次前十的英雄:", hero: "英雄", all_matches_: "全期场次:", match_count: "场次", winrate: "胜率", imp: "表现", win_count: "胜场", lose_count: "败场", recently_heroes: "近期使用场次大于1的英雄:", recently_positions: "近25场各个位置的表现:", winning_streak: "连胜", losing_streak: "连败", id: "ID", mode: "模式", kda_kc: "KDA(参战率)", time: "时间", duration: "时长", rank: "段位", un_parsed: "(未解析)", combined_win_loss_summary: "组合胜负情况:", yesterdays_summary: "昨日总结", last_weeks_summary: "上周总结", report_won: "胜", report_lost: "负", report_winrate: "胜率", anonymous_player_1: "数据未公开", anonymous_player_2: "背景仅供展示目的,不属于{player}的数据。", rank_fun_up_message: "热烈祝贺群友 AVATAR_PLACEHOLDER{name} 在天梯中再获进步,<br/>由 PREV_PLACEHOLDER 升为 CURR_PLACEHOLDER,再接再厉,再创辉煌!", rank_fun_down_message: "AVATAR_PLACEHOLDER<br/>寄", titles: { MVP: "MVP-#FFA500", Soul: "魂-#66CCFF", Rich: "富-#FFD700", Wise: "睿-#8888FF", Controller: "控-#FF00FF", Nuker: "爆-#CC0088", Breaker: "破-#DD0000", Ghost: "鬼-#CCCCCC", Utility: "辅-#20B2AA", Assister: "助-#006400", Demolisher: "拆-#FEDCBA", Healer: "奶-#00FF00", Tank: "耐-#84A1C7", Idle: "摸-#DDDDDD" }, situation: "局势", networth: "经济", experience: "经验", OUTCOME_MAP: { RADIANT_VICTORY: "天辉优势", RADIANT_STOMP: "天辉碾压", DIRE_VICTORY: "夜魇优势", DIRE_STOMP: "夜魇碾压", TIE: "势均力敌" }, lane_top: "上路", lane_mid: "中路", lane_bottom: "下路", empty_extra_info: "比赛未解析或信息缺失,无法展示更多数据。", opendota: { networth_unavailable: "经济走势图不可用", networth_unavailable_reason: "数据源 OpenDota 未提供每分钟经济数据。", lane_outcome_tip: "(数据基于累计获得金币而非经济,仅供参考)", gold_t: "累计获得金币" }, report: { daily: { plugin_name: "Koishi Dota 2 Plugin", title: "每日战报", meta: { date_format: "yyyy年L月d日 cccc", summary: "战报生成于 {0}。高密度战斗数据自动汇总。", footer_format: "ID: #{0} • 服务器: {1}" }, stats: { matches: "总场次", win_rate: "胜率", kills: "总击杀", duration: "总时长", avg_time: "平均时长", matches_subtext: "{0}胜 - {1}负", vs_yesterday: "较昨日", kills_avg: "场均 {0}" }, spotlight: { mvp_title: "全 场 最 佳", lvp_title: "头 号 战 犯", score_label: "综合评分" }, squad: { title: "表现排行", subtitle: "基于 KDA 倒序排列", header: { rank: "排名", player_info: "玩家", hero_pool: "英雄池(基于胜场)", kda: "KDA", impact: "贡献 / 经济" }, impact: { dmg: "伤害", gold: "经济" } }, footer: { generated_by: "Generated by Koishi Bot" } } } } } };
814
814
  }
815
815
  });
816
816
 
@@ -2314,9 +2314,12 @@ var DatabaseService = class extends import_koishi7.Service {
2314
2314
  );
2315
2315
  ctx.model.extend("dt_match_extension", { matchId: "unsigned", startTime: "timestamp", data: "json" }, { autoInc: false, primary: ["matchId"] });
2316
2316
  }
2317
- async insertReportData(matchId, startTime, data) {
2317
+ async insertMatchExtension(matchId, startTime, data) {
2318
2318
  return this.ctx.database.upsert("dt_match_extension", [{ matchId, startTime, data }]);
2319
2319
  }
2320
+ async getMatchExtension(matchIds) {
2321
+ return this.ctx.database.get("dt_match_extension", { matchId: matchIds });
2322
+ }
2320
2323
  async setPlayerRank(playerId, rank) {
2321
2324
  return this.ctx.database.set("dt_subscribed_players", playerId, { rank });
2322
2325
  }
@@ -2426,6 +2429,16 @@ var StratzAPI = class extends import_koishi8.Service {
2426
2429
  (data) => !!data?.player
2427
2430
  );
2428
2431
  }
2432
+ async queryPlayersMatchesForDaily_legacy(steamAccountIds, seconds) {
2433
+ return this.query(
2434
+ "PlayersMatchesForDaily_Legacy",
2435
+ {
2436
+ steamAccountIds,
2437
+ seconds
2438
+ },
2439
+ (data) => !!data?.players
2440
+ );
2441
+ }
2429
2442
  async queryPlayersMatchesForDaily(steamAccountIds, seconds) {
2430
2443
  return this.query(
2431
2444
  "PlayersMatchesForDaily",
@@ -2695,11 +2708,7 @@ var ViewRenderer = class extends import_koishi10.Service {
2695
2708
  async render(html) {
2696
2709
  const { fonts } = this.config;
2697
2710
  const finalHtml = html.replace("</head>", `${this.getFontStyleBlock()}</head>`);
2698
- const fontFamilies = Array.from(/* @__PURE__ */ new Set([
2699
- ...fonts.sans || [],
2700
- ...fonts.serif || [],
2701
- ...fonts.mono || []
2702
- ])).filter(Boolean);
2711
+ const fontFamilies = Array.from(/* @__PURE__ */ new Set([...fonts.sans || [], ...fonts.serif || [], ...fonts.mono || []])).filter(Boolean);
2703
2712
  return this.ctx.puppeteer.render(
2704
2713
  finalHtml,
2705
2714
  fontFamilies.length === 0 ? void 0 : async (page, next) => {
@@ -2713,17 +2722,20 @@ var ViewRenderer = class extends import_koishi10.Service {
2713
2722
  await page.exposeFunction("dota2tracker_font_service_get_format", (format) => {
2714
2723
  return this.ctx.dota2tracker.font.getFontFormat(format);
2715
2724
  });
2716
- await page.evaluate(async (fonts2) => {
2717
- const win = window;
2718
- const loaders = fonts2.map(async (font) => {
2719
- const format = await win.dota2tracker_font_service_get_format(font.format);
2720
- const fontFace = new win.FontFace(font.family, `url("${font.path}") format("${format}")`, font.descriptors);
2721
- win.document.fonts.add(fontFace);
2722
- await fontFace.load();
2723
- });
2724
- await Promise.all(loaders);
2725
- await win.document.fonts.ready;
2726
- }, fontInfos.map((f) => ({ ...f, path: (0, import_node_url.pathToFileURL)(f.path).href })));
2725
+ await page.evaluate(
2726
+ async (fonts2) => {
2727
+ const win = window;
2728
+ const loaders = fonts2.map(async (font) => {
2729
+ const format = await win.dota2tracker_font_service_get_format(font.format);
2730
+ const fontFace = new win.FontFace(font.family, `url("${font.path}") format("${format}")`, font.descriptors);
2731
+ win.document.fonts.add(fontFace);
2732
+ await fontFace.load();
2733
+ });
2734
+ await Promise.all(loaders);
2735
+ await win.document.fonts.ready;
2736
+ },
2737
+ fontInfos.map((f) => ({ ...f, path: (0, import_node_url.pathToFileURL)(f.path).href }))
2738
+ );
2727
2739
  }
2728
2740
  const body = await page.$("body");
2729
2741
  return next(body);
@@ -2849,9 +2861,11 @@ var FontService = class extends import_koishi11.Service {
2849
2861
  try {
2850
2862
  this.watcher = import_node_fs3.default.watch(fontsPath, (eventType, filename) => {
2851
2863
  if (filename && /\.(ttf|otf|woff2?|ttc|sfnt)$/.test(filename)) {
2852
- this.logger.debug(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.font_loader.reload", { filename, eventType }));
2853
2864
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
2854
- this.debounceTimer = setTimeout(() => this.loadFonts(fontsPath), 200);
2865
+ this.debounceTimer = setTimeout(() => {
2866
+ this.logger.debug(this.ctx.dota2tracker.i18n.gt("dota2tracker.logger.font_loader", { filename: "batch", eventType: "change" }));
2867
+ this.loadFonts(fontsPath);
2868
+ }, 200);
2855
2869
  }
2856
2870
  });
2857
2871
  } catch (e) {
@@ -3522,38 +3536,40 @@ var ReportTask = class extends import_koishi15.Service {
3522
3536
  static {
3523
3537
  __name(this, "ReportTask");
3524
3538
  }
3525
- /*
3526
- 还没计划好怎么动这一坨,先原样移植吧。
3527
- */
3528
3539
  constructor(ctx) {
3529
- super(ctx, "dota2tracker.report", true);
3540
+ super(ctx, "dota2tracker.report-task", true);
3530
3541
  this.config = ctx.config;
3531
3542
  if (this.config.dailyReportSwitch) {
3532
3543
  ctx.cron(`0 ${this.config.dailyReportHours} * * *`, async () => {
3533
- try {
3534
- const oneDayAgo = Math.floor(import_luxon9.DateTime.now().minus({ days: 1 }).toSeconds());
3535
- await this.report(oneDayAgo, "dota2tracker.template.yesterdays_summary", this.config.dailyReportShowCombi);
3536
- } catch (error) {
3537
- handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.config);
3538
- }
3544
+ await this.runDailyJob();
3539
3545
  });
3540
3546
  }
3541
3547
  if (this.config.weeklyReportSwitch) {
3542
3548
  ctx.cron(`0 ${this.config.weeklyReportDayHours[1]} * * ${this.config.weeklyReportDayHours[0]}`, async () => {
3543
- try {
3544
- const oneWeekAgo = Math.floor(import_luxon9.DateTime.now().minus({ weeks: 1 }).toSeconds());
3545
- await this.report(oneWeekAgo, "dota2tracker.template.last_weeks_summary", this.config.weeklyReportShowCombi);
3546
- } catch (error) {
3547
- handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.config);
3548
- }
3549
+ await this.runWeeklyJob();
3549
3550
  });
3550
3551
  }
3551
3552
  }
3552
- async report(timeAgo, titleKey, showCombi) {
3553
+ async runDailyJob() {
3554
+ const bundles = await this.ctx.dota2tracker.report.generateDailyReportBundles();
3555
+ for (const bundle of bundles) {
3556
+ const image = await this.ctx.dota2tracker.view.renderToImageByFile(bundle.report, "daily", "report" /* Report */, await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: bundle.channelId }));
3557
+ await this.ctx.broadcast([`${bundle.platform}:${bundle.channelId}`], image);
3558
+ }
3559
+ }
3560
+ async runWeeklyJob() {
3561
+ try {
3562
+ const oneWeekAgo = Math.floor(import_luxon9.DateTime.now().minus({ weeks: 1 }).toSeconds());
3563
+ await this.report_legacy(oneWeekAgo, "dota2tracker.template.last_weeks_summary", this.config.weeklyReportShowCombi);
3564
+ } catch (error) {
3565
+ handleError(error, this.logger, this.ctx.dota2tracker.i18n, this.config);
3566
+ }
3567
+ }
3568
+ async report_legacy(timeAgo, titleKey, showCombi) {
3553
3569
  const subscribedGuilds = await this.ctx.database.get("dt_subscribed_guilds", void 0);
3554
3570
  const subscribedPlayersInGuild = (await this.ctx.database.get("dt_subscribed_players", void 0)).filter((player) => subscribedGuilds.some((guild) => guild.guildId == player.guildId));
3555
3571
  const steamIds = subscribedPlayersInGuild.map((player) => player.steamId).filter((value, index, self) => self.indexOf(value) === index);
3556
- const players = (await this.ctx.dota2tracker.stratzAPI.queryPlayersMatchesForDaily(steamIds, timeAgo)).players.filter((player) => player.matches?.length > 0);
3572
+ const players = (await this.ctx.dota2tracker.stratzAPI.queryPlayersMatchesForDaily_legacy(steamIds, timeAgo)).players.filter((player) => player.matches?.length > 0);
3557
3573
  const matches = players.map((player) => player.matches.map((match) => match)).flat().filter((item, index, self) => index === self.findIndex((t) => t.id === item.id));
3558
3574
  for (let subPlayer of subscribedPlayersInGuild) {
3559
3575
  let player = players.find((player2) => subPlayer.steamId == player2.steamAccount.id);
@@ -3615,7 +3631,7 @@ var ReportTask = class extends import_koishi15.Service {
3615
3631
  combinations,
3616
3632
  showCombi
3617
3633
  },
3618
- "daily",
3634
+ "daily_legacy",
3619
3635
  "report" /* Report */,
3620
3636
  languageTag
3621
3637
  )
@@ -4537,7 +4553,8 @@ __name(convertBuildingEvents, "convertBuildingEvents");
4537
4553
 
4538
4554
  // src/app/core/report.service.ts
4539
4555
  var import_koishi18 = require("koishi");
4540
- var ReportService = class extends import_koishi18.Service {
4556
+ var import_luxon11 = require("luxon");
4557
+ var ReportService = class _ReportService extends import_koishi18.Service {
4541
4558
  static {
4542
4559
  __name(this, "ReportService");
4543
4560
  }
@@ -4558,7 +4575,288 @@ var ReportService = class extends import_koishi18.Service {
4558
4575
  partyId: player.partyId
4559
4576
  });
4560
4577
  }
4561
- this.ctx.dota2tracker.database.insertReportData(extensionData.matchId, new Date(match.startDateTime * 1e3), extensionData);
4578
+ this.ctx.dota2tracker.database.insertMatchExtension(extensionData.matchId, new Date(match.startDateTime * 1e3), extensionData);
4579
+ }
4580
+ /**
4581
+ * 入口函数,返回报告数据
4582
+ */
4583
+ async generateDailyReportBundles(options = { days: 1 }) {
4584
+ const today = import_luxon11.DateTime.now().startOf("day");
4585
+ const targetDate = today.minus({ days: options.days });
4586
+ const dataStartDate = targetDate.minus({ days: options.days });
4587
+ const users = await this.ctx.dota2tracker.database.getActiveSubscribedPlayers();
4588
+ const steamIds = [...new Set(users.map((user) => user.steamId))];
4589
+ const data = await this.ctx.dota2tracker.stratzAPI.queryPlayersMatchesForDaily(steamIds, Math.floor(dataStartDate.toSeconds()));
4590
+ const allMatchIds = [...new Set(data.players.flatMap((p) => p.matches.map((m) => m.id)))].map((id) => Number(id));
4591
+ const extensions = await this.ctx.dota2tracker.database.getMatchExtension(allMatchIds);
4592
+ const getImageUrl = this.ctx.dota2tracker.view.getImageUrl.bind(this.ctx.dota2tracker.view);
4593
+ return await _ReportService.formatDailyReportBundles(
4594
+ data,
4595
+ users,
4596
+ extensions,
4597
+ this.ctx.dota2tracker.dotaconstants,
4598
+ targetDate,
4599
+ async (platform, guildId) => {
4600
+ const lang = await this.ctx.dota2tracker.i18n.getLanguageTag({ channelId: guildId });
4601
+ return {
4602
+ t: /* @__PURE__ */ __name((key, params) => this.ctx.i18n.render([lang], [key], params).join(""), "t"),
4603
+ locale: lang,
4604
+ getHeroName: /* @__PURE__ */ __name((heroId) => this.ctx.dota2tracker.i18n.$t(lang, `dota2tracker.template.hero_names.${heroId}`), "getHeroName")
4605
+ };
4606
+ },
4607
+ getImageUrl
4608
+ );
4609
+ }
4610
+ /**
4611
+ * 静态格式化函数,解耦数据获取与处理逻辑,方便测试与 HMR
4612
+ */
4613
+ static async formatDailyReportBundles(data, users, extensions, dotaconstants, targetDate, getTranslator, getImageUrl) {
4614
+ const bundles = [];
4615
+ const groups = this.groupUsersByChannel(users);
4616
+ for (const [key, squadUsers] of groups.entries()) {
4617
+ const [platform, channelId] = key.split(":");
4618
+ const { t, locale, getHeroName } = await getTranslator(platform, channelId);
4619
+ const squadSteamIds = squadUsers.map((u) => u.steamId);
4620
+ const squadPlayerData = data.players.filter((p) => squadSteamIds.includes(p.steamAccount.id));
4621
+ if (squadPlayerData.length === 0) continue;
4622
+ const squadStats = this.calculateSquadStats(squadPlayerData, squadSteamIds, targetDate);
4623
+ if (squadStats.totalMatches === 0) continue;
4624
+ const playerRows = [];
4625
+ const playerStats = [];
4626
+ const impactData = [];
4627
+ for (const user of squadUsers) {
4628
+ const playerData = squadPlayerData.find((p) => p.steamAccount.id === user.steamId);
4629
+ if (!playerData || playerData.matches.length === 0) continue;
4630
+ const processed = this.processPlayer(user, playerData, dotaconstants, targetDate, extensions, getImageUrl);
4631
+ if (processed.impact.matchCount === 0) continue;
4632
+ playerRows.push(processed.row);
4633
+ playerStats.push(processed.stats);
4634
+ impactData.push(processed.impact);
4635
+ }
4636
+ if (playerRows.length === 0) continue;
4637
+ this.calculateImpactPercentages(impactData);
4638
+ playerStats.sort((a, b) => b.maxMvpScore - a.maxMvpScore || b.avgKda - a.avgKda);
4639
+ const mvpStat = playerStats[0];
4640
+ const lvpStat = playerStats[playerStats.length - 1];
4641
+ const mvpPlayerData = squadPlayerData.find((p) => p.steamAccount.id === mvpStat.steamId);
4642
+ const lvpPlayerData = squadPlayerData.find((p) => p.steamAccount.id === lvpStat.steamId);
4643
+ playerRows.sort((a, b) => parseFloat(b.kda.ratio) - parseFloat(a.kda.ratio));
4644
+ playerRows.forEach((row, i) => row.rank = i + 1);
4645
+ const report = {
4646
+ meta: {
4647
+ date: targetDate.setLocale(locale).toFormat(t("dota2tracker.template.report.daily.meta.date_format")),
4648
+ summary: t("dota2tracker.template.report.daily.meta.summary", [channelId]),
4649
+ // footerId: t("dota2tracker.template.report.daily.meta.footer_format", [channelId.slice(-4).toUpperCase(), platform.toUpperCase()]),
4650
+ footerId: "koishi-plugin-@sjtdev/dota2tracker"
4651
+ },
4652
+ headerStats: {
4653
+ matches: { value: squadStats.totalMatches, subtext: t("dota2tracker.template.report.daily.stats.matches_subtext", [squadStats.totalWins, squadStats.totalMatches - squadStats.totalWins]) },
4654
+ winRate: {
4655
+ value: `${squadStats.winRate.toFixed(1)}%`,
4656
+ subtext: `${t("dota2tracker.template.report.daily.stats.vs_yesterday")} ${squadStats.winRateDiff >= 0 ? "▲" : "▼"} ${Math.abs(squadStats.winRateDiff).toFixed(1)}%`,
4657
+ isPositive: squadStats.winRateDiff >= 0,
4658
+ isWinRateAbove50: squadStats.winRate >= 50
4659
+ },
4660
+ kills: { value: squadStats.totalKills.toLocaleString(), subtext: t("dota2tracker.template.report.daily.stats.kills_avg", [squadStats.avgKills.toFixed(1)]) },
4661
+ duration: { value: this.formatDuration(squadStats.totalDuration), subtext: `${t("dota2tracker.template.report.daily.stats.avg_time")} ${this.formatDuration(squadStats.avgDuration)}` }
4662
+ },
4663
+ spotlights: {
4664
+ mvp: this.buildSpotlightCard(mvpPlayerData, "MVP", mvpStat.bestMatchId, extensions, dotaconstants, t, getHeroName, getImageUrl),
4665
+ lvp: this.buildSpotlightCard(lvpPlayerData, "LVP", lvpStat.worstMatchId, extensions, dotaconstants, t, getHeroName, getImageUrl)
4666
+ },
4667
+ squad: playerRows
4668
+ };
4669
+ bundles.push({ channelId, platform, report });
4670
+ }
4671
+ return bundles;
4672
+ }
4673
+ static groupUsersByChannel(users) {
4674
+ const groups = /* @__PURE__ */ new Map();
4675
+ for (const user of users) {
4676
+ const key = `${user.platform}:${user.guildId}`;
4677
+ if (!groups.has(key)) groups.set(key, []);
4678
+ groups.get(key).push(user);
4679
+ }
4680
+ return groups;
4681
+ }
4682
+ static calculateSquadStats(squadPlayerData, squadSteamIds, targetDate) {
4683
+ const targetSeconds = targetDate.toSeconds();
4684
+ const currentMatches = /* @__PURE__ */ new Map();
4685
+ const previousMatches = /* @__PURE__ */ new Map();
4686
+ for (const player of squadPlayerData) {
4687
+ for (const match of player.matches) {
4688
+ if (match.startDateTime >= targetSeconds) {
4689
+ currentMatches.set(match.id, match);
4690
+ } else {
4691
+ previousMatches.set(match.id, match);
4692
+ }
4693
+ }
4694
+ }
4695
+ const calcStats = /* @__PURE__ */ __name((matchesMap) => {
4696
+ const matchesArray = Array.from(matchesMap.values());
4697
+ const totalMatches = matchesArray.length;
4698
+ let totalWins = 0;
4699
+ let totalKills = 0;
4700
+ let totalDuration = 0;
4701
+ for (const match of matchesArray) {
4702
+ totalDuration += match.durationSeconds;
4703
+ const squadMembersInMatch = match.players.filter((p) => squadSteamIds.includes(p.steamAccount?.id));
4704
+ if (squadMembersInMatch.some((p) => p.isRadiant === match.didRadiantWin)) {
4705
+ totalWins += 1;
4706
+ }
4707
+ totalKills += squadMembersInMatch.reduce((sum, p) => sum + (p.kills || 0), 0);
4708
+ }
4709
+ return {
4710
+ totalMatches,
4711
+ totalWins,
4712
+ totalKills,
4713
+ totalDuration,
4714
+ winRate: totalMatches > 0 ? totalWins / totalMatches * 100 : 0,
4715
+ avgKills: totalMatches > 0 ? totalKills / totalMatches : 0,
4716
+ avgDuration: totalMatches > 0 ? totalDuration / totalMatches : 0
4717
+ };
4718
+ }, "calcStats");
4719
+ const currentStats = calcStats(currentMatches);
4720
+ const previousStats = calcStats(previousMatches);
4721
+ return {
4722
+ ...currentStats,
4723
+ winRateDiff: currentStats.winRate - previousStats.winRate
4724
+ };
4725
+ }
4726
+ static processPlayer(user, playerData, dotaconstants, targetDate, extensions, getImageUrl) {
4727
+ const targetSeconds = targetDate.toSeconds();
4728
+ let pWins = 0, pKills = 0, pDeaths = 0, pAssists = 0;
4729
+ let pHeroDamage = 0, pTowerDamage = 0, pNetworth = 0;
4730
+ let kdaSum = 0, mvpScoreSum = 0, bestScore = -1, worstScore = Infinity;
4731
+ let bestKda = -1, worstKda = Infinity;
4732
+ let bestMatchId = 0, worstMatchId = 0;
4733
+ const playedHeroes = /* @__PURE__ */ new Map();
4734
+ let processedMatchCount = 0;
4735
+ for (const m of playerData.matches) {
4736
+ if (m.startDateTime < targetSeconds) continue;
4737
+ processedMatchCount++;
4738
+ const self = m.players.find((p) => p.steamAccount?.id === user.steamId);
4739
+ if (self.isRadiant === m.didRadiantWin) pWins++;
4740
+ pKills += self.kills || 0;
4741
+ pDeaths += self.deaths || 0;
4742
+ pAssists += self.assists || 0;
4743
+ pHeroDamage += self.heroDamage || 0;
4744
+ pTowerDamage += self.towerDamage || 0;
4745
+ pNetworth += self.networth || 0;
4746
+ const matchKda = ((self.kills || 0) + (self.assists || 0)) / Math.max(1, self.deaths || 0);
4747
+ kdaSum += matchKda;
4748
+ const extension = extensions.find((e) => Number(e.matchId) === Number(m.id));
4749
+ const playerExtension = extension?.data?.players?.find((p) => p.steamAccountId === user.steamId);
4750
+ const mvpScore = playerExtension?.mvpScore || 0;
4751
+ mvpScoreSum += mvpScore;
4752
+ if (mvpScore > bestScore || mvpScore === bestScore && matchKda > bestKda) {
4753
+ bestScore = mvpScore;
4754
+ bestKda = matchKda;
4755
+ bestMatchId = m.id;
4756
+ }
4757
+ if (mvpScore < worstScore || mvpScore === worstScore && matchKda < worstKda) {
4758
+ worstScore = mvpScore;
4759
+ worstKda = matchKda;
4760
+ worstMatchId = m.id;
4761
+ }
4762
+ const current = playedHeroes.get(self.heroId) || { count: 0, wins: 0 };
4763
+ current.count++;
4764
+ if (self.isRadiant === m.didRadiantWin) current.wins++;
4765
+ playedHeroes.set(self.heroId, current);
4766
+ }
4767
+ const matchCount = processedMatchCount;
4768
+ const sortedHeroes = Array.from(playedHeroes.entries()).sort((a, b) => b[1].wins - a[1].wins || a[1].count - b[1].count);
4769
+ const row = {
4770
+ rank: 0,
4771
+ player: {
4772
+ name: user.nickName || playerData.steamAccount.name || "Unknown",
4773
+ avatarUrl: playerData.steamAccount.avatar || "",
4774
+ winCount: pWins,
4775
+ loseCount: matchCount - pWins
4776
+ },
4777
+ heroes: sortedHeroes.slice(0, 3).map(([heroId, stats]) => {
4778
+ const hero = dotaconstants.heroes[heroId];
4779
+ return {
4780
+ url: hero ? getImageUrl(hero.name.replace("npc_dota_hero_", ""), "heroes" /* Heroes */) : "",
4781
+ wins: stats.wins,
4782
+ losses: stats.count - stats.wins
4783
+ };
4784
+ }),
4785
+ plusHeroesCount: Math.max(0, sortedHeroes.length - 3),
4786
+ kda: {
4787
+ ratio: pKills + pAssists === 0 ? "0.0" : ((pKills + pAssists) / Math.max(1, pDeaths)).toFixed(1),
4788
+ detail: matchCount > 0 ? `${(pKills / matchCount).toFixed(1)} / ${(pDeaths / matchCount).toFixed(1)} / ${(pAssists / matchCount).toFixed(1)}` : "0.0/0.0/0.0"
4789
+ },
4790
+ impact: {
4791
+ damage: { heroPercent: 0, buildingsPercent: 0 },
4792
+ networth: { percent: 0 }
4793
+ }
4794
+ };
4795
+ return {
4796
+ row,
4797
+ stats: {
4798
+ steamId: user.steamId,
4799
+ avgKda: matchCount > 0 ? kdaSum / matchCount : 0,
4800
+ maxMvpScore: bestScore,
4801
+ // Use bestScore as maxMvpScore
4802
+ bestMatchId,
4803
+ worstMatchId
4804
+ },
4805
+ impact: { heroDamage: pHeroDamage, towerDamage: pTowerDamage, networth: pNetworth, matchCount, row }
4806
+ };
4807
+ }
4808
+ static calculateImpactPercentages(impactData) {
4809
+ let maxAvgTotalDamage = 0;
4810
+ let maxAvgNetworth = 0;
4811
+ for (const data of impactData) {
4812
+ const avgTotalDamage = (data.heroDamage + data.towerDamage) / data.matchCount;
4813
+ const avgNetworth = data.networth / data.matchCount;
4814
+ if (avgTotalDamage > maxAvgTotalDamage) maxAvgTotalDamage = avgTotalDamage;
4815
+ if (avgNetworth > maxAvgNetworth) maxAvgNetworth = avgNetworth;
4816
+ }
4817
+ for (const data of impactData) {
4818
+ const avgHeroDamage = data.heroDamage / data.matchCount;
4819
+ const avgTowerDamage = data.towerDamage / data.matchCount;
4820
+ const avgNetworth = data.networth / data.matchCount;
4821
+ const heroPercent = maxAvgTotalDamage > 0 ? Math.round(avgHeroDamage / maxAvgTotalDamage * 100) : 0;
4822
+ const buildingsPercent = maxAvgTotalDamage > 0 ? Math.round(avgTowerDamage / maxAvgTotalDamage * 100) : 0;
4823
+ data.row.impact.damage.heroPercent = Math.min(100, heroPercent);
4824
+ data.row.impact.damage.buildingsPercent = Math.min(100 - data.row.impact.damage.heroPercent, buildingsPercent);
4825
+ data.row.impact.networth.percent = maxAvgNetworth > 0 ? Math.round(avgNetworth / maxAvgNetworth * 100) : 0;
4826
+ }
4827
+ }
4828
+ static formatDuration(seconds) {
4829
+ const mins = Math.floor(seconds / 60);
4830
+ const secs = Math.floor(seconds % 60);
4831
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
4832
+ }
4833
+ static buildSpotlightCard(playerData, type, matchId, extensions, dotaconstants, t, getHeroName, getImageUrl) {
4834
+ const match = playerData.matches.find((m) => m.id === matchId);
4835
+ const self = match.players.find((p) => p.steamAccount?.id === playerData.steamAccount.id);
4836
+ const extension = extensions.find((e) => Number(e.matchId) === Number(matchId));
4837
+ const playerExtension = extension?.data?.players?.find((p) => p.steamAccountId === playerData.steamAccount.id);
4838
+ const badgeKeys = playerExtension?.titles || [];
4839
+ const mvpScore = playerExtension?.mvpScore || 0;
4840
+ const matchKda = ((self.kills || 0) + (self.assists || 0)) / Math.max(1, self.deaths || 0);
4841
+ return {
4842
+ type,
4843
+ player: {
4844
+ name: playerData.steamAccount.name || "Unknown",
4845
+ heroName: getHeroName(self.heroId),
4846
+ kda: `${self.kills || 0} / ${self.deaths || 0} / ${self.assists || 0} (${matchKda.toFixed(1)})`,
4847
+ heroBannerUrl: dotaconstants.heroes[self.heroId] ? getImageUrl(dotaconstants.heroes[self.heroId].name.replace("npc_dota_hero_", ""), "heroes" /* Heroes */, "png" /* png */) : "",
4848
+ avatarUrl: playerData.steamAccount.avatar || ""
4849
+ },
4850
+ score: {
4851
+ value: mvpScore ? mvpScore.toFixed(1) : "-",
4852
+ label: t("dota2tracker.template.report.daily.spotlight.score_label")
4853
+ },
4854
+ badges: badgeKeys.map((key) => {
4855
+ const translated = t(key);
4856
+ const [text, hexColor] = translated.split("-#");
4857
+ return { text: text || key, hexColor: hexColor ? `#${hexColor}` : "#FFA500" };
4858
+ })
4859
+ };
4562
4860
  }
4563
4861
  };
4564
4862
 
@@ -4627,7 +4925,7 @@ var Config = import_koishi19.Schema.intersect([
4627
4925
  import_koishi19.Schema.object({
4628
4926
  dailyReportSwitch: import_koishi19.Schema.const(true).required(),
4629
4927
  dailyReportHours: import_koishi19.Schema.number().min(0).max(23).default(6),
4630
- dailyReportShowCombi: import_koishi19.Schema.boolean().default(true)
4928
+ dailyReportShowCombi: import_koishi19.Schema.boolean().default(true).deprecated()
4631
4929
  }),
4632
4930
  import_koishi19.Schema.object({})
4633
4931
  ]).i18n(getI18n("report")),
@@ -10,12 +10,17 @@ query PlayersMatchesForDaily($steamAccountIds: [Long]!, $seconds: Long!) {
10
10
  didRadiantWin
11
11
  parsedDateTime
12
12
  startDateTime
13
+ durationSeconds
13
14
  players {
15
+ heroId
14
16
  kills
15
17
  deaths
16
18
  assists
17
19
  imp
18
20
  isRadiant
21
+ heroDamage
22
+ towerDamage
23
+ networth
19
24
  steamAccount {
20
25
  id
21
26
  }
@@ -0,0 +1,25 @@
1
+ query PlayersMatchesForDaily_Legacy($steamAccountIds: [Long]!, $seconds: Long!) {
2
+ players(steamAccountIds: $steamAccountIds) {
3
+ steamAccount {
4
+ id
5
+ name
6
+ avatar
7
+ }
8
+ matches(request: { startDateTime: $seconds, take: 50 }) {
9
+ id
10
+ didRadiantWin
11
+ parsedDateTime
12
+ startDateTime
13
+ players {
14
+ kills
15
+ deaths
16
+ assists
17
+ imp
18
+ isRadiant
19
+ steamAccount {
20
+ id
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties;@layer theme,base,components,utilities;@layer theme{:root,:host{--font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-serif: "Cinzel", serif;--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-orange-500: oklch(70.5% .213 47.604);--color-slate-300: oklch(86.9% .022 252.894);--color-slate-400: oklch(70.4% .04 256.788);--color-slate-500: oklch(55.4% .046 257.417);--color-slate-600: oklch(44.6% .043 257.281);--color-slate-700: oklch(37.2% .044 257.287);--color-slate-800: oklch(27.9% .041 260.031);--color-black: #000;--color-white: #fff;--spacing: .25rem;--container-md: 28rem;--text-xs: .75rem;--text-xs--line-height: calc(1 / .75);--text-sm: .875rem;--text-sm--line-height: calc(1.25 / .875);--text-base: 1rem;--text-base--line-height: 1.5 ;--text-lg: 1.125rem;--text-lg--line-height: calc(1.75 / 1.125);--text-xl: 1.25rem;--text-xl--line-height: calc(1.75 / 1.25);--text-2xl: 1.5rem;--text-2xl--line-height: calc(2 / 1.5);--text-3xl: 1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl: 2.25rem;--text-4xl--line-height: calc(2.5 / 2.25);--text-6xl: 3.75rem;--text-6xl--line-height: 1;--font-weight-medium: 500;--font-weight-bold: 700;--font-weight-black: 900;--tracking-tight: -.025em;--tracking-wider: .05em;--tracking-widest: .1em;--leading-tight: 1.25;--radius-lg: .5rem;--radius-xl: .75rem;--drop-shadow-md: 0 3px 3px rgb(0 0 0 / .12);--blur-sm: 8px;--default-transition-duration: .15s;--default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);--default-font-family: var(--font-sans);--default-mono-font-family: var(--font-mono);--color-primary: #137fec;--color-background-dark: #101922;--color-dota-red: #ff3c3c;--color-dota-gold: #e8bc56;--color-dota-green: #0bda5b;--color-card-dark: #16202c;--font-display: "Inter", sans-serif}}@layer base{*,:after,:before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings, normal);font-variation-settings:var(--default-font-variation-settings, normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings, normal);font-variation-settings:var(--default-mono-font-variation-settings, normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:transparent;opacity:1}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::placeholder{color:currentcolor;@supports (color: color-mix(in lab,red,red)){color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]),::file-selector-button{appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.z-10{z-index:10}.col-span-1{grid-column:span 1 / span 1}.col-span-2{grid-column:span 2 / span 2}.col-span-3{grid-column:span 3 / span 3}.col-span-4{grid-column:span 4 / span 4}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-\[27px\]{height:27px}.h-full{height:100%}.min-h-\[22px\]{min-height:22px}.min-h-\[280px\]{min-height:280px}.min-h-screen{min-height:100vh}.w-1{width:calc(var(--spacing) * 1)}.w-6{width:calc(var(--spacing) * 6)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-\[48px\]{width:48px}.w-full{width:100%}.max-w-\[900px\]{max-width:900px}.max-w-md{max-width:var(--container-md)}.flex-1{flex:1}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:calc(infinity * 1px)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-dota-gold{border-color:var(--color-dota-gold)}.border-dota-red{border-color:var(--color-dota-red)}.border-slate-600{border-color:var(--color-slate-600)}.border-slate-700{border-color:var(--color-slate-700)}.border-white\/5{border-color:color-mix(in srgb,#fff 5%,transparent);@supports (color: color-mix(in lab,red,red)){border-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.border-white\/10{border-color:color-mix(in srgb,#fff 10%,transparent);@supports (color: color-mix(in lab,red,red)){border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.bg-\[\#0d141c\]{background-color:#0d141c}.bg-\[\#16202c\]{background-color:#16202c}.bg-black\/20{background-color:color-mix(in srgb,#000 20%,transparent);@supports (color: color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.bg-black\/40{background-color:color-mix(in srgb,#000 40%,transparent);@supports (color: color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-card-dark{background-color:var(--color-card-dark)}.bg-dota-gold{background-color:var(--color-dota-gold)}.bg-dota-gold\/50{background-color:color-mix(in srgb,#e8bc56 50%,transparent);@supports (color: color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-dota-gold) 50%,transparent)}}.bg-dota-green\/50{background-color:color-mix(in srgb,#0bda5b 50%,transparent);@supports (color: color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-dota-green) 50%,transparent)}}.bg-dota-red{background-color:var(--color-dota-red)}.bg-dota-red\/50{background-color:color-mix(in srgb,#ff3c3c 50%,transparent);@supports (color: color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-dota-red) 50%,transparent)}}.bg-orange-500{background-color:var(--color-orange-500)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-gradient-to-t{--tw-gradient-position: to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-card-dark{--tw-gradient-from: var(--color-card-dark);--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-card-dark\/80{--tw-gradient-via: color-mix(in srgb, #16202c 80%, transparent);@supports (color: color-mix(in lab,red,red)){--tw-gradient-via: color-mix(in oklab, var(--color-card-dark) 80%, transparent)}--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-via-stops)}.to-transparent{--tw-gradient-to: transparent;--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-cover{background-size:cover}.bg-center{background-position:center}.fill-current{fill:currentcolor}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pb-0\.5{padding-bottom:calc(var(--spacing) * .5)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.font-display{font-family:var(--font-display)}.font-mono{font-family:var(--font-mono)}.font-serif{font-family:var(--font-serif)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading, var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading, var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading, var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading, var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading, var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading, var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading, var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading, var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading: 1;line-height:1}.leading-tight{--tw-leading: var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight: var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight: var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight: var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-\[0\.2em\]{--tw-tracking: .2em;letter-spacing:.2em}.tracking-tight{--tw-tracking: var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking: var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking: var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-black{color:var(--color-black)}.text-dota-gold{color:var(--color-dota-gold)}.text-dota-green{color:var(--color-dota-green)}.text-dota-red{color:var(--color-dota-red)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-60{opacity:60%}.shadow-\[0_0_30px_rgba\(232\,188\,86\,0\.15\)\]{--tw-shadow: 0 0 30px var(--tw-shadow-color, rgba(232,188,86,.15));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_30px_rgba\(255\,60\,60\,0\.15\)\]{--tw-shadow: 0 0 30px var(--tw-shadow-color, rgba(255,60,60,.15));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / .1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / .1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-dota-gold\/20{--tw-shadow-color: color-mix(in srgb, #e8bc56 20%, transparent);@supports (color: color-mix(in lab,red,red)){--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-dota-gold) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-dota-red\/20{--tw-shadow-color: color-mix(in srgb, #ff3c3c 20%, transparent);@supports (color: color-mix(in lab,red,red)){--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-dota-red) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.drop-shadow-\[0_1px_2px_rgba\(0\,0\,0\,0\.8\)\]{--tw-drop-shadow-size: drop-shadow(0 1px 2px var(--tw-drop-shadow-color, rgba(0,0,0,.8)));--tw-drop-shadow: var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_2px_10px_rgba\(0\,0\,0\,0\.5\)\]{--tw-drop-shadow-size: drop-shadow(0 2px 10px var(--tw-drop-shadow-color, rgba(0,0,0,.5)));--tw-drop-shadow: var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-md{--tw-drop-shadow-size: drop-shadow(0 3px 3px var(--tw-drop-shadow-color, rgb(0 0 0 / .12)));--tw-drop-shadow: drop-shadow(var(--drop-shadow-md));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur: blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function));transition-duration:var(--tw-duration, var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function));transition-duration:var(--tw-duration, var(--default-transition-duration))}.duration-700{--tw-duration: .7s;transition-duration:.7s}.group-hover\:scale-105{&:is(:where(.group):hover *){@media (hover: hover){--tw-scale-x: 105%;--tw-scale-y: 105%;--tw-scale-z: 105%;scale:var(--tw-scale-x) var(--tw-scale-y)}}}.hover\:bg-white\/\[0\.02\]{&:hover{@media (hover: hover){background-color:color-mix(in srgb,#fff 2%,transparent);@supports (color: color-mix(in lab,red,red)){background-color:color-mix(in oklab,var(--color-white) 2%,transparent)}}}}.sm\:px-6{@media (width >= 40rem){padding-inline:calc(var(--spacing) * 6)}}.md\:grid{@media (width >= 48rem){display:grid}}.md\:grid-cols-4{@media (width >= 48rem){grid-template-columns:repeat(4,minmax(0,1fr))}}.md\:grid-cols-12{@media (width >= 48rem){grid-template-columns:repeat(12,minmax(0,1fr))}}.md\:flex-row{@media (width >= 48rem){flex-direction:row}}.md\:text-6xl{@media (width >= 48rem){font-size:var(--text-6xl);line-height:var(--tw-leading, var(--text-6xl--line-height))}}}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:var(--color-background-dark)}::-webkit-scrollbar-thumb{background:#233648;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--color-primary)}@property --tw-border-style{syntax: "*"; inherits: false; initial-value: solid;}@property --tw-gradient-position{syntax: "*"; inherits: false;}@property --tw-gradient-from{syntax: "<color>"; inherits: false; initial-value: #0000;}@property --tw-gradient-via{syntax: "<color>"; inherits: false; initial-value: #0000;}@property --tw-gradient-to{syntax: "<color>"; inherits: false; initial-value: #0000;}@property --tw-gradient-stops{syntax: "*"; inherits: false;}@property --tw-gradient-via-stops{syntax: "*"; inherits: false;}@property --tw-gradient-from-position{syntax: "<length-percentage>"; inherits: false; initial-value: 0%;}@property --tw-gradient-via-position{syntax: "<length-percentage>"; inherits: false; initial-value: 50%;}@property --tw-gradient-to-position{syntax: "<length-percentage>"; inherits: false; initial-value: 100%;}@property --tw-leading{syntax: "*"; inherits: false;}@property --tw-font-weight{syntax: "*"; inherits: false;}@property --tw-tracking{syntax: "*"; inherits: false;}@property --tw-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: "*"; inherits: false;}@property --tw-shadow-alpha{syntax: "<percentage>"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: "*"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: "<percentage>"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: "*"; inherits: false;}@property --tw-ring-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: "*"; inherits: false;}@property --tw-inset-ring-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: "*"; inherits: false;}@property --tw-ring-offset-width{syntax: "<length>"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: "*"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: "*"; inherits: false; initial-value: 0 0 #0000;}@property --tw-blur{syntax: "*"; inherits: false;}@property --tw-brightness{syntax: "*"; inherits: false;}@property --tw-contrast{syntax: "*"; inherits: false;}@property --tw-grayscale{syntax: "*"; inherits: false;}@property --tw-hue-rotate{syntax: "*"; inherits: false;}@property --tw-invert{syntax: "*"; inherits: false;}@property --tw-opacity{syntax: "*"; inherits: false;}@property --tw-saturate{syntax: "*"; inherits: false;}@property --tw-sepia{syntax: "*"; inherits: false;}@property --tw-drop-shadow{syntax: "*"; inherits: false;}@property --tw-drop-shadow-color{syntax: "*"; inherits: false;}@property --tw-drop-shadow-alpha{syntax: "<percentage>"; inherits: false; initial-value: 100%;}@property --tw-drop-shadow-size{syntax: "*"; inherits: false;}@property --tw-backdrop-blur{syntax: "*"; inherits: false;}@property --tw-backdrop-brightness{syntax: "*"; inherits: false;}@property --tw-backdrop-contrast{syntax: "*"; inherits: false;}@property --tw-backdrop-grayscale{syntax: "*"; inherits: false;}@property --tw-backdrop-hue-rotate{syntax: "*"; inherits: false;}@property --tw-backdrop-invert{syntax: "*"; inherits: false;}@property --tw-backdrop-opacity{syntax: "*"; inherits: false;}@property --tw-backdrop-saturate{syntax: "*"; inherits: false;}@property --tw-backdrop-sepia{syntax: "*"; inherits: false;}@property --tw-duration{syntax: "*"; inherits: false;}@property --tw-scale-x{syntax: "*"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: "*"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: "*"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-gradient-position: initial;--tw-gradient-from: #0000;--tw-gradient-via: #0000;--tw-gradient-to: #0000;--tw-gradient-stops: initial;--tw-gradient-via-stops: initial;--tw-gradient-from-position: 0%;--tw-gradient-via-position: 50%;--tw-gradient-to-position: 100%;--tw-leading: initial;--tw-font-weight: initial;--tw-tracking: initial;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-blur: initial;--tw-brightness: initial;--tw-contrast: initial;--tw-grayscale: initial;--tw-hue-rotate: initial;--tw-invert: initial;--tw-opacity: initial;--tw-saturate: initial;--tw-sepia: initial;--tw-drop-shadow: initial;--tw-drop-shadow-color: initial;--tw-drop-shadow-alpha: 100%;--tw-drop-shadow-size: initial;--tw-backdrop-blur: initial;--tw-backdrop-brightness: initial;--tw-backdrop-contrast: initial;--tw-backdrop-grayscale: initial;--tw-backdrop-hue-rotate: initial;--tw-backdrop-invert: initial;--tw-backdrop-opacity: initial;--tw-backdrop-saturate: initial;--tw-backdrop-sepia: initial;--tw-duration: initial;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}
@@ -0,0 +1 @@
1
+ @theme{ --color-primary: #137fec; --color-background-light: #f6f7f8; --color-background-dark: #101922; --color-dota-red: #ff3c3c; --color-dota-dark-red: #b91c1c; --color-dota-gold: #e8bc56; --color-dota-dark-gold: #b48518; --color-dota-green: #0bda5b; --color-card-dark: #16202c; --color-card-darker: #0d141c; --color-surface: #1c2633; --font-display: "Inter", sans-serif; --font-serif: "Cinzel", serif; --image-smoke-pattern: radial-gradient(circle at 50% 50%, rgba(20, 30, 40, 0) 0%, rgba(16, 25, 34, .8) 100%); --image-topographic: linear-gradient(rgba(16, 25, 34, .95), rgba(16, 25, 34, .95)), repeating-linear-gradient(45deg, #1a2634 0px, #1a2634 1px, transparent 1px, transparent 10px); }::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:var(--color-background-dark)}::-webkit-scrollbar-thumb{background:#233648;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--color-primary)}
@@ -1,21 +1,7 @@
1
- <% const { title, players, combinations, showCombi } = data; %> <% const getImpInfo = (impVal) => {
2
- const abs = Math.abs(impVal);
3
- const isPos = impVal > 0;
4
- const isOver = abs > 25;
5
-
6
- let left = 0, right = 0;
7
- if (isOver) {
8
- if (isPos) right = abs;
9
- else left = abs;
10
- } else {
11
- left = right = abs;
12
- }
13
-
14
- return {
15
- valStr: (isPos ? "+" : "") + impVal,
16
- barClass: `score_bar ${isPos ? "pos" : "neg"}${isOver ? " over" : ""}`,
17
- leftStyle: `width: ${left}px`,
18
- rightStyle: `width: ${right}px`
19
- };
20
- };
21
- %> <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Daily Report</title> <%- `<style>` %> <%- include("../common/styles/normalize.min.css") %> <%- include("./daily/base.css") %> <% if (fontFamily) { %><%- `body { font-family: ${fontFamily}; }` %><% } %> <%- `</style>` %> </head><body><h3 class="title"><%= title %></h3><div class="players"> <% players.forEach(p => { %> <% const winRate = Math.round((p.winCount / p.matches.length) * 100); %> <% const imp = getImpInfo(p.avgImp); %> <div class="player"><img src="<%= p.steamAccount.avatar %>" class="avatar"/> <span class="name"><%= p.name %></span><span class="count"><span class="win"><%= $t("dota2tracker.template.report_won") %><%= p.winCount %></span><span class="lose"><%= $t("dota2tracker.template.report_lost") %><%= p.loseCount %></span><span><%= $t("dota2tracker.template.report_winrate") %> <%= winRate %>%</span></span><div class="performance"><div class="<%= imp.barClass %>"><div class="left" <%- `style="${imp.leftStyle}"` %>></div><div class="pipe"></div><div class="right" <%- `style="${imp.rightStyle}"` %>></div></div><span class="score_value"><%= imp.valStr %></span></div><span class="kda"><%= p.avgKills %>/<%= p.avgDeaths %>/<%= p.avgAssists %> (<%= p.avgKDA %>)</span></div> <% }); %> </div><div class="combinations" <%- !showCombi ? `style="display:none;"` : "" %>><span style="grid-column:1/-1"><%= $t("dota2tracker.template.combined_win_loss_summary") %></span> <% combinations.forEach(combi => { %> <% const combiRate = Math.round((combi.winCount / combi.matches.length) * 100); %> <div class="players"> <% combi.players.forEach(p => { %> <img src="<%= p.steamAccount.avatar %>" class="avatar"/> <% }); %> </div><span class="win"><%= $t("dota2tracker.template.report_won") %><%= combi.winCount %></span><span class="lose"><%= $t("dota2tracker.template.report_lost") %><%= combi.matches.length - combi.winCount %></span><span><%= $t("dota2tracker.template.report_winrate") %> <%= combiRate %>%</span> <% }); %> </div></body></html>
1
+ <!DOCTYPE html><html class="dark" lang="en"><head><meta charset="utf-8"/><meta content="width=device-width,initial-scale=1" name="viewport"/><title>Dota 2 Daily Recap</title> <%- `<style>` %> <%- include(`./daily/style.css`) %> <% if (fontFamily) { %><%- `body { font-family: ${fontFamily}; }` %><% } %> <%- `</style>` %> <style>::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:#101922}::-webkit-scrollbar-thumb{background:#233648;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#137fec}.bg-texture{background-color:#101922;background-image:radial-gradient(at 0 0,rgba(19,127,236,.08) 0,transparent 50%),radial-gradient(at 100% 0,rgba(255,60,60,.05) 0,transparent 50%),radial-gradient(at 100% 100%,rgba(232,188,86,.05) 0,transparent 50%)}</style></head><body class="bg-texture font-display text-white overflow-x-hidden antialiased min-h-screen"><div class="w-full flex justify-center py-8 px-4 sm:px-6"><div class="w-full max-w-[900px] flex flex-col gap-8"><div class="flex flex-col items-center justify-center pt-2 pb-4 text-center"><div class="mb-3 border-b-2 border-dota-red pb-1"><span class="text-xs font-bold tracking-[0.2em] uppercase text-dota-gold drop-shadow-md"> <%= $t("dota2tracker.template.report.daily.plugin_name") %> </span></div><h1 class="text-4xl md:text-6xl font-serif font-black tracking-tight text-white uppercase drop-shadow-[0_2px_10px_rgba(0,0,0,0.5)] mb-2"> <%= $t("dota2tracker.template.report.daily.title") %> </h1><div class="flex flex-col items-center gap-1 text-slate-400 font-medium"><p class="text-lg tracking-widest uppercase text-slate-300"><%= data.meta.date %></p><p class="text-xs opacity-60 max-w-md mx-auto"> <%= data.meta.summary %> </p></div></div><div class="grid grid-cols-2 md:grid-cols-4 gap-3"><div class="flex flex-col items-center justify-center p-4 rounded bg-card-dark border border-white/5 shadow-lg relative overflow-hidden group"><span class="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1"> <%= $t("dota2tracker.template.report.daily.stats.matches") %> </span><span class="text-3xl font-black text-white"><%= data.headerStats.matches.value %></span><span class="text-xs text-slate-400 mt-1"><%= data.headerStats.matches.subtext %></span></div><div class="flex flex-col items-center justify-center p-4 rounded bg-card-dark border border-white/5 shadow-lg relative overflow-hidden group"><div class="absolute inset-x-0 bottom-0 h-1 <%= data.headerStats.winRate.isWinRateAbove50 ? 'bg-dota-green/50' : 'bg-dota-red/50' %>"></div><span class="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1"> <%= $t("dota2tracker.template.report.daily.stats.win_rate") %> </span><span class="text-3xl font-black <%= data.headerStats.winRate.isWinRateAbove50 ? 'text-dota-green' : 'text-dota-red' %>"><%= data.headerStats.winRate.value %></span><span class="text-xs mt-1 <%= data.headerStats.winRate.isPositive ? 'text-dota-green' : 'text-dota-red' %>"><%= data.headerStats.winRate.subtext %></span></div><div class="flex flex-col items-center justify-center p-4 rounded bg-card-dark border border-white/5 shadow-lg relative overflow-hidden group"><div class="absolute inset-x-0 bottom-0 h-1 bg-dota-red/50"></div><span class="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1"> <%= $t("dota2tracker.template.report.daily.stats.kills") %> </span><span class="text-3xl font-black text-dota-red"><%= data.headerStats.kills.value %></span><span class="text-xs text-slate-400 mt-1"><%= data.headerStats.kills.subtext %></span></div><div class="flex flex-col items-center justify-center p-4 rounded bg-card-dark border border-white/5 shadow-lg relative overflow-hidden group"><div class="absolute inset-x-0 bottom-0 h-1 bg-dota-gold/50"></div><span class="text-slate-500 text-[10px] font-bold uppercase tracking-widest mb-1"> <%= $t("dota2tracker.template.report.daily.stats.duration") %> </span><span class="text-3xl font-black text-dota-gold"><%= data.headerStats.duration.value %></span><span class="text-xs text-slate-400 mt-1"> <%= data.headerStats.duration.subtext %> </span></div></div><div class="flex flex-col md:flex-row gap-6 mt-2"> <% const lights = [data.spotlights.mvp, data.spotlights.lvp]; %> <% lights.forEach(function(light) { %> <div class="flex-1 relative rounded-lg border <%= light.type === 'MVP' ? 'border-dota-gold shadow-[0_0_30px_rgba(232,188,86,0.15)]' : 'border-dota-red shadow-[0_0_30px_rgba(255,60,60,0.15)]' %> bg-card-dark overflow-hidden group"><div class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105" style="background-image:url('<%= light.player.heroBannerUrl %>');opacity:.4"></div><div class="absolute inset-0 bg-gradient-to-t from-card-dark via-card-dark/80 to-transparent"></div><div class="relative z-10 p-6 flex flex-col h-full min-h-[280px]"><div class="flex justify-between items-center"><div class="<%= light.type === 'MVP' ? 'bg-dota-gold text-black shadow-dota-gold/20' : 'bg-dota-red text-white shadow-dota-red/20' %> text-xs font-black uppercase px-3 py-1 rounded shadow-lg"> <%= light.type === 'MVP' ? $t("dota2tracker.template.report.daily.spotlight.mvp_title") : $t("dota2tracker.template.report.daily.spotlight.lvp_title") %> </div><svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 fill-current" viewBox="0 0 24 24"> <% if (light.type === 'MVP') { %> <path class="text-dota-gold" d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z"/> <% } else { %> <path class="text-dota-red" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> <% } %> </svg></div><div class="mt-auto"><h3 class="text-3xl font-serif font-bold text-white leading-none mb-1 drop-shadow-md"><%= light.player.name %></h3><p class="text-slate-300 text-sm font-medium leading-none mb-1"><%= light.player.heroName %></p><p class="text-slate-400 text-sm font-mono mb-3"><%= light.player.kda %></p><div class="bg-black/40 backdrop-blur-sm rounded-lg p-3 border border-white/10"><div class="flex flex-col gap-2"><div class="flex justify-between items-center border-b border-white/10 pb-2"><span class="text-xs text-slate-400 uppercase tracking-widest font-bold"> <%= light.score.label %> </span><span class="font-mono text-2xl font-bold <%= light.type === 'MVP' ? 'text-dota-gold' : 'text-dota-red' %>"><%= light.score.value %></span></div><div class="flex flex-wrap gap-2 pt-1 min-h-[22px]"> <% if (light.badges && light.badges.length > 0) { %> <% light.badges.forEach(function(badge) { %> <%
2
+ // Convert HEX to RGB for background opacity
3
+ const r = parseInt(badge.hexColor.slice(1, 3), 16);
4
+ const g = parseInt(badge.hexColor.slice(3, 5), 16);
5
+ const b = parseInt(badge.hexColor.slice(5, 7), 16);
6
+ const bgStyle = `background-color: rgba(${r}, ${g}, ${b}, 0.2); border-color: rgba(${r}, ${g}, ${b}, 0.3); color: ${badge.hexColor};`;
7
+ %> <span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase border" style="<%= bgStyle %>"><%= badge.text %></span> <% }); %> <% } else { %> <% } %> </div></div></div></div></div></div> <% }); %> </div><div class="bg-[#0d141c] rounded-xl border border-white/5 overflow-hidden"><div class="flex items-center justify-between p-4 border-b border-white/5 bg-[#16202c]"><h2 class="text-lg font-serif font-bold text-white uppercase tracking-wider flex items-center gap-2"><span class="w-1 h-4 bg-dota-red rounded-full"></span> <%= $t("dota2tracker.template.report.daily.squad.title") %> </h2><div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest bg-black/20 px-2 py-1 rounded border border-white/5"> <%= $t("dota2tracker.template.report.daily.squad.subtitle") %> </div></div><div class="hidden md:grid grid-cols-12 gap-4 px-4 py-3 text-[10px] font-bold text-slate-500 uppercase tracking-widest bg-black/20"><div class="col-span-1 text-center"><%= $t("dota2tracker.template.report.daily.squad.header.rank") %></div><div class="col-span-4 pl-2"><%= $t("dota2tracker.template.report.daily.squad.header.player_info") %></div><div class="col-span-3"><%= $t("dota2tracker.template.report.daily.squad.header.hero_pool") %></div><div class="col-span-2 text-center"><%= $t("dota2tracker.template.report.daily.squad.header.kda") %></div><div class="col-span-2 text-left pl-2"><%= $t("dota2tracker.template.report.daily.squad.header.impact") %></div></div><div class="flex flex-col"> <% data.squad.forEach(function(row) { %> <div class="flex flex-col md:grid md:grid-cols-12 gap-4 p-4 border-b border-white/5 hover:bg-white/[0.02] transition-colors items-center"><div class="col-span-1 flex items-center justify-center"><span class="font-serif italic text-2xl <%= row.rank === 1 ? 'text-dota-gold' : (row.rank === data.squad.length ? 'text-dota-red' : 'text-slate-400') %> font-bold"><%= row.rank %></span></div><div class="col-span-4 w-full flex items-center gap-3 pl-2"><div class="w-12 h-12 rounded bg-cover bg-center border border-slate-600 shadow-md" style="background-image:url('<%= row.player.avatarUrl || '' %>')"></div><div class="flex flex-col"><span class="font-bold text-white text-base leading-tight"><%= row.player.name %></span><div class="flex items-center gap-1 text-xs font-medium mt-0.5"><span class="text-dota-green"><%= row.player.winCount %> <%= $t("dota2tracker.template.won") %></span><span class="text-slate-500">-</span> <span class="text-dota-red"><%= row.player.loseCount %> <%= $t("dota2tracker.template.lost") %></span></div></div></div><div class="col-span-3 w-full flex items-center gap-2"> <% row.heroes.forEach(function(hero) { %> <div class="relative w-[48px] h-[27px] rounded border border-slate-700 shadow-sm overflow-hidden group"><div class="absolute inset-0 bg-cover bg-center" style="background-image:url('<%= hero.url %>')"></div><div class="absolute inset-x-0 bottom-0 flex justify-between px-1 pb-0.5 pointer-events-none"><span class="text-[10px] font-bold leading-none text-dota-green drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]" style="text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000"><%= hero.wins %></span><span class="text-[10px] font-bold leading-none text-dota-red drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]" style="text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000"><%= hero.losses %></span></div></div> <% }); %> <% if (row.plusHeroesCount > 0) { %> <span class="text-[10px] font-bold bg-slate-800 text-slate-400 px-1.5 py-0.5 rounded border border-slate-700">+<%= row.plusHeroesCount %></span> <% } %> </div><div class="col-span-2 w-full flex flex-col items-center justify-center"><span class="text-xl font-bold text-white leading-none"><%= row.kda.ratio %></span><div class="text-xs text-slate-500 font-mono mt-1"><%= row.kda.detail %></div></div><div class="col-span-2 w-full flex flex-col justify-center gap-1.5 pl-2"><div class="flex items-center gap-2 w-full"><span class="text-[9px] w-6 text-slate-500 font-bold uppercase"> <%= $t("dota2tracker.template.report.daily.squad.impact.dmg") %> </span><div class="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden flex"><div class="h-full bg-dota-red" style="width: <%= row.impact.damage.heroPercent %>%"></div><div class="h-full bg-orange-500" style="width: <%= row.impact.damage.buildingsPercent %>%"></div></div></div><div class="flex items-center gap-2 w-full"><span class="text-[9px] w-6 text-slate-500 font-bold uppercase"> <%= $t("dota2tracker.template.report.daily.squad.impact.gold") %> </span><div class="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden"><div class="h-full bg-dota-gold" style="width: <%= row.impact.networth.percent %>%"></div></div></div></div></div> <% }); %> </div></div><div class="flex flex-col items-center justify-center text-center gap-1 pb-6 opacity-60"><div class="flex items-center gap-2 text-dota-gold text-xs font-bold uppercase tracking-widest" style="display:none"><span class="material-symbols-outlined text-sm">auto_awesome</span> <span><%= $t("dota2tracker.template.report.daily.footer.generated_by") %></span></div><p class="text-[10px] text-slate-500 font-mono"><%= data.meta.footerId %></p></div></div></div></body></html>
@@ -0,0 +1,21 @@
1
+ <% const { title, players, combinations, showCombi } = data; %> <% const getImpInfo = (impVal) => {
2
+ const abs = Math.abs(impVal);
3
+ const isPos = impVal > 0;
4
+ const isOver = abs > 25;
5
+
6
+ let left = 0, right = 0;
7
+ if (isOver) {
8
+ if (isPos) right = abs;
9
+ else left = abs;
10
+ } else {
11
+ left = right = abs;
12
+ }
13
+
14
+ return {
15
+ valStr: (isPos ? "+" : "") + impVal,
16
+ barClass: `score_bar ${isPos ? "pos" : "neg"}${isOver ? " over" : ""}`,
17
+ leftStyle: `width: ${left}px`,
18
+ rightStyle: `width: ${right}px`
19
+ };
20
+ };
21
+ %> <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Daily Report</title> <%- `<style>` %> <%- include("../common/styles/normalize.min.css") %> <%- include("./daily_legacy/base.css") %> <% if (fontFamily) { %><%- `body { font-family: ${fontFamily}; }` %><% } %> <%- `</style>` %> </head><body><h3 class="title"><%= title %></h3><div class="players"> <% players.forEach(p => { %> <% const winRate = Math.round((p.winCount / p.matches.length) * 100); %> <% const imp = getImpInfo(p.avgImp); %> <div class="player"><img src="<%= p.steamAccount.avatar %>" class="avatar"/> <span class="name"><%= p.name %></span><span class="count"><span class="win"><%= $t("dota2tracker.template.report_won") %><%= p.winCount %></span><span class="lose"><%= $t("dota2tracker.template.report_lost") %><%= p.loseCount %></span><span><%= $t("dota2tracker.template.report_winrate") %> <%= winRate %>%</span></span><div class="performance"><div class="<%= imp.barClass %>"><div class="left" <%- `style="${imp.leftStyle}"` %>></div><div class="pipe"></div><div class="right" <%- `style="${imp.rightStyle}"` %>></div></div><span class="score_value"><%= imp.valStr %></span></div><span class="kda"><%= p.avgKills %>/<%= p.avgDeaths %>/<%= p.avgAssists %> (<%= p.avgKDA %>)</span></div> <% }); %> </div><div class="combinations" <%- !showCombi ? `style="display:none;"` : "" %>><span style="grid-column:1/-1"><%= $t("dota2tracker.template.combined_win_loss_summary") %></span> <% combinations.forEach(combi => { %> <% const combiRate = Math.round((combi.winCount / combi.matches.length) * 100); %> <div class="players"> <% combi.players.forEach(p => { %> <img src="<%= p.steamAccount.avatar %>" class="avatar"/> <% }); %> </div><span class="win"><%= $t("dota2tracker.template.report_won") %><%= combi.winCount %></span><span class="lose"><%= $t("dota2tracker.template.report_lost") %><%= combi.matches.length - combi.winCount %></span><span><%= $t("dota2tracker.template.report_winrate") %> <%= combiRate %>%</span> <% }); %> </div></body></html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sjtdev/koishi-plugin-dota2tracker",
3
3
  "description": "koishi插件-追踪群友的DOTA2对局 | A Koishi plugin to track Dota 2 matches",
4
- "version": "2.4.0",
4
+ "version": "2.5.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [