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"] }