UNPKG

dotaconstants

Version:

Constant data for Dota 2 applications

1,403 lines (1,331 loc) 68.9 kB
import fs from "node:fs"; import vdfparser from "vdf-parser"; import { cleanupArray } from "./util.ts"; const extraStrings = { DOTA_ABILITY_BEHAVIOR_NONE: "None", DOTA_ABILITY_BEHAVIOR_PASSIVE: "Passive", DOTA_ABILITY_BEHAVIOR_UNIT_TARGET: "Unit Target", DOTA_ABILITY_BEHAVIOR_CHANNELLED: "Channeled", DOTA_ABILITY_BEHAVIOR_POINT: "Point Target", DOTA_ABILITY_BEHAVIOR_ROOT_DISABLES: "Root", DOTA_ABILITY_BEHAVIOR_AOE: "AOE", DOTA_ABILITY_BEHAVIOR_NO_TARGET: "No Target", DOTA_ABILITY_BEHAVIOR_AUTOCAST: "Autocast", DOTA_ABILITY_BEHAVIOR_ATTACK: "Attack Modifier", DOTA_ABILITY_BEHAVIOR_IMMEDIATE: "Instant Cast", DOTA_ABILITY_BEHAVIOR_HIDDEN: "Hidden", DAMAGE_TYPE_PHYSICAL: "Physical", DAMAGE_TYPE_MAGICAL: "Magical", DAMAGE_TYPE_PURE: "Pure", SPELL_IMMUNITY_ENEMIES_YES: "Yes", SPELL_IMMUNITY_ENEMIES_NO: "No", SPELL_IMMUNITY_ALLIES_YES: "Yes", SPELL_IMMUNITY_ALLIES_NO: "No", SPELL_DISPELLABLE_YES: "Yes", SPELL_DISPELLABLE_YES_STRONG: "Strong Dispels Only", SPELL_DISPELLABLE_NO: "No", DOTA_UNIT_TARGET_TEAM_BOTH: "Both", DOTA_UNIT_TARGET_TEAM_ENEMY: "Enemy", DOTA_UNIT_TARGET_TEAM_FRIENDLY: "Friendly", DOTA_UNIT_TARGET_HERO: "Hero", DOTA_UNIT_TARGET_BASIC: "Basic", DOTA_UNIT_TARGET_BUILDING: "Building", DOTA_UNIT_TARGET_TREE: "Tree", }; const ignoreStrings = new Set([ "DOTA_ABILITY_BEHAVIOR_ROOT_DISABLES", "DOTA_ABILITY_BEHAVIOR_DONT_RESUME_ATTACK", "DOTA_ABILITY_BEHAVIOR_DONT_RESUME_MOVEMENT", "DOTA_ABILITY_BEHAVIOR_IGNORE_BACKSWING", "DOTA_ABILITY_BEHAVIOR_TOGGLE", "DOTA_ABILITY_BEHAVIOR_IGNORE_PSEUDO_QUEUE", "DOTA_ABILITY_BEHAVIOR_SHOW_IN_GUIDES", ]); const badNames = new Set([ "Version", "npc_dota_hero_base", "npc_dota_hero_target_dummy", "npc_dota_units_base", "npc_dota_thinker", "npc_dota_companion", "npc_dota_loadout_generic", "npc_dota_techies_remote_mine", "npc_dota_treant_life_bomb", "npc_dota_lich_ice_spire", "npc_dota_mutation_pocket_roshan", "npc_dota_scout_hawk", "npc_dota_greater_hawk", ]); const extraAttribKeys = [ "AbilityCastRange", "AbilityChargeRestoreTime", "AbilityDuration", "AbilityChannelTime", "AbilityCastPoint", "AbilityCharges", "AbilityManaCost", "AbilityCooldown", ]; // Use standardized names for base attributes const generatedHeaders = { abilitycastrange: "CAST RANGE", abilitycastpoint: "CAST TIME", abilitycharges: "MAX CHARGES", max_charges: "MAX CHARGES", abilitychargerestoretime: "CHARGE RESTORE TIME", charge_restore_time: "CHARGE RESTORE TIME", abilityduration: "DURATION", abilitychanneltime: "CHANNEL TIME", }; // Already formatted for mc and cd const excludeAttributes = new Set(["abilitymanacost", "abilitycooldown"]); // Some attributes we remap, so keep track of them if there's dupes const remapAttributes = { abilitychargerestoretime: "charge_restore_time", abilitycharges: "max_charges", }; const notAbilities = new Set([ "Version", "version", "ability_base", "default_attack", "attribute_bonus", "ability_deward", ]); const itemQualOverrides = { fluffy_hat: "component", overwhelming_blink: "artifact", swift_blink: "artifact", arcane_blink: "artifact", moon_shard: "common", aghanims_shard: "consumable", kaya: "artifact", helm_of_the_dominator: "common", helm_of_the_overlord: "common", desolator: "epic", mask_of_madness: "common", orb_of_corrosion: "common", falcon_blade: "common", mage_slayer: "artifact", revenants_brooch: "epic", }; const idsUrl = "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_ability_ids.txt"; const heroesUrl = "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_heroes.txt"; const abilitiesLoc = "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/resource/localization/abilities_english.txt"; const npcAbilitiesUrl = "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_abilities.txt"; const npcUnitsUrl = "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/npc_units.txt"; const localizationUrl = "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/resource/localization/dota_english.txt"; let aghsAbilityValues = {}; const heroDataUrls: string[] = []; const heroDataIndex: string[] = []; const abilitiesUrls = [abilitiesLoc, npcAbilitiesUrl]; start(); async function start() { const resp = await fetch(heroesUrl); const heroesVdf = parseJsonOrVdf(await resp.text(), heroesUrl); let ids = Object.keys(heroesVdf.DOTAHeroes) .filter((name) => !badNames.has(name)) .map((key) => heroesVdf.DOTAHeroes[key].HeroID) .sort((a, b) => Number(a) - Number(b)); ids.forEach((key) => { heroDataUrls.push( "http://www.dota2.com/datafeed/herodata?language=english&hero_id=" + key, ); }); let names = Object.keys(heroesVdf.DOTAHeroes).filter( (name) => !badNames.has(name), ); names.forEach((name) => { // The hero abilities were moved to individual hero files, e.g. https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/heroes/npc_dota_hero_abaddon.txt abilitiesUrls.push( "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/heroes/" + name + ".txt", ); heroDataIndex.push(name); }); const sources = [ { key: "items", url: [ abilitiesLoc, "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/items.txt", "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/npc/neutral_items.txt", idsUrl, ], transform: (respObj: any) => { const strings = respObj[0].lang.Tokens; const scripts = respObj[1].DOTAAbilities; const neutrals = respObj[2].neutral_items; const idLookup = respObj[3].DOTAAbilityIDs.ItemAbilities.Locked; // parse neutral items into name => tier map const neutralItemNameTierMap = getNeutralItemNameTierMap(neutrals); // Fix places where valve doesnt care about correct case Object.keys(strings).forEach((key) => { if (key.includes("DOTA_Tooltip_Ability_")) { strings[ key.replace("DOTA_Tooltip_Ability_", "DOTA_Tooltip_ability_") ] = strings[key]; } }); let items = {}; Object.keys(scripts) .filter((key) => { return ( !(key.includes("item_recipe") && scripts[key].ItemCost === "0") && key !== "Version" ); }) .forEach((key) => { const specialAttrs = getSpecialAttrs(scripts[key]); let item = { ...replaceSpecialAttribs( strings[`DOTA_Tooltip_ability_${key}_Description`], specialAttrs, true, scripts[key], key, ), }; item.id = Number(idLookup[key]); item.img = `/apps/dota2/images/dota_react/items/${key.replace( /^item_/, "", )}.png?t=${1593393829403}`; if (key.includes("item_recipe")) { item.img = `/apps/dota2/images/dota_react/items/recipe.png?t=${1593393829403}`; } item.dname = strings[`DOTA_Tooltip_ability_${key}`] || strings[`DOTA_Tooltip_ability_${key}:n`]; item.qual = itemQualOverrides[key] ?? scripts[key].ItemQuality; item.cost = parseInt(scripts[key].ItemCost); const behavior = formatBehavior(scripts[key].AbilityBehavior); item.behavior = behavior !== "Passive" ? behavior : undefined; item.dmg_type = formatBehavior(scripts[key].AbilityUnitDamageType) || undefined; item.bkbpierce = formatBehavior(scripts[key].SpellImmunityType) || undefined; item.dispellable = formatBehavior(scripts[key].SpellDispellableType) || undefined; item.target_team = formatBehavior(scripts[key].AbilityUnitTargetTeam) || undefined; item.target_type = formatBehavior(scripts[key].AbilityUnitTargetType) || undefined; let notes: any[] = []; for ( let i = 0; strings[`DOTA_Tooltip_ability_${key}_Note${i}`]; i++ ) { notes.push( replaceSpecialAttribs( strings[`DOTA_Tooltip_ability_${key}_Note${i}`], specialAttrs, false, scripts[key], key, ), ); } item.notes = notes.join("\n"); item.attrib = scripts[key].AbilityValues ? Object.entries(scripts[key].AbilityValues).map( ([abilityKey, val]: [string, any]) => { let display; const tooltipKey = `DOTA_Tooltip_ability_${key}_${abilityKey}`; const string = strings[tooltipKey]; if (tooltipKey in strings) { if (string.includes("$") || /[a-z]/.test(string)) { // Normal attributes, e.g. + 25 Movement Speed display = string.replace( /(%)?([+-])?(\$\w+)?/, (str, pct = "", pm = "", variable) => `${pm} {value}${pct} ` + (strings[ `dota_ability_variable_${variable?.replace( "$", "", )}` ] || ""), ); } if (!/[a-z]/.test(string)) { // Upper case stats, where the number is displayed in the end display = string.replace( /^(%)?(.+)/, (str, pct = "", rest) => { return `${rest} {value}${pct}`; }, ); } } return { key: abilityKey, display: display?.replace(/<[^>]*>/g, ""), value: (val.value ?? val).split(" ").join(" / "), }; }, ) : []; item.mc = parseInt(scripts[key].AbilityManaCost) || false; item.hc = parseInt(scripts[key].AbilityHealthCost) || false; item.cd = parseInt(scripts[key].AbilityCooldown) || false; item.lore = ( strings[`DOTA_Tooltip_ability_${key}_Lore`] || "" ).replace(/\\n/g, "\r\n"); item.components = null; item.created = false; item.charges = parseInt(scripts[key].ItemInitialCharges) || false; if (neutralItemNameTierMap[key]) { item.tier = neutralItemNameTierMap[key]; } items[key.replace(/^item_/, "")] = item; }); // Load recipes Object.keys(scripts) .filter( (key) => scripts[key].ItemRequirements && scripts[key].ItemResult, ) .forEach((key) => { const result_key = scripts[key].ItemResult.replace(/^item_/, ""); items[result_key].components = scripts[key].ItemRequirements["01"] ?.split(";") .map((item) => item.replace(/^item_/, "").replace("*", "")); items[result_key].created = true; }); //Manually Adding DiffBlade2 for match data prior to 7.07 items["diffusal_blade_2"] = { id: 196, img: "/apps/dota2/images/dota_react/items/diffusal_blade_2.png?3", dname: "Diffusal Blade", qual: "artifact", cost: 3850, desc: "Active: Purge Targets an enemy, removing buffs from the target and slowing it for 4 seconds.Range: 600\nPassive: ManabreakEach attack burns 50 mana from the target, and deals 0.8 physical damage per burned mana. Burns 16 mana per attack from melee illusions and 8 mana per attack from ranged illusions. Dispel Type: Basic Dispel", notes: "Does not stack with other manabreak abilities.", attrib: [ { key: "bonus_agility", display: "+ {value} Agility", value: "25 / 35", }, { key: "bonus_intellect", display: "+ {value} Intelligence", value: "10 / 15", }, { key: "initial_charges", value: "8", }, { key: "feedback_mana_burn", value: "50", }, { key: "feedback_mana_burn_illusion_melee", value: "16", }, { key: "feedback_mana_burn_illusion_ranged", value: "8", }, { key: "purge_summoned_damage", value: "99999", }, { key: "purge_rate", value: "5", }, { key: "purge_root_duration", value: "3", }, { key: "purge_slow_duration", value: "4", }, { key: "damage_per_burn", value: "0.8", }, { key: "cast_range_tooltip", value: "600", }, ], mc: false, cd: 4, lore: "An enchanted blade that allows the user to cut straight into the enemy's soul.", components: ["diffusal_blade", "recipe_diffusal_blade"], created: true, }; //Manually added for match data prior to 7.07 items["recipe_iron_talon"] = { id: 238, img: "/apps/dota2/images/dota_react/items/recipe.png?3", dname: "Iron Talon Recipe", cost: 125, desc: "", notes: "", attrib: [], mc: false, cd: false, lore: "", components: null, created: false, }; return items; }, }, { key: "item_ids", url: idsUrl, transform: (respObj: any) => { const data = respObj.DOTAAbilityIDs.ItemAbilities.Locked; // Flip the keys and values const itemIds: any = {}; Object.entries(data).forEach(([key, val]: [string, any]) => { // Remove item_ prefix itemIds[val] = key.replace("item_", ""); }); //manually adding DiffBlade2 itemIds[196] = "diffusal_blade_2"; return itemIds; }, }, { key: "abilities", url: abilitiesUrls, transform: (respObj: any) => { const strings = respObj[0].lang.Tokens; let scripts = respObj[1].DOTAAbilities; // Merge into scripts all the hero abilities (the rest of the array) for (let i = 2; i < respObj.length; i++) { // console.log(respObj[i]); const heroAbs = respObj[i].DOTAAbilities; scripts = { ...scripts, ...heroAbs }; } let abilities = {}; Object.keys(scripts) .filter((key) => !notAbilities.has(key)) .forEach((key) => { let ability: any = {}; let specialAttr = getSpecialAttrs(scripts[key]); ability.dname = replaceSValues( strings[`DOTA_Tooltip_ability_${key}`] ?? strings[`DOTA_Tooltip_Ability_${key}`], specialAttr, key, ); // Check for unreplaced `s:bonus_<talent>` if ( scripts[key].ad_linked_abilities && scripts[scripts[key].ad_linked_abilities] ) { ability.dname = replaceBonusSValues( key, ability.dname, scripts[scripts[key].ad_linked_abilities].AbilityValues, ); } if (scripts[key].Innate === "1") { ability.is_innate = true; } ability.behavior = formatBehavior(scripts[key].AbilityBehavior) || undefined; ability.dmg_type = formatBehavior(scripts[key].AbilityUnitDamageType) || undefined; ability.bkbpierce = formatBehavior(scripts[key].SpellImmunityType) || undefined; ability.dispellable = formatBehavior(scripts[key].SpellDispellableType) || undefined; ability.target_team = formatBehavior(scripts[key].AbilityUnitTargetTeam) || undefined; ability.target_type = formatBehavior(scripts[key].AbilityUnitTargetType) || undefined; ability.desc = replaceSpecialAttribs( strings[`DOTA_Tooltip_ability_${key}_Description`], specialAttr, false, scripts[key], key, ); ability.dmg = scripts[key].AbilityDamage && formatValues(scripts[key].AbilityDamage); // Clean up duplicate remapped values (we needed dupes for the tooltip) if (specialAttr) { Object.entries(remapAttributes).forEach(([oldAttr, newAttr]) => { const oldAttrIdx = specialAttr.findIndex( (attr) => Object.keys(attr)[0] === oldAttr, ); const newAttrIdx = specialAttr.findIndex( (attr) => Object.keys(attr)[0] === newAttr, ); if (oldAttrIdx !== -1 && newAttrIdx !== -1) { specialAttr.splice(oldAttrIdx, 1); } }); } ability.attrib = formatAttrib( specialAttr, strings, `DOTA_Tooltip_ability_${key}_`, ); ability.lore = strings[`DOTA_Tooltip_ability_${key}_Lore`]; const ManaCostKey = scripts[key].AbilityManaCost ?? scripts[key].AbilityValues?.AbilityManaCost; const CooldownKey = scripts[key].AbilityCooldown ?? scripts[key].AbilityValues?.AbilityCooldown; if (ManaCostKey) { const manaCost = isObj(ManaCostKey) ? ManaCostKey["value"] : ManaCostKey; ability.mc = formatValues(manaCost, false, "/"); } if (CooldownKey) { const cooldown = isObj(CooldownKey) ? CooldownKey["value"] : CooldownKey; ability.cd = formatValues(cooldown, false, "/"); } ability.img = `/apps/dota2/images/dota_react/abilities/${key}.png`; if (key.indexOf("special_bonus") === 0) { ability = { dname: ability.dname }; } abilities[key] = ability; if (specialAttr) { let aghsObj = {}; if ( scripts[key].IsGrantedByScepter || scripts[key].IsGrantedByShard ) { // simple straight copy to lookup for (const attrib of specialAttr) { for (const key of Object.keys(attrib)) { const val = attrib[key]; if (isObj(val)) { aghsObj[key] = val["value"]; } else { aghsObj[key] = val; } } } } else { for (const attrib of specialAttr) { for (const key of Object.keys(attrib)) { const val = attrib[key]; if (val === null) { continue; } // handle bonus objects if (isObj(val)) { // first case: standard attribute with aghs bonus for (const bonus of Object.keys(val)) { if ( bonus.indexOf("scepter") !== -1 || bonus.indexOf("shard") !== -1 ) { const rawBonus = val[bonus] .replace("+", "") .replace("-", "") .replace("x", "") .replace("%", ""); // bonus_bonus doesn't exist, it's shard_bonus or scepter_bonus at that point const aghsPrefix = bonus.indexOf("scepter") !== -1 ? "scepter" : "shard"; const bonusKey = key.startsWith("bonus_") ? `${aghsPrefix}_${key}` : `bonus_${key}`; aghsObj[bonusKey] = rawBonus; aghsObj[`${key}`] = calculateValueFromBonus( val["value"], val[bonus], ); } } // second case: aghs bonus attribute if ( key.indexOf("scepter") !== -1 || key.indexOf("shard") !== -1 ) { const bonus = Object.keys(val) .filter((k) => k !== key) .find( (k) => k.indexOf("scepter") !== -1 || k.indexOf("shard") !== -1, ); if (bonus) { aghsObj[key] = calculateValueFromBonus( val["value"] ?? val[key], val[bonus], ); } else { aghsObj[key] = val["value"] ?? val[key]; } } // third case: value requires aghs if (Object.keys(val).length == 2) { // value and requires attr if ( (val["value"] && val["RequiresScepter"]) || val["RequiresShard"] ) { aghsObj[key] = val["value"]; } } } else { // simple key to value aghsObj[key] = val; } } } } aghsAbilityValues[key] = aghsObj; } }); // Some unique talents don't show up in each hero data file // e.g. special_bonus_unique_axe_8 // Do a pass through the strings and if any are missing, add them with just basic description Object.keys(strings).forEach((str) => { if ( str.startsWith("DOTA_Tooltip_ability_special_bonus_unique") && !str.endsWith("Description") ) { const abName = str.slice("DOTA_Tooltip_ability_".length); if (!abilities[abName]) { abilities[abName] = { dname: strings[str], }; } } }); return abilities; }, }, { key: "ability_ids", url: idsUrl, transform: (respObj: any) => { const data = respObj.DOTAAbilityIDs.UnitAbilities.Locked; // Flip the keys and values const abilityIds = {}; Object.entries(data).forEach(([key, val]: [string, any]) => { abilityIds[val] = key; }); return abilityIds; }, }, { key: "neutral_abilities", url: npcUnitsUrl, transform: (respObj: any) => { const abilitySlots = [ "Ability1", "Ability2", "Ability3", "Ability4", "Ability5", "Ability6", "Ability7", "Ability8", ]; // filter out placeholder abilities const badNeutralAbilities = new Set([ "creep_piercing", "creep_irresolute", "flagbearer_creep_aura_effect", "creep_siege", "backdoor_protection", "backdoor_protection_in_base", "filler_ability", "neutral_upgrade", ]); // filter out attachable units, couriers, buildings and siege creeps const badUnitRelationships = new Set([ "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_ATTACHED", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_COURIER", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_BUILDING", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_BARRACKS", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_SIEGE", ]); const units = respObj.DOTAUnits; const baseUnit = units["npc_dota_units_base"]; function getUnitProp(unit, prop, name = "") { if (unit[prop] !== undefined) { return unit[prop]; } // include from other unit if (unit.include_keys_from) { return getUnitProp(units[unit.include_keys_from], prop); } // check if BaseClass is defined non-natively, if so, read from that // also make sure we aren't reading from itself if ( unit.BaseClass && unit.BaseClass !== name && units[unit.BaseClass] ) { return getUnitProp(units[unit.BaseClass], prop, unit.BaseClass); } // Fallback to the base unit return baseUnit[prop]; } const keys = Object.keys(units).filter((name) => { if (badNames.has(name)) { return false; } const unit = units[name]; // only special units have a minimap icon if (unit.MinimapIcon) { return false; } if (getUnitProp(unit, "BountyXP") === "0") { return false; } // if HasInventory=0 explicitly (derived from hero), then we can filter it out // if it has an inventory, it's not an neutral if ( unit.HasInventory === "0" || getUnitProp(unit, "HasInventory") === "1" ) { return false; } if ( badUnitRelationships.has(getUnitProp(unit, "UnitRelationshipClass")) ) { return false; } let hasAbility = false; for (const slot of abilitySlots) { const ability = getUnitProp(unit, slot); if (ability && !badNeutralAbilities.has(ability)) { hasAbility = true; break; } } return hasAbility; }); const neutralAbilities = {}; keys.forEach((key) => { const unit = units[key]; for (const slot of abilitySlots) { const ability = getUnitProp(unit, slot); if ( ability && !badNeutralAbilities.has(ability) && !neutralAbilities[ability] ) { neutralAbilities[ability] = { img: `/assets/images/dota2/neutral_abilities/${ability}.png`, }; } } }); return neutralAbilities; }, }, { key: "ancients", url: npcUnitsUrl, transform: (respObj: any) => { // filter out attachable units, couriers, buildings and siege creeps const badUnitRelationships = new Set([ "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_ATTACHED", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_COURIER", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_BUILDING", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_BARRACKS", "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_SIEGE", ]); const units = respObj.DOTAUnits; const baseUnit = units["npc_dota_units_base"]; function getUnitProp(unit, prop, name = "") { if (unit[prop] !== undefined) { return unit[prop]; } // include from other unit if (unit.include_keys_from) { return getUnitProp(units[unit.include_keys_from], prop); } // check if BaseClass is defined non-natively, if so, read from that // also make sure we aren't reading from itself if ( unit.BaseClass && unit.BaseClass !== name && units[unit.BaseClass] ) { return getUnitProp(units[unit.BaseClass], prop, unit.BaseClass); } // Fallback to the base unit return baseUnit[prop]; } const keys = Object.keys(units).filter((name) => { if (badNames.has(name)) { return false; } const unit = units[name]; // only special units have a minimap icon if (unit.MinimapIcon) { return false; } if (getUnitProp(unit, "BountyXP") === "0") { return false; } // if HasInventory=0 explicitly (derived from hero), then we can filter it out // if it has an inventory, it's not an neutral if ( unit.HasInventory === "0" || getUnitProp(unit, "HasInventory") === "1" ) { return false; } if ( getUnitProp(unit, "UnitRelationshipClass") !== "DOTA_NPC_UNIT_RELATIONSHIP_TYPE_DEFAULT" ) { return false; } const level = getUnitProp(unit, "Level"); if (level === "0" || level === "1") { return false; } if (getUnitProp(unit, "TeamName") !== "DOTA_TEAM_NEUTRALS") { return false; } if (getUnitProp(unit, "IsNeutralUnitType") === "0") { return false; } if (getUnitProp(unit, "IsRoshan") === "1") { return false; } return getUnitProp(unit, "IsAncient") === "1"; }); const ancients = {}; keys.forEach((key) => { ancients[key] = 1; }); return ancients; }, }, { key: "heroes", url: [localizationUrl, heroesUrl], transform: (respObj: any) => { let heroes: any = []; let keys = Object.keys(respObj[1].DOTAHeroes).filter( (name) => !badNames.has(name), ); keys.forEach((name) => { let h: any = formatVpkHero(name, respObj[1]); h.localized_name = respObj[1].DOTAHeroes[name].workshop_guide_name ?? respObj[0].lang.Tokens[name + ":n"]; heroes.push(h); }); heroes = heroes.sort((a, b) => a.id - b.id); let heroesObj = {}; for (let hero of heroes) { hero.id = Number(hero.id); heroesObj[hero.id] = hero; } return heroesObj; }, }, { key: "hero_lore", url: [ "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/resource/localization/hero_lore_english.txt", heroesUrl, ], transform: (respObj: any) => { let keys = Object.keys(respObj[1].DOTAHeroes).filter( (name) => !badNames.has(name), ); let sortedHeroes: { name: string; id: number }[] = []; keys.forEach((name) => { const hero = respObj[1].DOTAHeroes[name]; sortedHeroes.push({ name, id: hero.HeroID }); }); sortedHeroes = sortedHeroes.sort((a, b) => a.id - b.id); const lore = respObj[0].lang.Tokens; const heroLore = {}; sortedHeroes.forEach((hero) => { const heroKey = hero.name.replace("npc_dota_hero_", ""); heroLore[heroKey] = lore[`${hero.name}_bio`] .replace(/\t+/g, " ") .replace(/\n+/g, " ") .replace(/<br>+/g, " ") .replace(/\s+/g, " ") .replace(/\\/g, "") .replace(/"/g, "'") .trim(); }); return heroLore; }, }, { key: "hero_abilities", url: [heroesUrl, ...abilitiesUrls], transform: (respObj: any) => { const [heroObj, abilityLoc, _, ...heroAbils] = respObj; const strings = abilityLoc.lang.Tokens; // Fix places where valve doesn't care about correct case Object.keys(strings).forEach((key) => { strings[key.toLowerCase()] = strings[key]; }); let scripts = {}; for (let i = 0; i < heroAbils.length; i++) { scripts[heroDataIndex[i]] = heroAbils[i].DOTAAbilities; } let heroes = heroObj.DOTAHeroes; const heroAbilities = {}; Object.keys(heroes).forEach(function (heroKey) { if ( heroKey != "Version" && heroKey != "npc_dota_hero_base" && heroKey != "npc_dota_hero_target_dummy" ) { const newHero = { abilities: [] as any[], talents: [] as any[], facets: [], }; let talentCounter = 2; Object.keys(heroes[heroKey]).forEach(function (key) { let talentIndexStart = heroes[heroKey]["AbilityTalentStart"] != undefined ? heroes[heroKey]["AbilityTalentStart"] : 10; let abilityRegexMatch = key.match(/Ability([0-9]+)/); if (abilityRegexMatch && heroes[heroKey][key] != "") { let abilityNum = parseInt(abilityRegexMatch[1]); if (abilityNum < talentIndexStart) { newHero["abilities"].push(heroes[heroKey][key]); } else { // -8 not -10 because going from 0-based index -> 1 and flooring divison result newHero["talents"].push({ name: heroes[heroKey][key], level: Math.floor(talentCounter / 2), }); talentCounter++; } } }); heroAbilities[heroKey] = newHero; } Object.entries(heroes[heroKey].Facets ?? []).forEach( ([key, value]: [string, any], i) => { const abilities = Object.values(value.Abilities || {}).map( (a: any) => a.AbilityName, ); const [ability] = abilities; const title = strings[`dota_tooltip_facet_${key}`] || strings[`dota_tooltip_ability_${ability}`] || ""; let description = strings[`dota_tooltip_facet_${key}_description`] || strings[`dota_tooltip_ability_${ability}_description`] || ""; if (description.includes("{s:facet_ability_name}")) { description = description.replace( "{s:facet_ability_name}", title, ); } const allAttribs: any[] = []; Object.values(scripts[heroKey]).forEach((ability) => { const specialAttribs = getSpecialAttrs(ability) || []; allAttribs.push(...specialAttribs); }); if (abilities.length > 0) { description = replaceSpecialAttribs( description, getSpecialAttrs(scripts[heroKey][ability]) || [], false, scripts[heroKey][ability], ability, ); } const matches = description.matchAll(/{s:bonus_(\w+)}/g); for (let [, bonusKey] of matches) { const obj = allAttribs.find((obj) => bonusKey in obj)?.[bonusKey] ?? {}; const facetKey = Object.keys(obj).find( (k) => k.startsWith("special_bonus_facet") && k.includes(key), ); if (facetKey) { description = description.replace( `{s:bonus_${bonusKey}}`, removeSigns(obj[facetKey]), ); } } heroAbilities[heroKey].facets.push({ id: i, name: key, deprecated: value.Deprecated, icon: value.Icon?.toLowerCase(), color: value.Color, gradient_id: Number(value.GradientID), title, description: removeHTML(description), abilities: abilities.length > 0 ? abilities : undefined, }); }, ); }); return heroAbilities; }, }, // { // key: "region", // url: "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/regions.txt", // transform: (respObj: any) => { // const region = {}; // const regions = respObj.regions; // for (const key in regions) { // if (Number(regions[key].region) > 0) { // region[regions[key].region] = regions[key].display_name // .slice("#dota_region_".length) // .split("_") // .map((s) => s.toUpperCase()) // .join(" "); // } // } // return region; // }, // }, // { // key: "cluster", // url: "https://api.stratz.com/api/v1/Cluster", // transform: (respObj: any) => { // const cluster = {}; // respObj.forEach(({ id, regionId }) => { // cluster[id] = regionId; // }); // return cluster; // }, // }, { key: "countries", url: "https://raw.githubusercontent.com/mledoze/countries/master/countries.json", transform: (respObj: any) => { const countries = {}; respObj .map((c: any) => ({ name: { common: c.name.common, }, cca2: c.cca2, })) .forEach((c) => { countries[c.cca2] = c; }); return countries; }, }, { key: "chat_wheel", url: [ "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/scripts/chat_wheel.txt", localizationUrl, "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/resource/localization/hero_chat_wheel_english.txt", ], transform: (respObj: any) => { const chat_wheel = respObj[0].chat_wheel; const lang = respObj[1].lang.Tokens; const chat_wheel_lang = respObj[2].hero_chat_wheel; const result = {}; function localize(input) { if (!/^#/.test(input)) { return input; } let key = input.replace(/^#/, ""); return lang[key] || chat_wheel_lang[key] || key; } function addMessage(key, message) { let data = { id: parseInt(message.message_id), name: key, all_chat: message.all_chat == "1" ? true : undefined, label: localize(message.label), message: localize(message.message), image: message.image, badge_tier: message.unlock_hero_badge_tier, sound_ext: undefined as string | undefined, }; if (message.sound) { if (/^soundboard\./.test(message.sound) || /^wisp_/.test(key)) { // All of the soundboard clips and the IO responses are wav files data.sound_ext = "wav"; } else if (message.message_id / 1000 >= 121) { // Gets the hero id from the message id // If the hero is grimstroke or newer, the files are aac data.sound_ext = "aac"; } else { // All other response clips used are mp3s data.sound_ext = "mp3"; } } result[data.id] = data; } for (let key in chat_wheel.messages) { addMessage(key, chat_wheel.messages[key]); } for (let hero_id in chat_wheel.hero_messages) { for (let key in chat_wheel.hero_messages[hero_id]) { addMessage(key, chat_wheel.hero_messages[hero_id][key]); } } return result; }, }, // Requires items and hero names so needs to run after { key: "patchnotes", url: "https://raw.githubusercontent.com/dotabuff/d2vpkr/master/dota/resource/localization/patchnotes/patchnotes_english.txt", transform: (respObj: any) => { let items = Object.keys( JSON.parse(fs.readFileSync("./build/items.json").toString()), ); let heroes = Object.keys( JSON.parse(fs.readFileSync("./build/hero_lore.json").toString()), ); const data = respObj.patch; let result: any = {}; let keys = Object.keys(data); for (let key of keys) { let keyArr = key.replace("DOTA_Patch_", "").split("_"); let patch = keyArr.splice(0, 2).join("_"); if (!result[patch]) result[patch] = { general: [], items: {}, heroes: {}, }; if (keyArr[0].toLowerCase() == "general") { result[patch].general.push(data[key]); } else if (keyArr[0] == "item") { let searchName = keyArr.slice(1); let itemName = parseNameFromArray(searchName, items); if (itemName) { if (!result[patch].items[itemName]) result[patch].items[itemName] = []; const cleanEntry = data[key].replace(/<[^>]*>/g, ""); if (cleanEntry !== "") { result[patch].items[itemName].push(cleanEntry); } } else { if (!result[patch].items.misc) result[patch].items.misc = []; result[patch].items.misc.push(data[key]); } } else { let heroName = parseNameFromArray(keyArr, heroes); // Extracting ability name let abilityName = keyArr.length > 1 ? keyArr.join("_").replace(`${heroName}_`, "") : "general"; abilityName = !abilityName || abilityName === heroName ? "general" : abilityName; if (heroName) { if (!result[patch].heroes[heroName]) result[patch].heroes[heroName] = {}; let isTalent = data[key].startsWith("Talent:"); isTalent = isTalent || abilityName.startsWith("talent"); if (isTalent) { if (!result[patch].heroes[heroName].talents) result[patch].heroes[heroName].talents = []; result[patch].heroes[heroName].talents.push( data[key].replace("Talent:", "").trim(), ); } else { // DOTA_Patch_7_32_shredder_shredder_chakram_2_2 // DOTA_Patch_7_32_tinker_tinker_rearm_3_info // remove everything to the right of the first _n that is found, where n is a number abilityName = abilityName.replace(/_[0-9]+.*/, ""); // if abilityName is just an underscore and number like "_n" then set it to general abilityName = parseInt(abilityName.replace(/_/, "")) ? "general" : abilityName; if (!result[patch].heroes[heroName][abilityName]) result[patch].heroes[heroName][abilityName] = []; result[patch].heroes[heroName][abilityName].push( data[key].trim(), ); } } else { if (!result[patch].heroes.misc) result[patch].heroes.misc = []; result[patch].heroes.misc.push(data[key].trim()); } } } return result; }, }, // NOTE: This needs to run after abilities since it depends on aghsAbilityValues { key: "aghs_desc", url: heroDataUrls, transform: (respObj: any) => { const herodata = respObj; const aghs_desc_arr: any[] = []; // for every hero herodata.forEach((hd_hero) => { if (!hd_hero) { return; } hd_hero = hd_hero.result.data.heroes[0]; // object to store data about aghs scepter/shard for a hero let aghs_element = { hero_name: hd_hero.name, hero_id: hd_hero.id, has_scepter: false, scepter_desc: "", scepter_skill_name: "", scepter_new_skill: false, has_shard: false, shard_desc: "", shard_skill_name: "", shard_new_skill: false, }; hd_hero.abilities.forEach((ability) => { // skip unused skills if (ability.name_loc == "" || ability.desc_loc == "") { return; // i guess this is continue in JS :| } let scepterName = null; let shardName = null; // ------------- Scepter ------------- if (ability.ability_is_granted_by_scepter) { // scepter grants new ability aghs_element.scepter_desc = ability.desc_loc; aghs_element.scepter_skill_name = ability.name_loc; scepterName = ability.name; aghs_element.scepter_new_skill = true; aghs_element.has_scepter = true; } else if ( ability.ability_has_scepter && !(ability.scepter_loc == "") ) { // scepter ugprades an ability aghs_element.scepter_desc = ability.scepter_loc; aghs_element.scepter_skill_name = ability.name_loc; scepterName = ability.name; aghs_element.scepter_new_skill = false; aghs_element.has_scepter = true; } // -------------- Shard -------------- if (ability.ability_is_granted_by_shard) { // scepter grants new ability aghs_element.shard_desc = ability.desc_loc; aghs_element.shard_skill_name = ability.name_loc; shardName = ability.name; aghs_element.shard_new_skill = true; aghs_element.has_shard = true; } else if ( ability.ability_has_shard && !(ability.shard_loc == "") ) { // scepter ugprades an ability aghs_element.shard_desc = ability.shard_loc; aghs_element.shard_skill_name = ability.name_loc; shardName = ability.name; aghs_element.shard_new_skill = false; aghs_element.has_shard = true; } if (scepterName) { const values = aghsAbilityValues[scepterName]; aghs_element.scepter_desc = aghs_element.scepter_desc.replace( /%([^% ]*)%/g, findAghsAbilityValue(values), ); } if (shardName) { const values = aghsAbilityValues[shardName]; aghs_element.shard_desc = aghs_element.shard_desc.replace( /%([^% ]*)%/g, findAghsAbilityValue(values), ); } // clean up <br> and double % signs aghs_element.scepter_desc = aghs_element.scepter_desc .replace(/<br>/gi, "\n") .replace("%%", "%"); aghs_element.shard_desc = aghs_element.shard_desc .replace(/<br>/gi, "\n") .replace("%%", "%"); }); // Error handling if (!aghs_element.has_shard) { console.log( aghs_element.hero_name + "[" + aghs_element.hero_id + "]" + ": Didn't find a scepter...", ); } if (!aghs_element.has_scepter) { console.log( aghs_element.hero_name + "[" + aghs_element.hero_id + "]" + ": Didn't find a shard...", ); } // push the current hero"s element into the array aghs_desc_arr.push(aghs_element); }); return aghs_desc_arr; }, }, ]; for (let i = 0; i < sources.length; i++) { const s = sources[i]; const url = s.url; // Make all urls into array const arr = Array.isArray(url) ? url : [url]; // console.log(arr); const resps = await Promise.all( arr.map(async (url) => { console.log(url); const resp = await fetch(url); return parseJsonOrVdf(await resp.text(), url); }), ); let final: any = resps; if (s.transform) { final = s.transform(resps.length === 1 ? resps[0] : resps); } fs.writeFileSync( "./build/" + s.key + ".json", JSON.stringify(final, null, 2), ); } // Copy manual json files to build const jsons = fs.readdirSync("./json"); jsons.forEach((filename) => { fs.writeFileSync( "./build/" + filename, fs.readFileSync("./json/" + filename, "utf-8"), ); }); // Reference built files i