diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44539da --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Project place file +/roblox-dialogue-maker.rbxlx + +# Roblox Studio lock files +/*.rbxlx.lock +/*.rbxl.lock + +# Note to developers: Use Wally to locally install packages. +# Do not include Packages in commits. +Packages \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ef42f34 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "luau-lsp.sourcemap.rojoProjectFile": "client.project.json" +} \ No newline at end of file diff --git a/README.md b/README.md index a1ac908..748907a 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ You can either get [the version Beastslash updates at the Roblox Library](https: ## How do I use it? Check out [How to use the Dialogue Maker](https://github.com/beastslash/roblox-dialogue-maker/wiki/How-to-use-the-Dialogue-Maker) on the wiki! +## Development +Use `client.project.json` to develop with only the DialogueClientScript. Use `plugin.project.json` to develop both. + ## Can I contribute? Sure! If you feel like that the Dialogue Maker can be improved for everyone, just send a feature request in the issues. You could also submit a pull request if you already added it yourself. Beastslash will sync changes made between the plugin and repository. diff --git a/aftman.toml b/aftman.toml new file mode 100644 index 0000000..edbe618 --- /dev/null +++ b/aftman.toml @@ -0,0 +1,8 @@ +# This file lists tools managed by Aftman, a cross-platform toolchain manager. +# For more information, see https://github.com/LPGhatguy/aftman + +# To add a new tool, add an entry to this table. +[tools] +rojo = "rojo-rbx/rojo@7.4.4" +wally = "UpliftGames/wally@0.3.2" +# rojo = "rojo-rbx/rojo@6.2.0" \ No newline at end of file diff --git a/client.project.json b/client.project.json new file mode 100644 index 0000000..e5565fb --- /dev/null +++ b/client.project.json @@ -0,0 +1,19 @@ +{ + "name": "Dialogue Maker", + "tree": { + "$className": "DataModel", + "StarterPlayer": { + "StarterPlayerScripts": { + "DialogueClientScript": { + "$properties": { + "Disabled": false + }, + "$path": "src/DialogueClientScript", + "Packages": { + "$path": "Packages" + } + } + } + } + } +} \ No newline at end of file diff --git a/plugin.project.json b/plugin.project.json new file mode 100644 index 0000000..6c29776 --- /dev/null +++ b/plugin.project.json @@ -0,0 +1,20 @@ +{ + "name": "Dialogue Maker", + "tree": { + "$className": "DataModel", + "ServerStorage": { + "DialoguePluginScript": { + "$path": "src/DialoguePluginScript", + "Packages": { + "$path": "Packages" + }, + "DialogueClientScript": { + "$path": "src/DialogueClientScript", + "Packages": { + "$path": "Packages" + } + } + } + } + } +} \ No newline at end of file diff --git a/sourcemap.json b/sourcemap.json new file mode 100644 index 0000000..9bbf174 --- /dev/null +++ b/sourcemap.json @@ -0,0 +1 @@ +{"name":"Dialogue Maker","className":"DataModel","filePaths":["client.project.json"],"children":[{"name":"StarterPlayer","className":"StarterPlayer","children":[{"name":"StarterPlayerScripts","className":"StarterPlayerScripts","children":[{"name":"DialogueClientScript","className":"LocalScript","filePaths":["src/DialogueClientScript\\init.meta.json","src/DialogueClientScript\\init.client.lua"],"children":[{"name":"API","className":"Folder","children":[{"name":"Dialogue","className":"ModuleScript","filePaths":["src/DialogueClientScript\\API\\Dialogue.lua"]},{"name":"Player","className":"ModuleScript","filePaths":["src/DialogueClientScript\\API\\Player.lua"]},{"name":"Triggers","className":"ModuleScript","filePaths":["src/DialogueClientScript\\API\\Triggers.lua"]}]},{"name":"ReactComponents","className":"Folder","children":[{"name":"Themes","className":"Folder","children":[{"name":"BareBonesTheme","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactComponents\\Themes\\BareBonesTheme\\init.lua"],"children":[{"name":"MessageComponentList","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactComponents\\Themes\\BareBonesTheme\\MessageComponentList.lua"]},{"name":"ResponseButton","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactComponents\\Themes\\BareBonesTheme\\ResponseButton.lua"]},{"name":"ResponseComponentList","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactComponents\\Themes\\BareBonesTheme\\ResponseComponentList.lua"]},{"name":"TextSegment","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactComponents\\Themes\\BareBonesTheme\\TextSegment.lua"]}]},{"name":"BigAndBoldTheme","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactComponents\\Themes\\BigAndBoldTheme.lua"]}]}]},{"name":"ReactHooks","className":"Folder","children":[{"name":"useContinueDialogue","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactHooks\\useContinueDialogue.lua"]},{"name":"useDynamicSize","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactHooks\\useDynamicSize.lua"]},{"name":"useKeybindContinue","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactHooks\\useKeybindContinue.lua"]},{"name":"useLookAtPlayer","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactHooks\\useLookAtPlayer.lua"]},{"name":"useOutOfDistanceDetection","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactHooks\\useOutOfDistanceDetection.lua"]},{"name":"usePages","className":"ModuleScript","filePaths":["src/DialogueClientScript\\ReactHooks\\usePages.lua"]}]},{"name":"Settings","className":"ModuleScript","filePaths":["src/DialogueClientScript\\Settings.lua"]},{"name":"Types","className":"ModuleScript","filePaths":["src/DialogueClientScript\\Types.lua"]},{"name":"Packages","className":"Folder","children":[{"name":"_Index","className":"Folder","children":[{"name":"jsdotlua_boolean@1.2.7","className":"Folder","children":[{"name":"boolean","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_boolean@1.2.7\\boolean\\src\\init.lua","Packages\\_Index\\jsdotlua_boolean@1.2.7\\boolean\\default.project.json"],"children":[{"name":"toJSBoolean","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_boolean@1.2.7\\boolean\\src\\toJSBoolean.lua"]}]},{"name":"number","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_boolean@1.2.7\\number.lua"]}]},{"name":"jsdotlua_collections@1.2.7","className":"Folder","children":[{"name":"collections","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\init.lua","Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\default.project.json"],"children":[{"name":"Array","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\init.lua"],"children":[{"name":"concat","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\concat.lua"]},{"name":"every","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\every.lua"]},{"name":"filter","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\filter.lua"]},{"name":"find","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\find.lua"]},{"name":"findIndex","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\findIndex.lua"]},{"name":"flat","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\flat.lua"]},{"name":"flatMap","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\flatMap.lua"]},{"name":"forEach","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\forEach.lua"]},{"name":"from","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\from\\init.lua"],"children":[{"name":"fromArray","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\from\\fromArray.lua"]},{"name":"fromMap","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\from\\fromMap.lua"]},{"name":"fromSet","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\from\\fromSet.lua"]},{"name":"fromString","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\from\\fromString.lua"]}]},{"name":"includes","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\includes.lua"]},{"name":"indexOf","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\indexOf.lua"]},{"name":"isArray","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\isArray.lua"]},{"name":"join","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\join.lua"]},{"name":"map","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\map.lua"]},{"name":"reduce","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\reduce.lua"]},{"name":"reverse","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\reverse.lua"]},{"name":"shift","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\shift.lua"]},{"name":"slice","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\slice.lua"]},{"name":"some","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\some.lua"]},{"name":"sort","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\sort.lua"]},{"name":"splice","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\splice.lua"]},{"name":"unshift","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Array\\unshift.lua"]}]},{"name":"Map","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Map\\init.lua"],"children":[{"name":"Map","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Map\\Map.lua"]},{"name":"coerceToMap","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Map\\coerceToMap.lua"]},{"name":"coerceToTable","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Map\\coerceToTable.lua"]}]},{"name":"Object","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\init.lua"],"children":[{"name":"None","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\None.lua"]},{"name":"assign","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\assign.lua"]},{"name":"entries","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\entries.lua"]},{"name":"freeze","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\freeze.lua"]},{"name":"is","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\is.lua"]},{"name":"isFrozen","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\isFrozen.lua"]},{"name":"keys","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\keys.lua"]},{"name":"preventExtensions","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\preventExtensions.lua"]},{"name":"seal","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\seal.lua"]},{"name":"values","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Object\\values.lua"]}]},{"name":"Set","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\Set\\init.lua"]},{"name":"WeakMap","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\WeakMap.lua"]},{"name":"inspect","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\collections\\src\\inspect.lua"]}]},{"name":"es7-types","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\es7-types.lua"]},{"name":"instance-of","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_collections@1.2.7\\instance-of.lua"]}]},{"name":"jsdotlua_console@1.2.7","className":"Folder","children":[{"name":"collections","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_console@1.2.7\\collections.lua"]},{"name":"console","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_console@1.2.7\\console\\src\\init.lua","Packages\\_Index\\jsdotlua_console@1.2.7\\console\\default.project.json"],"children":[{"name":"makeConsoleImpl","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_console@1.2.7\\console\\src\\makeConsoleImpl.lua"]}]}]},{"name":"jsdotlua_es7-types@1.2.7","className":"Folder","children":[{"name":"es7-types","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_es7-types@1.2.7\\es7-types\\src\\init.lua","Packages\\_Index\\jsdotlua_es7-types@1.2.7\\es7-types\\default.project.json"]}]},{"name":"jsdotlua_instance-of@1.2.7","className":"Folder","children":[{"name":"instance-of","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_instance-of@1.2.7\\instance-of\\src\\init.lua","Packages\\_Index\\jsdotlua_instance-of@1.2.7\\instance-of\\default.project.json"],"children":[{"name":"instanceof","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_instance-of@1.2.7\\instance-of\\src\\instanceof.lua"]}]}]},{"name":"jsdotlua_luau-polyfill@1.2.7","className":"Folder","children":[{"name":"boolean","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\boolean.lua"]},{"name":"collections","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\collections.lua"]},{"name":"console","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\console.lua"]},{"name":"es7-types","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\es7-types.lua"]},{"name":"instance-of","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\instance-of.lua"]},{"name":"luau-polyfill","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\init.lua","Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\default.project.json"],"children":[{"name":"AssertionError","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\AssertionError\\init.lua"],"children":[{"name":"AssertionError.global","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\AssertionError\\AssertionError.global.lua"]}]},{"name":"Error","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\Error\\init.lua"],"children":[{"name":"Error.global","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\Error\\Error.global.lua"]}]},{"name":"Promise","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\Promise.lua"]},{"name":"encodeURIComponent","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\encodeURIComponent.lua"]},{"name":"extends","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\luau-polyfill\\src\\extends.lua"]}]},{"name":"math","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\math.lua"]},{"name":"number","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\number.lua"]},{"name":"string","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\string.lua"]},{"name":"symbol-luau","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\symbol-luau.lua"]},{"name":"timers","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_luau-polyfill@1.2.7\\timers.lua"]}]},{"name":"jsdotlua_math@1.2.7","className":"Folder","children":[{"name":"math","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_math@1.2.7\\math\\src\\init.lua","Packages\\_Index\\jsdotlua_math@1.2.7\\math\\default.project.json"],"children":[{"name":"clz32","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_math@1.2.7\\math\\src\\clz32.lua"]}]}]},{"name":"jsdotlua_number@1.2.7","className":"Folder","children":[{"name":"number","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\init.lua","Packages\\_Index\\jsdotlua_number@1.2.7\\number\\default.project.json"],"children":[{"name":"MAX_SAFE_INTEGER","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\MAX_SAFE_INTEGER.lua"]},{"name":"MIN_SAFE_INTEGER","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\MIN_SAFE_INTEGER.lua"]},{"name":"isFinite","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\isFinite.lua"]},{"name":"isInteger","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\isInteger.lua"]},{"name":"isNaN","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\isNaN.lua"]},{"name":"isSafeInteger","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\isSafeInteger.lua"]},{"name":"toExponential","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_number@1.2.7\\number\\src\\toExponential.lua"]}]}]},{"name":"jsdotlua_promise@3.5.2","className":"Folder","children":[{"name":"promise","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_promise@3.5.2\\promise\\lib\\init.lua","Packages\\_Index\\jsdotlua_promise@3.5.2\\promise\\default.project.json"],"children":[{"name":"init.spec","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_promise@3.5.2\\promise\\lib\\init.spec.lua"]}]}]},{"name":"jsdotlua_react-reconciler@17.1.0","className":"Folder","children":[{"name":"luau-polyfill","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\luau-polyfill.lua"]},{"name":"promise","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\promise.lua"]},{"name":"react-reconciler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\init.lua","Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\default.project.json"],"children":[{"name":"DebugTracing","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\DebugTracing.lua"]},{"name":"MaxInts","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\MaxInts.lua"]},{"name":"ReactCapturedValue","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactCapturedValue.lua"]},{"name":"ReactChildFiber.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactChildFiber.new.lua"]},{"name":"ReactCurrentFiber","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactCurrentFiber.lua"]},{"name":"ReactFiber.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiber.new.lua"]},{"name":"ReactFiberBeginWork.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberBeginWork.new.lua"]},{"name":"ReactFiberClassComponent.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberClassComponent.new.lua"]},{"name":"ReactFiberCommitWork.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberCommitWork.new.lua"]},{"name":"ReactFiberCompleteWork.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberCompleteWork.new.lua"]},{"name":"ReactFiberComponentStack","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberComponentStack.lua"]},{"name":"ReactFiberContext.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberContext.new.lua"]},{"name":"ReactFiberDevToolsHook.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberDevToolsHook.new.lua"]},{"name":"ReactFiberErrorDialog","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberErrorDialog.lua"]},{"name":"ReactFiberErrorLogger","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberErrorLogger.lua"]},{"name":"ReactFiberFlags","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberFlags.lua"]},{"name":"ReactFiberHooks.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberHooks.new.lua"]},{"name":"ReactFiberHostConfig","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberHostConfig.lua"]},{"name":"ReactFiberHostContext.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberHostContext.new.lua"]},{"name":"ReactFiberHotReloading.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberHotReloading.new.lua"]},{"name":"ReactFiberHydrationContext.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberHydrationContext.new.lua"]},{"name":"ReactFiberLane","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberLane.lua"]},{"name":"ReactFiberLazyComponent.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberLazyComponent.new.lua"]},{"name":"ReactFiberNewContext.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberNewContext.new.lua"]},{"name":"ReactFiberOffscreenComponent","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberOffscreenComponent.lua"]},{"name":"ReactFiberReconciler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberReconciler.lua"]},{"name":"ReactFiberReconciler.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberReconciler.new.lua"]},{"name":"ReactFiberRoot.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberRoot.new.lua"]},{"name":"ReactFiberSchedulerPriorities.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberSchedulerPriorities.roblox.lua"]},{"name":"ReactFiberStack.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberStack.new.lua"]},{"name":"ReactFiberSuspenseComponent.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberSuspenseComponent.new.lua"]},{"name":"ReactFiberSuspenseContext.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberSuspenseContext.new.lua"]},{"name":"ReactFiberThrow.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberThrow.new.lua"]},{"name":"ReactFiberTransition","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberTransition.lua"]},{"name":"ReactFiberTreeReflection","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberTreeReflection.lua"]},{"name":"ReactFiberUnwindWork.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberUnwindWork.new.lua"]},{"name":"ReactFiberWorkInProgress","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberWorkInProgress.lua"]},{"name":"ReactFiberWorkLoop.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactFiberWorkLoop.new.lua"]},{"name":"ReactHookEffectTags","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactHookEffectTags.lua"]},{"name":"ReactInternalTypes","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactInternalTypes.lua"]},{"name":"ReactMutableSource.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactMutableSource.new.lua"]},{"name":"ReactPortal","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactPortal.lua"]},{"name":"ReactProfilerTimer.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactProfilerTimer.new.lua"]},{"name":"ReactRootTags","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactRootTags.lua"]},{"name":"ReactStrictModeWarnings.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactStrictModeWarnings.new.lua"]},{"name":"ReactTestSelectors","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactTestSelectors.lua"]},{"name":"ReactTypeOfMode","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactTypeOfMode.lua"]},{"name":"ReactUpdateQueue.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactUpdateQueue.new.lua"]},{"name":"ReactWorkTags","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\ReactWorkTags.lua"]},{"name":"SchedulerWithReactIntegration.new","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\SchedulerWithReactIntegration.new.lua"]},{"name":"SchedulingProfiler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\SchedulingProfiler.lua"]},{"name":"forks","className":"Folder","children":[{"name":"ReactFiberHostConfig.test","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react-reconciler\\src\\forks\\ReactFiberHostConfig.test.lua"]}]}]},{"name":"react","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\react.lua"]},{"name":"scheduler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\scheduler.lua"]},{"name":"shared","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-reconciler@17.1.0\\shared.lua"]}]},{"name":"jsdotlua_react-roblox@17.1.0","className":"Folder","children":[{"name":"luau-polyfill","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\luau-polyfill.lua"]},{"name":"react-reconciler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-reconciler.lua"]},{"name":"react-roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\init.lua","Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\default.project.json"],"children":[{"name":"ReactReconciler.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\ReactReconciler.roblox.lua"]},{"name":"client","className":"Folder","children":[{"name":"ReactRoblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\ReactRoblox.lua"]},{"name":"ReactRobloxComponent","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\ReactRobloxComponent.lua"]},{"name":"ReactRobloxComponentTree","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\ReactRobloxComponentTree.lua"]},{"name":"ReactRobloxHostConfig","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\ReactRobloxHostConfig.lua"]},{"name":"ReactRobloxHostTypes.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\ReactRobloxHostTypes.roblox.lua"]},{"name":"ReactRobloxRoot","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\ReactRobloxRoot.lua"]},{"name":"roblox","className":"Folder","children":[{"name":"RobloxComponentProps","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\roblox\\RobloxComponentProps.lua"]},{"name":"SingleEventManager","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\roblox\\SingleEventManager.lua"]},{"name":"getDefaultInstanceProperty","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react-roblox\\src\\client\\roblox\\getDefaultInstanceProperty.lua"]}]}]}]},{"name":"react","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\react.lua"]},{"name":"scheduler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\scheduler.lua"]},{"name":"shared","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react-roblox@17.1.0\\shared.lua"]}]},{"name":"jsdotlua_react@17.1.0","className":"Folder","children":[{"name":"luau-polyfill","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\luau-polyfill.lua"]},{"name":"react","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\init.lua","Packages\\_Index\\jsdotlua_react@17.1.0\\react\\default.project.json"],"children":[{"name":"None.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\None.roblox.lua"]},{"name":"React","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\React.lua"]},{"name":"ReactBaseClasses","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactBaseClasses.lua"]},{"name":"ReactBinding.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactBinding.roblox.lua"]},{"name":"ReactChildren","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactChildren.lua"]},{"name":"ReactContext","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactContext.lua"]},{"name":"ReactCreateRef","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactCreateRef.lua"]},{"name":"ReactElement","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactElement.lua"]},{"name":"ReactElementValidator","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactElementValidator.lua"]},{"name":"ReactForwardRef","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactForwardRef.lua"]},{"name":"ReactHooks","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactHooks.lua"]},{"name":"ReactLazy","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactLazy.lua"]},{"name":"ReactMemo","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactMemo.lua"]},{"name":"ReactMutableSource","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactMutableSource.lua"]},{"name":"ReactNoopUpdateQueue","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\ReactNoopUpdateQueue.lua"]},{"name":"createSignal.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\react\\src\\createSignal.roblox.lua"]}]},{"name":"shared","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_react@17.1.0\\shared.lua"]}]},{"name":"jsdotlua_scheduler@17.1.0","className":"Folder","children":[{"name":"luau-polyfill","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\luau-polyfill.lua"]},{"name":"scheduler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\init.lua","Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\default.project.json"],"children":[{"name":"Scheduler","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\Scheduler.lua"]},{"name":"SchedulerFeatureFlags","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\SchedulerFeatureFlags.lua"]},{"name":"SchedulerHostConfig","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\SchedulerHostConfig.lua"]},{"name":"SchedulerMinHeap","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\SchedulerMinHeap.lua"]},{"name":"SchedulerPriorities","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\SchedulerPriorities.lua"]},{"name":"SchedulerProfiling","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\SchedulerProfiling.lua"]},{"name":"Tracing","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\Tracing.lua"]},{"name":"TracingSubscriptions","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\TracingSubscriptions.lua"]},{"name":"forks","className":"Folder","children":[{"name":"SchedulerHostConfig.default","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\forks\\SchedulerHostConfig.default.lua"]},{"name":"SchedulerHostConfig.mock","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\forks\\SchedulerHostConfig.mock.lua"]}]},{"name":"unstable_mock","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\scheduler\\src\\unstable_mock.lua"]}]},{"name":"shared","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_scheduler@17.1.0\\shared.lua"]}]},{"name":"jsdotlua_shared@17.1.0","className":"Folder","children":[{"name":"luau-polyfill","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\luau-polyfill.lua"]},{"name":"shared","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\init.lua","Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\default.project.json"],"children":[{"name":"ConsolePatchingDev.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ConsolePatchingDev.roblox.lua"]},{"name":"ErrorHandling.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ErrorHandling.roblox.lua"]},{"name":"ExecutionEnvironment","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ExecutionEnvironment.lua"]},{"name":"PropMarkers","className":"Folder","children":[{"name":"Change","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\PropMarkers\\Change.lua"]},{"name":"Event","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\PropMarkers\\Event.lua"]},{"name":"Tag","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\PropMarkers\\Tag.lua"]}]},{"name":"ReactComponentStackFrame","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactComponentStackFrame.lua"]},{"name":"ReactElementType","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactElementType.lua"]},{"name":"ReactErrorUtils","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactErrorUtils.lua"]},{"name":"ReactFeatureFlags","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactFeatureFlags.lua"]},{"name":"ReactFiberHostConfig","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactFiberHostConfig\\init.lua"],"children":[{"name":"WithNoHydration","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactFiberHostConfig\\WithNoHydration.lua"]},{"name":"WithNoPersistence","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactFiberHostConfig\\WithNoPersistence.lua"]},{"name":"WithNoTestSelectors","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactFiberHostConfig\\WithNoTestSelectors.lua"]}]},{"name":"ReactInstanceMap","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactInstanceMap.lua"]},{"name":"ReactSharedInternals","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSharedInternals\\init.lua"],"children":[{"name":"IsSomeRendererActing","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSharedInternals\\IsSomeRendererActing.lua"]},{"name":"ReactCurrentBatchConfig","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSharedInternals\\ReactCurrentBatchConfig.lua"]},{"name":"ReactCurrentDispatcher","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSharedInternals\\ReactCurrentDispatcher.lua"]},{"name":"ReactCurrentOwner","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSharedInternals\\ReactCurrentOwner.lua"]},{"name":"ReactDebugCurrentFrame","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSharedInternals\\ReactDebugCurrentFrame.lua"]}]},{"name":"ReactSymbols","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactSymbols.lua"]},{"name":"ReactTypes","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactTypes.lua"]},{"name":"ReactVersion","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\ReactVersion.lua"]},{"name":"Symbol.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\Symbol.roblox.lua"]},{"name":"Type.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\Type.roblox.lua"]},{"name":"UninitializedState.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\UninitializedState.roblox.lua"]},{"name":"checkPropTypes","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\checkPropTypes.lua"]},{"name":"console","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\console.lua"]},{"name":"consoleWithStackDev","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\consoleWithStackDev.lua"]},{"name":"enqueueTask.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\enqueueTask.roblox.lua"]},{"name":"flowtypes.roblox","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\flowtypes.roblox.lua"]},{"name":"formatProdErrorMessage","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\formatProdErrorMessage.lua"]},{"name":"getComponentName","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\getComponentName.lua"]},{"name":"invariant","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\invariant.lua"]},{"name":"invokeGuardedCallbackImpl","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\invokeGuardedCallbackImpl.lua"]},{"name":"isValidElementType","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\isValidElementType.lua"]},{"name":"objectIs","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\objectIs.lua"]},{"name":"shallowEqual","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_shared@17.1.0\\shared\\src\\shallowEqual.lua"]}]}]},{"name":"jsdotlua_string@1.2.7","className":"Folder","children":[{"name":"es7-types","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\es7-types.lua"]},{"name":"number","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\number.lua"]},{"name":"string","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\init.lua","Packages\\_Index\\jsdotlua_string@1.2.7\\string\\default.project.json"],"children":[{"name":"charCodeAt","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\charCodeAt.lua"]},{"name":"endsWith","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\endsWith.lua"]},{"name":"findOr","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\findOr.lua"]},{"name":"includes","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\includes.lua"]},{"name":"indexOf","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\indexOf.lua"]},{"name":"lastIndexOf","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\lastIndexOf.lua"]},{"name":"slice","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\slice.lua"]},{"name":"split","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\split.lua"]},{"name":"startsWith","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\startsWith.lua"]},{"name":"substr","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\substr.lua"]},{"name":"trim","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\trim.lua"]},{"name":"trimEnd","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\trimEnd.lua"]},{"name":"trimStart","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_string@1.2.7\\string\\src\\trimStart.lua"]}]}]},{"name":"jsdotlua_symbol-luau@1.0.1","className":"Folder","children":[{"name":"symbol-luau","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_symbol-luau@1.0.1\\symbol-luau\\./src\\init.lua","Packages\\_Index\\jsdotlua_symbol-luau@1.0.1\\symbol-luau\\default.project.json"],"children":[{"name":"Registry.global","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_symbol-luau@1.0.1\\symbol-luau\\./src\\Registry.global.lua"]},{"name":"Symbol","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_symbol-luau@1.0.1\\symbol-luau\\./src\\Symbol.lua"]}]}]},{"name":"jsdotlua_timers@1.2.7","className":"Folder","children":[{"name":"collections","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_timers@1.2.7\\collections.lua"]},{"name":"timers","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_timers@1.2.7\\timers\\src\\init.lua","Packages\\_Index\\jsdotlua_timers@1.2.7\\timers\\default.project.json"],"children":[{"name":"makeIntervalImpl","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_timers@1.2.7\\timers\\src\\makeIntervalImpl.lua"]},{"name":"makeTimerImpl","className":"ModuleScript","filePaths":["Packages\\_Index\\jsdotlua_timers@1.2.7\\timers\\src\\makeTimerImpl.lua"]}]}]}]},{"name":"react-roblox","className":"ModuleScript","filePaths":["Packages\\react-roblox.lua"]},{"name":"react","className":"ModuleScript","filePaths":["Packages\\react.lua"]}]}]}]}]}]} \ No newline at end of file diff --git a/src/DialogueClientScript/API/Dialogue.lua b/src/DialogueClientScript/API/Dialogue.lua new file mode 100644 index 0000000..1201075 --- /dev/null +++ b/src/DialogueClientScript/API/Dialogue.lua @@ -0,0 +1,601 @@ +--!strict +local ReactRoblox = require(script.Parent.Parent.Packages["react-roblox"]); +local React = require(script.Parent.Parent.Packages.react); +local Players = game:GetService("Players"); +local Player = Players.LocalPlayer; + +local themeChangedEvent = Instance.new("BindableEvent"); + +local DialogueModule = { + isPlayerTalkingWithNPC = false; + currentTheme = nil; + onThemeChanged = themeChangedEvent.Event; +}; + +local DialogueClientScript = script.Parent.Parent; +local Types = require(DialogueClientScript.Types); +type Page = Types.Page; + +local clientSettings = require(DialogueClientScript.Settings); + +-- @since v5.0.0 +function DialogueModule:getThemeModuleScript(themeName: string, useDefaultIfNotFound: boolean?): ModuleScript + + -- Check if we have the theme + local ThemeFolder = DialogueClientScript.ReactComponents.Themes; + local themeModuleScript = ThemeFolder:FindFirstChild(themeName); + if themeName and not themeModuleScript and useDefaultIfNotFound then + + if themeName ~= "" then + + warn("[Dialogue Maker]: Can't find theme \"" .. themeName .. "\" in the Themes folder of the DialogueClientScript. Using default theme..."); + + end + + themeModuleScript = ThemeFolder:FindFirstChild(clientSettings.defaultTheme); + + end + + -- Return the theme module script. + assert(themeModuleScript, "There isn't an available theme."); + return themeModuleScript; + +end; + +-- Searches for a ModuleScript based on a given directory. Errors if it doesn't exist. +-- @since v5.0.0 +-- @returns A module script of a given directory. +function DialogueModule:getContentScriptFromPriority(dialogueContainer: Folder, targetPath: {string}): ModuleScript + + local currentPath = ""; + local CurrentDirectoryScript: ModuleScript | Folder = dialogueContainer; + for index, directory in targetPath do + + currentPath = currentPath .. (if currentPath ~= "" then "." else "") .. directory; + local PossibleDirectory = CurrentDirectoryScript:FindFirstChild(directory); + if not PossibleDirectory or not PossibleDirectory:IsA("ModuleScript") then + + error("[Dialogue Maker]" .. currentPath .. " is not a ModuleScript"); + + end + CurrentDirectoryScript = PossibleDirectory; + + end; + + if CurrentDirectoryScript:IsA("Folder") then + + error("[Dialogue Maker] Target path (" .. table.concat(targetPath, ".") .. ") not found."); + + end + + return CurrentDirectoryScript; + +end; + +-- Returns a list of Page objects based on the given content array by fitting it in a given text label in a given text container. +-- @since v5.0.0 +function DialogueModule:getPages(contentArray: Types.ContentArray, textLabel: TextLabel): {Page} + + local pages: {Types.Page} = {}; + local currentPage: Types.Page = {}; + local textContainer = textLabel.Parent; + assert(textContainer and textContainer:IsA("GuiObject"), "TextLabel must be in a text container."); + + local TextContainerClone = textContainer:Clone(); + local textLabelClone = textLabel:Clone(); + textLabelClone.MaxVisibleGraphemes = -1; + + TextContainerClone.Visible = false; + TextContainerClone.Parent = textContainer.Parent; + + local segment = TextContainerClone:FindFirstChild("Segment"); + if segment then + + segment:Destroy(); + + end + + local xSizeOffset = 0; + + local function newPage() + + table.insert(pages, currentPage); + currentPage = {}; + textLabelClone = textLabelClone:Clone(); + + for _, child in TextContainerClone:GetChildren() do + + if child.Name ~= "TextWrapper" then + + child:Destroy(); + + end; + + end + + textLabelClone.Parent = TextContainerClone; + xSizeOffset = 0; + + end + + for contentArrayIndex, contentArrayItem in contentArray do + + local contentArrayItemType = typeof(contentArrayItem); + + if contentArrayItemType == "string" then + + -- Calculate the X size offset. + local uiListLayout = TextContainerClone:FindFirstChild("UIListLayout"); + assert(uiListLayout and uiListLayout:IsA("UIListLayout"), "[Dialogue Maker] UIListLayout not found"); + + local lastSpaceIndex: number? = nil; + + repeat + + local function addTextLabelToPage(TextLabel: TextLabel) + + table.insert(currentPage, { + type = "text"; + text = TextLabel.Text; + size = TextLabel.AbsoluteSize; + }); + + end + + textLabelClone = textLabelClone:Clone(); + textLabelClone.Visible = true; + + if lastSpaceIndex then + + textLabelClone.Text = (contentArrayItem :: string):sub(lastSpaceIndex + 1); + + else + + textLabelClone.Text = contentArrayItem :: string; + + end + + textLabelClone.Parent = TextContainerClone; + + if not textLabelClone.TextFits then + + -- Check if we should add a new page. + if uiListLayout.AbsoluteContentSize.Y > TextContainerClone.AbsoluteSize.Y then + + -- Add the current page to the page list. + newPage(); + + end + + end + + if textLabelClone.TextFits then + + local function getRichTextIndices(text: string) + + local richTextTagIndices: {Types.RichTextTagInformation} = {}; + local openTagIndices: {number} = {}; + local textCopy = text; + local tagPattern = "<[^<>]->"; + local pointer = 1; + for tag in textCopy:gmatch(tagPattern) do + + -- Get the tag name and attributes. + local tagText = tag:match("<([^<>]-)>"); + if tagText then + + local firstSpaceIndex = tagText:find(" "); + local tagTextLength = tagText:len(); + local name = tagText:sub(1, (firstSpaceIndex and firstSpaceIndex - 1) or tagTextLength); + if name:sub(1, 1) == "/" then + + for _, index in openTagIndices do + + if richTextTagIndices[index].name == name:sub(2) then + + -- Add a tag end offset. + local _, endOffset = textCopy:find(tagPattern); + if endOffset then + + richTextTagIndices[index].endOffset = pointer + endOffset; + + end; + + -- Remove the tag from the open tag table. + table.remove(openTagIndices, index); + break; + + end + + end + + else + + -- Get the tag start offset. + local attributes = firstSpaceIndex and tagText:sub(firstSpaceIndex + 1) or ""; + table.insert(richTextTagIndices, { + name = name; + attributes = attributes; + startOffset = textCopy:find(tagPattern) :: number + pointer - 1; + }); + table.insert(openTagIndices, #richTextTagIndices); + + end + + -- Remove the tag from our copy. + local _, pointerUpdate = textCopy:find(tagPattern); + if pointerUpdate then + + pointer += pointerUpdate - 1; + textCopy = textCopy:sub(pointerUpdate); + + end; + + end; + + end + + return richTextTagIndices; + + end + + local function getLineBreakPositions(text: string, TextLabel: TextLabel, isRichText: boolean): {number} + + -- Iterate through each character. + local breakpoints: {number} = {}; + local originalTextLabelText = TextLabel.Text; + TextLabel.Text = ""; + local lastSpaceIndex: number = 1; + local skipCounter = 0; + local remainingRichTextTags = getRichTextIndices(text); + for index, character in text:split("") do + + -- Check if this is an offset. + if skipCounter ~= 0 then + + skipCounter -= 1; + continue; + + end + + if isRichText then + + for _, richTextTagIndex in remainingRichTextTags do + + if richTextTagIndex.startOffset == index then + + skipCounter = ("<" .. richTextTagIndex.name .. (if richTextTagIndex.attributes and richTextTagIndex.attributes ~= "" then " " .. richTextTagIndex.attributes else "") .. ">"):len() - 1; + break; + + elseif richTextTagIndex.endOffset :: number - (""):len() == index then + + skipCounter = (""):len() - 1; + break; + + end + + end + + end; + + if skipCounter > 0 then + + continue; + + end + + -- Keep track of spaces. + if character == " " then + + lastSpaceIndex = index; + + end + + -- Keep track of the original text bounds. + local originalTextBoundsY = TextLabel.TextBounds.Y; + + -- Add the character and applicable rich text tags. + TextLabel.Text = TextLabel.ContentText .. character; + if isRichText then + + for _, richTextTagInfo in remainingRichTextTags do + + local tagStartOffset = richTextTagInfo.startOffset; + local tagEndOffset = richTextTagInfo.endOffset :: number; + if index >= tagStartOffset and tagEndOffset > (breakpoints[#breakpoints] or 0) then + + local prefix = "<" .. richTextTagInfo.name .. (if richTextTagInfo.attributes and richTextTagInfo.attributes ~= "" then " " .. richTextTagInfo.attributes else "") .. ">"; + local suffix = ""; + local startOffset = tagStartOffset - (breakpoints[#breakpoints] or 0); + local endOffset = (tagEndOffset - (breakpoints[#breakpoints] or 0)) - prefix:len() - suffix:len(); + TextLabel.Text = TextLabel.ContentText:sub(1, startOffset - 1) .. prefix .. TextLabel.ContentText:sub(startOffset, endOffset - 1) .. suffix .. TextLabel.ContentText:sub(endOffset); + + end + + end + + end; + + if TextLabel.TextBounds.Y > originalTextBoundsY then + + local currentTextBoundsY = TextLabel.TextBounds.Y; + TextLabel.TextWrapped = false; + + if TextLabel.TextBounds.Y < currentTextBoundsY then + + table.insert(breakpoints, lastSpaceIndex); + TextLabel.Text = text:sub(lastSpaceIndex + 1, index); + + end + + TextLabel.TextWrapped = true; + + end + + end + + TextLabel.Text = originalTextLabelText; + + -- Return breakpoints. + return breakpoints; + + end + + local originalText = textLabelClone.Text; + local breakpoints = getLineBreakPositions(originalText, textLabelClone, textLabelClone.RichText); + local lastBreakpointIndex = breakpoints[#breakpoints]; + + if lastBreakpointIndex then + + -- Create another TextLabel to replace the last line of text. + -- This will allow the TextWrapper to accurately calculate + -- how much space is available on the X-axis. + local ParagraphTextLabel = textLabelClone:Clone(); + ParagraphTextLabel.Text = originalText:sub(1, lastBreakpointIndex); + ParagraphTextLabel.Parent = textLabelClone.Parent; + addTextLabelToPage(ParagraphTextLabel); + + -- Fix the textLabelClone's text back. + textLabelClone.Parent = nil; + textLabelClone.Parent = ParagraphTextLabel.Parent; + textLabelClone.Text = originalText:sub(lastBreakpointIndex + 1); + + end; + + addTextLabelToPage(textLabelClone); + + xSizeOffset += textLabelClone.TextBounds.X; + + lastSpaceIndex = nil; + + else + + -- Remove a word from the text until we can fit the text. + lastSpaceIndex = 0; + repeat + + lastSpaceIndex = table.pack(textLabelClone.Text:find(".* "))[2] :: number; + + if not lastSpaceIndex and textLabelClone.TextBounds.Y < textLabelClone.TextSize * textLabelClone.LineHeight then + + -- The given area is too small. Add this to a new page. + print("New page!"); + newPage(); + continue; + + end + + assert(lastSpaceIndex, "[Dialogue Maker] Unable to fit text in text container even after removing the spaces. Is the text too big?"); + textLabelClone.Text = textLabelClone.Text:sub(1, lastSpaceIndex - 1); + + until textLabelClone.TextFits; + + -- Add the remaining text to a new page. + addTextLabelToPage(textLabelClone); + + xSizeOffset = 0; + + end + + until not lastSpaceIndex; + + elseif contentArrayItemType == "table" then + + -- TODO: Add effects + + end; + + end + + TextContainerClone:Destroy(); + + -- Return all pages for this message. + if currentPage[1] then + + newPage(); + + end + + return pages; + +end; + +-- Checks if the local player passes a condition. +-- @since v5.0.0 +function DialogueModule:doesPlayerPassCondition(contentScript: ModuleScript): boolean + + local conditionScript = contentScript:FindFirstChild("Condition") :: ModuleScript?; + local conditionResult = if conditionScript then require(conditionScript)() :: boolean else true; + return conditionResult; + +end; + +function DialogueModule:setTheme(theme: ModuleScript): () + + self.currentTheme = theme; + themeChangedEvent:Fire(theme); + +end; + +-- @since v5.0.0 +function DialogueModule:readDialogue(npc: Model, npcSettings: Types.NPCSettings): () + + -- Make sure we aren't already talking to an NPC + assert(not DialogueModule.isPlayerTalkingWithNPC, "[Dialogue Maker] Cannot read dialogue because player is currently talking with another NPC."); + DialogueModule.isPlayerTalkingWithNPC = true; + + -- Make sure we have a DialogueContainer. + local NPCDialogueContainer: Folder? = npc:FindFirstChild("DialogueContainer") :: Folder; + assert(NPCDialogueContainer, "DialogueContainer not found in NPC."); + + -- Initialize the theme, then listen for changes + local themeModuleScript = DialogueModule:getThemeModuleScript(npcSettings.general.themeName, true); + local dialogueGUI = Instance.new("ScreenGui"); + dialogueGUI.Parent = Player.PlayerGui; + local root = ReactRoblox.createRoot(dialogueGUI); + self:setTheme(themeModuleScript); + + -- Show the dialogue to the player + local currentDialoguePriority = "1"; + local currentContentScript: ModuleScript; + while DialogueModule.isPlayerTalkingWithNPC and task.wait() do + + -- Get the current directory. + local isSuccess = pcall(function() + + currentContentScript = DialogueModule:getContentScriptFromPriority(NPCDialogueContainer, currentDialoguePriority:split(".")); + + end); + + if not isSuccess then + + break; + + end; + + local dialogueType = currentContentScript:GetAttribute("DialogueType"); + + if DialogueModule:doesPlayerPassCondition(currentContentScript) then + + local dialogueContentArray = (require(currentContentScript) :: () -> Types.ContentArray)(); + if dialogueType == "Redirect" then + + -- A redirect is available, so let's switch priorities. + assert(typeof(dialogueContentArray[1]) == "string", "[Dialogue Maker] Item at index 1 is not a directory."); + currentDialoguePriority = dialogueContentArray[1] :: string; + continue; + + end; + + -- Get a list of responses from the dialogue. + local responses: {ModuleScript} = {}; + for _, PossibleResponse in currentContentScript:GetChildren() do + + if PossibleResponse:IsA("ModuleScript") and tonumber(PossibleResponse.Name) and PossibleResponse:GetAttribute("DialogueType") == "Response" and DialogueModule:doesPlayerPassCondition(PossibleResponse) then + + table.insert(responses, PossibleResponse); + + end + + end + + -- Sort responses because :GetChildren() doesn't guarantee it + table.sort(responses, function(responseScript1, responseScript2) + + return responseScript1.Name < responseScript2.Name; + + end); + + local onCompletionEvent = Instance.new("BindableEvent"); + local function renderRoot() + + root:render(React.createElement(require(themeModuleScript) :: any, { + responseContentScripts = responses; + dialogueContentArray = dialogueContentArray; + onComplete = function(selectedResponseContentScript: ModuleScript?) + + -- Run action. + local actionScript = currentContentScript:FindFirstChild("Action") :: ModuleScript?; + if actionScript then + + (require(actionScript) :: () -> ())(); + + end; + + -- Check if there is more dialogue. + local hasPossibleDialogue = false; + local nextScript = if selectedResponseContentScript then selectedResponseContentScript else currentContentScript; + for _, possibleContentScript in nextScript:GetChildren() do + + local possibleDialogueType = possibleContentScript:GetAttribute("DialogueType"); + if possibleContentScript:IsA("ModuleScript") and tonumber(possibleContentScript.Name) and (possibleDialogueType == "Message" or possibleDialogueType == "Redirect") then + + hasPossibleDialogue = true; + break; + + end + + end + + if DialogueModule.isPlayerTalkingWithNPC and hasPossibleDialogue then + + currentDialoguePriority = `{if selectedResponseContentScript then `{currentDialoguePriority}.{selectedResponseContentScript.Name}` else currentDialoguePriority}.1`; + + else + + DialogueModule.isPlayerTalkingWithNPC = false; + + end; + + onCompletionEvent:Fire(); + + end; + onTimeout = function() + + DialogueModule.isPlayerTalkingWithNPC = false; + onCompletionEvent:Fire(); + + end; + clientSettings = clientSettings; + npcSettings = npcSettings; + npc = npc; + })); + + end; + + local themeChangedEvent = DialogueModule.onThemeChanged:Connect(function() + + renderRoot(); + + end); + + renderRoot(); + onCompletionEvent.Event:Wait(); + themeChangedEvent:Disconnect(); + + elseif DialogueModule.isPlayerTalkingWithNPC then + + -- There is a message; however, the player failed the condition. + -- Let's check if there's something else available. + local SplitPriority = currentDialoguePriority:split("."); + SplitPriority[#SplitPriority] = tostring(tonumber(SplitPriority[#SplitPriority]) :: number + 1); + currentDialoguePriority = table.concat(SplitPriority, "."); + + end; + + end; + + -- Free the player :) + root:unmount(); + dialogueGUI:Destroy(); + DialogueModule.isPlayerTalkingWithNPC = false; + +end; + +Player.CharacterRemoving:Connect(function() + + DialogueModule.isPlayerTalkingWithNPC = false; + +end); + +return DialogueModule; diff --git a/src/DialogueClientScript/API/Player.lua b/src/DialogueClientScript/API/Player.lua new file mode 100644 index 0000000..87ad257 --- /dev/null +++ b/src/DialogueClientScript/API/Player.lua @@ -0,0 +1,17 @@ +--!strict +local PlayerModule = {}; +local Player = game:GetService("Players").LocalPlayer; + +function PlayerModule:freezePlayer(): () + + (require(Player.PlayerScripts:WaitForChild("PlayerModule")) :: any):GetControls():Disable(); + +end; + +function PlayerModule:unfreezePlayer(): () + + (require(Player.PlayerScripts:WaitForChild("PlayerModule")) :: any):GetControls():Enable(); + +end; + +return PlayerModule; diff --git a/src/DialoguePluginScript/DialogueClientScript/API/Triggers.lua b/src/DialogueClientScript/API/Triggers.lua similarity index 79% rename from src/DialoguePluginScript/DialogueClientScript/API/Triggers.lua rename to src/DialogueClientScript/API/Triggers.lua index 6302a82..5c59b1b 100644 --- a/src/DialoguePluginScript/DialogueClientScript/API/Triggers.lua +++ b/src/DialogueClientScript/API/Triggers.lua @@ -4,7 +4,7 @@ local ProximityPrompts = {}; local SpeechBubbles = {}; local ClickDetectors = {}; -function TriggerModule.createSpeechBubble(npc: Model, properties: {[string]: any}): BillboardGui +function TriggerModule:createSpeechBubble(npc: Model, properties: {[string]: any}): BillboardGui SpeechBubbles[npc] = Instance.new("BillboardGui"); SpeechBubbles[npc].Name = "SpeechBubble"; @@ -27,7 +27,7 @@ function TriggerModule.createSpeechBubble(npc: Model, properties: {[string]: any end; -function TriggerModule.disableAllSpeechBubbles(): () +function TriggerModule:disableAllSpeechBubbles(): () for _, speechBubble in pairs(SpeechBubbles) do @@ -37,7 +37,7 @@ function TriggerModule.disableAllSpeechBubbles(): () end; -function TriggerModule.enableAllSpeechBubbles(): () +function TriggerModule:enableAllSpeechBubbles(): () for _, speechBubble in pairs(SpeechBubbles) do @@ -47,19 +47,19 @@ function TriggerModule.enableAllSpeechBubbles(): () end; -function TriggerModule.addClickDetector(npc: Model, clickDetector: ClickDetector): () +function TriggerModule:addClickDetector(npc: Model, clickDetector: ClickDetector): () ClickDetectors[npc] = clickDetector; end; -function TriggerModule.addProximityPrompt(npc: Model, proximityPrompt: ProximityPrompt): () +function TriggerModule:addProximityPrompt(npc: Model, proximityPrompt: ProximityPrompt): () ProximityPrompts[npc] = proximityPrompt end -function TriggerModule.disableAllClickDetectors(): () +function TriggerModule:disableAllClickDetectors(): () for _, clickDetector in pairs(ClickDetectors) do @@ -75,12 +75,12 @@ function TriggerModule.disableAllClickDetectors(): () end; -function TriggerModule.enableAllClickDetectors(): () +function TriggerModule:enableAllClickDetectors(): () for _, clickDetector in pairs(ClickDetectors) do local OriginalParent = clickDetector:FindFirstChild("OriginalParent"); - if OriginalParent:IsA("ObjectValue") and OriginalParent.Value then + if OriginalParent and OriginalParent:IsA("ObjectValue") and OriginalParent.Value then clickDetector.Parent = OriginalParent.Value; OriginalParent:Destroy(); @@ -91,7 +91,7 @@ function TriggerModule.enableAllClickDetectors(): () end; -function TriggerModule.disableAllProximityPrompts(): () +function TriggerModule:disableAllProximityPrompts(): () for _, proximityPrompt in pairs(ProximityPrompts) do @@ -106,12 +106,12 @@ function TriggerModule.disableAllProximityPrompts(): () end; end; -function TriggerModule.enableAllProximityPrompts(): () +function TriggerModule:enableAllProximityPrompts(): () for _, proximityDetector in pairs(ProximityPrompts) do local OriginalParent = proximityDetector:FindFirstChild("OriginalParent"); - if OriginalParent:IsA("ObjectValue") and OriginalParent.Value then + if OriginalParent and OriginalParent:IsA("ObjectValue") and OriginalParent.Value then proximityDetector.Parent = OriginalParent.Value; OriginalParent:Destroy(); diff --git a/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/MessageComponentList.lua b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/MessageComponentList.lua new file mode 100644 index 0000000..7b5db5e --- /dev/null +++ b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/MessageComponentList.lua @@ -0,0 +1,77 @@ +--!strict +local React = require(script.Parent.Parent.Parent.Parent.Packages.react); +local Types = require(script.Parent.Parent.Parent.Parent.Types); +type Page = Types.Page; +type NPCSettings = Types.NPCSettings; + +export type useMessageComponentsProps = { + pages: {Page}?; + currentPageIndex: number; + skipPageEvent: BindableEvent?; + textSegmentComponent: any; + responseContentScripts: {ModuleScript}; + npcSettings: NPCSettings; + textSize: number; + onTimeout: () -> (); + setIsNPCTalking: (boolean) -> (); +} + +local function MessageComponentList(props: useMessageComponentsProps) + + -- Props + local pages = props.pages; + local currentPageIndex = props.currentPageIndex; + local skipPageEvent = props.skipPageEvent; + local TextSegment = props.textSegmentComponent; + + React.useEffect(function(): () + + props.setIsNPCTalking(true); + + end, {pages :: any, currentPageIndex}); + + local messageComponentList = {}; + if pages then + + local page = pages[currentPageIndex]; + if page then + + for index, dialogueContentItem in page do + + if dialogueContentItem.type == "effect" then + + dialogueContentItem.run(skipPageEvent); + table.insert(messageComponentList, React.createElement(React.Fragment)); + + elseif dialogueContentItem.type == "text" then + + -- Determine new offset. + table.insert(messageComponentList, React.createElement(TextSegment, { + text = dialogueContentItem.text; + skipPageEvent = if skipPageEvent then skipPageEvent.Event else nil; + layoutOrder = index; + textSize = props.textSize; + onComplete = function() + + if index == #page then + + props.setIsNPCTalking(false); + + end; + + end; + })); + + end; + + end; + + end; + + end; + + return messageComponentList; + +end; + +return MessageComponentList; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/ResponseButton.lua b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/ResponseButton.lua new file mode 100644 index 0000000..147367a --- /dev/null +++ b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/ResponseButton.lua @@ -0,0 +1,18 @@ +--!strict +local React = require(script.Parent.Parent.Parent.Parent.Packages.react); + +export type ResponseProperties = { + onClick: () -> (); + text: string; +} + +local function ResponseButton(props: ResponseProperties) + + return React.createElement("TextButton", { + [React.Event.Activated] = props.onClick; + Text = props.text; + }); + +end; + +return ResponseButton; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/ResponseComponentList.lua b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/ResponseComponentList.lua new file mode 100644 index 0000000..78da597 --- /dev/null +++ b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/ResponseComponentList.lua @@ -0,0 +1,30 @@ +--!strict +local React = require(script.Parent.Parent.Parent.Parent.Packages.react); + +export type ResponseComponentListProperties = { + responseButtonComponent: any; + responseContentScripts: {ModuleScript}; + onComplete: (ModuleScript) -> (); +} + +local function ResponseComponentList(props: ResponseComponentListProperties) + + local responseComponents = {}; + for _, responseModule in props.responseContentScripts do + + table.insert(responseComponents, React.createElement(props.responseButtonComponent, { + text = (require(responseModule) :: () -> {string})()[1]; -- The response's text is always at the first index. + onClick = function() + + props.onComplete(responseModule); + + end; + })) + + end; + + return responseComponents; + +end; + +return ResponseComponentList; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/TextSegment.lua b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/TextSegment.lua new file mode 100644 index 0000000..ceeef28 --- /dev/null +++ b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/TextSegment.lua @@ -0,0 +1,82 @@ +--!strict +local React = require(script.Parent.Parent.Parent.Parent.Packages.react); + +export type TextSegmentProperties = { + text: string; + skipPageEvent: RBXScriptSignal?; + letterDelay: number; + layoutOrder: number; + textSize: number; + onComplete: () -> (); + isTest: boolean?; +} + +local function TextSegment(props: TextSegmentProperties, textLabelRef: any) + + local text = props.text; + local maxVisibleGraphemes, setMaxVisibleGraphemes = React.useState(0); + local textLabelRefFallback = React.useRef(nil :: TextLabel?); + + React.useEffect(function() + + setMaxVisibleGraphemes(0); + + end, {text}); + + React.useEffect(function(): () + + if not props.isTest then + + local typewriterTask = task.delay(props.letterDelay, function() + + local textLabel = (textLabelRef or textLabelRefFallback).current; + if maxVisibleGraphemes ~= -1 and textLabel and maxVisibleGraphemes < #textLabel.ContentText then + + setMaxVisibleGraphemes(maxVisibleGraphemes + 1); + + else + + props.onComplete(); + + end; + + end); + + if props.skipPageEvent then + + local skipConnection = props.skipPageEvent:Once(function() + + task.cancel(typewriterTask); + setMaxVisibleGraphemes(-1); + + end); + + return function() + + skipConnection:Disconnect(); + + end; + + end; + + end; + + end, {maxVisibleGraphemes}); + + return React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY; + Text = text; + MaxVisibleGraphemes = maxVisibleGraphemes; + ref = textLabelRef or textLabelRefFallback; + LayoutOrder = props.layoutOrder; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Regular); + TextSize = props.textSize; + BackgroundTransparency = 1; + Visible = not props.isTest; + TextXAlignment = Enum.TextXAlignment.Left; + TextWrapped = true; + }) + +end; + +return React.forwardRef(TextSegment); \ No newline at end of file diff --git a/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/init.lua b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/init.lua new file mode 100644 index 0000000..83f63e5 --- /dev/null +++ b/src/DialogueClientScript/ReactComponents/Themes/BareBonesTheme/init.lua @@ -0,0 +1,163 @@ +--!strict +-- BareBonesTheme is the first theme that was created for Dialogue Maker. +-- As the name describes, it is a barebones theme that priorities function over form. +-- Developers can use this theme as a template for creating their own. +-- Programmer: Christian Toney (Christian_Toney) +local TextSegment = require(script.TextSegment); +local ResponseButton = require(script.ResponseButton); +local MessageComponentList = require(script.MessageComponentList); +local ResponseComponentList = require(script.ResponseComponentList); +local DialogueClientScript = script.Parent.Parent.Parent; +local ReactHooks = DialogueClientScript.ReactHooks; +local useKeybindContinue = require(ReactHooks.useKeybindContinue); +local useLookAtPlayer = require(ReactHooks.useLookAtPlayer); +local usePages = require(ReactHooks.usePages); +local useOutOfDistanceDetection = require(ReactHooks.useOutOfDistanceDetection); +local useContinueDialogue = require(ReactHooks.useContinueDialogue); +local useDynamicSize = require(ReactHooks.useDynamicSize); +local React = require(DialogueClientScript.Packages.react); +local Types = require(DialogueClientScript.Types); +type ThemeProperties = Types.ThemeProperties; + +local skipPageEvent = Instance.new("BindableEvent"); + +local function BareBonesTheme(props: ThemeProperties) + + -- Props + local npc = props.npc; + local clientSettings = props.clientSettings; + local npcSettings = props.npcSettings; + local npcName = npcSettings.general.npcName; + local responseContentScripts = props.responseContentScripts; + + -- Refs + local clickSoundRef = React.useRef(nil :: Sound?); + local textContainerRef = React.useRef(nil :: GuiObject?); + + -- States + local currentPageIndex, setCurrentPageIndex = React.useState(1); + local isNPCTalking, setIsNPCTalking = React.useState(false); + + -- Hooks + local pages = usePages(props.dialogueContentArray, textContainerRef); + local continueDialogue = useContinueDialogue({ + pages = pages; + clickSoundRef = clickSoundRef; + allowPlayerToSkipDelay = npcSettings.general.allowPlayerToSkipDelay; + currentPageIndex = currentPageIndex; + setCurrentPageIndex = setCurrentPageIndex; + onComplete = props.onComplete; + skipPageEvent = skipPageEvent; + isNPCTalking = isNPCTalking; + responseContentScripts = responseContentScripts; + }); + local sizeX, sizeY, textSize = useDynamicSize({ + { + sizeX = 310; + sizeY = 117; + textSize = 14; + } + }); + useKeybindContinue(clientSettings, continueDialogue); + useLookAtPlayer(npc, npcSettings); + useOutOfDistanceDetection(npc, npcSettings, props.onTimeout); + + return React.createElement("Frame", { + AnchorPoint = Vector2.new(0.5, 1); + Position = UDim2.new(0.5, 0, 1, -15); + AutomaticSize = Enum.AutomaticSize.XY; + BackgroundTransparency = 1; + [React.Event.InputBegan] = function(self: Frame, input: InputObject) + + if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then + + continueDialogue(); + + end; + + end; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + }); + NPCNameTextLabel = if npcSettings.general.showName then React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY; + Text = npcName; + LayoutOrder = 1; + }) else nil; + HorizontalContent = React.createElement("Frame", { + AutomaticSize = Enum.AutomaticSize.XY; + BackgroundTransparency = 1; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + Padding = UDim.new(0, 5); + VerticalAlignment = Enum.VerticalAlignment.Bottom; + }); + NPCTextContainer = React.createElement("Frame", { + Size = UDim2.new(0, sizeX, 0, sizeY); + ref = textContainerRef; + BackgroundColor3 = Color3.new(1, 1, 1); + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + Wraps = true; + }); + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 5); + PaddingTop = UDim.new(0, 5); + PaddingRight = UDim.new(0, 5); + PaddingBottom = UDim.new(0, 5); + }); + TestSegment = React.createElement(TextSegment, { + isTest = true; + textSize = textSize; + }); + MessageComponentList = React.createElement(MessageComponentList, { + pages = pages, + currentPageIndex = currentPageIndex, + skipPageEvent = skipPageEvent; + textSegmentComponent = TextSegment; + npcSettings = npcSettings; + responseContentScripts = responseContentScripts; + onTimeout = props.onTimeout; + setIsNPCTalking = setIsNPCTalking; + textSize = textSize; + }); + }); + ResponseContainer = if #responseContentScripts > 0 then React.createElement("ScrollingFrame", { + BackgroundTransparency = 1; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + }); + ResponseComponentList = React.createElement(ResponseComponentList, { + responseButtonComponent = ResponseButton; + responseContentScripts = props.responseContentScripts; + onComplete = props.onComplete; + }); + }) else nil; + ContinueButton = React.createElement("ImageButton", { + Size = UDim2.new(0, 20, 0, 20); + LayoutOrder = 3; + Image = "rbxassetid://90966430453504"; + BackgroundColor3 = if isNPCTalking and not npcSettings.general.allowPlayerToSkipDelay then Color3.new(0.705882, 0.705882, 0.705882) else Color3.new(1, 1, 1); + ImageColor3 = if isNPCTalking and not npcSettings.general.allowPlayerToSkipDelay then Color3.new(0.486275, 0.486275, 0.486275) else Color3.new(1, 1, 1); + [React.Event.Activated] = if isNPCTalking and not npcSettings.general.allowPlayerToSkipDelay then nil else function() + + continueDialogue() + + end; + }); + ClickSound = if clientSettings.defaultClickSound then React.createElement("Sound", { + SoundId = `rbxassetid://{clientSettings.defaultClickSound}`; + ref = clickSoundRef; + }) else nil; + }); + }) + +end; + +return BareBonesTheme; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactComponents/Themes/BigAndBoldTheme.lua b/src/DialogueClientScript/ReactComponents/Themes/BigAndBoldTheme.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/DialogueClientScript/ReactHooks/useContinueDialogue.lua b/src/DialogueClientScript/ReactHooks/useContinueDialogue.lua new file mode 100644 index 0000000..05db9f3 --- /dev/null +++ b/src/DialogueClientScript/ReactHooks/useContinueDialogue.lua @@ -0,0 +1,50 @@ +--!strict +local Types = require(script.Parent.Parent.Types); +type Page = Types.Page; + +export type ContinueDialogueProperties = { + isNPCTalking: boolean; + clickSoundRef: {current: Sound?}; + allowPlayerToSkipDelay: boolean; + pages: {Page}; + currentPageIndex: number; + setCurrentPageIndex: (number) -> (); + onComplete: () -> (); + skipPageEvent: BindableEvent; + responseContentScripts: {ModuleScript}; +} + +local function useContinueDialogue(props: ContinueDialogueProperties) + + return function() + + if props.isNPCTalking then + + local clickSound = props.clickSoundRef.current; + if clickSound then + + clickSound:Play(); + + end; + + if props.allowPlayerToSkipDelay then + + props.skipPageEvent:Fire(); + + end; + + elseif props.pages and #props.pages > props.currentPageIndex then + + props.setCurrentPageIndex(props.currentPageIndex + 1); + + elseif #props.responseContentScripts == 0 then + + props.onComplete(); + + end; + + end; + +end; + +return useContinueDialogue; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactHooks/useDynamicSize.lua b/src/DialogueClientScript/ReactHooks/useDynamicSize.lua new file mode 100644 index 0000000..95fdc2c --- /dev/null +++ b/src/DialogueClientScript/ReactHooks/useDynamicSize.lua @@ -0,0 +1,68 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); + +export type DynamicSizeProperties = { + minimumHeight: number?; + minimumWidth: number?; + sizeX: number; + sizeY: number; + textSize: number; +} + +local function useDynamicSize(array: {DynamicSizeProperties}) + + local function getSizeBasedOnViewport(): (number, number, number) + + local viewportSize = workspace.CurrentCamera.ViewportSize; + local selectedProperties; + for _, properties in array do + + local isDefault = not properties.minimumHeight and not properties.minimumWidth; + local isAtOrBelowMinimumHeight = properties.minimumHeight and properties.minimumHeight >= viewportSize.Y; + local isAtOrBelowMinimumWidth = properties.minimumWidth and properties.minimumWidth >= viewportSize.X; + if isDefault or (isAtOrBelowMinimumHeight and isAtOrBelowMinimumWidth) then + + selectedProperties = properties; + + end; + + end; + + assert(selectedProperties, "No sizes available for current viewport."); + + return selectedProperties.sizeX, selectedProperties.sizeY, selectedProperties.textSize; + + end; + + local defaultX, defaultY, defaultTextSize = getSizeBasedOnViewport(); + local sizeX, setSizeX = React.useState(defaultX); + local sizeY, setSizeY = React.useState(defaultY); + local textSize, setTextSize = React.useState(defaultTextSize); + + React.useEffect(function() + + local function updateSize() + + local newX, newY, newTextSize = getSizeBasedOnViewport(); + + setSizeX(newX); + setSizeY(newY); + setTextSize(newTextSize); + + end; + + local viewportChangedEvent = workspace.CurrentCamera:GetAttributeChangedSignal("ViewportChanged"):Connect(updateSize); + + return function() + + viewportChangedEvent:Disconnect(); + + end; + + end, {}); + + return sizeX, sizeY, textSize; + +end + +return useDynamicSize; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactHooks/useKeybindContinue.lua b/src/DialogueClientScript/ReactHooks/useKeybindContinue.lua new file mode 100644 index 0000000..63e776c --- /dev/null +++ b/src/DialogueClientScript/ReactHooks/useKeybindContinue.lua @@ -0,0 +1,41 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local ContextActionService = game:GetService("ContextActionService"); +local UserInputService = game:GetService("UserInputService"); +local Types = require(script.Parent.Parent.Types); +type ClientSettings = Types.ClientSettings; + +function useKeybindContinue(clientSettings: ClientSettings, continueDialogueFunction: () -> ()) + + React.useEffect(function(): () + + local defaultChatContinueKey = clientSettings.defaultChatContinueKey; + local defaultChatContinueKeyGamepad = clientSettings.defaultChatContinueKeyGamepad; + + if clientSettings.keybindsEnabled then + + local function checkKeybinds(keybind: Enum.KeyCode) + + if keybind and not UserInputService:IsKeyDown(defaultChatContinueKey) and not UserInputService:IsKeyDown(defaultChatContinueKeyGamepad) then + + continueDialogueFunction(); + + end; + + end; + + ContextActionService:BindAction("ContinueDialogue", checkKeybinds, false, defaultChatContinueKey, defaultChatContinueKeyGamepad); + + return function() + + ContextActionService:UnbindAction("ContinueDialogue"); + + end; + + end; + + end, {clientSettings}); + +end; + +return useKeybindContinue; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactHooks/useLookAtPlayer.lua b/src/DialogueClientScript/ReactHooks/useLookAtPlayer.lua new file mode 100644 index 0000000..6b7ab5f --- /dev/null +++ b/src/DialogueClientScript/ReactHooks/useLookAtPlayer.lua @@ -0,0 +1,75 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Types = require(script.Parent.Parent.Types); +local Player = game:GetService("Players").LocalPlayer; +local TweenService = game:GetService("TweenService"); +type ClientSettings = Types.ClientSettings; +type NPCSettings = Types.NPCSettings; + +local function useLookAtPlayer(npc: Model, npcSettings: NPCSettings) + + React.useEffect(function(): () + + -- Check if the NPC needs to look at the player. + if npcSettings.general.npcLooksAtPlayerDuringDialogue and npcSettings.general.npcNeckRotationMaxY then + + -- Handle this in a coroutine because the look shouldn't stop the dialogue. + local lookTask = task.spawn(function() + + local NPCHead: BasePart? = npc:FindFirstChild("Head") :: BasePart; + local NPCPrimaryPart: BasePart? = npc.PrimaryPart :: BasePart; + local NPCHumanoid: Humanoid? = npc:FindFirstChild("Humanoid") :: Humanoid; + local NPCTorso: BasePart? = NPCHumanoid and NPCHumanoid.RigType == Enum.HumanoidRigType.R6 and (npc:FindFirstChild("Torso") :: BasePart) or nil; + local NPCNeckParent = NPCTorso or NPCHead; + local NPCNeck: Motor6D? = NPCNeckParent and NPCNeckParent:FindFirstChild("Neck") :: Motor6D; + local PlayerCharacter: Model? = Player.Character; + local PlayerHead: BasePart? = (PlayerCharacter and PlayerCharacter:FindFirstChild("Head") :: BasePart); + if NPCNeck then + + -- Set the base position. + NPCNeck.C0 = CFrame.new(NPCNeck.C0.Position) * CFrame.fromOrientation(0, 0, 0); + NPCNeck.C1 = CFrame.new(NPCNeck.C1.Position) * CFrame.fromOrientation(0, 0, 0); + local OriginalC0 = NPCNeck.C0; + local OriginalC1 = NPCNeck.C1; + + while NPCPrimaryPart and NPCHead and NPCNeck and PlayerHead and task.wait() do + + local maxRotationX = npcSettings.general.npcNeckRotationMaxX; + local maxRotationY = npcSettings.general.npcNeckRotationMaxY; + local maxRotationZ = npcSettings.general.npcNeckRotationMaxZ; + local goalRotationX, goalRotationY, goalRotationZ = CFrame.new(NPCHead.Position, PlayerHead.Position):ToOrientation(); + local rotationOffsetX = goalRotationX - math.rad(NPCPrimaryPart.Orientation.X); + local rotationOffsetY = goalRotationY - math.rad(NPCPrimaryPart.Orientation.Y); + local rotationOffsetZ = goalRotationZ - math.rad(NPCPrimaryPart.Orientation.Z); + local rotationXAbs = math.abs(rotationOffsetX); + local rotationYAbs = math.abs(rotationOffsetY); + local rotationZAbs = math.abs(rotationOffsetZ); + TweenService:Create(NPCNeck, TweenInfo.new(0.3), { + C0 = CFrame.new(NPCNeck.C0.Position) * CFrame.fromOrientation( + ((rotationXAbs > maxRotationX and maxRotationX * (rotationOffsetX / rotationXAbs) * ((rotationXAbs > math.pi and -1) or 1)) or rotationOffsetX), + ((rotationYAbs > maxRotationY and maxRotationY * (rotationOffsetY / rotationYAbs) * ((rotationYAbs > math.pi and -1) or 1)) or rotationOffsetY), + ((rotationZAbs > maxRotationZ and maxRotationZ * (rotationOffsetZ / rotationZAbs) * ((rotationZAbs > math.pi and -1) or 1)) or rotationOffsetZ) + ) + }):Play(); + + end + + TweenService:Create(NPCNeck, TweenInfo.new(0.3), {C0 = OriginalC0, C1 = OriginalC1}):Play(); + + end + + end); + + return function() + + task.cancel(lookTask); + + end; + + end + + end, {}); + +end; + +return useLookAtPlayer; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactHooks/useOutOfDistanceDetection.lua b/src/DialogueClientScript/ReactHooks/useOutOfDistanceDetection.lua new file mode 100644 index 0000000..2dced93 --- /dev/null +++ b/src/DialogueClientScript/ReactHooks/useOutOfDistanceDetection.lua @@ -0,0 +1,43 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Types = require(script.Parent.Parent.Types); +type NPCSettings = Types.NPCSettings; +local Players = game:GetService("Players"); + +local function useOutOfDistanceDetection(npc: Model, npcSettings: NPCSettings, endConversationFunction: () -> ()) + + React.useEffect(function(): () + + local NPCPrimaryPart = npc.PrimaryPart; + local MaxConversationDistance = npcSettings.general.maxConversationDistance; + local EndConversationIfOutOfDistance = npcSettings.general.endConversationIfOutOfDistance; + if EndConversationIfOutOfDistance and MaxConversationDistance and NPCPrimaryPart then + + local detectionTask = task.spawn(function() + + while task.wait() do + + if math.abs(NPCPrimaryPart.Position.Magnitude - Players.LocalPlayer.Character.PrimaryPart.Position.Magnitude) > MaxConversationDistance then + + endConversationFunction(); + break; + + end; + + end; + + end); + + return function() + + task.cancel(detectionTask); + + end; + + end; + + end, {npc :: any, npcSettings, endConversationFunction}); + +end; + +return useOutOfDistanceDetection; \ No newline at end of file diff --git a/src/DialogueClientScript/ReactHooks/usePages.lua b/src/DialogueClientScript/ReactHooks/usePages.lua new file mode 100644 index 0000000..17e5036 --- /dev/null +++ b/src/DialogueClientScript/ReactHooks/usePages.lua @@ -0,0 +1,33 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local DialogueAPI = require(script.Parent.Parent.API.Dialogue); +local Types = require(script.Parent.Parent.Types); +type Page = Types.Page; + +local function usePages(dialogueContentArray, textContainerRef: any) + + local pages, setPages = React.useState({} :: {Page}); + + React.useEffect(function() + + local textContainer = textContainerRef.current; + if textContainer and textContainer.Parent then + + local testTextContainer = textContainer:Clone(); + local testTextSegment = testTextContainer:FindFirstChild("TestSegment"); + testTextContainer.Parent = textContainer.Parent; + + local pages = DialogueAPI:getPages(dialogueContentArray, testTextSegment); + setPages(pages); + + testTextContainer:Destroy(); + + end; + + end, {dialogueContentArray, textContainerRef}); + + return pages; + +end; + +return usePages; \ No newline at end of file diff --git a/src/DialogueClientScript/Settings.lua b/src/DialogueClientScript/Settings.lua new file mode 100644 index 0000000..e3c3ca1 --- /dev/null +++ b/src/DialogueClientScript/Settings.lua @@ -0,0 +1,24 @@ +--!strict +local Types = require(script.Parent.Types); +type ClientSettings = Types.ClientSettings; + +local Settings: ClientSettings = { + + -- [ Theme Settings ] -- + defaultTheme = "BareBonesTheme"; + + -- [ Response Settings ] -- + showResponsesAfterMessageFinished = true; + defaultClickSound = 0; + + -- [ Chat Triggers and Keybinds ] -- + minimumDistanceFromCharacter = 10; + keybindsEnabled = true; + defaultChatTriggerKey = Enum.KeyCode.F; + defaultChatTriggerKeyGamepad = Enum.KeyCode.ButtonX; + defaultChatContinueKey = Enum.KeyCode.F; + defaultChatContinueKeyGamepad = Enum.KeyCode.ButtonA; + +}; + +return Settings; \ No newline at end of file diff --git a/src/DialoguePluginScript/DialogueClientScript/Types.lua b/src/DialogueClientScript/Types.lua similarity index 50% rename from src/DialoguePluginScript/DialogueClientScript/Types.lua rename to src/DialogueClientScript/Types.lua index f615ca1..1f1ecf6 100644 --- a/src/DialoguePluginScript/DialogueClientScript/Types.lua +++ b/src/DialogueClientScript/Types.lua @@ -4,7 +4,7 @@ export type Effect = { type: "effect"; - run: (isPlayerSkipping: boolean) -> any; + run: (skipPageEvent: BindableEvent?) -> any; getMaxDimensions: () -> {x: number, y: number}; @@ -16,6 +16,15 @@ export type Effect = { } +export type UseEffectFunction = (effectName: string, effectProperties: {[string]: any}) -> Effect; + +export type RichTextTagInformation = { + attributes: string?; + endOffset: number?; + name: string; + startOffset: number; +} + export type NPCSettings = { general: { @@ -100,15 +109,52 @@ export type NPCSettings = { }; -export type UseEffectFunction = (effectName: string, effectProperties: {[string]: any}) -> Effect; +export type ClientSettings = { -export type RichTextTagInformation = { - attributes: string?; - endOffset: number?; - name: string; - startOffset: number; + -- This is the default theme that will be used when talking with NPCs + defaultTheme: string; + + -- Prevents the player from selecting responses without first viewing the dialogue + showResponsesAfterMessageFinished: boolean; + + -- Replace this with an audio ID that'll play every time a player continues a conversation or selects a response. Replace with 0 to not play any sound. + defaultClickSound: number; + + -- Minimum distance from a character required for keybinds should work + minimumDistanceFromCharacter: number; + + -- Whether keybinds should work + keybindsEnabled: boolean; + + -- Keyboard keybind to start a conversation with an NPC + defaultChatTriggerKey: Enum.KeyCode; + + -- Gamepad keybind to start a conversation with an NPC + defaultChatTriggerKeyGamepad: Enum.KeyCode; + + -- Keyboard keybind to continue a conversation with an NPC + defaultChatContinueKey: Enum.KeyCode; + + -- Gamepad keybind to continue a conversation with an NPC + defaultChatContinueKeyGamepad: Enum.KeyCode; +}; + +export type ThemeProperties = { + responseContentScripts: {ModuleScript}; + clientSettings: ClientSettings; + npcSettings: NPCSettings; + dialogueContentArray: any; + npc: Model; + onComplete: (selectedResponseContentScript: ModuleScript?) -> (); + onTimeout: () -> (); } -export type Page = {{type: "text"; text: string; size: UDim2} | Effect}; +export type Page = { + { + type: "text"; + text: string; + size: Vector2; + } | Effect +}; return {}; diff --git a/src/DialoguePluginScript/DialogueClientScript.lua b/src/DialogueClientScript/init.client.lua similarity index 64% rename from src/DialoguePluginScript/DialogueClientScript.lua rename to src/DialogueClientScript/init.client.lua index 051df42..9c54d5e 100644 --- a/src/DialoguePluginScript/DialogueClientScript.lua +++ b/src/DialogueClientScript/init.client.lua @@ -1,12 +1,11 @@ --!strict local Players = game:GetService("Players"); -local ControllerService = game:GetService("ControllerService"); local RunService = game:GetService("RunService"); local ContextActionService = game:GetService("ContextActionService"); local UserInputService = game:GetService("UserInputService"); +local CollectionService = game:GetService("CollectionService"); local Player = game:GetService("Players").LocalPlayer; local PlayerGui = Player:WaitForChild("PlayerGui"); -local ReplicatedStorage = game:GetService("ReplicatedStorage"); local APIFolder = script.API; local api = { dialogue = require(APIFolder.Dialogue); @@ -19,27 +18,27 @@ local Types = require(script.Types); local function readDialogue(NPC: Model, npcSettings: Types.NPCSettings) -- Make sure we can't talk to another NPC - api.triggers.disableAllSpeechBubbles(); - api.triggers.disableAllClickDetectors(); - api.triggers.disableAllProximityPrompts(); + api.triggers:disableAllSpeechBubbles(); + api.triggers:disableAllClickDetectors(); + api.triggers:disableAllProximityPrompts(); local freezePlayer = npcSettings.general.freezePlayer; if freezePlayer then - api.player.freezePlayer(); + api.player:freezePlayer(); end; -- Let the Dialogue module handle it. - api.dialogue.readDialogue(NPC, npcSettings); + api.dialogue:readDialogue(NPC, npcSettings); -- Clean up. - api.triggers.enableAllSpeechBubbles(); - api.triggers.enableAllClickDetectors(); - api.triggers.enableAllProximityPrompts(); + api.triggers:enableAllSpeechBubbles(); + api.triggers:enableAllClickDetectors(); + api.triggers:enableAllProximityPrompts(); if freezePlayer then - api.player.unfreezePlayer(); + api.player:unfreezePlayer(); end; @@ -47,50 +46,30 @@ end -- Iterate through every NPC print("[Dialogue Maker]: Preparing dialogue received from the server..."); -for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do +for _, npc in CollectionService:GetTagged("DialogueMakerNPC") do -- Make sure all NPCs aren't affected if this one doesn't load properly - if not NPCLocation:IsA("ObjectValue") then - - warn("[Dialogue Maker] " .. NPCLocation.Name .. " is not an ObjectValue. Skipping..."); - continue; - - end; - - local NPC: Model = NPCLocation.Value :: Model; - if not NPC then - - warn("[Dialogue Maker] " .. NPCLocation.Name .. " does not have a Value. Skipping..."); - continue; - - elseif not NPC:IsA("Model") then - - warn("[Dialogue Maker] " .. NPC.Name .. "'s Value is not a Model. Skipping..."); - continue; - - end - local success, msg = pcall(function() -- Set up speech bubbles. - local dialogueSettings = require(NPC:FindFirstChild("NPCDialogueSettings")) :: Types.NPCSettings; + local dialogueSettings = require(npc:FindFirstChild("NPCDialogueSettings")) :: Types.NPCSettings; if dialogueSettings.speechBubble.enabled then - local SpeechBubblePart = dialogueSettings.speechBubble.BasePart; + local SpeechBubblePart = dialogueSettings.speechBubble.location; if SpeechBubblePart and SpeechBubblePart:IsA("BasePart") then -- Listen if the player clicks the speech bubble - local SpeechBubble = api.triggers.createSpeechBubble(NPC, dialogueSettings); + local SpeechBubble = api.triggers:createSpeechBubble(npc, dialogueSettings); (SpeechBubble:FindFirstChild("SpeechBubbleButton") :: ImageButton).MouseButton1Click:Connect(function() - readDialogue(NPC, dialogueSettings); + readDialogue(npc, dialogueSettings); end); SpeechBubble.Parent = PlayerGui; else - warn("[Dialogue Maker]: The SpeechBubblePart for " .. NPC.Name .. " is not a Part."); + warn("[Dialogue Maker]: The SpeechBubblePart for " .. npc.Name .. " is not a Part."); end; @@ -99,7 +78,7 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do -- Next, the prompt regions. if dialogueSettings.promptRegion.enabled then - local PromptRegionPart = dialogueSettings.promptRegion.BasePart; + local PromptRegionPart = dialogueSettings.promptRegion.location; if PromptRegionPart and PromptRegionPart:IsA("BasePart") then PromptRegionPart.Touched:Connect(function(part) @@ -108,7 +87,7 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do local PlayerFromCharacter = Players:GetPlayerFromCharacter(part.Parent); if PlayerFromCharacter == Player then - api.dialogue.readDialogue(NPC, dialogueSettings); + api.dialogue:readDialogue(npc, dialogueSettings); end; @@ -116,7 +95,7 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do else - warn("[Dialogue Maker]: The PromptRegionPart for " .. NPC.Name .. " is not a Part."); + warn("[Dialogue Maker]: The PromptRegionPart for " .. npc.Name .. " is not a Part."); end; @@ -125,28 +104,28 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do -- Now, the proximity prompts. if dialogueSettings.proximityPrompt.enabled then - local ProximityPrompt = dialogueSettings.proximityPrompt.Instance; + local ProximityPrompt = dialogueSettings.proximityPrompt.location; if dialogueSettings.proximityPrompt.autoCreate then local ProximityPromptTemp = Instance.new("ProximityPrompt"); - ProximityPromptTemp.Parent = NPC; + ProximityPromptTemp.Parent = npc; ProximityPrompt = ProximityPromptTemp; end; if ProximityPrompt and ProximityPrompt:IsA("ProximityPrompt") then - api.triggers.addProximityPrompt(NPC, ProximityPrompt); + api.triggers:addProximityPrompt(npc, ProximityPrompt); ProximityPrompt.Triggered:Connect(function() - readDialogue(NPC, dialogueSettings); + readDialogue(npc, dialogueSettings); end); else - warn("[Dialogue Maker]: The proximity prompt location for " .. NPC.Name .. " is not a ProximityPrompt."); + warn("[Dialogue Maker]: The proximity prompt location for " .. npc.Name .. " is not a ProximityPrompt."); end; @@ -155,28 +134,28 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do -- Almost there: it's time for the click detectors. if dialogueSettings.clickDetector.enabled then - local ClickDetector = dialogueSettings.clickDetector.Instance; + local ClickDetector = dialogueSettings.clickDetector.location; if dialogueSettings.clickDetector.autoCreate then local ClickDetectorTemp = Instance.new("ClickDetector"); - ClickDetectorTemp.Parent = NPC; + ClickDetectorTemp.Parent = npc; ClickDetector = ClickDetectorTemp; end; if ClickDetector and ClickDetector:IsA("ClickDetector") then - api.triggers.addClickDetector(NPC, ClickDetector); + api.triggers:addClickDetector(npc, ClickDetector); ClickDetector.MouseClick:Connect(function() - readDialogue(NPC, dialogueSettings); + readDialogue(npc, dialogueSettings); end); else - warn("[Dialogue Maker]: The ClickDetectorLocation for " .. NPC.Name .. " is not a ClickDetector."); + warn("[Dialogue Maker]: The ClickDetectorLocation for " .. npc.Name .. " is not a ClickDetector."); end; @@ -193,7 +172,7 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do if CanPressButton and (UserInputService:IsKeyDown(defaultChatTriggerKey) or UserInputService:IsKeyDown(defaultChatTriggerKeyGamepad)) then - readDialogue(NPC, dialogueSettings); + readDialogue(npc, dialogueSettings); end; @@ -203,7 +182,7 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do -- Check if the player is in range RunService.Heartbeat:Connect(function() - CanPressButton = Player:DistanceFromCharacter(NPC:GetPivot().Position) < clientSettings.minimumDistanceFromCharacter; + CanPressButton = Player:DistanceFromCharacter(npc:GetPivot().Position) < clientSettings.minimumDistanceFromCharacter; end); @@ -214,7 +193,7 @@ for _, NPCLocation: ObjectValue in ipairs(script.NPCLocations:GetChildren()) do -- One NPC doesn't stop the show, but it's important for you to know which ones didn't load properly. if not success then - warn("[Dialogue Maker]: Couldn't load NPC " .. NPC.Name .. ": " .. msg); + warn("[Dialogue Maker]: Couldn't load NPC " .. npc.Name .. ": " .. msg); end; diff --git a/src/DialogueClientScript/init.meta.json b/src/DialogueClientScript/init.meta.json new file mode 100644 index 0000000..54f4692 --- /dev/null +++ b/src/DialogueClientScript/init.meta.json @@ -0,0 +1,5 @@ +{ + "properties": { + "Disabled": true + } +} \ No newline at end of file diff --git a/src/DialoguePluginScript.lua b/src/DialoguePluginScript.lua deleted file mode 100644 index add85a1..0000000 --- a/src/DialoguePluginScript.lua +++ /dev/null @@ -1,839 +0,0 @@ ---!strict -local Selection = game:GetService("Selection"); -local StarterPlayer = game:GetService("StarterPlayer"); -local StarterPlayerScripts = StarterPlayer.StarterPlayerScripts; -local ChangeHistoryService = game:GetService("ChangeHistoryService"); - --- Make sure we have all of the plugin GUI stuff. -local DialogueMakerFrame: Frame = script.DialogueMakerGUI.MainFrame:Clone(); -assert(DialogueMakerFrame, "[Dialogue Maker] Couldn't start because DialogueMakerGUI lacks a MainFrame. Try reinstalling the plugin."); - -local DialogueContainer: Frame = DialogueMakerFrame:FindFirstChild("DialogueContainer") :: Frame; -assert(DialogueContainer, "[Dialogue Maker] Couldn't start because MainFrame lacks a DialogueContainer. Try reinstalling the plugin."); - -local DialogueMessageList: ScrollingFrame = DialogueContainer:FindFirstChild("DialogueMessageList") :: ScrollingFrame; -assert(DialogueMessageList, "[Dialogue Maker] Couldn't start because DialogueContainer lacks a DialogueMessageList. Try reinstalling the plugin."); - -local DialogueMessageTemplate: Frame = DialogueMessageList:FindFirstChild("DialogueMessageTemplate") :: Frame; -assert(DialogueMessageList, "[Dialogue Maker] Couldn't start because DialogueMessageList lacks a DialogueMessageTemplate. Try reinstalling the plugin."); -local DialogueMessageTemplateClone: Frame = (DialogueMessageTemplate :: Frame):Clone(); -(DialogueMessageTemplate :: Frame):Destroy(); -DialogueMessageTemplate = DialogueMessageTemplateClone; - -local Tools: Frame = DialogueMakerFrame:FindFirstChild("Tools") :: Frame; -assert(Tools, "[Dialogue Maker] Couldn't start because DialogueMakerGUI lacks Tools. Try reinstalling the plugin."); - -local CurrentDialogueContainer: ModuleScript?; -local Model; -local viewingPriority = ""; -local function repairNPC(): () - - if not Model:FindFirstChild("DialogueContainer") then - - -- Add the dialogue container to the NPC - local DialogueContainer = Instance.new("Folder"); - DialogueContainer.Name = "DialogueContainer"; - - -- Add the dialogue folder to the model - DialogueContainer.Parent = Model; - viewingPriority = ""; - - return; - - end; - - CurrentDialogueContainer = Model:FindFirstChild("DialogueContainer") :: ModuleScript; - assert(CurrentDialogueContainer, "[Dialogue Maker] DialogueContainer not found..."); - - if not Model:FindFirstChild("NPCDialogueSettings") then - - print("[Dialogue Maker] Adding settings script to "..Model.Name) - - local SettingsScript = script.NPCSettingsTemplate:Clone(); - SettingsScript.Name = "NPCDialogueSettings"; - SettingsScript.Parent = Model; - - print("[Dialogue Maker] Added settings script to "..Model.Name) - - end; - - -- Initialize DialogueLocations. - local DialogueClientScript = StarterPlayerScripts:FindFirstChild("DialogueClientScript"); - - assert(DialogueClientScript, "[Dialogue Maker] DialogueClientScript wasn't found in the StarterPlayerScripts! \nPlease replace the script by pressing the \"Fix Scripts\" button."); - - for _, dialogueLocation in ipairs(DialogueClientScript.DialogueLocations:GetChildren()) do - - if dialogueLocation.Value == Model then - - return; - - end - - end; - - local DialogueLocation = Instance.new("ObjectValue"); - DialogueLocation.Value = Model; - DialogueLocation.Name = "DialogueLocation"; - DialogueLocation.Parent = DialogueClientScript.DialogueLocations; - -end; - -type EventTypes = { - AddMessage: RBXScriptConnection?; - AdjustSettingsRequested: RBXScriptConnection?; - AttributeChanged: {RBXScriptConnection?}; - ChildAdded: RBXScriptConnection?; - ChildRemoved: RBXScriptConnection?; - DeleteMode: RBXScriptConnection?; - DeleteYesButton: RBXScriptConnection?; - DeleteNoButton: RBXScriptConnection?; - PriorityFocusLost: {RBXScriptConnection?}; - TypeDropdown: {RBXScriptConnection?}; - ViewChildren: {RBXScriptConnection?}; - ViewContent: {RBXScriptConnection?}; - ViewParent: RBXScriptConnection?; -}; - -local Events: EventTypes = { - AttributeChanged = {}; - PriorityFocusLost = {}; - TypeDropdown = {}; - ViewChildren = {}; - ViewContent = {}; -}; - -local Toolbar = plugin:CreateToolbar("Dialogue Maker by Beastslash"); -local EditDialogueButton = Toolbar:CreateButton("Edit Dialogue", "Edit dialogue of a selected NPC. The selected object must be a singular model.", "rbxassetid://14109181603"); -local isDeleteModeEnabled = false; -local DeletePromptShown = false; -local isDialogueEditorOpen = false; -local ViewStatus = DialogueMakerFrame:FindFirstChild("ViewStatus"); -local DialogueLocationStatus = ViewStatus:FindFirstChild("DialogueLocationStatus") :: TextLabel; -local ModelLocationFrame = ViewStatus:FindFirstChild("ModelLocationFrame"); - -type DialogueContainerClass = ModuleScript | Folder; - -local function disconnectEvents(): () - - for key, PossibleEvent: RBXScriptConnection | {[number]: RBXScriptConnection} in pairs(Events) do - - if typeof(PossibleEvent) == "table" then - - for _, event in ipairs(PossibleEvent) do - - event:Disconnect() - - end - Events[key] = {}; - - else - - PossibleEvent:Disconnect(); - Events[key] = nil; - - end; - - end; - -end - --- Refreshes all events and GUI elements in the plugin. --- @since v1.0.0 -local function syncDialogueGUI(DirectoryContentScript: DialogueContainerClass): () - - -- Make sure everything with the NPC is OK - repairNPC(); - - -- Disconnect any past event - disconnectEvents(); - - -- Hook some button events - assert(CurrentDialogueContainer, "[Dialogue Maker] CurrentDialogueContainer not found"); - Events.AdjustSettingsRequested = (Tools:FindFirstChild("AdjustSettings") :: TextButton).MouseButton1Click:Connect(function() - - -- Make sure all of the important objects are in the NPC - repairNPC(); - - plugin:OpenScript(Model:FindFirstChild("NPCDialogueSettings")); - - end); - - Events.AddMessage = (Tools:FindFirstChild("AddMessage") :: TextButton).MouseButton1Click:Connect(function() - - local CurrentDirectory = CurrentDialogueContainer; - - if viewingPriority ~= "" then - - local path = viewingPriority:split("."); - for _, directory in ipairs(path) do - - CurrentDirectory = CurrentDirectory:FindFirstChild(directory) :: ModuleScript; - - end; - - end - - -- Create the dialogue script. - local targetPriority = 1; - for _, Child in ipairs(CurrentDirectory:GetChildren()) do - - local comparedName = tonumber(Child.Name); - if comparedName and comparedName >= targetPriority then - - targetPriority = comparedName + 1; - - end - - end; - - local MessageContentScript = script.ContentTemplate:Clone(); - MessageContentScript.Name = targetPriority; - MessageContentScript:SetAttribute("DialogueType", "Message"); - MessageContentScript.Parent = CurrentDirectory; - - -- Now let's re-order the dialogue - syncDialogueGUI(CurrentDirectory); - - end); - - if viewingPriority == "" then - - DialogueLocationStatus.Text = "Viewing the beginning of the conversation"; - - else - - DialogueLocationStatus.Text = "Viewing " .. viewingPriority; - - local ViewParentButton = Tools:FindFirstChild("ViewParent") :: TextButton; - local ViewParentTextLabel = ViewParentButton:FindFirstChild("TextLabel") :: TextLabel; - local ViewParentImageLabel = ViewParentButton:FindFirstChild("ImageLabel") :: ImageLabel; - Events.ViewParent = ViewParentButton.MouseButton1Click:Connect(function() - - local GRAY = Color3.fromRGB(149, 149, 149); - ViewParentTextLabel.TextColor3 = GRAY; - ViewParentImageLabel.ImageColor3 = GRAY; - ViewParentButton.BackgroundTransparency = 1; - - local NewViewingPriority = viewingPriority:split("."); - NewViewingPriority[#NewViewingPriority] = nil; - viewingPriority = table.concat(NewViewingPriority,"."); - - syncDialogueGUI(DirectoryContentScript.Parent :: DialogueContainerClass); - - end); - - local WHITE = Color3.fromRGB(255, 255, 255); - ViewParentTextLabel.TextColor3 = WHITE; - ViewParentImageLabel.ImageColor3 = WHITE; - ViewParentButton.BackgroundTransparency = 0; - - end; - - local DeleteModeButton = Tools:FindFirstChild("DeleteMode") :: TextButton; - local DeleteModeImageLabel = DeleteModeButton:FindFirstChild("ImageLabel") :: ImageLabel; - local DeleteModeTextLabel = DeleteModeButton:FindFirstChild("TextLabel") :: TextLabel; - Events.DeleteMode = DeleteModeButton.MouseButton1Click:Connect(function() - - -- Toggle delete mode and tell the current status to the developer. - isDeleteModeEnabled = not isDeleteModeEnabled; - - DeleteModeButton.BackgroundColor3 = if isDeleteModeEnabled then Color3.fromRGB(217, 39, 39) else Color3.fromRGB(74, 74, 74); - - print("[Dialogue Maker] " .. if isDeleteModeEnabled then "Warning: Delete Mode has been enabled!" else "Whew. Delete Mode has been disabled."); - - end); - - print("[Dialogue Maker] " .. DialogueLocationStatus.Text); - - -- Clean up the old dialogue - for _, status in ipairs(DialogueMessageList:GetChildren()) do - - if not status:IsA("UIListLayout") then - - status:Destroy(); - - end; - - end; - - -- Separate the dialogue item types. - local responses: {ModuleScript} = {}; - local messages: {ModuleScript} = {}; - local redirects: {ModuleScript} = {}; - for _, PossibleDialogueItem in ipairs(DirectoryContentScript:GetChildren()) do - - if PossibleDialogueItem:IsA("ModuleScript") and tonumber(PossibleDialogueItem.Name) then - - -- Get the dialogue item type. - local DialogueType = PossibleDialogueItem:GetAttribute("DialogueType"); - table.insert(if DialogueType == "Response" then responses elseif DialogueType == "Message" then messages else redirects, PossibleDialogueItem); - - end - - end - - -- Sort the directory based on priority - local function sortByMessagePriority(dialogueA: ModuleScript, dialogueB: ModuleScript) - - local messageAPriority = tonumber(dialogueA.Name) or math.huge; - local messageBPriority = tonumber(dialogueB.Name) or math.huge; - - return messageAPriority < messageBPriority; - - end; - - table.sort(responses, sortByMessagePriority); - table.sort(messages, sortByMessagePriority); - table.sort(redirects, sortByMessagePriority); - - -- Listen for instance family changes. - local function refreshDialogueGUI() - - syncDialogueGUI(DirectoryContentScript) - - end; - - Events.ChildAdded = DirectoryContentScript.ChildAdded:Connect(refreshDialogueGUI); - Events.ChildRemoved = DirectoryContentScript.ChildRemoved:Connect(refreshDialogueGUI); - - -- Create new status - local currentZIndex = #responses + #messages + #redirects; - for _, category in ipairs({responses, messages, redirects}) do - - for _, ContentScript in ipairs(category) do - - -- Make sure the message container is completely visible, even when dropdowns are open. - local DialogueMessageContainer = DialogueMessageTemplate:Clone(); - DialogueMessageContainer.ZIndex = currentZIndex; - currentZIndex -= 1; - DialogueMessageContainer.Visible = true; - DialogueMessageContainer.Parent = DialogueMessageList; - - -- Set the message priority. - local DialogueMessageContainerChildContainer = DialogueMessageContainer:FindFirstChild("Container") :: Frame; - local DialogueMessagePriorityTextBox = DialogueMessageContainerChildContainer:FindFirstChild("Priority") :: TextBox; - local splitPriority = viewingPriority:split("."); - if viewingPriority == "" then table.remove(splitPriority, 1) end; - table.insert(splitPriority, ContentScript.Name); - DialogueMessagePriorityTextBox.PlaceholderText = splitPriority[#splitPriority]; - DialogueMessagePriorityTextBox.Text = splitPriority[#splitPriority]; - - -- Set up what to do if the priority changes. - table.insert(Events.PriorityFocusLost, DialogueMessagePriorityTextBox.FocusLost:Connect(function(input) - - -- Make sure the priority is valid - local isUserTextInvalid = false; - local userText = DialogueMessagePriorityTextBox.Text; - if userText:sub(1, 1) == "." or userText:sub(userText:len()) == "." then - - isUserTextInvalid = true; - - end; - - local CurrentDirectory = CurrentDialogueContainer; - splitPriority = DialogueMessagePriorityTextBox.Text:split("."); - if not isUserTextInvalid then - - for index, priority in ipairs(splitPriority) do - - -- Make sure everyone's a number - if not tonumber(priority) then - - warn("[Dialogue Maker] " .. DialogueMessagePriorityTextBox.Text .. " is not a valid priority. Make sure you're not using any characters other than numbers and periods."); - isUserTextInvalid = true; - break; - - end; - - -- Make sure the folder exists - local TargetDirectory = CurrentDirectory:FindFirstChild(priority); - if not TargetDirectory and index ~= #splitPriority then - - warn("[Dialogue Maker] " .. DialogueMessagePriorityTextBox.Text .. " is not a valid priority. Make sure all parent directories exist."); - isUserTextInvalid = true; - break; - - elseif index == #splitPriority then - - if TargetDirectory then - - warn("[Dialogue Maker] " .. DialogueMessagePriorityTextBox.Text .. " is not a valid priority. Make sure that " .. DialogueMessagePriorityTextBox.Text .. " isn't already being used."); - isUserTextInvalid = true; - - else - - CurrentDirectory = ContentScript; - local UserSplitPriority = DialogueMessagePriorityTextBox.Text:split("."); - ContentScript.Name = UserSplitPriority[#UserSplitPriority]; - ContentScript.Parent = CurrentDirectory; - - end; - break; - - end; - - CurrentDirectory = CurrentDirectory:FindFirstChild(priority) :: ModuleScript; - - end; - - end; - - -- Refresh the GUI - refreshDialogueGUI(); - - end)); - - -- Add functionality to create special scripts, like actions and conditions. - local function openSpecialScript(Folder: Folder, Template: ModuleScript): () - - -- Search through the script list - local SpecialScript; - for _, PossibleSpecialScript in ipairs(Folder:GetChildren()) do - - if PossibleSpecialScript:IsA("ModuleScript") and (PossibleSpecialScript:FindFirstChild("ContentScript") :: ObjectValue).Value == ContentScript then - - SpecialScript = PossibleSpecialScript; - break; - - end; - - end; - - if not SpecialScript then - - local TempSpecialScript = Template:Clone(); - TempSpecialScript.Name = table.concat(splitPriority, "."); - (TempSpecialScript:FindFirstChild("ContentScript") :: ObjectValue).Value = ContentScript; - TempSpecialScript.Parent = Folder; - SpecialScript = TempSpecialScript; - - end; - - -- Open the condition script - plugin:OpenScript(SpecialScript); - - end; - - local ViewChildrenButton = DialogueMessageContainerChildContainer:FindFirstChild("ViewChildren") :: TextButton; - local OpenScriptsButton = DialogueMessageContainerChildContainer:FindFirstChild("OpenScripts"); - local OpenScriptsList = OpenScriptsButton:FindFirstChild("List"); - local ConditionButton = OpenScriptsList:FindFirstChild("Condition") :: TextButton; - ConditionButton.MouseButton1Click:Connect(function() - - openSpecialScript(StarterPlayerScripts.DialogueClientScript.Conditions, script.ConditionTemplate); - - end); - - -- Asks the user if they want to delete this message. - -- @since v1.0.0 - local function showDeleteModePrompt(): () - - if DeletePromptShown then return; end; - - DeletePromptShown = true; - - -- Show the deletion options to the user - local DeleteFrame = DialogueMessageContainer:FindFirstChild("DeleteFrame") :: Frame; - DeleteFrame.Visible = true; - - -- Add the deletion functionality - Events.DeleteYesButton = (DeleteFrame:FindFirstChild("YesButton") :: TextButton).MouseButton1Click:Connect(function() - - -- Hide the deletion options from the user - DeleteFrame.Visible = false; - - -- Delete the dialogue - ContentScript:Destroy(); - - -- Allow the user to continue using the plugin - DeletePromptShown = false; - - end); - - -- Give the user the option to back out - Events.DeleteNoButton = (DeleteFrame:FindFirstChild("NoButton") :: TextButton).MouseButton1Click:Connect(function() - - -- Debounce - if Events.DeleteNoButton then Events.DeleteNoButton:Disconnect() end; - - -- Hide the deletion options from the user - DeleteFrame.Visible = false; - - -- Allow the user to continue using the plugin - DeletePromptShown = false; - - end); - - end; - - -- Find the dialogue type and adjust accordingly. - local dialogueType = ContentScript:GetAttribute("DialogueType"); - local isResponse = dialogueType == "Response"; - local isRedirect = dialogueType == "Redirect"; - local ActionButton = OpenScriptsList:FindFirstChild("Action") :: TextButton; - if isRedirect then - - -- Don't show the action button for redirects - DialogueMessageContainer.BackgroundTransparency = 0.4; - DialogueMessageContainer.BackgroundColor3 = Color3.fromRGB(21, 44, 126); - ActionButton.Visible = false; - ViewChildrenButton.Visible = false; - - else - - if isResponse then - - -- Don't show the Before Action button for responses - DialogueMessageContainer.BackgroundTransparency = 0.4; - DialogueMessageContainer.BackgroundColor3 = Color3.fromRGB(30,103,19); - ActionButton.Visible = false; - - else - - DialogueMessageContainer.BackgroundTransparency = 1; - ActionButton.MouseButton1Click:Connect(function() - - openSpecialScript(StarterPlayerScripts.DialogueClientScript.Actions, script.ActionTemplate); - - end); - - end - - table.insert(Events.ViewChildren, ViewChildrenButton.MouseButton1Click:Connect(function() - - if isDeleteModeEnabled then - - showDeleteModePrompt(); - return; - - end; - - ViewChildrenButton.Visible = false; - - -- Go to the target directory - viewingPriority = table.concat(splitPriority, "."); - local CurrentDirectory = CurrentDialogueContainer; - - for index, directory in ipairs(splitPriority) do - - CurrentDirectory = CurrentDirectory:FindFirstChild(directory) :: ModuleScript; - - end; - - syncDialogueGUI(ContentScript); - - end)); - - end; - - -- Add functionality to the type dropdown. - local DialogueMessageTypeDropdownButton = DialogueMessageContainerChildContainer:FindFirstChild("DialogueTypeDropdown") :: TextButton; - (DialogueMessageTypeDropdownButton:FindFirstChild("DialogueType") :: TextLabel).Text = ContentScript:GetAttribute("DialogueType"); - table.insert(Events.TypeDropdown, DialogueMessageTypeDropdownButton.MouseButton1Click:Connect(function() - - if isDeleteModeEnabled then - - showDeleteModePrompt(); - return; - - end; - - local List = DialogueMessageTypeDropdownButton:FindFirstChild("List") :: ScrollingFrame; - List.Visible = not List.Visible; - if List.Visible then - - local MessageButton = List:FindFirstChild("Message") :: TextButton; - table.insert(Events.TypeDropdown, MessageButton.MouseButton1Click:Connect(function() - - ContentScript:SetAttribute("DialogueType", "Message"); - - end)); - - local RedirectButton = List:FindFirstChild("Redirect") :: TextButton; - table.insert(Events.TypeDropdown, RedirectButton.MouseButton1Click:Connect(function() - - ContentScript:SetAttribute("DialogueType", "Redirect"); - - end)); - - local ResponseButton = List:FindFirstChild("Response") :: TextButton; - table.insert(Events.TypeDropdown, ResponseButton.MouseButton1Click:Connect(function() - - ContentScript:SetAttribute("DialogueType", "Response"); - - end)); - - end - - end)); - - -- Add functionality to the View Content button. - local ViewContentButton = DialogueMessageContainerChildContainer:FindFirstChild("ViewContent") :: TextButton; - table.insert(Events.ViewContent, ViewContentButton.MouseButton1Click:Connect(function() - - plugin:OpenScript(ContentScript); - - end)); - - table.insert(Events.AttributeChanged, ContentScript.AttributeChanged:Connect(refreshDialogueGUI)); - - end; - - end; - - -- Adjust the canvas size accordingly. - DialogueMessageList.CanvasSize = UDim2.new(0, 0, 0, (DialogueMessageList:FindFirstChild("UIListLayout") :: UIListLayout).AbsoluteContentSize.Y); - -end; - - -local PluginGui: DockWidgetPluginGui?; - --- Closes the editor when called --- @since v1.0.0 -local function closeDialogueEditor(): () - - disconnectEvents(); - - viewingPriority = ""; - - DialogueMakerFrame.Parent = nil; - - if PluginGui then PluginGui:Destroy(); end; - EditDialogueButton:SetActive(false); - isDialogueEditorOpen = false; - -end; - --- Open the editor when called. --- @since v1.0.0 -local function openDialogueEditor(): () - - PluginGui = plugin:CreateDockWidgetPluginGui("Dialogue Maker", DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, true, 525, 241, 525, 139)); - repairNPC(); - if PluginGui and CurrentDialogueContainer then - - PluginGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling; - PluginGui.Title = "Dialogue Maker"; - PluginGui:BindToClose(closeDialogueEditor); - - (ModelLocationFrame:FindFirstChild("ModelLocation") :: TextLabel).Text = Model.Name; - DialogueMakerFrame.Parent = PluginGui; - isDialogueEditorOpen = true; - - -- Let's get the current dialogue settings - syncDialogueGUI(CurrentDialogueContainer) - - end; - -end; - --- Catch the button click event -EditDialogueButton.Click:Connect(function() - - if isDialogueEditorOpen then - - closeDialogueEditor(); - return; - - end; - - local isTestSuccessful, errorMessage = pcall(function() - - -- Check if the user is selecting an object. - local SelectedObjects = Selection:Get(); - assert(#SelectedObjects ~= 0, "You didn't select an object."); - assert(#SelectedObjects == 1, "You must select one object; not multiple objects."); - - -- Check if the model has a part - Model = SelectedObjects[1]; - assert(Model:IsA("Model"), "You must select a Model, not a "..Model.ClassName.."."); - - local ModelHasPart = false; - for _, object in ipairs(Model:GetChildren()) do - - if object:IsA("BasePart") then - - ModelHasPart = true; - break; - - end - - end; - - assert(ModelHasPart, "Your selected model doesn't have a part inside of it."); - - end); - - if not isTestSuccessful then - - EditDialogueButton:SetActive(false); - error("[Dialogue Maker] " .. errorMessage, 0); - - end - - -- Verify NPC dialogue folder - repairNPC(); - - -- Add the chat receiver script in the starter player scripts - if not StarterPlayerScripts:FindFirstChild("DialogueClientScript") then - - print("[Dialogue Maker] Adding DialogueClientScript to the StarterPlayerScripts..."); - local DialogueClientScript = script.DialogueClientScript:Clone() - DialogueClientScript.Parent = StarterPlayerScripts; - DialogueClientScript.Disabled = false; - print("[Dialogue Maker] Added DialogueClientScript to the StarterPlayerScripts."); - - -- Add this model to the DialogueManager - local DialogueLocation = Instance.new("ObjectValue"); - DialogueLocation.Value = Model; - DialogueLocation.Name = "DialogueLocation"; - DialogueLocation.Parent = DialogueClientScript.DialogueLocations; - - end; - - -- Now we can open the dialogue editor. - openDialogueEditor(); - -end); - -local isBusy = false; -local ResetScriptsButton = Toolbar:CreateButton("Fix Scripts", "Reset DialogueMakerSharedDependencies and DialogueClientScript back to the a stable version.", "rbxassetid://14109193905"); -ResetScriptsButton.Click:Connect(function() - - -- Debounce - assert(not isBusy, "[Dialogue Maker] One moment please..."); - isBusy = true; - - local Success, Msg = pcall(function() - -- Set an undo point - ChangeHistoryService:SetWaypoint("Resetting Dialogue Maker scripts"); - - -- Make copies - local NewDialogueClientScript = script.DialogueClientScript:Clone(); - local ClientAPI = NewDialogueClientScript.API:Clone(); - local NewThemes = NewDialogueClientScript.Themes:Clone(); - - -- Save the old copies - local OldDialogueClientScript = StarterPlayerScripts:FindFirstChild("DialogueClientScript") or NewDialogueClientScript:Clone(); - - -- Remove the children from the new copies - for _, child in ipairs(OldDialogueClientScript:GetChildren()) do - - child.Parent = nil; - - end; - - -- Enable the scripts - OldDialogueClientScript.Disabled = false; - - -- Check for themes - local OldThemes = OldDialogueClientScript:FindFirstChild("Themes"); - if not OldThemes then - - NewThemes.Parent = OldDialogueClientScript; - - end; - - -- Check for API - local OldAPI = OldDialogueClientScript:FindFirstChild("API"); - if OldAPI then - - OldAPI.Parent = nil; - - end - - -- Take the children from the old scripts - for _, Child in ipairs(OldDialogueClientScript:GetChildren()) do - - Child.Parent = NewDialogueClientScript; - - end; - - -- Delete the old scripts - OldDialogueClientScript.Parent = nil; - - -- Put the new instances in their places - NewDialogueClientScript.Parent = StarterPlayerScripts; - ClientAPI.Parent = NewDialogueClientScript; - - -- Finalize the undo point - ChangeHistoryService:SetWaypoint("Reset Dialogue Maker scripts"); - - end) - - -- Done! - isBusy = false; - print("[Dialogue Maker] " .. if Success then "Fixed Dialogue Maker scripts!" else ("Couldn't fix scripts: " .. Msg)); - -end); - -local RemoveUnusedInstancesButton = Toolbar:CreateButton("Remove Unused Instances", "Deletes unused actions, conditions, and dialogue locations.", "rbxassetid://14109207161") -RemoveUnusedInstancesButton.Click:Connect(function() - - assert(not isBusy, "[Dialogue Maker] One moment please..."); - isBusy = true; - - local Count = 0; - pcall(function() - - -- Set an undo point - ChangeHistoryService:SetWaypoint("Removing unused Dialogue Maker instances"); - - -- Remove the unused instances - for _, folder in ipairs(StarterPlayerScripts.DialogueClientScript:GetChildren()) do - - if not folder:IsA("Folder") then - - continue; - - end - - for _, child in ipairs(folder:GetChildren()) do - - if folder.Name == "Actions" then - - for _, module in ipairs(child:GetChildren()) do - - local NPC = module:FindFirstChild("NPC"); - if not NPC or not NPC.Value or not NPC.Value.Parent then - - Count += 1; - module.Parent = nil; - - end - - end - - elseif folder.Name ~= "DialogueLocations" then - - local NPC = child:FindFirstChild("NPC"); - if not NPC or not NPC.Value or not NPC.Value.Parent then - - Count += 1; - child.Parent = nil; - - end - - elseif not child.Value or not child.Value.Parent then - - Count += 1; - child.Parent = nil; - - end; - - end; - - end; - - -- Finalize the undo point - ChangeHistoryService:SetWaypoint("Removed unused Dialogue Maker instances"); - end) - - -- Done! - isBusy = false; - local Plural = if Count ~= 1 then "s" else ""; - print("[Dialogue Maker] Removed unused " .. Count .. " Dialogue Maker instance" .. Plural .. "!") - -end); \ No newline at end of file diff --git a/src/DialoguePluginScript/Colors.lua b/src/DialoguePluginScript/Colors.lua new file mode 100644 index 0000000..b0292a9 --- /dev/null +++ b/src/DialoguePluginScript/Colors.lua @@ -0,0 +1,11 @@ +return { + backgroundResponse = Color3.fromRGB(30, 103, 19); + backgroundRedirect = Color3.fromRGB(21, 44, 126); + backgroundWarning = Color3.fromRGB(207, 57, 51); + backgroundDeletionFrame = Color3.new(0, 0, 0); + backgroundTableHeader = Color3.fromRGB(72, 72, 72); + backgroundTableRow = Color3.fromRGB(50, 50, 50); + text = Color3.new(1, 1, 1); + textDisabled = Color3.fromRGB(182, 182, 182); + textPlaceholder = Color3.fromRGB(175, 175, 175) +} \ No newline at end of file diff --git a/src/DialoguePluginScript/DialogueClientScript/API/Dialogue.lua b/src/DialoguePluginScript/DialogueClientScript/API/Dialogue.lua deleted file mode 100644 index 8b13df6..0000000 --- a/src/DialoguePluginScript/DialogueClientScript/API/Dialogue.lua +++ /dev/null @@ -1,1048 +0,0 @@ ---!strict -local UserInputService = game:GetService("UserInputService"); -local ContextActionService = game:GetService("ContextActionService"); -local ReplicatedStorage = game:GetService("ReplicatedStorage"); -local TweenService = game:GetService("TweenService"); -local Players = game:GetService("Players"); -local Player = Players.LocalPlayer; - -local DialogueModule = { - isPlayerTakingWithNPC = false; -}; - -local DialogueClientScript = script.Parent.Parent; -local Types = require(DialogueClientScript.Types); - -local clientSettings = require(DialogueClientScript.Settings); -local defaultThemes = clientSettings.defaultThemes; -function DialogueModule.getDefaultThemeName(viewportWidth: number, viewportHeight: number): string - - assert(defaultThemes, "[Dialogue Maker] Couldn't get default themes from the server."); - - local defaultThemeName; - for _, themeInfo in ipairs(defaultThemes) do - - if viewportWidth >= themeInfo.minimumViewportWidth and viewportHeight >= themeInfo.minimumViewportHeight then - - defaultThemeName = themeInfo.themeName; - - end - - end - - return defaultThemeName; - -end; - -function DialogueModule.createNewDialogueGui(themeName: string?): ScreenGui - - -- Check if we have the theme - local ThemeFolder = DialogueClientScript.Themes; - local DialogueGui = ThemeFolder:FindFirstChild(themeName); - if themeName and not DialogueGui then - - if themeName ~= "" then - - warn("[Dialogue Maker]: Can't find theme \"" .. themeName .. "\" in the Themes folder of the DialogueClientScript. Using default theme..."); - - end - - local ScreenGuiTest = Instance.new("ScreenGui"); - ScreenGuiTest.Parent = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui"); - local ViewportSize = ScreenGuiTest.AbsoluteSize; - local DefaultThemeName = DialogueModule.getDefaultThemeName(ViewportSize.X, ViewportSize.Y); - ScreenGuiTest:Destroy(); - DialogueGui = ThemeFolder:FindFirstChild(DefaultThemeName); - - end - - if not DialogueGui then - - error("[Dialogue Maker]: There isn't a default theme", 0); - - end - - -- Return the theme - return DialogueGui:Clone(); - -end; - --- Searches for a ModuleScript based on a given directory. Errors if it doesn't exist. --- @since v1.0.0 --- @returns A module script of a given directory. -function DialogueModule.goToDirectory(DialogueContainerFolder: Folder, targetPath: {string}): ModuleScript - - local currentPath = ""; - local CurrentDirectoryScript: ModuleScript | Folder = DialogueContainerFolder; - for index, directory in ipairs(targetPath) do - - currentPath = currentPath .. (if currentPath ~= "" then "." else "") .. directory; - local PossibleDirectory = CurrentDirectoryScript:FindFirstChild(directory); - if not PossibleDirectory or not PossibleDirectory:IsA("ModuleScript") then - - error("[Dialogue Maker]" .. currentPath .. " is not a ModuleScript"); - - end - CurrentDirectoryScript = PossibleDirectory; - - end; - - if CurrentDirectoryScript:IsA("Folder") then - - error("[Dialogue Maker] Target path (" .. table.concat(targetPath, ".") .. ") not found."); - - end - - return CurrentDirectoryScript; - -end; - --- @since v1.0.0 -function DialogueModule.clearResponses(responseContainer: ScrollingFrame): () - - for _, response in ipairs(responseContainer:GetChildren()) do - - if not response:IsA("UIListLayout") then - - response:Destroy(); - - end; - - end; - -end; - -function deleteNonTextWrapperChildren(TextContainer: Instance) - - for _, child in ipairs(TextContainer:GetChildren()) do - - if child.Name ~= "TextWrapper" then - - child:Destroy(); - - end; - - end - -end - --- @since v5.0.0 -function DialogueModule.getPages(contentArray: Types.ContentArray, TextContainer: GuiObject, TextLabel: TextLabel): {Types.Page} - - local pages: {Types.Page} = {}; - local currentPage: Types.Page = {}; - local TextContainerClone = TextContainer:Clone(); - local TextLabelClone = TextLabel:Clone(); - - TextContainerClone.Visible = false; - TextContainerClone.Parent = TextContainer.Parent; - - if TextContainerClone:FindFirstChild("Segment") then - - TextContainerClone:FindFirstChild("Segment"):Destroy(); - - end - - local xSizeOffset = 0; - - local function newPage() - - table.insert(pages, currentPage); - currentPage = {}; - - TextLabelClone = TextLabelClone:Clone(); - - deleteNonTextWrapperChildren(TextContainerClone); - - TextLabelClone.Parent = TextContainerClone; - TextLabelClone.Size = UDim2.new(1, 0, 1, 0); - - xSizeOffset = 0; - - end - - - for contentArrayIndex, contentArrayItem in ipairs(contentArray) do - - local contentArrayItemType = typeof(contentArrayItem); - - if contentArrayItemType == "string" then - - -- Calculate the X size offset. - local TextWrapper = TextContainerClone:FindFirstChild("TextWrapper"); - assert(TextWrapper and TextWrapper:IsA("UIListLayout"), "[Dialogue Maker] TextWrapper not found"); - - local lastSpaceIndex: number? = nil; - - repeat - - local function addTextLabelToPage(TextLabel: TextLabel) - - table.insert(currentPage, { - type = "text"; - text = TextLabel.Text; - size = TextLabel.Size; - }); - - end - - TextLabelClone = TextLabelClone:Clone(); - - if lastSpaceIndex then - - TextLabelClone.Text = (contentArrayItem :: string):sub(lastSpaceIndex + 1); - - else - - TextLabelClone.Text = contentArrayItem :: string; - - end - - TextLabelClone.Size = UDim2.new(1, -xSizeOffset, if xSizeOffset > 0 then 0 else 1, if xSizeOffset > 0 then TextLabelClone.TextSize * TextLabelClone.LineHeight else -TextWrapper.AbsoluteContentSize.Y); - TextLabelClone.Parent = TextContainerClone; - - if not TextLabelClone.TextFits then - - -- Check if we should add a new page. - if TextWrapper.AbsoluteContentSize.Y > TextContainerClone.AbsoluteSize.Y then - - -- Add the current page to the page list. - newPage(); - - end - - end - - if TextLabelClone.TextFits then - - local function getRichTextIndices(text: string) - - local richTextTagIndices: {Types.RichTextTagInformation} = {}; - local openTagIndices: {number} = {}; - local textCopy = text; - local tagPattern = "<[^<>]->"; - local pointer = 1; - for tag in textCopy:gmatch(tagPattern) do - - -- Get the tag name and attributes. - local tagText = tag:match("<([^<>]-)>"); - if tagText then - - local firstSpaceIndex = tagText:find(" "); - local tagTextLength = tagText:len(); - local name = tagText:sub(1, (firstSpaceIndex and firstSpaceIndex - 1) or tagTextLength); - if name:sub(1, 1) == "/" then - - for _, index in ipairs(openTagIndices) do - - if richTextTagIndices[index].name == name:sub(2) then - - -- Add a tag end offset. - local _, endOffset = textCopy:find(tagPattern); - if endOffset then - - richTextTagIndices[index].endOffset = pointer + endOffset; - - end; - - -- Remove the tag from the open tag table. - table.remove(openTagIndices, index); - break; - - end - - end - - else - - -- Get the tag start offset. - local startOffset = pointer; - local attributes = firstSpaceIndex and tagText:sub(firstSpaceIndex + 1) or ""; - table.insert(richTextTagIndices, { - name = name; - attributes = attributes; - startOffset = textCopy:find(tagPattern) :: number + pointer - 1; - }); - table.insert(openTagIndices, #richTextTagIndices); - - end - - -- Remove the tag from our copy. - local _, pointerUpdate = textCopy:find(tagPattern); - if pointerUpdate then - - pointer += pointerUpdate - 1; - textCopy = textCopy:sub(pointerUpdate); - - end; - - end; - - end - - return richTextTagIndices; - - end - - local function getLineBreakPositions(text: string, TextLabel: TextLabel, isRichText: boolean): {number} - - -- Iterate through each character. - local breakpoints: {number} = {}; - local originalTextLabelText = TextLabel.Text; - TextLabel.Text = ""; - local lastSpaceIndex: number = 1; - local skipCounter = 0; - local remainingRichTextTags = getRichTextIndices(text); - for index, character in ipairs(text:split("")) do - - -- Check if this is an offset. - if skipCounter ~= 0 then - - skipCounter -= 1; - continue; - - end - - if isRichText then - - for _, richTextTagIndex in ipairs(remainingRichTextTags) do - - if richTextTagIndex.startOffset == index then - - skipCounter = ("<" .. richTextTagIndex.name .. (if richTextTagIndex.attributes and richTextTagIndex.attributes ~= "" then " " .. richTextTagIndex.attributes else "") .. ">"):len() - 1; - break; - - elseif richTextTagIndex.endOffset :: number - (""):len() == index then - - skipCounter = (""):len() - 1; - break; - - end - - end - - end; - - if skipCounter > 0 then - - continue; - - end - - -- Keep track of spaces. - if character == " " then - - lastSpaceIndex = index; - - end - - -- Keep track of the original text bounds. - local originalTextBoundsY = TextLabel.TextBounds.Y; - - -- Add the character and applicable rich text tags. - TextLabel.Text = TextLabel.ContentText .. character; - if isRichText then - - for _, richTextTagInfo in ipairs(remainingRichTextTags) do - - local startOffset = richTextTagInfo.startOffset; - local endOffset = richTextTagInfo.endOffset :: number; - if index >= startOffset and endOffset > (breakpoints[#breakpoints] or 0) then - - local prefix = "<" .. richTextTagInfo.name .. (if richTextTagInfo.attributes and richTextTagInfo.attributes ~= "" then " " .. richTextTagInfo.attributes else "") .. ">"; - local suffix = ""; - local startOffset = startOffset - (breakpoints[#breakpoints] or 0); - local endOffset = (endOffset - (breakpoints[#breakpoints] or 0)) - prefix:len() - suffix:len(); - TextLabel.Text = TextLabel.ContentText:sub(1, startOffset - 1) .. prefix .. TextLabel.ContentText:sub(startOffset, endOffset - 1) .. suffix .. TextLabel.ContentText:sub(endOffset); - - end - - end - - end; - - - if TextLabel.TextBounds.Y > originalTextBoundsY then - - local currentTextBoundsY = TextLabel.TextBounds.Y; - TextLabel.TextWrapped = false; - - if TextLabel.TextBounds.Y < currentTextBoundsY then - - table.insert(breakpoints, lastSpaceIndex); - TextLabel.Text = text:sub(lastSpaceIndex + 1, index); - - end - - TextLabel.TextWrapped = true; - - end - - end - - TextLabel.Text = originalTextLabelText; - - -- Return breakpoints. - return breakpoints; - - end - - local originalText = TextLabelClone.Text; - local breakpoints = getLineBreakPositions(originalText, TextLabelClone, TextLabelClone.RichText); - local lastBreakpointIndex = breakpoints[#breakpoints]; - - if lastBreakpointIndex then - - -- Create another TextLabel to replace the last line of text. - -- This will allow the TextWrapper to accurately calculate - -- how much space is available on the X-axis. - local ParagraphTextLabel = TextLabelClone:Clone(); - ParagraphTextLabel.Text = originalText:sub(1, lastBreakpointIndex); - ParagraphTextLabel.Parent = TextLabelClone.Parent; - ParagraphTextLabel.Size = UDim2.new(0, ParagraphTextLabel.TextBounds.X, 0, ParagraphTextLabel.TextBounds.Y + (ParagraphTextLabel.TextSize * ParagraphTextLabel.LineHeight - ParagraphTextLabel.TextSize)); - addTextLabelToPage(ParagraphTextLabel); - - -- Fix the TextLabelClone's text back. - TextLabelClone.Parent = nil; - TextLabelClone.Parent = ParagraphTextLabel.Parent; - TextLabelClone.Text = originalText:sub(lastBreakpointIndex + 1); - - end; - - TextLabelClone.Size = UDim2.new(0, TextLabelClone.TextBounds.X, 0, TextLabelClone.TextBounds.Y + (TextLabelClone.TextSize * TextLabelClone.LineHeight - TextLabelClone.TextSize)); - addTextLabelToPage(TextLabelClone); - - xSizeOffset += TextLabelClone.TextBounds.X; - - lastSpaceIndex = nil; - - else - - -- Remove a word from the text until we can fit the text. - lastSpaceIndex = 0; - repeat - - lastSpaceIndex = table.pack(TextLabelClone.Text:find(".* "))[2] :: number; - if not lastSpaceIndex and TextLabelClone.TextBounds.Y < TextLabelClone.TextSize * TextLabelClone.LineHeight then - - -- The given area is too small. Add this to a new page. - newPage(); - continue; - - end - - assert(lastSpaceIndex, "[Dialogue Maker] Unable to fit text in text container even after removing the spaces. Is the text too big?"); - TextLabelClone.Text = TextLabelClone.Text:sub(1, lastSpaceIndex - 1); - - until TextLabelClone.TextFits; - - TextLabelClone.Size = UDim2.new(0, TextLabelClone.TextBounds.X, 0, TextLabelClone.TextBounds.Y + (TextLabelClone.TextSize * TextLabelClone.LineHeight - TextLabelClone.TextSize)); - - -- Add the remaining text to a new page. - addTextLabelToPage(TextLabelClone); - - xSizeOffset = 0; - - end - - until not lastSpaceIndex; - - elseif contentArrayItemType == "table" then - - -- TODO: Add effects - - end; - - end - - TextContainerClone:Destroy(); - - -- Return all pages for this message. - if currentPage[1] then - - newPage(); - - end - - return pages; - -end; - -local isPlayerTakingWithNPC = false; - --- @since v1.0.0 -function DialogueModule.readDialogue(NPC: Model, npcSettings: Types.NPCSettings): () - - local Events = {}; - - -- Make sure we aren't already talking to an NPC - assert(not DialogueModule.isPlayerTakingWithNPC, "[Dialogue Maker] Cannot read dialogue because player is currently talking with another NPC."); - DialogueModule.isPlayerTakingWithNPC = true; - - -- Make sure we have a DialogueContainer. - local NPCDialogueContainer: Folder? = NPC:FindFirstChild("DialogueContainer") :: Folder; - assert(NPCDialogueContainer, "DialogueContainer not found in NPC."); - - -- Check if the NPC needs to look at the player. - if npcSettings.general.npcLooksAtPlayerDuringDialogue and npcSettings.general.npcNeckRotationMaxY then - - -- Handle this in a coroutine because the look shouldn't stop the dialogue. - coroutine.wrap(function() - - local NPCHead: BasePart? = NPC:FindFirstChild("Head") :: BasePart; - local NPCPrimaryPart: BasePart? = NPC.PrimaryPart :: BasePart; - local NPCHumanoid: Humanoid? = NPC:FindFirstChild("Humanoid") :: Humanoid; - local NPCTorso: BasePart? = NPCHumanoid and NPCHumanoid.RigType == Enum.HumanoidRigType.R6 and (NPC:FindFirstChild("Torso") :: BasePart) or nil; - local NPCNeckParent = NPCTorso or NPCHead; - local NPCNeck: Motor6D? = NPCNeckParent and NPCNeckParent:FindFirstChild("Neck") :: Motor6D; - local PlayerCharacter: Model? = Player.Character; - local PlayerHead: BasePart? = (PlayerCharacter and PlayerCharacter:FindFirstChild("Head") :: BasePart); - if NPCNeck then - - -- Set the base position. - NPCNeck.C0 = CFrame.new(NPCNeck.C0.Position) * CFrame.fromOrientation(0, 0, 0); - NPCNeck.C1 = CFrame.new(NPCNeck.C1.Position) * CFrame.fromOrientation(0, 0, 0); - local OriginalC0 = NPCNeck.C0; - local OriginalC1 = NPCNeck.C1; - - while DialogueModule.isPlayerTakingWithNPC and NPCPrimaryPart and NPCHead and NPCNeck and PlayerHead and task.wait() do - - local maxRotationX = npcSettings.general.npcNeckRotationMaxX; - local maxRotationY = npcSettings.general.npcNeckRotationMaxY; - local maxRotationZ = npcSettings.general.npcNeckRotationMaxZ; - local goalRotationX, goalRotationY, goalRotationZ = CFrame.new(NPCHead.Position, PlayerHead.Position):ToOrientation(); - local rotationOffsetX = goalRotationX - math.rad(NPCPrimaryPart.Orientation.X); - local rotationOffsetY = goalRotationY - math.rad(NPCPrimaryPart.Orientation.Y); - local rotationOffsetZ = goalRotationZ - math.rad(NPCPrimaryPart.Orientation.Z); - local rotationXAbs = math.abs(rotationOffsetX); - local rotationYAbs = math.abs(rotationOffsetY); - local rotationZAbs = math.abs(rotationOffsetZ); - TweenService:Create(NPCNeck, TweenInfo.new(0.3), { - C0 = CFrame.new(NPCNeck.C0.Position) * CFrame.fromOrientation( - ((rotationXAbs > maxRotationX and maxRotationX * (rotationOffsetX / rotationXAbs) * ((rotationXAbs > math.pi and -1) or 1)) or rotationOffsetX), - ((rotationYAbs > maxRotationY and maxRotationY * (rotationOffsetY / rotationYAbs) * ((rotationYAbs > math.pi and -1) or 1)) or rotationOffsetY), - ((rotationZAbs > maxRotationZ and maxRotationZ * (rotationOffsetZ / rotationZAbs) * ((rotationZAbs > math.pi and -1) or 1)) or rotationOffsetZ) - ) - }):Play(); - - end - - TweenService:Create(NPCNeck, TweenInfo.new(0.3), {C0 = OriginalC0, C1 = OriginalC1}):Play(); - - end - - end)(); - - end - - -- Set the theme and prepare the response template - local DialogueGUI: ScreenGui = DialogueModule.createNewDialogueGui(npcSettings.general.themeName); - local ResponseContainer, ResponseTemplate, ClickSound: Sound?, ClickSoundEnabled, OldDialogueGui; - local GUIDialogueContainer = DialogueGUI:FindFirstChild("DialogueContainer"); - local npcName = npcSettings.general.npcName; - local function setupDialogueGui(): () - - -- Set up responses - DialogueGUI.Parent = Player:WaitForChild("PlayerGui"); - GUIDialogueContainer = DialogueGUI:FindFirstChild("DialogueContainer"); - ResponseContainer = GUIDialogueContainer:FindFirstChild("ResponseContainer"); - assert(ResponseContainer and ResponseContainer:IsA("ScrollingFrame"), "[Dialogue Maker] ResponseContainer is not a ScrollingFrame"); - ResponseTemplate = ResponseContainer:FindFirstChild("ResponseTemplate"):Clone(); - - -- Set NPC name - local NPCNameContainer = GUIDialogueContainer:FindFirstChild("NPCNameContainer"); - if NPCNameContainer:IsA("GuiObject") then - - local NPCNameTextClass = NPCNameContainer:FindFirstChild("NPCName"); - if NPCNameTextClass:IsA("TextLabel") then - - NPCNameTextClass.Text = npcName; - if npcSettings.general.fitName then - - NPCNameContainer.Size = UDim2.new(NPCNameContainer.Size.X.Scale, NPCNameTextClass.TextBounds.X + npcSettings.general.textBoundsOffset, NPCNameContainer.Size.Y.Scale, NPCNameContainer.Size.Y.Offset); - - end; - - NPCNameContainer.Visible = npcName ~= ""; - - end - - end; - - -- Setup click sound - local PossibleClickSound = DialogueGUI:FindFirstChild("ClickSound"); - if PossibleClickSound and PossibleClickSound:IsA("Sound") then - - ClickSound = PossibleClickSound; - - end; - - ClickSoundEnabled = false; - - local defaultClickSound = clientSettings.defaultClickSound; - if defaultClickSound and defaultClickSound ~= 0 then - - if not ClickSound then - - local NewClickSound = Instance.new("Sound"); - NewClickSound.Name = "ClickSound"; - NewClickSound.Parent = DialogueGUI; - ClickSound = NewClickSound; - - end; - - ClickSoundEnabled = true; - (ClickSound :: Sound).SoundId = "rbxassetid://" .. defaultClickSound; - - end; - - end; - - setupDialogueGui(); - - if GUIDialogueContainer:IsA("GuiObject") and ResponseContainer:IsA("ScrollingFrame") and ResponseTemplate:IsA("TextButton") then - - -- Initialize the theme, then listen for changes - script.CurrentTheme.Value = DialogueGUI; - local ThemeChangedEvent = script.CurrentTheme.Changed:Connect(function(newTheme) - - DialogueGUI:Destroy(); - DialogueGUI = newTheme; - setupDialogueGui(); - - end); - - -- If necessary, end conversation if player or NPC goes out of distance - local NPCPrimaryPart = NPC.PrimaryPart; - local MaxConversationDistance = npcSettings.general.maxConversationDistance; - local EndConversationIfOutOfDistance = npcSettings.general.endConversationIfOutOfDistance; - if EndConversationIfOutOfDistance and MaxConversationDistance and NPCPrimaryPart then - - coroutine.wrap(function() - - while task.wait() and DialogueModule.isPlayerTakingWithNPC do - - if math.abs(NPCPrimaryPart.Position.Magnitude - Player.Character.PrimaryPart.Position.Magnitude) > MaxConversationDistance then - - DialogueModule.isPlayerTakingWithNPC = false; - break; - - end; - - end; - - end)(); - - end; - - -- Show the dialouge to the player - local currentDialoguePriority = "1"; - local CurrentContentScript: ModuleScript; - while DialogueModule.isPlayerTakingWithNPC and task.wait() do - - -- Get the current directory. - CurrentContentScript = DialogueModule.goToDirectory(NPCDialogueContainer, currentDialoguePriority:split(".")); - local dialogueType = CurrentContentScript:GetAttribute("DialogueType"); - - -- Checks if the local player passes a condition. - -- @since v5.0.0 - local function doesPlayerPassCondition(ContentScript: ModuleScript): boolean - - -- Search for condition - for _, PossibleCondition in ipairs(DialogueClientScript.Conditions:GetChildren()) do - - if PossibleCondition.ContentScript.Value == ContentScript then - - -- Check if there is no condition or the condition passed - return (require(PossibleCondition) :: () -> boolean)(); - - end; - - end; - - return true; - - end - - if doesPlayerPassCondition(CurrentContentScript) then - - local function useEffect(effectName: string, ...: any): Types.Effect - - -- Try to find the effect script based on the name. - local EffectScript = DialogueClientScript.Effects:FindFirstChild(effectName); - assert(EffectScript and EffectScript:IsA("ModuleScript"), "[Dialogue Maker] " .. effectName .. " is not a valid effect. Check your Effects folder to make sure there's a ModuleScript with that name."); - return require(EffectScript)(...) :: Types.Effect; - - end; - - local dialogueContentArray = (require(CurrentContentScript) :: (useEffect: typeof(useEffect)) -> Types.ContentArray)(useEffect); - if dialogueType == "Redirect" then - - -- A redirect is available, so let's switch priorities. - assert(typeof(dialogueContentArray[1]) == "string", "[Dialogue Maker] Item at index 1 is not a directory."); - currentDialoguePriority = dialogueContentArray[1] :: string; - continue; - - end; - - -- Get a list of responses from the dialogue. - local responses: {{ModuleScript: ModuleScript; properties: any}} = {}; - for _, PossibleResponse in ipairs(CurrentContentScript:GetChildren()) do - - if PossibleResponse:IsA("ModuleScript") and tonumber(PossibleResponse.Name) and PossibleResponse:GetAttribute("DialogueType") == "Response" then - - table.insert(responses, { - ModuleScript = PossibleResponse, - properties = require(PossibleResponse) :: any - }); - - end - - end - - -- Determine which text container we should use. - local areResponsesEnabled = false; - local NPCTextContainerWithResponses = GUIDialogueContainer:FindFirstChild("NPCTextContainerWithResponses") :: GuiObject; - local NPCTextContainerWithoutResponses = GUIDialogueContainer:FindFirstChild("NPCTextContainerWithoutResponses") :: GuiObject; - if #responses > 0 then - - -- Clear the text container just in case there was some responses left behind. - DialogueModule.clearResponses(ResponseContainer); - - end; - - local TextContainer = if #responses > 0 then NPCTextContainerWithResponses else NPCTextContainerWithoutResponses; - NPCTextContainerWithResponses.Visible = #responses > 0; - NPCTextContainerWithoutResponses.Visible = not (#responses > 0); - areResponsesEnabled = #responses > 0; - - -- Ensure we have a text container line. - local TextContainerLine: TextLabel? = TextContainer:FindFirstChild("Segment") :: TextLabel; - assert(TextContainerLine, "[Dialogue Maker] Segment not found."); - - -- Make the NPC stop talking if the player clicks the frame - local isNPCTalking = true; - local isNPCPaused = false; - local isSkipping = false; - local isWaitingForPlayerResponse = true; - local onSkip; - local defaultChatContinueKey = clientSettings.defaultChatContinueKey; - local defaultChatContinueKeyGamepad = clientSettings.defaultChatContinueKeyGamepad; - local ContinueDialogue = function(keybind: Enum.KeyCode?): () - - -- Ensure the player is holding the key. - if (keybind and not UserInputService:IsKeyDown(defaultChatContinueKey) and not UserInputService:IsKeyDown(defaultChatContinueKeyGamepad)) then - - return; - - end; - - if isNPCTalking then - - if ClickSoundEnabled and ClickSound then - - ClickSound:Play(); - - end; - - if isNPCPaused then - - isNPCPaused = false; - - end; - - if npcSettings.general.allowPlayerToSkipDelay then - - isSkipping = true; - onSkip(); - - end; - - elseif #responses == 0 then - - isWaitingForPlayerResponse = false; - - end; - - end; - - Events.DialogueClicked = GUIDialogueContainer.InputBegan:Connect(function(input) - - -- Make sure the player clicked the frame - if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then - - ContinueDialogue(); - - end; - - end); - - if clientSettings.keybindsEnabled then - - local KEYS_PRESSED = UserInputService:GetKeysPressed(); - local KeybindPressed = false; - if UserInputService:IsKeyDown(defaultChatContinueKey) or UserInputService:IsKeyDown(defaultChatContinueKeyGamepad) then - - coroutine.wrap(function() - - while UserInputService:IsKeyDown(defaultChatContinueKey) or UserInputService:IsKeyDown(defaultChatContinueKeyGamepad) do - - task.wait(); - - end; - ContextActionService:BindAction("ContinueDialogue", ContinueDialogue, false, defaultChatContinueKey, defaultChatContinueKeyGamepad); - - end)(); - - else - - ContextActionService:BindAction("ContinueDialogue", ContinueDialogue, false, defaultChatContinueKey, defaultChatContinueKeyGamepad); - - end; - - end; - - -- Determine how many pages we need to show the dialogue. - local pages = DialogueModule.getPages(dialogueContentArray, TextContainer, TextContainerLine); - - -- Show what's on every page. - TextContainerLine.Text = ""; - TextContainerLine.Visible = false; - DialogueGUI.Enabled = true; - local componentsToDelete = {}; - for pageIndex, page in ipairs(pages) do - - for _, child in ipairs(componentsToDelete) do - - child:Destroy(); - - end - - for dialogueContentItemIndex, dialogueContentItem in ipairs(page) do - - if dialogueContentItem.type == "effect" then - - -- The item is an effect. Let's run it. - print("[Dialogue Maker] [" .. dialogueContentItemIndex .. "/" .. #page .. "] [Effect] " .. (npcName or "Unknown NPC") .. ": " .. dialogueContentItem.name); - dialogueContentItem.run(isSkipping); - - elseif dialogueContentItem.type == "text" then - - -- Print to the debug console. - print("[Dialogue Maker] [" .. dialogueContentItemIndex .. "/" .. #page .. "] [Message] " .. (npcName or "Unknown NPC") .. ": " .. dialogueContentItem.text); - - -- Determine new offset. - local TextContainerLineCopy = TextContainerLine:Clone(); - TextContainerLineCopy.Position = UDim2.new(); - TextContainerLineCopy.Text = dialogueContentItem.text; - TextContainerLineCopy.Size = dialogueContentItem.size; - TextContainerLineCopy.Name = pageIndex .. "_" .. dialogueContentItemIndex; - TextContainerLineCopy.Visible = true; - TextContainerLineCopy.Parent = TextContainerLine.Parent; - - table.insert(componentsToDelete, TextContainerLineCopy); - - onSkip = function() - - TextContainerLineCopy.MaxVisibleGraphemes = -1; - - end; - - if isSkipping then - - onSkip(); - - else - - for count = 1, #TextContainerLineCopy.ContentText do - - TextContainerLineCopy.MaxVisibleGraphemes = count; - - task.wait(npcSettings.general.letterDelay); - - if TextContainerLineCopy.MaxVisibleGraphemes == -1 then - - break; - - end - - end; - - end; - - end; - - end - - -- Check if there are more pages. - if pages[pageIndex + 1] and isNPCTalking then - - -- Wait for the player to click - local ClickToContinueButton: GuiButton? = GUIDialogueContainer:FindFirstChild("ClickToContinue") :: GuiButton; - if ClickToContinueButton then - - ClickToContinueButton.Visible = true; - - end; - - isNPCPaused = true; - while isNPCPaused and isNPCTalking and DialogueModule.isPlayerTakingWithNPC do - - task.wait(); - - end; - - -- Let the NPC speak again - if ClickToContinueButton then - - ClickToContinueButton.Visible = false; - - end; - isNPCPaused = false; - - end; - - isSkipping = false; - - end; - isNPCTalking = false; - - local chosenResponse; - if areResponsesEnabled and DialogueModule.isPlayerTakingWithNPC then - - -- Sort responses because :GetChildren() doesn't guarantee it - table.sort(responses, function(folder1, folder2) - - return folder1.ModuleScript.Name < folder2.ModuleScript.Name; - - end); - - -- Add response buttons - for _, response in ipairs(responses) do - - if doesPlayerPassCondition(response.ModuleScript) then - - local ResponseButton = ResponseTemplate:Clone(); - ResponseButton.Name = "Response"; - ResponseButton.Text = response.properties()[1]; - ResponseButton.Parent = ResponseContainer; - ResponseButton.MouseButton1Click:Connect(function() - - -- Acknowledge that the player clicked the button. - print("[Dialogue Maker] [Response] " .. Player.Name .. " (" .. Player.UserId .. "): " .. ResponseButton.Text); - ResponseContainer.Visible = false; - - if ClickSoundEnabled and ClickSound then - - ClickSound:Play(); - - end; - - chosenResponse = response; - isWaitingForPlayerResponse = false; - - end); - - end; - - end; - - ResponseContainer.CanvasSize = UDim2.new(0, ResponseContainer.CanvasSize.X.Offset, 0, (ResponseContainer:FindFirstChild("UIListLayout") :: UIListLayout).AbsoluteContentSize.Y); - ResponseContainer.Visible = true; - - end; - - -- Run the timeout code in the background - coroutine.wrap(function() - - if npcSettings.timeout.enabled then - - -- Wait for the player if the developer wants to - if not areResponsesEnabled or not npcSettings.timeout.waitForResponse then - - -- Wait the timeout set by the developer - task.wait(npcSettings.timeout.seconds); - isWaitingForPlayerResponse = false; - - end; - - end; - - end)(); - - while isWaitingForPlayerResponse and DialogueModule.isPlayerTakingWithNPC do - - task.wait(); - - end; - - -- Run action - if DialogueModule.isPlayerTakingWithNPC then - - for _, PossibleAction in ipairs(DialogueClientScript.Actions:GetChildren()) do - - if PossibleAction.ContentScript.Value == CurrentContentScript then - - (require(PossibleAction) :: () -> ())(); - break; - - end; - - end; - - end; - - -- Check if there is more dialogue. - local hasPossibleDialogue = false; - local NextScript = if chosenResponse then chosenResponse.ModuleScript else CurrentContentScript; - for _, PossibleDialogue in ipairs(NextScript:GetChildren()) do - - local DialogueType = PossibleDialogue:GetAttribute("DialogueType"); - if PossibleDialogue:IsA("ModuleScript") and tonumber(PossibleDialogue.Name) and (DialogueType == "Message" or DialogueType == "Redirect") then - - hasPossibleDialogue = true; - break; - - end - - end - - if DialogueModule.isPlayerTakingWithNPC and hasPossibleDialogue then - - currentDialoguePriority = (if chosenResponse then currentDialoguePriority .. "." .. chosenResponse.ModuleScript.Name else currentDialoguePriority) .. ".1"; - - else - - DialogueGUI:Destroy(); - DialogueModule.isPlayerTakingWithNPC = false; - - end; - - elseif DialogueModule.isPlayerTakingWithNPC then - - -- There is a message; however, the player failed the condition. - -- Let's check if there's something else available. - local SplitPriority = currentDialoguePriority:split("."); - SplitPriority[#SplitPriority] = tostring(tonumber(SplitPriority[#SplitPriority]) :: number + 1); - currentDialoguePriority = table.concat(SplitPriority, "."); - - end; - - end; - - -- Free the player :) - ThemeChangedEvent:Disconnect(); - - end; - - DialogueModule.isPlayerTakingWithNPC = false; - -end; - -Player.CharacterRemoving:Connect(function() - - DialogueModule.isPlayerTakingWithNPC = false; - -end); - -return DialogueModule; diff --git a/src/DialoguePluginScript/DialogueClientScript/API/Player.lua b/src/DialoguePluginScript/DialogueClientScript/API/Player.lua deleted file mode 100644 index 1aaff76..0000000 --- a/src/DialoguePluginScript/DialogueClientScript/API/Player.lua +++ /dev/null @@ -1,17 +0,0 @@ ---!strict -local PlayerModule = {}; -local Player = game:GetService("Players").LocalPlayer; - -function PlayerModule.freezePlayer(): () - - require(Player.PlayerScripts:WaitForChild("PlayerModule")):GetControls():Disable(); - -end; - -function PlayerModule.unfreezePlayer(): () - - require(Player.PlayerScripts:WaitForChild("PlayerModule")):GetControls():Enable(); - -end; - -return PlayerModule; diff --git a/src/DialoguePluginScript/DialogueClientScript/Settings.lua b/src/DialoguePluginScript/DialogueClientScript/Settings.lua deleted file mode 100644 index e56dd26..0000000 --- a/src/DialoguePluginScript/DialogueClientScript/Settings.lua +++ /dev/null @@ -1,63 +0,0 @@ ---!strict -export type ClientSettings = { - - -- This is the default theme that will be used when talking with NPCs - defaultThemes: { - [number]: { - minimumViewportWidth: number; - minimumViewportHeight: number; - themeName: string; - } - }; - - -- Prevents the player from selecting responses without first viewing the dialogue - showResponsesAfterMessageFinished: boolean; - - -- Replace this with an audio ID that'll play every time a player continues a conversation or selects a response. Replace with 0 to not play any sound. - defaultClickSound: number; - - -- Minimum distance from a character required for keybinds should work - minimumDistanceFromCharacter: number; - - -- Whether keybinds should work - keybindsEnabled: boolean; - - -- Keyboard keybind to start a conversation with an NPC - defaultChatTriggerKey: Enum.KeyCode; - - -- Gamepad keybind to start a conversation with an NPC - defaultChatTriggerKeyGamepad: Enum.KeyCode; - - -- Keyboard keybind to continue a conversation with an NPC - defaultChatContinueKey: Enum.KeyCode; - - -- Gamepad keybind to continue a conversation with an NPC - defaultChatContinueKeyGamepad: Enum.KeyCode; -}; - -local Settings: ClientSettings = { - - -- [ Theme Settings ] -- - defaultThemes = { - { - minimumViewportWidth = 0; - minimumViewportHeight = 0; - themeName = "BigAndBoldDialogue"; - } - }; - - -- [ Response Settings ] -- - showResponsesAfterMessageFinished = true; - defaultClickSound = 0; - - -- [ Chat Triggers and Keybinds ] -- - minimumDistanceFromCharacter = 10; - keybindsEnabled = true; - defaultChatTriggerKey = Enum.KeyCode.F; - defaultChatTriggerKeyGamepad = Enum.KeyCode.ButtonX; - defaultChatContinueKey = Enum.KeyCode.F; - defaultChatContinueKeyGamepad = Enum.KeyCode.ButtonA; - -}; - -return Settings; \ No newline at end of file diff --git a/src/DialoguePluginScript/DialogueMakerGUI.rbxm b/src/DialoguePluginScript/DialogueMakerGUI.rbxm deleted file mode 100644 index eb832d8..0000000 Binary files a/src/DialoguePluginScript/DialogueMakerGUI.rbxm and /dev/null differ diff --git a/src/DialoguePluginScript/ReactComponents/DialogueItem.lua b/src/DialoguePluginScript/ReactComponents/DialogueItem.lua new file mode 100644 index 0000000..6c95ebc --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/DialogueItem.lua @@ -0,0 +1,319 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Colors = require(script.Parent.Parent.Colors); +local Dropdown = require(script.Parent.Dropdown); +local useDialogueContainer = require(script.Parent.Parent.ReactHooks.useDialogueContainer); +local DropdownOption = require(script.Parent.DropdownOption) + +export type DialogueItemProperties = { + type: "Response" | "Message" | "Redirect"; + contentScript: ModuleScript; + setDialogueParent: (ModuleScript | Folder) -> (); + isDeleteModeEnabled: boolean; + layoutOrder: number; + zIndex: number; + priority: string; + dialogueParent: ModuleScript | Folder; + plugin: Plugin; +} + +local function DialogueItem(props: DialogueItemProperties) + + local showDeletionConfirmation, setShowDeletionConfirmation = React.useState(false); + local isDialogueTypeDropdownOpen, setIsDialogueTypeDropdownOpen = React.useState(false); + local isConnectionsDropdownOpen, setIsConnectionsDropdownOpen = React.useState(false); + local isDeleteModeEnabled = props.isDeleteModeEnabled; + local dialogueContainer = useDialogueContainer(props.dialogueParent); + local contentScript = props.contentScript; + + local function openSpecialScript(scriptType: "Action" | "Condition"): () + + -- Create a special script if necessary. + local specialScript = contentScript:FindFirstChild(scriptType) :: ModuleScript?; + + if not specialScript then + + local newSpecialScript = script.Parent.Parent.Templates[`{scriptType}Template`]:Clone(); + newSpecialScript.Name = scriptType; + newSpecialScript.Parent = contentScript; + specialScript = newSpecialScript; + + end; + + -- Open the condition script + props.plugin:OpenScript(specialScript :: ModuleScript); + + end; + + local isResponse = props.type == "Response"; + local isRedirect = props.type == "Redirect"; + + return React.createElement("Frame", { + [React.Event.InputEnded] = function(self: Frame, input: InputObject) + + if input.UserInputType == Enum.UserInputType.MouseButton1 then + + setShowDeletionConfirmation(true); + + end + + end; + BackgroundColor3 = if isResponse then Colors.backgroundResponse else Colors.backgroundRedirect; + BackgroundTransparency = if isResponse or isRedirect then 0.4 else 1; + BorderSizePixel = 0; + ZIndex = props.zIndex; + LayoutOrder = props.layoutOrder; + Size = UDim2.new(1, 0, 0, 22); + }, { + DeletionConfirmationFrame = if showDeletionConfirmation then React.createElement("Frame", { + ZIndex = 2; + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 0.5; + BackgroundColor3 = Colors.backgroundDeletionFrame; + BorderSizePixel = 0; + }, { + UIListLayout = React.createElement("UIListLayout", { + FillDirection = Enum.FillDirection.Horizontal; + SortOrder = Enum.SortOrder.LayoutOrder; + HorizontalAlignment = Enum.HorizontalAlignment.Center; + VerticalAlignment = Enum.VerticalAlignment.Center; + Padding = UDim.new(0, 5); + }); + QuestionTextLabel = React.createElement("TextLabel", { + BackgroundTransparency = 1; + LayoutOrder = 1; + Text = "Delete?"; + TextColor3 = Colors.text; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Bold); + AutomaticSize = Enum.AutomaticSize.XY; + TextSize = 16; + }); + ConfirmButton = React.createElement("TextButton", { + LayoutOrder = 2; + BackgroundColor3 = Colors.backgroundWarning; + Text = "Yes"; + TextSize = 16; + TextColor3 = Colors.text; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Regular); + BorderSizePixel = 0; + AutomaticSize = Enum.AutomaticSize.XY; + [React.Event.Activated] = function() + + contentScript:Destroy(); + setShowDeletionConfirmation(false); + + end; + }, { + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 5); + PaddingRight = UDim.new(0, 5); + }); + }); + CancelButton = React.createElement("TextButton", { + BackgroundTransparency = 1; + LayoutOrder = 3; + Text = "No"; + TextColor3 = Colors.text; + TextSize = 16; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Regular); + AutomaticSize = Enum.AutomaticSize.XY; + BorderSizePixel = 0; + [React.Event.Activated] = function() + + setShowDeletionConfirmation(false); + + end; + }, { + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 5); + PaddingRight = UDim.new(0, 5); + }); + }); + }) else nil; + Content = React.createElement("Frame", { + ZIndex = 1; + BackgroundTransparency = 1; + Size = UDim2.new(1, 0, 1, 0); + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + Padding = UDim.new(0, 3); + }); + PriorityTextBox = React.createElement(if showDeletionConfirmation then "TextLabel" else "TextBox", { + Text = props.priority; + PlaceholderText = if showDeletionConfirmation then nil else props.priority; + TextColor3 = Colors.text; + PlaceholderColor3 = if showDeletionConfirmation then nil else Colors.textPlaceholder; + LayoutOrder = 1; + Size = UDim2.new(0, 60, 1, 0); + BackgroundTransparency = 1; + [React.Event.FocusLost] = if showDeletionConfirmation then nil else function(self) + + -- Make sure the priority is valid + local isUserTextInvalid = false; + local userText = self.Text :: string; + if userText:sub(1, 1) == "." or userText:sub(userText:len()) == "." then + + isUserTextInvalid = true; + + end; + + local currentDirectory: Folder | ModuleScript = dialogueContainer; + local splitPriority = userText:split("."); + if not isUserTextInvalid then + + for index, priority in splitPriority do + + -- Make sure everyone's a number + if not tonumber(priority) then + + warn("[Dialogue Maker] " .. userText .. " is not a valid priority. Make sure you're not using any characters other than numbers and periods."); + isUserTextInvalid = true; + break; + + end; + + -- Make sure the folder exists + local TargetDirectory = currentDirectory:FindFirstChild(priority); + if not TargetDirectory and index ~= #splitPriority then + + warn("[Dialogue Maker] " .. userText .. " is not a valid priority. Make sure all parent directories exist."); + isUserTextInvalid = true; + break; + + elseif index == #splitPriority then + + if TargetDirectory then + + warn("[Dialogue Maker] " .. userText .. " is not a valid priority. Make sure that " .. userText .. " isn't already being used."); + isUserTextInvalid = true; + + else + + local UserSplitPriority = userText:split("."); + props.contentScript.Name = UserSplitPriority[#UserSplitPriority]; + contentScript.Parent = currentDirectory; + + end; + break; + + end; + + currentDirectory = currentDirectory:FindFirstChild(priority) :: ModuleScript; + + end; + + end; + + if isUserTextInvalid then + + -- Reset the text. + self.Text = props.priority; + + end; + + end; + }); + DialogueTypeDropdown = React.createElement(Dropdown, { + layoutOrder = 2; + size = UDim2.new(0, 0, 1, 0); + text = props.type; + isDisabled = isDeleteModeEnabled; + toggleButtonChildren = { + UIFlexItem = React.createElement("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Fill; + }); + }; + isOpen = isDialogueTypeDropdownOpen; + setIsOpen = setIsDialogueTypeDropdownOpen; + }, { + MessageButton = React.createElement(DropdownOption, { + onClick = function() + + props.contentScript:SetAttribute("DialogueType", "Message"); + setIsDialogueTypeDropdownOpen(false); + + end; + layoutOrder = 1; + text = "Message"; + iconImage = "rbxassetid://14099768265"; + }); + RedirectButton = React.createElement(DropdownOption, { + onClick = function() + + props.contentScript:SetAttribute("DialogueType", "Redirect"); + setIsDialogueTypeDropdownOpen(false); + + end; + layoutOrder = 2; + text = "Redirect"; + iconImage = "rbxassetid://14099768484"; + }); + ResponseButton = React.createElement(DropdownOption, { + onClick = function() + + props.contentScript:SetAttribute("DialogueType", "Response"); + setIsDialogueTypeDropdownOpen(false); + + end; + layoutOrder = 1; + text = "Response"; + iconImage = "rbxassetid://14099769224"; + }); + }); + ConnectionsDropDown = React.createElement(Dropdown, { + text = "View"; + layoutOrder = 3; + size = UDim2.new(0, 90, 1, 0); + isDisabled = isDeleteModeEnabled; + isOpen = isConnectionsDropdownOpen; + setIsOpen = setIsConnectionsDropdownOpen; + }, { + ViewContentButton = React.createElement(DropdownOption, { + layoutOrder = 1; + text = "Content"; + onClick = function() + + props.plugin:OpenScript(props.contentScript); + setIsConnectionsDropdownOpen(false); + + end; + }); + ViewChildrenButton = if isRedirect then nil else React.createElement(DropdownOption, { + layoutOrder = 2; + text = "Children"; + onClick = function() + + props.setDialogueParent(props.contentScript); + setIsConnectionsDropdownOpen(false); + + end; + }); + ConditionButton = React.createElement(DropdownOption, { + layoutOrder = 3; + text = "Condition"; + onClick = function() + + openSpecialScript("Condition"); + setIsConnectionsDropdownOpen(false); + + end; + }, {}); + ActionButton = if isRedirect or isResponse then nil else React.createElement(DropdownOption, { + layoutOrder = 4; + text = "Action"; + onClick = function() + + openSpecialScript("Action"); + setIsConnectionsDropdownOpen(false); + + end; + }, {}); + }); + }); + }) + +end; + +return DialogueItem; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/DialogueTable.lua b/src/DialoguePluginScript/ReactComponents/DialogueTable.lua new file mode 100644 index 0000000..eda7d21 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/DialogueTable.lua @@ -0,0 +1,38 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local DialogueTableHeader = require(script.Parent.DialogueTableHeader); +local DialogueTableBody = require(script.Parent.DialogueTableBody); + +export type DialogueTableProperties = { + isDeleteModeEnabled: boolean; + dialogueParent: ModuleScript | Folder; + setDialogueParent: (ModuleScript | Folder) -> (); + plugin: Plugin; +} + +local function DialogueTable(props: DialogueTableProperties) + + return React.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + LayoutOrder = 2; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + Padding = UDim.new(0, 2); + }); + UIFlexItem = React.createElement("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Shrink; + }); + DialogueTableHeader = React.createElement(DialogueTableHeader); + DialogueTableBody = React.createElement(DialogueTableBody, { + dialogueParent = props.dialogueParent; + isDeleteModeEnabled = props.isDeleteModeEnabled; + setDialogueParent = props.setDialogueParent; + plugin = props.plugin; + }); + }) + +end; + +return DialogueTable; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/DialogueTableBody.lua b/src/DialoguePluginScript/ReactComponents/DialogueTableBody.lua new file mode 100644 index 0000000..7db7955 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/DialogueTableBody.lua @@ -0,0 +1,129 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local DialogueItem = require(script.Parent.DialogueItem); +local Colors = require(script.Parent.Parent.Colors); + +export type DialogueTableBodyProperties = { + dialogueParent: ModuleScript | Folder; + setDialogueParent: (ModuleScript | Folder) -> (); + isDeleteModeEnabled: boolean; + plugin: Plugin; +} + +local function DialogueTableBody(props: DialogueTableBodyProperties) + + local dialogueParent = props.dialogueParent; + local isDeleteModeEnabled = props.isDeleteModeEnabled; + local dialogueItems, setDialogueItems = React.useState({}); + React.useEffect(function() + + local contentScriptConnections: {RBXScriptConnection} = {}; + + local function refreshTable() + + for _, connection in contentScriptConnections do + + connection:Disconnect(); + + end; + + -- Separate the dialogue item types. + local responses: {ModuleScript} = {}; + local messages: {ModuleScript} = {}; + local redirects: {ModuleScript} = {}; + for _, PossibleDialogueItem in dialogueParent:GetChildren() do + + if PossibleDialogueItem:IsA("ModuleScript") and tonumber(PossibleDialogueItem.Name) then + + -- Get the dialogue item type. + local DialogueType = PossibleDialogueItem:GetAttribute("DialogueType"); + table.insert(if DialogueType == "Response" then responses elseif DialogueType == "Message" then messages else redirects, PossibleDialogueItem); + + end + + end + + -- Sort the directory based on priority + local function sortByMessagePriority(dialogueA: ModuleScript, dialogueB: ModuleScript) + + local messageAPriority = tonumber(dialogueA.Name) or math.huge; + local messageBPriority = tonumber(dialogueB.Name) or math.huge; + + return messageAPriority < messageBPriority; + + end; + + table.sort(redirects, sortByMessagePriority); + table.sort(responses, sortByMessagePriority); + table.sort(messages, sortByMessagePriority); + + -- Create new status + local currentZIndex = #redirects + #responses + #messages; + local dialogueItems = {}; + local currentLayoutOrder = 1; + for categoryIndex, category in {redirects, responses, messages} do + + for _, childContentScript in category do + + -- Make sure the message container is completely visible, even when dropdowns are open. + local dialogueItem = React.createElement(DialogueItem, { + type = ({"Redirect", "Response", "Message"})[categoryIndex]; + layoutOrder = currentLayoutOrder; + zIndex = currentZIndex; + contentScript = childContentScript; + isDeleteModeEnabled = isDeleteModeEnabled; + priority = childContentScript.Name; + dialogueParent = dialogueParent; + setDialogueParent = props.setDialogueParent; + plugin = props.plugin; + }); + + table.insert(dialogueItems, dialogueItem); + + currentZIndex -= 1; + currentLayoutOrder += 1; + + table.insert(contentScriptConnections, childContentScript:GetPropertyChangedSignal("Name"):Connect(refreshTable)); + table.insert(contentScriptConnections, childContentScript:GetAttributeChangedSignal("DialogueType"):Connect(refreshTable)); + + end; + + end; + setDialogueItems(dialogueItems); + + end; + + local childAddedEvent = dialogueParent.ChildAdded:Connect(refreshTable); + local childRemovedEvent = dialogueParent.ChildRemoved:Connect(refreshTable); + refreshTable(); + + return function() + + childAddedEvent:Disconnect(); + childRemovedEvent:Disconnect(); + + end; + + end, {dialogueParent :: any, isDeleteModeEnabled}); + + return React.createElement("ScrollingFrame", { + LayoutOrder = 2; + Size = UDim2.new(1, 0, 1, 0); + BackgroundColor3 = Colors.backgroundTableRow; + BorderSizePixel = 0; + AutomaticCanvasSize = Enum.AutomaticSize.Y; + CanvasSize = UDim2.new(0, 0, 0, 0); + ScrollingDirection = Enum.ScrollingDirection.Y; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + }); + UIFlexItem = React.createElement("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Shrink; + }); + Children = React.createElement(React.Fragment, {}, {dialogueItems}); + }) + +end; + +return DialogueTableBody; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/DialogueTableHeader.lua b/src/DialoguePluginScript/ReactComponents/DialogueTableHeader.lua new file mode 100644 index 0000000..3f1de26 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/DialogueTableHeader.lua @@ -0,0 +1,64 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Colors = require(script.Parent.Parent.Colors); + +local function DialogueTableHeader() + + return React.createElement("Frame", { + LayoutOrder = 1; + BackgroundColor3 = Color3.fromRGB(57, 57, 57); + Size = UDim2.new(1, 0, 0, 22); + BorderSizePixel = 0; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + Padding = UDim.new(0, 3); + }); + PriorityTextLabel = React.createElement("TextLabel", { + LayoutOrder = 1; + Text = "Priority"; + BorderSizePixel = 0; + BackgroundColor3 = Colors.backgroundTableHeader; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Medium); + Size = UDim2.new(0, 60, 1, 0); + TextSize = 14; + TextColor3 = Colors.text; + TextXAlignment = Enum.TextXAlignment.Center; + }); + TypeTextLabel = React.createElement("TextLabel", { + LayoutOrder = 2; + BorderSizePixel = 0; + BackgroundColor3 = Colors.backgroundTableHeader; + Text = "Type"; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Medium); + Size = UDim2.new(0, 0, 1, 0); + AutomaticSize = Enum.AutomaticSize.X; + TextSize = 14; + TextColor3 = Colors.text; + TextXAlignment = Enum.TextXAlignment.Left; + }, { + UIFlexItem = React.createElement("UIFlexItem", { + FlexMode = Enum.UIFlexMode.Fill; + }); + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 5); + PaddingRight = UDim.new(0, 5); + }); + }); + ConnectionsTextLabel = React.createElement("TextLabel", { + LayoutOrder = 4; + Text = "Connections"; + BorderSizePixel = 0; + BackgroundColor3 = Colors.backgroundTableHeader; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Medium); + Size = UDim2.new(0, 90, 1, 0); + TextSize = 14; + TextColor3 = Colors.text; + TextXAlignment = Enum.TextXAlignment.Center; + }); + }) + +end; + +return DialogueTableHeader; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/Dropdown.lua b/src/DialoguePluginScript/ReactComponents/Dropdown.lua new file mode 100644 index 0000000..47b9b98 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/Dropdown.lua @@ -0,0 +1,100 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Colors = require(script.Parent.Parent.Colors); + +export type DropdownProps = { + text: string?; + children: any; + size: UDim2?; + layoutOrder: number?; + automaticSize: Enum.AutomaticSize?; + toggleButtonChildren: any; + isDisabled: boolean?; + isOpen: boolean; + setIsOpen: (boolean) -> (); +} + +local function Dropdown(props: DropdownProps) + + return React.createElement("Frame", { + BackgroundTransparency = 1; + Size = props.size; + LayoutOrder = props.layoutOrder; + AutomaticSize = props.automaticSize; + }, { + Children = React.createElement(React.Fragment, {}, {props.toggleButtonChildren}); + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + Padding = UDim.new(0, 5); + }); + ToggleButton = React.createElement(if props.isDisabled then "TextLabel" else "TextButton", { + LayoutOrder = 1; + Size = UDim2.new(1, 0, 1, 0); + BackgroundColor3 = Color3.fromRGB(70, 70, 70); + [React.Event.Activated] = if props.isDisabled then nil else function() + + props.setIsOpen(not props.isOpen); + + end; + Text = ""; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween; + HorizontalAlignment = Enum.HorizontalAlignment.Center; + VerticalAlignment = Enum.VerticalAlignment.Center; + }); + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 5); + PaddingRight = UDim.new(0, 5); + }); + TextLabel = if props.text == "" then nil else React.createElement("TextLabel", { + LayoutOrder = 1; + Text = props.text; + TextXAlignment = if props.text == "" then Enum.TextXAlignment.Center else Enum.TextXAlignment.Left; + TextColor3 = Colors.text; + FontFace = Font.fromId(11702779517); + TextSize = 14; + }); + DropdownIconLabel = React.createElement("ImageLabel", { + LayoutOrder = 2; + Image = "rbxassetid://14098646461"; + Size = UDim2.new(0, 20, 0, 20); + BackgroundTransparency = 1; + }); + UICorner = React.createElement("UICorner", { + CornerRadius = UDim.new(0, 15); + }); + }); + OptionsFrame = if props.isOpen and not props.isDisabled then React.createElement("ScrollingFrame", { + LayoutOrder = 2; + Size = UDim2.new(1, 0, 0, 0); + AutomaticCanvasSize = Enum.AutomaticSize.Y; + AutomaticSize = Enum.AutomaticSize.Y; + CanvasSize = UDim2.new(1, 0, 1, 0); + [React.Event.InputEnded] = function(self, input) + + if input == Enum.UserInputType.MouseButton1 then + + props.setIsOpen(not props.isOpen); + + end; + + end; + }, { + UICorner = React.createElement("UICorner", { + CornerRadius = UDim.new(0, 15); + }); + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + }); + Children = React.createElement(React.Fragment, {}, { + props.children; + }); + }) else nil; + }); + +end; + +return Dropdown; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/DropdownOption.lua b/src/DialoguePluginScript/ReactComponents/DropdownOption.lua new file mode 100644 index 0000000..f05fe67 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/DropdownOption.lua @@ -0,0 +1,56 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Colors = require(script.Parent.Parent.Colors); + +export type DropdownOptionProperties = { + layoutOrder: number; + onClick: () -> (); + iconImage: string?; + text: string; +} + +local function DropdownOption(props: DropdownOptionProperties) + + return React.createElement("TextButton", { + LayoutOrder = 1; + Size = UDim2.new(1, 0, 0, 25); + BackgroundColor3 = Color3.fromRGB(70, 70, 70); + Text = ""; + [React.Event.Activated] = function() + + props.onClick(); + + end; + BorderSizePixel = 0; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + VerticalAlignment = Enum.VerticalAlignment.Center; + Padding = UDim.new(0, 5); + }); + IconLabel = if props.iconImage then React.createElement("ImageLabel", { + LayoutOrder = 1; + Image = props.iconImage; + Size = UDim2.new(0, 20, 0, 20); + BackgroundTransparency = 1; + }) else nil; + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 5); + PaddingRight = UDim.new(0, 5); + }); + TextLabel = React.createElement("TextLabel", { + AutomaticSize = Enum.AutomaticSize.XY; + Size = UDim2.new(0, 0, 0, 0); + LayoutOrder = 2; + Text = props.text; + FontFace = Font.fromId(11702779517); + BackgroundTransparency = 1; + TextSize = 14; + TextColor3 = Colors.text; + }); + }); + +end; + +return DropdownOption; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/StatusSection.lua b/src/DialoguePluginScript/ReactComponents/StatusSection.lua new file mode 100644 index 0000000..8805afe --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/StatusSection.lua @@ -0,0 +1,36 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local useViewingPriority = require(script.Parent.Parent.ReactHooks.useViewingPriority); +local Colors = require(script.Parent.Parent.Colors); + +export type StatusSectionProperties = { + dialogueParent: ModuleScript | Folder; +} + +local function StatusSection(props: StatusSectionProperties) + + local viewingPriority = useViewingPriority(props.dialogueParent); + + return React.createElement("TextLabel", { + LayoutOrder = 3; + Text = `Viewing {if viewingPriority == "" then "the beginning of the conversation" else viewingPriority}`; + TextColor3 = Colors.text; + BorderSizePixel = 0; + BackgroundTransparency = 1; + TextSize = 14; + FontFace = Font.fromId(11702779517, Enum.FontWeight.Regular); + Size = UDim2.new(1, 0, 0, 0); + AutomaticSize = Enum.AutomaticSize.Y; + TextXAlignment = Enum.TextXAlignment.Left; + }, { + UIPadding = React.createElement("UIPadding", { + PaddingTop = UDim.new(0, 5); + PaddingBottom = UDim.new(0, 5); + PaddingLeft = UDim.new(0, 10); + PaddingRight = UDim.new(0, 10); + }); + }); + +end; + +return StatusSection; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/Toolbar.lua b/src/DialoguePluginScript/ReactComponents/Toolbar.lua new file mode 100644 index 0000000..3d3140c --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/Toolbar.lua @@ -0,0 +1,101 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local ToolbarButton = require(script.Parent.ToolbarButton); + +type ToolbarProps = { + dialogueParent: ModuleScript | Folder; + setDialogueParent: (ModuleScript | Folder) -> (); + model: Model; + repairNPC: () -> (); + isDeleteModeEnabled: boolean; + setIsDeleteModeEnabled: (boolean) -> (); + plugin: Plugin; +} + +local function Toolbar(props: ToolbarProps) + + local dialogueParent = props.dialogueParent; + + return React.createElement("Frame", { + Size = UDim2.new(1, 0, 0, 40); + LayoutOrder = 1; + BackgroundColor3 = Color3.fromRGB(74, 74, 74); + BorderSizePixel = 0; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + }); + ViewParentButton = React.createElement(ToolbarButton, { + iconImage = "rbxassetid://14098871159"; + text = "View parent"; + layoutOrder = 1; + isDisabled = dialogueParent:IsA("Folder"); + onClick = function() + + props.setDialogueParent(props.dialogueParent.Parent :: ModuleScript | Folder); + + end; + }); + AddMessageButton = React.createElement(ToolbarButton, { + iconImage = "rbxassetid://14099284898"; + text = "Add message"; + layoutOrder = 2; + onClick = function() + + -- Ensure the NPC is properly configured. + props.repairNPC(); + + -- Find a name for the content script. + local targetPriority = 1; + for _, instance in dialogueParent:GetChildren() do + + local comparedName = tonumber(instance.Name); + if comparedName and comparedName >= targetPriority then + + targetPriority = comparedName + 1; + + end + + end; + + -- Create the content script. + local MessageContentScript = script.Parent.Parent.Templates.ContentTemplate:Clone(); + MessageContentScript.Name = targetPriority; + MessageContentScript:SetAttribute("DialogueType", "Message"); + MessageContentScript.Parent = dialogueParent; + + end; + }); + AdjustSettingsButton = React.createElement(ToolbarButton, { + iconImage = "rbxassetid://14099277263"; + text = "Adjust settings"; + layoutOrder = 3; + onClick = function() + + props.repairNPC(); + + local npcDialogueSettingsScript = props.model:FindFirstChild("NPCDialogueSettings") :: Script; + props.plugin:OpenScript(npcDialogueSettingsScript); + + end; + }); + ToggleDeleteModeButton = React.createElement(ToolbarButton, { + iconImage = "rbxassetid://14099268988"; + text = "Toggle delete mode"; + layoutOrder = 4; + BackgroundColor3 = if props.isDeleteModeEnabled then Color3.fromRGB(217, 39, 39) else nil; + isHighlighted = props.isDeleteModeEnabled; + onClick = function() + + local isDeleteModeEnabled = not props.isDeleteModeEnabled; + props.setIsDeleteModeEnabled(isDeleteModeEnabled); + print("[Dialogue Maker] " .. if isDeleteModeEnabled then "Warning: Delete Mode has been enabled!" else "Whew. Delete Mode has been disabled."); + + end; + }); + }); + +end; + +return Toolbar; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/ToolbarButton.lua b/src/DialoguePluginScript/ReactComponents/ToolbarButton.lua new file mode 100644 index 0000000..a848938 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/ToolbarButton.lua @@ -0,0 +1,66 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Colors = require(script.Parent.Parent.Colors); + +export type ToolbarButtonProps = { + iconImage: string; + text: string; + layoutOrder: number; + isDisabled: boolean?; + isHighlighted: boolean?; + onClick: () -> (); +}; + +local function ToolbarButton(props: ToolbarButtonProps) + + return React.createElement("TextButton", { + LayoutOrder = props.layoutOrder; + BackgroundTransparency = 0; + BackgroundColor3 = if props.isHighlighted then Colors.backgroundWarning else Color3.fromRGB(74, 74, 74); + AutoButtonColor = not props.isDisabled; + Text = ""; + Size = UDim2.new(0, 0, 0, 0); + AutomaticSize = Enum.AutomaticSize.XY; + BorderSizePixel = 0; + [React.Event.Activated] = function() + + if (not props.isDisabled) then + + props.onClick(); + + end; + + end; + }, { + UIPadding = React.createElement("UIPadding", { + PaddingLeft = UDim.new(0, 10); + PaddingRight = UDim.new(0, 10); + }); + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + FillDirection = Enum.FillDirection.Horizontal; + VerticalAlignment = Enum.VerticalAlignment.Center; + Padding = UDim.new(0, 5); + }); + IconImageLabel = React.createElement("ImageLabel", { + LayoutOrder = 1; + BackgroundTransparency = 1; + Image = props.iconImage; + Size = UDim2.new(0, 24, 0, 24); + ImageColor3 = if props.isDisabled then Colors.textDisabled else Colors.text; + }); + TextLabel = React.createElement("TextLabel", { + LayoutOrder = 2; + BackgroundTransparency = 1; + Text = props.text; + TextSize = 12; + Size = UDim2.new(0, 0, 1, 0); + AutomaticSize = Enum.AutomaticSize.X; + TextColor3 = if props.isDisabled then Colors.textDisabled else Colors.text; + FontFace = Font.fromId(11702779517); + }); + }); + +end; + +return ToolbarButton; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactComponents/Window.lua b/src/DialoguePluginScript/ReactComponents/Window.lua new file mode 100644 index 0000000..cdfe5e3 --- /dev/null +++ b/src/DialoguePluginScript/ReactComponents/Window.lua @@ -0,0 +1,48 @@ +--!strict +local React = require(script.Parent.Parent.Packages.react); +local Toolbar = require(script.Parent.Toolbar); +local StatusSection = require(script.Parent.StatusSection); +local DialogueTable = require(script.Parent.DialogueTable); + +export type WindowProperties = { + model: Model; + repairNPC: () -> (); + plugin: Plugin; +} + +local function Window(props: WindowProperties) + + local isDeleteModeEnabled, setIsDeleteModeEnabled = React.useState(false); + local dialogueParent, setDialogueParent = React.useState(props.model:FindFirstChild("DialogueContainer") :: (ModuleScript | Folder)); + + return React.createElement("Frame", { + Size = UDim2.new(1, 0, 1, 0); + BackgroundTransparency = 1; + }, { + UIListLayout = React.createElement("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder; + Padding = UDim.new(0, 0); + }); + Toolbar = React.createElement(Toolbar, { + isDeleteModeEnabled = isDeleteModeEnabled; + setIsDeleteModeEnabled = setIsDeleteModeEnabled; + dialogueParent = dialogueParent; + setDialogueParent = setDialogueParent; + plugin = props.plugin; + repairNPC = props.repairNPC; + model = props.model; + }); + DialogueTable = React.createElement(DialogueTable, { + isDeleteModeEnabled = isDeleteModeEnabled; + dialogueParent = dialogueParent; + setDialogueParent = setDialogueParent; + plugin = props.plugin; + }); + StatusSection = React.createElement(StatusSection, { + dialogueParent = dialogueParent; + }); + }); + +end; + +return Window; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactHooks/useDialogueContainer.lua b/src/DialoguePluginScript/ReactHooks/useDialogueContainer.lua new file mode 100644 index 0000000..ba7adb8 --- /dev/null +++ b/src/DialoguePluginScript/ReactHooks/useDialogueContainer.lua @@ -0,0 +1,18 @@ +--!strict +local function useDialogueContainer(dialogueParent: ModuleScript | Folder): Folder + + local possibleDialogueContainer = dialogueParent; + + while not possibleDialogueContainer:IsA("Folder") do + + local possibleParent = possibleDialogueContainer.Parent; + assert(possibleParent and (possibleParent:IsA("ModuleScript") or possibleParent:IsA("Folder"))); + possibleDialogueContainer = dialogueParent.Parent :: ModuleScript | Folder; + + end; + + return possibleDialogueContainer :: Folder; + +end; + +return useDialogueContainer; \ No newline at end of file diff --git a/src/DialoguePluginScript/ReactHooks/useViewingPriority.lua b/src/DialoguePluginScript/ReactHooks/useViewingPriority.lua new file mode 100644 index 0000000..50b1fcf --- /dev/null +++ b/src/DialoguePluginScript/ReactHooks/useViewingPriority.lua @@ -0,0 +1,21 @@ +--!strict +local function useViewingPriority(dialogueParent: ModuleScript | Folder): string + + local viewingPriority = ""; + + local possibleDialogueContainer = dialogueParent; + while not possibleDialogueContainer:IsA("Folder") do + + viewingPriority = `{dialogueParent.Name}{if viewingPriority ~= "" then `.{viewingPriority}` else ""}`; + + local possibleParent = possibleDialogueContainer.Parent; + assert(possibleParent and (possibleParent:IsA("ModuleScript") or possibleParent:IsA("Folder"))); + possibleDialogueContainer = possibleParent :: ModuleScript | Folder; + + end; + + return viewingPriority; + +end; + +return useViewingPriority; \ No newline at end of file diff --git a/src/DialoguePluginScript/ActionTemplate.lua b/src/DialoguePluginScript/Templates/ActionTemplate.lua similarity index 100% rename from src/DialoguePluginScript/ActionTemplate.lua rename to src/DialoguePluginScript/Templates/ActionTemplate.lua diff --git a/src/DialoguePluginScript/ConditionTemplate.lua b/src/DialoguePluginScript/Templates/ConditionTemplate.lua similarity index 100% rename from src/DialoguePluginScript/ConditionTemplate.lua rename to src/DialoguePluginScript/Templates/ConditionTemplate.lua diff --git a/src/DialoguePluginScript/ContentTemplate.lua b/src/DialoguePluginScript/Templates/ContentTemplate.lua similarity index 100% rename from src/DialoguePluginScript/ContentTemplate.lua rename to src/DialoguePluginScript/Templates/ContentTemplate.lua diff --git a/src/DialoguePluginScript/NPCSettingsTemplate.lua b/src/DialoguePluginScript/Templates/NPCSettingsTemplate.lua similarity index 75% rename from src/DialoguePluginScript/NPCSettingsTemplate.lua rename to src/DialoguePluginScript/Templates/NPCSettingsTemplate.lua index 6460baf..268f891 100644 --- a/src/DialoguePluginScript/NPCSettingsTemplate.lua +++ b/src/DialoguePluginScript/Templates/NPCSettingsTemplate.lua @@ -1,126 +1,124 @@ --!strict -local Types = require(game:GetService("StarterPlayer").StarterPlayerScripts.DialogueClientScript.Types) - -local settings: Types.NPCSettings = { +local settings = { general = { -- This will be the NPC name shown to the player. - npcName = "NPC"; + npcName = "NPC" :: string; -- When true, the NPC's name will be shown when the player talks to them. - showName = false; + showName = false :: boolean; -- When true, the NPCNameFrame will be automatically resized to fit NPC names. - fitName = true; + fitName = true :: boolean; -- If General.FitName is true, this value will be added to the TextBounds offset of the NPC's name. - textBoundsOffset = 30; + textBoundsOffset = 30 :: number; -- Change this to a theme you've added to the Themes folder in order to override default theme settings. - themeName = ""; + themeName = "" :: string; -- Change this to the amount of seconds you want to wait before the next letter in the NPC's message is shown. -- [accepts number >= 0] - letterDelay = 0.025; + letterDelay = 0.025 :: number; -- If true, this allows the player to show all of the message without waiting for it to be pieced back together. - allowPlayerToSkipDelay = true; + allowPlayerToSkipDelay = true :: boolean; -- If true, the player will freeze when the dialogue starts and will be unfrozen when the dialogue ends. - freezePlayer = true; + freezePlayer = true :: boolean; -- If true, the conversation will end if the PrimaryParts of the NPC and the player exceed the MaximumConversationDistance. - endConversationIfOutOfDistance = false; + endConversationIfOutOfDistance = false :: boolean; -- Maximum magnitude between the NPC's HumanoidRootPart and the player's PrimaryPart before the conversation ends. Requires EndConversationIfOutOfDistance to be true. - maxConversationDistance = 10; + maxConversationDistance = 10 :: number; -- If true, the NPC will look at the player character during dialogue. Requires the NPC character and the player character to be Humanoids. - npcLooksAtPlayerDuringDialogue = false; + npcLooksAtPlayerDuringDialogue = false :: boolean; -- The maximum angle of the NPC's neck on the X axis. Requires NPCLooksAtPlayerDuringDialogue to be true. - npcNeckRotationMaxX = 0.8726; + npcNeckRotationMaxX = 0.8726 :: number; -- The maximum angle of the NPC's neck on the Y axis. Requires NPCLooksAtPlayerDuringDialogue to be true. - npcNeckRotationMaxY = 1.0472; + npcNeckRotationMaxY = 1.0472 :: number; -- The maximum angle of the NPC's neck on the Z axis. Requires NPCLooksAtPlayerDuringDialogue to be true. - npcNeckRotationMaxZ = 0.8726; + npcNeckRotationMaxZ = 0.8726 :: number; }; promptRegion = { -- Do you want the conversation to automatically start when the player touches a part? - enabled = false; + enabled = false :: boolean; -- Change this value to a part. (Ex. workspace.Part) - BasePart = nil; + BasePart = nil :: BasePart?; }; timeout = { -- When true, the conversation to automatically ends after ConversationTimeoutSeconds seconds. - enabled = false; + enabled = false :: boolean; -- Set this to the amount of seconds you want to wait before closing the dialogue. -- [accepts number >= 0] - seconds = 0; + seconds = 0 :: number; -- If true, this causes dialogue to ignore the set timeout in order to wait for the player's response. - waitForResponse = true; + waitForResponse = true :: boolean; }; speechBubble = { -- If true, this causes a speech bubble to appear over the NPC's head. - enabled = false; + enabled = false :: boolean; -- Set this to a BasePart to set the speech bubble's origin point. - BasePart = nil; + BasePart = nil :: BasePart?; -- Change this to a Roblox asset ID. Example: "rbxassetid://6403436054" - image = ""; + image = "" :: string; -- How big do you want the speech bubble to be? -- More info: https://create.roblox.com/docs/reference/engine/classes/BillboardGui#Size - size = UDim2.new(1, 0, 1, 0); + size = UDim2.new(1, 0, 1, 0) :: UDim2; -- How far do you want the bubble away from BasePart? -- More info: https://create.roblox.com/docs/reference/engine/classes/BillboardGui#StudsOffset - studsOffset = Vector3.new(0, 0, 0); + studsOffset = Vector3.new(0, 0, 0) :: Vector3; }; clickDetector = { -- If true, this causes the player to be able to trigger the dialogue by activating a ClickDetector. - enabled = false; + enabled = false :: boolean; -- If true, this automatically creates a ClickDetector inside of the NPC's model. - autoCreate = true; + autoCreate = true :: boolean; -- If true, the ClickDetector's parent will be nil until the dialogue is over. This hides the cursor from the player. - disappearsWhenDialogueActive = true; + disappearsWhenDialogueActive = true :: boolean; -- Replace this with the location of the ClickDetector. (Ex. workspace.Model.ClickDetector) This setting will be ignored if AutomaticallyCreateClickDetector is true. - Instance = nil; + Instance = nil :: ClickDetector?; }; proximityPrompt = { -- If true, this causes the player to be able to trigger the dialogue by activating the ProximityPrompt. You must set a PrimaryPart in your NPC model for this to work. - enabled = true; + enabled = true :: boolean; -- If true, this automatically creates a ProximityPrompt inside of the NPC's model. - autoCreate = true; + autoCreate = true :: boolean; -- The location of the ProximityPrompt. (Ex. workspace.Model.ProximityPrompt) This setting will be ignored if AutoCreate is true. - Instance = nil; + Instance = nil :: ProximityPrompt?; }; diff --git a/src/DialoguePluginScript/init.server.lua b/src/DialoguePluginScript/init.server.lua new file mode 100644 index 0000000..9047cdf --- /dev/null +++ b/src/DialoguePluginScript/init.server.lua @@ -0,0 +1,238 @@ +--!strict +local Selection = game:GetService("Selection"); +local StarterPlayer = game:GetService("StarterPlayer"); +local StarterPlayerScripts = StarterPlayer:FindFirstChild("StarterPlayerScripts"); +local ChangeHistoryService = game:GetService("ChangeHistoryService"); + +local React = require(script.Packages.react); +local ReactRoblox = require(script.Packages["react-roblox"]); +local Window = require(script.ReactComponents.Window); + +local EditDialogueButton: PluginToolbarButton; +local isDialogueEditorOpen = false; +local PluginGui: DockWidgetPluginGui?; + +-- Closes the editor when called +-- @since v1.0.0 +local function closeDialogueEditor(): () + + if PluginGui then PluginGui:Destroy(); end; + EditDialogueButton:SetActive(false); + isDialogueEditorOpen = false; + +end; + +local function repairNPC(model: Model): () + + if not model:FindFirstChild("DialogueContainer") then + + -- Add the dialogue container to the NPC + local DialogueContainer = Instance.new("Folder"); + DialogueContainer.Name = "DialogueContainer"; + + -- Add the dialogue folder to the model + DialogueContainer.Parent = model; + return; + + end; + + if not model:FindFirstChild("NPCDialogueSettings") then + + print(`[Dialogue Maker] Adding settings script to {model.Name}`); + + local SettingsScript = script.Templates.NPCSettingsTemplate:Clone(); + SettingsScript.Name = "NPCDialogueSettings"; + SettingsScript.Parent = model; + + print(`[Dialogue Maker] Added settings script to {model.Name}`) + + end; + + -- Initialize dialogue locations for indexing. + model:AddTag("DialogueMakerNPC"); + +end; + +-- Open the editor when called. +-- @since v1.0.0 +local function openDialogueEditor(model: Model): () + + PluginGui = plugin:CreateDockWidgetPluginGui(`Dialogue Maker - Model "{model.Name}"`, DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, true, 512, 241, 512, 150)); + if PluginGui then + + PluginGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling; + PluginGui.Title = `Dialogue Maker - Model "{model.Name}"`; + PluginGui:BindToClose(closeDialogueEditor); + + local pluginGUIRoot = ReactRoblox.createRoot(PluginGui); + pluginGUIRoot:render(React.createElement(Window, { + plugin = plugin; + model = model; + repairNPC = function() + + repairNPC(model); + + end; + })); + + end; + +end; + +local Toolbar = plugin:CreateToolbar("Dialogue Maker by Beastslash"); +EditDialogueButton = Toolbar:CreateButton("Edit Dialogue", "Edit dialogue of a selected NPC. The selected object must be a singular model.", "rbxassetid://14109181603"); +EditDialogueButton.Click:Connect(function() + + if isDialogueEditorOpen then + + closeDialogueEditor(); + return; + + end; + + local model: Model; + local isTestSuccessful, errorMessage = pcall(function() + + -- Check if the user is selecting an object. + local SelectedObjects = Selection:Get(); + assert(#SelectedObjects ~= 0, "You didn't select an object."); + assert(#SelectedObjects == 1, "You must select one object; not multiple objects."); + + -- Check if the model has a part + model = SelectedObjects[1] + assert(model:IsA("Model"), `You must select a Model, not a {model.ClassName}.`); + + local ModelHasPart = false; + for _, object in model:GetChildren() do + + if object:IsA("BasePart") then + + ModelHasPart = true; + break; + + end + + end; + + assert(ModelHasPart, "Your selected model doesn't have a part inside of it."); + + end); + + if not isTestSuccessful then + + EditDialogueButton:SetActive(false); + error("[Dialogue Maker] " .. errorMessage, 0); + + end + + -- Verify NPC dialogue folder + repairNPC(model); + + -- Add the chat receiver script in the StarterPlayerScripts. + if not StarterPlayerScripts:FindFirstChild("DialogueClientScript") then + + print("[Dialogue Maker] Adding DialogueClientScript to the StarterPlayerScripts..."); + local DialogueClientScript = script.DialogueClientScript:Clone() + DialogueClientScript.Parent = StarterPlayerScripts; + DialogueClientScript.Disabled = false; + print("[Dialogue Maker] Added DialogueClientScript to the StarterPlayerScripts."); + + end; + + -- Now we can open the dialogue editor. + openDialogueEditor(model); + +end); + +local ResetScriptsButton = Toolbar:CreateButton("Fix Scripts", "Reset DialogueMakerSharedDependencies and DialogueClientScript back to the a stable version.", "rbxassetid://14109193905"); +ResetScriptsButton.Click:Connect(function() + + -- Debounce + ResetScriptsButton.Enabled = false; + + local Success, Msg = pcall(function() + + -- Set an undo point just in case the user wants to revert this. + ChangeHistoryService:SetWaypoint("Resetting Dialogue Maker scripts"); + + -- Delete the old script + local oldDialogueClientScript = StarterPlayerScripts:FindFirstChild("DialogueClientScript"); + if oldDialogueClientScript then + + oldDialogueClientScript:Destroy(); + + end; + + -- Put the new instances in their places + local newDialogueClientScript = script.DialogueClientScript:Clone(); + newDialogueClientScript.Enabled = true; + newDialogueClientScript.Parent = StarterPlayerScripts; + + -- Finalize the undo point + ChangeHistoryService:SetWaypoint("Reset Dialogue Maker scripts"); + + end) + + -- Done! + ResetScriptsButton.Enabled = true; + print("[Dialogue Maker] " .. if Success then "Fixed Dialogue Maker scripts!" else ("Couldn't fix scripts: " .. Msg)); + +end); + +local RemoveUnusedInstancesButton = Toolbar:CreateButton("Remove Unused Instances", "Deletes unused actions, conditions, and dialogue locations.", "rbxassetid://14109207161") +RemoveUnusedInstancesButton.Click:Connect(function() + + RemoveUnusedInstancesButton.Enabled = false; + + local count = 0; + pcall(function() + + -- Set an undo point + ChangeHistoryService:SetWaypoint("Removing unused Dialogue Maker instances"); + + -- Remove the unused instances + for _, folder in StarterPlayerScripts.DialogueClientScript:GetChildren() do + + if not folder:IsA("Folder") then + + continue; + + end + + for _, child in folder:GetChildren() do + + if folder.Name == "Actions" then + + for _, module in child:GetChildren() do + + local NPC = module:FindFirstChild("NPC"); + if not NPC or not NPC.Value or not NPC.Value.Parent then + + count += 1; + module:Destroy(); + + end + + end + + elseif not child.Value or not child.Value.Parent then + + count += 1; + child:Destroy(); + + end; + + end; + + end; + + -- Finalize the undo point + ChangeHistoryService:SetWaypoint("Removed unused Dialogue Maker instances"); + end) + + -- Done! + RemoveUnusedInstancesButton.Enabled = true; + local plural = if count ~= 1 then "s" else ""; + print(`[Dialogue Maker] Removed unused {count} Dialogue Maker instance{plural}!`) + +end); \ No newline at end of file diff --git a/wally.lock b/wally.lock new file mode 100644 index 0000000..aab1e34 --- /dev/null +++ b/wally.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Wally. +# It is not intended for manual editing. +registry = "test" + +[[package]] +name = "beastslash/roblox-dialogue-maker" +version = "0.1.0" +dependencies = [["react", "jsdotlua/react@17.1.0"], ["react-roblox", "jsdotlua/react-roblox@17.1.0"]] + +[[package]] +name = "jsdotlua/boolean" +version = "1.2.7" +dependencies = [["number", "jsdotlua/number@1.2.7"]] + +[[package]] +name = "jsdotlua/collections" +version = "1.2.7" +dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"]] + +[[package]] +name = "jsdotlua/console" +version = "1.2.7" +dependencies = [["collections", "jsdotlua/collections@1.2.7"]] + +[[package]] +name = "jsdotlua/es7-types" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/instance-of" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/luau-polyfill" +version = "1.2.7" +dependencies = [["boolean", "jsdotlua/boolean@1.2.7"], ["collections", "jsdotlua/collections@1.2.7"], ["console", "jsdotlua/console@1.2.7"], ["es7-types", "jsdotlua/es7-types@1.2.7"], ["instance-of", "jsdotlua/instance-of@1.2.7"], ["math", "jsdotlua/math@1.2.7"], ["number", "jsdotlua/number@1.2.7"], ["string", "jsdotlua/string@1.2.7"], ["symbol-luau", "jsdotlua/symbol-luau@1.0.1"], ["timers", "jsdotlua/timers@1.2.7"]] + +[[package]] +name = "jsdotlua/math" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/number" +version = "1.2.7" +dependencies = [] + +[[package]] +name = "jsdotlua/promise" +version = "3.5.2" +dependencies = [] + +[[package]] +name = "jsdotlua/react" +version = "17.1.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["shared", "jsdotlua/shared@17.1.0"]] + +[[package]] +name = "jsdotlua/react-reconciler" +version = "17.1.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["promise", "jsdotlua/promise@3.5.2"], ["react", "jsdotlua/react@17.1.0"], ["scheduler", "jsdotlua/scheduler@17.1.0"], ["shared", "jsdotlua/shared@17.1.0"]] + +[[package]] +name = "jsdotlua/react-roblox" +version = "17.1.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["react", "jsdotlua/react@17.1.0"], ["react-reconciler", "jsdotlua/react-reconciler@17.1.0"], ["scheduler", "jsdotlua/scheduler@17.1.0"], ["shared", "jsdotlua/shared@17.1.0"]] + +[[package]] +name = "jsdotlua/scheduler" +version = "17.1.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"], ["shared", "jsdotlua/shared@17.1.0"]] + +[[package]] +name = "jsdotlua/shared" +version = "17.1.0" +dependencies = [["luau-polyfill", "jsdotlua/luau-polyfill@1.2.7"]] + +[[package]] +name = "jsdotlua/string" +version = "1.2.7" +dependencies = [["es7-types", "jsdotlua/es7-types@1.2.7"], ["number", "jsdotlua/number@1.2.7"]] + +[[package]] +name = "jsdotlua/symbol-luau" +version = "1.0.1" +dependencies = [] + +[[package]] +name = "jsdotlua/timers" +version = "1.2.7" +dependencies = [["collections", "jsdotlua/collections@1.2.7"]] diff --git a/wally.toml b/wally.toml new file mode 100644 index 0000000..c0f690f --- /dev/null +++ b/wally.toml @@ -0,0 +1,10 @@ +[package] +name = "beastslash/roblox-dialogue-maker" +version = "0.1.0" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +private = true + +[dependencies] +react = "jsdotlua/react@17.1.0" +react-roblox = "jsdotlua/react-roblox@17.1.0" \ No newline at end of file