diff --git a/.gitignore b/.gitignore
index 99fcae6..f199325 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,6 @@ node_modules
/content/panorama/scripts/custom_game/**/*.js
/game/scripts/vscripts/**/*.lua
-!/game/scripts/vscripts/lib/timers.lua
# Dota 2 stuff
*.bin
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..2b2aae8
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "css.lint.unknownProperties": "ignore"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index bc343d7..621bc75 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
# ModDota template
-A template for Dota 2 Custom Games built with modern technologies. It includes
+A template for Dota 2 Custom Games built with modern technologies.
+
+[This tutorial](https://moddota.com/scripting/Typescript/typescript-introduction/) explains how to set up and use the template.
+
+The template includes:
- [TypeScript for Panorama](https://moddota.com/panorama/introduction-to-panorama-ui-with-typescript)
- [TypeScript for VScripts](https://typescripttolua.github.io/)
@@ -15,6 +19,21 @@ A template for Dota 2 Custom Games built with modern technologies. It includes
After that you can press `Ctrl+Shift+B` in VSCode or run `npm run dev` command in terminal to compile your code and watch for changes.
+## Contents:
+
+* **[src/common]:** TypeScript .d.ts type declaration files with types that can be shared between Panorama and VScripts
+* **[src/vscripts]:** TypeScript code for Dota addon (Lua) vscripts. Compiles lua to game/scripts/vscripts.
+* **[src/panorama]:** TypeScript code for panorama UI. Compiles js to content/panorama/scripts/custom_game
+
+--
+
+* **[game/*]:** Dota game directory containing files such as npc kv files and compiled lua scripts.
+* **[content/*]:** Dota content directory containing panorama sources other than scripts (xml, css, compiled js)
+
+--
+
+* **[scripts/*]:** Repository installation scripts
+
## Continuous Integration
This template includes a [GitHub Actions](https://github.com/features/actions) [workflow](.github/workflows/ci.yml) that builds your custom game on every commit and fails when there are type errors.
diff --git a/content/panorama/images/custom_game/tstl.png b/content/panorama/images/custom_game/tstl.png
new file mode 100644
index 0000000..a60542a
Binary files /dev/null and b/content/panorama/images/custom_game/tstl.png differ
diff --git a/content/panorama/layout/custom_game/hud.xml b/content/panorama/layout/custom_game/hud.xml
index 00a8794..5a128b4 100644
--- a/content/panorama/layout/custom_game/hud.xml
+++ b/content/panorama/layout/custom_game/hud.xml
@@ -1,15 +1,31 @@
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/panorama/scripts/custom_game/hud.ts b/content/panorama/scripts/custom_game/hud.ts
deleted file mode 100644
index 25c34da..0000000
--- a/content/panorama/scripts/custom_game/hud.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-$.Msg("Hud panorama loaded");
-
-GameEvents.Subscribe("my_custom_event", event => {
- $.Msg("Received custom event", event);
-});
-
-GameEvents.SendCustomGameEventToServer<{}>("ui_loaded", {});
diff --git a/content/panorama/scripts/custom_game/manifest.ts b/content/panorama/scripts/custom_game/manifest.ts
deleted file mode 100644
index fd64528..0000000
--- a/content/panorama/scripts/custom_game/manifest.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_ACTION_PANEL, false);
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_ACTION_MINIMAP, false);
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_INVENTORY_PANEL, false);
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_INVENTORY_SHOP, false);
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_FLYOUT_SCOREBOARD, false);
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_TOP_BAR_BACKGROUND, false);
-GameUI.SetDefaultUIEnabled(DotaDefaultUIElement_t.DOTA_DEFAULT_UI_TOP_HEROES, false);
-
-const hudRoot = $.GetContextPanel().GetParent()!.GetParent()!;
-hudRoot.FindChildTraverse("quickstats")!.style.visibility = "collapse";
diff --git a/content/panorama/styles/custom_game/hud.css b/content/panorama/styles/custom_game/hud.css
index 70f6a22..671d30a 100644
--- a/content/panorama/styles/custom_game/hud.css
+++ b/content/panorama/styles/custom_game/hud.css
@@ -2,3 +2,57 @@
width: 100%;
height: 100%;
}
+
+#ExamplePanel {
+ width: 500px;
+ height: 500px;
+ /* Using a background image already in dota */
+ background-image: url("s2r://panorama/images/compendium/international2020/compendium/dashboard_panel_bg_psd.vtex");
+
+ horizontal-align: center;
+ vertical-align: center;
+}
+
+#ExamplePanel #BannerImage {
+ width: 200px;
+ height: 200px;
+ horizontal-align: center;
+ y: 40px;
+}
+
+#ExamplePanel .ExampleTitle {
+ horizontal-align: center;
+ font-size: 30px;
+ y: 260px;
+}
+
+#ExamplePanel .ExampleParagraph {
+ y: 320px;
+ margin-left: 30px;
+ margin-right: 30px;
+}
+
+#ExamplePanel #CloseButton {
+ width: 100px;
+ height: 50px;
+ background-color: rgb(228, 228, 228);
+ horizontal-align: center;
+ vertical-align: bottom;
+ y: -30px;
+}
+
+#ExamplePanel #CloseButton Label {
+ horizontal-align: center;
+ vertical-align: center;
+ color: black;
+}
+
+#ExamplePanel #CloseButton:hover {
+ background-color: rgb(0, 135, 245);
+}
+
+#ExamplePanel #CloseButton:hover Label {
+ color: white;
+}
+
+
diff --git a/game/scripts/vscripts/lib/timers.lua b/game/scripts/vscripts/lib/timers.lua
deleted file mode 100644
index 992e759..0000000
--- a/game/scripts/vscripts/lib/timers.lua
+++ /dev/null
@@ -1,248 +0,0 @@
-TIMERS_VERSION = "1.02"
-
---[[
-
- -- A timer running every second that starts immediately on the next frame, respects pauses
- Timers:CreateTimer(function()
- print ("Hello. I'm running immediately and then every second thereafter.")
- return 1.0
- end
- )
-
- -- A timer running every second that starts 5 seconds in the future, respects pauses
- Timers:CreateTimer(5, function()
- print ("Hello. I'm running 5 seconds after you called me and then every second thereafter.")
- return 1.0
- end
- )
-
- -- 10 second delayed, run once using gametime (respect pauses)
- Timers:CreateTimer({
- endTime = 10, -- when this timer should first execute, you can omit this if you want it to run first on the next frame
- callback = function()
- print ("Hello. I'm running 10 seconds after when I was started.")
- end
- })
-
- -- 10 second delayed, run once regardless of pauses
- Timers:CreateTimer({
- useGameTime = false,
- endTime = 10, -- when this timer should first execute, you can omit this if you want it to run first on the next frame
- callback = function()
- print ("Hello. I'm running 10 seconds after I was started even if someone paused the game.")
- end
- })
-
-
- -- A timer running every second that starts after 2 minutes regardless of pauses
- Timers:CreateTimer("uniqueTimerString3", {
- useGameTime = false,
- endTime = 120,
- callback = function()
- print ("Hello. I'm running after 2 minutes and then every second thereafter.")
- return 1
- end
- })
-
-
- -- A timer using the old style to repeat every second starting 5 seconds ahead
- Timers:CreateTimer("uniqueTimerString3", {
- useOldStyle = true,
- endTime = GameRules:GetGameTime() + 5,
- callback = function()
- print ("Hello. I'm running after 5 seconds and then every second thereafter.")
- return GameRules:GetGameTime() + 1
- end
- })
-
-]]
-
-TIMERS_THINK = 0.01
-
-if Timers == nil then
- print ( '[Timers] creating Timers' )
- Timers = {}
- Timers.__index = Timers
-end
-
-function Timers:new( o )
- o = o or {}
- setmetatable( o, Timers )
- return o
-end
-
-function Timers:_xpcall (f, ...)
- print(f)
- print({...})
- PrintTable({...})
- local result = xpcall (function () return f(unpack(arg)) end,
- function (msg)
- -- build the error message
- return msg..'\n'..debug.traceback()..'\n'
- end)
-
- print(result)
- PrintTable(result)
- if not result[1] then
- -- throw an error
- end
- -- remove status code
- table.remove (result, 1)
- return unpack (result)
-end
-
-function Timers:start()
- Timers = self
- self.timers = {}
-
- local ent = Entities:CreateByClassname("info_target") -- Entities:FindByClassname(nil, 'CWorld')
- ent:SetThink("Think", self, "timers", TIMERS_THINK)
-end
-
-function Timers:Think()
- if GameRules:State_Get() >= DOTA_GAMERULES_STATE_POST_GAME then
- return
- end
-
- -- Track game time, since the dt passed in to think is actually wall-clock time not simulation time.
- local now = GameRules:GetGameTime()
-
- -- Process timers
- for k,v in pairs(Timers.timers) do
- local bUseGameTime = true
- if v.useGameTime ~= nil and v.useGameTime == false then
- bUseGameTime = false
- end
- local bOldStyle = false
- if v.useOldStyle ~= nil and v.useOldStyle == true then
- bOldStyle = true
- end
-
- local now = GameRules:GetGameTime()
- if not bUseGameTime then
- now = Time()
- end
-
- if v.endTime == nil then
- v.endTime = now
- end
- -- Check if the timer has finished
- if now >= v.endTime then
- -- Remove from timers list
- Timers.timers[k] = nil
-
- -- Run the callback
- local status, nextCall
- if v.context then
- status, nextCall = xpcall(function() return v.callback(v.context, v) end, function (msg)
- return msg..'\n'..debug.traceback()..'\n'
- end)
- else
- status, nextCall = xpcall(function() return v.callback(v) end, function (msg)
- return msg..'\n'..debug.traceback()..'\n'
- end)
- end
-
- -- Make sure it worked
- if status then
- -- Check if it needs to loop
- if nextCall then
- -- Change its end time
-
- if bOldStyle then
- v.endTime = v.endTime + nextCall - now
- else
- v.endTime = v.endTime + nextCall
- end
-
- Timers.timers[k] = v
- end
-
- -- Update timer data
- --self:UpdateTimerData()
- else
- -- Nope, handle the error
- Timers:HandleEventError('Timer', k, nextCall)
- end
- end
- end
-
- return TIMERS_THINK
-end
-
-function Timers:HandleEventError(name, event, err)
- print(err)
-
- -- Ensure we have data
- name = tostring(name or 'unknown')
- event = tostring(event or 'unknown')
- err = tostring(err or 'unknown')
-
- -- Tell everyone there was an error
- --Say(nil, name .. ' threw an error on event '..event, false)
- --Say(nil, err, false)
-
- -- Prevent loop arounds
- if not self.errorHandled then
- -- Store that we handled an error
- self.errorHandled = true
- end
-end
-
-function Timers:CreateTimer(name, args, context)
- if type(name) == "function" then
- if args ~= nil then
- context = args
- end
- args = {callback = name}
- name = DoUniqueString("timer")
- elseif type(name) == "table" then
- args = name
- name = DoUniqueString("timer")
- elseif type(name) == "number" then
- args = {endTime = name, callback = args}
- name = DoUniqueString("timer")
- end
- if not args.callback then
- print("Invalid timer created: "..name)
- return
- end
-
-
- local now = GameRules:GetGameTime()
- if args.useGameTime ~= nil and args.useGameTime == false then
- now = Time()
- end
-
- if args.endTime == nil then
- args.endTime = now
- elseif args.useOldStyle == nil or args.useOldStyle == false then
- args.endTime = now + args.endTime
- end
-
- args.context = context
-
- Timers.timers[name] = args
-
- return name
-end
-
-function Timers:RemoveTimer(name)
- Timers.timers[name] = nil
-end
-
-function Timers:RemoveTimers(killAll)
- local timers = {}
-
- if not killAll then
- for k,v in pairs(Timers.timers) do
- if v.persist then
- timers[k] = v
- end
- end
- end
-
- Timers.timers = timers
-end
-
-if not Timers.timers then Timers:start() end
diff --git a/package-lock.json b/package-lock.json
index f9f1ac2..c420951 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,19 +6,19 @@
"": {
"hasInstallScript": true,
"devDependencies": {
- "@moddota/dota-lua-types": "^4.10.0",
- "@moddota/panorama-types": "^1.9.0",
+ "@moddota/dota-lua-types": "^4.11.0",
+ "@moddota/panorama-types": "^1.10.0",
"find-steam-app": "^1.0.2",
"fs-extra": "^9.0.0",
"npm-run-all": "^4.1.5",
"typescript": "^4.2.3",
- "typescript-to-lua": "^0.39.3"
+ "typescript-to-lua": "^0.40.1"
}
},
"node_modules/@moddota/dota-lua-types": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@moddota/dota-lua-types/-/dota-lua-types-4.10.0.tgz",
- "integrity": "sha512-rDBFRKAy3dcYniBtVA3Hru+ZWyBQkqJWBB/KG9BYFbnNiLxbwY/Z5Nj23QXtLkN3kixbehSQibntFSmrAiepeg==",
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/@moddota/dota-lua-types/-/dota-lua-types-4.11.0.tgz",
+ "integrity": "sha512-5hiVqzQHEi9lliJSu+jpqMtx9l6bq1E3lgp+QokLd1rLseRE2TkWPhu162jmIQrUz+7YI+t+i2PpMT9UgZdN4w==",
"dev": true,
"dependencies": {
"lua-types": "^2.8.0",
@@ -35,9 +35,9 @@
"dev": true
},
"node_modules/@moddota/panorama-types": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@moddota/panorama-types/-/panorama-types-1.9.0.tgz",
- "integrity": "sha512-FlNPzXCuojUO/+WiXT30ugFH/4Na3hjAzMNM0ODFsttHWAFDXjbRfSu3S6pPcD0qkzvGk7ESFCpx0dTfLw405w==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@moddota/panorama-types/-/panorama-types-1.10.0.tgz",
+ "integrity": "sha512-0XzWeAVqebR0fEfEHq4MToiqdHH4NRT426qvUEJPQD2T0VGQqeAocO75IbXiA4ehE8oKYUUOHm6RCEPfPT2ivA==",
"dev": true,
"dependencies": {
"tslib": "^2.0.3"
@@ -161,6 +161,19 @@
"once": "^1.4.0"
}
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.8.2",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
+ "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -313,9 +326,9 @@
}
},
"node_modules/graceful-fs": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
- "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
"dev": true
},
"node_modules/has": {
@@ -850,6 +863,15 @@
"node": ">=4"
}
},
+ "node_modules/tapable": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
+ "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
@@ -857,9 +879,9 @@
"dev": true
},
"node_modules/typescript": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
- "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
+ "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -870,14 +892,15 @@
}
},
"node_modules/typescript-to-lua": {
- "version": "0.39.3",
- "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-0.39.3.tgz",
- "integrity": "sha512-a9unziEbOc+sDT6ZeKLQV538WhX+rOaOCVjA/pvcjUs0oIESjEGM0HJnz/B3e1sUk/EtgU+b6bZKBcqKX1q4VQ==",
+ "version": "0.40.1",
+ "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-0.40.1.tgz",
+ "integrity": "sha512-wbnXddF+kPWKDQMcuVZLRUeZsMde5Qni6FnYIc3SKOAhg2/ZAm9ctWQsyQLwH8piMUX+uZzFXaJ3pMe186crfw==",
"dev": true,
"dependencies": {
+ "enhanced-resolve": "^5.8.2",
"resolve": "^1.15.1",
"source-map": "^0.7.3",
- "typescript": ">=4.0.2"
+ "typescript": "~4.3.2"
},
"bin": {
"tstl": "dist/tstl.js"
@@ -944,9 +967,9 @@
},
"dependencies": {
"@moddota/dota-lua-types": {
- "version": "4.10.0",
- "resolved": "https://registry.npmjs.org/@moddota/dota-lua-types/-/dota-lua-types-4.10.0.tgz",
- "integrity": "sha512-rDBFRKAy3dcYniBtVA3Hru+ZWyBQkqJWBB/KG9BYFbnNiLxbwY/Z5Nj23QXtLkN3kixbehSQibntFSmrAiepeg==",
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/@moddota/dota-lua-types/-/dota-lua-types-4.11.0.tgz",
+ "integrity": "sha512-5hiVqzQHEi9lliJSu+jpqMtx9l6bq1E3lgp+QokLd1rLseRE2TkWPhu162jmIQrUz+7YI+t+i2PpMT9UgZdN4w==",
"dev": true,
"requires": {
"lua-types": "^2.8.0",
@@ -962,9 +985,9 @@
}
},
"@moddota/panorama-types": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@moddota/panorama-types/-/panorama-types-1.9.0.tgz",
- "integrity": "sha512-FlNPzXCuojUO/+WiXT30ugFH/4Na3hjAzMNM0ODFsttHWAFDXjbRfSu3S6pPcD0qkzvGk7ESFCpx0dTfLw405w==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@moddota/panorama-types/-/panorama-types-1.10.0.tgz",
+ "integrity": "sha512-0XzWeAVqebR0fEfEHq4MToiqdHH4NRT426qvUEJPQD2T0VGQqeAocO75IbXiA4ehE8oKYUUOHm6RCEPfPT2ivA==",
"dev": true,
"requires": {
"tslib": "^2.0.3"
@@ -1072,6 +1095,16 @@
"once": "^1.4.0"
}
},
+ "enhanced-resolve": {
+ "version": "5.8.2",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz",
+ "integrity": "sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ }
+ },
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -1204,9 +1237,9 @@
}
},
"graceful-fs": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
- "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==",
"dev": true
},
"has": {
@@ -1631,6 +1664,12 @@
"has-flag": "^3.0.0"
}
},
+ "tapable": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz",
+ "integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==",
+ "dev": true
+ },
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
@@ -1638,20 +1677,21 @@
"dev": true
},
"typescript": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
- "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
+ "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"dev": true
},
"typescript-to-lua": {
- "version": "0.39.3",
- "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-0.39.3.tgz",
- "integrity": "sha512-a9unziEbOc+sDT6ZeKLQV538WhX+rOaOCVjA/pvcjUs0oIESjEGM0HJnz/B3e1sUk/EtgU+b6bZKBcqKX1q4VQ==",
+ "version": "0.40.1",
+ "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-0.40.1.tgz",
+ "integrity": "sha512-wbnXddF+kPWKDQMcuVZLRUeZsMde5Qni6FnYIc3SKOAhg2/ZAm9ctWQsyQLwH8piMUX+uZzFXaJ3pMe186crfw==",
"dev": true,
"requires": {
+ "enhanced-resolve": "^5.8.2",
"resolve": "^1.15.1",
"source-map": "^0.7.3",
- "typescript": ">=4.0.2"
+ "typescript": "~4.3.2"
},
"dependencies": {
"resolve": {
diff --git a/package.json b/package.json
index b2f1ec3..afecca0 100644
--- a/package.json
+++ b/package.json
@@ -5,19 +5,19 @@
"postinstall": "node scripts/install.js",
"launch": "node scripts/launch.js",
"build": "run-p build:*",
- "build:panorama": "tsc --project content/panorama/scripts/custom_game/tsconfig.json",
- "build:vscripts": "tstl --project game/scripts/vscripts/tsconfig.json",
+ "build:panorama": "tsc --project src/panorama/tsconfig.json",
+ "build:vscripts": "tstl --project src/vscripts/tsconfig.json",
"dev": "run-p dev:*",
- "dev:panorama": "tsc --project content/panorama/scripts/custom_game/tsconfig.json --watch",
- "dev:vscripts": "tstl --project game/scripts/vscripts/tsconfig.json --watch"
+ "dev:panorama": "tsc --project src/panorama/tsconfig.json --watch",
+ "dev:vscripts": "tstl --project src/vscripts/tsconfig.json --watch"
},
"devDependencies": {
- "@moddota/dota-lua-types": "^4.10.0",
- "@moddota/panorama-types": "^1.9.0",
+ "@moddota/dota-lua-types": "^4.11.0",
+ "@moddota/panorama-types": "^1.10.0",
"find-steam-app": "^1.0.2",
"fs-extra": "^9.0.0",
"npm-run-all": "^4.1.5",
"typescript": "^4.2.3",
- "typescript-to-lua": "^0.39.3"
+ "typescript-to-lua": "^0.40.1"
}
}
diff --git a/src/common/events.d.ts b/src/common/events.d.ts
new file mode 100644
index 0000000..2ec3a82
--- /dev/null
+++ b/src/common/events.d.ts
@@ -0,0 +1,29 @@
+/**
+ * This file contains types for the events you want to send between the UI (Panorama)
+ * and the server (VScripts).
+ *
+ * IMPORTANT:
+ *
+ * The dota engine will change the type of event data slightly when it is sent, so on the
+ * Panorama side your event handlers will have to handle NetworkedData, changes are:
+ * - Booleans are turned to 0 | 1
+ * - Arrays are automatically translated to objects when sending them as event. You have
+ * to change them back into arrays yourself! See 'toArray()' in src/panorama/hud.ts
+ */
+
+// To declare an event for use, add it to this table with the type of its data
+interface CustomGameEventDeclarations {
+ example_event: ExampleEventData,
+ ui_panel_closed: UIPanelClosedEventData
+}
+
+// Define the type of data sent by the example_event event
+interface ExampleEventData {
+ myNumber: number;
+ myBoolean: boolean;
+ myString: string;
+ myArrayOfNumbers: number[]
+}
+
+// This event has no data
+interface UIPanelClosedEventData {}
\ No newline at end of file
diff --git a/src/common/general.d.ts b/src/common/general.d.ts
new file mode 100644
index 0000000..d90bc84
--- /dev/null
+++ b/src/common/general.d.ts
@@ -0,0 +1,15 @@
+/**
+ * This file contains some general types related to your game that can be shared between
+ * front-end (Panorama) and back-end (VScripts). Only put stuff in here you need to share.
+ */
+
+interface Color {
+ r: number,
+ g: number,
+ b: number
+}
+
+interface UnitData {
+ name: string,
+ level: number
+}
\ No newline at end of file
diff --git a/src/panorama/hud.ts b/src/panorama/hud.ts
new file mode 100644
index 0000000..b5eb3f5
--- /dev/null
+++ b/src/panorama/hud.ts
@@ -0,0 +1,45 @@
+$.Msg("Hud panorama loaded");
+
+function OnCloseButtonClicked() {
+ $.Msg("Example close button clicked");
+
+ // Find panel by id
+ const examplePanel = $("#ExamplePanel");
+
+ // Remove panel
+ examplePanel.DeleteAsync(0);
+
+ // Send event to server
+ GameEvents.SendCustomGameEventToServer("ui_panel_closed", {});
+}
+
+GameEvents.Subscribe("example_event", (data: NetworkedData) => {
+ const myNumber = data.myNumber;
+ const myString = data.myString;
+
+ const myBoolean = data.myBoolean; // After sending to client this is now type 0 | 1!
+
+ const myArrayObject = data.myArrayOfNumbers; // After sending this is now an object!
+
+ const myArray = toArray(myArrayObject); // We can turn it back into an array ourselves.
+
+ $.Msg("Received example event", myNumber, myString, myBoolean, myArrayObject, myArray);
+
+});
+
+/**
+ * Turn a table object into an array.
+ * @param obj The object to transform to an array.
+ * @returns An array with items of the value type of the original object.
+ */
+function toArray(obj: Record): T[] {
+ const result = [];
+
+ let key = 1;
+ while (obj[key]) {
+ result.push(obj[key]);
+ key++;
+ }
+
+ return result;
+}
diff --git a/src/panorama/manifest.ts b/src/panorama/manifest.ts
new file mode 100644
index 0000000..99113d3
--- /dev/null
+++ b/src/panorama/manifest.ts
@@ -0,0 +1 @@
+$.Msg("ui manifest loaded");
\ No newline at end of file
diff --git a/content/panorama/scripts/custom_game/tsconfig.json b/src/panorama/tsconfig.json
similarity index 64%
rename from content/panorama/scripts/custom_game/tsconfig.json
rename to src/panorama/tsconfig.json
index 4d9983d..de749b7 100644
--- a/content/panorama/scripts/custom_game/tsconfig.json
+++ b/src/panorama/tsconfig.json
@@ -1,10 +1,12 @@
{
"compilerOptions": {
"rootDir": ".",
+ "outDir": "../../content/panorama/scripts/custom_game",
"target": "es2017",
"lib": ["es2017"],
"types": ["@moddota/panorama-types"],
"moduleResolution": "node",
"strict": true
- }
+ },
+ "include": ["**/*.ts", "../common/**/*.ts"]
}
diff --git a/game/scripts/vscripts/GameMode.ts b/src/vscripts/GameMode.ts
similarity index 55%
rename from game/scripts/vscripts/GameMode.ts
rename to src/vscripts/GameMode.ts
index efa855e..ceff960 100644
--- a/game/scripts/vscripts/GameMode.ts
+++ b/src/vscripts/GameMode.ts
@@ -1,7 +1,7 @@
import { reloadable } from "./lib/tstl-utils";
-import "./modifiers/modifier_panic";
+import { modifier_panic } from "./modifiers/modifier_panic";
-const heroSelectionTime = 10;
+const heroSelectionTime = 20;
declare global {
interface CDOTAGamerules {
@@ -17,18 +17,39 @@ export class GameMode {
}
public static Activate(this: void) {
+ // When the addon activates, create a new instance of this GameMode class.
GameRules.Addon = new GameMode();
}
constructor() {
this.configure();
+
+ // Register event listeners for dota engine events
ListenToGameEvent("game_rules_state_change", () => this.OnStateChange(), undefined);
ListenToGameEvent("npc_spawned", event => this.OnNpcSpawned(event), undefined);
+
+ // Register event listeners for events from the UI
+ CustomGameEventManager.RegisterListener("ui_panel_closed", (_, data) => {
+ print(`Player ${data.PlayerID} has closed their UI panel.`);
+
+ // Respond by sending back an example event
+ const player = PlayerResource.GetPlayer(data.PlayerID)!;
+ CustomGameEventManager.Send_ServerToPlayer(player, "example_event", {
+ myNumber: 42,
+ myBoolean: true,
+ myString: "Hello!",
+ myArrayOfNumbers: [1.414, 2.718, 3.142]
+ });
+
+ // Also apply the panic modifier to the sending player's hero
+ const hero = player.GetAssignedHero();
+ hero.AddNewModifier(hero, undefined, modifier_panic.name, { duration: 5 });
+ });
}
private configure(): void {
- GameRules.SetCustomGameTeamMaxPlayers(DOTATeam_t.DOTA_TEAM_GOODGUYS, 3);
- GameRules.SetCustomGameTeamMaxPlayers(DOTATeam_t.DOTA_TEAM_BADGUYS, 3);
+ GameRules.SetCustomGameTeamMaxPlayers(DotaTeam.GOODGUYS, 3);
+ GameRules.SetCustomGameTeamMaxPlayers(DotaTeam.BADGUYS, 3);
GameRules.SetShowcaseTime(0);
GameRules.SetHeroSelectionTime(heroSelectionTime);
@@ -38,14 +59,23 @@ export class GameMode {
const state = GameRules.State_Get();
// Add 4 bots to lobby in tools
- if (IsInToolsMode() && state == DOTA_GameState.DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP) {
+ if (IsInToolsMode() && state == GameState.CUSTOM_GAME_SETUP) {
for (let i = 0; i < 4; i++) {
Tutorial.AddBot("npc_dota_hero_lina", "", "", false);
}
}
+ if (state === GameState.CUSTOM_GAME_SETUP) {
+ // Automatically skip setup in tools
+ if (IsInToolsMode()) {
+ Timers.CreateTimer(3, () => {
+ GameRules.FinishCustomGameSetup();
+ });
+ }
+ }
+
// Start game once pregame hits
- if (state == DOTA_GameState.DOTA_GAMERULES_STATE_PRE_GAME) {
+ if (state === GameState.PRE_GAME) {
Timers.CreateTimer(0.2, () => this.StartGame());
}
}
@@ -66,11 +96,8 @@ export class GameMode {
private OnNpcSpawned(event: NpcSpawnedEvent) {
// After a hero unit spawns, apply modifier_panic for 8 seconds
const unit = EntIndexToHScript(event.entindex) as CDOTA_BaseNPC; // Cast to npc since this is the 'npc_spawned' event
+ // Give all real heroes (not illusions) the meepo_earthbind_ts_example spell
if (unit.IsRealHero()) {
- Timers.CreateTimer(1, () => {
- unit.AddNewModifier(unit, undefined, "modifier_panic", { duration: 8 });
- });
-
if (!unit.HasAbility("meepo_earthbind_ts_example")) {
// Add lua ability to the unit
unit.AddAbility("meepo_earthbind_ts_example");
diff --git a/game/scripts/vscripts/abilities/heroes/meepo/earthbind_ts_example.ts b/src/vscripts/abilities/heroes/meepo/earthbind_ts_example.ts
similarity index 84%
rename from game/scripts/vscripts/abilities/heroes/meepo/earthbind_ts_example.ts
rename to src/vscripts/abilities/heroes/meepo/earthbind_ts_example.ts
index 6b9b7c9..1aa6cc2 100644
--- a/game/scripts/vscripts/abilities/heroes/meepo/earthbind_ts_example.ts
+++ b/src/vscripts/abilities/heroes/meepo/earthbind_ts_example.ts
@@ -40,7 +40,7 @@ export class meepo_earthbind_ts_example extends BaseAbility {
const radius = this.GetSpecialValueFor("radius");
this.particle = ParticleManager.CreateParticle(
"particles/units/heroes/hero_meepo/meepo_earthbind_projectile_fx.vpcf",
- ParticleAttachment_t.PATTACH_CUSTOMORIGIN,
+ ParticleAttachment.CUSTOMORIGIN,
caster,
);
@@ -57,9 +57,9 @@ export class meepo_earthbind_ts_example extends BaseAbility {
fEndRadius: radius,
Source: caster,
bHasFrontalCone: false,
- iUnitTargetTeam: DOTA_UNIT_TARGET_TEAM.DOTA_UNIT_TARGET_TEAM_NONE,
- iUnitTargetFlags: DOTA_UNIT_TARGET_FLAGS.DOTA_UNIT_TARGET_FLAG_NONE,
- iUnitTargetType: DOTA_UNIT_TARGET_TYPE.DOTA_UNIT_TARGET_NONE,
+ iUnitTargetTeam: UnitTargetTeam.NONE,
+ iUnitTargetFlags: UnitTargetFlags.NONE,
+ iUnitTargetType: UnitTargetType.NONE,
vVelocity: (direction * projectileSpeed) as Vector,
bProvidesVision: true,
iVisionRadius: radius,
@@ -77,9 +77,9 @@ export class meepo_earthbind_ts_example extends BaseAbility {
location,
undefined,
radius,
- DOTA_UNIT_TARGET_TEAM.DOTA_UNIT_TARGET_TEAM_ENEMY,
- DOTA_UNIT_TARGET_TYPE.DOTA_UNIT_TARGET_BASIC | DOTA_UNIT_TARGET_TYPE.DOTA_UNIT_TARGET_HERO,
- DOTA_UNIT_TARGET_FLAGS.DOTA_UNIT_TARGET_FLAG_NONE,
+ UnitTargetTeam.ENEMY,
+ UnitTargetType.BASIC | UnitTargetType.HERO,
+ UnitTargetFlags.NONE,
0,
false,
);
diff --git a/game/scripts/vscripts/addon_game_mode.ts b/src/vscripts/addon_game_mode.ts
similarity index 61%
rename from game/scripts/vscripts/addon_game_mode.ts
rename to src/vscripts/addon_game_mode.ts
index 7665d59..ed777fc 100644
--- a/game/scripts/vscripts/addon_game_mode.ts
+++ b/src/vscripts/addon_game_mode.ts
@@ -1,11 +1,13 @@
import "./lib/timers";
import { GameMode } from "./GameMode";
+// Connect GameMode.Activate and GameMode.Precache to the dota engine
Object.assign(getfenv(), {
Activate: GameMode.Activate,
Precache: GameMode.Precache,
});
if (GameRules.Addon) {
+ // This code is only run after script_reload, not at startup
GameRules.Addon.Reload();
}
diff --git a/game/scripts/vscripts/lib/dota_ts_adapter.ts b/src/vscripts/lib/dota_ts_adapter.ts
similarity index 87%
rename from game/scripts/vscripts/lib/dota_ts_adapter.ts
rename to src/vscripts/lib/dota_ts_adapter.ts
index b0d7c9a..30f9b23 100644
--- a/game/scripts/vscripts/lib/dota_ts_adapter.ts
+++ b/src/vscripts/lib/dota_ts_adapter.ts
@@ -85,17 +85,17 @@ export const registerModifier = (name?: string) => (modifier: new () => CDOTA_Mo
}
};
- let type = LuaModifierType.LUA_MODIFIER_MOTION_NONE;
+ let type = LuaModifierMotionType.NONE;
let base = (modifier as any).____super;
while (base) {
if (base === BaseModifierMotionBoth) {
- type = LuaModifierType.LUA_MODIFIER_MOTION_BOTH;
+ type = LuaModifierMotionType.BOTH;
break;
} else if (base === BaseModifierMotionHorizontal) {
- type = LuaModifierType.LUA_MODIFIER_MOTION_HORIZONTAL;
+ type = LuaModifierMotionType.HORIZONTAL;
break;
} else if (base === BaseModifierMotionVertical) {
- type = LuaModifierType.LUA_MODIFIER_MOTION_VERTICAL;
+ type = LuaModifierMotionType.VERTICAL;
break;
}
@@ -105,6 +105,17 @@ export const registerModifier = (name?: string) => (modifier: new () => CDOTA_Mo
LinkLuaModifier(name, fileName, type);
};
+/**
+ * Use to expose top-level functions in entity scripts.
+ * Usage: registerEntityFunction("OnStartTouch", (trigger: TriggerStartTouchEvent) => { });
+ */
+export function registerEntityFunction(name: string, f: (...args: any[]) => any) {
+ const [env] = getFileScope();
+ env[name] = function (this: void, ...args: any[]) {
+ f(...args);
+ };
+}
+
function clearTable(table: object) {
for (const key in table) {
delete (table as any)[key];
diff --git a/game/scripts/vscripts/lib/timers.d.ts b/src/vscripts/lib/timers.d.ts
similarity index 100%
rename from game/scripts/vscripts/lib/timers.d.ts
rename to src/vscripts/lib/timers.d.ts
diff --git a/src/vscripts/lib/timers.lua b/src/vscripts/lib/timers.lua
new file mode 100644
index 0000000..14caf92
--- /dev/null
+++ b/src/vscripts/lib/timers.lua
@@ -0,0 +1,293 @@
+TIMERS_VERSION = "1.06"
+
+--[[
+
+ 1.06 modified by Celireor (now uses binary heap priority queue instead of iteration to determine timer of shortest duration)
+
+ DO NOT MODIFY A REALTIME TIMER TO USE GAMETIME OR VICE VERSA MIDWAY WITHOUT FIRST REMOVING AND RE-ADDING THE TIMER
+
+ -- A timer running every second that starts immediately on the next frame, respects pauses
+ Timers:CreateTimer(function()
+ print ("Hello. I'm running immediately and then every second thereafter.")
+ return 1.0
+ end
+ )
+
+ -- The same timer as above with a shorthand call
+ Timers(function()
+ print ("Hello. I'm running immediately and then every second thereafter.")
+ return 1.0
+ end)
+
+
+ -- A timer which calls a function with a table context
+ Timers:CreateTimer(GameMode.someFunction, GameMode)
+
+ -- A timer running every second that starts 5 seconds in the future, respects pauses
+ Timers:CreateTimer(5, function()
+ print ("Hello. I'm running 5 seconds after you called me and then every second thereafter.")
+ return 1.0
+ end
+ )
+
+ -- 10 second delayed, run once using gametime (respect pauses)
+ Timers:CreateTimer({
+ endTime = 10, -- when this timer should first execute, you can omit this if you want it to run first on the next frame
+ callback = function()
+ print ("Hello. I'm running 10 seconds after when I was started.")
+ end
+ })
+
+ -- 10 second delayed, run once regardless of pauses
+ Timers:CreateTimer({
+ useGameTime = false,
+ endTime = 10, -- when this timer should first execute, you can omit this if you want it to run first on the next frame
+ callback = function()
+ print ("Hello. I'm running 10 seconds after I was started even if someone paused the game.")
+ end
+ })
+
+
+ -- A timer running every second that starts after 2 minutes regardless of pauses
+ Timers:CreateTimer("uniqueTimerString3", {
+ useGameTime = false,
+ endTime = 120,
+ callback = function()
+ print ("Hello. I'm running after 2 minutes and then every second thereafter.")
+ return 1
+ end
+ })
+
+]]
+
+-- Binary Heap implementation copy-pasted from https://gist.github.com/starwing/1757443a1bd295653c39
+-- BinaryHeap[1] always points to the element with the lowest "key" variable
+-- API
+-- BinaryHeap(key) - Creates a new BinaryHeap with key. The key is the name of the integer variable used to sort objects.
+-- BinaryHeap:Insert - Inserts an object into BinaryHeap
+-- BinaryHeap:Remove - Removes an object from BinaryHeap
+
+BinaryHeap = BinaryHeap or {}
+BinaryHeap.__index = BinaryHeap
+
+function BinaryHeap:Insert(item)
+ local index = #self + 1
+ local key = self.key
+ item.index = index
+ self[index] = item
+ while index > 1 do
+ local parent = math.floor(index/2)
+ if self[parent][key] <= item[key] then
+ break
+ end
+ self[index], self[parent] = self[parent], self[index]
+ self[index].index = index
+ self[parent].index = parent
+ index = parent
+ end
+ return item
+end
+
+function BinaryHeap:Remove(item)
+ local index = item.index
+ if self[index] ~= item then return end
+ local key = self.key
+ local heap_size = #self
+ if index == heap_size then
+ self[heap_size] = nil
+ return
+ end
+ self[index] = self[heap_size]
+ self[index].index = index
+ self[heap_size] = nil
+ while true do
+ local left = index*2
+ local right = left + 1
+ if not self[left] then break end
+ local newindex = right
+ if self[index][key] >= self[left][key] then
+ if not self[right] or self[left][key] < self[right][key] then
+ newindex = left
+ end
+ elseif not self[right] or self[index][key] <= self[right][key] then
+ break
+ end
+ self[index], self[newindex] = self[newindex], self[index]
+ self[index].index = index
+ self[newindex].index = newindex
+ index = newindex
+ end
+end
+
+setmetatable(BinaryHeap, {__call = function(self, key) return setmetatable({key=key}, self) end})
+
+function table.merge(input1, input2)
+ for i,v in pairs(input2) do
+ input1[i] = v
+ end
+ return input1
+end
+
+
+TIMERS_THINK = 0.01
+
+if Timers == nil then
+ print ( '[Timers] creating Timers' )
+ Timers = {}
+ setmetatable(Timers, {
+ __call = function(t, ...)
+ return t:CreateTimer(...)
+ end
+ })
+ --Timers.__index = Timers
+end
+
+function Timers:start()
+ self.started = true
+ Timers = self
+ self:InitializeTimers()
+ self.nextTickCallbacks = {}
+
+ local ent = SpawnEntityFromTableSynchronous("info_target", {targetname="timers_lua_thinker"})
+ ent:SetThink("Think", self, "timers", TIMERS_THINK)
+end
+
+function Timers:Think()
+ local nextTickCallbacks = table.merge({}, Timers.nextTickCallbacks)
+ Timers.nextTickCallbacks = {}
+ for _, cb in ipairs(nextTickCallbacks) do
+ local status, result = xpcall(cb, debug.traceback)
+ if not status then
+ Timers:HandleEventError(result)
+ end
+ end
+ if GameRules:State_Get() >= DOTA_GAMERULES_STATE_POST_GAME then
+ return
+ end
+
+ -- Track game time, since the dt passed in to think is actually wall-clock time not simulation time.
+ local now = GameRules:GetGameTime()
+
+ -- Process timers
+ self:ExecuteTimers(self.realTimeHeap, Time())
+ self:ExecuteTimers(self.gameTimeHeap, GameRules:GetGameTime())
+
+ return TIMERS_THINK
+end
+
+function Timers:ExecuteTimers(timerList, now)
+ --Empty timer, ignore
+ if not timerList[1] then return end
+
+ --Timers are alr. sorted by end time upon insertion
+ local currentTimer = timerList[1]
+
+ currentTimer.endTime = currentTimer.endTime or now
+ --Check if timer has finished
+ if now >= currentTimer.endTime then
+ -- Remove from timers list
+ timerList:Remove(currentTimer)
+ Timers.runningTimer = k
+ Timers.removeSelf = false
+
+ -- Run the callback
+ local status, timerResult
+ if currentTimer.context then
+ status, timerResult = xpcall(function() return currentTimer.callback(currentTimer.context, currentTimer) end, debug.traceback)
+ else
+ status, timerResult = xpcall(function() return currentTimer.callback(currentTimer) end, debug.traceback)
+ end
+
+ Timers.runningTimer = nil
+
+ -- Make sure it worked
+ if status then
+ -- Check if it needs to loop
+ if timerResult and not Timers.removeSelf then
+ -- Change its end time
+
+ currentTimer.endTime = currentTimer.endTime + timerResult
+
+ timerList:Insert(currentTimer)
+ end
+
+ -- Update timer data
+ --self:UpdateTimerData()
+ else
+ -- Nope, handle the error
+ Timers:HandleEventError(timerResult)
+ end
+ --run again!
+ self:ExecuteTimers(timerList, now)
+ end
+end
+
+function Timers:HandleEventError(err)
+ if IsInToolsMode() then
+ print(err)
+ else
+ StatsClient:HandleError(err)
+ end
+end
+
+function Timers:CreateTimer(arg1, arg2, context)
+ local timer
+ if type(arg1) == "function" then
+ if arg2 ~= nil then
+ context = arg2
+ end
+ timer = {callback = arg1}
+ elseif type(arg1) == "table" then
+ timer = arg1
+ elseif type(arg1) == "number" then
+ timer = {endTime = arg1, callback = arg2}
+ end
+ if not timer.callback then
+ print("Invalid timer created")
+ return
+ end
+
+ local now = GameRules:GetGameTime()
+ local timerHeap = self.gameTimeHeap
+ if timer.useGameTime ~= nil and timer.useGameTime == false then
+ now = Time()
+ timerHeap = self.realTimeHeap
+ end
+
+ if timer.endTime == nil then
+ timer.endTime = now
+ else
+ timer.endTime = now + timer.endTime
+ end
+
+ timer.context = context
+
+ timerHeap:Insert(timer)
+
+ return timer
+end
+
+function Timers:NextTick(callback)
+ table.insert(Timers.nextTickCallbacks, callback)
+end
+
+function Timers:RemoveTimer(name)
+ local timerHeap = self.gameTimeHeap
+ if name.useGameTime ~= nil and name.useGameTime == false then
+ timerHeap = self.realTimeHeap
+ end
+
+ timerHeap:Remove(name)
+ if Timers.runningTimer == name then
+ Timers.removeSelf = true
+ end
+end
+
+function Timers:InitializeTimers()
+ self.realTimeHeap = BinaryHeap("endTime")
+ self.gameTimeHeap = BinaryHeap("endTime")
+end
+
+if not Timers.started then Timers:start() end
+
+GameRules.Timers = Timers
\ No newline at end of file
diff --git a/game/scripts/vscripts/lib/tstl-utils.ts b/src/vscripts/lib/tstl-utils.ts
similarity index 100%
rename from game/scripts/vscripts/lib/tstl-utils.ts
rename to src/vscripts/lib/tstl-utils.ts
diff --git a/game/scripts/vscripts/modifiers/modifier_panic.ts b/src/vscripts/modifiers/modifier_panic.ts
similarity index 84%
rename from game/scripts/vscripts/modifiers/modifier_panic.ts
rename to src/vscripts/modifiers/modifier_panic.ts
index 50845a2..67461f8 100644
--- a/game/scripts/vscripts/modifiers/modifier_panic.ts
+++ b/src/vscripts/modifiers/modifier_panic.ts
@@ -3,8 +3,8 @@ import { BaseModifier, registerModifier } from "../lib/dota_ts_adapter";
// Base speed modifier -- Could be moved to a separate file
class ModifierSpeed extends BaseModifier {
// Declare functions
- DeclareFunctions(): modifierfunction[] {
- return [modifierfunction.MODIFIER_PROPERTY_MOVESPEED_ABSOLUTE];
+ DeclareFunctions(): ModifierFunction[] {
+ return [ModifierFunction.MOVESPEED_ABSOLUTE];
}
GetModifierMoveSpeed_Absolute(): number {
@@ -17,7 +17,7 @@ export class modifier_panic extends ModifierSpeed {
// Set state
CheckState(): Partial> {
return {
- [modifierstate.MODIFIER_STATE_COMMAND_RESTRICTED]: true,
+ [ModifierState.COMMAND_RESTRICTED]: true,
};
}
diff --git a/game/scripts/vscripts/tsconfig.json b/src/vscripts/tsconfig.json
similarity index 69%
rename from game/scripts/vscripts/tsconfig.json
rename to src/vscripts/tsconfig.json
index 863ea2b..11f40e1 100644
--- a/game/scripts/vscripts/tsconfig.json
+++ b/src/vscripts/tsconfig.json
@@ -1,9 +1,10 @@
{
"compilerOptions": {
"rootDir": ".",
+ "outDir": "../../game/scripts/vscripts",
"target": "esnext",
"lib": ["esnext"],
- "types": ["@moddota/dota-lua-types"],
+ "types": ["@moddota/dota-lua-types/normalized"],
"plugins": [{ "transform": "@moddota/dota-lua-types/transformer" }],
"moduleResolution": "node",
"experimentalDecorators": true,
@@ -12,5 +13,6 @@
"tstl": {
"luaTarget": "JIT",
"sourceMapTraceback": true
- }
+ },
+ "include": ["**/*.ts", "../common/**/*.ts"]
}