├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── content ├── panorama │ └── scripts │ │ └── vector_target.js └── particles │ └── vector_target │ └── vector_target_range_finder_line.vpcf └── game └── scripts └── vscripts └── vector_target.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | 217 | ############# 218 | # DOTA 2 219 | ############# 220 | 221 | *.vpk 222 | *.vpcf_c 223 | *.vsnd_c 224 | *.vsndevts_c 225 | *.vmat_c 226 | *.vtex_c 227 | *.bin 228 | *.vxml_c 229 | *.vcss_c 230 | *.vjs_c -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #vector_target.lua 2 | ##0.2.3 3 | * fixed a bug causing the range finder particle timer to duplicate 4 | * fixed default range finder particle disappearing if initial/terminal control points were set to the same vector 5 | 6 | ##0.2.2 7 | * more fixes to order cancellation (client vs server timing fixes) 8 | 9 | ##0.2.1 10 | * fixed issues with canceling in-progress orders 11 | 12 | ##0.2 13 | * such changelog much wow 14 | * `kv` option changed to `kvList` and now accepts a list of filenames and/or Lua tables 15 | * fixed various issues with vector targetable items not working properly -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Adam Curtis ( https://github.com/kallisti-dev/vector_target ) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Adam Curtis nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A library for vector targeted abilities in Dota 2 custom games. 2 | 3 | ![](http://gfycat.com/DisgustingKindBronco) 4 | 5 | #Table of Contents 6 | * [Features](#features) 7 | * [Installation](#installation) 8 | * [Basic Setup Guide](#basic-setup-guide) 9 | * [KV Options](#kv-options) 10 | * [Custom Targeting Particles](#custom-targeting-particles) 11 | * ["Point of Cast"](#point-of-cast) 12 | * [Minimum and Maximum Distance](#minimum-and-maximum-distance) 13 | * [Writing Ability Code](#writing-ability-code) 14 | * [Ability Properties and Methods](#ability-properties-and-methods) 15 | * [Code Examples](#code-examples) 16 | * [Advanced Topics](#advanced-topics) 17 | * [KV Loading (from File or Table)](#kv-loading-from-file-or-table) 18 | * [ExecuteOrderFilter](#executeorderfilter) 19 | * [Adding Vector Targeting Behavior to Abilities Dynamically](#adding-vector-targeting-behavior-to-abilities-dynamically) 20 | * [Planned Improvements and to-do](#planned-improvements-and-to-do) 21 | * [Feedback, Suggestions, Contributions](#feedback-suggestions-contributions) 22 | 23 | #Features 24 | * Fullly configurable through KV 25 | * Support for custom client-side targeting particles (as well as a default particle that mimics the built-in range finder) 26 | * Handles both normal and quick cast settings. 27 | * Handles shift queue (Note: there's currently some issues when the dota order queue gets maxed out) 28 | 29 | #Installation 30 | * Copy files from `game/scripts/vscripts` somewhere into your `dota 2 beta/game/dota_addons/*/vscripts/scripts` folder. 31 | * Copy files from `content/` into your `dota 2 beta/content/dota_addons/*/` folder. 32 | * Attach a copy of the [LICENSE](https://github.com/kallisti-dev/vector_target/blob/master/LICENSE) to your source code. 33 | 34 | #Basic Setup Guide 35 | * Add a `require` to your addons_game_mode.lua; For example, if you copied vector_target.lua into `game/dota_addons/*/scripts/vscripts/libraries/` the require would look like this: 36 | 37 | ```lua 38 | require("libraries.vector_target") 39 | ``` 40 | 41 | * Call `VectorTarget:Init` somewhere in your initialization code. 42 | 43 | ```lua 44 | VectorTarget:Init() 45 | ``` 46 | 47 | * If you plan on using the default range finder particles, you need to call `VectorTarget:Precache` in your `Precache` function, like this: 48 | 49 | ```lua 50 | --addon_game_mode.lua 51 | function Precache( context ) 52 | VectorTarget:Precache( context ) 53 | end 54 | ``` 55 | 56 | * Finally, you need to include vector_target.js in the `` of one of your panorama layouts. 57 | The layout you choose to load the library in is mostly irrelevant, as long as you load it before abilities 58 | can be casted, and only load it once. 59 | 60 | ```xml 61 | 62 | 63 | 64 | ``` 65 | 66 | #KV Options 67 | For configuration of individual ability behavior, we use custom KV options. Just add these options directly to your existing ability KV, in either `npc_abilities_custom.txt` or `npc_items_custom.txt`. See the [advanced KV loading](#kv-loading-from-file-or-table) section if you want more control over how KV options are loaded. 68 | 69 | For default vector targeting behavior, all you need to do is add a non-zero `VectorTarget` key to the ability's definition block. 70 | 71 | ```javascript 72 | "my_ability" 73 | { 74 | "VectorTarget" "1" 75 | } 76 | ``` 77 | 78 | For fine-tuning of vector targeting options, you can pass a block with various option keys. 79 | 80 | ```javascript 81 | "my_ability" 82 | { 83 | "VectorTarget" 84 | { 85 | "ParticleName" "particles/my_custom_particle.vpcf" 86 | "PointOfCast" "midpoint" 87 | "MaxDistance" "1000" 88 | "MinDistance" "500" 89 | } 90 | } 91 | ``` 92 | 93 | The following sections cover these options in detail. 94 | 95 | ###Custom Targeting Particles 96 | 97 | You can choose a custom particle system to use instead of the default range finder 98 | 99 | ```javascript 100 | "ParticleName" "particles/my_custom_particle.vpcf" 101 | ``` 102 | 103 | Since useful targeting indicators are for casual scrubs that play League of Legends and have no business being in Hardcore Games™, you can set this option to "0" to disable any kind of range finder. 104 | 105 | You can also feed in custom control point data to your particle system. 106 | 107 | ```javascript 108 | "ControlPoints" // use custom control points for the particle 109 | { 110 | "0" "initial" // Set CP0 to the vector's initial point (the first location clicked) 111 | "1" "terminal" // Set CP1 to the vector's terminal point (the second location clicked) 112 | } 113 | ``` 114 | 115 | ###"Point of Cast" 116 | 117 | While conceptually a vector targeted ability targets a vector, in the Dota engine it is just a normal point targeted ability. As such, there is a point that the unit must be in range of and turn towards before beginning the cast animation. By default, that point is chosen to be the first point clicked, but you can choose other points as well. This point is called the "point of cast" to avoid confusion with the term "cast point", which has a completely different meaning in Dota terminology. 118 | 119 | Using `midpoint` will make the caster turn towards the midpoint of the vector (treating it as a line segment) 120 | 121 | ```javascript 122 | "PointOfCast" "midpoint" 123 | ``` 124 | 125 | Using `terminal` will make the caster turn towards the terminal position (second point that was clicked) 126 | 127 | ```javascript 128 | "PointOfCast" "terminal" 129 | ``` 130 | 131 | For complete flexibility, you can instead override the method `GetPointOfCast` on your Lua ability, returning an exact Vector to use. 132 | 133 | ###Minimum and Maximum Distance 134 | 135 | ```javascript 136 | "MaxDistance" "1000" // Sets the max distance of the vector. Currently this isn't enforced and we don't 137 | // do much with this parameter other than return it via GetMaxDistance, 138 | // but this will likely change in the future. 139 | 140 | "MinDistance" "500" // Minimum vector distance, also not fully supported yet. 141 | ``` 142 | 143 | #Writing Ability Code 144 | 145 | Once you've defined abilities to have vector targeting behavior, you can start writing code to actually handle the ability's 146 | cast logic. When writing ability code you can access the targeting information from a cast via special methods that the library attaches to vector targeted abilities. 147 | 148 | ##Ability Properties and Methods 149 | Any ability that's been modified by the library will have a key named `isVectorTarget` set to true, and will have these methods: 150 | 151 | * `:GetInitialPosition()` - The initial position as a Vector 152 | 153 | * `:GetTerminalPosition()` - The terminal position as a Vector 154 | 155 | * `:GetMidpointPosition()` - The midpoint betwen initial/terminal as a Vector 156 | 157 | * `:GetTargetVector()` - The actual vector in the phrase "vector target", composed from the initial and terminal positions of the cast. 158 | 159 | * `:GetDirectionVector()` - The normalized target vector, indicating the direction in which the line was drawn. 160 | 161 | * `:GetPointOfCast()` - The point, as a Vector, that the caster turns towards before beginning the cast animation. 162 | 163 | * `:GetMaxDistance()` - The MaxDistance KV field. Currently unused by the library, but provided for ability logic. 164 | 165 | * `:GetMinDistance()` - The MinDistance KV field. Also unsued currently. 166 | 167 | #Code Examples 168 | * A Macropyre-like ability with vector targeting: 169 | * [Ability KV](https://github.com/kallisti-dev/WarOfExalts/blob/4aaf3c5db5ab4febd3e9ef1bd05c6529c4ca1a8a/game/dota_addons/warofexalts/scripts/npc/abilities/flameshaper_lava_wake.txt) 170 | * [Ability Lua](https://github.com/kallisti-dev/WarOfExalts/blob/6f62f8c5a21f0c837e9ac43bd34479230c10a76a/game/dota_addons/warofexalts/scripts/vscripts/heroes/flameshaper/flameshaper_lava_wake.lua) 171 | 172 | #Advanced Topics 173 | ##KV Loading (From File or Table) 174 | If you have a sophisticated custom KV setup for your addon or would simply prefer seperating dota-specific KV stuff from vector-target-specific KV stuff, you can use the `kvList` option when calling `VectorTarget:Init` to load KV options from other sources. 175 | 176 | ```lua 177 | VectorTarget:Init({ 178 | kvList = { "my_custom_kv_file.txt", "my_custom_kv_file2.txt", myTable } 179 | }) 180 | ``` 181 | 182 | `kvList` is an array of "KV sources", which can be either file names or Lua tables. If you use a table as a KV source, it should have the same format as a KV table returned by the Valve function `LoadKeyValues`. If you want to disable automatic KV loading, you can explicitly set `kvList` to false 183 | 184 | 185 | ##ExecuteOrderFilter 186 | 187 | This library uses `SetExecuteOrderFilter`. If you have other code that needs to run during this filter, you'll need to 188 | set the `noOrderFilter` option when calling `VectorTarget:Init`, and then call `VectorTarget:OrderFilter` in your own custom order filter. 189 | 190 | ```lua 191 | VectorTarget:Init({ noOrderFilter = true }) 192 | 193 | function MyExecuteOrderFilter(ctx, params) 194 | --insert your order filter logic here 195 | return VectorTarget:OrderFilter(params) 196 | end 197 | 198 | GameRules:GetGameModEntity():SetExecuteOrderFilter(MyExecuteOrderFilter, {}) 199 | ``` 200 | 201 | This is a simple example of what your custom filter might look like, but it could be written differently. The only requirement is that your custom order filter MUST `return false` whenever `VectorTarget:OrderFilter` returns false. This is because `VectorTarget:OrderFilter` needs to force the engine to ignore the initial cast order of a vector targeted ability. 202 | 203 | **A call to action for the modding community**: I would be very interested in working with the modding community 204 | to create a standard system for overloading these filter functions in a composable manner, perhaps something incorporated 205 | into barebones, or a fork of barebones. This would go a long way in making library code more readily interoptable. 206 | 207 | ##Adding Vector Targeting Behavior to Abilities Dynamically 208 | The library will "vectorify" all abilities immediately before the first time they're casted. If for some reason you want to do it earlier, you will need to manually call `VectorTarget:WrapAbility` 209 | 210 | ```lua 211 | VectorTarget:WrapAbility(myAbility) 212 | ``` 213 | 214 | After the first `VectorTarget:WrapAbility` call, further calls will have no additional effect. So it's safe to call this function multiple times on the same ability. 215 | 216 | `VectorTarget:WrapUnit` can be used to apply this wrapper to all the current abilities of a unit/NPC. 217 | 218 | #Planned Improvements and to-do 219 | * Better options for fast click-drag mode. 220 | * Support various combinations of unit-targeting and point-targeting, for example HoN's "Vector Entity" target type. 221 | * Add more built-in particles for area/cone abilities, and wide abilities. 222 | * Add more variables for the `ControlPoints` KV blocks. 223 | * Properly handling %variables from `AbilitySpecial` in VectorTarget KV block. 224 | * Enforce and fully support `MaxDistance` and `MinDistance`. Which includes: 225 | *Options for specifying the localization string for "invalid cast distance" error messages. 226 | *Add `ControlPoint` variables for range finders to properly show valid/invalid distances 227 | *Add level scaling format, i.e. `"MaxDistance" "500 600 700 800"` 228 | 229 | #Feedback, Suggestions, Contributions 230 | 231 | Plese contact [kallisti.dev@gmail.com](mailto:kallisti.dev@gmail.com) if you have an idea or suggestion, and please submit a pull request to the github repo if you have a modification that would improve the library. 232 | -------------------------------------------------------------------------------- /content/panorama/scripts/vector_target.js: -------------------------------------------------------------------------------- 1 | /* 2 | AUTHOR: Adam Curtis, Copyright 2015 3 | CONTACT: kallisti.dev@gmail.com 4 | WEBSITE: https://github.com/kallisti-dev/vector_target 5 | LICENSE: https://github.com/kallisti-dev/vector_target/blob/master/LICENSE 6 | 7 | Client-side handlers to accompany the server-side vector_target.lua library. Aside from including this script, 8 | no other client-side initialization is currently necessary. 9 | 10 | */ 11 | 'use strict'; 12 | var VECTOR_TARGET_VERSION = [0, 3, 0]; //version data 13 | 14 | var VectorTarget = {} // public API 15 | 16 | VectorTarget.SetFastClickDragMode = function(flag) { 17 | /* Enables fast click-drag mode, where releasing the mouse button will complete the cast. */ 18 | VectorTarget.fastClickDragMode = flag; 19 | }; 20 | 21 | VectorTarget.IsFastClickDragMode = function() { 22 | /* Checks whether or not we're in fast click-drag mode */ 23 | return VectorTarget.fastClickDragMode; 24 | }; 25 | 26 | 27 | (function() { 28 | //constants 29 | var UPDATE_RANGE_FINDER_RATE = 1/30; // rate in seconds to update range finder control points 30 | var INACTIVE_CANCEL_DELAY = 0.1 // number of seconds to wait before the UI senda the cancel order event (prevents some race conditons between client/server handling) 31 | //state variables 32 | var rangeFinderParticle; 33 | var eventKeys = { }; 34 | var prevEventKeys = { }; 35 | var inactiveTimer = Game.GetGameTime(); // amount of time that no ability has been active 36 | 37 | GameEvents.Subscribe("vector_target_order_start", function(keys) { 38 | //$.Msg("vector_target_order_start event"); 39 | //$.Msg(keys); 40 | //initialize local state 41 | eventKeys = keys; 42 | var p = keys.initialPosition; 43 | keys.initialPosition = [p.x, p.y, p.z]; 44 | Abilities.ExecuteAbility(keys.abilId, keys.unitId, false); //make ability our active ability so that a left-click will complete cast 45 | showRangeFinder(); 46 | }); 47 | 48 | function showRangeFinder() { 49 | if(!rangeFinderParticle && eventKeys.particleName) { 50 | rangeFinderParticle = Particles.CreateParticle(eventKeys.particleName, ParticleAttachment_t.PATTACH_ABSORIGIN, eventKeys.unitId); 51 | mapToControlPoints({ 52 | "initial": eventKeys.initialPosition, 53 | "terminal": [eventKeys.initialPosition[0] + 1, eventKeys.initialPosition[1], eventKeys.initialPosition[2]] 54 | }); 55 | }; 56 | } 57 | 58 | function hideRangeFinder() { 59 | if(rangeFinderParticle) { 60 | Particles.DestroyParticleEffect(rangeFinderParticle, false); 61 | Particles.ReleaseParticleIndex(rangeFinderParticle); 62 | rangeFinderParticle = undefined; 63 | } 64 | } 65 | 66 | function updateRangeFinder() { 67 | var activeAbil = Abilities.GetLocalPlayerActiveAbility(); 68 | if(eventKeys.abilId === activeAbil) { 69 | showRangeFinder(); 70 | } 71 | if(rangeFinderParticle) { 72 | if(eventKeys.abilId !== activeAbil) { 73 | hideRangeFinder(); 74 | } 75 | else { 76 | var pos = GameUI.GetScreenWorldPosition(GameUI.GetCursorPosition()); 77 | if(pos != null) { 78 | var start = eventKeys.initialPosition; 79 | var keys = { } 80 | if (pos[0] != start[0] || pos[1] != start[1] || pos[2] != start[2]) { 81 | keys.terminal = pos; 82 | keys.midpoint = vMidPoint(start, pos); 83 | keys.maxdistancedelta = 84 | mapToControlPoints(keys, true); 85 | } 86 | } 87 | } 88 | } 89 | if(eventKeys.abilId != null && activeAbil === -1) { 90 | var now = Game.GetGameTime(); 91 | inactiveTimer = inactiveTimer || now; 92 | if (now - inactiveTimer >= INACTIVE_CANCEL_DELAY ) { 93 | cancelVectorTargetOrder() 94 | } 95 | } 96 | else { 97 | inactiveTimer = null; 98 | } 99 | $.Schedule(UPDATE_RANGE_FINDER_RATE, updateRangeFinder); 100 | } 101 | updateRangeFinder(); 102 | 103 | function cancelVectorTargetOrder() { 104 | if(eventKeys.abilId === undefined) return; 105 | //$.Msg("Canceling ", eventKeys) 106 | GameEvents.SendCustomGameEventToServer("vector_target_order_cancel", eventKeys); 107 | finalize(); 108 | } 109 | 110 | 111 | function mapToControlPoints(keyMap, ignoreConst) { 112 | var cpMap = eventKeys.cpMap; 113 | for(var cp in cpMap) { 114 | var vector = cpMap[cp].split(" "); 115 | if(vector.length == 1) { 116 | vector = [vector[0], vector[0], vector[0]] 117 | } 118 | else if(vector.length != 3) { 119 | throw new Error("Vector for CP " + cp + " has " + vector.length + " components"); 120 | } 121 | var shouldSet = !ignoreConst; 122 | for(var i in vector) { 123 | var val = vector[i]; 124 | var out; 125 | if((out = keyMap[val]) !== undefined) { //check for string variables 126 | vector[i] = out[i]; 127 | if(ignoreConst) shouldSet = true; 128 | } 129 | else if(!isNaN(out = parseInt(val))) { //is a number 130 | vector[i] = out; 131 | } 132 | else { 133 | shouldSet = false; 134 | } 135 | } 136 | if(shouldSet) { 137 | //$.Msg(cp, vector) 138 | Particles.SetParticleControl(rangeFinderParticle, parseInt(cp), vector); 139 | } 140 | } 141 | } 142 | 143 | function finalize() { 144 | //$.Msg("finalizer called"); 145 | hideRangeFinder(); 146 | prevEventKeys = eventKeys; 147 | /* 148 | if(Abilities.GetLocalPlayerActiveAbility() == eventKeys.abilId) { 149 | $.Msg("re-execute"); 150 | Abilities.ExecuteAbility(eventKeys.abilId, eventKeys.unitId, false); 151 | } 152 | */ 153 | eventKeys = { }; 154 | } 155 | 156 | GameEvents.Subscribe("vector_target_order_cancel", function(keys) { 157 | //$.Msg("canceling"); 158 | if(keys.seqNum === eventKeys.seqNum && keys.abilId === eventKeys.abilId && keys.unitId === eventKeys.unitId) { 159 | finalize(); 160 | } 161 | }); 162 | 163 | GameEvents.Subscribe("vector_target_order_finish", function(keys) { 164 | //$.Msg("finished") 165 | if(keys.seqNum === eventKeys.seqNum && keys.abilId === eventKeys.abilId && keys.unitId === eventKeys.unitId) { 166 | finalize(); 167 | } 168 | }); 169 | 170 | GameEvents.Subscribe("dota_update_selected_unit", function(keys) { 171 | var selection = Players.GetSelectedEntities(Game.GetLocalPlayerID()); 172 | //$.Msg("update selected unit") 173 | if(selection[0] !== eventKeys.unitId) { 174 | cancelVectorTargetOrder(); 175 | } 176 | }); 177 | 178 | GameEvents.Subscribe("dota_hud_error_message", function(keys) { 179 | if(keys.reason == 105) { // reason code for a full order queue 180 | GameEvents.SendCustomGameEventToServer("vector_target_queue_full", prevEventKeys); 181 | } 182 | }); 183 | 184 | //fast click-drag handling 185 | GameUI.SetMouseCallback(function(eventName, button) { 186 | if (eventKeys.abilId && VectorTarget.IsFastClickDragMode() && eventName == "released" && button == 0) { 187 | Abilities.ExecuteAbility(eventKeys.abilId, eventKeys.unitId, true); 188 | } 189 | }); 190 | 191 | //VectorTarget.SetFastClickDragMode(true); 192 | 193 | /* functional programming helpers */ 194 | 195 | function zipWith(f, a, b) { 196 | return a.map(function(x, i) { return f(x, b[i]); }); 197 | } 198 | 199 | /* vector math */ 200 | 201 | function vMidPoint(a, b) { 202 | return zipWith(function(a,b) { return (a + b) / 2; }, a, b) 203 | //return [ (a[0] + b[0])/2, (a[1] + b[1])/2, (a[2] + b[2])/2 ]; 204 | } 205 | 206 | function vLength(v) { 207 | return Math.sqrt(v.reduce(function(a,b) { return a + Math.pow(b, 2); }, 0)); 208 | //return Math.sqrt(Math.pow(v[0], 2), Math.pow(v[1], 2), Math.pow(v[2], 2)); 209 | } 210 | 211 | function vNormalize(v) { 212 | var d = vLength(v); 213 | return vScalarDiv(v, d); 214 | //return [v[0] / d, v[1] / d, v[2] / d]; 215 | } 216 | 217 | function vScalarMul(v, s) { 218 | return v.map(function(x) { return x * s; }); 219 | //return [v[0] * s, v[1] * s, v[2] * s]; 220 | } 221 | 222 | function vScalarDiv(v, s) { 223 | return v.map(function(x) { return x / s; }); 224 | } 225 | 226 | function vDiff(a, b) { 227 | return zipWith(function(a,b){return a-b;}, a, b); 228 | } 229 | 230 | })(); 231 | 232 | $.Msg("vector_target.js loaded"); -------------------------------------------------------------------------------- /content/particles/vector_target/vector_target_range_finder_line.vpcf: -------------------------------------------------------------------------------- 1 | 2 | { 3 | _class = "CParticleSystemDefinition" 4 | m_bShouldHitboxesFallbackToRenderBounds = false 5 | m_nMaxParticles = 40 6 | m_flConstantRadius = 15.000000 7 | m_ConstantColor = 8 | [ 9 | 0, 10 | 255, 11 | 0, 12 | 255, 13 | ] 14 | m_Renderers = 15 | [ 16 | { 17 | _class = "C_OP_RenderRopes" 18 | m_nSequenceCombineMode = "SEQUENCE_COMBINE_MODE_USE_SEQUENCE_0" 19 | m_bMod2X = true 20 | m_nOrientationType = 3 21 | m_hTexture = resource:"materials/particle/particle_beam_generic.vtex" 22 | m_flRadiusScale = 0.500000 23 | m_flTextureVWorldSize = 19.999998 24 | m_flTextureVScrollRate = -19.999998 25 | m_nMaxTesselation = 4 26 | m_nMinTesselation = 4 27 | }, 28 | ] 29 | m_Operators = 30 | [ 31 | { 32 | _class = "C_OP_BasicMovement" 33 | }, 34 | { 35 | _class = "C_OP_Decay" 36 | m_nOpEndCapState = 1 37 | }, 38 | { 39 | _class = "C_OP_MaintainSequentialPath" 40 | m_flNumToAssign = 24.000000 41 | m_flTolerance = 2.000000 42 | m_PathParams = 43 | { 44 | m_nStartControlPointNumber = 1 45 | m_nEndControlPointNumber = 2 46 | } 47 | }, 48 | { 49 | _class = "C_OP_PercentageBetweenCPsVector" 50 | m_nEndCP = 2 51 | m_nStartCP = 1 52 | m_vecOutputMax = 53 | [ 54 | 0.000000, 55 | 0.600000, 56 | 0.000000, 57 | ] 58 | m_vecOutputMin = 59 | [ 60 | 0.000000, 61 | 0.900000, 62 | 0.500000, 63 | ] 64 | }, 65 | { 66 | _class = "C_OP_PercentageBetweenCPs" 67 | m_bActiveRange = true 68 | m_bScaleInitialRange = true 69 | m_nEndCP = 2 70 | m_nStartCP = 1 71 | m_nFieldOutput = 16 72 | m_flInputMax = 0.100000 73 | }, 74 | { 75 | _class = "C_OP_MovementPlaceOnGround" 76 | m_flOffset = 16.000000 77 | m_nRefCP1 = 2 78 | m_nRefCP2 = 1 79 | m_flTolerance = 2.000000 80 | m_flMaxTraceLength = 512.000000 81 | m_flTraceOffset = 128.000000 82 | m_CollisionGroupName = "DEBRIS" 83 | }, 84 | { 85 | _class = "C_OP_PercentageBetweenCPs" 86 | m_flInputMax = 0.001250 87 | m_flOutputMin = 0.100000 88 | m_nStartCP = 2 89 | m_bScaleCurrent = true 90 | m_bRadialCheck = false 91 | }, 92 | ] 93 | m_Initializers = 94 | [ 95 | { 96 | _class = "C_INIT_RandomRadius" 97 | m_flRadiusMin = 24.000000 98 | m_flRadiusMax = 24.000000 99 | }, 100 | { 101 | _class = "C_INIT_CreateSequentialPath" 102 | m_flNumToAssign = 40.000000 103 | m_PathParams = 104 | { 105 | m_nStartControlPointNumber = 1 106 | } 107 | }, 108 | { 109 | _class = "C_INIT_RandomColor" 110 | m_ColorMax = 111 | [ 112 | 45, 113 | 173, 114 | 0, 115 | 255, 116 | ] 117 | m_ColorMin = 118 | [ 119 | 45, 120 | 173, 121 | 0, 122 | 255, 123 | ] 124 | }, 125 | { 126 | _class = "C_INIT_RandomAlpha" 127 | m_nAlphaMax = 128 128 | m_nAlphaMin = 128 129 | }, 130 | { 131 | _class = "C_INIT_RandomLifeTime" 132 | }, 133 | { 134 | _class = "C_INIT_RemapCPtoVector" 135 | }, 136 | ] 137 | m_Emitters = 138 | [ 139 | { 140 | _class = "C_OP_InstantaneousEmitter" 141 | m_nParticlesToEmit = 24 142 | }, 143 | ] 144 | m_Children = 145 | [ 146 | { 147 | m_ChildRef = resource:"particles/ui_mouseactions/range_finder_d_glow.vpcf" 148 | }, 149 | ] 150 | m_flCullRadius = -1.000000 151 | } -------------------------------------------------------------------------------- /game/scripts/vscripts/vector_target.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | AUTHOR: Adam Curtis, Copyright 2015 3 | CONTACT: kallisti.dev@gmail.com 4 | WEBSITE: https://github.com/kallisti-dev/vector_target 5 | LICENSE: https://github.com/kallisti-dev/vector_target/blob/master/LICENSE 6 | --]] 7 | 8 | DEFAULT_VECTOR_TARGET_PARTICLE = "particles/vector_target/vector_target_range_finder_line.vpcf" 9 | DEFAULT_VECTOR_TARGET_CONTROL_POINTS = { 10 | [0] = "initial", 11 | [1] = "initial", 12 | [2] = "terminal" 13 | } 14 | 15 | --[[ 16 | VECTOR_TARGET_DEBUG_NONE = 0 -- no logging 17 | VECTOR_TARGET_DEBUG_DEFAULT = 1 -- default logging of important events 18 | VECTOR_TARGET_DEBUG_ALL = 2 -- detailed debug info 19 | ]] 20 | local reloading = false 21 | if VectorTarget == nil then 22 | VectorTarget = { 23 | inProgressOrders = { }, -- a table of vector orders currently in-progress, indexed by player ID 24 | abilityKeys = { }, -- data loaded from KV files, indexed by ability name 25 | kvSources = { }, -- a list of filenames / tables that were passed to LoadKV, used during reloading 26 | castQueues = { }, -- table of cast queues indexed by castQueues[unit ID][ability ID] 27 | userIds = { } -- user id -> player id 28 | --debugMode = VECTOR_TARGET_DEBUG_ALL, -- debug output mode 29 | } 30 | else 31 | reloading = true 32 | end 33 | 34 | VectorTarget.VERSION = {0,3,0}; 35 | 36 | local queue = class({}) -- sparse queue implementation 37 | 38 | -- call this in your Precache() function to precache vector targeting particles 39 | function VectorTarget:Precache(context) 40 | if self.initializedPrecache then return end 41 | print("[VECTORTARGET] precaching assets") 42 | --PrecacheResource("particle", "particles/vector_target_ring.vpcf", context) 43 | PrecacheResource("particle", "particles/vector_target/vector_target_range_finder_line.vpcf", context) 44 | self.initializedPrecache = true 45 | end 46 | 47 | 48 | -- call this in your init function to initialize for default use-case behavior 49 | function VectorTarget:Init(opts) 50 | print("[VECTORTARGET] initializing") 51 | if not self.initializedPrecache then 52 | print("[VECTORTARGET] warning: VectorTarget:Precache was not called before Init.") 53 | end 54 | opts = opts or { } 55 | if not opts.noEventListeners then 56 | self:InitEventListeners() 57 | end 58 | if not opts.noOrderFilter then 59 | self:InitOrderFilter() 60 | end 61 | if opts.kvList ~= false then 62 | self:LoadKV(opts.kvList or {"scripts/npc/npc_abilities_custom.txt", "scripts/npc/npc_items_custom.txt"}) 63 | end 64 | self.debugMode = opts.debug or self.debugMode 65 | end 66 | 67 | -- call this in your init function to start listening to events 68 | function VectorTarget:InitEventListeners() 69 | if self.initializedEventListeners then return end 70 | print("[VECTORTARGET] registering event listeners") 71 | -- Note: wrapping the calls in an anonymous function allows reloading to work properly 72 | --ListenToGameEvent("npc_spawned", function(...) self:_OnNpcSpawned(...) end, {}) 73 | CustomGameEventManager:RegisterListener("vector_target_order_cancel", function(...) self:_OnVectorTargetOrderCancel(...) end) 74 | CustomGameEventManager:RegisterListener("vector_target_queue_full", function(...) self:_OnVectorTargetQueueFull(...) end) 75 | --ListenToGameEvent('player_connect_full', Dynamic_Wrap(VectorTarget, "_OnPlayerConnectFull"), self) 76 | self.initializedEventListeners = true 77 | end 78 | 79 | -- call this in your init code to initialize the library's SetExecuteOrderFilter 80 | function VectorTarget:InitOrderFilter() 81 | if self.initializedOrderFilter then return end 82 | print("[VECTORTARGET] registering ExecuteOrderFilter (use noOrderFilter option to prevent this)") 83 | local mode = GameRules:GetGameModeEntity() 84 | mode:ClearExecuteOrderFilter() 85 | mode:SetExecuteOrderFilter(function(_, data) return self:OrderFilter(data) end, {}) -- Note: wrapping the call in an anonymous function allows reloading to work properly 86 | self.initializedOrderFilter = true 87 | end 88 | 89 | -- Loads vector target KV values from a file, or a table with the same format as one returned by LoadKeyValues() 90 | function VectorTarget:LoadKV(kvList, forgetSource) 91 | for _, kv in ipairs(kvList or { }) do 92 | local kvFile 93 | if type(kv) == "string" then 94 | kvFile = kv 95 | kv = LoadKeyValues(kvFile) 96 | if kv == nil then 97 | error("[VECTORTARGET] Error when loading KV from file: " .. kvFile) 98 | end 99 | elseif type(kv) ~= "table" then 100 | error("[VECTORTARGET] LoadKV: expected string or table but got " .. type(kv) .. ": " .. tostring(kv)) 101 | end 102 | print("[VECTORTARGET] Loading KV data from: " .. (kvFile or tostring(kv))) 103 | for name, keys in pairs(kv) do 104 | if type(keys) == "table" then 105 | keys = keys["VectorTarget"] 106 | if keys and keys ~= "false" and keys ~= "0" and (type(keys) ~= "number" or keys ~= 0) then 107 | if type(keys) ~= "table" then 108 | keys = { } 109 | end 110 | self.abilityKeys[name] = keys 111 | end 112 | else 113 | print("[VECTORTARGET] Warning: Expected a table for ability definition " .. name .. " but got " .. type(keys) .. " instead.") 114 | end 115 | end 116 | if not forgetSource then 117 | table.insert(self.kvSources, kvFile or kv) 118 | end 119 | end 120 | end 121 | 122 | function VectorTarget:ReloadAllKV(deletePrevious) 123 | --[[ Reloads KV from files/tables passed via VectorTarget:LoadKV 124 | 125 | If the first argument is false, prevents deletion of previous KV data before reloading 126 | ]] 127 | if deletePrevious ~= false then 128 | self.abilityKeys = { } 129 | end 130 | self:LoadKV(self.kvSources, true) 131 | end 132 | 133 | function VectorTarget:GetInProgressForPlayer(playerId) 134 | --[[ Retrieves the current in-progress order, if any, for the given player. 135 | ]] 136 | return self.inProgressOrders[playerId] 137 | end 138 | 139 | function VectorTarget:GetInProgressForUnit(unitId) 140 | --[[ Gets in-progress orders for current unit. 141 | 142 | Since multiple players could have in-progress orders on the same unit, this method returns an array of orders indexed by issuer's player ID 143 | ]] 144 | local out = { } 145 | for playerId, order in ipairs(self.inProgressOrders) do 146 | if order.unitId == unitId then 147 | out[playerId] = order 148 | end 149 | end 150 | return out 151 | end 152 | 153 | function VectorTarget:GetInProgressForAbility(abilId) 154 | --[[ Gets in-progress orders for current ability. 155 | 156 | Since multiple players could have in-progress orders on the same unit, this method returns an array of orders indexed by issuer's player ID. 157 | ]] 158 | local out = { } 159 | for playerId, order in ipairs(self.inProgressOrders) do 160 | if order.abilId == abilId then 161 | out[playerId] = order 162 | end 163 | end 164 | return out 165 | end 166 | 167 | -- get the cast queue for a given (unit, ability) pair 168 | function VectorTarget:GetCastQueue(unitId, abilId) 169 | local queues = self.castQueues 170 | local unitTable = queues[unitId] 171 | if not unitTable then 172 | unitTable = { } 173 | queues[unitId] = unitTable 174 | end 175 | local q = unitTable[abilId] 176 | if not q then 177 | q = queue() 178 | unitTable[abilId] = q 179 | end 180 | return q 181 | end 182 | 183 | -- given an array of unit ids, clear all cast queues associated with those units 184 | function VectorTarget:ClearQueuesForUnits(units) 185 | for _, unitId in pairs(units) do 186 | for _, q in pairs(self.castQueues[unitId] or { }) do 187 | if q then 188 | q:clear() 189 | end 190 | end 191 | end 192 | end 193 | 194 | -- get the largest sequence number for vector target orders issued on this unit 195 | function VectorTarget:GetMaxSequenceNumber(unitId) 196 | local out = -1 197 | for _, q in pairs(self.castQueues[unitId] or { }) do 198 | if q and q.last > out then 199 | out = q.last 200 | end 201 | end 202 | return out 203 | end 204 | 205 | -- call this on a unit to add vector target functionality to its abilities 206 | function VectorTarget:WrapUnit(unit) 207 | for i=0, unit:GetAbilityCount()-1 do 208 | local abil = unit:GetAbilityByIndex(i) 209 | if abil ~= nil then 210 | self:WrapAbility(abil) 211 | end 212 | end 213 | end 214 | 215 | --wrapper applied to all vector targeted abilities during initialization 216 | function VectorTarget:WrapAbility(abil, reloading) 217 | local keys = self.abilityKeys[abil:GetAbilityName()] 218 | if keys == nil then -- no VectorTarget block 219 | return 220 | end 221 | local VectorTarget = self 222 | local abiName = abil:GetAbilityName() 223 | local cName = abil:GetClassname() 224 | if "ability_lua" ~= cName and "item_lua" ~= cName then 225 | print("[VECTORTARGET] Warning: " .. abiName .. " is not a Lua ability/item and cannot be vector targeted.") 226 | return 227 | end 228 | if not reloading and abil.isVectorTarget then 229 | return 230 | end 231 | 232 | --initialize members 233 | abil.isVectorTarget = true -- use this to test if an ability has vector targeting 234 | abil._vectorTargetKeys = { 235 | initialPosition = nil, -- initial position of vector input 236 | terminalPosition = nil, -- terminal position of vector input 237 | minDistance = keys.MinDistance, 238 | maxDistance = keys.MaxDistance, 239 | pointOfCast = keys.PointOfCast or "initial", 240 | particleName = keys.ParticleName or DEFAULT_VECTOR_TARGET_PARTICLE, 241 | cpMap = keys.ControlPoints or DEFAULT_VECTOR_TARGET_CONTROL_POINTS, 242 | distance3D = keys.Distance3D 243 | } 244 | 245 | function abil:GetInitialPosition() 246 | return self._vectorTargetKeys.initialPosition 247 | end 248 | 249 | function abil:SetInitialPosition(v) 250 | if type(v) == "table" then 251 | v = Vector(v.x, v.y, v.z) 252 | end 253 | self._vectorTargetKeys.initialPosition = v 254 | end 255 | 256 | function abil:GetTerminalPosition() 257 | return self._vectorTargetKeys.terminalPosition 258 | end 259 | 260 | function abil:SetTerminalPosition(v) 261 | if type(v) == "table" then 262 | v = Vector(v.x, v.y, v.z) 263 | end 264 | self._vectorTargetKeys.terminalPosition = v 265 | end 266 | 267 | function abil:GetMidpointPosition() 268 | return VectorTarget._CalcMidPoint(self:GetInitialPosition(), self:GetTerminalPosition()) 269 | end 270 | 271 | function abil:GetTargetVector() 272 | local i = self:GetInitialPosition() 273 | local j = self:GetTerminalPosition() 274 | return Vector(j.x - i.x, j.y - i.y, j.z - i.z) 275 | end 276 | 277 | 278 | function abil:GetDirectionVector() 279 | return self:GetTargetVector():Normalized() 280 | end 281 | 282 | function abil:GetDistance() 283 | if self:IsDistance3D() then 284 | return self:GetTargetVector():Length() 285 | else 286 | return self:GetTargetVector():Length2D() 287 | end 288 | end 289 | 290 | if not abil.IsDistance3D then 291 | function abil:IsDistance3D() 292 | return self._vectorTargetKeys.distance3D 293 | end 294 | end 295 | 296 | 297 | 298 | if not abil.GetMinDistance then 299 | function abil:GetMinDistance() 300 | local min = self._vectorTargetKeys.minDistance 301 | if min ~= nil then 302 | return VectorTarget._GetLevelScalableKey(self, min) 303 | end 304 | end 305 | end 306 | 307 | if not abil.GetMaxDistance then 308 | function abil:GetMaxDistance() 309 | local max = self._vectorTargetKeys.maxDistance 310 | if max ~= nil then 311 | return VectorTarget._GetLevelScalableKey(self, max) 312 | end 313 | end 314 | end 315 | 316 | if not abil.IsDistanceInRange then 317 | function abil:IsDistanceInRange() 318 | local d, min, max = self:GetDistance(), self:GetMinDistance(), self:GetMaxDistance() 319 | return (min == nil or d >= min) and (max == nil or d <= max) 320 | end 321 | end 322 | 323 | if not abil.GetPointOfCast then 324 | function abil:GetPointOfCast() 325 | return VectorTarget._CalcPointOfCast(abil._vectorTargetKeys.pointOfCast, abil:GetInitialPosition(), abil:GetTerminalPosition()) 326 | end 327 | end 328 | 329 | if not abil.GetVectorTargetParticleName then 330 | function abil:GetVectorTargetParticleName() 331 | return self._vectorTargetKeys.particleName 332 | end 333 | end 334 | 335 | if not abil.GetVectorTargetControlPoints then 336 | function abil:GetVectorTargetControlPoints() 337 | return self._vectorTargetKeys.cpMap 338 | end 339 | end 340 | 341 | --override GetBehavior 342 | local _GetBehavior = abil.GetBehavior 343 | function abil:GetBehavior() 344 | local b = _GetBehavior(self) 345 | return bit.bor(b, DOTA_ABILITY_BEHAVIOR_POINT) 346 | end 347 | 348 | --override OnAbilityPhaseStart 349 | local _OnAbilityPhaseStart = abil.OnAbilityPhaseStart 350 | function abil:OnAbilityPhaseStart() 351 | if not reloading then 352 | local abilId = self:GetEntityIndex() 353 | local unitId = self:GetCaster():GetEntityIndex() 354 | local data = VectorTarget:GetCastQueue(unitId, abilId):popFirst() 355 | self:SetInitialPosition(data.initialPosition) 356 | self:SetTerminalPosition(data.terminalPosition) 357 | end 358 | return _OnAbilityPhaseStart(self) 359 | end 360 | 361 | --override CastFilterResultLocation 362 | local _CastFilterResultLocation = abil.CastFilterResultLocation 363 | function abil:CastFilterResultLocation(location) 364 | return VectorTarget:_CastFilterHelper(self, _CastFilterResultLocation, location) 365 | end 366 | 367 | --override GetCustomCastErrorLocation 368 | local _GetCustomCastErrorLocation = abil.GetCustomCastErrorLocation 369 | function abil:GetCustomCastErrorLocation(location) 370 | return VectorTarget:_GetCastErrorHelper(self, _GetCustomCastErrorLocation, location) 371 | end 372 | end 373 | 374 | function VectorTarget:OrderFilter(data) 375 | --util.printTable(data) 376 | local playerId = data.issuer_player_id_const 377 | local abilId = data.entindex_ability 378 | local inProgress = self.inProgressOrders[playerId] -- retrieve any in-progress orders for this player 379 | local seqNum = data.sequence_number_const 380 | local units = { } 381 | local nUnits = 0 382 | for i, unitId in pairs(data.units) do 383 | if seqNum > self:GetMaxSequenceNumber(unitId) then 384 | units[i] = unitId 385 | nUnits = nUnits + 1 386 | end 387 | end 388 | if nUnits == 0 then 389 | return true 390 | end 391 | --print("seq num: ", seqNum, "order type: ", data.order_type, "queue: ", data.queue) 392 | if abilId ~= nil and abilId > 0 then 393 | local abil = EntIndexToHScript(abilId) 394 | if abil ~= nil then 395 | self:WrapAbility(abil) 396 | if abil.isVectorTarget and data.order_type == DOTA_UNIT_ORDER_CAST_POSITION then 397 | local unitId = units["0"] or units[0] 398 | local targetPos = {x = data.position_x, y = data.position_y, z = data.position_z} 399 | if inProgress == nil or inProgress.abilId ~= abilId or inProgress.unitId ~= unitId then -- if no in-progress order, this order selects the initial point of a vector cast 400 | --print("inProgress", playerId, abilId, unitId) 401 | local cpMap = abil._vectorTargetKeys.cpMap 402 | local orderData = { 403 | initialPosition = targetPos, 404 | minDistance = abil:GetMinDistance(), 405 | maxDistance = abil:GetMaxDistance(), 406 | cpMap = cpMap, 407 | cpSpecials = VectorTarget._GetAbilitySpecials(abil, cpMap), 408 | particleName = abil._vectorTargetKeys.particleName, 409 | seqNum = seqNum, 410 | abilId = abilId, 411 | time = Time(), 412 | orderType = data.order_type, 413 | unitId = unitId, 414 | shiftPressed = data.queue, 415 | } 416 | self.inProgressOrders[playerId] = orderData --set this order as our player's current in-progress order 417 | CustomGameEventManager:Send_ServerToPlayer(PlayerResource:GetPlayer(playerId), "vector_target_order_start", orderData) 418 | return false 419 | else --in-progress order (initial point has been selected) 420 | if inProgress.shiftPressed == 1 then --make this order shift-queued if previous order was 421 | data.queue = 1 422 | elseif data.queue == 0 then -- if not shift queued, clear cast queue before we add to it 423 | self:ClearQueuesForUnits(units) 424 | end 425 | 426 | inProgress.terminalPosition = targetPos 427 | 428 | --temporarily set initial/terminal on the ability so we can call (a possibly overriden) abil:GetPointOfCast 429 | local p = VectorTarget._WithPoints(abil, inProgress.initialPosition, inProgress.terminalPosition, function() 430 | return abil:GetPointOfCast() 431 | end) 432 | data.position_x = p.x 433 | data.position_y = p.y 434 | data.position_z = p.z 435 | self:GetCastQueue(unitId, abilId):push(inProgress, seqNum) 436 | self.inProgressOrders[playerId] = nil 437 | -- something in the inProgress table causes the event system to crash the game, so we need to make a new table and pick out 438 | -- only the important values. 439 | CustomGameEventManager:Send_ServerToPlayer(PlayerResource:GetPlayer(playerId), "vector_target_order_finish", { 440 | --terminalPosition = inProgress.initialPosition, 441 | --initialPosition = inProgress.terminalPosition, 442 | unitId = inProgress.unitId, 443 | abilId = inProgress.abilId, 444 | seqNum = inProgress.seqNum, 445 | }) 446 | return true -- exit early 447 | end 448 | end 449 | end 450 | end 451 | if data.queue == 0 then -- if shift was not pressed, clear our cast queues for the unit(s) in question 452 | self:ClearQueuesForUnits(units) 453 | end 454 | if inProgress ~= nil then 455 | self.inProgressOrders[playerId] = nil 456 | CustomGameEventManager:Send_ServerToPlayer(PlayerResource:GetPlayer(playerId), "vector_target_order_cancel", inProgress) 457 | end 458 | return true 459 | end 460 | 461 | --[[ Library Event Handlers ]] 462 | 463 | function VectorTarget:_OnVectorTargetOrderCancel(eventSource, keys) 464 | local pId = eventSource - 1 465 | local inProgress = self.inProgressOrders[pId] 466 | if inProgress ~= nil and inProgress.seqNum == keys.seqNum then 467 | --print("canceling") 468 | self.inProgressOrders[pId] = nil 469 | end 470 | end 471 | 472 | function VectorTarget:_OnVectorTargetQueueFull(eventSource, keys) 473 | --print("queue full") 474 | --util.printTable(keys) 475 | end 476 | 477 | --[[ 478 | function VectorTarget:_OnPlayerConnectFull(keys) 479 | local p = EntIndexToHScript(keys.index + 1) 480 | self.userIds[keys.index] = p:GetPlayerID() 481 | end 482 | ]] 483 | 484 | --[[ 485 | function VectorTarget:_OnNpcSpawned(ctx, keys) 486 | self:WrapUnit(EntIndexToHScript(keys.entindex)) 487 | end 488 | ]] 489 | 490 | function VectorTarget:_OnScriptReload() 491 | VectorTarget:ReloadAllKV() 492 | --reload existing abilities 493 | for _, ents in ipairs({Entities:FindAllByClassname("ability_lua"), Entities:FindAllByClassname("item_lua")}) do 494 | for _, abil in pairs(ents) do 495 | self:WrapAbility(abil, true) 496 | end 497 | end 498 | end 499 | 500 | --[[ Internal Helper/Utility Functions ]] 501 | 502 | function VectorTarget._GetAbilitySpecials(abil, t, lvl) 503 | lvl = lvl or abil:GetLevel() 504 | local out = { } 505 | for _,str in pairs(t) do 506 | for _, field in ipairs(VectorTarget._StringSplit(str)) do 507 | if VectorTarget:_IsSpecialField(field) then 508 | local name = VectorTarget:_ParseSpecialName(field) 509 | if out[name] == nil then 510 | out[name] = abil:GetLevelSpecialValueFor(name, lvl) 511 | end 512 | end 513 | end 514 | end 515 | return out 516 | end 517 | 518 | function VectorTarget._GetLevelScalableKey(abil, fieldString, lvl) 519 | local lvl = lvl or abil:GetLevel() 520 | local fields = VectorTarget._StringSplit(fieldString) 521 | local index = math.min(#fields, abil:GetMaxLevel(), lvl) 522 | local field = fields[index] 523 | if VectorTarget:_IsSpecialField(field) then 524 | field = abil:GetLevelSpecialValueFor(VectorTarget:_ParseSpecialName(field), lvl) 525 | end 526 | return tonumber(field) 527 | end 528 | 529 | function VectorTarget:_IsSpecialField(str) 530 | return string.sub(str, 1, 1) == "%" 531 | end 532 | 533 | function VectorTarget:_ParseSpecialName(str) 534 | return string.sub(str, 2) 535 | end 536 | 537 | function VectorTarget._CalcPointOfCast(mode, initial, terminal) 538 | if mode == "initial" then 539 | return initial 540 | elseif mode == "terminal" then 541 | return terminal 542 | elseif mode == "midpoint" then 543 | return VectorTarget._CalcMidPoint(initial, terminal) 544 | else 545 | error("[VECTORTARGET] invalid point-of-cast mode: " .. string(mode)) 546 | end 547 | end 548 | 549 | function VectorTarget._CalcMidPoint(a, b) 550 | return Vector((a.x + b.x)/2, (a.y + b.y)/2, (a.z + b.z)/2) 551 | end 552 | 553 | -- helper to temporarily set targeting information 554 | function VectorTarget._WithPoints(abil, initial, terminal, func, ...) 555 | local initialOld, terminalOld = abil:GetInitialPosition(), abil:GetTerminalPosition() 556 | abil:SetInitialPosition(initial) 557 | abil:SetTerminalPosition(terminal) 558 | local status, res = pcall(func, ...) 559 | abil:SetInitialPosition(initialOld) 560 | abil:SetTerminalPosition(terminalOld) 561 | if status then 562 | return res 563 | else 564 | error(res) 565 | end 566 | end 567 | 568 | function VectorTarget._StringSplit(s) 569 | local out = {} 570 | for word in string.gmatch(s, "%S+") do 571 | table.insert(out, word) 572 | end 573 | return out 574 | end 575 | 576 | function VectorTarget:_CastFilterHelper(abil, parentMethod, ...) 577 | local abilId = abil:GetEntityIndex() 578 | local unitId = abil:GetCaster():GetEntityIndex() 579 | local data = VectorTarget:GetCastQueue(unitId, abilId):peekLast() 580 | abil._vectorTargetKeys.castData = data 581 | --setup ability state 582 | abil:SetInitialPosition(data.initialPosition) 583 | abil:SetTerminalPosition(data.terminalPosition) 584 | local status = parentMethod(abil, ...) -- call parent method (CastFilterResultLocation, CastFilterResultTarget, etc) 585 | if status == UF_SUCCESS then 586 | if not abil:IsDistanceInRange() then 587 | status = UF_FAIL_CUSTOM 588 | data.castErrMsg = "#vector_target_distance_out_of_range" 589 | elseif abil.CastFilterResultVector then 590 | status = abil:CastFilterResultVector(data) 591 | end 592 | end 593 | return status 594 | end 595 | 596 | function VectorTarget:_GetCastErrorHelper(abil, parentMethod, ...) 597 | local data = abil._vectorTargetKeys.castData 598 | if data.castErrMsg then 599 | return data.castErrMsg 600 | end 601 | local msg = parentMethod(abil, ...) 602 | if (not msg or msg == "" or msg == "CUSTOM ERROR") and self.GetCustomCastErrorVector then 603 | msg = self:GetCustomCastErrorVector(data) 604 | end 605 | return msg 606 | end 607 | 608 | --[[ A sparse queue implementation ]] 609 | function queue.constructor(q) 610 | q.first = 0 611 | q.last = -1 612 | q.len = 0 613 | end 614 | 615 | function queue.push(q, value, seqN) 616 | --print("push", q.first, q.last, q.len) 617 | --[[if q:length() >= MAX_ORDER_QUEUE then 618 | print("[VECTORTARGET] warning: order queue has reached limit of " .. MAX_ORDER_QUEUE) 619 | return 620 | end]] 621 | if seqN == nil then 622 | seqN = q.last + 1 623 | end 624 | q[seqN] = value 625 | q.len = q.len + 1 626 | if q.len == 1 then 627 | q.first = seqN 628 | q.last = seqN 629 | elseif seqN > q.last then 630 | q.last = seqN 631 | elseif seqN < q.first then 632 | q.first = seqN 633 | end 634 | end 635 | 636 | function queue.popLast(q) 637 | local last = q.last 638 | if q.first > last then error("queue is empty") end 639 | local value = q[last] 640 | q[last] = nil 641 | q.len = q.len - 1 642 | for i = last, q.first, -1 do --find new last index 643 | if q[i] ~= nil then 644 | q.last = i 645 | return value 646 | end 647 | end 648 | q.last = q.first - 1 --empty 649 | return value 650 | end 651 | 652 | 653 | function queue.popFirst(q) 654 | --print("pop", q.first, q.last, q.len) 655 | local first = q.first 656 | if first > q.last then error("queue is empty") end 657 | local value = q[first] 658 | q[first] = nil 659 | q.len = q.len - 1 660 | for i = first, q.last do --find new first index 661 | if q[i] ~= nil then 662 | q.first = i 663 | return value 664 | end 665 | end 666 | q.first = q.last + 1 --empty 667 | return value 668 | end 669 | 670 | function queue.clear(q) 671 | for i = q.first, q.last do 672 | q[i] = nil 673 | end 674 | q.first = 0 675 | q.last = -1 676 | q.len = 0 677 | end 678 | 679 | function queue.peekLast(q) 680 | return q[q.last] 681 | end 682 | 683 | function queue.peekFirst(q) 684 | return q[q.first] 685 | end 686 | 687 | function queue.length(q) 688 | return q.len 689 | end 690 | 691 | if reloading then 692 | VectorTarget:_OnScriptReload() 693 | end --------------------------------------------------------------------------------