mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
27 Commits
cursor/add
...
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 |
@@ -18,19 +18,19 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsAnimatableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("AnimatableCapability",
|
||||
_("Objects with animations"),
|
||||
_("Actions and conditions for objects having animations (sprite, 3D models...)."),
|
||||
_("Animate objects."),
|
||||
"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",
|
||||
_("Actions and conditions for objects having animations (sprite, 3D models...).."),
|
||||
"",
|
||||
|
@@ -18,7 +18,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("EffectCapability",
|
||||
_("Effect capability"),
|
||||
_("Objects with effects"),
|
||||
_("Actions/conditions to enable/disable and change parameters of visual effects applied on objects."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
@@ -28,7 +28,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsEffectExtension(
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"EffectBehavior",
|
||||
_("Effect capability"),
|
||||
_("Objects with effects"),
|
||||
"Effect",
|
||||
_("Actions/conditions to enable/disable and change parameters of visual effects applied on objects."),
|
||||
"",
|
||||
|
@@ -18,7 +18,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("FlippableCapability",
|
||||
_("Flippable capability"),
|
||||
_("Flippable objects"),
|
||||
_("Actions/conditions for objects which can be flipped horizontally or vertically."),
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
@@ -28,7 +28,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFlippableExtension(
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
"FlippableBehavior",
|
||||
_("Flippable capability"),
|
||||
_("Flippable objects"),
|
||||
"Flippable",
|
||||
_("Actions/conditions for objects which can be flipped horizontally or vertically."),
|
||||
"",
|
||||
|
@@ -18,13 +18,13 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("OpacityCapability",
|
||||
_("Opacity capability"),
|
||||
_("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");
|
||||
@@ -32,7 +32,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsOpacityExtension(
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension
|
||||
.AddBehavior("OpacityBehavior",
|
||||
_("Opacity capability"),
|
||||
_("Objects with opacity"),
|
||||
"Opacity",
|
||||
_("Action/condition/expression to change or check the "
|
||||
"opacity of an object (0-255)."),
|
||||
|
@@ -18,7 +18,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsResizableExtension(
|
||||
extension
|
||||
.SetExtensionInformation(
|
||||
"ResizableCapability",
|
||||
_("Resizable capability"),
|
||||
_("Resizable objects"),
|
||||
_("Change or compare the size (width/height) of an object which can "
|
||||
"be resized (i.e: most objects)."),
|
||||
"Florian Rival",
|
||||
@@ -30,7 +30,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsResizableExtension(
|
||||
gd::BehaviorMetadata &aut =
|
||||
extension
|
||||
.AddBehavior("ResizableBehavior",
|
||||
_("Resizable capability"),
|
||||
_("Resizable objects"),
|
||||
"Resizable",
|
||||
_("Change or compare the size (width/height) of an "
|
||||
"object which can be resized (i.e: most objects)."),
|
||||
|
@@ -18,13 +18,13 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsScalableExtension(
|
||||
gd::PlatformExtension& extension) {
|
||||
extension
|
||||
.SetExtensionInformation("ScalableCapability",
|
||||
_("Scalable capability"),
|
||||
_("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"))
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Scalable objects"))
|
||||
.SetIcon("res/actions/scale24_black.png");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Size")).SetIcon(
|
||||
"res/actions/scale24_black.png");
|
||||
@@ -32,7 +32,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsScalableExtension(
|
||||
gd::BehaviorMetadata& aut =
|
||||
extension
|
||||
.AddBehavior("ScalableBehavior",
|
||||
_("Scalable capability"),
|
||||
_("Scalable objects"),
|
||||
"Scale",
|
||||
_("Actions/conditions/expression to change or check the "
|
||||
"scale of an object (default: 1)."),
|
||||
|
@@ -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."),
|
||||
"",
|
||||
|
@@ -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');
|
||||
}
|
||||
@@ -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()
|
||||
|
@@ -94,16 +94,6 @@ module.exports = {
|
||||
.setLabel(_('Vertical alignment'))
|
||||
.setGroup(_('Appearance'));
|
||||
|
||||
if (!objectContent.lineHeight) {
|
||||
objectContent.lineHeight = 0;
|
||||
}
|
||||
objectProperties
|
||||
.getOrCreate('lineHeight')
|
||||
.setValue(objectContent.lineHeight.toString())
|
||||
.setType('number')
|
||||
.setLabel(_('Line height (0 = default)'))
|
||||
.setGroup(_('Font'));
|
||||
|
||||
objectProperties
|
||||
.getOrCreate('fontFamily')
|
||||
.setValue(objectContent.fontFamily)
|
||||
@@ -404,19 +394,6 @@ module.exports = {
|
||||
expressionLabel: _('Get the wrapping width'),
|
||||
expressionDescription: _('Get the wrapping width'),
|
||||
},
|
||||
{
|
||||
functionName: 'LineHeight',
|
||||
iconPath: 'res/actions/characterSize24.png',
|
||||
type: 'number',
|
||||
instructionLabel: _('Line height'),
|
||||
paramLabel: _('Line height (0 = default)'),
|
||||
conditionDescription: _('Compare the base line height of the text.'),
|
||||
conditionSentence: _('the base line height'),
|
||||
actionDescription: _('Set base line height'),
|
||||
actionSentence: _('the base line height'),
|
||||
expressionLabel: _('Get the base line height'),
|
||||
expressionDescription: _('Get the base line height'),
|
||||
},
|
||||
];
|
||||
|
||||
addSettersAndGettersToObject(object, setterAndGetterProperties, 'BBText');
|
||||
|
@@ -32,7 +32,6 @@ namespace gdjs {
|
||||
wordWrap: runtimeObject._wrapping,
|
||||
wordWrapWidth: runtimeObject._wrappingWidth,
|
||||
align: runtimeObject._textAlign as PIXI.TextStyleAlign | undefined,
|
||||
lineHeight: runtimeObject._lineHeight > 0 ? runtimeObject._lineHeight : undefined,
|
||||
},
|
||||
});
|
||||
instanceContainer
|
||||
@@ -103,13 +102,6 @@ namespace gdjs {
|
||||
this._pixiObject.dirty = true;
|
||||
}
|
||||
|
||||
updateLineHeight(): void {
|
||||
//@ts-ignore Private member usage.
|
||||
this._pixiObject.textStyles.default.lineHeight =
|
||||
this._object._lineHeight;
|
||||
this._pixiObject.dirty = true;
|
||||
}
|
||||
|
||||
updatePosition(): void {
|
||||
if (this._object.isWrapping() && this._pixiObject.width !== 0) {
|
||||
const alignmentX =
|
||||
|
@@ -20,8 +20,6 @@ namespace gdjs {
|
||||
/** Alignment of the text: "left", "center" or "right" */
|
||||
align: 'left' | 'center' | 'right';
|
||||
verticalTextAlignment: 'top' | 'center' | 'bottom';
|
||||
/** Line height for multiline text (0 = default) */
|
||||
lineHeight: number;
|
||||
};
|
||||
};
|
||||
export type BBTextObjectData = ObjectData & BBTextObjectDataType;
|
||||
@@ -37,7 +35,6 @@ namespace gdjs {
|
||||
align: string;
|
||||
vta: string;
|
||||
hidden: boolean;
|
||||
lh: number;
|
||||
};
|
||||
|
||||
export type BBTextObjectNetworkSyncData = ObjectNetworkSyncData &
|
||||
@@ -64,7 +61,6 @@ namespace gdjs {
|
||||
|
||||
_textAlign: string;
|
||||
_verticalTextAlignment: string;
|
||||
_lineHeight: float;
|
||||
|
||||
_renderer: gdjs.BBTextRuntimeObjectRenderer;
|
||||
|
||||
@@ -91,7 +87,6 @@ namespace gdjs {
|
||||
this._textAlign = objectData.content.align;
|
||||
this._verticalTextAlignment =
|
||||
objectData.content.verticalTextAlignment || 'top';
|
||||
this._lineHeight = objectData.content.lineHeight || 0;
|
||||
this.hidden = !objectData.content.visible;
|
||||
|
||||
this._renderer = new gdjs.BBTextRuntimeObjectRenderer(
|
||||
@@ -147,11 +142,6 @@ namespace gdjs {
|
||||
newObjectData.content.verticalTextAlignment
|
||||
);
|
||||
}
|
||||
if (
|
||||
oldObjectData.content.lineHeight !== newObjectData.content.lineHeight
|
||||
) {
|
||||
this.setLineHeight(newObjectData.content.lineHeight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -168,7 +158,6 @@ namespace gdjs {
|
||||
align: this._textAlign,
|
||||
vta: this._verticalTextAlignment,
|
||||
hidden: this.hidden,
|
||||
lh: this._lineHeight,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,9 +196,6 @@ namespace gdjs {
|
||||
if (this.hidden !== undefined) {
|
||||
this.hide(networkSyncData.hidden);
|
||||
}
|
||||
if (networkSyncData.lh !== undefined) {
|
||||
this.setLineHeight(networkSyncData.lh);
|
||||
}
|
||||
}
|
||||
|
||||
override extraInitializationFromInitialInstance(
|
||||
@@ -411,23 +397,6 @@ namespace gdjs {
|
||||
: 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line height of the BBText object.
|
||||
* @return line height (0 = default)
|
||||
*/
|
||||
getLineHeight(): number {
|
||||
return this._lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set line height of the BBText object.
|
||||
* @param value line height (0 = default)
|
||||
*/
|
||||
setLineHeight(value: float): void {
|
||||
this._lineHeight = value;
|
||||
this._renderer.updateLineHeight();
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
gdjs.registerObject('BBText::BBText', gdjs.BBTextRuntimeObject);
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -454,8 +454,8 @@ void DeclareTextObjectExtension(gd::PlatformExtension& extension) {
|
||||
_("Line height"),
|
||||
_("the line height of a text object"),
|
||||
_("the line height"),
|
||||
_("Font"),
|
||||
"res/conditions/characterSize24.png")
|
||||
"",
|
||||
"res/actions/font24.png")
|
||||
.AddParameter("object", _("Object"), "Text")
|
||||
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions());
|
||||
|
||||
|
@@ -38,14 +38,6 @@ class TextObjectJsExtension : public gd::PlatformExtension {
|
||||
GetAllExpressionsForObject("TextObject::Text")["FontSize"]
|
||||
.SetFunctionName("getCharacterSize");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Text::SetLineHeight"]
|
||||
.SetFunctionName("setLineHeight")
|
||||
.SetGetter("getLineHeight");
|
||||
GetAllConditionsForObject("TextObject::Text")["TextObject::Text::LineHeight"]
|
||||
.SetFunctionName("getLineHeight");
|
||||
GetAllExpressionsForObject("TextObject::Text")["LineHeight"]
|
||||
.SetFunctionName("getLineHeight");
|
||||
|
||||
GetAllActionsForObject("TextObject::Text")["TextObject::Font"]
|
||||
.SetFunctionName("setFontName");
|
||||
|
||||
@@ -104,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),
|
||||
@@ -36,8 +37,7 @@ TextObject::TextObject()
|
||||
shadowOpacity(127),
|
||||
shadowAngle(90),
|
||||
shadowDistance(4),
|
||||
shadowBlurRadius(2),
|
||||
lineHeight(0) {}
|
||||
shadowBlurRadius(2) {}
|
||||
|
||||
TextObject::~TextObject() {};
|
||||
|
||||
@@ -51,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;
|
||||
@@ -111,10 +115,6 @@ bool TextObject::UpdateProperty(const gd::String& propertyName,
|
||||
shadowBlurRadius = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
if (propertyName == "lineHeight") {
|
||||
lineHeight = newValue.To<double>();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -134,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")
|
||||
@@ -260,13 +267,6 @@ std::map<gd::String, gd::PropertyDescriptor> TextObject::GetProperties() const {
|
||||
.SetAdvanced()
|
||||
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
|
||||
|
||||
objectProperties["lineHeight"]
|
||||
.SetValue(gd::String::From(lineHeight))
|
||||
.SetType("number")
|
||||
.SetLabel(_("Line height"))
|
||||
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
|
||||
.SetGroup(_("Font"));
|
||||
|
||||
return objectProperties;
|
||||
}
|
||||
|
||||
@@ -283,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");
|
||||
@@ -316,8 +317,6 @@ void TextObject::DoUnserializeFrom(gd::Project& project,
|
||||
SetShadowAngle(content.GetIntAttribute("shadowAngle", 90));
|
||||
SetShadowDistance(content.GetIntAttribute("shadowDistance", 4));
|
||||
SetShadowBlurRadius(content.GetIntAttribute("shadowBlurRadius", 2));
|
||||
|
||||
SetLineHeight(content.GetDoubleAttribute("lineHeight", 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,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);
|
||||
@@ -370,7 +370,6 @@ void TextObject::DoSerializeTo(gd::SerializerElement& element) const {
|
||||
content.SetAttribute("shadowAngle", shadowAngle);
|
||||
content.SetAttribute("shadowDistance", shadowDistance);
|
||||
content.SetAttribute("shadowBlurRadius", shadowBlurRadius);
|
||||
content.SetAttribute("lineHeight", lineHeight);
|
||||
}
|
||||
|
||||
void TextObject::ExposeResources(gd::ArbitraryResourceWorker& worker) {
|
||||
|
@@ -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; };
|
||||
@@ -113,9 +119,6 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
|
||||
void SetShadowBlurRadius(double value) { shadowBlurRadius = value; };
|
||||
double GetShadowBlurRadius() const { return shadowBlurRadius; };
|
||||
|
||||
void SetLineHeight(double value) { lineHeight = value; };
|
||||
double GetLineHeight() const { return lineHeight; };
|
||||
|
||||
private:
|
||||
virtual void DoUnserializeFrom(gd::Project& project,
|
||||
const gd::SerializerElement& element) override;
|
||||
@@ -123,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;
|
||||
@@ -140,5 +144,4 @@ class GD_EXTENSION_API TextObject : public gd::ObjectConfiguration {
|
||||
double shadowAngle;
|
||||
double shadowDistance;
|
||||
double shadowBlurRadius;
|
||||
double lineHeight;
|
||||
};
|
||||
|
@@ -64,7 +64,6 @@ namespace gdjs {
|
||||
style.wordWrap = this._object._wrapping;
|
||||
style.wordWrapWidth = this._object._wrappingWidth;
|
||||
style.breakWords = true;
|
||||
style.lineHeight = this._object._lineHeight > 0 ? this._object._lineHeight : undefined;
|
||||
style.stroke = gdjs.rgbToHexNumber(
|
||||
this._object._outlineColor[0],
|
||||
this._object._outlineColor[1],
|
||||
@@ -87,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,7 +22,8 @@ namespace gdjs {
|
||||
text: string;
|
||||
textAlignment: string;
|
||||
verticalTextAlignment: string;
|
||||
lineHeight: number;
|
||||
/** The line height */
|
||||
lineHeight: float;
|
||||
|
||||
isOutlineEnabled: boolean;
|
||||
outlineThickness: float;
|
||||
@@ -35,7 +36,6 @@ namespace gdjs {
|
||||
shadowDistance: float;
|
||||
shadowAngle: float;
|
||||
shadowBlurRadius: float;
|
||||
lineHeight: float;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -92,7 +92,6 @@ namespace gdjs {
|
||||
_wrapping: boolean = false;
|
||||
// A wrapping of 1 makes games crash on Firefox
|
||||
_wrappingWidth: float = 100;
|
||||
_lineHeight: float;
|
||||
|
||||
_isOutlineEnabled: boolean;
|
||||
_outlineThickness: float;
|
||||
@@ -105,8 +104,9 @@ namespace gdjs {
|
||||
_shadowAngle: float;
|
||||
_shadowBlur: float;
|
||||
|
||||
_lineHeight: float;
|
||||
|
||||
_padding: integer = 5;
|
||||
_lineHeight: float = 0;
|
||||
_str: string;
|
||||
_renderer: gdjs.TextRuntimeObjectRenderer;
|
||||
|
||||
@@ -133,7 +133,6 @@ namespace gdjs {
|
||||
this._str = content.text;
|
||||
this._textAlign = content.textAlignment || 'left';
|
||||
this._verticalTextAlignment = content.verticalTextAlignment || 'top';
|
||||
this._lineHeight = content.lineHeight || 0;
|
||||
|
||||
this._isOutlineEnabled = content.isOutlineEnabled;
|
||||
this._outlineThickness = content.outlineThickness;
|
||||
@@ -145,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,
|
||||
@@ -217,8 +217,8 @@ namespace gdjs {
|
||||
if (oldContent.shadowBlurRadius !== newContent.shadowBlurRadius) {
|
||||
this.setShadowBlurRadius(newContent.shadowBlurRadius);
|
||||
}
|
||||
if (oldContent.lineHeight !== newContent.lineHeight) {
|
||||
this.setLineHeight(newContent.lineHeight);
|
||||
if ((oldContent.lineHeight || 0) !== (newContent.lineHeight || 0)) {
|
||||
this.setLineHeight(newContent.lineHeight || 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -247,8 +247,8 @@ namespace gdjs {
|
||||
shd: this._shadowDistance,
|
||||
sha: this._shadowAngle,
|
||||
shb: this._shadowBlur,
|
||||
pad: this._padding,
|
||||
lh: this._lineHeight,
|
||||
pad: this._padding,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -322,12 +322,12 @@ namespace gdjs {
|
||||
if (networkSyncData.shb !== undefined) {
|
||||
this.setShadowBlurRadius(networkSyncData.shb);
|
||||
}
|
||||
if (networkSyncData.pad !== undefined) {
|
||||
this.setPadding(networkSyncData.pad);
|
||||
}
|
||||
if (networkSyncData.lh !== undefined) {
|
||||
this.setLineHeight(networkSyncData.lh);
|
||||
}
|
||||
if (networkSyncData.pad !== undefined) {
|
||||
this.setPadding(networkSyncData.pad);
|
||||
}
|
||||
}
|
||||
|
||||
override getRendererObject() {
|
||||
@@ -457,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.
|
||||
@@ -586,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();
|
||||
}
|
||||
@@ -704,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();
|
||||
}
|
||||
@@ -757,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;
|
||||
@@ -888,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;
|
||||
@@ -942,23 +920,6 @@ namespace gdjs {
|
||||
this._padding = value;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line height of the text object.
|
||||
* @return the line height
|
||||
*/
|
||||
getLineHeight(): number {
|
||||
return this._lineHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the line height of the text object.
|
||||
* @param value the line height
|
||||
*/
|
||||
setLineHeight(value: float): void {
|
||||
this._lineHeight = value;
|
||||
this._renderer.updateStyle();
|
||||
}
|
||||
}
|
||||
gdjs.registerObject('TextObject::Text', gdjs.TextRuntimeObject);
|
||||
}
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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])
|
||||
);
|
||||
},
|
||||
|
||||
|
@@ -841,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
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
@@ -894,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,6 +17,7 @@ import { Trans } from '@lingui/macro';
|
||||
import Link from '../UI/Link';
|
||||
import {
|
||||
hexNumberToRGBArray,
|
||||
rgbColorToHex,
|
||||
rgbOrHexToHexNumber,
|
||||
} from '../Utils/ColorTransformer';
|
||||
import { type SimplifiedBehavior } from './SimplifiedProject/SimplifiedProject';
|
||||
@@ -62,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 =
|
||||
@@ -122,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.
|
||||
*/
|
||||
@@ -145,6 +154,9 @@ export type EditorFunction = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
ensureExtensionInstalled: (options: {|
|
||||
extensionName: string,
|
||||
|}) => Promise<void>,
|
||||
@@ -1492,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 =>
|
||||
@@ -1500,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++) {
|
||||
@@ -1845,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 =>
|
||||
@@ -1853,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++) {
|
||||
@@ -2023,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>
|
||||
@@ -2031,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>
|
||||
@@ -2039,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>
|
||||
@@ -2247,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({
|
||||
@@ -2258,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(
|
||||
@@ -2348,7 +2392,7 @@ const createScene: EditorFunction = {
|
||||
if (project.hasLayoutNamed(scene_name)) {
|
||||
const scene = project.getLayout(scene_name);
|
||||
if (include_ui_layer && !scene.hasLayerNamed('UI')) {
|
||||
scene.insertNewLayer('UI', 0);
|
||||
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.`
|
||||
@@ -2363,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(
|
||||
@@ -2407,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(
|
||||
@@ -2564,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}
|
||||
|
@@ -6,6 +6,7 @@ import * as React from 'react';
|
||||
import TextField from '../UI/TextField';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import DismissableAlertMessage from '../UI/DismissableAlertMessage';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import Checkbox from '../UI/Checkbox';
|
||||
@@ -14,12 +15,14 @@ import { Line } from '../UI/Grid';
|
||||
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Window from '../Utils/Window';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const isDev = Window.isDev();
|
||||
|
||||
type Props = {|
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
eventsBasedObject: gdEventsBasedObject,
|
||||
onOpenCustomObjectEditor: () => void,
|
||||
unsavedChanges?: ?UnsavedChanges,
|
||||
@@ -29,6 +32,7 @@ type Props = {|
|
||||
|};
|
||||
|
||||
export default function EventsBasedObjectEditor({
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
onOpenCustomObjectEditor,
|
||||
unsavedChanges,
|
||||
@@ -47,135 +51,155 @@ export default function EventsBasedObjectEditor({
|
||||
);
|
||||
|
||||
return (
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<DismissableAlertMessage
|
||||
identifier="events-based-object-explanation"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
This is the configuration of your object. Make sure to choose a proper
|
||||
internal name as it's hard to change it later.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Internal Name</Trans>}
|
||||
value={eventsBasedObject.getName()}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Name displayed in editor</Trans>}
|
||||
value={eventsBasedObject.getFullName()}
|
||||
onChange={text => {
|
||||
eventsBasedObject.setFullName(text);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Description</Trans>}
|
||||
floatingLabelFixed
|
||||
translatableHintText={t`The description of the object should explain what the object is doing, and, briefly, how to use it.`}
|
||||
value={eventsBasedObject.getDescription()}
|
||||
onChange={text => {
|
||||
eventsBasedObject.setDescription(text);
|
||||
onChange();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={3}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Default name for created objects</Trans>}
|
||||
value={
|
||||
eventsBasedObject.getDefaultName() || eventsBasedObject.getName()
|
||||
}
|
||||
onChange={newName => {
|
||||
eventsBasedObject.setDefaultName(gd.Project.getSafeName(newName));
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Use 3D rendering</Trans>}
|
||||
checked={eventsBasedObject.isRenderedIn3D()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsRenderedIn3D(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Has animations</Trans>}
|
||||
checked={eventsBasedObject.isAnimatable()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsAnimatable(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Contains text</Trans>}
|
||||
checked={eventsBasedObject.isTextContainer()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsTextContainer(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
{isDev && (
|
||||
<ScrollView>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<DismissableAlertMessage
|
||||
identifier="events-based-object-explanation"
|
||||
kind="info"
|
||||
>
|
||||
<Trans>
|
||||
This is the configuration of your object. Make sure to choose a
|
||||
proper internal name as it's hard to change it later.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Internal Name</Trans>}
|
||||
value={eventsBasedObject.getName()}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Name displayed in editor</Trans>}
|
||||
value={eventsBasedObject.getFullName()}
|
||||
onChange={text => {
|
||||
eventsBasedObject.setFullName(text);
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Description</Trans>}
|
||||
floatingLabelFixed
|
||||
translatableHintText={t`The description of the object should explain what the object is doing, and, briefly, how to use it.`}
|
||||
value={eventsBasedObject.getDescription()}
|
||||
onChange={text => {
|
||||
eventsBasedObject.setDescription(text);
|
||||
onChange();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={3}
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Default name for created objects</Trans>}
|
||||
value={
|
||||
eventsBasedObject.getDefaultName() || eventsBasedObject.getName()
|
||||
}
|
||||
onChange={newName => {
|
||||
eventsBasedObject.setDefaultName(gd.Project.getSafeName(newName));
|
||||
onChange();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Use legacy renderer</Trans>}
|
||||
checked={eventsBasedObject.isUsingLegacyInstancesRenderer()}
|
||||
label={<Trans>Use 3D rendering</Trans>}
|
||||
checked={eventsBasedObject.isRenderedIn3D()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.makAsUsingLegacyInstancesRenderer(checked);
|
||||
eventsBasedObject.markAsRenderedIn3D(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Has animations</Trans>}
|
||||
checked={eventsBasedObject.isAnimatable()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsAnimatable(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Contains text</Trans>}
|
||||
checked={eventsBasedObject.isTextContainer()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsTextContainer(checked);
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<Trans>Expand inner area with parent</Trans>}
|
||||
checked={eventsBasedObject.isInnerAreaFollowingParentSize()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.markAsInnerAreaFollowingParentSize(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
label={<Trans>Private</Trans>}
|
||||
checked={eventsBasedObject.isPrivate()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.setPrivate(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
tooltipOrHelperText={
|
||||
eventsBasedObject.isPrivate() ? (
|
||||
<Trans>
|
||||
This object won't be visible in the scene and events editors.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This object will be visible in the scene and events editors.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Line noMargin justifyContent="center">
|
||||
<RaisedButton
|
||||
label={<Trans>Open visual editor for the object</Trans>}
|
||||
primary
|
||||
onClick={onOpenCustomObjectEditor}
|
||||
{isDev && (
|
||||
<Checkbox
|
||||
label={<Trans>Use legacy renderer</Trans>}
|
||||
checked={eventsBasedObject.isUsingLegacyInstancesRenderer()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.makAsUsingLegacyInstancesRenderer(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
label={<Trans>Private</Trans>}
|
||||
checked={eventsBasedObject.isPrivate()}
|
||||
onCheck={(e, checked) => {
|
||||
eventsBasedObject.setPrivate(checked);
|
||||
onChange();
|
||||
onEventsBasedObjectChildrenEdited(eventsBasedObject);
|
||||
}}
|
||||
tooltipOrHelperText={
|
||||
eventsBasedObject.isPrivate() ? (
|
||||
<Trans>
|
||||
This object won't be visible in the scene and events editors.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This object will be visible in the scene and events editors.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath="/objects/custom-objects-prefab-template"
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
{eventsFunctionsExtension.getOriginName() ===
|
||||
'gdevelop-extension-store' ? (
|
||||
<AlertMessage
|
||||
kind="error"
|
||||
renderRightButton={() => (
|
||||
<RaisedButton
|
||||
label={<Trans>Edit the default variant</Trans>}
|
||||
primary
|
||||
onClick={onOpenCustomObjectEditor}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Trans>
|
||||
The default variant is erased when the extension is updated.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : (
|
||||
<Line noMargin justifyContent="center">
|
||||
<RaisedButton
|
||||
label={<Trans>Open visual editor for the object</Trans>}
|
||||
primary
|
||||
onClick={onOpenCustomObjectEditor}
|
||||
/>
|
||||
</Line>
|
||||
)}
|
||||
<Line noMargin>
|
||||
<HelpButton
|
||||
key="help"
|
||||
helpPagePath="/objects/custom-objects-prefab-template"
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
@@ -19,7 +19,10 @@ import {
|
||||
} from '../../Utils/GDevelopServices/Project';
|
||||
import PlaceholderLoader from '../../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../../UI/PlaceholderError';
|
||||
import { getUserPublicProfilesByIds } from '../../Utils/GDevelopServices/User';
|
||||
import {
|
||||
getUserPublicProfilesByIds,
|
||||
type UserPublicProfileByIds,
|
||||
} from '../../Utils/GDevelopServices/User';
|
||||
import Dialog, { DialogPrimaryButton } from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import LeftLoader from '../../UI/LeftLoader';
|
||||
@@ -84,9 +87,10 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
| 'unexpected'
|
||||
| null
|
||||
>(null);
|
||||
const [userPublicProfileByIds, setUserPublicProfileByIds] = React.useState(
|
||||
{}
|
||||
);
|
||||
const [
|
||||
userPublicProfileByIds,
|
||||
setUserPublicProfileByIds,
|
||||
] = React.useState<UserPublicProfileByIds>({});
|
||||
const [
|
||||
showCollaboratorAddDialog,
|
||||
setShowCollaboratorAddDialog,
|
||||
@@ -312,6 +316,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<UserLine
|
||||
username={profile.username}
|
||||
fullName={profile.fullName}
|
||||
email={profile.email}
|
||||
level={currentUserLevel}
|
||||
/>
|
||||
@@ -365,6 +370,7 @@ const InviteHome = ({ cloudProjectId }: Props) => {
|
||||
projectUserAcls.map(projectUserAcl => (
|
||||
<UserLine
|
||||
username={getCollaboratorUsername(projectUserAcl.userId)}
|
||||
fullName={null}
|
||||
email={projectUserAcl.email}
|
||||
level={projectUserAcl.level}
|
||||
onDelete={() => {
|
||||
|
@@ -33,7 +33,7 @@ describe('EnumerateInstructions', () => {
|
||||
expect.objectContaining({
|
||||
displayedName: 'Animation finished',
|
||||
fullGroupName:
|
||||
'General ❯ Animatable capability ❯ Animations and images',
|
||||
'General ❯ Objects with animations ❯ Animations and images',
|
||||
type: 'AnimatableCapability::AnimatableBehavior::HasAnimationEnded',
|
||||
})
|
||||
);
|
||||
|
@@ -37,6 +37,10 @@ export type SceneEventsOutsideEditorChanges = {|
|
||||
newOrChangedAiGeneratedEventIds: Set<string>,
|
||||
|};
|
||||
|
||||
export type InstancesOutsideEditorChanges = {|
|
||||
scene: gdLayout,
|
||||
|};
|
||||
|
||||
export type RenderEditorContainerProps = {|
|
||||
isActive: boolean,
|
||||
projectItemName: ?string,
|
||||
@@ -179,11 +183,14 @@ export type RenderEditorContainerProps = {|
|
||||
) => void,
|
||||
onSceneObjectsDeleted: (scene: gdLayout) => void,
|
||||
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
|
||||
// Events editing
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
|
||||
onExtractAsExternalLayout: (name: string) => void,
|
||||
onExtractAsEventBasedObject: (
|
||||
extensionName: string,
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings';
|
||||
import {
|
||||
@@ -115,6 +116,10 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
saveUiSettings = () => {
|
||||
// const layout = this.getCustomObject();
|
||||
// const editor = this.editor;
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import SubscriptionChecker, {
|
||||
type SubscriptionCheckerInterface,
|
||||
@@ -66,6 +67,10 @@ export class DebuggerEditorContainer extends React.Component<
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
// To be updated, see https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops.
|
||||
UNSAFE_componentWillReceiveProps() {
|
||||
this._checkUserHasSubscription();
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
@@ -68,6 +69,10 @@ export class EventsEditorContainer extends React.Component<RenderEditorContainer
|
||||
}
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
getLayout(): ?gdLayout {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
|
||||
@@ -55,6 +56,10 @@ export class EventsFunctionsExtensionEditorContainer extends React.Component<Ren
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: RenderEditorContainerProps) {
|
||||
// We stop updates when the component is inactive.
|
||||
// If it's active, was active or becoming active again we let update propagate.
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import ExternalPropertiesDialog, {
|
||||
type ExternalProperties,
|
||||
@@ -99,6 +100,10 @@ export class ExternalEventsEditorContainer extends React.Component<
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
getExternalEvents(): ?gdExternalEvents {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (!project || !projectItemName) return null;
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import ExternalPropertiesDialog, {
|
||||
type ExternalProperties,
|
||||
@@ -157,6 +158,16 @@ export class ExternalLayoutEditorContainer extends React.Component<
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
if (changes.scene !== this.getLayout()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.onInstancesModifiedOutsideEditor();
|
||||
}
|
||||
}
|
||||
|
||||
getExternalLayout(): ?gdExternalLayout {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (!project || !projectItemName) return null;
|
||||
|
@@ -69,14 +69,32 @@ const styles = {
|
||||
};
|
||||
|
||||
const specializationLabels = {
|
||||
'game-development': <Trans>Game Development specialization</Trans>,
|
||||
'interaction-design': <Trans>Interaction Design specialization</Trans>,
|
||||
marketing: <Trans>Marketing specialization</Trans>,
|
||||
'game-development': <Trans>Game Development</Trans>,
|
||||
'interaction-design': <Trans>Interaction Design</Trans>,
|
||||
marketing: <Trans>Marketing</Trans>,
|
||||
|
||||
// Possible future additions:
|
||||
'game-design': <Trans>Game Design</Trans>,
|
||||
'programming-scripting': <Trans>Programming & Scripting</Trans>,
|
||||
'art-animation': <Trans>Art & Animation</Trans>,
|
||||
'audio-sound': <Trans>Audio & Sound</Trans>,
|
||||
'production-management': <Trans>Production & Project Management</Trans>,
|
||||
'liveops-analytics': <Trans>Live Ops & Analytics</Trans>,
|
||||
'narrative-writing': <Trans>Narrative & Writing</Trans>,
|
||||
};
|
||||
const specializationColors = {
|
||||
'game-development': '#5CB0FF',
|
||||
'interaction-design': '#CAC84E',
|
||||
marketing: '#FD3AE6',
|
||||
|
||||
// Possible future additions:
|
||||
'game-design': '#F28E2B', // orange
|
||||
'programming-scripting': '#59A14F', // green
|
||||
'art-animation': '#B07AA1', // purple
|
||||
'audio-sound': '#E15759', // red
|
||||
'production-management': '#9C755F', // brown
|
||||
'liveops-analytics': '#76B7B2', // teal
|
||||
'narrative-writing': '#79706E', // gray
|
||||
};
|
||||
|
||||
export const getSpecializationConfig = (
|
||||
@@ -98,7 +116,6 @@ export const getSpecializationConfig = (
|
||||
{specializationId
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, l => l.toUpperCase())}{' '}
|
||||
specialization
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
|
@@ -252,7 +252,7 @@ const MainPage = ({
|
||||
>
|
||||
<Column noMargin>
|
||||
<Text size="section-title">
|
||||
<Trans>In-app tutorials</Trans>
|
||||
<Trans>Free in-app tutorials</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
<Column noMargin>
|
||||
|
@@ -0,0 +1,119 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import FlatButton from '../../../../../UI/FlatButton';
|
||||
import Dialog, { DialogPrimaryButton } from '../../../../../UI/Dialog';
|
||||
import { ColumnStackLayout } from '../../../../../UI/Layout';
|
||||
import {
|
||||
type User,
|
||||
type EditUserChanges,
|
||||
type UsernameAvailability,
|
||||
} from '../../../../../Utils/GDevelopServices/User';
|
||||
import LeftLoader from '../../../../../UI/LeftLoader';
|
||||
import SemiControlledTextField from '../../../../../UI/SemiControlledTextField';
|
||||
import { UsernameField } from '../../../../../Profile/UsernameField';
|
||||
import AlertMessage from '../../../../../UI/AlertMessage';
|
||||
|
||||
type Props = {|
|
||||
member: User,
|
||||
onApply: (changes: EditUserChanges) => Promise<void>,
|
||||
onClose: () => void,
|
||||
isSaving: boolean,
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
export const EditStudentDialog = ({
|
||||
member,
|
||||
onApply,
|
||||
onClose,
|
||||
isSaving,
|
||||
error,
|
||||
}: Props) => {
|
||||
const [changes, setChanges] = React.useState<EditUserChanges | null>(null);
|
||||
|
||||
const [
|
||||
usernameAvailability,
|
||||
setUsernameAvailability,
|
||||
] = React.useState<?UsernameAvailability>(null);
|
||||
const [
|
||||
isValidatingUsername,
|
||||
setIsValidatingUsername,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
const canSave =
|
||||
!usernameAvailability || (usernameAvailability.isAvailable && !!changes);
|
||||
|
||||
const triggerApply = React.useCallback(
|
||||
async () => {
|
||||
if (!canSave || !changes) return;
|
||||
await onApply(changes);
|
||||
},
|
||||
[changes, canSave, onApply]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={<Trans>Edit student</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="close"
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={false}
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
id="close-button"
|
||||
/>,
|
||||
<LeftLoader isLoading={isSaving}>
|
||||
<DialogPrimaryButton
|
||||
key="apply"
|
||||
primary
|
||||
label={<Trans>Apply</Trans>}
|
||||
onClick={triggerApply}
|
||||
disabled={isSaving || !canSave}
|
||||
id="apply-button"
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
onRequestClose={onClose}
|
||||
onApply={triggerApply}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
{error && (
|
||||
<AlertMessage kind="error">
|
||||
<Trans>
|
||||
An error occurred while editing the student. Please try again.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
)}
|
||||
<SemiControlledTextField
|
||||
onChange={fullName => setChanges({ ...changes, fullName })}
|
||||
value={
|
||||
changes && typeof changes.fullName !== 'undefined'
|
||||
? changes.fullName
|
||||
: member.fullName || ''
|
||||
}
|
||||
disabled={isSaving}
|
||||
fullWidth
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Full name</Trans>}
|
||||
/>
|
||||
<UsernameField
|
||||
initialUsername={member.username}
|
||||
value={
|
||||
changes && typeof changes.username !== 'undefined'
|
||||
? changes.username
|
||||
: member.username || ''
|
||||
}
|
||||
onChange={(_, username) => setChanges({ ...changes, username })}
|
||||
disabled={isSaving}
|
||||
allowEmpty
|
||||
onAvailabilityChecked={setUsernameAvailability}
|
||||
onAvailabilityCheckLoading={setIsValidatingUsername}
|
||||
isValidatingUsername={isValidatingUsername}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@@ -3,7 +3,10 @@
|
||||
import * as React from 'react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import { type User } from '../../../../../Utils/GDevelopServices/User';
|
||||
import {
|
||||
type User,
|
||||
type EditUserChanges,
|
||||
} from '../../../../../Utils/GDevelopServices/User';
|
||||
import Text from '../../../../../UI/Text';
|
||||
import { LineStackLayout } from '../../../../../UI/Layout';
|
||||
import Checkbox from '../../../../../UI/Checkbox';
|
||||
@@ -12,9 +15,11 @@ import CheckboxChecked from '../../../../../UI/CustomSvgIcons/CheckboxChecked';
|
||||
import AsyncSemiControlledTextField from '../../../../../UI/AsyncSemiControlledTextField';
|
||||
import IconButton from '../../../../../UI/IconButton';
|
||||
import Key from '../../../../../UI/CustomSvgIcons/Key';
|
||||
import Edit from '../../../../../UI/CustomSvgIcons/Edit';
|
||||
import { Line } from '../../../../../UI/Grid';
|
||||
import { useResponsiveWindowSize } from '../../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import { textEllipsisStyle } from '../../../../../UI/TextEllipsis';
|
||||
import { EditStudentDialog } from './EditStudentDialog';
|
||||
|
||||
const primaryTextArchivedOpacity = 0.6;
|
||||
const primaryTextPlaceholderOpacity = 0.7;
|
||||
@@ -47,6 +52,10 @@ type Props = {|
|
||||
userId: string,
|
||||
newPassword: string,
|
||||
|}) => Promise<void>,
|
||||
onEdit: ({|
|
||||
editedUserId: string,
|
||||
changes: EditUserChanges,
|
||||
|}) => Promise<void>,
|
||||
|};
|
||||
|
||||
const ManageStudentRow = ({
|
||||
@@ -55,11 +64,18 @@ const ManageStudentRow = ({
|
||||
isArchived,
|
||||
onSelect,
|
||||
onChangePassword,
|
||||
onEdit,
|
||||
}: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const [isEditingPassword, setIsEditingPassword] = React.useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isSavingUser, setIsSavingUser] = React.useState<boolean>(false);
|
||||
const [isEditingUser, setIsEditingUser] = React.useState<boolean>(false);
|
||||
const [editedUserError, setEditedUserError] = React.useState<Error | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [
|
||||
passwordEditionError,
|
||||
setPasswordEditionError,
|
||||
@@ -110,15 +126,30 @@ const ManageStudentRow = ({
|
||||
uncheckedIcon={<CheckboxUnchecked />}
|
||||
checkedIcon={<CheckboxChecked />}
|
||||
/>
|
||||
{member.username ? (
|
||||
{member.username || member.fullName ? (
|
||||
<Text
|
||||
allowSelection
|
||||
style={{
|
||||
...textEllipsisStyle,
|
||||
opacity: isArchived ? primaryTextArchivedOpacity : undefined,
|
||||
}}
|
||||
tooltip={[member.fullName, member.username]
|
||||
.filter(Boolean)
|
||||
.join(' - ')}
|
||||
>
|
||||
<b>{member.username}</b>
|
||||
<b>
|
||||
{member.fullName ? (
|
||||
member.username ? (
|
||||
<Trans>
|
||||
{member.fullName} ({member.username})
|
||||
</Trans>
|
||||
) : (
|
||||
member.fullName
|
||||
)
|
||||
) : (
|
||||
member.username
|
||||
)}
|
||||
</b>
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
@@ -136,6 +167,15 @@ const ManageStudentRow = ({
|
||||
</i>
|
||||
</Text>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setIsEditingUser(true)}
|
||||
tooltip={t`Change username or full name`}
|
||||
>
|
||||
<Edit fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</LineStackLayout>
|
||||
);
|
||||
|
||||
@@ -205,6 +245,31 @@ const ManageStudentRow = ({
|
||||
</LineStackLayout>
|
||||
);
|
||||
|
||||
const editDialog = isEditingUser && (
|
||||
<EditStudentDialog
|
||||
member={member}
|
||||
isSaving={isSavingUser}
|
||||
error={editedUserError}
|
||||
onApply={async (changes: EditUserChanges) => {
|
||||
try {
|
||||
setEditedUserError(null);
|
||||
setIsSavingUser(true);
|
||||
await onEdit({ editedUserId: member.id, changes });
|
||||
setIsEditingUser(false);
|
||||
} catch (error) {
|
||||
console.error('An error occurred while editing user:', error);
|
||||
setEditedUserError(error);
|
||||
} finally {
|
||||
setIsSavingUser(false);
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsEditingUser(false);
|
||||
setEditedUserError(null);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
@@ -217,21 +282,23 @@ const ManageStudentRow = ({
|
||||
<Grid item xs={4} style={isMobile ? styles.mobileCell : styles.cell}>
|
||||
{passwordCell}
|
||||
</Grid>
|
||||
{editDialog}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid item xs={5} style={isMobile ? styles.mobileCell : styles.cell}>
|
||||
<Grid item xs={9} style={isMobile ? styles.mobileCell : styles.cell}>
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
{usernameCell}
|
||||
{emailCell}
|
||||
</LineStackLayout>
|
||||
</Grid>
|
||||
<Grid item xs={7} style={isMobile ? styles.mobileCell : styles.cell}>
|
||||
<Grid item xs={3} style={isMobile ? styles.mobileCell : styles.cell}>
|
||||
{passwordCell}
|
||||
</Grid>
|
||||
{editDialog}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -56,6 +56,7 @@ import { selectMessageByLocale } from '../../../../../Utils/i18n/MessageByLocale
|
||||
import TextButton from '../../../../../UI/TextButton';
|
||||
import Chip from '../../../../../UI/Chip';
|
||||
import { SubscriptionSuggestionContext } from '../../../../../Profile/Subscription/SubscriptionSuggestionContext';
|
||||
import { type EditUserChanges } from '../../../../../Utils/GDevelopServices/User';
|
||||
|
||||
const styles = {
|
||||
selectedMembersControlsContainer: {
|
||||
@@ -324,6 +325,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
onActivateMembers,
|
||||
onSetAdmin,
|
||||
onRefreshAdmins,
|
||||
onEditUser,
|
||||
} = React.useContext(TeamContext);
|
||||
|
||||
React.useEffect(
|
||||
@@ -337,6 +339,20 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
[credentialsCopySuccess]
|
||||
);
|
||||
|
||||
const onEditTeamMember = React.useCallback(
|
||||
async ({
|
||||
editedUserId,
|
||||
changes,
|
||||
}: {|
|
||||
editedUserId: string,
|
||||
changes: EditUserChanges,
|
||||
|}) => {
|
||||
await onEditUser(editedUserId, changes);
|
||||
await onRefreshMembers();
|
||||
},
|
||||
[onEditUser, onRefreshMembers]
|
||||
);
|
||||
|
||||
const onChangeTeamMemberPassword = React.useCallback(
|
||||
async ({
|
||||
userId,
|
||||
@@ -354,7 +370,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
const onCopyActiveCredentials = React.useCallback(
|
||||
() => {
|
||||
if (!members) return;
|
||||
let content = 'Username,Email,Password';
|
||||
let content = 'Username,Full Name,Email,Password';
|
||||
let membersToConsider = [];
|
||||
if (selectedUserIds.length === 0) {
|
||||
membersToConsider = members.filter(member => !member.deactivatedAt);
|
||||
@@ -364,7 +380,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
);
|
||||
}
|
||||
membersToConsider.forEach(member => {
|
||||
content += `\n${member.username || ''},${
|
||||
content += `\n${member.username || ''},${member.fullName || ''},${
|
||||
member.email
|
||||
},${member.password || ''}`;
|
||||
});
|
||||
@@ -673,12 +689,13 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
<UserLine
|
||||
key={adminUser.id}
|
||||
username={adminUser.username}
|
||||
fullName={adminUser.fullName}
|
||||
email={adminUser.email}
|
||||
level={null}
|
||||
onDelete={() => onRemoveAdmin(adminUser.email)}
|
||||
disabled={
|
||||
(profile && adminUser.id === profile.id) ||
|
||||
(adminEmailBeingRemoved &&
|
||||
(!!adminEmailBeingRemoved &&
|
||||
adminEmailBeingRemoved === adminUser.email)
|
||||
}
|
||||
/>
|
||||
@@ -822,10 +839,10 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
size="small"
|
||||
tooltip={
|
||||
selectedUserIds.length === 0
|
||||
? t`Copy active credentials`
|
||||
? t`Copy active credentials to CSV`
|
||||
: t`Copy ${
|
||||
selectedUserIds.length
|
||||
} credentials`
|
||||
} credentials to CSV`
|
||||
}
|
||||
disabled={
|
||||
hasNoActiveTeamMembers &&
|
||||
@@ -847,12 +864,12 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
<Column>
|
||||
{!isMobile && (
|
||||
<GridList cols={2} cellHeight={'auto'}>
|
||||
<Grid item xs={5}>
|
||||
<Grid item xs={9}>
|
||||
<Text style={{ opacity: 0.7 }}>
|
||||
<Trans>Student</Trans>
|
||||
</Text>
|
||||
</Grid>
|
||||
<Grid item xs={7}>
|
||||
<Grid item xs={3}>
|
||||
<Text style={{ opacity: 0.7 }}>
|
||||
<Trans>Password</Trans>
|
||||
</Text>
|
||||
@@ -885,6 +902,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
onChangePassword={
|
||||
onChangeTeamMemberPassword
|
||||
}
|
||||
onEdit={onEditTeamMember}
|
||||
isSelected={selectedUserIds.includes(
|
||||
member.id
|
||||
)}
|
||||
@@ -966,6 +984,7 @@ const ManageEducationAccountDialog = ({ onClose }: Props) => {
|
||||
onChangePassword={
|
||||
onChangeTeamMemberPassword
|
||||
}
|
||||
onEdit={onEditTeamMember}
|
||||
isSelected={selectedUserIds.includes(
|
||||
member.id
|
||||
)}
|
||||
|
@@ -117,9 +117,19 @@ const TeamMemberRow = ({
|
||||
/>
|
||||
) : (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
{member.username && (
|
||||
{(member.username || member.fullName) && (
|
||||
<Text allowSelection noMargin>
|
||||
{member.username}
|
||||
{member.fullName ? (
|
||||
member.username ? (
|
||||
<Trans>
|
||||
{member.fullName} ({member.username})
|
||||
</Trans>
|
||||
) : (
|
||||
member.fullName
|
||||
)
|
||||
) : (
|
||||
member.username
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
<Text allowSelection noMargin color="secondary">
|
||||
|
@@ -545,6 +545,20 @@ const useCourses = () => {
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (language) {
|
||||
console.info(
|
||||
`Resetting course chapters cache as language changed to ${language}.`
|
||||
);
|
||||
setChaptersByCourseIdByUserId(() => ({
|
||||
'': noCourseChapters,
|
||||
}));
|
||||
}
|
||||
},
|
||||
[language]
|
||||
);
|
||||
|
||||
// This callback will change (triggering re-renders)
|
||||
// anytime the chapters are fetched for a course for a user.
|
||||
const getCourseChapters = React.useCallback(
|
||||
|
@@ -5,6 +5,7 @@ import { type I18n as I18nType } from '@lingui/core';
|
||||
import {
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from '../BaseEditor';
|
||||
import {
|
||||
type FileMetadataAndStorageProviderName,
|
||||
@@ -178,6 +179,9 @@ export type HomePageEditorInterface = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
scene: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
|};
|
||||
|
||||
export const HomePage = React.memo<Props>(
|
||||
@@ -486,6 +490,13 @@ export const HomePage = React.memo<Props>(
|
||||
[]
|
||||
);
|
||||
|
||||
const onInstancesModifiedOutsideEditor = React.useCallback(
|
||||
(changes: InstancesOutsideEditorChanges) => {
|
||||
// No thing to be done.
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getProject,
|
||||
updateToolbar,
|
||||
@@ -494,6 +505,7 @@ export const HomePage = React.memo<Props>(
|
||||
onSceneObjectEdited,
|
||||
onSceneObjectsDeleted,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
}));
|
||||
|
||||
// As the homepage is never unmounted, we need to ensure the games platform
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import ResourcesEditor from '../../ResourcesEditor';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
@@ -50,6 +51,10 @@ export class ResourcesEditorContainer extends React.Component<RenderEditorContai
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RenderEditorContainerProps) {
|
||||
if (
|
||||
this.editor &&
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
type RenderEditorContainerProps,
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './BaseEditor';
|
||||
import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope';
|
||||
import { type ObjectWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
@@ -98,6 +99,16 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP
|
||||
// No thing to be done.
|
||||
}
|
||||
|
||||
onInstancesModifiedOutsideEditor(changes: InstancesOutsideEditorChanges) {
|
||||
if (changes.scene !== this.getLayout()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
this.editor.onInstancesModifiedOutsideEditor();
|
||||
}
|
||||
}
|
||||
|
||||
getLayout(): ?gdLayout {
|
||||
const { project, projectItemName } = this.props;
|
||||
if (
|
||||
|
@@ -172,16 +172,9 @@ export const openEditorTab = (
|
||||
editor => editor.key === key
|
||||
);
|
||||
if (existingEditorId !== -1) {
|
||||
return {
|
||||
...state,
|
||||
panes: {
|
||||
...state.panes,
|
||||
[paneIdentifier]: {
|
||||
...pane,
|
||||
currentTab: dontFocusTab ? pane.currentTab : existingEditorId,
|
||||
},
|
||||
},
|
||||
};
|
||||
return dontFocusTab
|
||||
? { ...state }
|
||||
: changeCurrentTab(state, paneIdentifier, existingEditorId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +196,7 @@ export const openEditorTab = (
|
||||
throw new Error(`Pane with identifier "${paneIdentifier}" is not valid.`);
|
||||
}
|
||||
|
||||
return {
|
||||
let newState = {
|
||||
...state,
|
||||
panes: {
|
||||
...state.panes,
|
||||
@@ -214,10 +207,14 @@ export const openEditorTab = (
|
||||
key === 'start page'
|
||||
? [editorTab, ...pane.editors]
|
||||
: [...pane.editors, editorTab],
|
||||
currentTab: dontFocusTab ? pane.currentTab : pane.editors.length,
|
||||
currentTab: pane.currentTab,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (!dontFocusTab) {
|
||||
newState = changeCurrentTab(newState, paneIdentifier, pane.editors.length);
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const changeCurrentTab = (
|
||||
@@ -591,6 +588,7 @@ export const moveTabToPosition = (
|
||||
else if (tabIsMovedFromLeftToRightOfCurrentTab) paneNewCurrentTab -= 1;
|
||||
else if (tabIsMovedFromRightToLeftOfCurrentTab) paneNewCurrentTab += 1;
|
||||
|
||||
// The index changes but the tab is the same so there is no need to call changeCurrentTab.
|
||||
return {
|
||||
...editorTabsState,
|
||||
panes: {
|
||||
|
@@ -24,7 +24,10 @@ import {
|
||||
saveUiSettings,
|
||||
} from './EditorTabs/EditorTabsHandler';
|
||||
import { type PreviewState } from './PreviewState';
|
||||
import { type SceneEventsOutsideEditorChanges } from './EditorContainers/BaseEditor';
|
||||
import {
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './EditorContainers/BaseEditor';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
|
||||
import { type GamesList } from '../GameDashboard/UseGamesList';
|
||||
@@ -206,6 +209,9 @@ export type EditorTabsPaneCommonProps = {|
|
||||
onSceneEventsModifiedOutsideEditor: (
|
||||
changes: SceneEventsOutsideEditorChanges
|
||||
) => void,
|
||||
onInstancesModifiedOutsideEditor: (
|
||||
changes: InstancesOutsideEditorChanges
|
||||
) => void,
|
||||
onExtensionInstalled: (extensionNames: Array<string>) => void,
|
||||
gamesList: GamesList,
|
||||
|
||||
@@ -294,6 +300,7 @@ const EditorTabsPane = React.forwardRef<Props, {||}>((props, ref) => {
|
||||
onSceneObjectEdited,
|
||||
onSceneObjectsDeleted,
|
||||
onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled,
|
||||
gamesList,
|
||||
setEditorTabs,
|
||||
@@ -681,6 +688,7 @@ const EditorTabsPane = React.forwardRef<Props, {||}>((props, ref) => {
|
||||
onSceneObjectEdited: onSceneObjectEdited,
|
||||
onSceneObjectsDeleted: onSceneObjectsDeleted,
|
||||
onSceneEventsModifiedOutsideEditor: onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor: onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled: onExtensionInstalled,
|
||||
gamesList,
|
||||
gamesPlatformFrameTools,
|
||||
|
@@ -58,6 +58,7 @@ import { renderResourcesEditorContainer } from './EditorContainers/ResourcesEdit
|
||||
import {
|
||||
type RenderEditorContainerPropsWithRef,
|
||||
type SceneEventsOutsideEditorChanges,
|
||||
type InstancesOutsideEditorChanges,
|
||||
} from './EditorContainers/BaseEditor';
|
||||
import { type Exporter } from '../ExportAndShare/ShareDialog';
|
||||
import ResourcesLoader from '../ResourcesLoader/index';
|
||||
@@ -2431,6 +2432,18 @@ const MainFrame = (props: Props) => {
|
||||
[state.editorTabs]
|
||||
);
|
||||
|
||||
const onInstancesModifiedOutsideEditor = React.useCallback(
|
||||
(changes: InstancesOutsideEditorChanges) => {
|
||||
for (const editor of getAllEditorTabs(state.editorTabs)) {
|
||||
const { editorRef } = editor;
|
||||
if (editorRef) {
|
||||
editorRef.onInstancesModifiedOutsideEditor(changes);
|
||||
}
|
||||
}
|
||||
},
|
||||
[state.editorTabs]
|
||||
);
|
||||
|
||||
const _onProjectItemModified = () => {
|
||||
triggerUnsavedChanges();
|
||||
forceUpdate();
|
||||
@@ -3872,6 +3885,7 @@ const MainFrame = (props: Props) => {
|
||||
onSceneObjectEdited: onSceneObjectEdited,
|
||||
onSceneObjectsDeleted: onSceneObjectsDeleted,
|
||||
onSceneEventsModifiedOutsideEditor: onSceneEventsModifiedOutsideEditor,
|
||||
onInstancesModifiedOutsideEditor: onInstancesModifiedOutsideEditor,
|
||||
onExtensionInstalled: onExtensionInstalled,
|
||||
gamesList: gamesList,
|
||||
};
|
||||
|
@@ -218,24 +218,6 @@ export default class TextEditor extends React.Component<EditorProps, void> {
|
||||
floatingLabelText={<Trans>Choose a font</Trans>}
|
||||
hintText={<Trans>Choose a font</Trans>}
|
||||
/>
|
||||
<Line noMargin>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Line height (0 = default)</Trans>}
|
||||
floatingLabelFixed
|
||||
id="text-object-line-height"
|
||||
type="number"
|
||||
commitOnBlur
|
||||
fullWidth
|
||||
value={textObjectConfiguration.getLineHeight()}
|
||||
onChange={value => {
|
||||
textObjectConfiguration.setLineHeight(
|
||||
parseInt(value, 10) || 0
|
||||
);
|
||||
this.forceUpdate();
|
||||
this.props.onSizeUpdated();
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Initial text to display</Trans>}
|
||||
@@ -389,6 +371,21 @@ export default class TextEditor extends React.Component<EditorProps, void> {
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
<Text size="block-title" noMargin>
|
||||
<Trans>Multiline</Trans>
|
||||
</Text>
|
||||
<Line noMargin>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Line height</Trans>}
|
||||
type="number"
|
||||
fullWidth
|
||||
value={textObjectConfiguration.getLineHeight()}
|
||||
onChange={value => {
|
||||
textObjectConfiguration.setLineHeight(parseFloat(value) || 0);
|
||||
this.forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
);
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ export default class RenderedTextInstance extends RenderedInstance {
|
||||
_color: string = '0;0;0';
|
||||
_textAlignment: string = 'left';
|
||||
_verticalTextAlignment: string = 'top';
|
||||
_lineHeight: number = 1;
|
||||
|
||||
_isOutlineEnabled = false;
|
||||
_outlineColor = '255;255;255';
|
||||
@@ -99,6 +98,7 @@ export default class RenderedTextInstance extends RenderedInstance {
|
||||
textObjectConfiguration.isItalic() !== this._isItalic ||
|
||||
textObjectConfiguration.isBold() !== this._isBold ||
|
||||
textObjectConfiguration.getCharacterSize() !== this._characterSize ||
|
||||
textObjectConfiguration.getLineHeight() !== this._lineHeight ||
|
||||
textObjectConfiguration.getTextAlignment() !== this._textAlignment ||
|
||||
textObjectConfiguration.getVerticalTextAlignment() !==
|
||||
this._verticalTextAlignment ||
|
||||
@@ -114,13 +114,13 @@ export default class RenderedTextInstance extends RenderedInstance {
|
||||
textObjectConfiguration.getShadowOpacity() !== this._shadowOpacity ||
|
||||
textObjectConfiguration.getShadowBlurRadius() !==
|
||||
this._shadowBlurRadius ||
|
||||
textObjectConfiguration.getLineHeight() !== this._lineHeight ||
|
||||
this._instance.hasCustomSize() !== this._wrapping ||
|
||||
(this.getCustomWidth() !== this._wrappingWidth && this._wrapping)
|
||||
) {
|
||||
this._isItalic = textObjectConfiguration.isItalic();
|
||||
this._isBold = textObjectConfiguration.isBold();
|
||||
this._characterSize = textObjectConfiguration.getCharacterSize();
|
||||
this._lineHeight = textObjectConfiguration.getLineHeight();
|
||||
this._textAlignment = textObjectConfiguration.getTextAlignment();
|
||||
this._verticalTextAlignment = textObjectConfiguration.getVerticalTextAlignment();
|
||||
this._color = textObjectConfiguration.getColor();
|
||||
@@ -138,7 +138,6 @@ export default class RenderedTextInstance extends RenderedInstance {
|
||||
|
||||
this._wrapping = this._instance.hasCustomSize();
|
||||
this._wrappingWidth = this.getCustomWidth();
|
||||
this._lineHeight = textObjectConfiguration.getLineHeight();
|
||||
this._styleFontDirty = true;
|
||||
}
|
||||
|
||||
@@ -171,12 +170,12 @@ export default class RenderedTextInstance extends RenderedInstance {
|
||||
style.fontSize = Math.max(1, this._characterSize);
|
||||
style.fontStyle = this._isItalic ? 'italic' : 'normal';
|
||||
style.fontWeight = this._isBold ? 'bold' : 'normal';
|
||||
style.lineHeight = this._lineHeight !== 0 ? this._lineHeight : undefined;
|
||||
style.fill = rgbStringToHexNumber(this._color);
|
||||
style.wordWrap = this._wrapping;
|
||||
style.wordWrapWidth = this._wrappingWidth <= 1 ? 1 : this._wrappingWidth;
|
||||
style.breakWords = true;
|
||||
style.align = this._textAlignment;
|
||||
style.lineHeight = this._lineHeight > 0 ? this._lineHeight : undefined;
|
||||
|
||||
style.stroke = rgbStringToHexNumber(this._outlineColor);
|
||||
style.strokeThickness = this._isOutlineEnabled
|
||||
|
@@ -4,7 +4,6 @@ import {
|
||||
type Profile,
|
||||
type LoginForm,
|
||||
type RegisterForm,
|
||||
type PatchUserPayload,
|
||||
type ForgotPasswordForm,
|
||||
type AuthError,
|
||||
type IdentityProvider,
|
||||
@@ -13,7 +12,10 @@ import { type PreferencesValues } from '../MainFrame/Preferences/PreferencesCont
|
||||
import { type CloudProjectWithUserAccessInfo } from '../Utils/GDevelopServices/Project';
|
||||
import { User as FirebaseUser } from 'firebase/auth';
|
||||
import { type Badge, type Achievement } from '../Utils/GDevelopServices/Badge';
|
||||
import { type Recommendation } from '../Utils/GDevelopServices/User';
|
||||
import {
|
||||
type Recommendation,
|
||||
type EditUserChanges,
|
||||
} from '../Utils/GDevelopServices/User';
|
||||
import { type Notification } from '../Utils/GDevelopServices/Notification';
|
||||
import {
|
||||
type Limits,
|
||||
@@ -62,7 +64,7 @@ export type AuthenticatedUser = {|
|
||||
preferences: PreferencesValues
|
||||
) => Promise<void>,
|
||||
onEditProfile: (
|
||||
payload: PatchUserPayload,
|
||||
changes: EditUserChanges,
|
||||
preferences: PreferencesValues
|
||||
) => Promise<void>,
|
||||
onResetPassword: ForgotPasswordForm => Promise<void>,
|
||||
|
@@ -8,6 +8,8 @@ import {
|
||||
getUserEarningsBalance,
|
||||
} from '../Utils/GDevelopServices/Usage';
|
||||
import {
|
||||
editUser,
|
||||
type EditUserChanges,
|
||||
getUserBadges,
|
||||
listDefaultRecommendations,
|
||||
listRecommendations,
|
||||
@@ -17,7 +19,6 @@ import { getAchievements } from '../Utils/GDevelopServices/Badge';
|
||||
import Authentication, {
|
||||
type LoginForm,
|
||||
type RegisterForm,
|
||||
type PatchUserPayload,
|
||||
type ChangeEmailForm,
|
||||
type AuthError,
|
||||
type ForgotPasswordForm,
|
||||
@@ -622,10 +623,11 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
if (!userProfile.isCreator) {
|
||||
// If the user is not a creator, then update the profile to say they now are.
|
||||
try {
|
||||
await authentication.editUserProfile(
|
||||
authentication.getAuthorizationHeader,
|
||||
{ isCreator: true }
|
||||
);
|
||||
await editUser(authentication.getAuthorizationHeader, {
|
||||
editedUserId: userProfile.id,
|
||||
userId: userProfile.id,
|
||||
changes: { isCreator: true },
|
||||
});
|
||||
} catch (error) {
|
||||
// Catch the error so that the user profile is still fetched.
|
||||
console.error('Error while updating the user profile:', error);
|
||||
@@ -1206,11 +1208,13 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
};
|
||||
|
||||
_doEdit = async (
|
||||
payload: PatchUserPayload,
|
||||
payload: EditUserChanges,
|
||||
preferences: PreferencesValues
|
||||
) => {
|
||||
const { authentication } = this.props;
|
||||
if (!authentication) return;
|
||||
const { profile } = this.state.authenticatedUser;
|
||||
if (!profile) return;
|
||||
|
||||
this.setState({
|
||||
editInProgress: true,
|
||||
@@ -1218,9 +1222,10 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
});
|
||||
this._automaticallyUpdateUserProfile = false;
|
||||
try {
|
||||
await authentication.editUserProfile(
|
||||
authentication.getAuthorizationHeader,
|
||||
{
|
||||
await editUser(authentication.getAuthorizationHeader, {
|
||||
editedUserId: profile.id,
|
||||
userId: profile.id,
|
||||
changes: {
|
||||
username: payload.username,
|
||||
description: payload.description,
|
||||
getGameStatsEmail: payload.getGameStatsEmail,
|
||||
@@ -1231,8 +1236,8 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
githubUsername: payload.githubUsername,
|
||||
communityLinks: payload.communityLinks,
|
||||
survey: payload.survey,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
await this._fetchUserProfileWithoutThrowingErrors();
|
||||
} catch (apiCallError) {
|
||||
this.setState({ apiCallError });
|
||||
@@ -1625,9 +1630,9 @@ export default class AuthenticatedUserProvider extends React.Component<
|
||||
badges={this.state.authenticatedUser.badges}
|
||||
subscription={this.state.authenticatedUser.subscription}
|
||||
onClose={() => this.openEditProfileDialog(false)}
|
||||
onEdit={async form => {
|
||||
onEdit={async changes => {
|
||||
try {
|
||||
await this._doEdit(form, this.props.preferencesValues);
|
||||
await this._doEdit(changes, this.props.preferencesValues);
|
||||
this.openEditProfileDialog(false);
|
||||
} catch (error) {
|
||||
// Ignore errors, we will let the user retry in their profile.
|
||||
|
@@ -207,7 +207,6 @@ const CreateAccountDialog = ({
|
||||
onChangeOptInNewsletterEmail={setGetNewsletterEmail}
|
||||
createAccountInProgress={createAccountInProgress}
|
||||
error={error}
|
||||
usernameAvailability={usernameAvailability}
|
||||
onChangeUsernameAvailability={setUsernameAvailability}
|
||||
isValidatingUsername={isValidatingUsername}
|
||||
onChangeIsValidatingUsername={setIsValidatingUsername}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user