mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
39 Commits
fix/subscr
...
feat/ai-la
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0c2913bbff | ||
![]() |
1d83da41a9 | ||
![]() |
a65f2174eb | ||
![]() |
9db493e87e | ||
![]() |
49a3a18b51 | ||
![]() |
0489e7036b | ||
![]() |
794d5a781c | ||
![]() |
c21dfbcc1f | ||
![]() |
cc75db6d09 | ||
![]() |
48d35a50b5 | ||
![]() |
3a0888046f | ||
![]() |
7917994835 | ||
![]() |
9e2bab43f7 | ||
![]() |
7e03f47f08 | ||
![]() |
7c6137a4fc | ||
![]() |
0cbd6e2fe9 | ||
![]() |
5acc1f5560 | ||
![]() |
887693a90d | ||
![]() |
fbb985833f | ||
![]() |
1b3734ff6b | ||
![]() |
6288b30ac3 | ||
![]() |
ee435f7081 | ||
![]() |
d75b4eb2a9 | ||
![]() |
5eeb505807 | ||
![]() |
30566e35ce | ||
![]() |
e058b7f295 | ||
![]() |
902a30a9f8 | ||
![]() |
8669b94fb0 | ||
![]() |
7fb08aea62 | ||
![]() |
e7a1548b0e | ||
![]() |
bdcb6f0533 | ||
![]() |
97849ce6f1 | ||
![]() |
d1c937caf4 | ||
![]() |
5ffe6279a2 | ||
![]() |
9260e2b77a | ||
![]() |
593465e2ec | ||
![]() |
8820350760 | ||
![]() |
7e1668229a | ||
![]() |
387b96b9a0 |
@@ -18,21 +18,21 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAnimatableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("AnimatableCapability",
|
||||
_("Animatable capability"),
|
||||
_("Animate objects."),
|
||||
_("Objects with animations"),
|
||||
_("Actions and conditions for objects having animations (sprite, 3D models...)."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Animatable capability"))
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Objects with animations"))
|
||||
.SetIcon("res/actions/animation24.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Animations and images"))
|
||||
.SetIcon("res/actions/animation24.png");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"AnimatableBehavior",
|
||||
_("Animatable capability"),
|
||||
_("Objects with animations"),
|
||||
"Animation",
|
||||
_("Animate objects."),
|
||||
_("Actions and conditions for objects having animations (sprite, 3D models...).."),
|
||||
"",
|
||||
"res/actions/animation24.png",
|
||||
"AnimatableBehavior",
|
||||
|
@@ -18,8 +18,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("EffectCapability",
|
||||
_("Effect capability"),
|
||||
_("Apply visual effects to objects."),
|
||||
_("Objects with effects"),
|
||||
_("Actions/conditions to enable/disable and change parameters of visual effects applied on objects."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
@@ -28,9 +28,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"EffectBehavior",
|
||||
_("Effect capability"),
|
||||
_("Objects with effects"),
|
||||
"Effect",
|
||||
_("Apply visual effects to objects."),
|
||||
_("Actions/conditions to enable/disable and change parameters of visual effects applied on objects."),
|
||||
"",
|
||||
"res/actions/effect_black.svg",
|
||||
"EffectBehavior",
|
||||
|
@@ -18,8 +18,8 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("FlippableCapability",
|
||||
_("Flippable capability"),
|
||||
_("Flip objects."),
|
||||
_("Flippable objects"),
|
||||
_("Actions/conditions for objects which can be flipped horizontally or vertically."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
@@ -28,9 +28,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"FlippableBehavior",
|
||||
_("Flippable capability"),
|
||||
_("Flippable objects"),
|
||||
"Flippable",
|
||||
_("Flip objects."),
|
||||
_("Actions/conditions for objects which can be flipped horizontally or vertically."),
|
||||
"",
|
||||
"res/actions/flipX24.png",
|
||||
"FlippableBehavior",
|
||||
|
@@ -18,27 +18,30 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("OpacityCapability",
|
||||
_("Opacity capability"),
|
||||
_("Change the object opacity."),
|
||||
_("Objects with opacity"),
|
||||
_("Action/condition/expression to change or "
|
||||
"check the opacity of an object (0-255)."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Opacity capability"))
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Objects with opacity"))
|
||||
.SetIcon("res/actions/opacity24.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Visibility"))
|
||||
.SetIcon("res/actions/opacity24.png");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"OpacityBehavior",
|
||||
_("Opacity capability"),
|
||||
"Opacity",
|
||||
_("Change the object opacity."),
|
||||
"",
|
||||
"res/actions/opacity24.png",
|
||||
"OpacityBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension
|
||||
.AddBehavior("OpacityBehavior",
|
||||
_("Objects with opacity"),
|
||||
"Opacity",
|
||||
_("Action/condition/expression to change or check the "
|
||||
"opacity of an object (0-255)."),
|
||||
"",
|
||||
"res/actions/opacity24.png",
|
||||
"OpacityBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
|
||||
aut.AddExpressionAndConditionAndAction(
|
||||
"number",
|
||||
@@ -52,8 +55,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "OpacityBehavior")
|
||||
.UseStandardParameters(
|
||||
"number", gd::ParameterOptions::MakeNewOptions().SetDescription(
|
||||
_("Opacity (0-255)")))
|
||||
"number",
|
||||
gd::ParameterOptions::MakeNewOptions().SetDescription(
|
||||
_("Opacity (0-255)")))
|
||||
.SetFunctionName("setOpacity")
|
||||
.SetGetter("getOpacity");
|
||||
aut.GetAllExpressions()["Value"].SetGroup("");
|
||||
|
@@ -16,11 +16,13 @@ namespace gd {
|
||||
void GD_CORE_API BuiltinExtensionsImplementer::ImplementsResizableExtension(
|
||||
gd::PlatformExtension &extension) {
|
||||
extension
|
||||
.SetExtensionInformation("ResizableCapability",
|
||||
_("Resizable capability"),
|
||||
_("Change the object dimensions."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionInformation(
|
||||
"ResizableCapability",
|
||||
_("Resizable objects"),
|
||||
_("Change or compare the size (width/height) of an object which can "
|
||||
"be resized (i.e: most objects)."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
|
||||
"res/actions/scale24_black.png");
|
||||
@@ -28,9 +30,10 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsResizableExtension(
|
||||
gd::BehaviorMetadata &aut =
|
||||
extension
|
||||
.AddBehavior("ResizableBehavior",
|
||||
_("Resizable capability"),
|
||||
_("Resizable objects"),
|
||||
"Resizable",
|
||||
_("Change the object dimensions."),
|
||||
_("Change or compare the size (width/height) of an "
|
||||
"object which can be resized (i.e: most objects)."),
|
||||
"",
|
||||
"res/actions/scale24_black.png",
|
||||
"ResizableBehavior",
|
||||
|
@@ -18,27 +18,30 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsScalableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("ScalableCapability",
|
||||
_("Scalable capability"),
|
||||
_("Change the object scale."),
|
||||
_("Scalable objects"),
|
||||
_("Actions/conditions/expression to change or "
|
||||
"check the scale of an object (default: 1)."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Scalable capability"))
|
||||
.SetIcon("res/actions/scale24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size"))
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Scalable objects"))
|
||||
.SetIcon("res/actions/scale24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
|
||||
"res/actions/scale24_black.png");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"ScalableBehavior",
|
||||
_("Scalable capability"),
|
||||
"Scale",
|
||||
_("Change the object scale."),
|
||||
"",
|
||||
"res/actions/scale24_black.png",
|
||||
"ResizableBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension
|
||||
.AddBehavior("ScalableBehavior",
|
||||
_("Scalable objects"),
|
||||
"Scale",
|
||||
_("Actions/conditions/expression to change or check the "
|
||||
"scale of an object (default: 1)."),
|
||||
"",
|
||||
"res/actions/scale24_black.png",
|
||||
"ResizableBehavior",
|
||||
std::make_shared<gd::Behavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>())
|
||||
.SetHidden();
|
||||
|
||||
aut.AddExpressionAndConditionAndAction(
|
||||
"number",
|
||||
|
@@ -18,17 +18,17 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTextContainerExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("TextContainerCapability",
|
||||
_("Text capability"),
|
||||
_("Objects containing a text"),
|
||||
_("Allows an object to contain a text, usually shown on screen, that can be modified."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Text capability"))
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Objects containing a text"))
|
||||
.SetIcon("res/conditions/text24_black.png");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"TextContainerBehavior",
|
||||
_("Text capability"),
|
||||
_("Objects containing a text"),
|
||||
"Text",
|
||||
_("Allows an object to contain a text, usually shown on screen, that can be modified."),
|
||||
"",
|
||||
|
@@ -16,7 +16,9 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinCommonConversions",
|
||||
_("Conversion"),
|
||||
"Expressions to convert number, texts and quantities.",
|
||||
"Expressions to convert numbers to string, strings to numbers, "
|
||||
"angles (degrees from/to radians) and a GDevelop variable to/from a "
|
||||
"JSON string.",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/common-conversions");
|
||||
@@ -41,7 +43,7 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
|
||||
extension
|
||||
.AddStrExpression("LargeNumberToString",
|
||||
_("Number > Text ( without scientific notation )"),
|
||||
_("Number > Text (without scientific notation)"),
|
||||
_("Convert the result of the expression to text, "
|
||||
"without using the scientific notation"),
|
||||
"",
|
||||
@@ -72,7 +74,8 @@ BuiltinExtensionsImplementer::ImplementsCommonConversionsExtension(
|
||||
_("Convert a variable to JSON"),
|
||||
_("JSON"),
|
||||
"res/conditions/toujours24_black.png")
|
||||
.AddParameter("variable", _("The variable to be stringified"),
|
||||
.AddParameter("variable",
|
||||
_("The variable to be stringified"),
|
||||
"AllowUndeclaredVariable");
|
||||
|
||||
// Deprecated
|
||||
|
@@ -15,10 +15,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinKeyboard",
|
||||
_("Keyboard"),
|
||||
_("Allows your game to respond to keyboard input. Note that this "
|
||||
_("Conditions to check keys pressed on a keyboard. Note that this "
|
||||
"does not work with on-screen keyboard on touch devices: use "
|
||||
"instead conditions related to touch when making a game for "
|
||||
"mobile/touchscreen devices."),
|
||||
"instead mouse/touch conditions when making a game for "
|
||||
"mobile/touchscreen devices or when making a new game from "
|
||||
"scratch."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/keyboard")
|
||||
@@ -84,7 +85,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsKeyboardExtension(
|
||||
"res/conditions/keyboard.png")
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
extension
|
||||
extension
|
||||
.AddCondition("AnyKeyReleased",
|
||||
_("Any key released"),
|
||||
_("Check if any key is released"),
|
||||
|
@@ -16,8 +16,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinMouse",
|
||||
_("Mouse and touch"),
|
||||
"Conditions and actions to handle either the mouse or touches on "
|
||||
"touchscreen. By default, conditions related to the mouse will also "
|
||||
"Conditions, actions and expressions to handle either the mouse or "
|
||||
"touches on a touchscreen. Notably: cursor position, mouse wheel, "
|
||||
"mouse buttons, touch positions, started/end touches, etc...\n"
|
||||
"\n"
|
||||
"By default, conditions related to the mouse will also "
|
||||
"handle the touches - so that it's easier to handle both in your "
|
||||
"game. You can disable this behavior if you want to handle them "
|
||||
"separately in different events.",
|
||||
@@ -273,28 +276,26 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsMouseExtension(
|
||||
.SetHidden();
|
||||
|
||||
extension
|
||||
.AddCondition(
|
||||
"MouseButtonFromTextPressed",
|
||||
_("Mouse button pressed or touch held"),
|
||||
_("Check if the specified mouse button is pressed or "
|
||||
"if a touch is in contact with the screen."),
|
||||
_("Touch or _PARAM1_ mouse button is down"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCondition("MouseButtonFromTextPressed",
|
||||
_("Mouse button pressed or touch held"),
|
||||
_("Check if the specified mouse button is pressed or "
|
||||
"if a touch is in contact with the screen."),
|
||||
_("Touch or _PARAM1_ mouse button is down"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("mouseButton", _("Button to check"))
|
||||
.MarkAsSimple();
|
||||
|
||||
extension
|
||||
.AddCondition(
|
||||
"MouseButtonFromTextReleased",
|
||||
_("Mouse button released"),
|
||||
_("Check if the specified mouse button was released."),
|
||||
_("Touch or _PARAM1_ mouse button is released"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCondition("MouseButtonFromTextReleased",
|
||||
_("Mouse button released"),
|
||||
_("Check if the specified mouse button was released."),
|
||||
_("Touch or _PARAM1_ mouse button is released"),
|
||||
"",
|
||||
"res/conditions/mouse24.png",
|
||||
"res/conditions/mouse.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("mouseButton", _("Button to check"))
|
||||
.MarkAsSimple();
|
||||
|
@@ -15,8 +15,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsNetworkExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinNetwork",
|
||||
_("Network"),
|
||||
_("Features to send web requests, communicate with external \"APIs\" "
|
||||
"and other network related tasks."),
|
||||
_("Actions to send web requests, communicate with external \"APIs\" "
|
||||
"and other network related tasks. Also contains an action to open "
|
||||
"a URL on the device browser."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/network")
|
||||
|
@@ -4,8 +4,8 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "AllBuiltinExtensions.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
|
||||
using namespace std;
|
||||
namespace gd {
|
||||
@@ -16,7 +16,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinScene",
|
||||
_("Scene"),
|
||||
_("Actions and conditions to manipulate the scenes during the game."),
|
||||
_("Actions/conditions to change the current scene (or pause it and "
|
||||
"launch another one, or go back to the previous one), check if a "
|
||||
"scene or the game has just started/resumed, preload assets of a "
|
||||
"scene, get the current scene name or loading progress, quit the "
|
||||
"game, set background color, or disable input when focus is lost."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("" /*TODO: Add a documentation page for this */);
|
||||
@@ -166,25 +170,28 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
extension
|
||||
.AddAction("PrioritizeLoadingOfScene",
|
||||
_("Preload scene"),
|
||||
_("Preload a scene resources as soon as possible in background."),
|
||||
_("Preload scene _PARAM1_ in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.AddAction(
|
||||
"PrioritizeLoadingOfScene",
|
||||
_("Preload scene"),
|
||||
_("Preload a scene resources as soon as possible in background."),
|
||||
_("Preload scene _PARAM1_ in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.SetHelpPath("/all-features/resources-loading")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("sceneName", _("Name of the new scene"))
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension.AddExpressionAndCondition("number",
|
||||
"SceneLoadingProgress",
|
||||
_("Scene loading progress"),
|
||||
_("The progress of resources loading in background for a scene (between 0 and 1)."),
|
||||
_("_PARAM1_ loading progress"),
|
||||
_(""),
|
||||
"res/actions/hourglass_black.svg")
|
||||
extension
|
||||
.AddExpressionAndCondition("number",
|
||||
"SceneLoadingProgress",
|
||||
_("Scene loading progress"),
|
||||
_("The progress of resources loading in "
|
||||
"background for a scene (between 0 and 1)."),
|
||||
_("_PARAM1_ loading progress"),
|
||||
_(""),
|
||||
"res/actions/hourglass_black.svg")
|
||||
.SetHelpPath("/all-features/resources-loading")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("sceneName", _("Scene name"))
|
||||
@@ -192,13 +199,14 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension(
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddCondition("AreSceneAssetsLoaded",
|
||||
_("Scene preloaded"),
|
||||
_("Check if scene resources have finished to load in background."),
|
||||
_("Scene _PARAM1_ was preloaded in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.AddCondition(
|
||||
"AreSceneAssetsLoaded",
|
||||
_("Scene preloaded"),
|
||||
_("Check if scene resources have finished to load in background."),
|
||||
_("Scene _PARAM1_ was preloaded in background"),
|
||||
"",
|
||||
"res/actions/hourglass_black.svg",
|
||||
"res/actions/hourglass_black.svg")
|
||||
.SetHelpPath("/all-features/resources-loading")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("sceneName", _("Scene name"))
|
||||
|
@@ -15,12 +15,13 @@ namespace gd {
|
||||
void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("Sprite",
|
||||
_("Sprite"),
|
||||
_("Sprite are animated object which can be used "
|
||||
"for most elements of a game."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionInformation(
|
||||
"Sprite",
|
||||
_("Sprite"),
|
||||
_("Sprite are animated objects which can be used "
|
||||
"for most elements of a 2D game."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/objects/sprite");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Sprite"))
|
||||
.SetIcon("CppPlatform/Extensions/spriteicon.png");
|
||||
@@ -30,7 +31,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
.AddObject<SpriteObject>("Sprite",
|
||||
_("Sprite"),
|
||||
_("Animated object which can be used for "
|
||||
"most elements of a game."),
|
||||
"most elements of a 2D game."),
|
||||
"CppPlatform/Extensions/spriteicon.png")
|
||||
.SetCategoryFullName(_("General"))
|
||||
.SetOpenFullEditorLabel(_("Edit animations"))
|
||||
@@ -645,11 +646,12 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSpriteExtension(
|
||||
"res/actions/sprite.png")
|
||||
.AddParameter("object", _("Object"), "Sprite");
|
||||
|
||||
obj.AddExpression("AnimationFrameCount",
|
||||
_("Number of frames"),
|
||||
_("Number of frames in the current animation of the object"),
|
||||
_("Animations and images"),
|
||||
"res/actions/sprite.png")
|
||||
obj.AddExpression(
|
||||
"AnimationFrameCount",
|
||||
_("Number of frames"),
|
||||
_("Number of frames in the current animation of the object"),
|
||||
_("Animations and images"),
|
||||
"res/actions/sprite.png")
|
||||
.AddParameter("object", _("Object"), "Sprite");
|
||||
|
||||
// Deprecated
|
||||
|
@@ -16,7 +16,8 @@ BuiltinExtensionsImplementer::ImplementsStringInstructionsExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinStringInstructions",
|
||||
_("Text manipulation"),
|
||||
"Provides expressions to manipulate strings (also called texts).",
|
||||
"Provides expressions to manipulate strings (also called texts): new "
|
||||
"line, upper/lowercase, substring, find, replace, etc...",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("" /*TODO: Add a documentation page for this */);
|
||||
@@ -191,7 +192,8 @@ BuiltinExtensionsImplementer::ImplementsStringInstructionsExtension(
|
||||
"res/conditions/toujours24_black.png")
|
||||
.AddParameter("string", _("Text in which the replacement must be done"))
|
||||
.AddParameter("string", _("Text to find inside the first text"))
|
||||
.AddParameter("string", _("Replacement to put instead of the text to find"));
|
||||
.AddParameter("string",
|
||||
_("Replacement to put instead of the text to find"));
|
||||
|
||||
extension
|
||||
.AddStrExpression("StrReplaceAll",
|
||||
@@ -199,10 +201,11 @@ BuiltinExtensionsImplementer::ImplementsStringInstructionsExtension(
|
||||
_("Replace all occurrences of a text by another."),
|
||||
"",
|
||||
"res/conditions/toujours24_black.png")
|
||||
.AddParameter("string", _("Text in which the replacement(s) must be done"))
|
||||
.AddParameter("string",
|
||||
_("Text in which the replacement(s) must be done"))
|
||||
.AddParameter("string", _("Text to find inside the first text"))
|
||||
.AddParameter("string", _("Replacement to put instead of the text to find"));
|
||||
|
||||
.AddParameter("string",
|
||||
_("Replacement to put instead of the text to find"));
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -15,9 +15,12 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinTime",
|
||||
_("Timers and time"),
|
||||
"Actions and conditions to run timers, get the current time or "
|
||||
"modify the time scale (speed at which the game is running - useful "
|
||||
"for slow motion effects).",
|
||||
"Actions and conditions to start, pause or reset scene timers, "
|
||||
"modify the time scale (speed at which the game "
|
||||
"is running - useful for slow motion effects). Also contains an "
|
||||
"action that wait for a delay before running the next actions and "
|
||||
"sub-events and expressions to read the time scale, time delta of "
|
||||
"the last frame or timer elapsed time.",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/timers-and-time");
|
||||
@@ -192,26 +195,28 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
|
||||
extension
|
||||
.AddExpression("TimerElapsedTime",
|
||||
_("Scene timer value"),
|
||||
_("Value of a scene timer"),
|
||||
_("Value of a scene timer (in seconds)"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("identifier", _("Timer's name"), "sceneTimer");
|
||||
|
||||
extension
|
||||
.AddExpression("TimeFromStart",
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddExpression(
|
||||
"TimeFromStart",
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
extension
|
||||
.AddExpression("TempsDebut",
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
_("Time elapsed since the beginning of the scene"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddExpression(
|
||||
"TempsDebut",
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
_("Time elapsed since the beginning of the scene (in seconds)."),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.SetHidden()
|
||||
.AddCodeOnlyParameter("currentScene", "");
|
||||
|
||||
@@ -226,16 +231,21 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsTimeExtension(
|
||||
extension
|
||||
.AddExpression("Time",
|
||||
_("Current time"),
|
||||
_("Current time"),
|
||||
_("Gives the current time"),
|
||||
"",
|
||||
"res/actions/time.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter(
|
||||
"stringWithSelector",
|
||||
_("Hour: hour - Minutes: min - Seconds: sec - Day of month: "
|
||||
"mday - Months since January: mon - Year since 1900: year - Days "
|
||||
"since Sunday: wday - Days since Jan 1st: yday - Timestamp (ms): "
|
||||
"timestamp\""),
|
||||
_("- Hour of the day: \"hour\"\n"
|
||||
"- Minutes: \"min\"\n"
|
||||
"- Seconds: \"sec\"\n"
|
||||
"- Day of month: \"mday\"\n"
|
||||
"- Months since January: \"mon\"\n"
|
||||
"- Year since 1900: \"year\"\n"
|
||||
"- Days since Sunday: \"wday\"\n"
|
||||
"- Days since Jan 1st: \"yday\"\n"
|
||||
"- Timestamp (ms): \"timestamp\""),
|
||||
"[\"hour\", \"min\", \"sec\", \"mon\", \"year\", \"wday\", \"mday\", "
|
||||
"\"yday\", \"timestamp\"]");
|
||||
}
|
||||
|
@@ -15,16 +15,17 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsWindowExtension(
|
||||
.SetExtensionInformation(
|
||||
"BuiltinWindow",
|
||||
_("Game window and resolution"),
|
||||
"Provides actions and conditions to manipulate the game window. "
|
||||
"Actions and conditions to manipulate the game window or change how "
|
||||
"the game is resized according to the screen size. "
|
||||
"Depending on the platform on which the game is running, not all of "
|
||||
"these features can be applied.",
|
||||
"these features can be applied.\n"
|
||||
"Also contains expressions to read the screen size.",
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("User interface")
|
||||
.SetExtensionHelpPath("/all-features/window");
|
||||
extension
|
||||
.AddInstructionOrExpressionGroupMetadata(
|
||||
_("Game window and resolution"))
|
||||
.AddInstructionOrExpressionGroupMetadata(_("Game window and resolution"))
|
||||
.SetIcon("res/actions/window24.png");
|
||||
|
||||
extension
|
||||
|
@@ -277,6 +277,10 @@ class GD_CORE_API MetadataProvider {
|
||||
return &metadata == &badObjectInfo;
|
||||
}
|
||||
|
||||
static bool IsBadEffectMetadata(const gd::EffectMetadata& metadata) {
|
||||
return &metadata == &badEffectMetadata;
|
||||
}
|
||||
|
||||
virtual ~MetadataProvider();
|
||||
|
||||
private:
|
||||
|
@@ -75,6 +75,17 @@ void ResourceExposer::ExposeProjectResources(
|
||||
// Expose global objects configuration resources
|
||||
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
|
||||
objectWorker.Launch(project.GetObjects());
|
||||
|
||||
// Exposed extension event resources
|
||||
// Note that using resources in extensions is very unlikely and probably not
|
||||
// worth the effort of something smart.
|
||||
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
|
||||
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
|
||||
e++) {
|
||||
auto &eventsFunctionsExtension = project.GetEventsFunctionsExtension(e);
|
||||
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
|
||||
project, eventsFunctionsExtension, eventWorker);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeLayoutResources(
|
||||
@@ -103,16 +114,6 @@ void ResourceExposer::ExposeLayoutResources(
|
||||
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
|
||||
gd::ProjectBrowserHelper::ExposeLayoutEventsAndDependencies(
|
||||
project, layout, eventWorker);
|
||||
|
||||
// Exposed extension event resources
|
||||
// Note that using resources in extensions is very unlikely and probably not
|
||||
// worth the effort of something smart.
|
||||
for (std::size_t e = 0; e < project.GetEventsFunctionsExtensionsCount();
|
||||
e++) {
|
||||
auto &eventsFunctionsExtension = project.GetEventsFunctionsExtension(e);
|
||||
gd::ProjectBrowserHelper::ExposeEventsFunctionsExtensionEvents(
|
||||
project, eventsFunctionsExtension, eventWorker);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeEffectResources(
|
||||
|
@@ -764,129 +764,6 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in event-based functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
|
||||
auto &function = extension.GetEventsFunctions().InsertNewEventsFunction(
|
||||
"MyFreeFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
auto& layout = project.InsertNewLayout("MyScene", 0);
|
||||
|
||||
// MyEventExtension::MyFreeFunction doesn't need to be actually used in
|
||||
// events because the implementation is naive.
|
||||
|
||||
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in event-based behavior functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
|
||||
auto& behavior = extension.GetEventsBasedBehaviors().InsertNew("MyBehavior", 0);
|
||||
auto& function = behavior.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
auto& layout = project.InsertNewLayout("MyScene", 0);
|
||||
|
||||
// MyEventExtension::MyBehavior::MyFunction doesn't need to be actually used in
|
||||
// events because the implementation is naive.
|
||||
|
||||
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in event-based object functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker(project.GetResourcesManager());
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtension", 0);
|
||||
auto& object = extension.GetEventsBasedObjects().InsertNew("MyObject", 0);
|
||||
auto& function = object.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
auto& layout = project.InsertNewLayout("MyScene", 0);
|
||||
|
||||
// MyEventExtension::MyObject::MyFunction doesn't need to be actually used in
|
||||
// events because the implementation is naive.
|
||||
|
||||
gd::ResourceExposer::ExposeLayoutResources(project, layout, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usages in layer effects") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
|
@@ -162,7 +162,12 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('3D object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Base3DBehavior')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(
|
||||
_('Angle (in degrees)')
|
||||
)
|
||||
)
|
||||
.setFunctionName('setRotationX')
|
||||
.setGetter('getRotationX');
|
||||
|
||||
@@ -178,7 +183,12 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('3D object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Base3DBehavior')
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(
|
||||
_('Angle (in degrees)')
|
||||
)
|
||||
)
|
||||
.setFunctionName('setRotationY')
|
||||
.setGetter('getRotationY');
|
||||
|
||||
@@ -196,7 +206,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('3D object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Base3DBehavior')
|
||||
.addParameter('number', _('Rotation angle'), '', false)
|
||||
.addParameter('number', _('Angle to add (in degrees)'), '', false)
|
||||
.markAsAdvanced()
|
||||
.setFunctionName('turnAroundX');
|
||||
|
||||
@@ -214,7 +224,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('3D object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Base3DBehavior')
|
||||
.addParameter('number', _('Rotation angle'), '', false)
|
||||
.addParameter('number', _('Angle to add (in degrees)'), '', false)
|
||||
.markAsAdvanced()
|
||||
.setFunctionName('turnAroundY');
|
||||
|
||||
@@ -232,7 +242,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('3D object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Base3DBehavior')
|
||||
.addParameter('number', _('Rotation angle'), '', false)
|
||||
.addParameter('number', _('Angle to add (in degrees)'), '', false)
|
||||
.markAsAdvanced()
|
||||
.setFunctionName('turnAroundZ');
|
||||
}
|
||||
@@ -242,7 +252,7 @@ module.exports = {
|
||||
.addObject(
|
||||
'Model3DObject',
|
||||
_('3D Model'),
|
||||
_('An animated 3D model.'),
|
||||
_('An animated 3D model, useful for most elements of a 3D game.'),
|
||||
'JsPlatform/Extensions/3d_model.svg',
|
||||
new gd.Model3DObjectConfiguration()
|
||||
)
|
||||
@@ -594,7 +604,12 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D model'), 'Model3DObject', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(
|
||||
_('Angle (in degrees)')
|
||||
)
|
||||
)
|
||||
.setHidden()
|
||||
.setFunctionName('setRotationX')
|
||||
.setGetter('getRotationX');
|
||||
@@ -611,7 +626,12 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D model'), 'Model3DObject', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(
|
||||
_('Angle (in degrees)')
|
||||
)
|
||||
)
|
||||
.setHidden()
|
||||
.setFunctionName('setRotationY')
|
||||
.setGetter('getRotationY');
|
||||
@@ -630,7 +650,7 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D model'), 'Model3DObject', false)
|
||||
.addParameter('number', _('Rotation angle'), '', false)
|
||||
.addParameter('number', _('Angle to add (in degrees)'), '', false)
|
||||
.markAsAdvanced()
|
||||
.setHidden()
|
||||
.setFunctionName('turnAroundX');
|
||||
@@ -649,7 +669,7 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D model'), 'Model3DObject', false)
|
||||
.addParameter('number', _('Rotation angle'), '', false)
|
||||
.addParameter('number', _('Angle to add (in degrees)'), '', false)
|
||||
.markAsAdvanced()
|
||||
.setHidden()
|
||||
.setFunctionName('turnAroundY');
|
||||
@@ -668,7 +688,7 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D model'), 'Model3DObject', false)
|
||||
.addParameter('number', _('Rotation angle'), '', false)
|
||||
.addParameter('number', _('Angle to add (in degrees)'), '', false)
|
||||
.markAsAdvanced()
|
||||
.setHidden()
|
||||
.setFunctionName('turnAroundZ');
|
||||
@@ -1493,7 +1513,12 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D cube'), 'Cube3DObject', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(
|
||||
_('Angle (in degrees)')
|
||||
)
|
||||
)
|
||||
.setFunctionName('setRotationX')
|
||||
.setHidden()
|
||||
.setGetter('getRotationX');
|
||||
@@ -1510,7 +1535,12 @@ module.exports = {
|
||||
'res/conditions/3d_box.svg'
|
||||
)
|
||||
.addParameter('object', _('3D cube'), 'Cube3DObject', false)
|
||||
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
|
||||
.useStandardParameters(
|
||||
'number',
|
||||
gd.ParameterOptions.makeNewOptions().setDescription(
|
||||
_('Angle (in degrees)')
|
||||
)
|
||||
)
|
||||
.setFunctionName('setRotationY')
|
||||
.setHidden()
|
||||
.setGetter('getRotationY');
|
||||
@@ -1878,7 +1908,9 @@ module.exports = {
|
||||
.addEffect('AmbientLight')
|
||||
.setFullName(_('Ambient light'))
|
||||
.setDescription(
|
||||
_('A light that illuminates all objects from every direction.')
|
||||
_(
|
||||
'A light that illuminates all objects from every direction. Often used along with a Directional light (though a Hemisphere light can be used instead of an Ambient light).'
|
||||
)
|
||||
)
|
||||
.markAsNotWorkingForObjects()
|
||||
.markAsOnlyWorkingFor3D()
|
||||
@@ -1899,7 +1931,11 @@ module.exports = {
|
||||
const effect = extension
|
||||
.addEffect('DirectionalLight')
|
||||
.setFullName(_('Directional light'))
|
||||
.setDescription(_('A very far light source like the sun.'))
|
||||
.setDescription(
|
||||
_(
|
||||
"A very far light source like the sun. This is the light to use for casting shadows for 3D objects (other lights won't emit shadows). Often used along with a Hemisphere light."
|
||||
)
|
||||
)
|
||||
.markAsNotWorkingForObjects()
|
||||
.markAsOnlyWorkingFor3D()
|
||||
.addIncludeFile('Extensions/3D/DirectionalLight.js');
|
||||
@@ -1983,7 +2019,7 @@ module.exports = {
|
||||
.setFullName(_('Hemisphere light'))
|
||||
.setDescription(
|
||||
_(
|
||||
'A light that illuminates objects from every direction with a gradient.'
|
||||
'A light that illuminates objects from every direction with a gradient. Often used along with a Directional light.'
|
||||
)
|
||||
)
|
||||
.markAsNotWorkingForObjects()
|
||||
|
@@ -383,6 +383,10 @@ namespace gdjs {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
override setWidth(width: float): void {
|
||||
this.setWrappingWidth(width);
|
||||
}
|
||||
|
||||
override getDrawableY(): float {
|
||||
return (
|
||||
this.getY() -
|
||||
|
@@ -426,6 +426,10 @@ namespace gdjs {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
override setWidth(width: float): void {
|
||||
this.setWrappingWidth(width);
|
||||
}
|
||||
|
||||
override getDrawableY(): float {
|
||||
return (
|
||||
this.getY() -
|
||||
|
@@ -21,7 +21,9 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'FileSystem',
|
||||
_('File system'),
|
||||
_('Access the filesystem of the operating system.'),
|
||||
_(
|
||||
'Access the filesystem of the operating system - only works on native, desktop games exported to Windows, Linux or macOS.'
|
||||
),
|
||||
'Matthias Meike',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
|
@@ -5,23 +5,25 @@ Copyright (c) 2008-2016 Florian Rival (Florian.Rival@gmail.com)
|
||||
This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void DeclareInventoryExtension(gd::PlatformExtension& extension) {
|
||||
extension.SetExtensionInformation(
|
||||
"Inventory",
|
||||
_("Inventories"),
|
||||
_("Provides actions and conditions to add an inventory to your game, "
|
||||
"with items in memory."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
extension
|
||||
.SetExtensionInformation(
|
||||
"Inventory",
|
||||
_("Inventories"),
|
||||
_("Actions and conditions to store named inventories in memory, "
|
||||
"with items (indexed by their name), a count for each of them, "
|
||||
"a maximum count and an equipped state. Can be loaded/saved "
|
||||
"from/to a GDevelop variable."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetExtensionHelpPath("/all-features/inventory")
|
||||
.SetCategory("Game mechanic");
|
||||
extension
|
||||
.AddInstructionOrExpressionGroupMetadata(_("Inventories"))
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Inventories"))
|
||||
.SetIcon("CppPlatform/Extensions/Inventoryicon.png");
|
||||
|
||||
extension
|
||||
@@ -164,14 +166,15 @@ void DeclareInventoryExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("InventoryTools::IsEquipped");
|
||||
|
||||
extension
|
||||
.AddAction("SerializeToVariable",
|
||||
_("Save an inventory in a scene variable"),
|
||||
_("Save all the items of the inventory in a scene variable, so that "
|
||||
"it can be restored later."),
|
||||
_("Save inventory _PARAM1_ in variable _PARAM2_"),
|
||||
_("Variables"),
|
||||
"CppPlatform/Extensions/Inventoryicon.png",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
.AddAction(
|
||||
"SerializeToVariable",
|
||||
_("Save an inventory in a scene variable"),
|
||||
_("Save all the items of the inventory in a scene variable, so that "
|
||||
"it can be restored later."),
|
||||
_("Save inventory _PARAM1_ in variable _PARAM2_"),
|
||||
_("Variables"),
|
||||
"CppPlatform/Extensions/Inventoryicon.png",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Inventory name"))
|
||||
@@ -204,13 +207,14 @@ void DeclareInventoryExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("InventoryTools::Count");
|
||||
|
||||
extension
|
||||
.AddExpression("Maximum",
|
||||
_("Item maximum"),
|
||||
_("Get the maximum of an item in the inventory, or 0 if it is unlimited"),
|
||||
"",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Inventory name"))
|
||||
.AddParameter("string", _("Item name"))
|
||||
.SetFunctionName("InventoryTools::Maximum");
|
||||
.AddExpression("Maximum",
|
||||
_("Item maximum"),
|
||||
_("Get the maximum of an item in the inventory, or 0 if "
|
||||
"it is unlimited"),
|
||||
"",
|
||||
"CppPlatform/Extensions/Inventoryicon.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("string", _("Inventory name"))
|
||||
.AddParameter("string", _("Item name"))
|
||||
.SetFunctionName("InventoryTools::Maximum");
|
||||
}
|
||||
|
@@ -21,7 +21,9 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Leaderboards',
|
||||
_('Leaderboards'),
|
||||
_('Allow your game to send scores to your leaderboards.'),
|
||||
_(
|
||||
'Allow your game to send scores to your leaderboards (anonymously or from the logged-in player) or display existing leaderboards to the player.'
|
||||
),
|
||||
'Florian Rival',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
|
@@ -22,7 +22,7 @@ module.exports = {
|
||||
'Lighting',
|
||||
_('Lights'),
|
||||
|
||||
'This provides a light object, and a behavior to mark other objects as being obstacles for the lights. This is a great way to create a special atmosphere to your game, along with effects, make it more realistic or to create gameplays based on lights.',
|
||||
'This provides a 2D light object, and a behavior to mark other 2D objects as being obstacles for the lights. This is a great way to create a special atmosphere to your game, along with effects, make it more realistic or to create gameplays based on lights.',
|
||||
'Harsimran Virk',
|
||||
'MIT'
|
||||
)
|
||||
@@ -51,7 +51,7 @@ module.exports = {
|
||||
_('Light Obstacle Behavior'),
|
||||
'LightObstacleBehavior',
|
||||
_(
|
||||
'Flag objects as being obstacles to light. The light emitted by light objects will be stopped by the object.'
|
||||
'Flag objects as being obstacles to 2D lights. The light emitted by light objects will be stopped by the object. This does not work on 3D objects and 3D games.'
|
||||
),
|
||||
'',
|
||||
'CppPlatform/Extensions/lightObstacleIcon32.png',
|
||||
@@ -164,7 +164,7 @@ module.exports = {
|
||||
'LightObject',
|
||||
_('Light'),
|
||||
_(
|
||||
'Displays a light on the scene, with a customizable radius and color. Add then the Light Obstacle behavior to the objects that must act as obstacle to the lights.'
|
||||
'Displays a 2D light on the scene, with a customizable radius and color. Add then the Light Obstacle behavior to the objects that must act as obstacle to the lights.'
|
||||
),
|
||||
'CppPlatform/Extensions/lightIcon32.png',
|
||||
lightObject
|
||||
|
@@ -239,7 +239,7 @@ namespace gdjs {
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
objectsLists: Hashtable<gdjs.RuntimeObject[]>,
|
||||
obj: gdjs.RuntimeObject | null,
|
||||
eventsFunctionContext: EventsFunctionContext | undefined
|
||||
eventsFunctionContext: EventsFunctionContext | null | undefined
|
||||
) {
|
||||
if (obj === null) {
|
||||
return false;
|
||||
|
@@ -21,7 +21,9 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Multiplayer',
|
||||
_('Multiplayer'),
|
||||
_('Allow players to connect to lobbies and play together.'),
|
||||
_(
|
||||
'This allows players to join online lobbies and synchronize gameplay across devices without needing to manage servers or networking.\n\nUse the "Open game lobbies" action to let players join a game, and use conditions like "Lobby game has just started" to begin gameplay. Add the "Multiplayer object" behavior to game objects that should be synchronized, and assign or change their ownership using player numbers. Variables and game state (like scenes, scores, or timers) are automatically synced by the host, with options to change ownership or disable sync when needed. Common multiplayer logic —like handling joins/leaves, collisions, and host migration— is supported out-of-the-box for up to 8 players per game.'
|
||||
),
|
||||
'Florian Rival',
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
|
@@ -389,26 +389,15 @@ namespace gdjs {
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
setColor(rgbColor): void {
|
||||
const colors = rgbColor.split(';');
|
||||
if (colors.length < 3) {
|
||||
return;
|
||||
}
|
||||
this._centerSprite.tint = gdjs.rgbToHexNumber(
|
||||
parseInt(colors[0], 10),
|
||||
parseInt(colors[1], 10),
|
||||
parseInt(colors[2], 10)
|
||||
);
|
||||
setColor(rgbOrHexColor: string): void {
|
||||
const tint = gdjs.rgbOrHexStringToNumber(rgbOrHexColor);
|
||||
this._centerSprite.tint = tint;
|
||||
for (
|
||||
let borderCounter = 0;
|
||||
borderCounter < this._borderSprites.length;
|
||||
borderCounter++
|
||||
) {
|
||||
this._borderSprites[borderCounter].tint = gdjs.rgbToHexNumber(
|
||||
parseInt(colors[0], 10),
|
||||
parseInt(colors[1], 10),
|
||||
parseInt(colors[2], 10)
|
||||
);
|
||||
this._borderSprites[borderCounter].tint = tint;
|
||||
}
|
||||
this._spritesContainer.cacheAsBitmap = false;
|
||||
}
|
||||
@@ -416,11 +405,11 @@ namespace gdjs {
|
||||
getColor() {
|
||||
const rgb = new PIXI.Color(this._centerSprite.tint).toRgbArray();
|
||||
return (
|
||||
Math.floor(rgb[0] * 255) +
|
||||
Math.round(rgb[0] * 255) +
|
||||
';' +
|
||||
Math.floor(rgb[1] * 255) +
|
||||
Math.round(rgb[1] * 255) +
|
||||
';' +
|
||||
Math.floor(rgb[2] * 255)
|
||||
Math.round(rgb[2] * 255)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,11 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Physics2',
|
||||
_('2D Physics Engine'),
|
||||
"The 2D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for 2D games that need to have realistic behaving objects and a gameplay centered around it.",
|
||||
"The 2D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for 2D games that need to have realistic behaving objects and a gameplay centered around it.\n" +
|
||||
'\n' +
|
||||
'Objects like floors or wall objects should usually be set to "Static" as type. Objects that should be moveable are usually "Dynamic" (default). "Kinematic" objects (typically, players or controlled characters) are only moved by their "linear velocity" and "angular velocity" - they can interact with other objects but only these other objects will move.\n' +
|
||||
'\n' +
|
||||
'Forces (and impulses) are expressed in all conditions/expressions/actions of the 2D physics engine in Newtons (N). Typical values for a force are 10-200 N. One meter is 100 pixels by default in the game (check the world scale). Mass is expressed in kilograms (kg).',
|
||||
'Florian Rival, Franco Maciel',
|
||||
'MIT'
|
||||
)
|
||||
|
@@ -21,7 +21,11 @@ module.exports = {
|
||||
.setExtensionInformation(
|
||||
'Physics3D',
|
||||
_('3D physics engine'),
|
||||
"The 3D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for almost all 3D games.",
|
||||
"The 3D physics engine simulates realistic object physics, with gravity, forces, collisions, joints, etc. It's perfect for almost all 3D games.\n" +
|
||||
'\n' +
|
||||
'Objects like floors or wall objects should usually be set to "Static" as type. Objects that should be moveable are usually "Dynamic" (default). "Kinematic" objects (typically, players or controlled characters) are only moved by their "linear velocity" and "angular velocity" - they can interact with other objects but only these other objects will move.\n' +
|
||||
'\n' +
|
||||
'Forces (and impulses) are expressed in all conditions/expressions/actions of the 3D physics engine in Newtons (N). Typical values for a force are 10-200 N. One meter is 100 pixels by default in the game (check the world scale). Mass is expressed in kilograms (kg).',
|
||||
'Florian Rival',
|
||||
'MIT'
|
||||
)
|
||||
@@ -2043,7 +2047,12 @@ module.exports = {
|
||||
'PhysicsCharacter3D',
|
||||
_('3D physics character'),
|
||||
'PhysicsCharacter3D',
|
||||
_('Jump and run on platforms.'),
|
||||
_(
|
||||
'Allow an object to jump and run on platforms that have the 3D physics behavior' +
|
||||
'(and which are generally set to "Static" as type, unless the platform is animated/moved in events).\n' +
|
||||
'\n' +
|
||||
'This behavior is usually used with one or more "mapper" behavior to let the player move it.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/physics_character3d.svg',
|
||||
'PhysicsCharacter3D',
|
||||
@@ -2612,7 +2621,7 @@ module.exports = {
|
||||
'JumpSustainTime',
|
||||
_('Jump sustain time'),
|
||||
_(
|
||||
'the jump sustain time of an object. This is the time during which keeping the jump button held allow the initial jump speed to be maintained.'
|
||||
'the jump sustain time of an object. This is the time during which keeping the jump button held allow the initial jump speed to be maintained'
|
||||
),
|
||||
_('the jump sustain time'),
|
||||
_('Character configuration'),
|
||||
@@ -3300,7 +3309,11 @@ module.exports = {
|
||||
'PhysicsCar3D',
|
||||
_('3D physics car'),
|
||||
'PhysicsCar3D',
|
||||
_('Simulate a realistic car using the 3D physics engine.'),
|
||||
_(
|
||||
"Simulate a realistic car using the 3D physics engine. This is mostly useful for the car controlled by the player (it's usually too complex for other cars in a game).\n" +
|
||||
'\n' +
|
||||
'This behavior is usually used with one or more "mapper" behavior to let the player move it.'
|
||||
),
|
||||
'',
|
||||
'JsPlatform/Extensions/physics_car3d.svg',
|
||||
'PhysicsCar3D',
|
||||
|
@@ -15,7 +15,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"PrimitiveDrawing",
|
||||
_("Shape painter"),
|
||||
_("This provides an object that can be used to draw arbitrary shapes "
|
||||
_("An object that can be used to draw arbitrary 2D shapes "
|
||||
"on the screen using events."),
|
||||
"Florian Rival and Aurélien Vivet",
|
||||
"Open source (MIT License)")
|
||||
@@ -28,7 +28,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.AddObject<ShapePainterObject>(
|
||||
"Drawer", //"Drawer" is kept for compatibility with GD<=3.6.76
|
||||
_("Shape painter"),
|
||||
_("Allows you to draw simple shapes on the screen using the "
|
||||
_("Allows to draw simple 2D shapes on the screen using the "
|
||||
"events."),
|
||||
"CppPlatform/Extensions/primitivedrawingicon.png")
|
||||
.SetCategoryFullName(_("Advanced"))
|
||||
@@ -125,11 +125,11 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("DrawEllipse");
|
||||
|
||||
obj.AddAction("FilletRectangle",
|
||||
_("Fillet Rectangle"),
|
||||
_("Draw a fillet rectangle on screen"),
|
||||
_("Draw from _PARAM1_;_PARAM2_ to _PARAM3_;_PARAM4_ a fillet "
|
||||
"rectangle (fillet: _PARAM5_)"
|
||||
"with _PARAM0_"),
|
||||
_("Fillet Rectangle"),
|
||||
_("Draw a fillet rectangle on screen"),
|
||||
_("Draw from _PARAM1_;_PARAM2_ to _PARAM3_;_PARAM4_ a fillet "
|
||||
"rectangle (fillet: _PARAM5_)"
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/filletRectangle24.png",
|
||||
"res/actions/filletRectangle.png")
|
||||
@@ -142,7 +142,6 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("expression", _("Fillet (in pixels)"))
|
||||
.SetFunctionName("DrawFilletRectangle");
|
||||
|
||||
|
||||
obj.AddAction("RoundedRectangle",
|
||||
_("Rounded rectangle"),
|
||||
_("Draw a rounded rectangle on screen"),
|
||||
@@ -170,54 +169,53 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
|
||||
_("Drawing"),
|
||||
"res/actions/chamferRectangle24.png",
|
||||
"res/actions/chamferRectangle.png")
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.AddParameter("expression", _("Chamfer (in pixels)"))
|
||||
.SetFunctionName("DrawChamferRectangle");
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("Left X position"))
|
||||
.AddParameter("expression", _("Top Y position"))
|
||||
.AddParameter("expression", _("Right X position"))
|
||||
.AddParameter("expression", _("Bottom Y position"))
|
||||
.AddParameter("expression", _("Chamfer (in pixels)"))
|
||||
.SetFunctionName("DrawChamferRectangle");
|
||||
|
||||
obj.AddAction("Torus",
|
||||
_("Torus"),
|
||||
_("Draw a torus on screen"),
|
||||
_("Draw at _PARAM1_;_PARAM2_ a torus with "
|
||||
"inner radius: _PARAM3_, outer radius: _PARAM4_ and "
|
||||
"with start arc angle: _PARAM5_°, end angle: _PARAM6_° "
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/torus24.png",
|
||||
"res/actions/torus.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression", _("Inner Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Outer Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Start Arc (in degrees)"))
|
||||
.AddParameter("expression", _("End Arc (in degrees)"))
|
||||
.SetFunctionName("DrawTorus");
|
||||
|
||||
|
||||
_("Torus"),
|
||||
_("Draw a torus on screen"),
|
||||
_("Draw at _PARAM1_;_PARAM2_ a torus with "
|
||||
"inner radius: _PARAM3_, outer radius: _PARAM4_ and "
|
||||
"with start arc angle: _PARAM5_°, end angle: _PARAM6_° "
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/torus24.png",
|
||||
"res/actions/torus.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression", _("Inner Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Outer Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Start Arc (in degrees)"))
|
||||
.AddParameter("expression", _("End Arc (in degrees)"))
|
||||
.SetFunctionName("DrawTorus");
|
||||
|
||||
obj.AddAction("RegularPolygon",
|
||||
_("Regular Polygon"),
|
||||
_("Draw a regular polygon on screen"),
|
||||
_("Draw at _PARAM1_;_PARAM2_ a regular polygon with _PARAM3_ sides and radius: "
|
||||
_("Draw at _PARAM1_;_PARAM2_ a regular polygon with _PARAM3_ "
|
||||
"sides and radius: "
|
||||
"_PARAM4_ (rotation: _PARAM5_) "
|
||||
"with _PARAM0_"),
|
||||
_("Drawing"),
|
||||
"res/actions/regularPolygon24.png",
|
||||
"res/actions/regularPolygon.png")
|
||||
_("Drawing"),
|
||||
"res/actions/regularPolygon24.png",
|
||||
"res/actions/regularPolygon.png")
|
||||
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression",
|
||||
_("Number of sides of the polygon (minimum: 3)"))
|
||||
.AddParameter("expression", _("Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Rotation (in degrees)"))
|
||||
.SetFunctionName("DrawRegularPolygon");
|
||||
.AddParameter("object", _("Shape Painter object"), "Drawer")
|
||||
.AddParameter("expression", _("X position of center"))
|
||||
.AddParameter("expression", _("Y position of center"))
|
||||
.AddParameter("expression",
|
||||
_("Number of sides of the polygon (minimum: 3)"))
|
||||
.AddParameter("expression", _("Radius (in pixels)"))
|
||||
.AddParameter("expression", _("Rotation (in degrees)"))
|
||||
.SetFunctionName("DrawRegularPolygon");
|
||||
|
||||
obj.AddAction(
|
||||
"Star",
|
||||
|
@@ -202,28 +202,18 @@ namespace gdjs {
|
||||
this._loadingSpineAtlases.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this clears the Spine atlases loaded in this manager.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const loadedSpineAtlas = this._loadedSpineAtlases.get(resourceData);
|
||||
if (loadedSpineAtlas) {
|
||||
loadedSpineAtlas.dispose();
|
||||
this._loadedSpineAtlases.delete(resourceData);
|
||||
}
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedSpineAtlas = this._loadedSpineAtlases.get(resourceData);
|
||||
if (loadedSpineAtlas) {
|
||||
loadedSpineAtlas.dispose();
|
||||
this._loadedSpineAtlases.delete(resourceData);
|
||||
}
|
||||
|
||||
const loadingSpineAtlas = this._loadingSpineAtlases.get(resourceData);
|
||||
if (loadingSpineAtlas) {
|
||||
loadingSpineAtlas.then((atl) => atl.dispose());
|
||||
this._loadingSpineAtlases.delete(resourceData);
|
||||
}
|
||||
});
|
||||
const loadingSpineAtlas = this._loadingSpineAtlases.get(resourceData);
|
||||
if (loadingSpineAtlas) {
|
||||
loadingSpineAtlas.then((atl) => atl.dispose());
|
||||
this._loadingSpineAtlases.delete(resourceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -127,21 +127,11 @@ namespace gdjs {
|
||||
this._loadedSpines.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this clears the Spine skeleton data loaded in this manager.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const loadedSpine = this._loadedSpines.get(resourceData);
|
||||
if (loadedSpine) {
|
||||
this._loadedSpines.delete(resourceData);
|
||||
}
|
||||
});
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedSpine = this._loadedSpines.get(resourceData);
|
||||
if (loadedSpine) {
|
||||
this._loadedSpines.delete(resourceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,8 @@ void DeclareSystemInfoExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"SystemInfo",
|
||||
_("System information"),
|
||||
_("Get information about the system and device running the game."),
|
||||
_("Conditions to check if the device has a touchscreen, is a mobile, "
|
||||
"or if the game runs as a preview."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Advanced");
|
||||
|
@@ -449,6 +449,16 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("object", _("Object"), "Text")
|
||||
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
|
||||
|
||||
obj.AddExpressionAndConditionAndAction("number",
|
||||
"LineHeight",
|
||||
_("Line height"),
|
||||
_("the line height of a text object"),
|
||||
_("the line height"),
|
||||
"",
|
||||
"res/actions/font24.png")
|
||||
.AddParameter("object", _("Object"), "Text")
|
||||
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
|
||||
|
||||
// Support for deprecated "Size" actions/conditions:
|
||||
obj.AddDuplicatedAction("Size", "Text::SetFontSize").SetHidden();
|
||||
obj.AddDuplicatedCondition("Size", "Text::FontSize").SetHidden();
|
||||
|
@@ -96,6 +96,14 @@ class TextObjectJsExtension : public gd::PlatformExtension {
|
||||
.SetFunctionName("setOutlineThickness")
|
||||
.SetGetter("getOutlineThickness");
|
||||
|
||||
GetAllExpressionsForObject("TextObject::Text")["LineHeight"]
|
||||
.SetFunctionName("getLineHeight");
|
||||
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::LineHeight"]
|
||||
.SetFunctionName("getLineHeight");
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetLineHeight"]
|
||||
.SetFunctionName("setLineHeight")
|
||||
.SetGetter("getLineHeight");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::ShowShadow"]
|
||||
.SetFunctionName("showShadow");
|
||||
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::IsShadowEnabled"]
|
||||
|
@@ -20,6 +20,7 @@ using namespace std;
|
||||
TextObject::TextObject()
|
||||
: text("Text"),
|
||||
characterSize(20),
|
||||
lineHeight(0),
|
||||
fontName(""),
|
||||
smoothed(true),
|
||||
bold(false),
|
||||
@@ -50,6 +51,10 @@ bool TextObject::UpdateProperty(const gd::String& propertyName,
|
||||
characterSize = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
if (propertyName == "lineHeight") {
|
||||
lineHeight = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
if (propertyName == "font") {
|
||||
fontName = newValue;
|
||||
return true;
|
||||
@@ -129,6 +134,13 @@ std::map<gd::String, gd::PropertyDescriptor> TextObject::GetProperties() const {
|
||||
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
|
||||
.SetGroup(_("Font"));
|
||||
|
||||
objectProperties["lineHeight"]
|
||||
.SetValue(gd::String::From(lineHeight))
|
||||
.SetType("number")
|
||||
.SetLabel(_("Line height"))
|
||||
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
|
||||
.SetGroup(_("Font"));
|
||||
|
||||
objectProperties["font"]
|
||||
.SetValue(fontName)
|
||||
.SetType("resource")
|
||||
@@ -271,6 +283,7 @@ void TextObject::DoUnserializeFrom(gd::Project& project,
|
||||
SetCharacterSize(content.GetChild("characterSize", 0, "CharacterSize")
|
||||
.GetValue()
|
||||
.GetInt());
|
||||
SetLineHeight(content.GetDoubleAttribute("lineHeight", 0));
|
||||
smoothed = content.GetBoolAttribute("smoothed");
|
||||
bold = content.GetBoolAttribute("bold");
|
||||
italic = content.GetBoolAttribute("italic");
|
||||
@@ -339,6 +352,7 @@ void TextObject::DoSerializeTo(gd::SerializerElement& element) const {
|
||||
content.AddChild("textAlignment").SetValue(GetTextAlignment());
|
||||
content.AddChild("verticalTextAlignment").SetValue(GetVerticalTextAlignment());
|
||||
content.AddChild("characterSize").SetValue(GetCharacterSize());
|
||||
content.AddChild("lineHeight").SetValue(GetLineHeight());
|
||||
content.AddChild("color").SetValue(GetColor());
|
||||
|
||||
content.SetAttribute("smoothed", smoothed);
|
||||
|
@@ -49,6 +49,12 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
|
||||
*/
|
||||
inline double GetCharacterSize() const { return characterSize; };
|
||||
|
||||
/** \brief Change the line height. */
|
||||
inline void SetLineHeight(double value) { lineHeight = value; };
|
||||
|
||||
/** \brief Get the line height. */
|
||||
inline double GetLineHeight() const { return lineHeight; };
|
||||
|
||||
/** \brief Return the name of the font resource used for the text.
|
||||
*/
|
||||
inline const gd::String& GetFontName() const { return fontName; };
|
||||
@@ -120,6 +126,7 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
|
||||
|
||||
gd::String text;
|
||||
double characterSize;
|
||||
double lineHeight;
|
||||
gd::String fontName;
|
||||
bool smoothed;
|
||||
bool bold, italic, underlined;
|
||||
|
@@ -86,6 +86,7 @@ namespace gdjs {
|
||||
? style.dropShadowDistance + style.dropShadowBlur
|
||||
: 0;
|
||||
style.padding = Math.ceil(this._object._padding + extraPaddingForShadow);
|
||||
style.lineHeight = this._object._lineHeight;
|
||||
|
||||
// Prevent spikey outlines by adding a miter limit
|
||||
style.miterLimit = 3;
|
||||
|
@@ -22,6 +22,8 @@ namespace gdjs {
|
||||
text: string;
|
||||
textAlignment: string;
|
||||
verticalTextAlignment: string;
|
||||
/** The line height */
|
||||
lineHeight: float;
|
||||
|
||||
isOutlineEnabled: boolean;
|
||||
outlineThickness: float;
|
||||
@@ -62,6 +64,7 @@ namespace gdjs {
|
||||
sha: float;
|
||||
shb: float;
|
||||
pad: integer;
|
||||
lh: float;
|
||||
};
|
||||
|
||||
export type TextObjectNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -101,6 +104,8 @@ namespace gdjs {
|
||||
_shadowAngle: float;
|
||||
_shadowBlur: float;
|
||||
|
||||
_lineHeight: float;
|
||||
|
||||
_padding: integer = 5;
|
||||
_str: string;
|
||||
_renderer: gdjs.TextRuntimeObjectRenderer;
|
||||
@@ -139,6 +144,7 @@ namespace gdjs {
|
||||
this._shadowDistance = content.shadowDistance;
|
||||
this._shadowBlur = content.shadowBlurRadius;
|
||||
this._shadowAngle = content.shadowAngle;
|
||||
this._lineHeight = content.lineHeight || 0;
|
||||
|
||||
this._renderer = new gdjs.TextRuntimeObjectRenderer(
|
||||
this,
|
||||
@@ -149,7 +155,7 @@ namespace gdjs {
|
||||
this.onCreated();
|
||||
}
|
||||
|
||||
updateFromObjectData(
|
||||
override updateFromObjectData(
|
||||
oldObjectData: TextObjectData,
|
||||
newObjectData: TextObjectData
|
||||
): boolean {
|
||||
@@ -211,10 +217,13 @@ namespace gdjs {
|
||||
if (oldContent.shadowBlurRadius !== newContent.shadowBlurRadius) {
|
||||
this.setShadowBlurRadius(newContent.shadowBlurRadius);
|
||||
}
|
||||
if ((oldContent.lineHeight || 0) !== (newContent.lineHeight || 0)) {
|
||||
this.setLineHeight(newContent.lineHeight || 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getNetworkSyncData(): TextObjectNetworkSyncData {
|
||||
override getNetworkSyncData(): TextObjectNetworkSyncData {
|
||||
return {
|
||||
...super.getNetworkSyncData(),
|
||||
str: this._str,
|
||||
@@ -238,11 +247,12 @@ namespace gdjs {
|
||||
shd: this._shadowDistance,
|
||||
sha: this._shadowAngle,
|
||||
shb: this._shadowBlur,
|
||||
lh: this._lineHeight,
|
||||
pad: this._padding,
|
||||
};
|
||||
}
|
||||
|
||||
updateFromNetworkSyncData(
|
||||
override updateFromNetworkSyncData(
|
||||
networkSyncData: TextObjectNetworkSyncData
|
||||
): void {
|
||||
super.updateFromNetworkSyncData(networkSyncData);
|
||||
@@ -312,28 +322,30 @@ namespace gdjs {
|
||||
if (networkSyncData.shb !== undefined) {
|
||||
this.setShadowBlurRadius(networkSyncData.shb);
|
||||
}
|
||||
if (networkSyncData.lh !== undefined) {
|
||||
this.setLineHeight(networkSyncData.lh);
|
||||
}
|
||||
if (networkSyncData.pad !== undefined) {
|
||||
this.setPadding(networkSyncData.pad);
|
||||
}
|
||||
}
|
||||
|
||||
getRendererObject() {
|
||||
override getRendererObject() {
|
||||
return this._renderer.getRendererObject();
|
||||
}
|
||||
|
||||
update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
override update(instanceContainer: gdjs.RuntimeInstanceContainer): void {
|
||||
this._renderer.ensureUpToDate();
|
||||
}
|
||||
|
||||
onDestroyed(): void {
|
||||
override onDestroyed(): void {
|
||||
super.onDestroyed();
|
||||
this._renderer.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the extra parameters that could be set for an instance.
|
||||
*/
|
||||
extraInitializationFromInitialInstance(initialInstanceData: InstanceData) {
|
||||
override extraInitializationFromInitialInstance(
|
||||
initialInstanceData: InstanceData
|
||||
) {
|
||||
if (initialInstanceData.customSize) {
|
||||
this.setWrappingWidth(initialInstanceData.width);
|
||||
this.setWrapping(true);
|
||||
@@ -353,27 +365,17 @@ namespace gdjs {
|
||||
this._renderer.updatePosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object position on X axis.
|
||||
*/
|
||||
setX(x: float): void {
|
||||
override setX(x: float): void {
|
||||
super.setX(x);
|
||||
this._updateTextPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object position on Y axis.
|
||||
*/
|
||||
setY(y: float): void {
|
||||
override setY(y: float): void {
|
||||
super.setY(y);
|
||||
this._updateTextPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the angle of the object.
|
||||
* @param angle The new angle of the object
|
||||
*/
|
||||
setAngle(angle: float): void {
|
||||
override setAngle(angle: float): void {
|
||||
super.setAngle(angle);
|
||||
this._renderer.updateAngle();
|
||||
}
|
||||
@@ -455,6 +457,22 @@ namespace gdjs {
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line height of the text.
|
||||
*/
|
||||
getLineHeight(): float {
|
||||
return this._lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the line height of the text.
|
||||
* @param value The new line height for the text.
|
||||
*/
|
||||
setLineHeight(value: float): void {
|
||||
this._lineHeight = value;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of the resource to use for the font.
|
||||
* @param fontResourceName The name of the font resource.
|
||||
@@ -499,14 +517,14 @@ namespace gdjs {
|
||||
/**
|
||||
* Get width of the text.
|
||||
*/
|
||||
getWidth(): float {
|
||||
override getWidth(): float {
|
||||
return this._wrapping ? this._wrappingWidth : this._renderer.getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get height of the text.
|
||||
*/
|
||||
getHeight(): float {
|
||||
override getHeight(): float {
|
||||
return this._renderer.getHeight();
|
||||
}
|
||||
|
||||
@@ -584,16 +602,10 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Change the text color.
|
||||
* @param colorString color as a "R;G;B" string, for example: "255;0;0"
|
||||
* @param rgbOrHexColor color as a "R;G;B" string, for example: "255;0;0"
|
||||
*/
|
||||
setColor(colorString: string): void {
|
||||
const color = colorString.split(';');
|
||||
if (color.length < 3) {
|
||||
return;
|
||||
}
|
||||
this._color[0] = parseInt(color[0], 10);
|
||||
this._color[1] = parseInt(color[1], 10);
|
||||
this._color[2] = parseInt(color[2], 10);
|
||||
setColor(rgbOrHexColor: string): void {
|
||||
this._color = gdjs.rgbOrHexToRGBColor(rgbOrHexColor);
|
||||
this._useGradient = false;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
@@ -685,11 +697,11 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
setWidth(width: float): void {
|
||||
override setWidth(width: float): void {
|
||||
this.setWrappingWidth(width);
|
||||
}
|
||||
|
||||
getDrawableY(): float {
|
||||
override getDrawableY(): float {
|
||||
return (
|
||||
this.getY() -
|
||||
(this._verticalTextAlignment === 'center'
|
||||
@@ -702,18 +714,12 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Set the outline for the text object.
|
||||
* @param str color as a "R;G;B" string, for example: "255;0;0"
|
||||
* @param rgbOrHexColor color as a "R;G;B" string, for example: "255;0;0"
|
||||
* @param thickness thickness of the outline (0 = disabled)
|
||||
* @deprecated Prefer independent setters.
|
||||
*/
|
||||
setOutline(str: string, thickness: number): void {
|
||||
const color = str.split(';');
|
||||
if (color.length < 3) {
|
||||
return;
|
||||
}
|
||||
this._outlineColor[0] = parseInt(color[0], 10);
|
||||
this._outlineColor[1] = parseInt(color[1], 10);
|
||||
this._outlineColor[2] = parseInt(color[2], 10);
|
||||
setOutline(rgbOrHexColor: string, thickness: number): void {
|
||||
this._outlineColor = gdjs.rgbOrHexToRGBColor(rgbOrHexColor);
|
||||
this._outlineThickness = thickness;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
@@ -755,25 +761,19 @@ namespace gdjs {
|
||||
|
||||
/**
|
||||
* Set the shadow for the text object.
|
||||
* @param str color as a "R;G;B" string, for example: "255;0;0"
|
||||
* @param rgbOrHexColor color as a "R;G;B" string, for example: "255;0;0"
|
||||
* @param distance distance between the shadow and the text, in pixels.
|
||||
* @param blur amount of shadow blur, in pixels.
|
||||
* @param angle shadow offset direction, in degrees.
|
||||
* @deprecated Prefer independent setters.
|
||||
*/
|
||||
setShadow(
|
||||
str: string,
|
||||
rgbOrHexColor: string,
|
||||
distance: number,
|
||||
blur: integer,
|
||||
angle: float
|
||||
): void {
|
||||
const color = str.split(';');
|
||||
if (color.length < 3) {
|
||||
return;
|
||||
}
|
||||
this._shadowColor[0] = parseInt(color[0], 10);
|
||||
this._shadowColor[1] = parseInt(color[1], 10);
|
||||
this._shadowColor[2] = parseInt(color[2], 10);
|
||||
this._shadowColor = gdjs.rgbOrHexToRGBColor(rgbOrHexColor);
|
||||
this._shadowDistance = distance;
|
||||
this._shadowBlur = blur;
|
||||
this._shadowAngle = angle;
|
||||
@@ -886,38 +886,18 @@ namespace gdjs {
|
||||
strThirdColor: string,
|
||||
strFourthColor: string
|
||||
): void {
|
||||
const colorFirst = strFirstColor.split(';');
|
||||
const colorSecond = strSecondColor.split(';');
|
||||
const colorThird = strThirdColor.split(';');
|
||||
const colorFourth = strFourthColor.split(';');
|
||||
this._gradient = [];
|
||||
if (colorFirst.length == 3) {
|
||||
this._gradient.push([
|
||||
parseInt(colorFirst[0], 10),
|
||||
parseInt(colorFirst[1], 10),
|
||||
parseInt(colorFirst[2], 10),
|
||||
]);
|
||||
if (strFirstColor) {
|
||||
this._gradient.push(gdjs.rgbOrHexToRGBColor(strFirstColor));
|
||||
}
|
||||
if (colorSecond.length == 3) {
|
||||
this._gradient.push([
|
||||
parseInt(colorSecond[0], 10),
|
||||
parseInt(colorSecond[1], 10),
|
||||
parseInt(colorSecond[2], 10),
|
||||
]);
|
||||
if (strSecondColor) {
|
||||
this._gradient.push(gdjs.rgbOrHexToRGBColor(strSecondColor));
|
||||
}
|
||||
if (colorThird.length == 3) {
|
||||
this._gradient.push([
|
||||
parseInt(colorThird[0], 10),
|
||||
parseInt(colorThird[1], 10),
|
||||
parseInt(colorThird[2], 10),
|
||||
]);
|
||||
if (strThirdColor) {
|
||||
this._gradient.push(gdjs.rgbOrHexToRGBColor(strThirdColor));
|
||||
}
|
||||
if (colorFourth.length == 3) {
|
||||
this._gradient.push([
|
||||
parseInt(colorFourth[0], 10),
|
||||
parseInt(colorFourth[1], 10),
|
||||
parseInt(colorFourth[2], 10),
|
||||
]);
|
||||
if (strFourthColor) {
|
||||
this._gradient.push(gdjs.rgbOrHexToRGBColor(strFourthColor));
|
||||
}
|
||||
this._gradientType = strGradientType;
|
||||
this._useGradient = this._gradient.length > 1 ? true : false;
|
||||
|
@@ -17,7 +17,7 @@ void DeclareTiledSpriteObjectExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"TiledSpriteObject",
|
||||
_("Tiled Sprite Object"),
|
||||
"Displays an image in a repeating pattern over an area. Useful for "
|
||||
"Displays a 2D image in a repeating pattern over an area. Useful for "
|
||||
"making backgrounds, including background that are scrolling when "
|
||||
"the camera moves. This is more performant than using multiple "
|
||||
"Sprite objects.",
|
||||
|
@@ -92,28 +92,18 @@ namespace gdjs {
|
||||
-this._object._yOffset % this._tiledSprite.texture.height;
|
||||
}
|
||||
|
||||
setColor(rgbColor: string): void {
|
||||
const colors = rgbColor.split(';');
|
||||
if (colors.length < 3) {
|
||||
return;
|
||||
}
|
||||
this._tiledSprite.tint =
|
||||
'0x' +
|
||||
gdjs.rgbToHex(
|
||||
parseInt(colors[0], 10),
|
||||
parseInt(colors[1], 10),
|
||||
parseInt(colors[2], 10)
|
||||
);
|
||||
setColor(rgbOrHexColor: string): void {
|
||||
this._tiledSprite.tint = gdjs.rgbOrHexStringToNumber(rgbOrHexColor);
|
||||
}
|
||||
|
||||
getColor() {
|
||||
const rgb = new PIXI.Color(this._tiledSprite.tint).toRgbArray();
|
||||
return (
|
||||
Math.floor(rgb[0] * 255) +
|
||||
Math.round(rgb[0] * 255) +
|
||||
';' +
|
||||
Math.floor(rgb[1] * 255) +
|
||||
Math.round(rgb[1] * 255) +
|
||||
';' +
|
||||
Math.floor(rgb[2] * 255)
|
||||
Math.round(rgb[2] * 255)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -16,8 +16,11 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.SetExtensionInformation(
|
||||
"TopDownMovementBehavior",
|
||||
_("Top-down movement"),
|
||||
_("Allows to move objects in either 4 or 8 directions, with the "
|
||||
"keyboard or using events."),
|
||||
_("Allows to move an object in either 4 or 8 directions, with the "
|
||||
"keyboard (default), a virtual stick (for this, also add the "
|
||||
"\"Top-down multitouch controller mapper\" behavior and a"
|
||||
"\"Multitouch Joystick\" object), gamepad or manually using "
|
||||
"events."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Movement")
|
||||
@@ -26,17 +29,17 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Top-down movement"))
|
||||
.SetIcon("CppPlatform/Extensions/topdownmovementicon16.png");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"TopDownMovementBehavior",
|
||||
_("Top-down movement (4 or 8 directions)"),
|
||||
"TopDownMovement",
|
||||
_("Move objects left, up, right, and "
|
||||
"down (and, optionally, diagonally)."),
|
||||
"",
|
||||
"CppPlatform/Extensions/topdownmovementicon.png",
|
||||
"TopDownMovementBehavior",
|
||||
std::make_shared<TopDownMovementBehavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>());
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension.AddBehavior("TopDownMovementBehavior",
|
||||
_("Top-down movement (4 or 8 directions)"),
|
||||
"TopDownMovement",
|
||||
_("Move objects left, up, right, and "
|
||||
"down (and, optionally, diagonally)."),
|
||||
"",
|
||||
"CppPlatform/Extensions/topdownmovementicon.png",
|
||||
"TopDownMovementBehavior",
|
||||
std::make_shared<TopDownMovementBehavior>(),
|
||||
std::make_shared<gd::BehaviorsSharedData>());
|
||||
|
||||
aut.AddAction("SimulateLeftKey",
|
||||
_("Simulate left key press"),
|
||||
@@ -119,7 +122,8 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
aut.AddAction("SimulateStick",
|
||||
_("Simulate stick control"),
|
||||
_("Simulate a stick control."),
|
||||
_("Simulate a stick control for _PARAM0_ with a _PARAM2_ angle and a _PARAM3_ force"),
|
||||
_("Simulate a stick control for _PARAM0_ with a _PARAM2_ angle "
|
||||
"and a _PARAM3_ force"),
|
||||
_("Top-down controls"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
@@ -130,25 +134,28 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.MarkAsAdvanced()
|
||||
.SetFunctionName("SimulateStick");
|
||||
|
||||
aut.AddScopedCondition("IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or simulated by an action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Top-down state"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
aut.AddScopedCondition(
|
||||
"IsUsingControl",
|
||||
_("Control pressed or simulated"),
|
||||
_("A control was applied from a default control or simulated by an "
|
||||
"action."),
|
||||
_("_PARAM0_ has the _PARAM2_ key pressed or simulated"),
|
||||
_("Top-down state"),
|
||||
"res/conditions/keyboard24.png",
|
||||
"res/conditions/keyboard.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("stringWithSelector",
|
||||
_("Key"),
|
||||
"[\"Left\", \"Right\", \"Up\", \"Down\", \"Stick\"]")
|
||||
.MarkAsAdvanced();
|
||||
.MarkAsAdvanced();
|
||||
|
||||
aut.AddExpression("StickAngle",
|
||||
_("Stick angle"),
|
||||
_("Return the angle of the simulated stick input (in degrees)"),
|
||||
_("Top-down controls"),
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
aut.AddExpression(
|
||||
"StickAngle",
|
||||
_("Stick angle"),
|
||||
_("Return the angle of the simulated stick input (in degrees)"),
|
||||
_("Top-down controls"),
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior");
|
||||
|
||||
@@ -361,19 +368,19 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
.MarkAsAdvanced()
|
||||
.SetHidden()
|
||||
.SetFunctionName("GetAngle");
|
||||
|
||||
aut.AddScopedCondition(
|
||||
"IsMovementAngleAround",
|
||||
_("Angle of movement"),
|
||||
_("Compare the angle of the top-down movement of the object."),
|
||||
_("Angle of movement of _PARAM0_ is _PARAM2_ ± _PARAM3_°"),
|
||||
|
||||
aut.AddScopedCondition(
|
||||
"IsMovementAngleAround",
|
||||
_("Angle of movement"),
|
||||
_("Compare the angle of the top-down movement of the object."),
|
||||
_("Angle of movement of _PARAM0_ is _PARAM2_ ± _PARAM3_°"),
|
||||
_("Top-down state"),
|
||||
"CppPlatform/Extensions/topdownmovementicon24.png",
|
||||
"CppPlatform/Extensions/topdownmovementicon16.png")
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("expression", _("Angle (in degrees)"))
|
||||
.AddParameter("expression", _("Tolerance (in degrees)"));
|
||||
.AddParameter("object", _("Object"))
|
||||
.AddParameter("behavior", _("Behavior"), "TopDownMovementBehavior")
|
||||
.AddParameter("expression", _("Angle (in degrees)"))
|
||||
.AddParameter("expression", _("Tolerance (in degrees)"));
|
||||
|
||||
aut.AddCondition("XVelocity",
|
||||
_("Speed on X axis"),
|
||||
|
@@ -347,17 +347,21 @@ describe('gdjs.TweenRuntimeBehavior', () => {
|
||||
// Check that there is no NaN.
|
||||
for (let i = 0; i < 11; i++) {
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(0);
|
||||
// The tween tries to set the camera zoom to 0, but it has no effect
|
||||
// because it doesn't make sense.
|
||||
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('can tween a layer camera zoom from 0', () => {
|
||||
// The zoom stays at 1 because 0 doesn't make sense.
|
||||
camera.setCameraZoom(runtimeScene, 0, '', 0);
|
||||
// Here, it actually tweens from 1 to 1.
|
||||
tween.tweenCameraZoom2(runtimeScene, 'MyTween', 1, '', 'linear', 0.25);
|
||||
// A camera zoom of 0 doesn't make sense.
|
||||
// Check that there is no NaN.
|
||||
for (let i = 0; i < 11; i++) {
|
||||
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(0);
|
||||
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
}
|
||||
expect(camera.getCameraZoom(runtimeScene, '', 0)).to.be(1);
|
||||
|
@@ -145,6 +145,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
|
||||
shadowDistance: 4,
|
||||
shadowAngle: 90,
|
||||
shadowBlurRadius: 2,
|
||||
lineHeight: 0,
|
||||
},
|
||||
});
|
||||
runtimeScene.addObject(object);
|
||||
|
@@ -145,6 +145,7 @@ describe('gdjs.TweenRuntimeBehavior', () => {
|
||||
shadowDistance: 4,
|
||||
shadowAngle: 90,
|
||||
shadowBlurRadius: 2,
|
||||
lineHeight: 0,
|
||||
},
|
||||
});
|
||||
runtimeScene.addObject(object);
|
||||
|
@@ -1326,11 +1326,11 @@ namespace gdjs {
|
||||
lightness
|
||||
);
|
||||
owner.setColor(
|
||||
Math.floor(rgbFromHslColor[0]) +
|
||||
Math.round(rgbFromHslColor[0]) +
|
||||
';' +
|
||||
Math.floor(rgbFromHslColor[1]) +
|
||||
Math.round(rgbFromHslColor[1]) +
|
||||
';' +
|
||||
Math.floor(rgbFromHslColor[2])
|
||||
Math.round(rgbFromHslColor[2])
|
||||
);
|
||||
};
|
||||
} else {
|
||||
@@ -1439,12 +1439,11 @@ namespace gdjs {
|
||||
if (!isColorable(this.owner)) return;
|
||||
const owner = this.owner;
|
||||
|
||||
const rgbFromColor: string[] = owner.getColor().split(';');
|
||||
if (rgbFromColor.length < 3) return;
|
||||
const rgbFromColor = gdjs.rgbOrHexToRGBColor(owner.getColor());
|
||||
const hslFromColor = gdjs.evtTools.tween.rgbToHsl(
|
||||
parseFloat(rgbFromColor[0]),
|
||||
parseFloat(rgbFromColor[1]),
|
||||
parseFloat(rgbFromColor[2])
|
||||
rgbFromColor[0],
|
||||
rgbFromColor[1],
|
||||
rgbFromColor[2]
|
||||
);
|
||||
|
||||
const toH = animateHue ? toHue : hslFromColor[0];
|
||||
@@ -1474,11 +1473,11 @@ namespace gdjs {
|
||||
);
|
||||
|
||||
owner.setColor(
|
||||
Math.floor(rgbFromHslColor[0]) +
|
||||
Math.round(rgbFromHslColor[0]) +
|
||||
';' +
|
||||
Math.floor(rgbFromHslColor[1]) +
|
||||
Math.round(rgbFromHslColor[1]) +
|
||||
';' +
|
||||
Math.floor(rgbFromHslColor[2])
|
||||
Math.round(rgbFromHslColor[2])
|
||||
);
|
||||
},
|
||||
|
||||
|
@@ -1228,14 +1228,11 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
|
||||
// Code only parameter type
|
||||
else if (metadata.GetType() == "objectsContext") {
|
||||
argOutput =
|
||||
"(typeof eventsFunctionContext !== 'undefined' ? eventsFunctionContext "
|
||||
": runtimeScene)";
|
||||
HasProjectAndLayout() ? "runtimeScene" : "eventsFunctionContext";
|
||||
}
|
||||
// Code only parameter type
|
||||
else if (metadata.GetType() == "eventsFunctionContext") {
|
||||
argOutput =
|
||||
"(typeof eventsFunctionContext !== 'undefined' ? eventsFunctionContext "
|
||||
": undefined)";
|
||||
argOutput = HasProjectAndLayout() ? "null" : "eventsFunctionContext";
|
||||
} else
|
||||
return gd::EventsCodeGenerator::GenerateParameterCodes(
|
||||
parameter,
|
||||
|
@@ -21,6 +21,9 @@ AdvancedExtension::AdvancedExtension() {
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
gd::String expressionCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -28,15 +31,16 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"number",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') { "
|
||||
"eventsFunctionContext.returnValue = " +
|
||||
expressionCode + "; }";
|
||||
return "eventsFunctionContext.returnValue = " + expressionCode + ";";
|
||||
});
|
||||
|
||||
GetAllActions()["SetReturnString"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
gd::String expressionCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -44,29 +48,31 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') { "
|
||||
"eventsFunctionContext.returnValue = " +
|
||||
expressionCode + "; }";
|
||||
return "eventsFunctionContext.returnValue = " + expressionCode + ";";
|
||||
});
|
||||
|
||||
GetAllActions()["SetReturnBoolean"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String booleanCode =
|
||||
(parameter == "True" || parameter == "Vrai") ? "true" : "false";
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') { "
|
||||
"eventsFunctionContext.returnValue = " +
|
||||
booleanCode + "; }";
|
||||
return "eventsFunctionContext.returnValue = " + booleanCode + ";";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyArgumentToVariable"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -74,17 +80,17 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "scenevar", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable +
|
||||
", false);\n"
|
||||
"}\n";
|
||||
return "gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable + ", false);\n";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyArgumentToVariable2"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -92,17 +98,17 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "variable", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable +
|
||||
", false);\n"
|
||||
"}\n";
|
||||
return "gdjs.Variable.copy(eventsFunctionContext.getArgument(" +
|
||||
parameter + "), " + variable + ", false);\n";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyVariableToArgument"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -110,17 +116,18 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "scenevar", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(" +
|
||||
variable + ", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n"
|
||||
"}\n";
|
||||
return "gdjs.Variable.copy(" + variable +
|
||||
", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n";
|
||||
});
|
||||
|
||||
GetAllActions()["CopyVariableToArgument2"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction &instruction,
|
||||
gd::EventsCodeGenerator &codeGenerator,
|
||||
gd::EventsCodeGenerationContext &context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("");
|
||||
}
|
||||
// This is duplicated from EventsCodeGenerator::GenerateParameterCodes
|
||||
gd::String parameter = instruction.GetParameter(0).GetPlainString();
|
||||
gd::String variable =
|
||||
@@ -128,17 +135,18 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator, context, "variable", instruction.GetParameter(1),
|
||||
"");
|
||||
|
||||
return "if (typeof eventsFunctionContext !== 'undefined') {\n"
|
||||
"gdjs.Variable.copy(" +
|
||||
variable + ", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n"
|
||||
"}\n";
|
||||
return "gdjs.Variable.copy(" + variable +
|
||||
", eventsFunctionContext.getArgument(" + parameter +
|
||||
"), false);\n";
|
||||
});
|
||||
|
||||
GetAllConditions()["GetArgumentAsBoolean"]
|
||||
.SetCustomCodeGenerator([](gd::Instruction& instruction,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("false");
|
||||
}
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -146,10 +154,8 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
instruction.GetParameter(0).GetPlainString());
|
||||
gd::String valueCode =
|
||||
gd::String(instruction.IsInverted() ? "!" : "") +
|
||||
"(typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"!!eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : false)";
|
||||
gd::String(instruction.IsInverted() ? "!" : "!!") +
|
||||
"eventsFunctionContext.getArgument(" + parameterNameCode + ")";
|
||||
gd::String outputCode =
|
||||
codeGenerator.GenerateUpperScopeBooleanFullName("isConditionTrue", context) +
|
||||
" = " + valueCode + ";\n";
|
||||
@@ -161,6 +167,9 @@ AdvancedExtension::AdvancedExtension() {
|
||||
.SetCustomCodeGenerator([](const std::vector<gd::Expression>& parameters,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("0");
|
||||
}
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -168,9 +177,8 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
!parameters.empty() ? parameters[0].GetPlainString() : "");
|
||||
|
||||
return "(typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0 : 0)";
|
||||
return "(Number(eventsFunctionContext.getArgument(" + parameterNameCode +
|
||||
")) || 0)";
|
||||
});
|
||||
|
||||
GetAllStrExpressions()["GetArgumentAsString"]
|
||||
@@ -178,6 +186,9 @@ AdvancedExtension::AdvancedExtension() {
|
||||
.SetCustomCodeGenerator([](const std::vector<gd::Expression>& parameters,
|
||||
gd::EventsCodeGenerator& codeGenerator,
|
||||
gd::EventsCodeGenerationContext& context) {
|
||||
if (codeGenerator.HasProjectAndLayout()) {
|
||||
return gd::String("\"\"");
|
||||
}
|
||||
gd::String parameterNameCode =
|
||||
gd::ExpressionCodeGenerator::GenerateExpressionCode(
|
||||
codeGenerator,
|
||||
@@ -185,9 +196,8 @@ AdvancedExtension::AdvancedExtension() {
|
||||
"string",
|
||||
!parameters.empty() ? parameters[0].GetPlainString() : "");
|
||||
|
||||
return "(typeof eventsFunctionContext !== 'undefined' ? \"\" + "
|
||||
"eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : \"\")";
|
||||
return "\"\" + eventsFunctionContext.getArgument(" + parameterNameCode +
|
||||
")";
|
||||
});
|
||||
|
||||
GetAllConditions()["CompareArgumentAsNumber"]
|
||||
@@ -210,12 +220,13 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator.GenerateUpperScopeBooleanFullName("isConditionTrue", context);
|
||||
|
||||
return resultingBoolean + " = " +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") + "(" +
|
||||
codeGenerator.GenerateRelationalOperation(
|
||||
operatorString,
|
||||
"((typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0 : 0)",
|
||||
codeGenerator.HasProjectAndLayout()
|
||||
? "0"
|
||||
: "(Number(eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ")) || 0)",
|
||||
operandCode) +
|
||||
");\n";
|
||||
});
|
||||
@@ -240,12 +251,13 @@ AdvancedExtension::AdvancedExtension() {
|
||||
codeGenerator.GenerateUpperScopeBooleanFullName("isConditionTrue", context);
|
||||
|
||||
return resultingBoolean + " = " +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") +
|
||||
gd::String(instruction.IsInverted() ? "!" : "") + "(" +
|
||||
codeGenerator.GenerateRelationalOperation(
|
||||
operatorString,
|
||||
"((typeof eventsFunctionContext !== 'undefined' ? "
|
||||
"\"\" + eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + ") : \"\")",
|
||||
codeGenerator.HasProjectAndLayout()
|
||||
? "\"\""
|
||||
: "(\"\" + eventsFunctionContext.getArgument(" +
|
||||
parameterNameCode + "))",
|
||||
operandCode) +
|
||||
");\n";
|
||||
});
|
||||
|
@@ -821,8 +821,7 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
}
|
||||
if (!codeGenerator.HasProjectAndLayout()) {
|
||||
functionParameters += ", eventsFunctionContext";
|
||||
callArguments += ", typeof eventsFunctionContext !== \'undefined\' ? "
|
||||
"eventsFunctionContext : undefined";
|
||||
callArguments += ", eventsFunctionContext";
|
||||
}
|
||||
|
||||
// Generate the function code
|
||||
@@ -842,13 +841,23 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
|
||||
event.GetParameterObjects(),
|
||||
parentContext.GetCurrentObject());
|
||||
|
||||
callingCode += "var objects = [];\n";
|
||||
for (unsigned int i = 0; i < realObjects.size(); ++i) {
|
||||
parentContext.ObjectsListNeeded(realObjects[i]);
|
||||
if (realObjects.size() == 1) {
|
||||
parentContext.ObjectsListNeeded(realObjects[0]);
|
||||
callingCode +=
|
||||
"objects.push.apply(objects," +
|
||||
codeGenerator.GetObjectListName(realObjects[i], parentContext) +
|
||||
");\n";
|
||||
"const objects = " +
|
||||
codeGenerator.GetObjectListName(realObjects[0], parentContext) +
|
||||
";\n";
|
||||
} else {
|
||||
// Groups are rarely used in JS events so it's fine to make
|
||||
// allocations.
|
||||
callingCode += "const objects = [];\n";
|
||||
for (unsigned int i = 0; i < realObjects.size(); ++i) {
|
||||
parentContext.ObjectsListNeeded(realObjects[i]);
|
||||
callingCode += "objects.push.apply(objects," +
|
||||
codeGenerator.GetObjectListName(realObjects[i],
|
||||
parentContext) +
|
||||
");\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,6 +26,28 @@ The game engine is in the _Runtime_ folder. If you want to work on the engine di
|
||||
|
||||
- To launch type checking with TypeScript, run `npm install` and `npm run check-types` in `GDJS` folder.
|
||||
|
||||
#### Building GDJS Runtime
|
||||
|
||||
To build the GDJS Runtime, run `npm run build` in the `GDJS` folder.
|
||||
|
||||
**Build Options:**
|
||||
|
||||
- **Production build (default)**: `npm run build` - builds with minification enabled
|
||||
- **Debug build**: `npm run build -- --debug` - builds without minification for easier debugging
|
||||
- **Custom output path**: `npm run build -- --out=/path/to/output` - specify custom output directory
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Standard production build
|
||||
npm run build
|
||||
|
||||
# Debug build for development (no minification)
|
||||
npm run build -- --debug
|
||||
|
||||
# Debug build with custom output path
|
||||
npm run build -- --debug --out=./debug-build
|
||||
```
|
||||
|
||||
### GDJS Platform (exporters, code generation...)
|
||||
|
||||
Check the [GDJS Platform](https://docs.gdevelop.io/GDJS%20Documentation/index.html) documentation or the [full GDevelop developers documentation](https://docs.gdevelop.io/).
|
||||
|
@@ -163,28 +163,18 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this clears the models, resources loaded and destroy 3D models loaders in this manager.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
|
||||
if (loadedThreeModel) {
|
||||
loadedThreeModel.scene.clear();
|
||||
this._loadedThreeModels.delete(resourceData);
|
||||
}
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedThreeModel = this._loadedThreeModels.get(resourceData);
|
||||
if (loadedThreeModel) {
|
||||
loadedThreeModel.scene.clear();
|
||||
this._loadedThreeModels.delete(resourceData);
|
||||
}
|
||||
|
||||
const downloadedArrayBuffer =
|
||||
this._downloadedArrayBuffers.get(resourceData);
|
||||
if (downloadedArrayBuffer) {
|
||||
this._downloadedArrayBuffers.delete(resourceData);
|
||||
}
|
||||
});
|
||||
const downloadedArrayBuffer =
|
||||
this._downloadedArrayBuffers.get(resourceData);
|
||||
if (downloadedArrayBuffer) {
|
||||
this._downloadedArrayBuffers.delete(resourceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -534,7 +534,9 @@ namespace gdjs {
|
||||
`Unloading of resources of kind ${kindResourceManager} for scene ${unloadedSceneName}: `,
|
||||
resources.map((resource) => resource.name).join(', ')
|
||||
);
|
||||
resourceManager.unloadResourcesList(resources);
|
||||
for (const resource of resources) {
|
||||
resourceManager.unloadResource(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -31,19 +31,19 @@ namespace gdjs {
|
||||
getResourceKinds(): Array<ResourceKind>;
|
||||
|
||||
/**
|
||||
* Should clear all resources, data, loaders stored by this manager.
|
||||
* Clear all resources, data, loaders stored by this manager.
|
||||
* Using the manager after calling this method is undefined behavior.
|
||||
*/
|
||||
dispose(): void;
|
||||
|
||||
/**
|
||||
* Should clear all specified resources data and anything stored by this manager
|
||||
* for these resources.
|
||||
* Clear any data in cache for a resource. Embedded resources are also
|
||||
* cleared.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
* Usually called when scene resources are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources that need to be clear
|
||||
* @param resourceData The resource to clear
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void;
|
||||
unloadResource(resourceData: ResourceData): void;
|
||||
}
|
||||
}
|
||||
|
@@ -462,12 +462,12 @@ namespace gdjs {
|
||||
/**
|
||||
* @param instanceContainer the container owning the layer
|
||||
* @param layerName The lighting layer with the ambient color.
|
||||
* @param rgbColor The color, in RGB format ("128;200;255").
|
||||
* @param rgbOrHexColor The color, in RGB format ("128;200;255").
|
||||
*/
|
||||
export const setLayerAmbientLightColor = function (
|
||||
instanceContainer: gdjs.RuntimeInstanceContainer,
|
||||
layerName: string,
|
||||
rgbColor: string
|
||||
rgbOrHexColor: string
|
||||
) {
|
||||
if (
|
||||
!instanceContainer.hasLayer(layerName) ||
|
||||
@@ -475,17 +475,10 @@ namespace gdjs {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const colors = rgbColor.split(';');
|
||||
if (colors.length < 3) {
|
||||
return;
|
||||
}
|
||||
const color = gdjs.rgbOrHexToRGBColor(rgbOrHexColor);
|
||||
return instanceContainer
|
||||
.getLayer(layerName)
|
||||
.setClearColor(
|
||||
parseInt(colors[0], 10),
|
||||
parseInt(colors[1], 10),
|
||||
parseInt(colors[2], 10)
|
||||
);
|
||||
.setClearColor(color[0], color[1], color[2]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -24,19 +24,12 @@ namespace gdjs {
|
||||
|
||||
export const setBackgroundColor = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
rgbColor: string
|
||||
rgbOrHexColor: string
|
||||
) {
|
||||
const colors = rgbColor.split(';');
|
||||
if (colors.length < 3) {
|
||||
return;
|
||||
}
|
||||
const color = gdjs.rgbOrHexToRGBColor(rgbOrHexColor);
|
||||
runtimeScene
|
||||
.getScene()
|
||||
.setBackgroundColor(
|
||||
parseInt(colors[0]),
|
||||
parseInt(colors[1]),
|
||||
parseInt(colors[2])
|
||||
);
|
||||
.setBackgroundColor(color[0], color[1], color[2]);
|
||||
};
|
||||
|
||||
export const getElapsedTimeInSeconds = function (
|
||||
|
@@ -206,26 +206,16 @@ namespace gdjs {
|
||||
this._loadedFontFamilySet.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this clears the caches of loaded font families.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const resource = this._loadedFontFamily.get(resourceData);
|
||||
if (resource) {
|
||||
this._loadedFontFamily.delete(resourceData);
|
||||
}
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const resource = this._loadedFontFamily.get(resourceData);
|
||||
if (resource) {
|
||||
this._loadedFontFamily.delete(resourceData);
|
||||
}
|
||||
|
||||
const fontName = this._getFontFamilyFromFilename(resourceData);
|
||||
if (fontName) {
|
||||
this._loadedFontFamilySet.delete(fontName);
|
||||
}
|
||||
});
|
||||
const fontName = this._getFontFamilyFromFilename(resourceData);
|
||||
if (fontName) {
|
||||
this._loadedFontFamilySet.delete(fontName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,6 @@ namespace gdjs {
|
||||
const logger = new gdjs.Logger('Engine runtime');
|
||||
const hexStringRegex = /^(#{0,1}[A-Fa-f0-9]{6})$/;
|
||||
const shorthandHexStringRegex = /^(#{0,1}[A-Fa-f0-9]{3})$/;
|
||||
const rgbStringRegex = /^(\d{1,3};\d{1,3};\d{1,3})/;
|
||||
|
||||
/**
|
||||
* Contains functions used by events (this is a convention only, functions can actually
|
||||
@@ -105,9 +104,9 @@ namespace gdjs {
|
||||
export const rgbOrHexToRGBColor = function (
|
||||
value: string
|
||||
): [number, number, number] {
|
||||
const rgbColor = extractRGBString(value);
|
||||
if (rgbColor) {
|
||||
const splitValue = rgbColor.split(';');
|
||||
// TODO Add a `result` parameter to allow to reuse the returned array.
|
||||
if (!value.startsWith('#')) {
|
||||
const splitValue = value.split(';');
|
||||
// If a RGB string is provided, return the RGB object.
|
||||
if (splitValue.length === 3) {
|
||||
return [
|
||||
@@ -145,11 +144,11 @@ namespace gdjs {
|
||||
* @param b Blue
|
||||
*/
|
||||
export const rgbToHexNumber = function (
|
||||
r: integer,
|
||||
g: integer,
|
||||
b: integer
|
||||
r: float,
|
||||
g: float,
|
||||
b: float
|
||||
): integer {
|
||||
return (r << 16) + (g << 8) + b;
|
||||
return (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -192,12 +191,6 @@ namespace gdjs {
|
||||
return matches[0];
|
||||
};
|
||||
|
||||
export const extractRGBString = (str: string): string | null => {
|
||||
const matches = str.match(rgbStringRegex);
|
||||
if (!matches) return null;
|
||||
return matches[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a random integer between 0 and max.
|
||||
* @param max The maximum value (inclusive).
|
||||
|
@@ -365,7 +365,7 @@ namespace gdjs {
|
||||
* It is basically a container to associate channels to sounds and keep a list
|
||||
* of all sounds being played.
|
||||
*/
|
||||
export class HowlerSoundManager {
|
||||
export class HowlerSoundManager implements gdjs.ResourceManager {
|
||||
_loadedMusics = new gdjs.ResourceCache<Howl>();
|
||||
_loadedSounds = new gdjs.ResourceCache<Howl>();
|
||||
_availableResources: Record<string, ResourceData> = {};
|
||||
@@ -940,26 +940,16 @@ namespace gdjs {
|
||||
this.unloadAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this unloads all audio from the specified resources from memory.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const musicRes = this._loadedMusics.get(resourceData);
|
||||
if (musicRes) {
|
||||
this.unloadAudio(resourceData.name, true);
|
||||
}
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const musicRes = this._loadedMusics.get(resourceData);
|
||||
if (musicRes) {
|
||||
this.unloadAudio(resourceData.name, true);
|
||||
}
|
||||
|
||||
const soundRes = this._loadedSounds.get(resourceData);
|
||||
if (soundRes) {
|
||||
this.unloadAudio(resourceData.name, false);
|
||||
}
|
||||
});
|
||||
const soundRes = this._loadedSounds.get(resourceData);
|
||||
if (soundRes) {
|
||||
this.unloadAudio(resourceData.name, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -209,25 +209,16 @@ namespace gdjs {
|
||||
this._callbacks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this clears the JSONs loaded in this manager.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const loadedJson = this._loadedJsons.get(resourceData);
|
||||
if (loadedJson) {
|
||||
this._loadedJsons.delete(resourceData);
|
||||
}
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedJson = this._loadedJsons.get(resourceData);
|
||||
if (loadedJson) {
|
||||
this._loadedJsons.delete(resourceData);
|
||||
}
|
||||
|
||||
const callback = this._callbacks.get(resourceData);
|
||||
if (callback) {
|
||||
this._callbacks.delete(resourceData);
|
||||
}
|
||||
});
|
||||
const callback = this._callbacks.get(resourceData);
|
||||
if (callback) {
|
||||
this._callbacks.delete(resourceData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ namespace gdjs {
|
||||
*/
|
||||
export class Layer extends gdjs.RuntimeLayer {
|
||||
_cameraRotation: float = 0;
|
||||
/** The camera zoom factor strictly greater than 0. */
|
||||
_zoomFactor: float = 1;
|
||||
_cameraX: float;
|
||||
_cameraY: float;
|
||||
@@ -166,6 +167,9 @@ namespace gdjs {
|
||||
* @param cameraId The camera number. Currently ignored.
|
||||
*/
|
||||
override setCameraZoom(newZoom: float, cameraId?: integer): void {
|
||||
if (newZoom <= 0) {
|
||||
return;
|
||||
}
|
||||
this._zoomFactor = newZoom;
|
||||
this._isCameraZDirty = true;
|
||||
this._renderer.updatePosition();
|
||||
@@ -283,8 +287,8 @@ namespace gdjs {
|
||||
|
||||
x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2;
|
||||
y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2;
|
||||
x /= Math.abs(this._zoomFactor);
|
||||
y /= Math.abs(this._zoomFactor);
|
||||
x /= this._zoomFactor;
|
||||
y /= this._zoomFactor;
|
||||
|
||||
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
|
||||
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
|
||||
@@ -320,8 +324,8 @@ namespace gdjs {
|
||||
): FloatPoint {
|
||||
x -= this._runtimeScene.getViewportOriginX();
|
||||
y -= this._runtimeScene.getViewportOriginY();
|
||||
x /= Math.abs(this._zoomFactor);
|
||||
y /= Math.abs(this._zoomFactor);
|
||||
x /= this._zoomFactor;
|
||||
y /= this._zoomFactor;
|
||||
|
||||
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
|
||||
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
|
||||
@@ -367,8 +371,8 @@ namespace gdjs {
|
||||
const sinValue = Math.sin(-angleInRadians);
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * tmp + cosValue * y;
|
||||
x *= Math.abs(this._zoomFactor);
|
||||
y *= Math.abs(this._zoomFactor);
|
||||
x *= this._zoomFactor;
|
||||
y *= this._zoomFactor;
|
||||
position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2;
|
||||
position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2;
|
||||
|
||||
@@ -404,8 +408,8 @@ namespace gdjs {
|
||||
const sinValue = Math.sin(-angleInRadians);
|
||||
x = cosValue * x - sinValue * y;
|
||||
y = sinValue * tmp + cosValue * y;
|
||||
x *= Math.abs(this._zoomFactor);
|
||||
y *= Math.abs(this._zoomFactor);
|
||||
x *= this._zoomFactor;
|
||||
y *= this._zoomFactor;
|
||||
x += this._runtimeScene.getViewportOriginX();
|
||||
y += this._runtimeScene.getViewportOriginY();
|
||||
|
||||
|
@@ -308,32 +308,21 @@ namespace gdjs {
|
||||
this._loadedFontsData.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this uninstalls fonts from memory and clear cache of loaded fonts.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
*/
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const loadedFont = this._loadedFontsData.get(resourceData);
|
||||
if (loadedFont) {
|
||||
this._loadedFontsData.delete(resourceData);
|
||||
}
|
||||
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const loadedFont = this._loadedFontsData.get(resourceData);
|
||||
if (loadedFont) {
|
||||
this._loadedFontsData.delete(resourceData);
|
||||
}
|
||||
for (const bitmapFontInstallKey in this._pixiBitmapFontsInUse) {
|
||||
if (bitmapFontInstallKey.endsWith(resourceData.file))
|
||||
PIXI.BitmapFont.uninstall(bitmapFontInstallKey);
|
||||
}
|
||||
|
||||
for (const bitmapFontInstallKey in this._pixiBitmapFontsInUse) {
|
||||
if (bitmapFontInstallKey.endsWith(resourceData.file))
|
||||
PIXI.BitmapFont.uninstall(bitmapFontInstallKey);
|
||||
}
|
||||
|
||||
for (const bitmapFontInstallKey of this._pixiBitmapFontsToUninstall) {
|
||||
if (bitmapFontInstallKey.endsWith(resourceData.file))
|
||||
PIXI.BitmapFont.uninstall(bitmapFontInstallKey);
|
||||
}
|
||||
});
|
||||
for (const bitmapFontInstallKey of this._pixiBitmapFontsToUninstall) {
|
||||
if (bitmapFontInstallKey.endsWith(resourceData.file))
|
||||
PIXI.BitmapFont.uninstall(bitmapFontInstallKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -55,7 +55,7 @@ namespace gdjs {
|
||||
* Map associating a resource name to the loaded Three.js texture.
|
||||
*/
|
||||
private _loadedThreeTextures: Hashtable<THREE.Texture>;
|
||||
private _loadedThreeMaterials: Hashtable<THREE.Material>;
|
||||
private _loadedThreeMaterials = new ThreeMaterialCache();
|
||||
|
||||
private _diskTextures = new Map<float, PIXI.Texture>();
|
||||
private _rectangleTextures = new Map<string, PIXI.Texture>();
|
||||
@@ -73,7 +73,6 @@ namespace gdjs {
|
||||
{ width: 192, height: 192 }
|
||||
);
|
||||
this._loadedThreeTextures = new Hashtable();
|
||||
this._loadedThreeMaterials = new Hashtable();
|
||||
}
|
||||
|
||||
getResourceKinds(): ResourceKind[] {
|
||||
@@ -224,38 +223,37 @@ namespace gdjs {
|
||||
*/
|
||||
getThreeMaterial(
|
||||
resourceName: string,
|
||||
{
|
||||
useTransparentTexture,
|
||||
forceBasicMaterial,
|
||||
vertexColors,
|
||||
}: {
|
||||
options: {
|
||||
useTransparentTexture: boolean;
|
||||
forceBasicMaterial: boolean;
|
||||
vertexColors: boolean;
|
||||
}
|
||||
): THREE.Material {
|
||||
const cacheKey = `${resourceName}|${useTransparentTexture ? 1 : 0}|${
|
||||
forceBasicMaterial ? 1 : 0
|
||||
}|${vertexColors ? 1 : 0}`;
|
||||
|
||||
const loadedThreeMaterial = this._loadedThreeMaterials.get(cacheKey);
|
||||
const loadedThreeMaterial = this._loadedThreeMaterials.get(
|
||||
resourceName,
|
||||
options
|
||||
);
|
||||
if (loadedThreeMaterial) return loadedThreeMaterial;
|
||||
|
||||
const material = forceBasicMaterial
|
||||
const material = options.forceBasicMaterial
|
||||
? new THREE.MeshBasicMaterial({
|
||||
map: this.getThreeTexture(resourceName),
|
||||
side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide,
|
||||
transparent: useTransparentTexture,
|
||||
vertexColors,
|
||||
side: options.useTransparentTexture
|
||||
? THREE.DoubleSide
|
||||
: THREE.FrontSide,
|
||||
transparent: options.useTransparentTexture,
|
||||
vertexColors: options.vertexColors,
|
||||
})
|
||||
: new THREE.MeshStandardMaterial({
|
||||
map: this.getThreeTexture(resourceName),
|
||||
side: useTransparentTexture ? THREE.DoubleSide : THREE.FrontSide,
|
||||
transparent: useTransparentTexture,
|
||||
side: options.useTransparentTexture
|
||||
? THREE.DoubleSide
|
||||
: THREE.FrontSide,
|
||||
transparent: options.useTransparentTexture,
|
||||
metalness: 0,
|
||||
vertexColors,
|
||||
vertexColors: options.vertexColors,
|
||||
});
|
||||
this._loadedThreeMaterials.put(cacheKey, material);
|
||||
this._loadedThreeMaterials.set(resourceName, options, material);
|
||||
return material;
|
||||
}
|
||||
|
||||
@@ -485,12 +483,7 @@ namespace gdjs {
|
||||
threeTexture.dispose();
|
||||
}
|
||||
|
||||
const threeMaterials: THREE.Material[] = [];
|
||||
this._loadedThreeMaterials.values(threeMaterials);
|
||||
this._loadedThreeMaterials.clear();
|
||||
for (const threeMaterial of threeMaterials) {
|
||||
threeMaterial.dispose();
|
||||
}
|
||||
this._loadedThreeMaterials.disposeAll();
|
||||
|
||||
for (const pixiTexture of this._diskTextures.values()) {
|
||||
if (pixiTexture.destroyed) {
|
||||
@@ -520,35 +513,113 @@ namespace gdjs {
|
||||
this._scaledTextures.clear();
|
||||
}
|
||||
|
||||
unloadResource(resourceData: ResourceData): void {
|
||||
const resourceName = resourceData.name;
|
||||
const texture = this._loadedTextures.getFromName(resourceName);
|
||||
if (texture) {
|
||||
texture.destroy(true);
|
||||
this._loadedTextures.delete(resourceData);
|
||||
}
|
||||
|
||||
const threeTexture = this._loadedThreeTextures.get(resourceName);
|
||||
if (threeTexture) {
|
||||
threeTexture.dispose();
|
||||
this._loadedThreeTextures.remove(resourceName);
|
||||
}
|
||||
|
||||
this._loadedThreeMaterials.dispose(resourceName);
|
||||
}
|
||||
}
|
||||
|
||||
class ThreeMaterialCache {
|
||||
private _flaggedMaterials = new Map<string, THREE.Material>();
|
||||
private _materialFlaggedKeys = new Map<string, Array<string>>();
|
||||
|
||||
/**
|
||||
* Unload the specified list of resources:
|
||||
* this clears the cache of loaded textures associated to these resources.
|
||||
*
|
||||
* Usually called when scene resoures are unloaded.
|
||||
*
|
||||
* @param resourcesList The list of specific resources
|
||||
* Return the three.js material associated to the specified resource name
|
||||
* and options.
|
||||
* @param resourceName The name of the resource
|
||||
* @param options
|
||||
* @returns The requested material.
|
||||
*/
|
||||
unloadResourcesList(resourcesList: ResourceData[]): void {
|
||||
resourcesList.forEach((resourceData) => {
|
||||
const resourceName = resourceData.name;
|
||||
const resource = this._loadedTextures.get(resourceData);
|
||||
if (resource) {
|
||||
resource.destroy(true);
|
||||
this._loadedTextures.delete(resourceData);
|
||||
}
|
||||
get(
|
||||
resourceName: string,
|
||||
{
|
||||
useTransparentTexture,
|
||||
forceBasicMaterial,
|
||||
vertexColors,
|
||||
}: {
|
||||
useTransparentTexture: boolean;
|
||||
forceBasicMaterial: boolean;
|
||||
vertexColors: boolean;
|
||||
}
|
||||
): THREE.Material | null {
|
||||
const flaggedKey = `${resourceName}|${useTransparentTexture ? 1 : 0}|${
|
||||
forceBasicMaterial ? 1 : 0
|
||||
}|${vertexColors ? 1 : 0}`;
|
||||
return this._flaggedMaterials.get(flaggedKey) || null;
|
||||
}
|
||||
|
||||
const threeTexture = this._loadedThreeTextures.get(resourceName);
|
||||
if (threeTexture) {
|
||||
threeTexture.dispose();
|
||||
this._loadedThreeTextures.remove(resourceName);
|
||||
}
|
||||
/**
|
||||
* Set the three.js material associated to the specified resource name
|
||||
* and options.
|
||||
* @param resourceName The name of the resource
|
||||
* @param options
|
||||
* @param material The material to add to the cache
|
||||
*/
|
||||
set(
|
||||
resourceName: string,
|
||||
{
|
||||
useTransparentTexture,
|
||||
forceBasicMaterial,
|
||||
vertexColors,
|
||||
}: {
|
||||
useTransparentTexture: boolean;
|
||||
forceBasicMaterial: boolean;
|
||||
vertexColors: boolean;
|
||||
},
|
||||
material: THREE.Material
|
||||
): void {
|
||||
const cacheKey = `${resourceName}|${useTransparentTexture ? 1 : 0}|${
|
||||
forceBasicMaterial ? 1 : 0
|
||||
}|${vertexColors ? 1 : 0}`;
|
||||
this._flaggedMaterials.set(cacheKey, material);
|
||||
let flaggedKeys = this._materialFlaggedKeys.get(resourceName);
|
||||
if (!flaggedKeys) {
|
||||
flaggedKeys = [];
|
||||
this._materialFlaggedKeys.set(resourceName, flaggedKeys);
|
||||
}
|
||||
flaggedKeys.push(cacheKey);
|
||||
}
|
||||
|
||||
const threeMaterials = this._loadedThreeMaterials.get(resourceName);
|
||||
if (threeMaterials) {
|
||||
threeMaterials.dispose();
|
||||
this._loadedThreeMaterials.remove(resourceName);
|
||||
/**
|
||||
* Delete and dispose all the three.js material associated to the specified
|
||||
* resource name.
|
||||
* @param resourceName The name of the resource
|
||||
*/
|
||||
dispose(resourceName: string): void {
|
||||
const flaggedKeys = this._materialFlaggedKeys.get(resourceName);
|
||||
if (flaggedKeys) {
|
||||
for (const flaggedKey of flaggedKeys) {
|
||||
const threeMaterial = this._flaggedMaterials.get(flaggedKey);
|
||||
if (threeMaterial) {
|
||||
threeMaterial.dispose();
|
||||
}
|
||||
this._flaggedMaterials.delete(flaggedKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
this._materialFlaggedKeys.delete(resourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete and dispose all the three.js material in the cache.
|
||||
*/
|
||||
disposeAll(): void {
|
||||
for (const material of this._flaggedMaterials.values()) {
|
||||
material.dispose();
|
||||
}
|
||||
this._flaggedMaterials.clear();
|
||||
this._materialFlaggedKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -140,18 +140,18 @@ namespace gdjs {
|
||||
this._sprite.visible = !this._object.hidden;
|
||||
}
|
||||
|
||||
setColor(rgbOrHexColor): void {
|
||||
setColor(rgbOrHexColor: string): void {
|
||||
this._sprite.tint = gdjs.rgbOrHexStringToNumber(rgbOrHexColor);
|
||||
}
|
||||
|
||||
getColor() {
|
||||
const rgb = new PIXI.Color(this._sprite.tint).toRgbArray();
|
||||
return (
|
||||
Math.floor(rgb[0] * 255) +
|
||||
Math.round(rgb[0] * 255) +
|
||||
';' +
|
||||
Math.floor(rgb[1] * 255) +
|
||||
Math.round(rgb[1] * 255) +
|
||||
';' +
|
||||
Math.floor(rgb[2] * 255)
|
||||
Math.round(rgb[2] * 255)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -7,6 +7,8 @@ const {
|
||||
} = require('./lib/runtime-files-list');
|
||||
const args = require('minimist')(process.argv.slice(2), {
|
||||
string: ['out'],
|
||||
boolean: ['debug'],
|
||||
default: { debug: false }
|
||||
});
|
||||
const fs = require('fs').promises;
|
||||
|
||||
@@ -52,7 +54,7 @@ shell.mkdir('-p', bundledOutPath);
|
||||
return build({
|
||||
sourcemap: true,
|
||||
entryPoints: [inPath],
|
||||
minify: true,
|
||||
minify: !args.debug,
|
||||
outfile: renameBuiltFile(outPath),
|
||||
}).catch(() => {
|
||||
// Error is already logged by esbuild.
|
||||
|
@@ -92,15 +92,13 @@ gdjs.MockedResourceManager = class MockedResourceManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose specific resources
|
||||
* Dispose specific resource
|
||||
*/
|
||||
unloadResourcesList(resourcesList) {
|
||||
for (const resource of resourcesList) {
|
||||
this.disposedResources.add(resource.name);
|
||||
this.loadedResources.delete(resource.name);
|
||||
this.loadResourceCallbacks.delete(resource.name);
|
||||
this.loadResourcePromises.delete(resource.name);
|
||||
}
|
||||
unloadResource(resource) {
|
||||
this.disposedResources.add(resource.name);
|
||||
this.loadedResources.delete(resource.name);
|
||||
this.loadResourceCallbacks.delete(resource.name);
|
||||
this.loadResourcePromises.delete(resource.name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,4 +107,4 @@ gdjs.MockedResourceManager = class MockedResourceManager {
|
||||
getResourceKinds() {
|
||||
return ['fake-resource-kind-for-testing-only'];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -36,11 +36,11 @@ describe('gdjs', function () {
|
||||
expect(gdjs.rgbOrHexToRGBColor('255;255;300')).to.eql([255, 255, 255]);
|
||||
expect(gdjs.rgbOrHexToRGBColor('999;12;6')).to.eql([255, 12, 6]);
|
||||
});
|
||||
it('should cut rgb values if string too long', function () {
|
||||
it('should cap rgb values', function () {
|
||||
expect(gdjs.rgbOrHexToRGBColor('255;255;200456')).to.eql([
|
||||
255,
|
||||
255,
|
||||
200,
|
||||
255,
|
||||
]);
|
||||
});
|
||||
it('should return components for black if unrecognized input', function () {
|
||||
@@ -48,7 +48,6 @@ describe('gdjs', function () {
|
||||
expect(gdjs.rgbOrHexToRGBColor('19819830803')).to.eql([0, 0, 0]);
|
||||
expect(gdjs.rgbOrHexToRGBColor('Infinity')).to.eql([0, 0, 0]);
|
||||
expect(gdjs.rgbOrHexToRGBColor('-4564')).to.eql([0, 0, 0]);
|
||||
expect(gdjs.rgbOrHexToRGBColor('9999;12;6')).to.eql([0, 0, 0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1143,6 +1143,7 @@ interface LayersContainer {
|
||||
boolean HasLayerNamed([Const] DOMString name);
|
||||
void RemoveLayer([Const] DOMString name);
|
||||
unsigned long GetLayersCount();
|
||||
unsigned long GetLayerPosition([Const] DOMString name);
|
||||
void SwapLayers(unsigned long firstLayerIndex, unsigned long secondLayerIndex);
|
||||
void MoveLayer(unsigned long oldIndex, unsigned long newIndex);
|
||||
void SerializeLayersTo([Ref] SerializerElement element);
|
||||
@@ -2932,6 +2933,7 @@ interface MetadataProvider {
|
||||
boolean STATIC_IsBadInstructionMetadata([Const, Ref] InstructionMetadata metadata);
|
||||
boolean STATIC_IsBadBehaviorMetadata([Const, Ref] BehaviorMetadata metadata);
|
||||
boolean STATIC_IsBadObjectMetadata([Const, Ref] ObjectMetadata metadata);
|
||||
boolean STATIC_IsBadEffectMetadata([Const, Ref] EffectMetadata metadata);
|
||||
};
|
||||
|
||||
enum ProjectDiagnostic_ErrorType {
|
||||
@@ -3728,6 +3730,8 @@ interface TextObject {
|
||||
[Const, Ref] DOMString GetText();
|
||||
void SetCharacterSize(double size);
|
||||
double GetCharacterSize();
|
||||
void SetLineHeight(double value);
|
||||
double GetLineHeight();
|
||||
void SetFontName([Const] DOMString string);
|
||||
[Const, Ref] DOMString GetFontName();
|
||||
boolean IsBold();
|
||||
|
@@ -606,6 +606,7 @@ typedef std::vector<gd::PropertyDescriptorChoice> VectorPropertyDescriptorChoice
|
||||
#define STATIC_IsBadInstructionMetadata IsBadInstructionMetadata
|
||||
#define STATIC_IsBadBehaviorMetadata IsBadBehaviorMetadata
|
||||
#define STATIC_IsBadObjectMetadata IsBadObjectMetadata
|
||||
#define STATIC_IsBadEffectMetadata IsBadEffectMetadata
|
||||
|
||||
#define STATIC_RenameObjectInEvents RenameObjectInEvents
|
||||
#define STATIC_RemoveObjectInEvents RemoveObjectInEvents
|
||||
|
@@ -550,13 +550,13 @@ describe('libGD.js - GDJS related tests', function () {
|
||||
// GetArgumentAsString("MyString") should be generated code to query and cast as a string
|
||||
// the argument
|
||||
expect(code).toMatch(
|
||||
'(typeof eventsFunctionContext !== \'undefined\' ? "" + eventsFunctionContext.getArgument("MyString") : "")'
|
||||
'("" + eventsFunctionContext.getArgument("MyString"))'
|
||||
);
|
||||
|
||||
// GetArgumentAsNumber("MyNumber") should be generated code to query and cast as a string
|
||||
// the argument
|
||||
expect(code).toMatch(
|
||||
'(typeof eventsFunctionContext !== \'undefined\' ? Number(eventsFunctionContext.getArgument("MyNumber")) || 0 : 0)'
|
||||
'(Number(eventsFunctionContext.getArgument("MyNumber")) || 0)'
|
||||
);
|
||||
|
||||
// The loop is using a counter somewhere
|
||||
|
@@ -64,7 +64,7 @@ describe('libGD.js object serialization', function() {
|
||||
obj.delete();
|
||||
|
||||
expect(jsonObject).toBe(
|
||||
'{"bold":false,"italic":false,"smoothed":true,"underlined":false,"string":"Text of the object, with 官话 characters","font":"","textAlignment":"left","characterSize":20.0,"color":{"b":0,"g":0,"r":0},"content":{"bold":false,"isOutlineEnabled":false,"isShadowEnabled":false,"italic":false,"outlineColor":"255;255;255","outlineThickness":2.0,"shadowAngle":90.0,"shadowBlurRadius":2.0,"shadowColor":"0;0;0","shadowDistance":4.0,"shadowOpacity":127.0,"smoothed":true,"underlined":false,"text":"Text of the object, with 官话 characters","font":"","textAlignment":"left","verticalTextAlignment":"top","characterSize":20.0,"color":"0;0;0"}}'
|
||||
'{"bold":false,"italic":false,"smoothed":true,"underlined":false,"string":"Text of the object, with 官话 characters","font":"","textAlignment":"left","characterSize":20.0,"color":{"b":0,"g":0,"r":0},"content":{"bold":false,"isOutlineEnabled":false,"isShadowEnabled":false,"italic":false,"outlineColor":"255;255;255","outlineThickness":2.0,"shadowAngle":90.0,"shadowBlurRadius":2.0,"shadowColor":"0;0;0","shadowDistance":4.0,"shadowOpacity":127.0,"smoothed":true,"underlined":false,"text":"Text of the object, with 官话 characters","font":"","textAlignment":"left","verticalTextAlignment":"top","characterSize":20.0,"lineHeight":0.0,"color":"0;0;0"}}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
4
GDevelop.js/types.d.ts
vendored
4
GDevelop.js/types.d.ts
vendored
@@ -941,6 +941,7 @@ export class LayersContainer extends EmscriptenObject {
|
||||
hasLayerNamed(name: string): boolean;
|
||||
removeLayer(name: string): void;
|
||||
getLayersCount(): number;
|
||||
getLayerPosition(name: string): number;
|
||||
swapLayers(firstLayerIndex: number, secondLayerIndex: number): void;
|
||||
moveLayer(oldIndex: number, newIndex: number): void;
|
||||
serializeLayersTo(element: SerializerElement): void;
|
||||
@@ -2109,6 +2110,7 @@ export class MetadataProvider extends EmscriptenObject {
|
||||
static isBadInstructionMetadata(metadata: InstructionMetadata): boolean;
|
||||
static isBadBehaviorMetadata(metadata: BehaviorMetadata): boolean;
|
||||
static isBadObjectMetadata(metadata: ObjectMetadata): boolean;
|
||||
static isBadEffectMetadata(metadata: EffectMetadata): boolean;
|
||||
}
|
||||
|
||||
export class ProjectDiagnostic extends EmscriptenObject {
|
||||
@@ -2758,6 +2760,8 @@ export class TextObject extends ObjectConfiguration {
|
||||
getText(): string;
|
||||
setCharacterSize(size: number): void;
|
||||
getCharacterSize(): number;
|
||||
setLineHeight(value: number): void;
|
||||
getLineHeight(): number;
|
||||
setFontName(string: string): void;
|
||||
getFontName(): string;
|
||||
isBold(): boolean;
|
||||
|
@@ -7,6 +7,7 @@ declare class gdLayersContainer {
|
||||
hasLayerNamed(name: string): boolean;
|
||||
removeLayer(name: string): void;
|
||||
getLayersCount(): number;
|
||||
getLayerPosition(name: string): number;
|
||||
swapLayers(firstLayerIndex: number, secondLayerIndex: number): void;
|
||||
moveLayer(oldIndex: number, newIndex: number): void;
|
||||
serializeLayersTo(element: gdSerializerElement): void;
|
||||
|
@@ -26,6 +26,7 @@ declare class gdMetadataProvider {
|
||||
static isBadInstructionMetadata(metadata: gdInstructionMetadata): boolean;
|
||||
static isBadBehaviorMetadata(metadata: gdBehaviorMetadata): boolean;
|
||||
static isBadObjectMetadata(metadata: gdObjectMetadata): boolean;
|
||||
static isBadEffectMetadata(metadata: gdEffectMetadata): boolean;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -5,6 +5,8 @@ declare class gdTextObject extends gdObjectConfiguration {
|
||||
getText(): string;
|
||||
setCharacterSize(size: number): void;
|
||||
getCharacterSize(): number;
|
||||
setLineHeight(value: number): void;
|
||||
getLineHeight(): number;
|
||||
setFontName(string: string): void;
|
||||
getFontName(): string;
|
||||
isBold(): boolean;
|
||||
|
@@ -33,11 +33,14 @@ export const parameters = {
|
||||
// that we don't use.
|
||||
controls: { hideNoControlsWarning: true },
|
||||
docs: { disable: true },
|
||||
mockAddonConfigs: {
|
||||
globalMockData: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
themeDecorator,
|
||||
GDevelopJsInitializerDecorator,
|
||||
i18nProviderDecorator,
|
||||
BrowserDropDownMenuDisablerDecorator
|
||||
]
|
||||
BrowserDropDownMenuDisablerDecorator,
|
||||
];
|
||||
|
62
newIDE/app/src/AiGeneration/AiConfiguration.js
Normal file
62
newIDE/app/src/AiGeneration/AiConfiguration.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// @flow
|
||||
import { type Limits } from '../Utils/GDevelopServices/Usage';
|
||||
import {
|
||||
type AiConfigurationPreset,
|
||||
type AiSettings,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
|
||||
export type AiConfigurationPresetWithAvailability = {|
|
||||
...AiConfigurationPreset,
|
||||
disabled: boolean,
|
||||
enableWith: 'higher-tier-plan' | null,
|
||||
|};
|
||||
|
||||
export const getAiConfigurationPresetsWithAvailability = ({
|
||||
getAiSettings,
|
||||
limits,
|
||||
}: {|
|
||||
getAiSettings: () => AiSettings | null,
|
||||
limits: ?Limits,
|
||||
|}): Array<AiConfigurationPresetWithAvailability> => {
|
||||
const aiSettings = getAiSettings();
|
||||
if (!aiSettings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!limits) {
|
||||
return aiSettings.aiRequest.presets.map(preset => ({
|
||||
...preset,
|
||||
enableWith: null,
|
||||
}));
|
||||
}
|
||||
|
||||
return aiSettings.aiRequest.presets.map(preset => {
|
||||
const presetAvailability = limits.capabilities.ai.availablePresets.find(
|
||||
presetAvailability =>
|
||||
presetAvailability.id === preset.id &&
|
||||
presetAvailability.mode === preset.mode
|
||||
);
|
||||
|
||||
return {
|
||||
...preset,
|
||||
disabled:
|
||||
presetAvailability && presetAvailability.disabled !== undefined
|
||||
? presetAvailability.disabled
|
||||
: preset.disabled,
|
||||
enableWith: (presetAvailability && presetAvailability.enableWith) || null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getDefaultAiConfigurationPresetId = (
|
||||
aiConfigurationPresetsWithAvailability: Array<AiConfigurationPresetWithAvailability>
|
||||
): string => {
|
||||
const defaultPresetWithAvailability = aiConfigurationPresetsWithAvailability.find(
|
||||
preset => preset.isDefault
|
||||
);
|
||||
|
||||
return (
|
||||
(defaultPresetWithAvailability && defaultPresetWithAvailability.id) ||
|
||||
'default'
|
||||
);
|
||||
};
|
@@ -0,0 +1,69 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import CompactSelectField from '../../UI/CompactSelectField';
|
||||
import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale';
|
||||
import SelectOption from '../../UI/SelectOption';
|
||||
import { type AiConfigurationPresetWithAvailability } from '../AiConfiguration';
|
||||
|
||||
type AiConfigurationPresetSelectorProps = {
|
||||
chosenOrDefaultAiConfigurationPresetId: string,
|
||||
setAiConfigurationPresetId: string => void,
|
||||
aiConfigurationPresetsWithAvailability: Array<AiConfigurationPresetWithAvailability>,
|
||||
aiRequestMode: string,
|
||||
};
|
||||
|
||||
export const AiConfigurationPresetSelector = ({
|
||||
chosenOrDefaultAiConfigurationPresetId,
|
||||
setAiConfigurationPresetId,
|
||||
aiConfigurationPresetsWithAvailability,
|
||||
aiRequestMode,
|
||||
}: AiConfigurationPresetSelectorProps) => {
|
||||
const filteredAiConfigurationPresets = aiConfigurationPresetsWithAvailability.filter(
|
||||
preset => preset.mode === aiRequestMode
|
||||
);
|
||||
|
||||
const noUpgradeAiConfigurationPresets = filteredAiConfigurationPresets.filter(
|
||||
preset => !preset.disabled || preset.enableWith !== 'higher-tier-plan'
|
||||
);
|
||||
const upgradeAiConfigurationPresets = filteredAiConfigurationPresets.filter(
|
||||
preset => preset.disabled && preset.enableWith === 'higher-tier-plan'
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<CompactSelectField
|
||||
value={chosenOrDefaultAiConfigurationPresetId}
|
||||
onChange={value => {
|
||||
setAiConfigurationPresetId(value);
|
||||
}}
|
||||
>
|
||||
{noUpgradeAiConfigurationPresets.map(preset => (
|
||||
<SelectOption
|
||||
key={preset.id}
|
||||
value={preset.id}
|
||||
label={selectMessageByLocale(i18n, preset.nameByLocale)}
|
||||
disabled={preset.disabled}
|
||||
shouldNotTranslate
|
||||
/>
|
||||
))}
|
||||
{upgradeAiConfigurationPresets.length > 0 && (
|
||||
<optgroup key={`upgrade`} label={i18n._(t`Upgrade for:`)}>
|
||||
{upgradeAiConfigurationPresets.map(preset => (
|
||||
<SelectOption
|
||||
key={preset.id}
|
||||
value={preset.id}
|
||||
label={selectMessageByLocale(i18n, preset.nameByLocale)}
|
||||
disabled={preset.disabled}
|
||||
shouldNotTranslate
|
||||
/>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</CompactSelectField>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
@@ -38,6 +38,11 @@ import { ChatMessages } from './ChatMessages';
|
||||
import Send from '../../UI/CustomSvgIcons/Send';
|
||||
import { FeedbackBanner } from './FeedbackBanner';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
type AiConfigurationPresetWithAvailability,
|
||||
getDefaultAiConfigurationPresetId,
|
||||
} from '../AiConfiguration';
|
||||
import { AiConfigurationPresetSelector } from './AiConfigurationPresetSelector';
|
||||
|
||||
const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 5;
|
||||
const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 10;
|
||||
@@ -65,6 +70,7 @@ type Props = {
|
||||
onStartNewAiRequest: (options: {|
|
||||
userRequest: string,
|
||||
mode: 'chat' | 'agent',
|
||||
aiConfigurationPresetId: string,
|
||||
|}) => void,
|
||||
onSendMessage: (options: {|
|
||||
userMessage: string,
|
||||
@@ -85,6 +91,7 @@ type Props = {
|
||||
aiRequestId: string | null,
|
||||
|}) => void,
|
||||
initialMode?: 'chat' | 'agent',
|
||||
aiConfigurationPresetsWithAvailability: Array<AiConfigurationPresetWithAvailability>,
|
||||
|
||||
onProcessFunctionCalls: (
|
||||
functionCalls: Array<AiRequestMessageAssistantFunctionCall>,
|
||||
@@ -251,6 +258,7 @@ const getPriceText = ({
|
||||
export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
(
|
||||
{
|
||||
aiConfigurationPresetsWithAvailability,
|
||||
project,
|
||||
aiRequest,
|
||||
isSending,
|
||||
@@ -288,6 +296,41 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
},
|
||||
[initialMode]
|
||||
);
|
||||
|
||||
const [
|
||||
aiConfigurationPresetId,
|
||||
setAiConfigurationPresetId,
|
||||
] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (!aiConfigurationPresetsWithAvailability.length) return;
|
||||
|
||||
if (!aiConfigurationPresetId) return;
|
||||
|
||||
if (
|
||||
aiConfigurationPresetsWithAvailability.find(
|
||||
preset =>
|
||||
preset.id === aiConfigurationPresetId &&
|
||||
preset.mode === newAiRequestMode
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The selected preset is not a valid choice for the current mode - reset it.
|
||||
console.info(
|
||||
"Reset the AI configuration preset because it's not valid for the current mode."
|
||||
);
|
||||
setAiConfigurationPresetId(null);
|
||||
},
|
||||
[
|
||||
newAiRequestMode,
|
||||
aiConfigurationPresetsWithAvailability,
|
||||
aiConfigurationPresetId,
|
||||
]
|
||||
);
|
||||
|
||||
const aiRequestId: string = aiRequest ? aiRequest.id : '';
|
||||
const [
|
||||
userRequestTextPerAiRequestId,
|
||||
@@ -421,6 +464,10 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
</Text>
|
||||
);
|
||||
|
||||
const chosenOrDefaultAiConfigurationPresetId =
|
||||
aiConfigurationPresetId ||
|
||||
getDefaultAiConfigurationPresetId(aiConfigurationPresetsWithAvailability);
|
||||
|
||||
if (!aiRequest) {
|
||||
return (
|
||||
<div
|
||||
@@ -469,6 +516,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
userRequest: userRequestTextPerAiRequestId[''],
|
||||
aiConfigurationPresetId: chosenOrDefaultAiConfigurationPresetId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -494,6 +542,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
userRequest: userRequestTextPerAiRequestId[''],
|
||||
aiConfigurationPresetId: chosenOrDefaultAiConfigurationPresetId,
|
||||
});
|
||||
}}
|
||||
placeholder={newChatPlaceholder}
|
||||
@@ -502,8 +551,20 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
<Column>
|
||||
<LineStackLayout
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<AiConfigurationPresetSelector
|
||||
chosenOrDefaultAiConfigurationPresetId={
|
||||
chosenOrDefaultAiConfigurationPresetId
|
||||
}
|
||||
setAiConfigurationPresetId={
|
||||
setAiConfigurationPresetId
|
||||
}
|
||||
aiConfigurationPresetsWithAvailability={
|
||||
aiConfigurationPresetsWithAvailability
|
||||
}
|
||||
aiRequestMode={newAiRequestMode}
|
||||
/>
|
||||
<RaisedButton
|
||||
color="primary"
|
||||
icon={<Send />}
|
||||
@@ -527,6 +588,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
userRequest: userRequestTextPerAiRequestId[''],
|
||||
aiConfigurationPresetId: chosenOrDefaultAiConfigurationPresetId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
@@ -2,10 +2,16 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
getAiRequest,
|
||||
fetchAiSettings,
|
||||
type AiRequest,
|
||||
type AiSettings,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { type EditorFunctionCallResult } from '../EditorFunctions/EditorFunctionCallRunner';
|
||||
import Window from '../Utils/Window';
|
||||
import { AI_SETTINGS_FETCH_TIMEOUT } from '../Utils/GlobalFetchTimeouts';
|
||||
import { useAsyncLazyMemo } from '../Utils/UseLazyMemo';
|
||||
import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||
|
||||
type EditorFunctionCallResultsStorage = {|
|
||||
getEditorFunctionCallResults: (
|
||||
@@ -186,6 +192,7 @@ export const useAiRequestsStorage = (): AiRequestStorage => {
|
||||
type AiRequestContextState = {|
|
||||
aiRequestStorage: AiRequestStorage,
|
||||
editorFunctionCallResultsStorage: EditorFunctionCallResultsStorage,
|
||||
getAiSettings: () => AiSettings | null,
|
||||
|};
|
||||
|
||||
export const AiRequestContext = React.createContext<AiRequestContextState>({
|
||||
@@ -203,6 +210,7 @@ export const AiRequestContext = React.createContext<AiRequestContextState>({
|
||||
addEditorFunctionCallResults: () => {},
|
||||
clearEditorFunctionCallResults: () => {},
|
||||
},
|
||||
getAiSettings: () => null,
|
||||
});
|
||||
|
||||
type AiRequestProviderProps = {|
|
||||
@@ -213,12 +221,44 @@ export const AiRequestProvider = ({ children }: AiRequestProviderProps) => {
|
||||
const editorFunctionCallResultsStorage = useEditorFunctionCallResultsStorage();
|
||||
const aiRequestStorage = useAiRequestsStorage();
|
||||
|
||||
const environment = Window.isDev() ? 'staging' : 'live';
|
||||
const getAiSettings = useAsyncLazyMemo(
|
||||
React.useCallback(
|
||||
async (): Promise<AiSettings | null> => {
|
||||
try {
|
||||
const aiSettings = await retryIfFailed({ times: 2 }, () =>
|
||||
fetchAiSettings({
|
||||
environment,
|
||||
})
|
||||
);
|
||||
|
||||
return aiSettings;
|
||||
} catch (error) {
|
||||
console.error('Error while fetching AI settings:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[environment]
|
||||
)
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
getAiSettings();
|
||||
}, AI_SETTINGS_FETCH_TIMEOUT);
|
||||
return () => clearTimeout(timeoutId);
|
||||
},
|
||||
[getAiSettings]
|
||||
);
|
||||
|
||||
const state = React.useMemo(
|
||||
() => ({
|
||||
aiRequestStorage,
|
||||
editorFunctionCallResultsStorage,
|
||||
getAiSettings,
|
||||
}),
|
||||
[aiRequestStorage, editorFunctionCallResultsStorage]
|
||||
[aiRequestStorage, editorFunctionCallResultsStorage, getAiSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -5,6 +5,7 @@ import { I18n } from '@lingui/react';
|
||||
import {
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from '../MainFrame/EditorContainers/BaseEditor';
|
||||
import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import Paper from '../UI/Paper';
|
||||
@@ -53,6 +54,7 @@ import { useCreateAiProjectDialog } from './UseCreateAiProjectDialog';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
import { getAiConfigurationPresetsWithAvailability } from './AiConfiguration';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -66,6 +68,7 @@ const useProcessFunctionCalls = ({
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
}: {|
|
||||
i18n: I18nType,
|
||||
@@ -84,6 +87,9 @@ const useProcessFunctionCalls = ({
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
|}) => {
|
||||
const { ensureExtensionInstalled } = useEnsureExtensionInstalled({
|
||||
@@ -158,6 +164,7 @@ const useProcessFunctionCalls = ({
|
||||
});
|
||||
},
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
});
|
||||
@@ -178,6 +185,7 @@ const useProcessFunctionCalls = ({
|
||||
searchAndInstallAsset,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
triggerSendEditorFunctionCallResults,
|
||||
editorCallbacks,
|
||||
]
|
||||
@@ -348,6 +356,9 @@ type Props = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
initialMode: 'chat' | 'agent' | null,
|
||||
initialAiRequestId: string | null,
|
||||
@@ -371,6 +382,9 @@ export type AskAiEditorInterface = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
startOrOpenChat: ({|
|
||||
mode: 'chat' | 'agent',
|
||||
aiRequestId: string | null,
|
||||
@@ -380,6 +394,7 @@ export type AskAiEditorInterface = {|
|
||||
export type NewAiRequestOptions = {|
|
||||
mode: 'chat' | 'agent',
|
||||
userRequest: string,
|
||||
aiConfigurationPresetId: string,
|
||||
|};
|
||||
|
||||
const noop = () => {};
|
||||
@@ -399,6 +414,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onCreateProjectFromExample,
|
||||
onOpenLayout,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
initialMode,
|
||||
initialAiRequestId,
|
||||
@@ -470,6 +486,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
const {
|
||||
aiRequestStorage,
|
||||
editorFunctionCallResultsStorage,
|
||||
getAiSettings,
|
||||
} = React.useContext(AiRequestContext);
|
||||
const {
|
||||
getEditorFunctionCallResults,
|
||||
@@ -515,6 +532,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
onSceneObjectEdited: noop,
|
||||
onSceneObjectsDeleted: noop,
|
||||
onSceneEventsModifiedOutsideEditor: noop,
|
||||
onInstancesModifiedOutsideEditor: noop,
|
||||
startOrOpenChat: onStartOrOpenChat,
|
||||
}));
|
||||
|
||||
@@ -572,7 +590,11 @@ export const AskAiEditor = React.memo<Props>(
|
||||
}
|
||||
|
||||
// Read the options and reset them (to avoid launching the same request twice).
|
||||
const { mode, userRequest } = newAiRequestOptions;
|
||||
const {
|
||||
mode,
|
||||
userRequest,
|
||||
aiConfigurationPresetId,
|
||||
} = newAiRequestOptions;
|
||||
startNewAiRequest(null);
|
||||
|
||||
// If no project is opened, create a new empty one if the request is for
|
||||
@@ -590,6 +612,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
startNewAiRequest({
|
||||
mode,
|
||||
userRequest,
|
||||
aiConfigurationPresetId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating a new empty project:', error);
|
||||
@@ -647,6 +670,10 @@ export const AskAiEditor = React.memo<Props>(
|
||||
fileMetadata,
|
||||
storageProviderName,
|
||||
mode,
|
||||
toolsVersion: 'v2',
|
||||
aiConfiguration: {
|
||||
presetId: aiConfigurationPresetId,
|
||||
},
|
||||
});
|
||||
|
||||
console.info('Successfully created a new AI request:', aiRequest);
|
||||
@@ -933,6 +960,7 @@ export const AskAiEditor = React.memo<Props>(
|
||||
getEditorFunctionCallResults,
|
||||
addEditorFunctionCallResults,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
i18n,
|
||||
onExtensionInstalled,
|
||||
});
|
||||
@@ -942,6 +970,9 @@ export const AskAiEditor = React.memo<Props>(
|
||||
<Paper square background="dark" style={styles.paper}>
|
||||
<div style={styles.chatContainer}>
|
||||
<AiRequestChat
|
||||
aiConfigurationPresetsWithAvailability={getAiConfigurationPresetsWithAvailability(
|
||||
{ limits, getAiSettings }
|
||||
)}
|
||||
project={project || null}
|
||||
ref={aiRequestChatRef}
|
||||
aiRequest={selectedAiRequest}
|
||||
@@ -1030,6 +1061,9 @@ export const renderAskAiEditorContainer = (
|
||||
onSceneEventsModifiedOutsideEditor={
|
||||
props.onSceneEventsModifiedOutsideEditor
|
||||
}
|
||||
onInstancesModifiedOutsideEditor={
|
||||
props.onInstancesModifiedOutsideEditor
|
||||
}
|
||||
onExtensionInstalled={props.onExtensionInstalled}
|
||||
initialMode={
|
||||
(props.extraEditorProps && props.extraEditorProps.mode) || null
|
||||
|
@@ -65,7 +65,7 @@ export const AnnouncementsFeed = ({
|
||||
|
||||
const classesForClickableContainer = useStylesForClickableContainer();
|
||||
|
||||
if (error) {
|
||||
if (error && !hideLoader) {
|
||||
return (
|
||||
<PlaceholderError onRetry={fetchAnnouncementsAndPromotions}>
|
||||
<Trans>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import {
|
||||
type PublicAssetPacks,
|
||||
type PrivateAssetPack,
|
||||
@@ -8,7 +9,17 @@ import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
getArchivedBundleListingData,
|
||||
getArchivedPrivateGameTemplateListingData,
|
||||
getArchivedPrivateAssetPackListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
PrivateAssetPackTile,
|
||||
PublicAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
BundleTile,
|
||||
} from './ShopTiles';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
|
||||
/**
|
||||
* A simple slug generator that allows to link to asset packs on
|
||||
@@ -136,3 +147,350 @@ export const getBundleListingDataFromUserFriendlySlug = ({
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getBundleTiles = ({
|
||||
allBundleListingDatas,
|
||||
displayedBundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
}: {|
|
||||
allBundleListingDatas: ?Array<BundleListingData>,
|
||||
displayedBundleListingDatas: ?Array<BundleListingData>,
|
||||
onBundleSelection: ?(BundleListingData) => void,
|
||||
receivedBundles: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!allBundleListingDatas ||
|
||||
!displayedBundleListingDatas ||
|
||||
!onBundleSelection ||
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
|
||||
const bundleTiles: Array<React.Node> = [];
|
||||
const ownedBundleTiles: Array<React.Node> = [];
|
||||
|
||||
displayedBundleListingDatas
|
||||
.filter(
|
||||
bundleListingData =>
|
||||
!openedShopCategory ||
|
||||
bundleListingData.categories.includes(openedShopCategory)
|
||||
)
|
||||
.forEach(bundleListingData => {
|
||||
const isBundleOwned =
|
||||
!!receivedBundles &&
|
||||
!!receivedBundles.find(bundle => bundle.id === bundleListingData.id);
|
||||
const tile = (
|
||||
<BundleTile
|
||||
bundleListingData={bundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(bundleListingData);
|
||||
}}
|
||||
owned={isBundleOwned}
|
||||
key={bundleListingData.id}
|
||||
/>
|
||||
);
|
||||
if (isBundleOwned) {
|
||||
ownedBundleTiles.push(tile);
|
||||
} else {
|
||||
bundleTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle archived bundles that are owned by the user.
|
||||
// These are bundles that are not listed in the shop anymore,
|
||||
// but that the user has already purchased.
|
||||
const archivedOwnedBundleTiles = (receivedBundles || [])
|
||||
.filter(
|
||||
bundle =>
|
||||
!allBundleListingDatas.find(
|
||||
bundleListingData => bundleListingData.id === bundle.id
|
||||
)
|
||||
)
|
||||
.map(bundle => {
|
||||
const archivedBundleListingData = getArchivedBundleListingData({
|
||||
bundle,
|
||||
});
|
||||
return (
|
||||
<BundleTile
|
||||
bundleListingData={archivedBundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(archivedBundleListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={bundle.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return [...ownedBundleTiles, ...archivedOwnedBundleTiles, ...bundleTiles];
|
||||
};
|
||||
|
||||
export const getGameTemplateTiles = ({
|
||||
allPrivateGameTemplateListingDatas,
|
||||
displayedPrivateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
hasAssetPackFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}: {|
|
||||
allPrivateGameTemplateListingDatas: ?Array<PrivateGameTemplateListingData>,
|
||||
displayedPrivateGameTemplateListingDatas: ?Array<PrivateGameTemplateListingData>,
|
||||
onPrivateGameTemplateSelection: ?(PrivateGameTemplateListingData) => void,
|
||||
receivedGameTemplates: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
hasAssetPackFiltersApplied?: boolean,
|
||||
onlyShowAssets?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!allPrivateGameTemplateListingDatas ||
|
||||
!displayedPrivateGameTemplateListingDatas ||
|
||||
!onPrivateGameTemplateSelection ||
|
||||
hasAssetFiltersApplied ||
|
||||
hasAssetPackFiltersApplied ||
|
||||
onlyShowAssets
|
||||
)
|
||||
return [];
|
||||
|
||||
const gameTemplateTiles: Array<React.Node> = [];
|
||||
const ownedGameTemplateTiles: Array<React.Node> = [];
|
||||
|
||||
const filteredGameTemplates = displayedPrivateGameTemplateListingDatas.filter(
|
||||
privateGameTemplateListingData =>
|
||||
!openedShopCategory || openedShopCategory === 'game-template'
|
||||
);
|
||||
|
||||
filteredGameTemplates.forEach(privateGameTemplateListingData => {
|
||||
const isGameTemplateOwned =
|
||||
!!receivedGameTemplates &&
|
||||
!!receivedGameTemplates.find(
|
||||
pack => pack.id === privateGameTemplateListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={privateGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(privateGameTemplateListingData);
|
||||
}}
|
||||
owned={isGameTemplateOwned}
|
||||
key={privateGameTemplateListingData.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isGameTemplateOwned) {
|
||||
ownedGameTemplateTiles.push(tile);
|
||||
} else {
|
||||
gameTemplateTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle archived game templates that are owned by the user.
|
||||
// These are game templates that are not listed in the shop anymore,
|
||||
// but that the user has already purchased.
|
||||
const archivedOwnedGameTemplateTiles = (receivedGameTemplates || [])
|
||||
.filter(
|
||||
gameTemplate =>
|
||||
!allPrivateGameTemplateListingDatas.find(
|
||||
privateGameTemplateListingData =>
|
||||
privateGameTemplateListingData.id === gameTemplate.id
|
||||
)
|
||||
)
|
||||
.map(gameTemplate => {
|
||||
const archivedGameTemplateListingData = getArchivedPrivateGameTemplateListingData(
|
||||
{
|
||||
gameTemplate,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={archivedGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(archivedGameTemplateListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={gameTemplate.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
...ownedGameTemplateTiles,
|
||||
...archivedOwnedGameTemplateTiles,
|
||||
...gameTemplateTiles,
|
||||
];
|
||||
};
|
||||
|
||||
export const getPublicAssetPackTiles = ({
|
||||
publicAssetPacks,
|
||||
onPublicAssetPackSelection,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
}: {|
|
||||
publicAssetPacks: ?(PublicAssetPack[]),
|
||||
onPublicAssetPackSelection: ?(PublicAssetPack) => void,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
|}): Array<React.Node> => {
|
||||
if (
|
||||
!publicAssetPacks ||
|
||||
!onPublicAssetPackSelection ||
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
|
||||
const filteredAssetPacks = publicAssetPacks.filter(
|
||||
assetPack =>
|
||||
!openedShopCategory || assetPack.categories.includes(openedShopCategory)
|
||||
);
|
||||
|
||||
return filteredAssetPacks.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
export const getAssetPackTiles = ({
|
||||
allPrivateAssetPackListingDatas,
|
||||
displayedPrivateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicAssetPackTiles,
|
||||
receivedAssetPacks,
|
||||
openedShopCategory,
|
||||
hasAssetFiltersApplied,
|
||||
}: {|
|
||||
allPrivateAssetPackListingDatas: ?Array<PrivateAssetPackListingData>,
|
||||
displayedPrivateAssetPackListingDatas: ?Array<PrivateAssetPackListingData>,
|
||||
onPrivateAssetPackSelection: ?(PrivateAssetPackListingData) => void,
|
||||
publicAssetPackTiles?: Array<React.Node>,
|
||||
receivedAssetPacks: ?Array<any>,
|
||||
openedShopCategory?: ?string,
|
||||
hasAssetFiltersApplied?: boolean,
|
||||
|}): {|
|
||||
allAssetPackStandAloneTiles: Array<React.Node>,
|
||||
allAssetPackBundleTiles: Array<React.Node>,
|
||||
|} => {
|
||||
if (
|
||||
!allPrivateAssetPackListingDatas ||
|
||||
!displayedPrivateAssetPackListingDatas ||
|
||||
hasAssetFiltersApplied
|
||||
) {
|
||||
return {
|
||||
allAssetPackStandAloneTiles: [],
|
||||
allAssetPackBundleTiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
const filteredAssetPacks = displayedPrivateAssetPackListingDatas.filter(
|
||||
assetPackListingData =>
|
||||
!openedShopCategory ||
|
||||
assetPackListingData.categories.includes(openedShopCategory)
|
||||
);
|
||||
|
||||
!!onPrivateAssetPackSelection &&
|
||||
filteredAssetPacks.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(pack => pack.id === assetPackListingData.id);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle archived asset packs that are owned by the user.
|
||||
// These are asset packs that are not listed in the shop anymore,
|
||||
// but that the user has already purchased.
|
||||
const archivedOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const archivedOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
!!onPrivateAssetPackSelection &&
|
||||
(receivedAssetPacks || [])
|
||||
.filter(
|
||||
assetPack =>
|
||||
!allPrivateAssetPackListingDatas.find(
|
||||
privateAssetPackListingData =>
|
||||
privateAssetPackListingData.id === assetPack.id
|
||||
)
|
||||
)
|
||||
.forEach(assetPack => {
|
||||
const archivedAssetPackListingData = getArchivedPrivateAssetPackListingData(
|
||||
{
|
||||
assetPack,
|
||||
}
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={archivedAssetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(archivedAssetPackListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={assetPack.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (
|
||||
archivedAssetPackListingData.includedListableProductIds &&
|
||||
!!archivedAssetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
archivedOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
archivedOwnedAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
const allAssetPackBundleTiles = [
|
||||
...privateOwnedAssetPackBundleTiles,
|
||||
...archivedOwnedAssetPackBundleTiles,
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allAssetPackStandAloneTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles,
|
||||
...archivedOwnedAssetPackStandAloneTiles,
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
publicAssetPackTiles || [],
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allAssetPackStandAloneTiles, allAssetPackBundleTiles };
|
||||
};
|
||||
|
@@ -11,9 +11,6 @@ import {
|
||||
type PrivateAssetPackListingData,
|
||||
type PrivateGameTemplateListingData,
|
||||
type BundleListingData,
|
||||
getArchivedBundleListingData,
|
||||
getArchivedPrivateGameTemplateListingData,
|
||||
getArchivedPrivateAssetPackListingData,
|
||||
} from '../Utils/GDevelopServices/Shop';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
@@ -22,19 +19,18 @@ import {
|
||||
type WindowSizeType,
|
||||
} from '../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
import {
|
||||
CategoryTile,
|
||||
PrivateAssetPackTile,
|
||||
PublicAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
BundleTile,
|
||||
} from './ShopTiles';
|
||||
import { CategoryTile } from './ShopTiles';
|
||||
import { useDebounce } from '../Utils/UseDebounce';
|
||||
import PromotionsSlideshow from '../Promotions/PromotionsSlideshow';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { EarnCredits } from '../GameDashboard/Wallet/EarnCredits';
|
||||
import { LARGE_WIDGET_SIZE } from '../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
import {
|
||||
getBundleTiles,
|
||||
getGameTemplateTiles,
|
||||
getAssetPackTiles,
|
||||
getPublicAssetPackTiles,
|
||||
} from './AssetStoreUtils';
|
||||
|
||||
const cellSpacing = 10;
|
||||
|
||||
@@ -253,204 +249,47 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
? shopCategories[openedShopCategory].title
|
||||
: null;
|
||||
|
||||
const starterPacksTiles: Array<React.Node> = starterPacks
|
||||
.filter(
|
||||
assetPack =>
|
||||
!openedShopCategory ||
|
||||
assetPack.categories.includes(openedShopCategory)
|
||||
)
|
||||
.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
const publicPackTiles: Array<React.Node> = React.useMemo(
|
||||
() =>
|
||||
getPublicAssetPackTiles({
|
||||
publicAssetPacks: starterPacks,
|
||||
onPublicAssetPackSelection,
|
||||
openedShopCategory,
|
||||
}),
|
||||
[starterPacks, onPublicAssetPackSelection, openedShopCategory]
|
||||
);
|
||||
|
||||
const {
|
||||
allAssetPackStandAloneTiles,
|
||||
allAssetPackBundleTiles,
|
||||
} = React.useMemo(
|
||||
() => {
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
privateAssetPackListingDatas
|
||||
.filter(
|
||||
assetPackListingData =>
|
||||
!openedShopCategory ||
|
||||
assetPackListingData.categories.includes(openedShopCategory)
|
||||
)
|
||||
.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === assetPackListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const archivedOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const archivedOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
// Some asset pack products can be archived, meaning the listing data
|
||||
// is not available anymore, but the user still owns the asset pack.
|
||||
// We look at the remaining receivedAssetPacks to display them.
|
||||
(receivedAssetPacks || [])
|
||||
.filter(
|
||||
assetPack =>
|
||||
!privateAssetPackListingDatas.find(
|
||||
privateAssetPackListingData =>
|
||||
privateAssetPackListingData.id === assetPack.id
|
||||
)
|
||||
)
|
||||
.forEach(assetPack => {
|
||||
const archivedAssetPackListingData = getArchivedPrivateAssetPackListingData(
|
||||
{
|
||||
assetPack,
|
||||
}
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={archivedAssetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(archivedAssetPackListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={assetPack.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (
|
||||
archivedAssetPackListingData.includedListableProductIds &&
|
||||
!!archivedAssetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
archivedOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
archivedOwnedAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
const allAssetPackBundleTiles = [
|
||||
...privateOwnedAssetPackBundleTiles, // Display owned bundles first.
|
||||
...archivedOwnedAssetPackBundleTiles,
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allAssetPackStandAloneTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles, // Display owned packs first.
|
||||
...archivedOwnedAssetPackStandAloneTiles,
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
starterPacksTiles,
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allAssetPackStandAloneTiles, allAssetPackBundleTiles };
|
||||
},
|
||||
() =>
|
||||
getAssetPackTiles({
|
||||
allPrivateAssetPackListingDatas: privateAssetPackListingDatas,
|
||||
displayedPrivateAssetPackListingDatas: privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicAssetPackTiles: publicPackTiles,
|
||||
receivedAssetPacks,
|
||||
openedShopCategory,
|
||||
}),
|
||||
[
|
||||
privateAssetPackListingDatas,
|
||||
openedShopCategory,
|
||||
onPrivateAssetPackSelection,
|
||||
starterPacksTiles,
|
||||
publicPackTiles,
|
||||
receivedAssetPacks,
|
||||
]
|
||||
);
|
||||
|
||||
const allBundleTiles = React.useMemo(
|
||||
() => {
|
||||
const bundleTiles: Array<React.Node> = [];
|
||||
const ownedBundleTiles: Array<React.Node> = [];
|
||||
|
||||
bundleListingDatas
|
||||
.filter(
|
||||
bundleListingData =>
|
||||
!openedShopCategory ||
|
||||
bundleListingData.categories.includes(openedShopCategory)
|
||||
)
|
||||
.forEach(bundleListingData => {
|
||||
const isBundleOwned =
|
||||
!!receivedBundles &&
|
||||
!!receivedBundles.find(
|
||||
bundle => bundle.id === bundleListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<BundleTile
|
||||
bundleListingData={bundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(bundleListingData);
|
||||
}}
|
||||
owned={isBundleOwned}
|
||||
key={bundleListingData.id}
|
||||
/>
|
||||
);
|
||||
if (isBundleOwned) {
|
||||
ownedBundleTiles.push(tile);
|
||||
} else {
|
||||
bundleTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Some bundle products can be archived, meaning the listing data
|
||||
// is not available anymore, but the user still owns the bundle.
|
||||
// We look at the remaining receivedBundles to display them.
|
||||
const archivedOwnedBundleTiles = (receivedBundles || [])
|
||||
.filter(
|
||||
bundle =>
|
||||
!bundleListingDatas.find(
|
||||
bundleListingData => bundleListingData.id === bundle.id
|
||||
)
|
||||
)
|
||||
.map(bundle => {
|
||||
const archivedBundleListingData = getArchivedBundleListingData({
|
||||
bundle,
|
||||
});
|
||||
return (
|
||||
<BundleTile
|
||||
bundleListingData={archivedBundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(archivedBundleListingData);
|
||||
}}
|
||||
owned={true}
|
||||
key={bundle.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
...ownedBundleTiles, // Display owned bundles first.
|
||||
...archivedOwnedBundleTiles,
|
||||
...bundleTiles,
|
||||
];
|
||||
},
|
||||
() =>
|
||||
getBundleTiles({
|
||||
allBundleListingDatas: bundleListingDatas,
|
||||
displayedBundleListingDatas: bundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
openedShopCategory,
|
||||
}),
|
||||
[
|
||||
bundleListingDatas,
|
||||
openedShopCategory,
|
||||
@@ -460,79 +299,14 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
);
|
||||
|
||||
const gameTemplateTiles = React.useMemo(
|
||||
() => {
|
||||
const gameTemplateTiles: Array<React.Node> = [];
|
||||
const ownedGameTemplateTiles: Array<React.Node> = [];
|
||||
|
||||
// Only show game templates if the category is not set or is set to "game-template".
|
||||
privateGameTemplateListingDatas
|
||||
.filter(
|
||||
privateGameTemplateListingData =>
|
||||
!openedShopCategory || openedShopCategory === 'game-template'
|
||||
)
|
||||
.forEach(privateGameTemplateListingData => {
|
||||
const isGameTemplateOwned =
|
||||
!!receivedGameTemplates &&
|
||||
!!receivedGameTemplates.find(
|
||||
pack => pack.id === privateGameTemplateListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={privateGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(
|
||||
privateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
owned={isGameTemplateOwned}
|
||||
key={privateGameTemplateListingData.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isGameTemplateOwned) {
|
||||
ownedGameTemplateTiles.push(tile);
|
||||
} else {
|
||||
gameTemplateTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
// Some game template products can be archived, meaning the listing data
|
||||
// is not available anymore, but the user still owns the game template.
|
||||
// We look at the remaining receivedGameTemplates to display them.
|
||||
const archivedOwnedGameTemplateTiles = (receivedGameTemplates || [])
|
||||
.filter(
|
||||
gameTemplate =>
|
||||
!privateGameTemplateListingDatas.find(
|
||||
privateGameTemplateListingData =>
|
||||
privateGameTemplateListingData.id === gameTemplate.id
|
||||
)
|
||||
)
|
||||
.map(gameTemplate => {
|
||||
const archivedGameTemplateListingData = getArchivedPrivateGameTemplateListingData(
|
||||
{
|
||||
gameTemplate,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={archivedGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(
|
||||
archivedGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
owned={true}
|
||||
key={gameTemplate.id}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
...ownedGameTemplateTiles, // Display owned game templates first.
|
||||
...archivedOwnedGameTemplateTiles,
|
||||
...gameTemplateTiles,
|
||||
];
|
||||
},
|
||||
() =>
|
||||
getGameTemplateTiles({
|
||||
allPrivateGameTemplateListingDatas: privateGameTemplateListingDatas,
|
||||
displayedPrivateGameTemplateListingDatas: privateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
openedShopCategory,
|
||||
}),
|
||||
[
|
||||
privateGameTemplateListingDatas,
|
||||
openedShopCategory,
|
||||
|
@@ -25,15 +25,7 @@ import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
import {
|
||||
AssetCardTile,
|
||||
AssetFolderTile,
|
||||
PrivateAssetPackTile,
|
||||
PrivateGameTemplateTile,
|
||||
PublicAssetPackTile,
|
||||
BundleTile,
|
||||
} from './ShopTiles';
|
||||
import { AssetCardTile, AssetFolderTile } from './ShopTiles';
|
||||
import PrivateAssetPackAudioFilesDownloadButton from './PrivateAssets/PrivateAssetPackAudioFilesDownloadButton';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import { Column, LargeSpacer, Line } from '../UI/Grid';
|
||||
@@ -55,6 +47,13 @@ import { OwnedProductLicense } from './ProductLicense/ProductLicenseOptions';
|
||||
import { getUserProductPurchaseUsageType } from './ProductPageHelper';
|
||||
import PublicProfileContext from '../Profile/PublicProfileContext';
|
||||
import { BundleStoreContext } from './Bundles/BundleStoreContext';
|
||||
import {
|
||||
getBundleTiles,
|
||||
getGameTemplateTiles,
|
||||
getAssetPackTiles,
|
||||
getPublicAssetPackTiles,
|
||||
} from './AssetStoreUtils';
|
||||
import { LARGE_WIDGET_SIZE } from '../MainFrame/EditorContainers/HomePage/CardWidget';
|
||||
|
||||
const ASSETS_DISPLAY_LIMIT = 60;
|
||||
|
||||
@@ -142,9 +141,13 @@ export const getAssetShortHeadersToDisplay = (
|
||||
};
|
||||
|
||||
const cellSpacing = 10;
|
||||
const MAX_COLUMNS = getShopItemsColumns('xlarge', true);
|
||||
const MAX_SECTION_WIDTH = (LARGE_WIDGET_SIZE + 2 * 5) * MAX_COLUMNS; // widget size + 5 padding per side
|
||||
const styles = {
|
||||
grid: {
|
||||
margin: '0 2px', // Remove the default margin of the grid but keep the horizontal padding for focus outline.
|
||||
// Avoid tiles taking too much space on large screens.
|
||||
maxWidth: MAX_SECTION_WIDTH,
|
||||
width: `calc(100% + ${cellSpacing}px)`, // This is needed to compensate for the `margin: -5px` added by MUI related to spacing.
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
@@ -296,10 +299,13 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
const {
|
||||
error: gameTemplateStoreError,
|
||||
fetchGameTemplates,
|
||||
privateGameTemplateListingDatas: allPrivateGameTemplateListingDatas,
|
||||
} = React.useContext(PrivateGameTemplateStoreContext);
|
||||
const { error: bundleStoreError, fetchBundles } = React.useContext(
|
||||
BundleStoreContext
|
||||
);
|
||||
const {
|
||||
error: bundleStoreError,
|
||||
fetchBundles,
|
||||
bundleListingDatas: allBundleListingDatas,
|
||||
} = React.useContext(BundleStoreContext);
|
||||
const {
|
||||
receivedAssetPacks,
|
||||
receivedGameTemplates,
|
||||
@@ -511,23 +517,13 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
const publicPacksTiles: Array<React.Node> = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!publicAssetPacks ||
|
||||
!onPublicAssetPackSelection ||
|
||||
// Don't show public packs if filtering on assets.
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
return publicAssetPacks.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
},
|
||||
const publicPackTiles: Array<React.Node> = React.useMemo(
|
||||
() =>
|
||||
getPublicAssetPackTiles({
|
||||
publicAssetPacks,
|
||||
onPublicAssetPackSelection,
|
||||
hasAssetFiltersApplied,
|
||||
}),
|
||||
[publicAssetPacks, onPublicAssetPackSelection, hasAssetFiltersApplied]
|
||||
);
|
||||
|
||||
@@ -535,135 +531,38 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
allAssetPackStandAloneTiles,
|
||||
allAssetPackBundleTiles,
|
||||
} = React.useMemo(
|
||||
() => {
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
if (
|
||||
!privateAssetPackListingDatas ||
|
||||
!receivedAssetPacks ||
|
||||
// Don't show private packs if filtering on assets.
|
||||
hasAssetFiltersApplied
|
||||
) {
|
||||
return {
|
||||
allAssetPackStandAloneTiles: [],
|
||||
allAssetPackBundleTiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
!!onPrivateAssetPackSelection &&
|
||||
privateAssetPackListingDatas.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === assetPackListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const allAssetPackBundleTiles = [
|
||||
...privateOwnedAssetPackBundleTiles, // Display owned bundles first.
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allAssetPackStandAloneTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles, // Display owned packs first.
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
publicPacksTiles,
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allAssetPackStandAloneTiles, allAssetPackBundleTiles };
|
||||
},
|
||||
() =>
|
||||
getAssetPackTiles({
|
||||
allPrivateAssetPackListingDatas,
|
||||
displayedPrivateAssetPackListingDatas: privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicAssetPackTiles: publicPackTiles,
|
||||
receivedAssetPacks,
|
||||
hasAssetFiltersApplied,
|
||||
}),
|
||||
[
|
||||
allPrivateAssetPackListingDatas,
|
||||
privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicPacksTiles,
|
||||
publicPackTiles,
|
||||
receivedAssetPacks,
|
||||
hasAssetFiltersApplied,
|
||||
]
|
||||
);
|
||||
|
||||
const gameTemplateTiles = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!privateGameTemplateListingDatas ||
|
||||
!onPrivateGameTemplateSelection ||
|
||||
// Don't show private game templates if filtering on assets.
|
||||
hasAssetFiltersApplied ||
|
||||
// Don't show private game templates if filtering on asset packs.
|
||||
hasAssetPackFiltersApplied ||
|
||||
onlyShowAssets
|
||||
)
|
||||
return [];
|
||||
|
||||
const notOwnedGameTemplateTiles: Array<React.Node> = [];
|
||||
const ownedGameTemplateTiles: Array<React.Node> = [];
|
||||
|
||||
privateGameTemplateListingDatas.forEach(
|
||||
privateGameTemplateListingData => {
|
||||
const isGameTemplateOwned =
|
||||
!!receivedGameTemplates &&
|
||||
!!receivedGameTemplates.find(
|
||||
pack => pack.id === privateGameTemplateListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateGameTemplateTile
|
||||
privateGameTemplateListingData={privateGameTemplateListingData}
|
||||
onSelect={() => {
|
||||
onPrivateGameTemplateSelection(
|
||||
privateGameTemplateListingData
|
||||
);
|
||||
}}
|
||||
owned={isGameTemplateOwned}
|
||||
key={privateGameTemplateListingData.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isGameTemplateOwned) {
|
||||
ownedGameTemplateTiles.push(tile);
|
||||
} else {
|
||||
notOwnedGameTemplateTiles.push(tile);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return [
|
||||
...ownedGameTemplateTiles, // Display owned game templates first.
|
||||
...notOwnedGameTemplateTiles,
|
||||
];
|
||||
},
|
||||
() =>
|
||||
getGameTemplateTiles({
|
||||
allPrivateGameTemplateListingDatas,
|
||||
displayedPrivateGameTemplateListingDatas: privateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
hasAssetFiltersApplied,
|
||||
hasAssetPackFiltersApplied,
|
||||
onlyShowAssets,
|
||||
}),
|
||||
[
|
||||
allPrivateGameTemplateListingDatas,
|
||||
privateGameTemplateListingDatas,
|
||||
onPrivateGameTemplateSelection,
|
||||
receivedGameTemplates,
|
||||
@@ -674,46 +573,16 @@ const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
);
|
||||
|
||||
const bundleTiles = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!bundleListingDatas ||
|
||||
!onBundleSelection ||
|
||||
// Don't show bundles if filtering on assets.
|
||||
hasAssetFiltersApplied
|
||||
)
|
||||
return [];
|
||||
|
||||
const notOwnedBundleTiles: Array<React.Node> = [];
|
||||
const ownedBundleTiles: Array<React.Node> = [];
|
||||
|
||||
bundleListingDatas.forEach(bundleListingData => {
|
||||
const isBundleOwned =
|
||||
!!receivedBundles &&
|
||||
!!receivedBundles.find(pack => pack.id === bundleListingData.id);
|
||||
const tile = (
|
||||
<BundleTile
|
||||
bundleListingData={bundleListingData}
|
||||
onSelect={() => {
|
||||
onBundleSelection(bundleListingData);
|
||||
}}
|
||||
owned={isBundleOwned}
|
||||
key={bundleListingData.id}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isBundleOwned) {
|
||||
ownedBundleTiles.push(tile);
|
||||
} else {
|
||||
notOwnedBundleTiles.push(tile);
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
...ownedBundleTiles, // Display owned bundles first.
|
||||
...notOwnedBundleTiles,
|
||||
];
|
||||
},
|
||||
() =>
|
||||
getBundleTiles({
|
||||
allBundleListingDatas: allBundleListingDatas,
|
||||
displayedBundleListingDatas: bundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
hasAssetFiltersApplied,
|
||||
}),
|
||||
[
|
||||
allBundleListingDatas,
|
||||
bundleListingDatas,
|
||||
onBundleSelection,
|
||||
receivedBundles,
|
||||
|
@@ -211,6 +211,18 @@ const BundleInformationPage = ({
|
||||
[bundlePurchases, bundleListingData, bundleListingDatas, receivedBundles]
|
||||
);
|
||||
const isAlreadyReceived = !!userBundlePurchaseUsageType;
|
||||
const isOwningAnotherVariant = React.useMemo(
|
||||
() => {
|
||||
if (!bundle || isAlreadyReceived || !receivedBundles) return false;
|
||||
|
||||
// Another bundle older version of that bundle can be owned.
|
||||
// We look at the tag to determine if the bundle is the same.
|
||||
return !!receivedBundles.find(
|
||||
receivedBundle => receivedBundle.tag === bundle.tag
|
||||
);
|
||||
},
|
||||
[bundle, isAlreadyReceived, receivedBundles]
|
||||
);
|
||||
|
||||
const additionalProductThumbnailsIncludedInBundle: string[] = React.useMemo(
|
||||
() => {
|
||||
@@ -357,7 +369,7 @@ const BundleInformationPage = ({
|
||||
|
||||
const onClickBuy = React.useCallback(
|
||||
async () => {
|
||||
if (!bundle) return;
|
||||
if (!bundle || isOwningAnotherVariant) return;
|
||||
if (isAlreadyReceived) {
|
||||
onBundleOpen(bundleListingData);
|
||||
return;
|
||||
@@ -381,7 +393,13 @@ const BundleInformationPage = ({
|
||||
console.warn('Unable to send event', e);
|
||||
}
|
||||
},
|
||||
[bundle, bundleListingData, isAlreadyReceived, onBundleOpen]
|
||||
[
|
||||
bundle,
|
||||
bundleListingData,
|
||||
isAlreadyReceived,
|
||||
isOwningAnotherVariant,
|
||||
onBundleOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const mediaItems = React.useMemo(
|
||||
@@ -506,7 +524,14 @@ const BundleInformationPage = ({
|
||||
</Text>
|
||||
</LineStackLayout>
|
||||
<Spacer />
|
||||
{!isAlreadyReceived ? (
|
||||
{isOwningAnotherVariant ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
You own an older version of this bundle. Browse the
|
||||
store to access it!
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : !isAlreadyReceived ? (
|
||||
<>
|
||||
{!shouldUseOrSimulateAppStoreProduct && (
|
||||
<SecureCheckout />
|
||||
|
@@ -26,7 +26,6 @@ import {
|
||||
type BundleListingData,
|
||||
type CourseListingData,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../Utils/GDevelopServices/Usage';
|
||||
import {
|
||||
getProductsIncludedInBundle,
|
||||
getUserProductPurchaseUsageType,
|
||||
@@ -255,13 +254,9 @@ const getColumnsFromWindowSize = (windowSize: WindowSizeType) => {
|
||||
|
||||
type Props = {|
|
||||
onDisplayBundle: (bundleListingData: BundleListingData) => void,
|
||||
getSubscriptionPlansWithPricingSystems: () => Array<SubscriptionPlanWithPricingSystems> | null,
|
||||
|};
|
||||
|
||||
const BundlePreviewBanner = ({
|
||||
onDisplayBundle,
|
||||
getSubscriptionPlansWithPricingSystems,
|
||||
}: Props) => {
|
||||
const BundlePreviewBanner = ({ onDisplayBundle }: Props) => {
|
||||
const { isMobile, isLandscape, windowSize } = useResponsiveWindowSize();
|
||||
const numberOfTilesToDisplay = getColumnsFromWindowSize(windowSize) - 1; // Reserve one tile for the bundle preview.
|
||||
const { privateGameTemplateListingDatas } = React.useContext(
|
||||
@@ -339,8 +334,6 @@ const BundlePreviewBanner = ({
|
||||
]
|
||||
);
|
||||
|
||||
const subscriptionPlansWithPricingSystems = getSubscriptionPlansWithPricingSystems();
|
||||
|
||||
const redemptionCodesIncludedInBundle = React.useMemo(
|
||||
() =>
|
||||
bundleListingData
|
||||
@@ -387,7 +380,7 @@ const BundlePreviewBanner = ({
|
||||
// $FlowFixMe
|
||||
coursesIncludedInBundle[index];
|
||||
if (!courseListingData) {
|
||||
return <div style={{ flex: 1 }} />;
|
||||
return <div style={{ flex: 1 }} key={`empty-tile-${index}`} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -496,7 +489,6 @@ const BundlePreviewBanner = ({
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
subscriptionPlansWithPricingSystems,
|
||||
})}
|
||||
</span>
|
||||
</Text>
|
||||
|
@@ -8,14 +8,12 @@ import {
|
||||
type CreditsPackageListingData,
|
||||
type IncludedRedemptionCode,
|
||||
} from '../../Utils/GDevelopServices/Shop';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../Utils/GDevelopServices/Usage';
|
||||
|
||||
export const renderEstimatedTotalPriceFormatted = ({
|
||||
i18n,
|
||||
bundleListingData,
|
||||
productListingDatasIncludedInBundle,
|
||||
redemptionCodesIncludedInBundle,
|
||||
subscriptionPlansWithPricingSystems,
|
||||
}: {
|
||||
i18n: I18nType,
|
||||
bundleListingData: ?BundleListingData,
|
||||
@@ -27,14 +25,12 @@ export const renderEstimatedTotalPriceFormatted = ({
|
||||
| CreditsPackageListingData
|
||||
>,
|
||||
redemptionCodesIncludedInBundle: ?Array<IncludedRedemptionCode>,
|
||||
subscriptionPlansWithPricingSystems: ?Array<SubscriptionPlanWithPricingSystems>,
|
||||
}): ?string => {
|
||||
let totalPrice = 0;
|
||||
if (
|
||||
!bundleListingData ||
|
||||
!productListingDatasIncludedInBundle ||
|
||||
!redemptionCodesIncludedInBundle ||
|
||||
!subscriptionPlansWithPricingSystems
|
||||
!redemptionCodesIncludedInBundle
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -106,19 +102,33 @@ export const renderEstimatedTotalPriceFormatted = ({
|
||||
for (const redemptionCode of redemptionCodesIncludedInBundle) {
|
||||
const planId = redemptionCode.givenSubscriptionPlanId;
|
||||
if (planId) {
|
||||
const subscriptionPlanWithPricingSystems = subscriptionPlansWithPricingSystems.find(
|
||||
plan => plan.id === planId
|
||||
);
|
||||
if (subscriptionPlanWithPricingSystems) {
|
||||
const monthlyPricingSystem = subscriptionPlanWithPricingSystems.pricingSystems.find(
|
||||
pricingSystem => pricingSystem.period === 'month'
|
||||
let estimatedAmountInCents = null;
|
||||
if (redemptionCode.estimatedPrices) {
|
||||
const estimatedPrice = redemptionCode.estimatedPrices.find(
|
||||
price => price.currency === currencyCode
|
||||
);
|
||||
if (monthlyPricingSystem) {
|
||||
totalPrice +=
|
||||
monthlyPricingSystem.amountInCents *
|
||||
Math.round(redemptionCode.durationInDays / 30);
|
||||
if (estimatedPrice) {
|
||||
estimatedAmountInCents = estimatedPrice.value;
|
||||
}
|
||||
}
|
||||
|
||||
// If no estimated price is provided, guess a mostly correct value
|
||||
// for backward compatibility.
|
||||
if (estimatedAmountInCents === null) {
|
||||
const monthlyEstimatedAmountInCents =
|
||||
planId === 'gdevelop_silver'
|
||||
? 599
|
||||
: planId === 'gdevelop_gold'
|
||||
? 1099
|
||||
: planId === 'gdevelop_startup'
|
||||
? 3499
|
||||
: 0;
|
||||
estimatedAmountInCents =
|
||||
monthlyEstimatedAmountInCents *
|
||||
Math.max(1, Math.round(redemptionCode.durationInDays / 30));
|
||||
}
|
||||
|
||||
totalPrice += estimatedAmountInCents || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -202,7 +202,9 @@ const PrivateAssetPackInformationPage = ({
|
||||
CreditsPackageStoreContext
|
||||
);
|
||||
const [selectedUsageType, setSelectedUsageType] = React.useState<string>(
|
||||
privateAssetPackListingData.prices[0].usageType
|
||||
privateAssetPackListingData.prices.length
|
||||
? privateAssetPackListingData.prices[0].usageType
|
||||
: ''
|
||||
);
|
||||
const [
|
||||
purchasingPrivateAssetPackListingData,
|
||||
@@ -235,9 +237,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
const userAssetPackPurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: privateAssetPackListingData
|
||||
? privateAssetPackListingData.id
|
||||
: null,
|
||||
productId: privateAssetPackListingData.id,
|
||||
receivedProducts: [
|
||||
...(receivedAssetPacks || []),
|
||||
...(receivedBundles || []),
|
||||
|
@@ -170,7 +170,9 @@ const PrivateGameTemplateInformationPage = ({
|
||||
null
|
||||
);
|
||||
const [selectedUsageType, setSelectedUsageType] = React.useState<string>(
|
||||
privateGameTemplateListingData.prices[0].usageType
|
||||
privateGameTemplateListingData.prices.length
|
||||
? privateGameTemplateListingData.prices[0].usageType
|
||||
: ''
|
||||
);
|
||||
const [
|
||||
purchasingPrivateGameTemplateListingData,
|
||||
@@ -202,9 +204,7 @@ const PrivateGameTemplateInformationPage = ({
|
||||
const userGameTemplatePurchaseUsageType = React.useMemo(
|
||||
() =>
|
||||
getUserProductPurchaseUsageType({
|
||||
productId: privateGameTemplateListingData
|
||||
? privateGameTemplateListingData.id
|
||||
: null,
|
||||
productId: privateGameTemplateListingData.id,
|
||||
receivedProducts: [
|
||||
...(receivedGameTemplates || []),
|
||||
...(receivedBundles || []),
|
||||
|
@@ -3,8 +3,10 @@ import { unserializeFromJSObject } from '../Utils/Serializer';
|
||||
import {
|
||||
type AiGeneratedEventChange,
|
||||
type AiGeneratedEventUndeclaredVariable,
|
||||
type AiGeneratedEventMissingObjectBehavior,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import { mapFor } from '../Utils/MapFor';
|
||||
import { isBehaviorDefaultCapability } from '../BehaviorsEditor/EnumerateBehaviorsMetadata';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -520,8 +522,7 @@ export const addObjectUndeclaredVariables = ({
|
||||
.getVariables()
|
||||
.insertNew(undeclaredVariable.name, 0);
|
||||
setupVariable(variable, undeclaredVariable.type);
|
||||
}
|
||||
if (
|
||||
} else if (
|
||||
project
|
||||
.getObjects()
|
||||
.getObjectGroups()
|
||||
@@ -536,3 +537,129 @@ export const addObjectUndeclaredVariables = ({
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addMissingObjectBehaviors = ({
|
||||
project,
|
||||
scene,
|
||||
objectName,
|
||||
missingBehaviors,
|
||||
}: {|
|
||||
project: gdProject,
|
||||
scene: gdLayout,
|
||||
objectName: string,
|
||||
missingBehaviors: Array<AiGeneratedEventMissingObjectBehavior>,
|
||||
|}) => {
|
||||
const projectScopedContainers = gd.ProjectScopedContainers.makeNewProjectScopedContainersForProjectAndLayout(
|
||||
project,
|
||||
scene
|
||||
);
|
||||
|
||||
const objectOrGroupBehaviorNames = projectScopedContainers
|
||||
.getObjectsContainersList()
|
||||
.getBehaviorsOfObject(objectName, true)
|
||||
.toJSArray();
|
||||
|
||||
const addBehaviorToObject = (
|
||||
object: gdObject,
|
||||
behaviorName: string,
|
||||
behaviorType: string
|
||||
) => {
|
||||
if (object.hasBehaviorNamed(behaviorName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
gd.WholeProjectRefactorer.addBehaviorAndRequiredBehaviors(
|
||||
project,
|
||||
object,
|
||||
behaviorType,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
const addBehaviorToObjectGroup = (
|
||||
group: gdObjectGroup,
|
||||
behaviorName: string,
|
||||
behaviorType: string
|
||||
) => {
|
||||
const objectNames = group.getAllObjectsNames().toJSArray();
|
||||
objectNames.forEach(objectName => {
|
||||
if (scene.getObjects().hasObjectNamed(objectName)) {
|
||||
const object = scene.getObjects().getObject(objectName);
|
||||
addBehaviorToObject(object, behaviorName, behaviorType);
|
||||
} else if (project.getObjects().hasObjectNamed(objectName)) {
|
||||
const object = project.getObjects().getObject(objectName);
|
||||
addBehaviorToObject(object, behaviorName, behaviorType);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
missingBehaviors.forEach(missingBehavior => {
|
||||
if (objectOrGroupBehaviorNames.includes(missingBehavior.name)) {
|
||||
// This behavior is already present, no need to add it.
|
||||
return;
|
||||
}
|
||||
|
||||
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
missingBehavior.type
|
||||
);
|
||||
|
||||
if (gd.MetadataProvider.isBadBehaviorMetadata(behaviorMetadata)) {
|
||||
console.warn(
|
||||
`Unknown behavior type: "${missingBehavior.type}". Skipping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBehaviorDefaultCapability(behaviorMetadata)) {
|
||||
console.warn(
|
||||
`Behavior "${missingBehavior.name}" of type "${
|
||||
missingBehavior.type
|
||||
}" is a default capability and cannot be added to object "${objectName}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scene.getObjects().hasObjectNamed(objectName)) {
|
||||
const object = scene.getObjects().getObject(objectName);
|
||||
addBehaviorToObject(object, missingBehavior.name, missingBehavior.type);
|
||||
} else if (
|
||||
scene
|
||||
.getObjects()
|
||||
.getObjectGroups()
|
||||
.has(objectName)
|
||||
) {
|
||||
const group = scene
|
||||
.getObjects()
|
||||
.getObjectGroups()
|
||||
.get(objectName);
|
||||
|
||||
addBehaviorToObjectGroup(
|
||||
group,
|
||||
missingBehavior.name,
|
||||
missingBehavior.type
|
||||
);
|
||||
} else if (project.getObjects().hasObjectNamed(objectName)) {
|
||||
const object = project.getObjects().getObject(objectName);
|
||||
addBehaviorToObject(object, missingBehavior.name, missingBehavior.type);
|
||||
} else if (
|
||||
project
|
||||
.getObjects()
|
||||
.getObjectGroups()
|
||||
.has(objectName)
|
||||
) {
|
||||
const group = project
|
||||
.getObjects()
|
||||
.getObjectGroups()
|
||||
.get(objectName);
|
||||
|
||||
addBehaviorToObjectGroup(
|
||||
group,
|
||||
missingBehavior.name,
|
||||
missingBehavior.type
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
scene.updateBehaviorsSharedData(project);
|
||||
};
|
||||
|
@@ -53,6 +53,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -95,6 +96,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -131,6 +133,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -165,6 +168,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -200,6 +204,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -234,6 +239,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -272,6 +278,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
{
|
||||
operationName: 'insert_before_event',
|
||||
@@ -283,6 +290,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
@@ -321,6 +329,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -356,6 +365,7 @@ describe('applyEventsChanges', () => {
|
||||
extensionNames: [],
|
||||
undeclaredVariables: [],
|
||||
undeclaredObjectVariables: {},
|
||||
missingObjectBehaviors: {},
|
||||
},
|
||||
];
|
||||
applyEventsChanges(
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
type AssetSearchAndInstallOptions,
|
||||
type AssetSearchAndInstallResult,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from '.';
|
||||
|
||||
export type EditorFunctionCallResult =
|
||||
@@ -39,6 +40,9 @@ export type ProcessEditorFunctionCallsOptions = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
ensureExtensionInstalled: (options: {|
|
||||
extensionName: string,
|
||||
|}) => Promise<void>,
|
||||
@@ -53,6 +57,7 @@ export const processEditorFunctionCalls = async ({
|
||||
editorCallbacks,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ignore,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
@@ -136,6 +141,7 @@ export const processEditorFunctionCalls = async ({
|
||||
args,
|
||||
generateEvents,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
ensureExtensionInstalled,
|
||||
searchAndInstallAsset,
|
||||
}
|
||||
|
@@ -32,6 +32,12 @@ export type ExpressionSummary = {|
|
||||
relevantForSceneEvents?: boolean,
|
||||
|};
|
||||
|
||||
export type PropertySummary = {|
|
||||
name: string,
|
||||
description: string,
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export type ObjectSummary = {|
|
||||
name: string,
|
||||
fullName: string,
|
||||
@@ -51,6 +57,17 @@ export type BehaviorSummary = {|
|
||||
expressions: Array<ExpressionSummary>,
|
||||
|};
|
||||
|
||||
export type EffectSummary = {|
|
||||
name: string,
|
||||
fullName: string,
|
||||
description: string,
|
||||
notWorkingForObjects: boolean,
|
||||
onlyWorkingFor2D: boolean,
|
||||
onlyWorkingFor3D: boolean,
|
||||
unique: boolean,
|
||||
properties: Array<PropertySummary>,
|
||||
|};
|
||||
|
||||
export type ExtensionSummary = {|
|
||||
extensionName: string,
|
||||
extensionFullName: string,
|
||||
@@ -60,6 +77,7 @@ export type ExtensionSummary = {|
|
||||
freeExpressions: Array<ExpressionSummary>,
|
||||
objects: { [string]: ObjectSummary },
|
||||
behaviors: { [string]: BehaviorSummary },
|
||||
effects: { [string]: EffectSummary },
|
||||
|};
|
||||
|
||||
const normalizeType = (parameterType: string) => {
|
||||
@@ -102,6 +120,29 @@ const getParameterSummary = (
|
||||
return parameterSummary;
|
||||
};
|
||||
|
||||
const getPropertySummary = (
|
||||
propertyName: string,
|
||||
property: gdPropertyDescriptor
|
||||
) => {
|
||||
return {
|
||||
name: propertyName,
|
||||
description: property.getDescription(),
|
||||
type: property.getType(),
|
||||
};
|
||||
};
|
||||
|
||||
const getPropertiesSummary = (
|
||||
propertiesMetadata: gdMapStringPropertyDescriptor
|
||||
) => {
|
||||
return propertiesMetadata
|
||||
.keys()
|
||||
.toJSArray()
|
||||
.map(propertyName => {
|
||||
const property = propertiesMetadata.get(propertyName);
|
||||
return getPropertySummary(propertyName, property);
|
||||
});
|
||||
};
|
||||
|
||||
export const buildExtensionSummary = ({
|
||||
gd,
|
||||
extension,
|
||||
@@ -111,6 +152,7 @@ export const buildExtensionSummary = ({
|
||||
}): ExtensionSummary => {
|
||||
const objects: { [string]: ObjectSummary } = {};
|
||||
const behaviors: { [string]: BehaviorSummary } = {};
|
||||
const effects: { [string]: EffectSummary } = {};
|
||||
|
||||
const generateInstructionsSummaries = ({
|
||||
instructionsMetadata,
|
||||
@@ -254,6 +296,27 @@ export const buildExtensionSummary = ({
|
||||
|
||||
behaviors[behaviorType] = behaviorSummary;
|
||||
});
|
||||
extension
|
||||
.getExtensionEffectTypes()
|
||||
.toJSArray()
|
||||
.forEach(effectType => {
|
||||
const effectMetadata = extension.getEffectMetadata(effectType);
|
||||
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
|
||||
return;
|
||||
}
|
||||
const effectSummary: EffectSummary = {
|
||||
name: effectMetadata.getType(),
|
||||
fullName: effectMetadata.getFullName(),
|
||||
description: effectMetadata.getDescription(),
|
||||
notWorkingForObjects: effectMetadata.isMarkedAsNotWorkingForObjects(),
|
||||
onlyWorkingFor2D: effectMetadata.isMarkedAsOnlyWorkingFor2D(),
|
||||
onlyWorkingFor3D: effectMetadata.isMarkedAsOnlyWorkingFor3D(),
|
||||
unique: effectMetadata.isMarkedAsUnique(),
|
||||
properties: getPropertiesSummary(effectMetadata.getProperties()),
|
||||
};
|
||||
|
||||
effects[effectType] = effectSummary;
|
||||
});
|
||||
|
||||
return {
|
||||
extensionName: extension.getName(),
|
||||
@@ -275,5 +338,6 @@ export const buildExtensionSummary = ({
|
||||
],
|
||||
objects,
|
||||
behaviors,
|
||||
effects,
|
||||
};
|
||||
};
|
||||
|
@@ -43,6 +43,10 @@ type SimplifiedScene = {|
|
||||
|};
|
||||
|
||||
type SimplifiedProject = {|
|
||||
properties: {|
|
||||
gameResolutionWidth: number,
|
||||
gameResolutionHeight: number,
|
||||
|},
|
||||
globalObjects: Array<SimplifiedObject>,
|
||||
globalObjectGroups: Array<SimplifiedObjectGroup>,
|
||||
scenes: Array<SimplifiedScene>,
|
||||
@@ -328,6 +332,10 @@ export const makeSimplifiedProjectBuilder = (gd: libGDevelop) => {
|
||||
// Filter extensions to only include extensions from the project.
|
||||
|
||||
const simplifiedProject: SimplifiedProject = {
|
||||
properties: {
|
||||
gameResolutionWidth: project.getGameResolutionWidth(),
|
||||
gameResolutionHeight: project.getGameResolutionHeight(),
|
||||
},
|
||||
globalObjects,
|
||||
globalObjectGroups: getSimplifiedObjectGroups(
|
||||
project.getObjects().getObjectGroups(),
|
||||
|
@@ -58,6 +58,10 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
],
|
||||
"globalVariables": Array [],
|
||||
"properties": Object {
|
||||
"gameResolutionHeight": 600,
|
||||
"gameResolutionWidth": 800,
|
||||
},
|
||||
"scenes": Array [
|
||||
Object {
|
||||
"instancesOnSceneDescription": "On the scene, there are:
|
||||
@@ -890,6 +894,7 @@ describe('SimplifiedProject', () => {
|
||||
},
|
||||
},
|
||||
"description": "A fake extension with a fake behavior containing 2 properties.",
|
||||
"effects": Object {},
|
||||
"extensionFullName": "Fake extension with a fake behavior",
|
||||
"extensionName": "FakeBehavior",
|
||||
"freeActions": Array [],
|
||||
|
@@ -7,6 +7,7 @@ import { serializeToJSObject } from '../Utils/Serializer';
|
||||
import { type AiGeneratedEvent } from '../Utils/GDevelopServices/Generation';
|
||||
import { renderNonTranslatedEventsAsText } from '../EventsSheet/EventsTree/TextRenderer';
|
||||
import {
|
||||
addMissingObjectBehaviors,
|
||||
addObjectUndeclaredVariables,
|
||||
addUndeclaredVariables,
|
||||
applyEventsChanges,
|
||||
@@ -16,12 +17,17 @@ import { Trans } from '@lingui/macro';
|
||||
import Link from '../UI/Link';
|
||||
import {
|
||||
hexNumberToRGBArray,
|
||||
rgbColorToHex,
|
||||
rgbOrHexToHexNumber,
|
||||
} from '../Utils/ColorTransformer';
|
||||
import { type SimplifiedBehavior } from './SimplifiedProject/SimplifiedProject';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import { applyVariableChange } from './ApplyVariableChange';
|
||||
import {
|
||||
addDefaultLightToAllLayers,
|
||||
addDefaultLightToLayer,
|
||||
} from '../ProjectCreation/CreateProject';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -58,10 +64,13 @@ export type EditorFunctionGenericOutput = {|
|
||||
properties?: any,
|
||||
sharedProperties?: any,
|
||||
instances?: any,
|
||||
layers?: any,
|
||||
behaviors?: Array<SimplifiedBehavior>,
|
||||
animationNames?: string,
|
||||
generatedEventsErrorDiagnostics?: string,
|
||||
aiGeneratedEventId?: string,
|
||||
propertiesLayersEffectsForSceneNamed?: string,
|
||||
warnings?: string,
|
||||
|};
|
||||
|
||||
export type EventsGenerationResult =
|
||||
@@ -118,6 +127,10 @@ export type SceneEventsOutsideEditorChanges = {|
|
||||
newOrChangedAiGeneratedEventIds: Set<string>,
|
||||
|};
|
||||
|
||||
export type InstancesOutsideEditorChanges = {|
|
||||
scene: gdLayout,
|
||||
|};
|
||||
|
||||
/**
|
||||
* A function that does something in the editor on the given project.
|
||||
*/
|
||||
@@ -141,6 +154,9 @@ export type EditorFunction = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
ensureExtensionInstalled: (options: {|
|
||||
extensionName: string,
|
||||
|}) => Promise<void>,
|
||||
@@ -1488,7 +1504,6 @@ const put2dInstances: EditorFunction = {
|
||||
// Create the array of existing instances to move/modify, and new instances to create.
|
||||
const modifiedAndCreatedInstances: Array<gdInitialInstance> = [];
|
||||
iterateOnInstances(initialInstances, instance => {
|
||||
if (instance.getLayer() !== layer_name) return;
|
||||
if (instance.getObjectName() !== object_name) return;
|
||||
if (
|
||||
existingInstanceIds.some(id =>
|
||||
@@ -1496,6 +1511,8 @@ const put2dInstances: EditorFunction = {
|
||||
)
|
||||
) {
|
||||
modifiedAndCreatedInstances.push(instance);
|
||||
// Take the opportunity to move to a new layer if specified.
|
||||
instance.setLayer(layer_name);
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < newInstancesCount; i++) {
|
||||
@@ -1841,7 +1858,6 @@ const put3dInstances: EditorFunction = {
|
||||
// Create the array of existing instances to move/modify, and new instances to create.
|
||||
const modifiedAndCreatedInstances: Array<gdInitialInstance> = [];
|
||||
iterateOnInstances(initialInstances, instance => {
|
||||
if (instance.getLayer() !== layer_name) return;
|
||||
if (instance.getObjectName() !== object_name) return;
|
||||
if (
|
||||
existingInstanceIds.some(id =>
|
||||
@@ -1849,6 +1865,8 @@ const put3dInstances: EditorFunction = {
|
||||
)
|
||||
) {
|
||||
modifiedAndCreatedInstances.push(instance);
|
||||
// Take the opportunity to move to a new layer if specified.
|
||||
instance.setLayer(layer_name);
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < newInstancesCount; i++) {
|
||||
@@ -2019,7 +2037,12 @@ const addSceneEvents: EditorFunction = {
|
||||
const details = shouldShowDetails ? (
|
||||
<ColumnStackLayout noMargin>
|
||||
{eventsDescription && (
|
||||
<Text noMargin allowSelection color="secondary">
|
||||
<Text
|
||||
noMargin
|
||||
allowSelection
|
||||
color="secondary"
|
||||
style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}
|
||||
>
|
||||
<b>
|
||||
<Trans>Description</Trans>
|
||||
</b>
|
||||
@@ -2027,7 +2050,12 @@ const addSceneEvents: EditorFunction = {
|
||||
</Text>
|
||||
)}
|
||||
{placementHint && (
|
||||
<Text noMargin allowSelection color="secondary">
|
||||
<Text
|
||||
noMargin
|
||||
allowSelection
|
||||
color="secondary"
|
||||
style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}
|
||||
>
|
||||
<b>
|
||||
<Trans>Generation hint</Trans>
|
||||
</b>
|
||||
@@ -2035,7 +2063,12 @@ const addSceneEvents: EditorFunction = {
|
||||
</Text>
|
||||
)}
|
||||
{objectsList && (
|
||||
<Text noMargin allowSelection color="secondary">
|
||||
<Text
|
||||
noMargin
|
||||
allowSelection
|
||||
color="secondary"
|
||||
style={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}
|
||||
>
|
||||
<b>
|
||||
<Trans>Related objects</Trans>
|
||||
</b>
|
||||
@@ -2243,8 +2276,10 @@ const addSceneEvents: EditorFunction = {
|
||||
undeclaredVariables: change.undeclaredVariables,
|
||||
});
|
||||
|
||||
const objectNames = Object.keys(change.undeclaredObjectVariables);
|
||||
for (const objectName of objectNames) {
|
||||
const objectNamesWithUndeclaredVariables = Object.keys(
|
||||
change.undeclaredObjectVariables
|
||||
);
|
||||
for (const objectName of objectNamesWithUndeclaredVariables) {
|
||||
const undeclaredVariables =
|
||||
change.undeclaredObjectVariables[objectName];
|
||||
addObjectUndeclaredVariables({
|
||||
@@ -2254,6 +2289,19 @@ const addSceneEvents: EditorFunction = {
|
||||
undeclaredVariables,
|
||||
});
|
||||
}
|
||||
|
||||
const objectNamesWithMissingBehavior = Object.keys(
|
||||
change.missingObjectBehaviors
|
||||
);
|
||||
for (const objectName of objectNamesWithMissingBehavior) {
|
||||
const missingBehaviors = change.missingObjectBehaviors[objectName];
|
||||
addMissingObjectBehaviors({
|
||||
project,
|
||||
scene,
|
||||
objectName,
|
||||
missingBehaviors,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyEventsChanges(
|
||||
@@ -2344,7 +2392,8 @@ const createScene: EditorFunction = {
|
||||
if (project.hasLayoutNamed(scene_name)) {
|
||||
const scene = project.getLayout(scene_name);
|
||||
if (include_ui_layer && !scene.hasLayerNamed('UI')) {
|
||||
scene.insertNewLayer('UI', 1);
|
||||
scene.insertNewLayer('UI', scene.getLayersCount());
|
||||
addDefaultLightToLayer(scene.getLayer('UI'));
|
||||
return makeGenericSuccess(
|
||||
`Scene with name "${scene_name}" already exists, no need to re-create it. A layer called "UI" was added to it.`
|
||||
);
|
||||
@@ -2358,7 +2407,7 @@ const createScene: EditorFunction = {
|
||||
const scenesCount = project.getLayoutsCount();
|
||||
const scene = project.insertNewLayout(scene_name, scenesCount);
|
||||
if (include_ui_layer) {
|
||||
scene.insertNewLayer('UI', 0);
|
||||
scene.insertNewLayer('UI', scene.getLayersCount());
|
||||
}
|
||||
if (background_color) {
|
||||
const colorAsRgb = hexNumberToRGBArray(
|
||||
@@ -2366,6 +2415,7 @@ const createScene: EditorFunction = {
|
||||
);
|
||||
scene.setBackgroundColor(colorAsRgb[0], colorAsRgb[1], colorAsRgb[2]);
|
||||
}
|
||||
addDefaultLightToAllLayers(scene);
|
||||
|
||||
return makeGenericSuccess(
|
||||
include_ui_layer
|
||||
@@ -2401,6 +2451,581 @@ const deleteScene: EditorFunction = {
|
||||
},
|
||||
};
|
||||
|
||||
const serializeEffectProperties = (
|
||||
effect: gdEffect,
|
||||
effectMetadata: gdEffectMetadata
|
||||
) => {
|
||||
const effectProperties = effectMetadata.getProperties();
|
||||
const propertyNames = effectProperties.keys().toJSArray();
|
||||
return propertyNames
|
||||
.map(name => {
|
||||
const propertyDescriptor = effectProperties.get(name);
|
||||
if (shouldHideProperty(propertyDescriptor)) return null;
|
||||
|
||||
// Set the value of the property to what is stored in the effect.
|
||||
// If it's not set, none of these will be set and the "value" will be the default one
|
||||
// serialized by the property descriptor.
|
||||
let value = null;
|
||||
if (effect.hasDoubleParameter(name)) {
|
||||
value = effect.getDoubleParameter(name);
|
||||
} else if (effect.hasStringParameter(name)) {
|
||||
value = effect.getStringParameter(name);
|
||||
} else if (effect.hasBooleanParameter(name)) {
|
||||
value = effect.getBooleanParameter(name);
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return serializeNamedProperty(name, propertyDescriptor);
|
||||
}
|
||||
|
||||
return {
|
||||
...serializeNamedProperty(name, propertyDescriptor),
|
||||
value,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const inspectScenePropertiesLayersEffects: EditorFunction = {
|
||||
renderForEditor: ({ args }) => {
|
||||
const scene_name = extractRequiredString(args, 'scene_name');
|
||||
|
||||
return {
|
||||
text: (
|
||||
<Trans>
|
||||
Inspecting scene properties, layers and effects for scene {scene_name}
|
||||
.
|
||||
</Trans>
|
||||
),
|
||||
};
|
||||
},
|
||||
launchFunction: async ({ project, args }) => {
|
||||
const scene_name = extractRequiredString(args, 'scene_name');
|
||||
|
||||
if (!project.hasLayoutNamed(scene_name)) {
|
||||
return makeGenericFailure(`Scene not found: "${scene_name}".`);
|
||||
}
|
||||
|
||||
const scene = project.getLayout(scene_name);
|
||||
const layersContainer = scene.getLayers();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
propertiesLayersEffectsForSceneNamed: scene.getName(),
|
||||
properties: {
|
||||
backgroundColor: rgbColorToHex(
|
||||
scene.getBackgroundColorRed(),
|
||||
scene.getBackgroundColorGreen(),
|
||||
scene.getBackgroundColorBlue()
|
||||
),
|
||||
stopSoundsOnStartup: scene.stopSoundsOnStartup(),
|
||||
|
||||
// Also include some project related properties:
|
||||
gameResolutionWidth: project.getGameResolutionWidth(),
|
||||
gameResolutionHeight: project.getGameResolutionHeight(),
|
||||
gameOrientation: project.getOrientation(),
|
||||
gameScaleMode: project.getScaleMode(),
|
||||
gameName: project.getName(),
|
||||
},
|
||||
layers: mapFor(0, layersContainer.getLayersCount(), i => {
|
||||
const layer = layersContainer.getLayerAt(i);
|
||||
const effectsContainer = layer.getEffects();
|
||||
return {
|
||||
name: layer.getName(),
|
||||
position: i,
|
||||
effects: mapFor(0, effectsContainer.getEffectsCount(), j => {
|
||||
const effect = effectsContainer.getEffectAt(j);
|
||||
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
effect.getEffectType()
|
||||
);
|
||||
|
||||
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
effectName: effect.getName(),
|
||||
effectType: effect.getEffectType(),
|
||||
effectProperties: serializeEffectProperties(
|
||||
effect,
|
||||
effectMetadata
|
||||
),
|
||||
};
|
||||
}).filter(Boolean),
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const isFuzzyMatch = (string1: string, string2: string) => {
|
||||
const simplifiedString1 = string1.toLowerCase().replace(/\s|_|-/g, '');
|
||||
const simplifiedString2 = string2.toLowerCase().replace(/\s|_|-/g, '');
|
||||
|
||||
return simplifiedString1 === simplifiedString2;
|
||||
};
|
||||
|
||||
const changeScenePropertiesLayersEffects: EditorFunction = {
|
||||
renderForEditor: ({ args, shouldShowDetails }) => {
|
||||
const scene_name = extractRequiredString(args, 'scene_name');
|
||||
|
||||
const changed_properties = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_properties'
|
||||
);
|
||||
const changed_layers = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_layers'
|
||||
);
|
||||
const changed_layer_effects = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_layer_effects'
|
||||
);
|
||||
|
||||
const changedPropertiesCount =
|
||||
(changed_properties && changed_properties.length) || 0;
|
||||
const changedLayersCount = (changed_layers && changed_layers.length) || 0;
|
||||
const changedLayerEffectsCount =
|
||||
(changed_layer_effects && changed_layer_effects.length) || 0;
|
||||
|
||||
return {
|
||||
text:
|
||||
changedPropertiesCount > 0 &&
|
||||
changedLayersCount > 0 &&
|
||||
changedLayerEffectsCount > 0 ? (
|
||||
<Trans>
|
||||
Changing some scene properties, layers and effects for scene{' '}
|
||||
{scene_name}.
|
||||
</Trans>
|
||||
) : changedPropertiesCount > 0 && changedLayersCount > 0 ? (
|
||||
<Trans>
|
||||
Changing some scene properties and layers for scene {scene_name}.
|
||||
</Trans>
|
||||
) : changedPropertiesCount > 0 && changedLayerEffectsCount > 0 ? (
|
||||
<Trans>
|
||||
Changing some scene properties and effects for scene {scene_name}.
|
||||
</Trans>
|
||||
) : changedLayerEffectsCount > 0 && changedLayersCount > 0 ? (
|
||||
<Trans>
|
||||
Changing some scene effects and layers for scene {scene_name}.
|
||||
</Trans>
|
||||
) : changedPropertiesCount > 0 ? (
|
||||
<Trans>Changing some scene properties for scene {scene_name}.</Trans>
|
||||
) : changedLayersCount > 0 ? (
|
||||
<Trans>Changing some scene layers for scene {scene_name}.</Trans>
|
||||
) : changedLayerEffectsCount > 0 ? (
|
||||
<Trans>Changing some scene effects for scene {scene_name}.</Trans>
|
||||
) : (
|
||||
<Trans>Unknown changes attempted for scene {scene_name}.</Trans>
|
||||
),
|
||||
};
|
||||
},
|
||||
launchFunction: async ({
|
||||
project,
|
||||
args,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
}) => {
|
||||
const scene_name = extractRequiredString(args, 'scene_name');
|
||||
|
||||
if (!project.hasLayoutNamed(scene_name)) {
|
||||
return makeGenericFailure(`Scene not found: "${scene_name}".`);
|
||||
}
|
||||
const scene = project.getLayout(scene_name);
|
||||
|
||||
const changes = [];
|
||||
const warnings = [];
|
||||
|
||||
const changed_properties = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_properties'
|
||||
);
|
||||
const changed_layers = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_layers'
|
||||
);
|
||||
const changed_layer_effects = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_layer_effects'
|
||||
);
|
||||
|
||||
if (changed_properties)
|
||||
changed_properties.forEach(changed_property => {
|
||||
const propertyName = SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'property_name'
|
||||
);
|
||||
const newValue = SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'new_value'
|
||||
);
|
||||
if (propertyName === null || newValue === null) {
|
||||
warnings.push(
|
||||
`Missing "property_name" or "new_value" in the changed_property object: ${JSON.stringify(
|
||||
changed_property
|
||||
)}. It was ignored and not changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFuzzyMatch(propertyName, 'backgroundColor')) {
|
||||
const colorAsRgb = hexNumberToRGBArray(rgbOrHexToHexNumber(newValue));
|
||||
scene.setBackgroundColor(colorAsRgb[0], colorAsRgb[1], colorAsRgb[2]);
|
||||
changes.push('Modified the scene background color.');
|
||||
} else if (isFuzzyMatch(propertyName, 'gameResolutionWidth')) {
|
||||
project.setGameResolutionSize(
|
||||
parseInt(newValue),
|
||||
project.getGameResolutionHeight()
|
||||
);
|
||||
changes.push('Modified the game resolution width.');
|
||||
} else if (isFuzzyMatch(propertyName, 'stopSoundsOnStartup')) {
|
||||
scene.setStopSoundsOnStartup(newValue.toLowerCase() === 'true');
|
||||
changes.push(
|
||||
'Modified whether sounds should be stopped on scene startup.'
|
||||
);
|
||||
} else if (isFuzzyMatch(propertyName, 'gameResolutionHeight')) {
|
||||
project.setGameResolutionSize(
|
||||
project.getGameResolutionWidth(),
|
||||
parseInt(newValue)
|
||||
);
|
||||
changes.push('Modified the game resolution height.');
|
||||
} else if (isFuzzyMatch(propertyName, 'gameOrientation')) {
|
||||
project.setOrientation(newValue);
|
||||
changes.push('Modified the game orientation.');
|
||||
} else if (isFuzzyMatch(propertyName, 'gameScaleMode')) {
|
||||
project.setScaleMode(newValue);
|
||||
changes.push('Modified the game scale mode.');
|
||||
} else if (isFuzzyMatch(propertyName, 'gameName')) {
|
||||
project.setName(newValue);
|
||||
changes.push('Modified the game name.');
|
||||
} else {
|
||||
warnings.push(
|
||||
`Unknown property for the scene: "${propertyName}". It was ignored and not changed.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (changed_layers) {
|
||||
changed_layers.forEach(changed_layer => {
|
||||
const layerName = SafeExtractor.extractStringProperty(
|
||||
changed_layer,
|
||||
'layer_name'
|
||||
);
|
||||
if (layerName === null) {
|
||||
warnings.push(
|
||||
`Missing "layer_name" in an item of changed_layers. It was ignored and not changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const new_layer_name = SafeExtractor.extractStringProperty(
|
||||
changed_layer,
|
||||
'new_layer_name'
|
||||
);
|
||||
const new_layer_position = SafeExtractor.extractNumberProperty(
|
||||
changed_layer,
|
||||
'new_layer_position'
|
||||
);
|
||||
const delete_this_layer = SafeExtractor.extractBooleanProperty(
|
||||
changed_layer,
|
||||
'delete_this_layer'
|
||||
);
|
||||
const move_instances_to_layer = SafeExtractor.extractStringProperty(
|
||||
changed_layer,
|
||||
'move_instances_to_layer'
|
||||
);
|
||||
|
||||
if (scene.hasLayerNamed(layerName)) {
|
||||
if (delete_this_layer) {
|
||||
if (move_instances_to_layer) {
|
||||
gd.WholeProjectRefactorer.mergeLayersInScene(
|
||||
project,
|
||||
scene,
|
||||
layerName,
|
||||
move_instances_to_layer
|
||||
);
|
||||
} else {
|
||||
// Note: some instances will be invalidated because of this.
|
||||
gd.WholeProjectRefactorer.removeLayerInScene(
|
||||
project,
|
||||
scene,
|
||||
layerName
|
||||
);
|
||||
}
|
||||
scene.getLayers().removeLayer(layerName);
|
||||
changes.push(
|
||||
`Removed layer "${layerName}" for scene "${scene.getName()}".`
|
||||
);
|
||||
} else {
|
||||
if (new_layer_name) {
|
||||
gd.WholeProjectRefactorer.renameLayerInScene(
|
||||
project,
|
||||
scene,
|
||||
layerName,
|
||||
new_layer_name
|
||||
);
|
||||
changes.push(
|
||||
`Renamed layer "${layerName}" to "${new_layer_name}" for scene "${scene.getName()}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (new_layer_position !== null) {
|
||||
scene
|
||||
.getLayers()
|
||||
.moveLayer(
|
||||
scene.getLayers().getLayerPosition(layerName),
|
||||
new_layer_position
|
||||
);
|
||||
changes.push(
|
||||
`Moved layer "${layerName}" to position ${new_layer_position} for scene "${scene.getName()}".`
|
||||
);
|
||||
}
|
||||
|
||||
// /!\ Tell the editor that some instances have potentially been modified (and even removed).
|
||||
// This will force the instances editor to destroy and mount again the
|
||||
// renderers to avoid keeping any references to existing instances, and also drop any selection.
|
||||
onInstancesModifiedOutsideEditor({
|
||||
scene,
|
||||
});
|
||||
} else {
|
||||
scene
|
||||
.getLayers()
|
||||
.insertNewLayer(
|
||||
new_layer_name || layerName,
|
||||
new_layer_position === null
|
||||
? scene.getLayersCount()
|
||||
: new_layer_position
|
||||
);
|
||||
changes.push(
|
||||
`Created new layer "${new_layer_name ||
|
||||
layerName}" for scene "${scene.getName()}" at position ${new_layer_position ||
|
||||
0}.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changed_layer_effects) {
|
||||
changed_layer_effects.forEach(changed_layer_effect => {
|
||||
const layerName = SafeExtractor.extractStringProperty(
|
||||
changed_layer_effect,
|
||||
'layer_name'
|
||||
);
|
||||
if (layerName === null) {
|
||||
warnings.push(
|
||||
`Missing "layer_name" in an item of changed_layer_effects. It was ignored and not changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!scene.hasLayerNamed(layerName)) {
|
||||
warnings.push(
|
||||
`Layer not found: "${layerName}". It was ignored and no effects on it were changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const layer = scene.getLayers().getLayer(layerName);
|
||||
const effectsContainer = layer.getEffects();
|
||||
|
||||
const effectName = SafeExtractor.extractStringProperty(
|
||||
changed_layer_effect,
|
||||
'effect_name'
|
||||
);
|
||||
if (effectName === null) {
|
||||
warnings.push(
|
||||
`Missing "effect_name" in an item of changed_layer_effects. It was ignored and not changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const effect_type = SafeExtractor.extractStringProperty(
|
||||
changed_layer_effect,
|
||||
'effect_type'
|
||||
);
|
||||
const new_effect_name = SafeExtractor.extractStringProperty(
|
||||
changed_layer_effect,
|
||||
'new_effect_name'
|
||||
);
|
||||
const new_effect_position = SafeExtractor.extractNumberProperty(
|
||||
changed_layer_effect,
|
||||
'new_effect_position'
|
||||
);
|
||||
const delete_this_effect = SafeExtractor.extractBooleanProperty(
|
||||
changed_layer_effect,
|
||||
'delete_this_effect'
|
||||
);
|
||||
let newlyCreatedEffect: gdEffect | null = null;
|
||||
|
||||
if (effectsContainer.hasEffectNamed(effectName)) {
|
||||
const effect = effectsContainer.getEffect(effectName);
|
||||
if (delete_this_effect) {
|
||||
effectsContainer.removeEffect(effectName);
|
||||
changes.push(
|
||||
`Removed "${effectName}" effect on layer "${layerName}".`
|
||||
);
|
||||
} else {
|
||||
if (new_effect_name) {
|
||||
effect.setName(new_effect_name);
|
||||
changes.push(
|
||||
`Renamed the "${effectName}" effect on layer "${layerName}" to "${new_effect_name}".`
|
||||
);
|
||||
}
|
||||
if (new_effect_position !== null) {
|
||||
effectsContainer.moveEffect(
|
||||
effectsContainer.getEffectPosition(effectName),
|
||||
new_effect_position
|
||||
);
|
||||
changes.push(
|
||||
`Moved the "${effectName}" effect on layer "${layerName}" to position ${new_effect_position}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (effect_type) {
|
||||
const newEffectName = new_effect_name || effectName;
|
||||
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
effect_type
|
||||
);
|
||||
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
|
||||
warnings.push(
|
||||
`Effect type "${effect_type}" is not a valid effect type. Effect "${newEffectName}" was NOT added.`
|
||||
);
|
||||
} else {
|
||||
newlyCreatedEffect = effectsContainer.insertNewEffect(
|
||||
newEffectName,
|
||||
new_effect_position || 0
|
||||
);
|
||||
newlyCreatedEffect.setEffectType(effect_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changed_properties = SafeExtractor.extractArrayProperty(
|
||||
changed_layer_effect,
|
||||
'changed_properties'
|
||||
);
|
||||
if (changed_properties) {
|
||||
if (!effectsContainer.hasEffectNamed(effectName)) {
|
||||
warnings.push(
|
||||
`Effect not found: "${effectName}". It was ignored and not changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const effect = effectsContainer.getEffect(effectName);
|
||||
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
effect.getEffectType()
|
||||
);
|
||||
|
||||
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
|
||||
warnings.push(
|
||||
`Effect "${effectName}" is not a valid effect. It was ignored and not changed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const effectProperties = effectMetadata.getProperties();
|
||||
|
||||
changed_properties.forEach(changed_property => {
|
||||
const propertyName = SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'property_name'
|
||||
);
|
||||
const newValue = SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'new_value'
|
||||
);
|
||||
if (propertyName === null || newValue === null) {
|
||||
warnings.push(
|
||||
`Missing "property_name" or "new_value" in an item of changed_properties. It was ignored and not changed. Make sure you follow the exact format for changing effect properties.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { foundProperty } = findPropertyByName({
|
||||
properties: effectProperties,
|
||||
name: propertyName,
|
||||
});
|
||||
if (!foundProperty) {
|
||||
warnings.push(
|
||||
`Property not found: "${propertyName}" in effect "${effectName}". It was ignored and not changed. Make sure you only change existing effect properties.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const lowercasedType = foundProperty.getType().toLowerCase();
|
||||
if (lowercasedType === 'number') {
|
||||
effect.setDoubleParameter(
|
||||
propertyName,
|
||||
parseFloat(newValue) || 0
|
||||
);
|
||||
} else if (lowercasedType === 'boolean') {
|
||||
effect.setBooleanParameter(
|
||||
propertyName,
|
||||
newValue.toLowerCase() === 'true'
|
||||
);
|
||||
} else {
|
||||
effect.setStringParameter(propertyName, newValue);
|
||||
}
|
||||
|
||||
changes.push(
|
||||
`Modified "${propertyName}" property of the "${effectName}" effect to "${newValue}".`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (newlyCreatedEffect) {
|
||||
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
newlyCreatedEffect.getEffectType()
|
||||
);
|
||||
if (gd.MetadataProvider.isBadEffectMetadata(effectMetadata)) {
|
||||
// Should not happen.
|
||||
} else {
|
||||
changes.push(
|
||||
`Created new "${newlyCreatedEffect.getName()}" effect on layer "${layerName}" at position ${new_effect_position ||
|
||||
0}. It properties are: ${serializeEffectProperties(
|
||||
newlyCreatedEffect,
|
||||
effectMetadata
|
||||
)
|
||||
// This stringify might not give the prettiest output, this could be improved.
|
||||
.map(serializedProperty => JSON.stringify(serializedProperty))
|
||||
.join(', ')}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changes.length === 0 && warnings.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No changes were made.',
|
||||
};
|
||||
} else if (changes.length === 0 && warnings.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
'No changes were made because of the issues listed in the warnings.',
|
||||
warnings: warnings.join('\n'),
|
||||
};
|
||||
} else if (changes.length > 0 && warnings.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: ['Successfully done the changes.', ...changes].join('\n'),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
message: [
|
||||
'Successfully done some changes but some issues were found - see the warnings.',
|
||||
...changes,
|
||||
].join('\n'),
|
||||
warnings: warnings.join('\n'),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const addOrEditVariable: EditorFunction = {
|
||||
renderForEditor: ({ args, shouldShowDetails }) => {
|
||||
const variable_name_or_path = extractRequiredString(
|
||||
@@ -2558,5 +3183,7 @@ export const editorFunctions: { [string]: EditorFunction } = {
|
||||
add_scene_events: addSceneEvents,
|
||||
create_scene: createScene,
|
||||
delete_scene: deleteScene,
|
||||
inspect_scene_properties_layers_effects: inspectScenePropertiesLayersEffects,
|
||||
change_scene_properties_layers_effects: changeScenePropertiesLayersEffects,
|
||||
add_or_edit_variable: addOrEditVariable,
|
||||
};
|
||||
|
@@ -19,6 +19,7 @@ import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExte
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import Window from '../Utils/Window';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -67,150 +68,154 @@ export default function EventsBasedBehaviorEditor({
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<DismissableAlertMessage
|
||||
identifier="events-based-behavior-explanation"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
This is the configuration of your behavior. Make sure to choose a
|
||||
proper internal name as it's hard to change it later. Enter a
|
||||
description explaining what the behavior is doing to the object.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Internal Name</Trans>}
|
||||
value={eventsBasedBehavior.getName()}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Name displayed in editor</Trans>}
|
||||
value={eventsBasedBehavior.getFullName()}
|
||||
onChange={text => {
|
||||
eventsBasedBehavior.setFullName(text);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Description</Trans>}
|
||||
helperMarkdownText={i18n._(
|
||||
t`Explain what the behavior is doing to the object. Start with a verb when possible.`
|
||||
)}
|
||||
value={eventsBasedBehavior.getDescription()}
|
||||
onChange={text => {
|
||||
eventsBasedBehavior.setDescription(text);
|
||||
onChange();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={3}
|
||||
/>
|
||||
<ObjectTypeSelector
|
||||
floatingLabelText={
|
||||
<Trans>Object on which this behavior can be used</Trans>
|
||||
}
|
||||
project={project}
|
||||
value={eventsBasedBehavior.getObjectType()}
|
||||
onChange={(objectType: string) => {
|
||||
eventsBasedBehavior.setObjectType(objectType);
|
||||
onChange();
|
||||
}}
|
||||
allowedObjectTypes={
|
||||
allObjectTypes.length === 0
|
||||
? undefined /* Allow anything as the behavior is not used */
|
||||
: allObjectTypes.length === 1
|
||||
? [
|
||||
'',
|
||||
allObjectTypes[0],
|
||||
] /* Allow only the type of the objects using the behavior */
|
||||
: [
|
||||
'',
|
||||
] /* More than one type of object are using the behavior. Only "any object" can be used on this behavior */
|
||||
}
|
||||
/>
|
||||
{allObjectTypes.length > 1 && (
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
This behavior is being used by multiple types of objects. Thus,
|
||||
you can't restrict its usage to any particular object type. All
|
||||
the object types using this behavior are listed here:
|
||||
{allObjectTypes.join(', ')}
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
{isDev && (
|
||||
<SelectField
|
||||
floatingLabelText={
|
||||
<Trans>Visibility in quick customization dialog</Trans>
|
||||
}
|
||||
value={eventsBasedBehavior.getQuickCustomizationVisibility()}
|
||||
onChange={(e, i, valueString: string) => {
|
||||
// $FlowFixMe
|
||||
const value: QuickCustomization_Visibility = valueString;
|
||||
eventsBasedBehavior.setQuickCustomizationVisibility(value);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Default}
|
||||
label={t`Default (visible)`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Visible}
|
||||
label={t`Always visible`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Hidden}
|
||||
label={t`Hidden`}
|
||||
/>
|
||||
</SelectField>
|
||||
)}
|
||||
<Checkbox
|
||||
label={<Trans>Private</Trans>}
|
||||
checked={eventsBasedBehavior.isPrivate()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedBehavior.setPrivate(checked);
|
||||
if (onConfigurationUpdated) onConfigurationUpdated('isPrivate');
|
||||
onChange();
|
||||
}}
|
||||
tooltipOrHelperText={
|
||||
eventsBasedBehavior.isPrivate() ? (
|
||||
<Trans>
|
||||
This behavior won't be visible in the scene and events
|
||||
editors.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This behavior will be visible in the scene and events editors.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{eventsBasedBehavior
|
||||
.getEventsFunctions()
|
||||
.getEventsFunctionsCount() === 0 && (
|
||||
<ScrollView>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<DismissableAlertMessage
|
||||
identifier="empty-events-based-behavior-explanation"
|
||||
identifier="events-based-behavior-explanation"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
Once you're done, start adding some functions to the behavior.
|
||||
Then, test the behavior by adding it to an object in a scene.
|
||||
This is the configuration of your behavior. Make sure to choose
|
||||
a proper internal name as it's hard to change it later. Enter a
|
||||
description explaining what the behavior is doing to the object.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
)}
|
||||
<Line noMargin>
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath="/behaviors/events-based-behaviors"
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Internal Name</Trans>}
|
||||
value={eventsBasedBehavior.getName()}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Name displayed in editor</Trans>}
|
||||
value={eventsBasedBehavior.getFullName()}
|
||||
onChange={text => {
|
||||
eventsBasedBehavior.setFullName(text);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Description</Trans>}
|
||||
helperMarkdownText={i18n._(
|
||||
t`Explain what the behavior is doing to the object. Start with a verb when possible.`
|
||||
)}
|
||||
value={eventsBasedBehavior.getDescription()}
|
||||
onChange={text => {
|
||||
eventsBasedBehavior.setDescription(text);
|
||||
onChange();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={3}
|
||||
/>
|
||||
<ObjectTypeSelector
|
||||
floatingLabelText={
|
||||
<Trans>Object on which this behavior can be used</Trans>
|
||||
}
|
||||
project={project}
|
||||
value={eventsBasedBehavior.getObjectType()}
|
||||
onChange={(objectType: string) => {
|
||||
eventsBasedBehavior.setObjectType(objectType);
|
||||
onChange();
|
||||
}}
|
||||
allowedObjectTypes={
|
||||
allObjectTypes.length === 0
|
||||
? undefined /* Allow anything as the behavior is not used */
|
||||
: allObjectTypes.length === 1
|
||||
? [
|
||||
'',
|
||||
allObjectTypes[0],
|
||||
] /* Allow only the type of the objects using the behavior */
|
||||
: [
|
||||
'',
|
||||
] /* More than one type of object are using the behavior. Only "any object" can be used on this behavior */
|
||||
}
|
||||
/>
|
||||
{allObjectTypes.length > 1 && (
|
||||
<AlertMessage kind="info">
|
||||
<Trans>
|
||||
This behavior is being used by multiple types of objects.
|
||||
Thus, you can't restrict its usage to any particular object
|
||||
type. All the object types using this behavior are listed
|
||||
here:
|
||||
{allObjectTypes.join(', ')}
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
{isDev && (
|
||||
<SelectField
|
||||
floatingLabelText={
|
||||
<Trans>Visibility in quick customization dialog</Trans>
|
||||
}
|
||||
value={eventsBasedBehavior.getQuickCustomizationVisibility()}
|
||||
onChange={(e, i, valueString: string) => {
|
||||
// $FlowFixMe
|
||||
const value: QuickCustomization_Visibility = valueString;
|
||||
eventsBasedBehavior.setQuickCustomizationVisibility(value);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Default}
|
||||
label={t`Default (visible)`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Visible}
|
||||
label={t`Always visible`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={gd.QuickCustomization.Hidden}
|
||||
label={t`Hidden`}
|
||||
/>
|
||||
</SelectField>
|
||||
)}
|
||||
<Checkbox
|
||||
label={<Trans>Private</Trans>}
|
||||
checked={eventsBasedBehavior.isPrivate()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedBehavior.setPrivate(checked);
|
||||
if (onConfigurationUpdated) onConfigurationUpdated('isPrivate');
|
||||
onChange();
|
||||
}}
|
||||
tooltipOrHelperText={
|
||||
eventsBasedBehavior.isPrivate() ? (
|
||||
<Trans>
|
||||
This behavior won't be visible in the scene and events
|
||||
editors.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This behavior will be visible in the scene and events
|
||||
editors.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{eventsBasedBehavior
|
||||
.getEventsFunctions()
|
||||
.getEventsFunctionsCount() === 0 && (
|
||||
<DismissableAlertMessage
|
||||
identifier="empty-events-based-behavior-explanation"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
Once you're done, start adding some functions to the behavior.
|
||||
Then, test the behavior by adding it to an object in a scene.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
)}
|
||||
<Line noMargin>
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath="/behaviors/events-based-behaviors"
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
|
@@ -72,6 +72,7 @@ export default function EventsBasedObjectEditorPanel({
|
||||
</Line>
|
||||
{currentTab === 'configuration' && (
|
||||
<EventsBasedObjectEditor
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
eventsBasedObject={eventsBasedObject}
|
||||
unsavedChanges={unsavedChanges}
|
||||
onOpenCustomObjectEditor={onOpenCustomObjectEditor}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user