From 44eb8896c5cdbd6ed7635ecfc947d7f635edc8b9 Mon Sep 17 00:00:00 2001 From: Hubert Walczak Date: Tue, 16 May 2023 17:39:31 +0200 Subject: [PATCH] Added combat log support --- package.json | 2 +- src/App.tsx | 17 +++- src/summaries.ts | 204 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/summaries.ts diff --git a/package.json b/package.json index 737aa09..1c4b499 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@types/node": "18.15.11", "@types/react": "18.0.35", "@types/react-dom": "18.0.11", - "dotagsi": "^1.1.3", + "dotagsi": "github:Macronic/dota2gsi", "buffer": "^6.0.3", "query-string": "^6.12.1", "react": "^18.2.0", diff --git a/src/App.tsx b/src/App.tsx index be1bcd6..df4cd98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { Match } from './api/interfaces'; import "./HUD/GameHUD/gamehud.scss"; import { exampleData } from './example'; import { initiateConnection } from './HUD/Camera/mediaStream'; +import { GameSummary } from './summaries'; const DOTA2 = new DOTA2GSI(); export const socket = io(isDev ? `localhost:${port}` : '/'); @@ -46,14 +47,22 @@ const dataLoader: DataLoader = { match: null } -class App extends React.Component { +class App extends React.Component { constructor(props: any) { super(props); this.state = { game: null, steamids: [], match: null, - checked: false + checked: false, + summary: { + players: {}, + tickSummary: { + creepsKilled: [], + abilitiesHit: [], + abilitiesUsed: [] + } + } } } @@ -130,6 +139,10 @@ class App extends React.Component { window.top && window.top.location.reload(); }); + + socket.on("combatLogUpdate", (summary: GameSummary) => { + this.setState({ summary }); + }); DOTA2.on('data', data => { if (!this.state.game || this.state.steamids.length) this.verifyPlayers(data); diff --git a/src/summaries.ts b/src/summaries.ts new file mode 100644 index 0000000..76cec49 --- /dev/null +++ b/src/summaries.ts @@ -0,0 +1,204 @@ +import { Player } from "dotagsi"; + +export const BOUNTY_RUNES_REASON_ID = 17; + +const CUSTOM_GOLD_REASON_MIDAS = -1; +const GOLD_REASON_CREEP_KILL = 13; +const MIDAS_GOLD_PER_USE = 160; + +const RuneTypes = [ + "haste", + "arcane", + "bounty", + "double_damage", + "illusion", + "invisibility", + "regeneration", + "water", +] as const; + +type RuneType = (typeof RuneTypes)[number]; + +export enum GoldReason { + Unspecified = 0, + Death = 1, + Buyback = 2, + PurchaseConsumable = 3, + PurchaseItem = 4, + AbandonedRedistribute = 5, + SellItem = 6, + AbilityCost = 7, + CheatCommand = 8, + SelectionPenalty = 9, + GameTick = 10, + Building = 11, + HeroKill = 12, + CreepKill = 13, + RoshanKill = 14, + CourierKill = 15, + SharedGold = 16, + BountyRune = 17, + Midas = CUSTOM_GOLD_REASON_MIDAS, +} + +type GoldReasonSummary = { [goldReason: number]: number }; + +type EnemyType = "melee" | "ranged" | "siege" | "summon" | "neutral" | "other"; + +type GoldEnemyTypeSummary = { + [enemy in EnemyType]: number; +}; + +const summonEnemyNameChecks = [ + "_beastmaster_boar", + "_death_ward", + "_brewmaster_earth", + "_eidolon", + "_visage_familiar", + "_brewmaster_fire", + "_invoker_forged_spirit", + "_furion_treant", + "_scout_hawk", + "_lycan_wolf", + "_lycan_wolf", + "_necromonicon", + "_necromonicon", + "_necromonicon", + "_necromonicon", + "_dark_troll_warlord_skeleton_warrior", + "_broodmother_spiderite", + "_broodmother_spiderling", + "_broodmother_web", + "_lone_druid_bear", + "_brewmaster_storm", + "_furion_treant", + "_undying_zombie", + "_warlock_golem", + "_elder_titan_ancestral_spirit", +]; + +const enemyNameToType = (name: string): EnemyType => { + if (name.includes("_neutral")) { + return "neutral"; + } + + if (name.includes("_creep")) { + if (name.includes("_melee")) { + return "melee"; + } + + if (name.includes("_ranged")) { + return "ranged"; + } + } + + if (name.includes("_siege")) { + return "siege"; + } + + for (const summonNameCheck of summonEnemyNameChecks) { + if (name.includes(summonNameCheck)) { + return "summon"; + } + } + + return "other"; +}; + +type Channelling = { + lastTimestamp: number; + lastRealTimeTimestamp: string; + isCurrentlyChannelling: boolean; + channellingTimes: number; +}; + +type EnemyTypeInProgress = { + killedEnemy?: string; + killedAt?: number; +}; + +type AbilityStats = { + [skillName: string]: { + hitCount: number; + useCount: number; + }; +}; + +export type PlayerSummary = { + goldReasonSummary: GoldReasonSummary; + goldEnemyTypeSummary: GoldEnemyTypeSummary; + activeRunes: RuneType[]; + bountyRuneTimeline: BountyRuneData[]; + + enemyTypeInProgress: EnemyTypeInProgress; + chanellings: Channelling; + abilityStats: AbilityStats; + towerDamage: number; +}; + +export type PlayerSummaries = { [hero: string]: PlayerSummary }; + +export type CreepKill = { + playerName: string; + creepName: string; +}; + +export type AbilityUse = { + playerName: string; + abilityName: string; + abilityLevel: number; +}; + +export type AbilityHit = { + playerName: string; + abilityName: string; + targetName: string; +}; + +export type TickSummary = { + creepsKilled: CreepKill[]; + abilitiesUsed: AbilityUse[]; + abilitiesHit: AbilityHit[]; +}; + +export type BountyRuneData = { + time: number; + gold: number; +}; + +export type GameSummary = { + players: PlayerSummaries; + tickSummary: TickSummary; +}; + + +export const findPlayerSummary = ( + summary: GameSummary | null | undefined, + player: Player +) => { + return summary && + summary.players && + player.hero && + player.hero.name && + player.hero.name in summary.players + ? summary.players[player.hero.name] + : null; +}; + +export type EnrichedPlayerData = Player & { summary: PlayerSummary | null }; + +export const enrichPossiblePlayerData = ( + summary: GameSummary | null | undefined, + player: Player | null | undefined +) => { + return player + ? { ...player, summary: findPlayerSummary(summary, player) } + : null; +}; + +export const enrichPlayerData = ( + summary: GameSummary | null | undefined, + player: Player +) => { + return { ...player, summary: findPlayerSummary(summary, player) }; +};